1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
21 VisualTestContext, WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::rust_lang;
36use lsp::CompletionParams;
37use multi_buffer::{IndentGuide, PathKey};
38use parking_lot::Mutex;
39use pretty_assertions::{assert_eq, assert_ne};
40use project::{
41 FakeFs,
42 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
43 project_settings::LspSettings,
44};
45use serde_json::{self, json};
46use settings::{
47 AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
48 IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
49};
50use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
51use std::{
52 iter,
53 sync::atomic::{self, AtomicUsize},
54};
55use test::build_editor_with_project;
56use text::ToPoint as _;
57use unindent::Unindent;
58use util::{
59 assert_set_eq, path,
60 rel_path::rel_path,
61 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
62 uri,
63};
64use workspace::{
65 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
66 OpenOptions, ViewId,
67 invalid_item_view::InvalidItemView,
68 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
69 register_project_item,
70};
71
72fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
73 editor
74 .selections
75 .display_ranges(&editor.display_snapshot(cx))
76}
77
78#[gpui::test]
79fn test_edit_events(cx: &mut TestAppContext) {
80 init_test(cx, |_| {});
81
82 let buffer = cx.new(|cx| {
83 let mut buffer = language::Buffer::local("123456", cx);
84 buffer.set_group_interval(Duration::from_secs(1));
85 buffer
86 });
87
88 let events = Rc::new(RefCell::new(Vec::new()));
89 let editor1 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 let entity = cx.entity();
93 cx.subscribe_in(
94 &entity,
95 window,
96 move |_, _, event: &EditorEvent, _, _| match event {
97 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
98 EditorEvent::BufferEdited => {
99 events.borrow_mut().push(("editor1", "buffer edited"))
100 }
101 _ => {}
102 },
103 )
104 .detach();
105 Editor::for_buffer(buffer.clone(), None, window, cx)
106 }
107 });
108
109 let editor2 = cx.add_window({
110 let events = events.clone();
111 |window, cx| {
112 cx.subscribe_in(
113 &cx.entity(),
114 window,
115 move |_, _, event: &EditorEvent, _, _| match event {
116 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
117 EditorEvent::BufferEdited => {
118 events.borrow_mut().push(("editor2", "buffer edited"))
119 }
120 _ => {}
121 },
122 )
123 .detach();
124 Editor::for_buffer(buffer.clone(), None, window, cx)
125 }
126 });
127
128 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
129
130 // Mutating editor 1 will emit an `Edited` event only for that editor.
131 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
132 assert_eq!(
133 mem::take(&mut *events.borrow_mut()),
134 [
135 ("editor1", "edited"),
136 ("editor1", "buffer edited"),
137 ("editor2", "buffer edited"),
138 ]
139 );
140
141 // Mutating editor 2 will emit an `Edited` event only for that editor.
142 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
143 assert_eq!(
144 mem::take(&mut *events.borrow_mut()),
145 [
146 ("editor2", "edited"),
147 ("editor1", "buffer edited"),
148 ("editor2", "buffer edited"),
149 ]
150 );
151
152 // Undoing on editor 1 will emit an `Edited` event only for that editor.
153 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
154 assert_eq!(
155 mem::take(&mut *events.borrow_mut()),
156 [
157 ("editor1", "edited"),
158 ("editor1", "buffer edited"),
159 ("editor2", "buffer edited"),
160 ]
161 );
162
163 // Redoing on editor 1 will emit an `Edited` event only for that editor.
164 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
165 assert_eq!(
166 mem::take(&mut *events.borrow_mut()),
167 [
168 ("editor1", "edited"),
169 ("editor1", "buffer edited"),
170 ("editor2", "buffer edited"),
171 ]
172 );
173
174 // Undoing on editor 2 will emit an `Edited` event only for that editor.
175 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
176 assert_eq!(
177 mem::take(&mut *events.borrow_mut()),
178 [
179 ("editor2", "edited"),
180 ("editor1", "buffer edited"),
181 ("editor2", "buffer edited"),
182 ]
183 );
184
185 // Redoing on editor 2 will emit an `Edited` event only for that editor.
186 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
187 assert_eq!(
188 mem::take(&mut *events.borrow_mut()),
189 [
190 ("editor2", "edited"),
191 ("editor1", "buffer edited"),
192 ("editor2", "buffer edited"),
193 ]
194 );
195
196 // No event is emitted when the mutation is a no-op.
197 _ = editor2.update(cx, |editor, window, cx| {
198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
199 s.select_ranges([0..0])
200 });
201
202 editor.backspace(&Backspace, window, cx);
203 });
204 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
205}
206
207#[gpui::test]
208fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
209 init_test(cx, |_| {});
210
211 let mut now = Instant::now();
212 let group_interval = Duration::from_millis(1);
213 let buffer = cx.new(|cx| {
214 let mut buf = language::Buffer::local("123456", cx);
215 buf.set_group_interval(group_interval);
216 buf
217 });
218 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
219 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
220
221 _ = editor.update(cx, |editor, window, cx| {
222 editor.start_transaction_at(now, window, cx);
223 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
224 s.select_ranges([2..4])
225 });
226
227 editor.insert("cd", window, cx);
228 editor.end_transaction_at(now, cx);
229 assert_eq!(editor.text(cx), "12cd56");
230 assert_eq!(
231 editor.selections.ranges(&editor.display_snapshot(cx)),
232 vec![4..4]
233 );
234
235 editor.start_transaction_at(now, window, cx);
236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
237 s.select_ranges([4..5])
238 });
239 editor.insert("e", window, cx);
240 editor.end_transaction_at(now, cx);
241 assert_eq!(editor.text(cx), "12cde6");
242 assert_eq!(
243 editor.selections.ranges(&editor.display_snapshot(cx)),
244 vec![5..5]
245 );
246
247 now += group_interval + Duration::from_millis(1);
248 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
249 s.select_ranges([2..2])
250 });
251
252 // Simulate an edit in another editor
253 buffer.update(cx, |buffer, cx| {
254 buffer.start_transaction_at(now, cx);
255 buffer.edit([(0..1, "a")], None, cx);
256 buffer.edit([(1..1, "b")], None, cx);
257 buffer.end_transaction_at(now, cx);
258 });
259
260 assert_eq!(editor.text(cx), "ab2cde6");
261 assert_eq!(
262 editor.selections.ranges(&editor.display_snapshot(cx)),
263 vec![3..3]
264 );
265
266 // Last transaction happened past the group interval in a different editor.
267 // Undo it individually and don't restore selections.
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 assert_eq!(
271 editor.selections.ranges(&editor.display_snapshot(cx)),
272 vec![2..2]
273 );
274
275 // First two transactions happened within the group interval in this editor.
276 // Undo them together and restore selections.
277 editor.undo(&Undo, window, cx);
278 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
279 assert_eq!(editor.text(cx), "123456");
280 assert_eq!(
281 editor.selections.ranges(&editor.display_snapshot(cx)),
282 vec![0..0]
283 );
284
285 // Redo the first two transactions together.
286 editor.redo(&Redo, window, cx);
287 assert_eq!(editor.text(cx), "12cde6");
288 assert_eq!(
289 editor.selections.ranges(&editor.display_snapshot(cx)),
290 vec![5..5]
291 );
292
293 // Redo the last transaction on its own.
294 editor.redo(&Redo, window, cx);
295 assert_eq!(editor.text(cx), "ab2cde6");
296 assert_eq!(
297 editor.selections.ranges(&editor.display_snapshot(cx)),
298 vec![6..6]
299 );
300
301 // Test empty transactions.
302 editor.start_transaction_at(now, window, cx);
303 editor.end_transaction_at(now, cx);
304 editor.undo(&Undo, window, cx);
305 assert_eq!(editor.text(cx), "12cde6");
306 });
307}
308
309#[gpui::test]
310fn test_ime_composition(cx: &mut TestAppContext) {
311 init_test(cx, |_| {});
312
313 let buffer = cx.new(|cx| {
314 let mut buffer = language::Buffer::local("abcde", cx);
315 // Ensure automatic grouping doesn't occur.
316 buffer.set_group_interval(Duration::ZERO);
317 buffer
318 });
319
320 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
321 cx.add_window(|window, cx| {
322 let mut editor = build_editor(buffer.clone(), window, cx);
323
324 // Start a new IME composition.
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 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
328 assert_eq!(editor.text(cx), "äbcde");
329 assert_eq!(
330 editor.marked_text_ranges(cx),
331 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
332 );
333
334 // Finalize IME composition.
335 editor.replace_text_in_range(None, "ā", window, cx);
336 assert_eq!(editor.text(cx), "ābcde");
337 assert_eq!(editor.marked_text_ranges(cx), None);
338
339 // IME composition edits are grouped and are undone/redone at once.
340 editor.undo(&Default::default(), window, cx);
341 assert_eq!(editor.text(cx), "abcde");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343 editor.redo(&Default::default(), window, cx);
344 assert_eq!(editor.text(cx), "ābcde");
345 assert_eq!(editor.marked_text_ranges(cx), None);
346
347 // Start a new IME composition.
348 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
352 );
353
354 // Undoing during an IME composition cancels it.
355 editor.undo(&Default::default(), window, cx);
356 assert_eq!(editor.text(cx), "ābcde");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
360 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
361 assert_eq!(editor.text(cx), "ābcdè");
362 assert_eq!(
363 editor.marked_text_ranges(cx),
364 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
365 );
366
367 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
368 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
369 assert_eq!(editor.text(cx), "ābcdę");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 // Start a new IME composition with multiple cursors.
373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
374 s.select_ranges([
375 OffsetUtf16(1)..OffsetUtf16(1),
376 OffsetUtf16(3)..OffsetUtf16(3),
377 OffsetUtf16(5)..OffsetUtf16(5),
378 ])
379 });
380 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
381 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
382 assert_eq!(
383 editor.marked_text_ranges(cx),
384 Some(vec![
385 OffsetUtf16(0)..OffsetUtf16(3),
386 OffsetUtf16(4)..OffsetUtf16(7),
387 OffsetUtf16(8)..OffsetUtf16(11)
388 ])
389 );
390
391 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
392 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
393 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
394 assert_eq!(
395 editor.marked_text_ranges(cx),
396 Some(vec![
397 OffsetUtf16(1)..OffsetUtf16(2),
398 OffsetUtf16(5)..OffsetUtf16(6),
399 OffsetUtf16(9)..OffsetUtf16(10)
400 ])
401 );
402
403 // Finalize IME composition with multiple cursors.
404 editor.replace_text_in_range(Some(9..10), "2", window, cx);
405 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
406 assert_eq!(editor.marked_text_ranges(cx), None);
407
408 editor
409 });
410}
411
412#[gpui::test]
413fn test_selection_with_mouse(cx: &mut TestAppContext) {
414 init_test(cx, |_| {});
415
416 let editor = cx.add_window(|window, cx| {
417 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
418 build_editor(buffer, window, cx)
419 });
420
421 _ = editor.update(cx, |editor, window, cx| {
422 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
423 });
424 assert_eq!(
425 editor
426 .update(cx, |editor, _, cx| display_ranges(editor, cx))
427 .unwrap(),
428 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
429 );
430
431 _ = editor.update(cx, |editor, window, cx| {
432 editor.update_selection(
433 DisplayPoint::new(DisplayRow(3), 3),
434 0,
435 gpui::Point::<f32>::default(),
436 window,
437 cx,
438 );
439 });
440
441 assert_eq!(
442 editor
443 .update(cx, |editor, _, cx| display_ranges(editor, cx))
444 .unwrap(),
445 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(1), 1),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| display_ranges(editor, cx))
461 .unwrap(),
462 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
463 );
464
465 _ = editor.update(cx, |editor, window, cx| {
466 editor.end_selection(window, cx);
467 editor.update_selection(
468 DisplayPoint::new(DisplayRow(3), 3),
469 0,
470 gpui::Point::<f32>::default(),
471 window,
472 cx,
473 );
474 });
475
476 assert_eq!(
477 editor
478 .update(cx, |editor, _, cx| display_ranges(editor, cx))
479 .unwrap(),
480 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
481 );
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
485 editor.update_selection(
486 DisplayPoint::new(DisplayRow(0), 0),
487 0,
488 gpui::Point::<f32>::default(),
489 window,
490 cx,
491 );
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| display_ranges(editor, cx))
497 .unwrap(),
498 [
499 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
500 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
501 ]
502 );
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.end_selection(window, cx);
506 });
507
508 assert_eq!(
509 editor
510 .update(cx, |editor, _, cx| display_ranges(editor, cx))
511 .unwrap(),
512 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
513 );
514}
515
516#[gpui::test]
517fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
518 init_test(cx, |_| {});
519
520 let editor = cx.add_window(|window, cx| {
521 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
522 build_editor(buffer, window, cx)
523 });
524
525 _ = editor.update(cx, |editor, window, cx| {
526 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.end_selection(window, cx);
531 });
532
533 _ = editor.update(cx, |editor, window, cx| {
534 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
535 });
536
537 _ = editor.update(cx, |editor, window, cx| {
538 editor.end_selection(window, cx);
539 });
540
541 assert_eq!(
542 editor
543 .update(cx, |editor, _, cx| display_ranges(editor, cx))
544 .unwrap(),
545 [
546 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
547 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
548 ]
549 );
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.end_selection(window, cx);
557 });
558
559 assert_eq!(
560 editor
561 .update(cx, |editor, _, cx| display_ranges(editor, cx))
562 .unwrap(),
563 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
564 );
565}
566
567#[gpui::test]
568fn test_canceling_pending_selection(cx: &mut TestAppContext) {
569 init_test(cx, |_| {});
570
571 let editor = cx.add_window(|window, cx| {
572 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
573 build_editor(buffer, window, cx)
574 });
575
576 _ = editor.update(cx, |editor, window, cx| {
577 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
578 assert_eq!(
579 display_ranges(editor, cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
581 );
582 });
583
584 _ = editor.update(cx, |editor, window, cx| {
585 editor.update_selection(
586 DisplayPoint::new(DisplayRow(3), 3),
587 0,
588 gpui::Point::<f32>::default(),
589 window,
590 cx,
591 );
592 assert_eq!(
593 display_ranges(editor, cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
595 );
596 });
597
598 _ = editor.update(cx, |editor, window, cx| {
599 editor.cancel(&Cancel, window, cx);
600 editor.update_selection(
601 DisplayPoint::new(DisplayRow(1), 1),
602 0,
603 gpui::Point::<f32>::default(),
604 window,
605 cx,
606 );
607 assert_eq!(
608 display_ranges(editor, cx),
609 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let editor = cx.add_window(|window, cx| {
619 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
620 build_editor(buffer, window, cx)
621 });
622
623 _ = editor.update(cx, |editor, window, cx| {
624 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
625 assert_eq!(
626 display_ranges(editor, cx),
627 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
628 );
629
630 editor.move_down(&Default::default(), window, cx);
631 assert_eq!(
632 display_ranges(editor, cx),
633 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
634 );
635
636 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
637 assert_eq!(
638 display_ranges(editor, cx),
639 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
640 );
641
642 editor.move_up(&Default::default(), window, cx);
643 assert_eq!(
644 display_ranges(editor, cx),
645 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
646 );
647 });
648}
649
650#[gpui::test]
651fn test_extending_selection(cx: &mut TestAppContext) {
652 init_test(cx, |_| {});
653
654 let editor = cx.add_window(|window, cx| {
655 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
656 build_editor(buffer, window, cx)
657 });
658
659 _ = editor.update(cx, |editor, window, cx| {
660 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
661 editor.end_selection(window, cx);
662 assert_eq!(
663 display_ranges(editor, cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
665 );
666
667 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
668 editor.end_selection(window, cx);
669 assert_eq!(
670 display_ranges(editor, cx),
671 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
672 );
673
674 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
675 editor.end_selection(window, cx);
676 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
677 assert_eq!(
678 display_ranges(editor, cx),
679 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
680 );
681
682 editor.update_selection(
683 DisplayPoint::new(DisplayRow(0), 1),
684 0,
685 gpui::Point::<f32>::default(),
686 window,
687 cx,
688 );
689 editor.end_selection(window, cx);
690 assert_eq!(
691 display_ranges(editor, cx),
692 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
693 );
694
695 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
696 editor.end_selection(window, cx);
697 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
698 editor.end_selection(window, cx);
699 assert_eq!(
700 display_ranges(editor, cx),
701 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
702 );
703
704 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
705 assert_eq!(
706 display_ranges(editor, cx),
707 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
708 );
709
710 editor.update_selection(
711 DisplayPoint::new(DisplayRow(0), 6),
712 0,
713 gpui::Point::<f32>::default(),
714 window,
715 cx,
716 );
717 assert_eq!(
718 display_ranges(editor, cx),
719 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
720 );
721
722 editor.update_selection(
723 DisplayPoint::new(DisplayRow(0), 1),
724 0,
725 gpui::Point::<f32>::default(),
726 window,
727 cx,
728 );
729 editor.end_selection(window, cx);
730 assert_eq!(
731 display_ranges(editor, cx),
732 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
733 );
734 });
735}
736
737#[gpui::test]
738fn test_clone(cx: &mut TestAppContext) {
739 init_test(cx, |_| {});
740
741 let (text, selection_ranges) = marked_text_ranges(
742 indoc! {"
743 one
744 two
745 threeˇ
746 four
747 fiveˇ
748 "},
749 true,
750 );
751
752 let editor = cx.add_window(|window, cx| {
753 let buffer = MultiBuffer::build_simple(&text, cx);
754 build_editor(buffer, window, cx)
755 });
756
757 _ = editor.update(cx, |editor, window, cx| {
758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
759 s.select_ranges(selection_ranges.clone())
760 });
761 editor.fold_creases(
762 vec![
763 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
764 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
765 ],
766 true,
767 window,
768 cx,
769 );
770 });
771
772 let cloned_editor = editor
773 .update(cx, |editor, _, cx| {
774 cx.open_window(Default::default(), |window, cx| {
775 cx.new(|cx| editor.clone(window, cx))
776 })
777 })
778 .unwrap()
779 .unwrap();
780
781 let snapshot = editor
782 .update(cx, |e, window, cx| e.snapshot(window, cx))
783 .unwrap();
784 let cloned_snapshot = cloned_editor
785 .update(cx, |e, window, cx| e.snapshot(window, cx))
786 .unwrap();
787
788 assert_eq!(
789 cloned_editor
790 .update(cx, |e, _, cx| e.display_text(cx))
791 .unwrap(),
792 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
793 );
794 assert_eq!(
795 cloned_snapshot
796 .folds_in_range(0..text.len())
797 .collect::<Vec<_>>(),
798 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
799 );
800 assert_set_eq!(
801 cloned_editor
802 .update(cx, |editor, _, cx| editor
803 .selections
804 .ranges::<Point>(&editor.display_snapshot(cx)))
805 .unwrap(),
806 editor
807 .update(cx, |editor, _, cx| editor
808 .selections
809 .ranges(&editor.display_snapshot(cx)))
810 .unwrap()
811 );
812 assert_set_eq!(
813 cloned_editor
814 .update(cx, |e, _window, cx| e
815 .selections
816 .display_ranges(&e.display_snapshot(cx)))
817 .unwrap(),
818 editor
819 .update(cx, |e, _, cx| e
820 .selections
821 .display_ranges(&e.display_snapshot(cx)))
822 .unwrap()
823 );
824}
825
826#[gpui::test]
827async fn test_navigation_history(cx: &mut TestAppContext) {
828 init_test(cx, |_| {});
829
830 use workspace::item::Item;
831
832 let fs = FakeFs::new(cx.executor());
833 let project = Project::test(fs, [], cx).await;
834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
835 let pane = workspace
836 .update(cx, |workspace, _, _| workspace.active_pane().clone())
837 .unwrap();
838
839 _ = workspace.update(cx, |_v, window, cx| {
840 cx.new(|cx| {
841 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
842 let mut editor = build_editor(buffer, window, cx);
843 let handle = cx.entity();
844 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
845
846 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
847 editor.nav_history.as_mut().unwrap().pop_backward(cx)
848 }
849
850 // Move the cursor a small distance.
851 // Nothing is added to the navigation history.
852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
853 s.select_display_ranges([
854 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
855 ])
856 });
857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
858 s.select_display_ranges([
859 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
860 ])
861 });
862 assert!(pop_history(&mut editor, cx).is_none());
863
864 // Move the cursor a large distance.
865 // The history can jump back to the previous position.
866 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
867 s.select_display_ranges([
868 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
869 ])
870 });
871 let nav_entry = pop_history(&mut editor, cx).unwrap();
872 editor.navigate(nav_entry.data.unwrap(), window, cx);
873 assert_eq!(nav_entry.item.id(), cx.entity_id());
874 assert_eq!(
875 editor
876 .selections
877 .display_ranges(&editor.display_snapshot(cx)),
878 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
879 );
880 assert!(pop_history(&mut editor, cx).is_none());
881
882 // Move the cursor a small distance via the mouse.
883 // Nothing is added to the navigation history.
884 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
885 editor.end_selection(window, cx);
886 assert_eq!(
887 editor
888 .selections
889 .display_ranges(&editor.display_snapshot(cx)),
890 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
891 );
892 assert!(pop_history(&mut editor, cx).is_none());
893
894 // Move the cursor a large distance via the mouse.
895 // The history can jump back to the previous position.
896 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
897 editor.end_selection(window, cx);
898 assert_eq!(
899 editor
900 .selections
901 .display_ranges(&editor.display_snapshot(cx)),
902 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
903 );
904 let nav_entry = pop_history(&mut editor, cx).unwrap();
905 editor.navigate(nav_entry.data.unwrap(), window, cx);
906 assert_eq!(nav_entry.item.id(), cx.entity_id());
907 assert_eq!(
908 editor
909 .selections
910 .display_ranges(&editor.display_snapshot(cx)),
911 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
912 );
913 assert!(pop_history(&mut editor, cx).is_none());
914
915 // Set scroll position to check later
916 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
917 let original_scroll_position = editor.scroll_manager.anchor();
918
919 // Jump to the end of the document and adjust scroll
920 editor.move_to_end(&MoveToEnd, window, cx);
921 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
922 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
923
924 let nav_entry = pop_history(&mut editor, cx).unwrap();
925 editor.navigate(nav_entry.data.unwrap(), window, cx);
926 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
927
928 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
929 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
930 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
931 let invalid_point = Point::new(9999, 0);
932 editor.navigate(
933 Box::new(NavigationData {
934 cursor_anchor: invalid_anchor,
935 cursor_position: invalid_point,
936 scroll_anchor: ScrollAnchor {
937 anchor: invalid_anchor,
938 offset: Default::default(),
939 },
940 scroll_top_row: invalid_point.row,
941 }),
942 window,
943 cx,
944 );
945 assert_eq!(
946 editor
947 .selections
948 .display_ranges(&editor.display_snapshot(cx)),
949 &[editor.max_point(cx)..editor.max_point(cx)]
950 );
951 assert_eq!(
952 editor.scroll_position(cx),
953 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
954 );
955
956 editor
957 })
958 });
959}
960
961#[gpui::test]
962fn test_cancel(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
967 build_editor(buffer, window, cx)
968 });
969
970 _ = editor.update(cx, |editor, window, cx| {
971 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
972 editor.update_selection(
973 DisplayPoint::new(DisplayRow(1), 1),
974 0,
975 gpui::Point::<f32>::default(),
976 window,
977 cx,
978 );
979 editor.end_selection(window, cx);
980
981 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
982 editor.update_selection(
983 DisplayPoint::new(DisplayRow(0), 3),
984 0,
985 gpui::Point::<f32>::default(),
986 window,
987 cx,
988 );
989 editor.end_selection(window, cx);
990 assert_eq!(
991 display_ranges(editor, cx),
992 [
993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
994 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
995 ]
996 );
997 });
998
999 _ = editor.update(cx, |editor, window, cx| {
1000 editor.cancel(&Cancel, window, cx);
1001 assert_eq!(
1002 display_ranges(editor, cx),
1003 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1004 );
1005 });
1006
1007 _ = editor.update(cx, |editor, window, cx| {
1008 editor.cancel(&Cancel, window, cx);
1009 assert_eq!(
1010 display_ranges(editor, cx),
1011 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1012 );
1013 });
1014}
1015
1016#[gpui::test]
1017fn test_fold_action(cx: &mut TestAppContext) {
1018 init_test(cx, |_| {});
1019
1020 let editor = cx.add_window(|window, cx| {
1021 let buffer = MultiBuffer::build_simple(
1022 &"
1023 impl Foo {
1024 // Hello!
1025
1026 fn a() {
1027 1
1028 }
1029
1030 fn b() {
1031 2
1032 }
1033
1034 fn c() {
1035 3
1036 }
1037 }
1038 "
1039 .unindent(),
1040 cx,
1041 );
1042 build_editor(buffer, window, cx)
1043 });
1044
1045 _ = editor.update(cx, |editor, window, cx| {
1046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1047 s.select_display_ranges([
1048 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1049 ]);
1050 });
1051 editor.fold(&Fold, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 "
1055 impl Foo {
1056 // Hello!
1057
1058 fn a() {
1059 1
1060 }
1061
1062 fn b() {⋯
1063 }
1064
1065 fn c() {⋯
1066 }
1067 }
1068 "
1069 .unindent(),
1070 );
1071
1072 editor.fold(&Fold, window, cx);
1073 assert_eq!(
1074 editor.display_text(cx),
1075 "
1076 impl Foo {⋯
1077 }
1078 "
1079 .unindent(),
1080 );
1081
1082 editor.unfold_lines(&UnfoldLines, window, cx);
1083 assert_eq!(
1084 editor.display_text(cx),
1085 "
1086 impl Foo {
1087 // Hello!
1088
1089 fn a() {
1090 1
1091 }
1092
1093 fn b() {⋯
1094 }
1095
1096 fn c() {⋯
1097 }
1098 }
1099 "
1100 .unindent(),
1101 );
1102
1103 editor.unfold_lines(&UnfoldLines, window, cx);
1104 assert_eq!(
1105 editor.display_text(cx),
1106 editor.buffer.read(cx).read(cx).text()
1107 );
1108 });
1109}
1110
1111#[gpui::test]
1112fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1113 init_test(cx, |_| {});
1114
1115 let editor = cx.add_window(|window, cx| {
1116 let buffer = MultiBuffer::build_simple(
1117 &"
1118 class Foo:
1119 # Hello!
1120
1121 def a():
1122 print(1)
1123
1124 def b():
1125 print(2)
1126
1127 def c():
1128 print(3)
1129 "
1130 .unindent(),
1131 cx,
1132 );
1133 build_editor(buffer, window, cx)
1134 });
1135
1136 _ = editor.update(cx, |editor, window, cx| {
1137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1138 s.select_display_ranges([
1139 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1140 ]);
1141 });
1142 editor.fold(&Fold, window, cx);
1143 assert_eq!(
1144 editor.display_text(cx),
1145 "
1146 class Foo:
1147 # Hello!
1148
1149 def a():
1150 print(1)
1151
1152 def b():⋯
1153
1154 def c():⋯
1155 "
1156 .unindent(),
1157 );
1158
1159 editor.fold(&Fold, window, cx);
1160 assert_eq!(
1161 editor.display_text(cx),
1162 "
1163 class Foo:⋯
1164 "
1165 .unindent(),
1166 );
1167
1168 editor.unfold_lines(&UnfoldLines, window, cx);
1169 assert_eq!(
1170 editor.display_text(cx),
1171 "
1172 class Foo:
1173 # Hello!
1174
1175 def a():
1176 print(1)
1177
1178 def b():⋯
1179
1180 def c():⋯
1181 "
1182 .unindent(),
1183 );
1184
1185 editor.unfold_lines(&UnfoldLines, window, cx);
1186 assert_eq!(
1187 editor.display_text(cx),
1188 editor.buffer.read(cx).read(cx).text()
1189 );
1190 });
1191}
1192
1193#[gpui::test]
1194fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1195 init_test(cx, |_| {});
1196
1197 let editor = cx.add_window(|window, cx| {
1198 let buffer = MultiBuffer::build_simple(
1199 &"
1200 class Foo:
1201 # Hello!
1202
1203 def a():
1204 print(1)
1205
1206 def b():
1207 print(2)
1208
1209
1210 def c():
1211 print(3)
1212
1213
1214 "
1215 .unindent(),
1216 cx,
1217 );
1218 build_editor(buffer, window, cx)
1219 });
1220
1221 _ = editor.update(cx, |editor, window, cx| {
1222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1223 s.select_display_ranges([
1224 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1225 ]);
1226 });
1227 editor.fold(&Fold, window, cx);
1228 assert_eq!(
1229 editor.display_text(cx),
1230 "
1231 class Foo:
1232 # Hello!
1233
1234 def a():
1235 print(1)
1236
1237 def b():⋯
1238
1239
1240 def c():⋯
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 editor.fold(&Fold, window, cx);
1248 assert_eq!(
1249 editor.display_text(cx),
1250 "
1251 class Foo:⋯
1252
1253
1254 "
1255 .unindent(),
1256 );
1257
1258 editor.unfold_lines(&UnfoldLines, window, cx);
1259 assert_eq!(
1260 editor.display_text(cx),
1261 "
1262 class Foo:
1263 # Hello!
1264
1265 def a():
1266 print(1)
1267
1268 def b():⋯
1269
1270
1271 def c():⋯
1272
1273
1274 "
1275 .unindent(),
1276 );
1277
1278 editor.unfold_lines(&UnfoldLines, window, cx);
1279 assert_eq!(
1280 editor.display_text(cx),
1281 editor.buffer.read(cx).read(cx).text()
1282 );
1283 });
1284}
1285
1286#[gpui::test]
1287fn test_fold_at_level(cx: &mut TestAppContext) {
1288 init_test(cx, |_| {});
1289
1290 let editor = cx.add_window(|window, cx| {
1291 let buffer = MultiBuffer::build_simple(
1292 &"
1293 class Foo:
1294 # Hello!
1295
1296 def a():
1297 print(1)
1298
1299 def b():
1300 print(2)
1301
1302
1303 class Bar:
1304 # World!
1305
1306 def a():
1307 print(1)
1308
1309 def b():
1310 print(2)
1311
1312
1313 "
1314 .unindent(),
1315 cx,
1316 );
1317 build_editor(buffer, window, cx)
1318 });
1319
1320 _ = editor.update(cx, |editor, window, cx| {
1321 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1322 assert_eq!(
1323 editor.display_text(cx),
1324 "
1325 class Foo:
1326 # Hello!
1327
1328 def a():⋯
1329
1330 def b():⋯
1331
1332
1333 class Bar:
1334 # World!
1335
1336 def a():⋯
1337
1338 def b():⋯
1339
1340
1341 "
1342 .unindent(),
1343 );
1344
1345 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1346 assert_eq!(
1347 editor.display_text(cx),
1348 "
1349 class Foo:⋯
1350
1351
1352 class Bar:⋯
1353
1354
1355 "
1356 .unindent(),
1357 );
1358
1359 editor.unfold_all(&UnfoldAll, window, cx);
1360 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1361 assert_eq!(
1362 editor.display_text(cx),
1363 "
1364 class Foo:
1365 # Hello!
1366
1367 def a():
1368 print(1)
1369
1370 def b():
1371 print(2)
1372
1373
1374 class Bar:
1375 # World!
1376
1377 def a():
1378 print(1)
1379
1380 def b():
1381 print(2)
1382
1383
1384 "
1385 .unindent(),
1386 );
1387
1388 assert_eq!(
1389 editor.display_text(cx),
1390 editor.buffer.read(cx).read(cx).text()
1391 );
1392 let (_, positions) = marked_text_ranges(
1393 &"
1394 class Foo:
1395 # Hello!
1396
1397 def a():
1398 print(1)
1399
1400 def b():
1401 p«riˇ»nt(2)
1402
1403
1404 class Bar:
1405 # World!
1406
1407 def a():
1408 «ˇprint(1)
1409
1410 def b():
1411 print(2)»
1412
1413
1414 "
1415 .unindent(),
1416 true,
1417 );
1418
1419 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1420 s.select_ranges(positions)
1421 });
1422
1423 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1424 assert_eq!(
1425 editor.display_text(cx),
1426 "
1427 class Foo:
1428 # Hello!
1429
1430 def a():⋯
1431
1432 def b():
1433 print(2)
1434
1435
1436 class Bar:
1437 # World!
1438
1439 def a():
1440 print(1)
1441
1442 def b():
1443 print(2)
1444
1445
1446 "
1447 .unindent(),
1448 );
1449 });
1450}
1451
1452#[gpui::test]
1453fn test_move_cursor(cx: &mut TestAppContext) {
1454 init_test(cx, |_| {});
1455
1456 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1457 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1458
1459 buffer.update(cx, |buffer, cx| {
1460 buffer.edit(
1461 vec![
1462 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1463 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1464 ],
1465 None,
1466 cx,
1467 );
1468 });
1469 _ = editor.update(cx, |editor, window, cx| {
1470 assert_eq!(
1471 display_ranges(editor, cx),
1472 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1473 );
1474
1475 editor.move_down(&MoveDown, window, cx);
1476 assert_eq!(
1477 display_ranges(editor, cx),
1478 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1479 );
1480
1481 editor.move_right(&MoveRight, window, cx);
1482 assert_eq!(
1483 display_ranges(editor, cx),
1484 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1485 );
1486
1487 editor.move_left(&MoveLeft, window, cx);
1488 assert_eq!(
1489 display_ranges(editor, cx),
1490 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1491 );
1492
1493 editor.move_up(&MoveUp, window, cx);
1494 assert_eq!(
1495 display_ranges(editor, cx),
1496 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1497 );
1498
1499 editor.move_to_end(&MoveToEnd, window, cx);
1500 assert_eq!(
1501 display_ranges(editor, cx),
1502 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1503 );
1504
1505 editor.move_to_beginning(&MoveToBeginning, window, cx);
1506 assert_eq!(
1507 display_ranges(editor, cx),
1508 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1509 );
1510
1511 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1512 s.select_display_ranges([
1513 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1514 ]);
1515 });
1516 editor.select_to_beginning(&SelectToBeginning, window, cx);
1517 assert_eq!(
1518 display_ranges(editor, cx),
1519 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1520 );
1521
1522 editor.select_to_end(&SelectToEnd, window, cx);
1523 assert_eq!(
1524 display_ranges(editor, cx),
1525 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1526 );
1527 });
1528}
1529
1530#[gpui::test]
1531fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1532 init_test(cx, |_| {});
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538
1539 assert_eq!('🟥'.len_utf8(), 4);
1540 assert_eq!('α'.len_utf8(), 2);
1541
1542 _ = editor.update(cx, |editor, window, cx| {
1543 editor.fold_creases(
1544 vec![
1545 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1546 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1547 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1548 ],
1549 true,
1550 window,
1551 cx,
1552 );
1553 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1554
1555 editor.move_right(&MoveRight, window, cx);
1556 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1557 editor.move_right(&MoveRight, window, cx);
1558 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1559 editor.move_right(&MoveRight, window, cx);
1560 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1561
1562 editor.move_down(&MoveDown, window, cx);
1563 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1564 editor.move_left(&MoveLeft, window, cx);
1565 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1566 editor.move_left(&MoveLeft, window, cx);
1567 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1568 editor.move_left(&MoveLeft, window, cx);
1569 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1570
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1573 editor.move_right(&MoveRight, window, cx);
1574 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1575 editor.move_right(&MoveRight, window, cx);
1576 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1577 editor.move_right(&MoveRight, window, cx);
1578 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1579
1580 editor.move_up(&MoveUp, window, cx);
1581 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1582 editor.move_down(&MoveDown, window, cx);
1583 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1584 editor.move_up(&MoveUp, window, cx);
1585 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1586
1587 editor.move_up(&MoveUp, window, cx);
1588 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1589 editor.move_left(&MoveLeft, window, cx);
1590 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1591 editor.move_left(&MoveLeft, window, cx);
1592 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1593 });
1594}
1595
1596#[gpui::test]
1597fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1598 init_test(cx, |_| {});
1599
1600 let editor = cx.add_window(|window, cx| {
1601 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1602 build_editor(buffer, window, cx)
1603 });
1604 _ = editor.update(cx, |editor, window, cx| {
1605 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1606 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1607 });
1608
1609 // moving above start of document should move selection to start of document,
1610 // but the next move down should still be at the original goal_x
1611 editor.move_up(&MoveUp, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1613
1614 editor.move_down(&MoveDown, window, cx);
1615 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1616
1617 editor.move_down(&MoveDown, window, cx);
1618 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1619
1620 editor.move_down(&MoveDown, window, cx);
1621 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1622
1623 editor.move_down(&MoveDown, window, cx);
1624 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1625
1626 // moving past end of document should not change goal_x
1627 editor.move_down(&MoveDown, window, cx);
1628 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1629
1630 editor.move_down(&MoveDown, window, cx);
1631 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1632
1633 editor.move_up(&MoveUp, window, cx);
1634 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1635
1636 editor.move_up(&MoveUp, window, cx);
1637 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1638
1639 editor.move_up(&MoveUp, window, cx);
1640 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1641 });
1642}
1643
1644#[gpui::test]
1645fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1646 init_test(cx, |_| {});
1647 let move_to_beg = MoveToBeginningOfLine {
1648 stop_at_soft_wraps: true,
1649 stop_at_indent: true,
1650 };
1651
1652 let delete_to_beg = DeleteToBeginningOfLine {
1653 stop_at_indent: false,
1654 };
1655
1656 let move_to_end = MoveToEndOfLine {
1657 stop_at_soft_wraps: true,
1658 };
1659
1660 let editor = cx.add_window(|window, cx| {
1661 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1662 build_editor(buffer, window, cx)
1663 });
1664 _ = editor.update(cx, |editor, window, cx| {
1665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1666 s.select_display_ranges([
1667 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1669 ]);
1670 });
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1675 assert_eq!(
1676 display_ranges(editor, cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1679 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1686 assert_eq!(
1687 display_ranges(editor, cx),
1688 &[
1689 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1691 ]
1692 );
1693 });
1694
1695 _ = editor.update(cx, |editor, window, cx| {
1696 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1697 assert_eq!(
1698 display_ranges(editor, cx),
1699 &[
1700 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1701 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1702 ]
1703 );
1704 });
1705
1706 _ = editor.update(cx, |editor, window, cx| {
1707 editor.move_to_end_of_line(&move_to_end, window, cx);
1708 assert_eq!(
1709 display_ranges(editor, cx),
1710 &[
1711 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1712 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1713 ]
1714 );
1715 });
1716
1717 // Moving to the end of line again is a no-op.
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.move_to_end_of_line(&move_to_end, window, cx);
1720 assert_eq!(
1721 display_ranges(editor, cx),
1722 &[
1723 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1724 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1725 ]
1726 );
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.move_left(&MoveLeft, window, cx);
1731 editor.select_to_beginning_of_line(
1732 &SelectToBeginningOfLine {
1733 stop_at_soft_wraps: true,
1734 stop_at_indent: true,
1735 },
1736 window,
1737 cx,
1738 );
1739 assert_eq!(
1740 display_ranges(editor, cx),
1741 &[
1742 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1743 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1744 ]
1745 );
1746 });
1747
1748 _ = editor.update(cx, |editor, window, cx| {
1749 editor.select_to_beginning_of_line(
1750 &SelectToBeginningOfLine {
1751 stop_at_soft_wraps: true,
1752 stop_at_indent: true,
1753 },
1754 window,
1755 cx,
1756 );
1757 assert_eq!(
1758 display_ranges(editor, cx),
1759 &[
1760 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1761 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1762 ]
1763 );
1764 });
1765
1766 _ = editor.update(cx, |editor, window, cx| {
1767 editor.select_to_beginning_of_line(
1768 &SelectToBeginningOfLine {
1769 stop_at_soft_wraps: true,
1770 stop_at_indent: true,
1771 },
1772 window,
1773 cx,
1774 );
1775 assert_eq!(
1776 display_ranges(editor, cx),
1777 &[
1778 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1779 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1780 ]
1781 );
1782 });
1783
1784 _ = editor.update(cx, |editor, window, cx| {
1785 editor.select_to_end_of_line(
1786 &SelectToEndOfLine {
1787 stop_at_soft_wraps: true,
1788 },
1789 window,
1790 cx,
1791 );
1792 assert_eq!(
1793 display_ranges(editor, cx),
1794 &[
1795 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1796 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1797 ]
1798 );
1799 });
1800
1801 _ = editor.update(cx, |editor, window, cx| {
1802 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1803 assert_eq!(editor.display_text(cx), "ab\n de");
1804 assert_eq!(
1805 display_ranges(editor, cx),
1806 &[
1807 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1808 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1809 ]
1810 );
1811 });
1812
1813 _ = editor.update(cx, |editor, window, cx| {
1814 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1815 assert_eq!(editor.display_text(cx), "\n");
1816 assert_eq!(
1817 display_ranges(editor, cx),
1818 &[
1819 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1820 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1821 ]
1822 );
1823 });
1824}
1825
1826#[gpui::test]
1827fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1828 init_test(cx, |_| {});
1829 let move_to_beg = MoveToBeginningOfLine {
1830 stop_at_soft_wraps: false,
1831 stop_at_indent: false,
1832 };
1833
1834 let move_to_end = MoveToEndOfLine {
1835 stop_at_soft_wraps: false,
1836 };
1837
1838 let editor = cx.add_window(|window, cx| {
1839 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1840 build_editor(buffer, window, cx)
1841 });
1842
1843 _ = editor.update(cx, |editor, window, cx| {
1844 editor.set_wrap_width(Some(140.0.into()), cx);
1845
1846 // We expect the following lines after wrapping
1847 // ```
1848 // thequickbrownfox
1849 // jumpedoverthelazydo
1850 // gs
1851 // ```
1852 // The final `gs` was soft-wrapped onto a new line.
1853 assert_eq!(
1854 "thequickbrownfox\njumpedoverthelaz\nydogs",
1855 editor.display_text(cx),
1856 );
1857
1858 // First, let's assert behavior on the first line, that was not soft-wrapped.
1859 // Start the cursor at the `k` on the first line
1860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1861 s.select_display_ranges([
1862 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1863 ]);
1864 });
1865
1866 // Moving to the beginning of the line should put us at the beginning of the line.
1867 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1868 assert_eq!(
1869 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1870 display_ranges(editor, cx)
1871 );
1872
1873 // Moving to the end of the line should put us at the end of the line.
1874 editor.move_to_end_of_line(&move_to_end, window, cx);
1875 assert_eq!(
1876 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1877 display_ranges(editor, cx)
1878 );
1879
1880 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1881 // Start the cursor at the last line (`y` that was wrapped to a new line)
1882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1883 s.select_display_ranges([
1884 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1885 ]);
1886 });
1887
1888 // Moving to the beginning of the line should put us at the start of the second line of
1889 // display text, i.e., the `j`.
1890 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1891 assert_eq!(
1892 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1893 display_ranges(editor, cx)
1894 );
1895
1896 // Moving to the beginning of the line again should be a no-op.
1897 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1898 assert_eq!(
1899 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1900 display_ranges(editor, cx)
1901 );
1902
1903 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1904 // next display line.
1905 editor.move_to_end_of_line(&move_to_end, window, cx);
1906 assert_eq!(
1907 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1908 display_ranges(editor, cx)
1909 );
1910
1911 // Moving to the end of the line again should be a no-op.
1912 editor.move_to_end_of_line(&move_to_end, window, cx);
1913 assert_eq!(
1914 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1915 display_ranges(editor, cx)
1916 );
1917 });
1918}
1919
1920#[gpui::test]
1921fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1922 init_test(cx, |_| {});
1923
1924 let move_to_beg = MoveToBeginningOfLine {
1925 stop_at_soft_wraps: true,
1926 stop_at_indent: true,
1927 };
1928
1929 let select_to_beg = SelectToBeginningOfLine {
1930 stop_at_soft_wraps: true,
1931 stop_at_indent: true,
1932 };
1933
1934 let delete_to_beg = DeleteToBeginningOfLine {
1935 stop_at_indent: true,
1936 };
1937
1938 let move_to_end = MoveToEndOfLine {
1939 stop_at_soft_wraps: false,
1940 };
1941
1942 let editor = cx.add_window(|window, cx| {
1943 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1944 build_editor(buffer, window, cx)
1945 });
1946
1947 _ = editor.update(cx, |editor, window, cx| {
1948 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1949 s.select_display_ranges([
1950 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1951 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1952 ]);
1953 });
1954
1955 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1956 // and the second cursor at the first non-whitespace character in the line.
1957 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1958 assert_eq!(
1959 display_ranges(editor, cx),
1960 &[
1961 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1962 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1963 ]
1964 );
1965
1966 // Moving to the beginning of the line again should be a no-op for the first cursor,
1967 // and should move the second cursor to the beginning of the line.
1968 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1969 assert_eq!(
1970 display_ranges(editor, cx),
1971 &[
1972 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1973 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1974 ]
1975 );
1976
1977 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1978 // and should move the second cursor back to the first non-whitespace character in the line.
1979 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1980 assert_eq!(
1981 display_ranges(editor, cx),
1982 &[
1983 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1984 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1985 ]
1986 );
1987
1988 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1989 // and to the first non-whitespace character in the line for the second cursor.
1990 editor.move_to_end_of_line(&move_to_end, window, cx);
1991 editor.move_left(&MoveLeft, window, cx);
1992 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1993 assert_eq!(
1994 display_ranges(editor, cx),
1995 &[
1996 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1997 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1998 ]
1999 );
2000
2001 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2002 // and should select to the beginning of the line for the second cursor.
2003 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2004 assert_eq!(
2005 display_ranges(editor, cx),
2006 &[
2007 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2008 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2009 ]
2010 );
2011
2012 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2013 // and should delete to the first non-whitespace character in the line for the second cursor.
2014 editor.move_to_end_of_line(&move_to_end, window, cx);
2015 editor.move_left(&MoveLeft, window, cx);
2016 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2017 assert_eq!(editor.text(cx), "c\n f");
2018 });
2019}
2020
2021#[gpui::test]
2022fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2023 init_test(cx, |_| {});
2024
2025 let move_to_beg = MoveToBeginningOfLine {
2026 stop_at_soft_wraps: true,
2027 stop_at_indent: true,
2028 };
2029
2030 let editor = cx.add_window(|window, cx| {
2031 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2032 build_editor(buffer, window, cx)
2033 });
2034
2035 _ = editor.update(cx, |editor, window, cx| {
2036 // test cursor between line_start and indent_start
2037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2038 s.select_display_ranges([
2039 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2040 ]);
2041 });
2042
2043 // cursor should move to line_start
2044 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2045 assert_eq!(
2046 display_ranges(editor, cx),
2047 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2048 );
2049
2050 // cursor should move to indent_start
2051 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2052 assert_eq!(
2053 display_ranges(editor, cx),
2054 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2055 );
2056
2057 // cursor should move to back to line_start
2058 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2059 assert_eq!(
2060 display_ranges(editor, cx),
2061 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2062 );
2063 });
2064}
2065
2066#[gpui::test]
2067fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2068 init_test(cx, |_| {});
2069
2070 let editor = cx.add_window(|window, cx| {
2071 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2072 build_editor(buffer, window, cx)
2073 });
2074 _ = editor.update(cx, |editor, window, cx| {
2075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2076 s.select_display_ranges([
2077 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2078 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2079 ])
2080 });
2081 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2082 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2083
2084 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2085 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2086
2087 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2088 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2089
2090 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2091 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2092
2093 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2094 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2095
2096 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2097 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2098
2099 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2100 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2101
2102 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2103 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2104
2105 editor.move_right(&MoveRight, window, cx);
2106 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2107 assert_selection_ranges(
2108 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2109 editor,
2110 cx,
2111 );
2112
2113 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2114 assert_selection_ranges(
2115 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2116 editor,
2117 cx,
2118 );
2119
2120 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2121 assert_selection_ranges(
2122 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2123 editor,
2124 cx,
2125 );
2126 });
2127}
2128
2129#[gpui::test]
2130fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2131 init_test(cx, |_| {});
2132
2133 let editor = cx.add_window(|window, cx| {
2134 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2135 build_editor(buffer, window, cx)
2136 });
2137
2138 _ = editor.update(cx, |editor, window, cx| {
2139 editor.set_wrap_width(Some(140.0.into()), cx);
2140 assert_eq!(
2141 editor.display_text(cx),
2142 "use one::{\n two::three::\n four::five\n};"
2143 );
2144
2145 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2146 s.select_display_ranges([
2147 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2148 ]);
2149 });
2150
2151 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2152 assert_eq!(
2153 display_ranges(editor, cx),
2154 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2155 );
2156
2157 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2158 assert_eq!(
2159 display_ranges(editor, cx),
2160 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2161 );
2162
2163 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2164 assert_eq!(
2165 display_ranges(editor, cx),
2166 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2167 );
2168
2169 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2170 assert_eq!(
2171 display_ranges(editor, cx),
2172 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2173 );
2174
2175 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2176 assert_eq!(
2177 display_ranges(editor, cx),
2178 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2179 );
2180
2181 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2182 assert_eq!(
2183 display_ranges(editor, cx),
2184 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2185 );
2186 });
2187}
2188
2189#[gpui::test]
2190async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192 let mut cx = EditorTestContext::new(cx).await;
2193
2194 let line_height = cx.editor(|editor, window, _| {
2195 editor
2196 .style()
2197 .unwrap()
2198 .text
2199 .line_height_in_pixels(window.rem_size())
2200 });
2201 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2202
2203 cx.set_state(
2204 &r#"ˇone
2205 two
2206
2207 three
2208 fourˇ
2209 five
2210
2211 six"#
2212 .unindent(),
2213 );
2214
2215 cx.update_editor(|editor, window, cx| {
2216 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2217 });
2218 cx.assert_editor_state(
2219 &r#"one
2220 two
2221 ˇ
2222 three
2223 four
2224 five
2225 ˇ
2226 six"#
2227 .unindent(),
2228 );
2229
2230 cx.update_editor(|editor, window, cx| {
2231 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2232 });
2233 cx.assert_editor_state(
2234 &r#"one
2235 two
2236
2237 three
2238 four
2239 five
2240 ˇ
2241 sixˇ"#
2242 .unindent(),
2243 );
2244
2245 cx.update_editor(|editor, window, cx| {
2246 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2247 });
2248 cx.assert_editor_state(
2249 &r#"one
2250 two
2251
2252 three
2253 four
2254 five
2255
2256 sixˇ"#
2257 .unindent(),
2258 );
2259
2260 cx.update_editor(|editor, window, cx| {
2261 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2262 });
2263 cx.assert_editor_state(
2264 &r#"one
2265 two
2266
2267 three
2268 four
2269 five
2270 ˇ
2271 six"#
2272 .unindent(),
2273 );
2274
2275 cx.update_editor(|editor, window, cx| {
2276 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2277 });
2278 cx.assert_editor_state(
2279 &r#"one
2280 two
2281 ˇ
2282 three
2283 four
2284 five
2285
2286 six"#
2287 .unindent(),
2288 );
2289
2290 cx.update_editor(|editor, window, cx| {
2291 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2292 });
2293 cx.assert_editor_state(
2294 &r#"ˇone
2295 two
2296
2297 three
2298 four
2299 five
2300
2301 six"#
2302 .unindent(),
2303 );
2304}
2305
2306#[gpui::test]
2307async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2308 init_test(cx, |_| {});
2309 let mut cx = EditorTestContext::new(cx).await;
2310 let line_height = cx.editor(|editor, window, _| {
2311 editor
2312 .style()
2313 .unwrap()
2314 .text
2315 .line_height_in_pixels(window.rem_size())
2316 });
2317 let window = cx.window;
2318 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2319
2320 cx.set_state(
2321 r#"ˇone
2322 two
2323 three
2324 four
2325 five
2326 six
2327 seven
2328 eight
2329 nine
2330 ten
2331 "#,
2332 );
2333
2334 cx.update_editor(|editor, window, cx| {
2335 assert_eq!(
2336 editor.snapshot(window, cx).scroll_position(),
2337 gpui::Point::new(0., 0.)
2338 );
2339 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2340 assert_eq!(
2341 editor.snapshot(window, cx).scroll_position(),
2342 gpui::Point::new(0., 3.)
2343 );
2344 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2345 assert_eq!(
2346 editor.snapshot(window, cx).scroll_position(),
2347 gpui::Point::new(0., 6.)
2348 );
2349 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2350 assert_eq!(
2351 editor.snapshot(window, cx).scroll_position(),
2352 gpui::Point::new(0., 3.)
2353 );
2354
2355 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2356 assert_eq!(
2357 editor.snapshot(window, cx).scroll_position(),
2358 gpui::Point::new(0., 1.)
2359 );
2360 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2361 assert_eq!(
2362 editor.snapshot(window, cx).scroll_position(),
2363 gpui::Point::new(0., 3.)
2364 );
2365 });
2366}
2367
2368#[gpui::test]
2369async fn test_autoscroll(cx: &mut TestAppContext) {
2370 init_test(cx, |_| {});
2371 let mut cx = EditorTestContext::new(cx).await;
2372
2373 let line_height = cx.update_editor(|editor, window, cx| {
2374 editor.set_vertical_scroll_margin(2, cx);
2375 editor
2376 .style()
2377 .unwrap()
2378 .text
2379 .line_height_in_pixels(window.rem_size())
2380 });
2381 let window = cx.window;
2382 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2383
2384 cx.set_state(
2385 r#"ˇone
2386 two
2387 three
2388 four
2389 five
2390 six
2391 seven
2392 eight
2393 nine
2394 ten
2395 "#,
2396 );
2397 cx.update_editor(|editor, window, cx| {
2398 assert_eq!(
2399 editor.snapshot(window, cx).scroll_position(),
2400 gpui::Point::new(0., 0.0)
2401 );
2402 });
2403
2404 // Add a cursor below the visible area. Since both cursors cannot fit
2405 // on screen, the editor autoscrolls to reveal the newest cursor, and
2406 // allows the vertical scroll margin below that cursor.
2407 cx.update_editor(|editor, window, cx| {
2408 editor.change_selections(Default::default(), window, cx, |selections| {
2409 selections.select_ranges([
2410 Point::new(0, 0)..Point::new(0, 0),
2411 Point::new(6, 0)..Point::new(6, 0),
2412 ]);
2413 })
2414 });
2415 cx.update_editor(|editor, window, cx| {
2416 assert_eq!(
2417 editor.snapshot(window, cx).scroll_position(),
2418 gpui::Point::new(0., 3.0)
2419 );
2420 });
2421
2422 // Move down. The editor cursor scrolls down to track the newest cursor.
2423 cx.update_editor(|editor, window, cx| {
2424 editor.move_down(&Default::default(), window, cx);
2425 });
2426 cx.update_editor(|editor, window, cx| {
2427 assert_eq!(
2428 editor.snapshot(window, cx).scroll_position(),
2429 gpui::Point::new(0., 4.0)
2430 );
2431 });
2432
2433 // Add a cursor above the visible area. Since both cursors fit on screen,
2434 // the editor scrolls to show both.
2435 cx.update_editor(|editor, window, cx| {
2436 editor.change_selections(Default::default(), window, cx, |selections| {
2437 selections.select_ranges([
2438 Point::new(1, 0)..Point::new(1, 0),
2439 Point::new(6, 0)..Point::new(6, 0),
2440 ]);
2441 })
2442 });
2443 cx.update_editor(|editor, window, cx| {
2444 assert_eq!(
2445 editor.snapshot(window, cx).scroll_position(),
2446 gpui::Point::new(0., 1.0)
2447 );
2448 });
2449}
2450
2451#[gpui::test]
2452async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2453 init_test(cx, |_| {});
2454 let mut cx = EditorTestContext::new(cx).await;
2455
2456 let line_height = cx.editor(|editor, window, _cx| {
2457 editor
2458 .style()
2459 .unwrap()
2460 .text
2461 .line_height_in_pixels(window.rem_size())
2462 });
2463 let window = cx.window;
2464 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2465 cx.set_state(
2466 &r#"
2467 ˇone
2468 two
2469 threeˇ
2470 four
2471 five
2472 six
2473 seven
2474 eight
2475 nine
2476 ten
2477 "#
2478 .unindent(),
2479 );
2480
2481 cx.update_editor(|editor, window, cx| {
2482 editor.move_page_down(&MovePageDown::default(), window, cx)
2483 });
2484 cx.assert_editor_state(
2485 &r#"
2486 one
2487 two
2488 three
2489 ˇfour
2490 five
2491 sixˇ
2492 seven
2493 eight
2494 nine
2495 ten
2496 "#
2497 .unindent(),
2498 );
2499
2500 cx.update_editor(|editor, window, cx| {
2501 editor.move_page_down(&MovePageDown::default(), window, cx)
2502 });
2503 cx.assert_editor_state(
2504 &r#"
2505 one
2506 two
2507 three
2508 four
2509 five
2510 six
2511 ˇseven
2512 eight
2513 nineˇ
2514 ten
2515 "#
2516 .unindent(),
2517 );
2518
2519 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2520 cx.assert_editor_state(
2521 &r#"
2522 one
2523 two
2524 three
2525 ˇfour
2526 five
2527 sixˇ
2528 seven
2529 eight
2530 nine
2531 ten
2532 "#
2533 .unindent(),
2534 );
2535
2536 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2537 cx.assert_editor_state(
2538 &r#"
2539 ˇone
2540 two
2541 threeˇ
2542 four
2543 five
2544 six
2545 seven
2546 eight
2547 nine
2548 ten
2549 "#
2550 .unindent(),
2551 );
2552
2553 // Test select collapsing
2554 cx.update_editor(|editor, window, cx| {
2555 editor.move_page_down(&MovePageDown::default(), window, cx);
2556 editor.move_page_down(&MovePageDown::default(), window, cx);
2557 editor.move_page_down(&MovePageDown::default(), window, cx);
2558 });
2559 cx.assert_editor_state(
2560 &r#"
2561 one
2562 two
2563 three
2564 four
2565 five
2566 six
2567 seven
2568 eight
2569 nine
2570 ˇten
2571 ˇ"#
2572 .unindent(),
2573 );
2574}
2575
2576#[gpui::test]
2577async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2578 init_test(cx, |_| {});
2579 let mut cx = EditorTestContext::new(cx).await;
2580 cx.set_state("one «two threeˇ» four");
2581 cx.update_editor(|editor, window, cx| {
2582 editor.delete_to_beginning_of_line(
2583 &DeleteToBeginningOfLine {
2584 stop_at_indent: false,
2585 },
2586 window,
2587 cx,
2588 );
2589 assert_eq!(editor.text(cx), " four");
2590 });
2591}
2592
2593#[gpui::test]
2594async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2595 init_test(cx, |_| {});
2596
2597 let mut cx = EditorTestContext::new(cx).await;
2598
2599 // For an empty selection, the preceding word fragment is deleted.
2600 // For non-empty selections, only selected characters are deleted.
2601 cx.set_state("onˇe two t«hreˇ»e four");
2602 cx.update_editor(|editor, window, cx| {
2603 editor.delete_to_previous_word_start(
2604 &DeleteToPreviousWordStart {
2605 ignore_newlines: false,
2606 ignore_brackets: false,
2607 },
2608 window,
2609 cx,
2610 );
2611 });
2612 cx.assert_editor_state("ˇe two tˇe four");
2613
2614 cx.set_state("e tˇwo te «fˇ»our");
2615 cx.update_editor(|editor, window, cx| {
2616 editor.delete_to_next_word_end(
2617 &DeleteToNextWordEnd {
2618 ignore_newlines: false,
2619 ignore_brackets: false,
2620 },
2621 window,
2622 cx,
2623 );
2624 });
2625 cx.assert_editor_state("e tˇ te ˇour");
2626}
2627
2628#[gpui::test]
2629async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2630 init_test(cx, |_| {});
2631
2632 let mut cx = EditorTestContext::new(cx).await;
2633
2634 cx.set_state("here is some text ˇwith a space");
2635 cx.update_editor(|editor, window, cx| {
2636 editor.delete_to_previous_word_start(
2637 &DeleteToPreviousWordStart {
2638 ignore_newlines: false,
2639 ignore_brackets: true,
2640 },
2641 window,
2642 cx,
2643 );
2644 });
2645 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2646 cx.assert_editor_state("here is some textˇwith a space");
2647
2648 cx.set_state("here is some text ˇwith a space");
2649 cx.update_editor(|editor, window, cx| {
2650 editor.delete_to_previous_word_start(
2651 &DeleteToPreviousWordStart {
2652 ignore_newlines: false,
2653 ignore_brackets: false,
2654 },
2655 window,
2656 cx,
2657 );
2658 });
2659 cx.assert_editor_state("here is some textˇwith a space");
2660
2661 cx.set_state("here is some textˇ with a space");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_next_word_end(
2664 &DeleteToNextWordEnd {
2665 ignore_newlines: false,
2666 ignore_brackets: true,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 // Same happens in the other direction.
2673 cx.assert_editor_state("here is some textˇwith a space");
2674
2675 cx.set_state("here is some textˇ with a space");
2676 cx.update_editor(|editor, window, cx| {
2677 editor.delete_to_next_word_end(
2678 &DeleteToNextWordEnd {
2679 ignore_newlines: false,
2680 ignore_brackets: false,
2681 },
2682 window,
2683 cx,
2684 );
2685 });
2686 cx.assert_editor_state("here is some textˇwith a space");
2687
2688 cx.set_state("here is some textˇ with a space");
2689 cx.update_editor(|editor, window, cx| {
2690 editor.delete_to_next_word_end(
2691 &DeleteToNextWordEnd {
2692 ignore_newlines: true,
2693 ignore_brackets: false,
2694 },
2695 window,
2696 cx,
2697 );
2698 });
2699 cx.assert_editor_state("here is some textˇwith a space");
2700 cx.update_editor(|editor, window, cx| {
2701 editor.delete_to_previous_word_start(
2702 &DeleteToPreviousWordStart {
2703 ignore_newlines: true,
2704 ignore_brackets: false,
2705 },
2706 window,
2707 cx,
2708 );
2709 });
2710 cx.assert_editor_state("here is some ˇwith a space");
2711 cx.update_editor(|editor, window, cx| {
2712 editor.delete_to_previous_word_start(
2713 &DeleteToPreviousWordStart {
2714 ignore_newlines: true,
2715 ignore_brackets: false,
2716 },
2717 window,
2718 cx,
2719 );
2720 });
2721 // Single whitespaces are removed with the word behind them.
2722 cx.assert_editor_state("here is ˇwith a space");
2723 cx.update_editor(|editor, window, cx| {
2724 editor.delete_to_previous_word_start(
2725 &DeleteToPreviousWordStart {
2726 ignore_newlines: true,
2727 ignore_brackets: false,
2728 },
2729 window,
2730 cx,
2731 );
2732 });
2733 cx.assert_editor_state("here ˇwith a space");
2734 cx.update_editor(|editor, window, cx| {
2735 editor.delete_to_previous_word_start(
2736 &DeleteToPreviousWordStart {
2737 ignore_newlines: true,
2738 ignore_brackets: false,
2739 },
2740 window,
2741 cx,
2742 );
2743 });
2744 cx.assert_editor_state("ˇwith a space");
2745 cx.update_editor(|editor, window, cx| {
2746 editor.delete_to_previous_word_start(
2747 &DeleteToPreviousWordStart {
2748 ignore_newlines: true,
2749 ignore_brackets: false,
2750 },
2751 window,
2752 cx,
2753 );
2754 });
2755 cx.assert_editor_state("ˇwith a space");
2756 cx.update_editor(|editor, window, cx| {
2757 editor.delete_to_next_word_end(
2758 &DeleteToNextWordEnd {
2759 ignore_newlines: true,
2760 ignore_brackets: false,
2761 },
2762 window,
2763 cx,
2764 );
2765 });
2766 // Same happens in the other direction.
2767 cx.assert_editor_state("ˇ a space");
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_next_word_end(
2770 &DeleteToNextWordEnd {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state("ˇ space");
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_next_word_end(
2781 &DeleteToNextWordEnd {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state("ˇ");
2790 cx.update_editor(|editor, window, cx| {
2791 editor.delete_to_next_word_end(
2792 &DeleteToNextWordEnd {
2793 ignore_newlines: true,
2794 ignore_brackets: false,
2795 },
2796 window,
2797 cx,
2798 );
2799 });
2800 cx.assert_editor_state("ˇ");
2801 cx.update_editor(|editor, window, cx| {
2802 editor.delete_to_previous_word_start(
2803 &DeleteToPreviousWordStart {
2804 ignore_newlines: true,
2805 ignore_brackets: false,
2806 },
2807 window,
2808 cx,
2809 );
2810 });
2811 cx.assert_editor_state("ˇ");
2812}
2813
2814#[gpui::test]
2815async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2816 init_test(cx, |_| {});
2817
2818 let language = Arc::new(
2819 Language::new(
2820 LanguageConfig {
2821 brackets: BracketPairConfig {
2822 pairs: vec![
2823 BracketPair {
2824 start: "\"".to_string(),
2825 end: "\"".to_string(),
2826 close: true,
2827 surround: true,
2828 newline: false,
2829 },
2830 BracketPair {
2831 start: "(".to_string(),
2832 end: ")".to_string(),
2833 close: true,
2834 surround: true,
2835 newline: true,
2836 },
2837 ],
2838 ..BracketPairConfig::default()
2839 },
2840 ..LanguageConfig::default()
2841 },
2842 Some(tree_sitter_rust::LANGUAGE.into()),
2843 )
2844 .with_brackets_query(
2845 r#"
2846 ("(" @open ")" @close)
2847 ("\"" @open "\"" @close)
2848 "#,
2849 )
2850 .unwrap(),
2851 );
2852
2853 let mut cx = EditorTestContext::new(cx).await;
2854 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2855
2856 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2857 cx.update_editor(|editor, window, cx| {
2858 editor.delete_to_previous_word_start(
2859 &DeleteToPreviousWordStart {
2860 ignore_newlines: true,
2861 ignore_brackets: false,
2862 },
2863 window,
2864 cx,
2865 );
2866 });
2867 // Deletion stops before brackets if asked to not ignore them.
2868 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2869 cx.update_editor(|editor, window, cx| {
2870 editor.delete_to_previous_word_start(
2871 &DeleteToPreviousWordStart {
2872 ignore_newlines: true,
2873 ignore_brackets: false,
2874 },
2875 window,
2876 cx,
2877 );
2878 });
2879 // Deletion has to remove a single bracket and then stop again.
2880 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2881
2882 cx.update_editor(|editor, window, cx| {
2883 editor.delete_to_previous_word_start(
2884 &DeleteToPreviousWordStart {
2885 ignore_newlines: true,
2886 ignore_brackets: false,
2887 },
2888 window,
2889 cx,
2890 );
2891 });
2892 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2893
2894 cx.update_editor(|editor, window, cx| {
2895 editor.delete_to_previous_word_start(
2896 &DeleteToPreviousWordStart {
2897 ignore_newlines: true,
2898 ignore_brackets: false,
2899 },
2900 window,
2901 cx,
2902 );
2903 });
2904 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2905
2906 cx.update_editor(|editor, window, cx| {
2907 editor.delete_to_previous_word_start(
2908 &DeleteToPreviousWordStart {
2909 ignore_newlines: true,
2910 ignore_brackets: false,
2911 },
2912 window,
2913 cx,
2914 );
2915 });
2916 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2917
2918 cx.update_editor(|editor, window, cx| {
2919 editor.delete_to_next_word_end(
2920 &DeleteToNextWordEnd {
2921 ignore_newlines: true,
2922 ignore_brackets: false,
2923 },
2924 window,
2925 cx,
2926 );
2927 });
2928 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2929 cx.assert_editor_state(r#"ˇ");"#);
2930
2931 cx.update_editor(|editor, window, cx| {
2932 editor.delete_to_next_word_end(
2933 &DeleteToNextWordEnd {
2934 ignore_newlines: true,
2935 ignore_brackets: false,
2936 },
2937 window,
2938 cx,
2939 );
2940 });
2941 cx.assert_editor_state(r#"ˇ"#);
2942
2943 cx.update_editor(|editor, window, cx| {
2944 editor.delete_to_next_word_end(
2945 &DeleteToNextWordEnd {
2946 ignore_newlines: true,
2947 ignore_brackets: false,
2948 },
2949 window,
2950 cx,
2951 );
2952 });
2953 cx.assert_editor_state(r#"ˇ"#);
2954
2955 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_previous_word_start(
2958 &DeleteToPreviousWordStart {
2959 ignore_newlines: true,
2960 ignore_brackets: true,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2967}
2968
2969#[gpui::test]
2970fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2971 init_test(cx, |_| {});
2972
2973 let editor = cx.add_window(|window, cx| {
2974 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2975 build_editor(buffer, window, cx)
2976 });
2977 let del_to_prev_word_start = DeleteToPreviousWordStart {
2978 ignore_newlines: false,
2979 ignore_brackets: false,
2980 };
2981 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2982 ignore_newlines: true,
2983 ignore_brackets: false,
2984 };
2985
2986 _ = editor.update(cx, |editor, window, cx| {
2987 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2988 s.select_display_ranges([
2989 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2990 ])
2991 });
2992 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2993 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2994 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2995 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2996 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2997 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2998 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2999 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3000 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3001 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3002 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3003 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3004 });
3005}
3006
3007#[gpui::test]
3008fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3009 init_test(cx, |_| {});
3010
3011 let editor = cx.add_window(|window, cx| {
3012 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3013 build_editor(buffer, window, cx)
3014 });
3015 let del_to_next_word_end = DeleteToNextWordEnd {
3016 ignore_newlines: false,
3017 ignore_brackets: false,
3018 };
3019 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3020 ignore_newlines: true,
3021 ignore_brackets: false,
3022 };
3023
3024 _ = editor.update(cx, |editor, window, cx| {
3025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3026 s.select_display_ranges([
3027 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3028 ])
3029 });
3030 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3031 assert_eq!(
3032 editor.buffer.read(cx).read(cx).text(),
3033 "one\n two\nthree\n four"
3034 );
3035 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3036 assert_eq!(
3037 editor.buffer.read(cx).read(cx).text(),
3038 "\n two\nthree\n four"
3039 );
3040 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3041 assert_eq!(
3042 editor.buffer.read(cx).read(cx).text(),
3043 "two\nthree\n four"
3044 );
3045 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3046 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3047 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3048 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3049 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3050 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3051 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3052 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3053 });
3054}
3055
3056#[gpui::test]
3057fn test_newline(cx: &mut TestAppContext) {
3058 init_test(cx, |_| {});
3059
3060 let editor = cx.add_window(|window, cx| {
3061 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3062 build_editor(buffer, window, cx)
3063 });
3064
3065 _ = editor.update(cx, |editor, window, cx| {
3066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3067 s.select_display_ranges([
3068 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3069 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3070 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3071 ])
3072 });
3073
3074 editor.newline(&Newline, window, cx);
3075 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3076 });
3077}
3078
3079#[gpui::test]
3080async fn test_newline_yaml(cx: &mut TestAppContext) {
3081 init_test(cx, |_| {});
3082
3083 let mut cx = EditorTestContext::new(cx).await;
3084 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3085 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3086
3087 // Object (between 2 fields)
3088 cx.set_state(indoc! {"
3089 test:ˇ
3090 hello: bye"});
3091 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 test:
3094 ˇ
3095 hello: bye"});
3096
3097 // Object (first and single line)
3098 cx.set_state(indoc! {"
3099 test:ˇ"});
3100 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3101 cx.assert_editor_state(indoc! {"
3102 test:
3103 ˇ"});
3104
3105 // Array with objects (after first element)
3106 cx.set_state(indoc! {"
3107 test:
3108 - foo: barˇ"});
3109 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 test:
3112 - foo: bar
3113 ˇ"});
3114
3115 // Array with objects and comment
3116 cx.set_state(indoc! {"
3117 test:
3118 - foo: bar
3119 - bar: # testˇ"});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 test:
3123 - foo: bar
3124 - bar: # test
3125 ˇ"});
3126
3127 // Array with objects (after second element)
3128 cx.set_state(indoc! {"
3129 test:
3130 - foo: bar
3131 - bar: fooˇ"});
3132 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3133 cx.assert_editor_state(indoc! {"
3134 test:
3135 - foo: bar
3136 - bar: foo
3137 ˇ"});
3138
3139 // Array with strings (after first element)
3140 cx.set_state(indoc! {"
3141 test:
3142 - fooˇ"});
3143 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3144 cx.assert_editor_state(indoc! {"
3145 test:
3146 - foo
3147 ˇ"});
3148}
3149
3150#[gpui::test]
3151fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3152 init_test(cx, |_| {});
3153
3154 let editor = cx.add_window(|window, cx| {
3155 let buffer = MultiBuffer::build_simple(
3156 "
3157 a
3158 b(
3159 X
3160 )
3161 c(
3162 X
3163 )
3164 "
3165 .unindent()
3166 .as_str(),
3167 cx,
3168 );
3169 let mut editor = build_editor(buffer, window, cx);
3170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3171 s.select_ranges([
3172 Point::new(2, 4)..Point::new(2, 5),
3173 Point::new(5, 4)..Point::new(5, 5),
3174 ])
3175 });
3176 editor
3177 });
3178
3179 _ = editor.update(cx, |editor, window, cx| {
3180 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3181 editor.buffer.update(cx, |buffer, cx| {
3182 buffer.edit(
3183 [
3184 (Point::new(1, 2)..Point::new(3, 0), ""),
3185 (Point::new(4, 2)..Point::new(6, 0), ""),
3186 ],
3187 None,
3188 cx,
3189 );
3190 assert_eq!(
3191 buffer.read(cx).text(),
3192 "
3193 a
3194 b()
3195 c()
3196 "
3197 .unindent()
3198 );
3199 });
3200 assert_eq!(
3201 editor.selections.ranges(&editor.display_snapshot(cx)),
3202 &[
3203 Point::new(1, 2)..Point::new(1, 2),
3204 Point::new(2, 2)..Point::new(2, 2),
3205 ],
3206 );
3207
3208 editor.newline(&Newline, window, cx);
3209 assert_eq!(
3210 editor.text(cx),
3211 "
3212 a
3213 b(
3214 )
3215 c(
3216 )
3217 "
3218 .unindent()
3219 );
3220
3221 // The selections are moved after the inserted newlines
3222 assert_eq!(
3223 editor.selections.ranges(&editor.display_snapshot(cx)),
3224 &[
3225 Point::new(2, 0)..Point::new(2, 0),
3226 Point::new(4, 0)..Point::new(4, 0),
3227 ],
3228 );
3229 });
3230}
3231
3232#[gpui::test]
3233async fn test_newline_above(cx: &mut TestAppContext) {
3234 init_test(cx, |settings| {
3235 settings.defaults.tab_size = NonZeroU32::new(4)
3236 });
3237
3238 let language = Arc::new(
3239 Language::new(
3240 LanguageConfig::default(),
3241 Some(tree_sitter_rust::LANGUAGE.into()),
3242 )
3243 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3244 .unwrap(),
3245 );
3246
3247 let mut cx = EditorTestContext::new(cx).await;
3248 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3249 cx.set_state(indoc! {"
3250 const a: ˇA = (
3251 (ˇ
3252 «const_functionˇ»(ˇ),
3253 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3254 )ˇ
3255 ˇ);ˇ
3256 "});
3257
3258 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3259 cx.assert_editor_state(indoc! {"
3260 ˇ
3261 const a: A = (
3262 ˇ
3263 (
3264 ˇ
3265 ˇ
3266 const_function(),
3267 ˇ
3268 ˇ
3269 ˇ
3270 ˇ
3271 something_else,
3272 ˇ
3273 )
3274 ˇ
3275 ˇ
3276 );
3277 "});
3278}
3279
3280#[gpui::test]
3281async fn test_newline_below(cx: &mut TestAppContext) {
3282 init_test(cx, |settings| {
3283 settings.defaults.tab_size = NonZeroU32::new(4)
3284 });
3285
3286 let language = Arc::new(
3287 Language::new(
3288 LanguageConfig::default(),
3289 Some(tree_sitter_rust::LANGUAGE.into()),
3290 )
3291 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3292 .unwrap(),
3293 );
3294
3295 let mut cx = EditorTestContext::new(cx).await;
3296 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3297 cx.set_state(indoc! {"
3298 const a: ˇA = (
3299 (ˇ
3300 «const_functionˇ»(ˇ),
3301 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3302 )ˇ
3303 ˇ);ˇ
3304 "});
3305
3306 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3307 cx.assert_editor_state(indoc! {"
3308 const a: A = (
3309 ˇ
3310 (
3311 ˇ
3312 const_function(),
3313 ˇ
3314 ˇ
3315 something_else,
3316 ˇ
3317 ˇ
3318 ˇ
3319 ˇ
3320 )
3321 ˇ
3322 );
3323 ˇ
3324 ˇ
3325 "});
3326}
3327
3328#[gpui::test]
3329async fn test_newline_comments(cx: &mut TestAppContext) {
3330 init_test(cx, |settings| {
3331 settings.defaults.tab_size = NonZeroU32::new(4)
3332 });
3333
3334 let language = Arc::new(Language::new(
3335 LanguageConfig {
3336 line_comments: vec!["// ".into()],
3337 ..LanguageConfig::default()
3338 },
3339 None,
3340 ));
3341 {
3342 let mut cx = EditorTestContext::new(cx).await;
3343 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3344 cx.set_state(indoc! {"
3345 // Fooˇ
3346 "});
3347
3348 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 // Foo
3351 // ˇ
3352 "});
3353 // Ensure that we add comment prefix when existing line contains space
3354 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3355 cx.assert_editor_state(
3356 indoc! {"
3357 // Foo
3358 //s
3359 // ˇ
3360 "}
3361 .replace("s", " ") // s is used as space placeholder to prevent format on save
3362 .as_str(),
3363 );
3364 // Ensure that we add comment prefix when existing line does not contain space
3365 cx.set_state(indoc! {"
3366 // Foo
3367 //ˇ
3368 "});
3369 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3370 cx.assert_editor_state(indoc! {"
3371 // Foo
3372 //
3373 // ˇ
3374 "});
3375 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3376 cx.set_state(indoc! {"
3377 ˇ// Foo
3378 "});
3379 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381
3382 ˇ// Foo
3383 "});
3384 }
3385 // Ensure that comment continuations can be disabled.
3386 update_test_language_settings(cx, |settings| {
3387 settings.defaults.extend_comment_on_newline = Some(false);
3388 });
3389 let mut cx = EditorTestContext::new(cx).await;
3390 cx.set_state(indoc! {"
3391 // Fooˇ
3392 "});
3393 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3394 cx.assert_editor_state(indoc! {"
3395 // Foo
3396 ˇ
3397 "});
3398}
3399
3400#[gpui::test]
3401async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3402 init_test(cx, |settings| {
3403 settings.defaults.tab_size = NonZeroU32::new(4)
3404 });
3405
3406 let language = Arc::new(Language::new(
3407 LanguageConfig {
3408 line_comments: vec!["// ".into(), "/// ".into()],
3409 ..LanguageConfig::default()
3410 },
3411 None,
3412 ));
3413 {
3414 let mut cx = EditorTestContext::new(cx).await;
3415 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3416 cx.set_state(indoc! {"
3417 //ˇ
3418 "});
3419 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3420 cx.assert_editor_state(indoc! {"
3421 //
3422 // ˇ
3423 "});
3424
3425 cx.set_state(indoc! {"
3426 ///ˇ
3427 "});
3428 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3429 cx.assert_editor_state(indoc! {"
3430 ///
3431 /// ˇ
3432 "});
3433 }
3434}
3435
3436#[gpui::test]
3437async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3438 init_test(cx, |settings| {
3439 settings.defaults.tab_size = NonZeroU32::new(4)
3440 });
3441
3442 let language = Arc::new(
3443 Language::new(
3444 LanguageConfig {
3445 documentation_comment: Some(language::BlockCommentConfig {
3446 start: "/**".into(),
3447 end: "*/".into(),
3448 prefix: "* ".into(),
3449 tab_size: 1,
3450 }),
3451
3452 ..LanguageConfig::default()
3453 },
3454 Some(tree_sitter_rust::LANGUAGE.into()),
3455 )
3456 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3457 .unwrap(),
3458 );
3459
3460 {
3461 let mut cx = EditorTestContext::new(cx).await;
3462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3463 cx.set_state(indoc! {"
3464 /**ˇ
3465 "});
3466
3467 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3468 cx.assert_editor_state(indoc! {"
3469 /**
3470 * ˇ
3471 "});
3472 // Ensure that if cursor is before the comment start,
3473 // we do not actually insert a comment prefix.
3474 cx.set_state(indoc! {"
3475 ˇ/**
3476 "});
3477 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479
3480 ˇ/**
3481 "});
3482 // Ensure that if cursor is between it doesn't add comment prefix.
3483 cx.set_state(indoc! {"
3484 /*ˇ*
3485 "});
3486 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3487 cx.assert_editor_state(indoc! {"
3488 /*
3489 ˇ*
3490 "});
3491 // Ensure that if suffix exists on same line after cursor it adds new line.
3492 cx.set_state(indoc! {"
3493 /**ˇ*/
3494 "});
3495 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 /**
3498 * ˇ
3499 */
3500 "});
3501 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3502 cx.set_state(indoc! {"
3503 /**ˇ */
3504 "});
3505 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3506 cx.assert_editor_state(indoc! {"
3507 /**
3508 * ˇ
3509 */
3510 "});
3511 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3512 cx.set_state(indoc! {"
3513 /** ˇ*/
3514 "});
3515 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3516 cx.assert_editor_state(
3517 indoc! {"
3518 /**s
3519 * ˇ
3520 */
3521 "}
3522 .replace("s", " ") // s is used as space placeholder to prevent format on save
3523 .as_str(),
3524 );
3525 // Ensure that delimiter space is preserved when newline on already
3526 // spaced delimiter.
3527 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3528 cx.assert_editor_state(
3529 indoc! {"
3530 /**s
3531 *s
3532 * ˇ
3533 */
3534 "}
3535 .replace("s", " ") // s is used as space placeholder to prevent format on save
3536 .as_str(),
3537 );
3538 // Ensure that delimiter space is preserved when space is not
3539 // on existing delimiter.
3540 cx.set_state(indoc! {"
3541 /**
3542 *ˇ
3543 */
3544 "});
3545 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 /**
3548 *
3549 * ˇ
3550 */
3551 "});
3552 // Ensure that if suffix exists on same line after cursor it
3553 // doesn't add extra new line if prefix is not on same line.
3554 cx.set_state(indoc! {"
3555 /**
3556 ˇ*/
3557 "});
3558 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3559 cx.assert_editor_state(indoc! {"
3560 /**
3561
3562 ˇ*/
3563 "});
3564 // Ensure that it detects suffix after existing prefix.
3565 cx.set_state(indoc! {"
3566 /**ˇ/
3567 "});
3568 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3569 cx.assert_editor_state(indoc! {"
3570 /**
3571 ˇ/
3572 "});
3573 // Ensure that if suffix exists on same line before
3574 // cursor it does not add comment prefix.
3575 cx.set_state(indoc! {"
3576 /** */ˇ
3577 "});
3578 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3579 cx.assert_editor_state(indoc! {"
3580 /** */
3581 ˇ
3582 "});
3583 // Ensure that if suffix exists on same line before
3584 // cursor it does not add comment prefix.
3585 cx.set_state(indoc! {"
3586 /**
3587 *
3588 */ˇ
3589 "});
3590 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3591 cx.assert_editor_state(indoc! {"
3592 /**
3593 *
3594 */
3595 ˇ
3596 "});
3597
3598 // Ensure that inline comment followed by code
3599 // doesn't add comment prefix on newline
3600 cx.set_state(indoc! {"
3601 /** */ textˇ
3602 "});
3603 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3604 cx.assert_editor_state(indoc! {"
3605 /** */ text
3606 ˇ
3607 "});
3608
3609 // Ensure that text after comment end tag
3610 // doesn't add comment prefix on newline
3611 cx.set_state(indoc! {"
3612 /**
3613 *
3614 */ˇtext
3615 "});
3616 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3617 cx.assert_editor_state(indoc! {"
3618 /**
3619 *
3620 */
3621 ˇtext
3622 "});
3623
3624 // Ensure if not comment block it doesn't
3625 // add comment prefix on newline
3626 cx.set_state(indoc! {"
3627 * textˇ
3628 "});
3629 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3630 cx.assert_editor_state(indoc! {"
3631 * text
3632 ˇ
3633 "});
3634 }
3635 // Ensure that comment continuations can be disabled.
3636 update_test_language_settings(cx, |settings| {
3637 settings.defaults.extend_comment_on_newline = Some(false);
3638 });
3639 let mut cx = EditorTestContext::new(cx).await;
3640 cx.set_state(indoc! {"
3641 /**ˇ
3642 "});
3643 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3644 cx.assert_editor_state(indoc! {"
3645 /**
3646 ˇ
3647 "});
3648}
3649
3650#[gpui::test]
3651async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3652 init_test(cx, |settings| {
3653 settings.defaults.tab_size = NonZeroU32::new(4)
3654 });
3655
3656 let lua_language = Arc::new(Language::new(
3657 LanguageConfig {
3658 line_comments: vec!["--".into()],
3659 block_comment: Some(language::BlockCommentConfig {
3660 start: "--[[".into(),
3661 prefix: "".into(),
3662 end: "]]".into(),
3663 tab_size: 0,
3664 }),
3665 ..LanguageConfig::default()
3666 },
3667 None,
3668 ));
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3672
3673 // Line with line comment should extend
3674 cx.set_state(indoc! {"
3675 --ˇ
3676 "});
3677 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3678 cx.assert_editor_state(indoc! {"
3679 --
3680 --ˇ
3681 "});
3682
3683 // Line with block comment that matches line comment should not extend
3684 cx.set_state(indoc! {"
3685 --[[ˇ
3686 "});
3687 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 --[[
3690 ˇ
3691 "});
3692}
3693
3694#[gpui::test]
3695fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3696 init_test(cx, |_| {});
3697
3698 let editor = cx.add_window(|window, cx| {
3699 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3700 let mut editor = build_editor(buffer, window, cx);
3701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3702 s.select_ranges([3..4, 11..12, 19..20])
3703 });
3704 editor
3705 });
3706
3707 _ = editor.update(cx, |editor, window, cx| {
3708 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3709 editor.buffer.update(cx, |buffer, cx| {
3710 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3711 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3712 });
3713 assert_eq!(
3714 editor.selections.ranges(&editor.display_snapshot(cx)),
3715 &[2..2, 7..7, 12..12],
3716 );
3717
3718 editor.insert("Z", window, cx);
3719 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3720
3721 // The selections are moved after the inserted characters
3722 assert_eq!(
3723 editor.selections.ranges(&editor.display_snapshot(cx)),
3724 &[3..3, 9..9, 15..15],
3725 );
3726 });
3727}
3728
3729#[gpui::test]
3730async fn test_tab(cx: &mut TestAppContext) {
3731 init_test(cx, |settings| {
3732 settings.defaults.tab_size = NonZeroU32::new(3)
3733 });
3734
3735 let mut cx = EditorTestContext::new(cx).await;
3736 cx.set_state(indoc! {"
3737 ˇabˇc
3738 ˇ🏀ˇ🏀ˇefg
3739 dˇ
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 ˇab ˇc
3744 ˇ🏀 ˇ🏀 ˇefg
3745 d ˇ
3746 "});
3747
3748 cx.set_state(indoc! {"
3749 a
3750 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3751 "});
3752 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3753 cx.assert_editor_state(indoc! {"
3754 a
3755 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3756 "});
3757}
3758
3759#[gpui::test]
3760async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3761 init_test(cx, |_| {});
3762
3763 let mut cx = EditorTestContext::new(cx).await;
3764 let language = Arc::new(
3765 Language::new(
3766 LanguageConfig::default(),
3767 Some(tree_sitter_rust::LANGUAGE.into()),
3768 )
3769 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3770 .unwrap(),
3771 );
3772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3773
3774 // test when all cursors are not at suggested indent
3775 // then simply move to their suggested indent location
3776 cx.set_state(indoc! {"
3777 const a: B = (
3778 c(
3779 ˇ
3780 ˇ )
3781 );
3782 "});
3783 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3784 cx.assert_editor_state(indoc! {"
3785 const a: B = (
3786 c(
3787 ˇ
3788 ˇ)
3789 );
3790 "});
3791
3792 // test cursor already at suggested indent not moving when
3793 // other cursors are yet to reach their suggested indents
3794 cx.set_state(indoc! {"
3795 ˇ
3796 const a: B = (
3797 c(
3798 d(
3799 ˇ
3800 )
3801 ˇ
3802 ˇ )
3803 );
3804 "});
3805 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 ˇ
3808 const a: B = (
3809 c(
3810 d(
3811 ˇ
3812 )
3813 ˇ
3814 ˇ)
3815 );
3816 "});
3817 // test when all cursors are at suggested indent then tab is inserted
3818 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 ˇ
3821 const a: B = (
3822 c(
3823 d(
3824 ˇ
3825 )
3826 ˇ
3827 ˇ)
3828 );
3829 "});
3830
3831 // test when current indent is less than suggested indent,
3832 // we adjust line to match suggested indent and move cursor to it
3833 //
3834 // when no other cursor is at word boundary, all of them should move
3835 cx.set_state(indoc! {"
3836 const a: B = (
3837 c(
3838 d(
3839 ˇ
3840 ˇ )
3841 ˇ )
3842 );
3843 "});
3844 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3845 cx.assert_editor_state(indoc! {"
3846 const a: B = (
3847 c(
3848 d(
3849 ˇ
3850 ˇ)
3851 ˇ)
3852 );
3853 "});
3854
3855 // test when current indent is less than suggested indent,
3856 // we adjust line to match suggested indent and move cursor to it
3857 //
3858 // when some other cursor is at word boundary, it should not move
3859 cx.set_state(indoc! {"
3860 const a: B = (
3861 c(
3862 d(
3863 ˇ
3864 ˇ )
3865 ˇ)
3866 );
3867 "});
3868 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3869 cx.assert_editor_state(indoc! {"
3870 const a: B = (
3871 c(
3872 d(
3873 ˇ
3874 ˇ)
3875 ˇ)
3876 );
3877 "});
3878
3879 // test when current indent is more than suggested indent,
3880 // we just move cursor to current indent instead of suggested indent
3881 //
3882 // when no other cursor is at word boundary, all of them should move
3883 cx.set_state(indoc! {"
3884 const a: B = (
3885 c(
3886 d(
3887 ˇ
3888 ˇ )
3889 ˇ )
3890 );
3891 "});
3892 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3893 cx.assert_editor_state(indoc! {"
3894 const a: B = (
3895 c(
3896 d(
3897 ˇ
3898 ˇ)
3899 ˇ)
3900 );
3901 "});
3902 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3903 cx.assert_editor_state(indoc! {"
3904 const a: B = (
3905 c(
3906 d(
3907 ˇ
3908 ˇ)
3909 ˇ)
3910 );
3911 "});
3912
3913 // test when current indent is more than suggested indent,
3914 // we just move cursor to current indent instead of suggested indent
3915 //
3916 // when some other cursor is at word boundary, it doesn't move
3917 cx.set_state(indoc! {"
3918 const a: B = (
3919 c(
3920 d(
3921 ˇ
3922 ˇ )
3923 ˇ)
3924 );
3925 "});
3926 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3927 cx.assert_editor_state(indoc! {"
3928 const a: B = (
3929 c(
3930 d(
3931 ˇ
3932 ˇ)
3933 ˇ)
3934 );
3935 "});
3936
3937 // handle auto-indent when there are multiple cursors on the same line
3938 cx.set_state(indoc! {"
3939 const a: B = (
3940 c(
3941 ˇ ˇ
3942 ˇ )
3943 );
3944 "});
3945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3946 cx.assert_editor_state(indoc! {"
3947 const a: B = (
3948 c(
3949 ˇ
3950 ˇ)
3951 );
3952 "});
3953}
3954
3955#[gpui::test]
3956async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3957 init_test(cx, |settings| {
3958 settings.defaults.tab_size = NonZeroU32::new(3)
3959 });
3960
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state(indoc! {"
3963 ˇ
3964 \t ˇ
3965 \t ˇ
3966 \t ˇ
3967 \t \t\t \t \t\t \t\t \t \t ˇ
3968 "});
3969
3970 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3971 cx.assert_editor_state(indoc! {"
3972 ˇ
3973 \t ˇ
3974 \t ˇ
3975 \t ˇ
3976 \t \t\t \t \t\t \t\t \t \t ˇ
3977 "});
3978}
3979
3980#[gpui::test]
3981async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3982 init_test(cx, |settings| {
3983 settings.defaults.tab_size = NonZeroU32::new(4)
3984 });
3985
3986 let language = Arc::new(
3987 Language::new(
3988 LanguageConfig::default(),
3989 Some(tree_sitter_rust::LANGUAGE.into()),
3990 )
3991 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3992 .unwrap(),
3993 );
3994
3995 let mut cx = EditorTestContext::new(cx).await;
3996 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3997 cx.set_state(indoc! {"
3998 fn a() {
3999 if b {
4000 \t ˇc
4001 }
4002 }
4003 "});
4004
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 fn a() {
4008 if b {
4009 ˇc
4010 }
4011 }
4012 "});
4013}
4014
4015#[gpui::test]
4016async fn test_indent_outdent(cx: &mut TestAppContext) {
4017 init_test(cx, |settings| {
4018 settings.defaults.tab_size = NonZeroU32::new(4);
4019 });
4020
4021 let mut cx = EditorTestContext::new(cx).await;
4022
4023 cx.set_state(indoc! {"
4024 «oneˇ» «twoˇ»
4025 three
4026 four
4027 "});
4028 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4029 cx.assert_editor_state(indoc! {"
4030 «oneˇ» «twoˇ»
4031 three
4032 four
4033 "});
4034
4035 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4036 cx.assert_editor_state(indoc! {"
4037 «oneˇ» «twoˇ»
4038 three
4039 four
4040 "});
4041
4042 // select across line ending
4043 cx.set_state(indoc! {"
4044 one two
4045 t«hree
4046 ˇ» four
4047 "});
4048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 t«hree
4052 ˇ» four
4053 "});
4054
4055 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4056 cx.assert_editor_state(indoc! {"
4057 one two
4058 t«hree
4059 ˇ» four
4060 "});
4061
4062 // Ensure that indenting/outdenting works when the cursor is at column 0.
4063 cx.set_state(indoc! {"
4064 one two
4065 ˇthree
4066 four
4067 "});
4068 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4069 cx.assert_editor_state(indoc! {"
4070 one two
4071 ˇthree
4072 four
4073 "});
4074
4075 cx.set_state(indoc! {"
4076 one two
4077 ˇ three
4078 four
4079 "});
4080 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4081 cx.assert_editor_state(indoc! {"
4082 one two
4083 ˇthree
4084 four
4085 "});
4086}
4087
4088#[gpui::test]
4089async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4090 // This is a regression test for issue #33761
4091 init_test(cx, |_| {});
4092
4093 let mut cx = EditorTestContext::new(cx).await;
4094 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4095 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4096
4097 cx.set_state(
4098 r#"ˇ# ingress:
4099ˇ# api:
4100ˇ# enabled: false
4101ˇ# pathType: Prefix
4102ˇ# console:
4103ˇ# enabled: false
4104ˇ# pathType: Prefix
4105"#,
4106 );
4107
4108 // Press tab to indent all lines
4109 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4110
4111 cx.assert_editor_state(
4112 r#" ˇ# ingress:
4113 ˇ# api:
4114 ˇ# enabled: false
4115 ˇ# pathType: Prefix
4116 ˇ# console:
4117 ˇ# enabled: false
4118 ˇ# pathType: Prefix
4119"#,
4120 );
4121}
4122
4123#[gpui::test]
4124async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4125 // This is a test to make sure our fix for issue #33761 didn't break anything
4126 init_test(cx, |_| {});
4127
4128 let mut cx = EditorTestContext::new(cx).await;
4129 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4130 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4131
4132 cx.set_state(
4133 r#"ˇingress:
4134ˇ api:
4135ˇ enabled: false
4136ˇ pathType: Prefix
4137"#,
4138 );
4139
4140 // Press tab to indent all lines
4141 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4142
4143 cx.assert_editor_state(
4144 r#"ˇingress:
4145 ˇapi:
4146 ˇenabled: false
4147 ˇpathType: Prefix
4148"#,
4149 );
4150}
4151
4152#[gpui::test]
4153async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4154 init_test(cx, |settings| {
4155 settings.defaults.hard_tabs = Some(true);
4156 });
4157
4158 let mut cx = EditorTestContext::new(cx).await;
4159
4160 // select two ranges on one line
4161 cx.set_state(indoc! {"
4162 «oneˇ» «twoˇ»
4163 three
4164 four
4165 "});
4166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4167 cx.assert_editor_state(indoc! {"
4168 \t«oneˇ» «twoˇ»
4169 three
4170 four
4171 "});
4172 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 \t\t«oneˇ» «twoˇ»
4175 three
4176 four
4177 "});
4178 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4179 cx.assert_editor_state(indoc! {"
4180 \t«oneˇ» «twoˇ»
4181 three
4182 four
4183 "});
4184 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4185 cx.assert_editor_state(indoc! {"
4186 «oneˇ» «twoˇ»
4187 three
4188 four
4189 "});
4190
4191 // select across a line ending
4192 cx.set_state(indoc! {"
4193 one two
4194 t«hree
4195 ˇ»four
4196 "});
4197 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 one two
4200 \tt«hree
4201 ˇ»four
4202 "});
4203 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4204 cx.assert_editor_state(indoc! {"
4205 one two
4206 \t\tt«hree
4207 ˇ»four
4208 "});
4209 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4210 cx.assert_editor_state(indoc! {"
4211 one two
4212 \tt«hree
4213 ˇ»four
4214 "});
4215 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4216 cx.assert_editor_state(indoc! {"
4217 one two
4218 t«hree
4219 ˇ»four
4220 "});
4221
4222 // Ensure that indenting/outdenting works when the cursor is at column 0.
4223 cx.set_state(indoc! {"
4224 one two
4225 ˇthree
4226 four
4227 "});
4228 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4229 cx.assert_editor_state(indoc! {"
4230 one two
4231 ˇthree
4232 four
4233 "});
4234 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4235 cx.assert_editor_state(indoc! {"
4236 one two
4237 \tˇthree
4238 four
4239 "});
4240 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4241 cx.assert_editor_state(indoc! {"
4242 one two
4243 ˇthree
4244 four
4245 "});
4246}
4247
4248#[gpui::test]
4249fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4250 init_test(cx, |settings| {
4251 settings.languages.0.extend([
4252 (
4253 "TOML".into(),
4254 LanguageSettingsContent {
4255 tab_size: NonZeroU32::new(2),
4256 ..Default::default()
4257 },
4258 ),
4259 (
4260 "Rust".into(),
4261 LanguageSettingsContent {
4262 tab_size: NonZeroU32::new(4),
4263 ..Default::default()
4264 },
4265 ),
4266 ]);
4267 });
4268
4269 let toml_language = Arc::new(Language::new(
4270 LanguageConfig {
4271 name: "TOML".into(),
4272 ..Default::default()
4273 },
4274 None,
4275 ));
4276 let rust_language = Arc::new(Language::new(
4277 LanguageConfig {
4278 name: "Rust".into(),
4279 ..Default::default()
4280 },
4281 None,
4282 ));
4283
4284 let toml_buffer =
4285 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4286 let rust_buffer =
4287 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4288 let multibuffer = cx.new(|cx| {
4289 let mut multibuffer = MultiBuffer::new(ReadWrite);
4290 multibuffer.push_excerpts(
4291 toml_buffer.clone(),
4292 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4293 cx,
4294 );
4295 multibuffer.push_excerpts(
4296 rust_buffer.clone(),
4297 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4298 cx,
4299 );
4300 multibuffer
4301 });
4302
4303 cx.add_window(|window, cx| {
4304 let mut editor = build_editor(multibuffer, window, cx);
4305
4306 assert_eq!(
4307 editor.text(cx),
4308 indoc! {"
4309 a = 1
4310 b = 2
4311
4312 const c: usize = 3;
4313 "}
4314 );
4315
4316 select_ranges(
4317 &mut editor,
4318 indoc! {"
4319 «aˇ» = 1
4320 b = 2
4321
4322 «const c:ˇ» usize = 3;
4323 "},
4324 window,
4325 cx,
4326 );
4327
4328 editor.tab(&Tab, window, cx);
4329 assert_text_with_selections(
4330 &mut editor,
4331 indoc! {"
4332 «aˇ» = 1
4333 b = 2
4334
4335 «const c:ˇ» usize = 3;
4336 "},
4337 cx,
4338 );
4339 editor.backtab(&Backtab, window, cx);
4340 assert_text_with_selections(
4341 &mut editor,
4342 indoc! {"
4343 «aˇ» = 1
4344 b = 2
4345
4346 «const c:ˇ» usize = 3;
4347 "},
4348 cx,
4349 );
4350
4351 editor
4352 });
4353}
4354
4355#[gpui::test]
4356async fn test_backspace(cx: &mut TestAppContext) {
4357 init_test(cx, |_| {});
4358
4359 let mut cx = EditorTestContext::new(cx).await;
4360
4361 // Basic backspace
4362 cx.set_state(indoc! {"
4363 onˇe two three
4364 fou«rˇ» five six
4365 seven «ˇeight nine
4366 »ten
4367 "});
4368 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4369 cx.assert_editor_state(indoc! {"
4370 oˇe two three
4371 fouˇ five six
4372 seven ˇten
4373 "});
4374
4375 // Test backspace inside and around indents
4376 cx.set_state(indoc! {"
4377 zero
4378 ˇone
4379 ˇtwo
4380 ˇ ˇ ˇ three
4381 ˇ ˇ four
4382 "});
4383 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4384 cx.assert_editor_state(indoc! {"
4385 zero
4386 ˇone
4387 ˇtwo
4388 ˇ threeˇ four
4389 "});
4390}
4391
4392#[gpui::test]
4393async fn test_delete(cx: &mut TestAppContext) {
4394 init_test(cx, |_| {});
4395
4396 let mut cx = EditorTestContext::new(cx).await;
4397 cx.set_state(indoc! {"
4398 onˇe two three
4399 fou«rˇ» five six
4400 seven «ˇeight nine
4401 »ten
4402 "});
4403 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4404 cx.assert_editor_state(indoc! {"
4405 onˇ two three
4406 fouˇ five six
4407 seven ˇten
4408 "});
4409}
4410
4411#[gpui::test]
4412fn test_delete_line(cx: &mut TestAppContext) {
4413 init_test(cx, |_| {});
4414
4415 let editor = cx.add_window(|window, cx| {
4416 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4417 build_editor(buffer, window, cx)
4418 });
4419 _ = editor.update(cx, |editor, window, cx| {
4420 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4421 s.select_display_ranges([
4422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4423 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4424 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4425 ])
4426 });
4427 editor.delete_line(&DeleteLine, window, cx);
4428 assert_eq!(editor.display_text(cx), "ghi");
4429 assert_eq!(
4430 display_ranges(editor, cx),
4431 vec![
4432 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4433 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4434 ]
4435 );
4436 });
4437
4438 let editor = cx.add_window(|window, cx| {
4439 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4440 build_editor(buffer, window, cx)
4441 });
4442 _ = editor.update(cx, |editor, window, cx| {
4443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4444 s.select_display_ranges([
4445 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4446 ])
4447 });
4448 editor.delete_line(&DeleteLine, window, cx);
4449 assert_eq!(editor.display_text(cx), "ghi\n");
4450 assert_eq!(
4451 display_ranges(editor, cx),
4452 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4453 );
4454 });
4455
4456 let editor = cx.add_window(|window, cx| {
4457 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4458 build_editor(buffer, window, cx)
4459 });
4460 _ = editor.update(cx, |editor, window, cx| {
4461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4462 s.select_display_ranges([
4463 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4464 ])
4465 });
4466 editor.delete_line(&DeleteLine, window, cx);
4467 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4468 assert_eq!(
4469 display_ranges(editor, cx),
4470 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4471 );
4472 });
4473}
4474
4475#[gpui::test]
4476fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4477 init_test(cx, |_| {});
4478
4479 cx.add_window(|window, cx| {
4480 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4481 let mut editor = build_editor(buffer.clone(), window, cx);
4482 let buffer = buffer.read(cx).as_singleton().unwrap();
4483
4484 assert_eq!(
4485 editor
4486 .selections
4487 .ranges::<Point>(&editor.display_snapshot(cx)),
4488 &[Point::new(0, 0)..Point::new(0, 0)]
4489 );
4490
4491 // When on single line, replace newline at end by space
4492 editor.join_lines(&JoinLines, window, cx);
4493 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4494 assert_eq!(
4495 editor
4496 .selections
4497 .ranges::<Point>(&editor.display_snapshot(cx)),
4498 &[Point::new(0, 3)..Point::new(0, 3)]
4499 );
4500
4501 // When multiple lines are selected, remove newlines that are spanned by the selection
4502 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4503 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4504 });
4505 editor.join_lines(&JoinLines, window, cx);
4506 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4507 assert_eq!(
4508 editor
4509 .selections
4510 .ranges::<Point>(&editor.display_snapshot(cx)),
4511 &[Point::new(0, 11)..Point::new(0, 11)]
4512 );
4513
4514 // Undo should be transactional
4515 editor.undo(&Undo, window, cx);
4516 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4517 assert_eq!(
4518 editor
4519 .selections
4520 .ranges::<Point>(&editor.display_snapshot(cx)),
4521 &[Point::new(0, 5)..Point::new(2, 2)]
4522 );
4523
4524 // When joining an empty line don't insert a space
4525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4526 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4527 });
4528 editor.join_lines(&JoinLines, window, cx);
4529 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4530 assert_eq!(
4531 editor
4532 .selections
4533 .ranges::<Point>(&editor.display_snapshot(cx)),
4534 [Point::new(2, 3)..Point::new(2, 3)]
4535 );
4536
4537 // We can remove trailing newlines
4538 editor.join_lines(&JoinLines, window, cx);
4539 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4540 assert_eq!(
4541 editor
4542 .selections
4543 .ranges::<Point>(&editor.display_snapshot(cx)),
4544 [Point::new(2, 3)..Point::new(2, 3)]
4545 );
4546
4547 // We don't blow up on the last line
4548 editor.join_lines(&JoinLines, window, cx);
4549 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4550 assert_eq!(
4551 editor
4552 .selections
4553 .ranges::<Point>(&editor.display_snapshot(cx)),
4554 [Point::new(2, 3)..Point::new(2, 3)]
4555 );
4556
4557 // reset to test indentation
4558 editor.buffer.update(cx, |buffer, cx| {
4559 buffer.edit(
4560 [
4561 (Point::new(1, 0)..Point::new(1, 2), " "),
4562 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4563 ],
4564 None,
4565 cx,
4566 )
4567 });
4568
4569 // We remove any leading spaces
4570 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4571 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4572 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4573 });
4574 editor.join_lines(&JoinLines, window, cx);
4575 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4576
4577 // We don't insert a space for a line containing only spaces
4578 editor.join_lines(&JoinLines, window, cx);
4579 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4580
4581 // We ignore any leading tabs
4582 editor.join_lines(&JoinLines, window, cx);
4583 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4584
4585 editor
4586 });
4587}
4588
4589#[gpui::test]
4590fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4591 init_test(cx, |_| {});
4592
4593 cx.add_window(|window, cx| {
4594 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4595 let mut editor = build_editor(buffer.clone(), window, cx);
4596 let buffer = buffer.read(cx).as_singleton().unwrap();
4597
4598 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4599 s.select_ranges([
4600 Point::new(0, 2)..Point::new(1, 1),
4601 Point::new(1, 2)..Point::new(1, 2),
4602 Point::new(3, 1)..Point::new(3, 2),
4603 ])
4604 });
4605
4606 editor.join_lines(&JoinLines, window, cx);
4607 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4608
4609 assert_eq!(
4610 editor
4611 .selections
4612 .ranges::<Point>(&editor.display_snapshot(cx)),
4613 [
4614 Point::new(0, 7)..Point::new(0, 7),
4615 Point::new(1, 3)..Point::new(1, 3)
4616 ]
4617 );
4618 editor
4619 });
4620}
4621
4622#[gpui::test]
4623async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4624 init_test(cx, |_| {});
4625
4626 let mut cx = EditorTestContext::new(cx).await;
4627
4628 let diff_base = r#"
4629 Line 0
4630 Line 1
4631 Line 2
4632 Line 3
4633 "#
4634 .unindent();
4635
4636 cx.set_state(
4637 &r#"
4638 ˇLine 0
4639 Line 1
4640 Line 2
4641 Line 3
4642 "#
4643 .unindent(),
4644 );
4645
4646 cx.set_head_text(&diff_base);
4647 executor.run_until_parked();
4648
4649 // Join lines
4650 cx.update_editor(|editor, window, cx| {
4651 editor.join_lines(&JoinLines, window, cx);
4652 });
4653 executor.run_until_parked();
4654
4655 cx.assert_editor_state(
4656 &r#"
4657 Line 0ˇ Line 1
4658 Line 2
4659 Line 3
4660 "#
4661 .unindent(),
4662 );
4663 // Join again
4664 cx.update_editor(|editor, window, cx| {
4665 editor.join_lines(&JoinLines, window, cx);
4666 });
4667 executor.run_until_parked();
4668
4669 cx.assert_editor_state(
4670 &r#"
4671 Line 0 Line 1ˇ Line 2
4672 Line 3
4673 "#
4674 .unindent(),
4675 );
4676}
4677
4678#[gpui::test]
4679async fn test_custom_newlines_cause_no_false_positive_diffs(
4680 executor: BackgroundExecutor,
4681 cx: &mut TestAppContext,
4682) {
4683 init_test(cx, |_| {});
4684 let mut cx = EditorTestContext::new(cx).await;
4685 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4686 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4687 executor.run_until_parked();
4688
4689 cx.update_editor(|editor, window, cx| {
4690 let snapshot = editor.snapshot(window, cx);
4691 assert_eq!(
4692 snapshot
4693 .buffer_snapshot()
4694 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4695 .collect::<Vec<_>>(),
4696 Vec::new(),
4697 "Should not have any diffs for files with custom newlines"
4698 );
4699 });
4700}
4701
4702#[gpui::test]
4703async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4704 init_test(cx, |_| {});
4705
4706 let mut cx = EditorTestContext::new(cx).await;
4707
4708 // Test sort_lines_case_insensitive()
4709 cx.set_state(indoc! {"
4710 «z
4711 y
4712 x
4713 Z
4714 Y
4715 Xˇ»
4716 "});
4717 cx.update_editor(|e, window, cx| {
4718 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4719 });
4720 cx.assert_editor_state(indoc! {"
4721 «x
4722 X
4723 y
4724 Y
4725 z
4726 Zˇ»
4727 "});
4728
4729 // Test sort_lines_by_length()
4730 //
4731 // Demonstrates:
4732 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4733 // - sort is stable
4734 cx.set_state(indoc! {"
4735 «123
4736 æ
4737 12
4738 ∞
4739 1
4740 æˇ»
4741 "});
4742 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4743 cx.assert_editor_state(indoc! {"
4744 «æ
4745 ∞
4746 1
4747 æ
4748 12
4749 123ˇ»
4750 "});
4751
4752 // Test reverse_lines()
4753 cx.set_state(indoc! {"
4754 «5
4755 4
4756 3
4757 2
4758 1ˇ»
4759 "});
4760 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4761 cx.assert_editor_state(indoc! {"
4762 «1
4763 2
4764 3
4765 4
4766 5ˇ»
4767 "});
4768
4769 // Skip testing shuffle_line()
4770
4771 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4772 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4773
4774 // Don't manipulate when cursor is on single line, but expand the selection
4775 cx.set_state(indoc! {"
4776 ddˇdd
4777 ccc
4778 bb
4779 a
4780 "});
4781 cx.update_editor(|e, window, cx| {
4782 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4783 });
4784 cx.assert_editor_state(indoc! {"
4785 «ddddˇ»
4786 ccc
4787 bb
4788 a
4789 "});
4790
4791 // Basic manipulate case
4792 // Start selection moves to column 0
4793 // End of selection shrinks to fit shorter line
4794 cx.set_state(indoc! {"
4795 dd«d
4796 ccc
4797 bb
4798 aaaaaˇ»
4799 "});
4800 cx.update_editor(|e, window, cx| {
4801 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4802 });
4803 cx.assert_editor_state(indoc! {"
4804 «aaaaa
4805 bb
4806 ccc
4807 dddˇ»
4808 "});
4809
4810 // Manipulate case with newlines
4811 cx.set_state(indoc! {"
4812 dd«d
4813 ccc
4814
4815 bb
4816 aaaaa
4817
4818 ˇ»
4819 "});
4820 cx.update_editor(|e, window, cx| {
4821 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4822 });
4823 cx.assert_editor_state(indoc! {"
4824 «
4825
4826 aaaaa
4827 bb
4828 ccc
4829 dddˇ»
4830
4831 "});
4832
4833 // Adding new line
4834 cx.set_state(indoc! {"
4835 aa«a
4836 bbˇ»b
4837 "});
4838 cx.update_editor(|e, window, cx| {
4839 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4840 });
4841 cx.assert_editor_state(indoc! {"
4842 «aaa
4843 bbb
4844 added_lineˇ»
4845 "});
4846
4847 // Removing line
4848 cx.set_state(indoc! {"
4849 aa«a
4850 bbbˇ»
4851 "});
4852 cx.update_editor(|e, window, cx| {
4853 e.manipulate_immutable_lines(window, cx, |lines| {
4854 lines.pop();
4855 })
4856 });
4857 cx.assert_editor_state(indoc! {"
4858 «aaaˇ»
4859 "});
4860
4861 // Removing all lines
4862 cx.set_state(indoc! {"
4863 aa«a
4864 bbbˇ»
4865 "});
4866 cx.update_editor(|e, window, cx| {
4867 e.manipulate_immutable_lines(window, cx, |lines| {
4868 lines.drain(..);
4869 })
4870 });
4871 cx.assert_editor_state(indoc! {"
4872 ˇ
4873 "});
4874}
4875
4876#[gpui::test]
4877async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879
4880 let mut cx = EditorTestContext::new(cx).await;
4881
4882 // Consider continuous selection as single selection
4883 cx.set_state(indoc! {"
4884 Aaa«aa
4885 cˇ»c«c
4886 bb
4887 aaaˇ»aa
4888 "});
4889 cx.update_editor(|e, window, cx| {
4890 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4891 });
4892 cx.assert_editor_state(indoc! {"
4893 «Aaaaa
4894 ccc
4895 bb
4896 aaaaaˇ»
4897 "});
4898
4899 cx.set_state(indoc! {"
4900 Aaa«aa
4901 cˇ»c«c
4902 bb
4903 aaaˇ»aa
4904 "});
4905 cx.update_editor(|e, window, cx| {
4906 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4907 });
4908 cx.assert_editor_state(indoc! {"
4909 «Aaaaa
4910 ccc
4911 bbˇ»
4912 "});
4913
4914 // Consider non continuous selection as distinct dedup operations
4915 cx.set_state(indoc! {"
4916 «aaaaa
4917 bb
4918 aaaaa
4919 aaaaaˇ»
4920
4921 aaa«aaˇ»
4922 "});
4923 cx.update_editor(|e, window, cx| {
4924 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4925 });
4926 cx.assert_editor_state(indoc! {"
4927 «aaaaa
4928 bbˇ»
4929
4930 «aaaaaˇ»
4931 "});
4932}
4933
4934#[gpui::test]
4935async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4936 init_test(cx, |_| {});
4937
4938 let mut cx = EditorTestContext::new(cx).await;
4939
4940 cx.set_state(indoc! {"
4941 «Aaa
4942 aAa
4943 Aaaˇ»
4944 "});
4945 cx.update_editor(|e, window, cx| {
4946 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4947 });
4948 cx.assert_editor_state(indoc! {"
4949 «Aaa
4950 aAaˇ»
4951 "});
4952
4953 cx.set_state(indoc! {"
4954 «Aaa
4955 aAa
4956 aaAˇ»
4957 "});
4958 cx.update_editor(|e, window, cx| {
4959 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4960 });
4961 cx.assert_editor_state(indoc! {"
4962 «Aaaˇ»
4963 "});
4964}
4965
4966#[gpui::test]
4967async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4968 init_test(cx, |_| {});
4969
4970 let mut cx = EditorTestContext::new(cx).await;
4971
4972 let js_language = Arc::new(Language::new(
4973 LanguageConfig {
4974 name: "JavaScript".into(),
4975 wrap_characters: Some(language::WrapCharactersConfig {
4976 start_prefix: "<".into(),
4977 start_suffix: ">".into(),
4978 end_prefix: "</".into(),
4979 end_suffix: ">".into(),
4980 }),
4981 ..LanguageConfig::default()
4982 },
4983 None,
4984 ));
4985
4986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4987
4988 cx.set_state(indoc! {"
4989 «testˇ»
4990 "});
4991 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4992 cx.assert_editor_state(indoc! {"
4993 <«ˇ»>test</«ˇ»>
4994 "});
4995
4996 cx.set_state(indoc! {"
4997 «test
4998 testˇ»
4999 "});
5000 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5001 cx.assert_editor_state(indoc! {"
5002 <«ˇ»>test
5003 test</«ˇ»>
5004 "});
5005
5006 cx.set_state(indoc! {"
5007 teˇst
5008 "});
5009 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5010 cx.assert_editor_state(indoc! {"
5011 te<«ˇ»></«ˇ»>st
5012 "});
5013}
5014
5015#[gpui::test]
5016async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5017 init_test(cx, |_| {});
5018
5019 let mut cx = EditorTestContext::new(cx).await;
5020
5021 let js_language = Arc::new(Language::new(
5022 LanguageConfig {
5023 name: "JavaScript".into(),
5024 wrap_characters: Some(language::WrapCharactersConfig {
5025 start_prefix: "<".into(),
5026 start_suffix: ">".into(),
5027 end_prefix: "</".into(),
5028 end_suffix: ">".into(),
5029 }),
5030 ..LanguageConfig::default()
5031 },
5032 None,
5033 ));
5034
5035 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5036
5037 cx.set_state(indoc! {"
5038 «testˇ»
5039 «testˇ» «testˇ»
5040 «testˇ»
5041 "});
5042 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5043 cx.assert_editor_state(indoc! {"
5044 <«ˇ»>test</«ˇ»>
5045 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5046 <«ˇ»>test</«ˇ»>
5047 "});
5048
5049 cx.set_state(indoc! {"
5050 «test
5051 testˇ»
5052 «test
5053 testˇ»
5054 "});
5055 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5056 cx.assert_editor_state(indoc! {"
5057 <«ˇ»>test
5058 test</«ˇ»>
5059 <«ˇ»>test
5060 test</«ˇ»>
5061 "});
5062}
5063
5064#[gpui::test]
5065async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5066 init_test(cx, |_| {});
5067
5068 let mut cx = EditorTestContext::new(cx).await;
5069
5070 let plaintext_language = Arc::new(Language::new(
5071 LanguageConfig {
5072 name: "Plain Text".into(),
5073 ..LanguageConfig::default()
5074 },
5075 None,
5076 ));
5077
5078 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5079
5080 cx.set_state(indoc! {"
5081 «testˇ»
5082 "});
5083 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5084 cx.assert_editor_state(indoc! {"
5085 «testˇ»
5086 "});
5087}
5088
5089#[gpui::test]
5090async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5091 init_test(cx, |_| {});
5092
5093 let mut cx = EditorTestContext::new(cx).await;
5094
5095 // Manipulate with multiple selections on a single line
5096 cx.set_state(indoc! {"
5097 dd«dd
5098 cˇ»c«c
5099 bb
5100 aaaˇ»aa
5101 "});
5102 cx.update_editor(|e, window, cx| {
5103 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5104 });
5105 cx.assert_editor_state(indoc! {"
5106 «aaaaa
5107 bb
5108 ccc
5109 ddddˇ»
5110 "});
5111
5112 // Manipulate with multiple disjoin selections
5113 cx.set_state(indoc! {"
5114 5«
5115 4
5116 3
5117 2
5118 1ˇ»
5119
5120 dd«dd
5121 ccc
5122 bb
5123 aaaˇ»aa
5124 "});
5125 cx.update_editor(|e, window, cx| {
5126 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5127 });
5128 cx.assert_editor_state(indoc! {"
5129 «1
5130 2
5131 3
5132 4
5133 5ˇ»
5134
5135 «aaaaa
5136 bb
5137 ccc
5138 ddddˇ»
5139 "});
5140
5141 // Adding lines on each selection
5142 cx.set_state(indoc! {"
5143 2«
5144 1ˇ»
5145
5146 bb«bb
5147 aaaˇ»aa
5148 "});
5149 cx.update_editor(|e, window, cx| {
5150 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5151 });
5152 cx.assert_editor_state(indoc! {"
5153 «2
5154 1
5155 added lineˇ»
5156
5157 «bbbb
5158 aaaaa
5159 added lineˇ»
5160 "});
5161
5162 // Removing lines on each selection
5163 cx.set_state(indoc! {"
5164 2«
5165 1ˇ»
5166
5167 bb«bb
5168 aaaˇ»aa
5169 "});
5170 cx.update_editor(|e, window, cx| {
5171 e.manipulate_immutable_lines(window, cx, |lines| {
5172 lines.pop();
5173 })
5174 });
5175 cx.assert_editor_state(indoc! {"
5176 «2ˇ»
5177
5178 «bbbbˇ»
5179 "});
5180}
5181
5182#[gpui::test]
5183async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5184 init_test(cx, |settings| {
5185 settings.defaults.tab_size = NonZeroU32::new(3)
5186 });
5187
5188 let mut cx = EditorTestContext::new(cx).await;
5189
5190 // MULTI SELECTION
5191 // Ln.1 "«" tests empty lines
5192 // Ln.9 tests just leading whitespace
5193 cx.set_state(indoc! {"
5194 «
5195 abc // No indentationˇ»
5196 «\tabc // 1 tabˇ»
5197 \t\tabc « ˇ» // 2 tabs
5198 \t ab«c // Tab followed by space
5199 \tabc // Space followed by tab (3 spaces should be the result)
5200 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5201 abˇ»ˇc ˇ ˇ // Already space indented«
5202 \t
5203 \tabc\tdef // Only the leading tab is manipulatedˇ»
5204 "});
5205 cx.update_editor(|e, window, cx| {
5206 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5207 });
5208 cx.assert_editor_state(
5209 indoc! {"
5210 «
5211 abc // No indentation
5212 abc // 1 tab
5213 abc // 2 tabs
5214 abc // Tab followed by space
5215 abc // Space followed by tab (3 spaces should be the result)
5216 abc // Mixed indentation (tab conversion depends on the column)
5217 abc // Already space indented
5218 ·
5219 abc\tdef // Only the leading tab is manipulatedˇ»
5220 "}
5221 .replace("·", "")
5222 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5223 );
5224
5225 // Test on just a few lines, the others should remain unchanged
5226 // Only lines (3, 5, 10, 11) should change
5227 cx.set_state(
5228 indoc! {"
5229 ·
5230 abc // No indentation
5231 \tabcˇ // 1 tab
5232 \t\tabc // 2 tabs
5233 \t abcˇ // Tab followed by space
5234 \tabc // Space followed by tab (3 spaces should be the result)
5235 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5236 abc // Already space indented
5237 «\t
5238 \tabc\tdef // Only the leading tab is manipulatedˇ»
5239 "}
5240 .replace("·", "")
5241 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5242 );
5243 cx.update_editor(|e, window, cx| {
5244 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5245 });
5246 cx.assert_editor_state(
5247 indoc! {"
5248 ·
5249 abc // No indentation
5250 « abc // 1 tabˇ»
5251 \t\tabc // 2 tabs
5252 « abc // Tab followed by spaceˇ»
5253 \tabc // Space followed by tab (3 spaces should be the result)
5254 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5255 abc // Already space indented
5256 « ·
5257 abc\tdef // Only the leading tab is manipulatedˇ»
5258 "}
5259 .replace("·", "")
5260 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5261 );
5262
5263 // SINGLE SELECTION
5264 // Ln.1 "«" tests empty lines
5265 // Ln.9 tests just leading whitespace
5266 cx.set_state(indoc! {"
5267 «
5268 abc // No indentation
5269 \tabc // 1 tab
5270 \t\tabc // 2 tabs
5271 \t abc // Tab followed by space
5272 \tabc // Space followed by tab (3 spaces should be the result)
5273 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5274 abc // Already space indented
5275 \t
5276 \tabc\tdef // Only the leading tab is manipulatedˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| {
5279 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5280 });
5281 cx.assert_editor_state(
5282 indoc! {"
5283 «
5284 abc // No indentation
5285 abc // 1 tab
5286 abc // 2 tabs
5287 abc // Tab followed by space
5288 abc // Space followed by tab (3 spaces should be the result)
5289 abc // Mixed indentation (tab conversion depends on the column)
5290 abc // Already space indented
5291 ·
5292 abc\tdef // Only the leading tab is manipulatedˇ»
5293 "}
5294 .replace("·", "")
5295 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5296 );
5297}
5298
5299#[gpui::test]
5300async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5301 init_test(cx, |settings| {
5302 settings.defaults.tab_size = NonZeroU32::new(3)
5303 });
5304
5305 let mut cx = EditorTestContext::new(cx).await;
5306
5307 // MULTI SELECTION
5308 // Ln.1 "«" tests empty lines
5309 // Ln.11 tests just leading whitespace
5310 cx.set_state(indoc! {"
5311 «
5312 abˇ»ˇc // No indentation
5313 abc ˇ ˇ // 1 space (< 3 so dont convert)
5314 abc « // 2 spaces (< 3 so dont convert)
5315 abc // 3 spaces (convert)
5316 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5317 «\tˇ»\t«\tˇ»abc // Already tab indented
5318 «\t abc // Tab followed by space
5319 \tabc // Space followed by tab (should be consumed due to tab)
5320 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5321 \tˇ» «\t
5322 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5323 "});
5324 cx.update_editor(|e, window, cx| {
5325 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5326 });
5327 cx.assert_editor_state(indoc! {"
5328 «
5329 abc // No indentation
5330 abc // 1 space (< 3 so dont convert)
5331 abc // 2 spaces (< 3 so dont convert)
5332 \tabc // 3 spaces (convert)
5333 \t abc // 5 spaces (1 tab + 2 spaces)
5334 \t\t\tabc // Already tab indented
5335 \t abc // Tab followed by space
5336 \tabc // Space followed by tab (should be consumed due to tab)
5337 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5338 \t\t\t
5339 \tabc \t // Only the leading spaces should be convertedˇ»
5340 "});
5341
5342 // Test on just a few lines, the other should remain unchanged
5343 // Only lines (4, 8, 11, 12) should change
5344 cx.set_state(
5345 indoc! {"
5346 ·
5347 abc // No indentation
5348 abc // 1 space (< 3 so dont convert)
5349 abc // 2 spaces (< 3 so dont convert)
5350 « abc // 3 spaces (convert)ˇ»
5351 abc // 5 spaces (1 tab + 2 spaces)
5352 \t\t\tabc // Already tab indented
5353 \t abc // Tab followed by space
5354 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5355 \t\t \tabc // Mixed indentation
5356 \t \t \t \tabc // Mixed indentation
5357 \t \tˇ
5358 « abc \t // Only the leading spaces should be convertedˇ»
5359 "}
5360 .replace("·", "")
5361 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5362 );
5363 cx.update_editor(|e, window, cx| {
5364 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5365 });
5366 cx.assert_editor_state(
5367 indoc! {"
5368 ·
5369 abc // No indentation
5370 abc // 1 space (< 3 so dont convert)
5371 abc // 2 spaces (< 3 so dont convert)
5372 «\tabc // 3 spaces (convert)ˇ»
5373 abc // 5 spaces (1 tab + 2 spaces)
5374 \t\t\tabc // Already tab indented
5375 \t abc // Tab followed by space
5376 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5377 \t\t \tabc // Mixed indentation
5378 \t \t \t \tabc // Mixed indentation
5379 «\t\t\t
5380 \tabc \t // Only the leading spaces should be convertedˇ»
5381 "}
5382 .replace("·", "")
5383 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5384 );
5385
5386 // SINGLE SELECTION
5387 // Ln.1 "«" tests empty lines
5388 // Ln.11 tests just leading whitespace
5389 cx.set_state(indoc! {"
5390 «
5391 abc // No indentation
5392 abc // 1 space (< 3 so dont convert)
5393 abc // 2 spaces (< 3 so dont convert)
5394 abc // 3 spaces (convert)
5395 abc // 5 spaces (1 tab + 2 spaces)
5396 \t\t\tabc // Already tab indented
5397 \t abc // Tab followed by space
5398 \tabc // Space followed by tab (should be consumed due to tab)
5399 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5400 \t \t
5401 abc \t // Only the leading spaces should be convertedˇ»
5402 "});
5403 cx.update_editor(|e, window, cx| {
5404 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5405 });
5406 cx.assert_editor_state(indoc! {"
5407 «
5408 abc // No indentation
5409 abc // 1 space (< 3 so dont convert)
5410 abc // 2 spaces (< 3 so dont convert)
5411 \tabc // 3 spaces (convert)
5412 \t abc // 5 spaces (1 tab + 2 spaces)
5413 \t\t\tabc // Already tab indented
5414 \t abc // Tab followed by space
5415 \tabc // Space followed by tab (should be consumed due to tab)
5416 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5417 \t\t\t
5418 \tabc \t // Only the leading spaces should be convertedˇ»
5419 "});
5420}
5421
5422#[gpui::test]
5423async fn test_toggle_case(cx: &mut TestAppContext) {
5424 init_test(cx, |_| {});
5425
5426 let mut cx = EditorTestContext::new(cx).await;
5427
5428 // If all lower case -> upper case
5429 cx.set_state(indoc! {"
5430 «hello worldˇ»
5431 "});
5432 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5433 cx.assert_editor_state(indoc! {"
5434 «HELLO WORLDˇ»
5435 "});
5436
5437 // If all upper case -> lower case
5438 cx.set_state(indoc! {"
5439 «HELLO WORLDˇ»
5440 "});
5441 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5442 cx.assert_editor_state(indoc! {"
5443 «hello worldˇ»
5444 "});
5445
5446 // If any upper case characters are identified -> lower case
5447 // This matches JetBrains IDEs
5448 cx.set_state(indoc! {"
5449 «hEllo worldˇ»
5450 "});
5451 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5452 cx.assert_editor_state(indoc! {"
5453 «hello worldˇ»
5454 "});
5455}
5456
5457#[gpui::test]
5458async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5459 init_test(cx, |_| {});
5460
5461 let mut cx = EditorTestContext::new(cx).await;
5462
5463 cx.set_state(indoc! {"
5464 «implement-windows-supportˇ»
5465 "});
5466 cx.update_editor(|e, window, cx| {
5467 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5468 });
5469 cx.assert_editor_state(indoc! {"
5470 «Implement windows supportˇ»
5471 "});
5472}
5473
5474#[gpui::test]
5475async fn test_manipulate_text(cx: &mut TestAppContext) {
5476 init_test(cx, |_| {});
5477
5478 let mut cx = EditorTestContext::new(cx).await;
5479
5480 // Test convert_to_upper_case()
5481 cx.set_state(indoc! {"
5482 «hello worldˇ»
5483 "});
5484 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5485 cx.assert_editor_state(indoc! {"
5486 «HELLO WORLDˇ»
5487 "});
5488
5489 // Test convert_to_lower_case()
5490 cx.set_state(indoc! {"
5491 «HELLO WORLDˇ»
5492 "});
5493 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5494 cx.assert_editor_state(indoc! {"
5495 «hello worldˇ»
5496 "});
5497
5498 // Test multiple line, single selection case
5499 cx.set_state(indoc! {"
5500 «The quick brown
5501 fox jumps over
5502 the lazy dogˇ»
5503 "});
5504 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5505 cx.assert_editor_state(indoc! {"
5506 «The Quick Brown
5507 Fox Jumps Over
5508 The Lazy Dogˇ»
5509 "});
5510
5511 // Test multiple line, single selection case
5512 cx.set_state(indoc! {"
5513 «The quick brown
5514 fox jumps over
5515 the lazy dogˇ»
5516 "});
5517 cx.update_editor(|e, window, cx| {
5518 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5519 });
5520 cx.assert_editor_state(indoc! {"
5521 «TheQuickBrown
5522 FoxJumpsOver
5523 TheLazyDogˇ»
5524 "});
5525
5526 // From here on out, test more complex cases of manipulate_text()
5527
5528 // Test no selection case - should affect words cursors are in
5529 // Cursor at beginning, middle, and end of word
5530 cx.set_state(indoc! {"
5531 ˇhello big beauˇtiful worldˇ
5532 "});
5533 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5536 "});
5537
5538 // Test multiple selections on a single line and across multiple lines
5539 cx.set_state(indoc! {"
5540 «Theˇ» quick «brown
5541 foxˇ» jumps «overˇ»
5542 the «lazyˇ» dog
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «THEˇ» quick «BROWN
5547 FOXˇ» jumps «OVERˇ»
5548 the «LAZYˇ» dog
5549 "});
5550
5551 // Test case where text length grows
5552 cx.set_state(indoc! {"
5553 «tschüߡ»
5554 "});
5555 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5556 cx.assert_editor_state(indoc! {"
5557 «TSCHÜSSˇ»
5558 "});
5559
5560 // Test to make sure we don't crash when text shrinks
5561 cx.set_state(indoc! {"
5562 aaa_bbbˇ
5563 "});
5564 cx.update_editor(|e, window, cx| {
5565 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5566 });
5567 cx.assert_editor_state(indoc! {"
5568 «aaaBbbˇ»
5569 "});
5570
5571 // Test to make sure we all aware of the fact that each word can grow and shrink
5572 // Final selections should be aware of this fact
5573 cx.set_state(indoc! {"
5574 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5575 "});
5576 cx.update_editor(|e, window, cx| {
5577 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5578 });
5579 cx.assert_editor_state(indoc! {"
5580 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5581 "});
5582
5583 cx.set_state(indoc! {"
5584 «hElLo, WoRld!ˇ»
5585 "});
5586 cx.update_editor(|e, window, cx| {
5587 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5588 });
5589 cx.assert_editor_state(indoc! {"
5590 «HeLlO, wOrLD!ˇ»
5591 "});
5592
5593 // Test selections with `line_mode() = true`.
5594 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5595 cx.set_state(indoc! {"
5596 «The quick brown
5597 fox jumps over
5598 tˇ»he lazy dog
5599 "});
5600 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5601 cx.assert_editor_state(indoc! {"
5602 «THE QUICK BROWN
5603 FOX JUMPS OVER
5604 THE LAZY DOGˇ»
5605 "});
5606}
5607
5608#[gpui::test]
5609fn test_duplicate_line(cx: &mut TestAppContext) {
5610 init_test(cx, |_| {});
5611
5612 let editor = cx.add_window(|window, cx| {
5613 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5614 build_editor(buffer, window, cx)
5615 });
5616 _ = editor.update(cx, |editor, window, cx| {
5617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5618 s.select_display_ranges([
5619 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5620 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5621 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5622 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5623 ])
5624 });
5625 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5626 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5627 assert_eq!(
5628 display_ranges(editor, cx),
5629 vec![
5630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5631 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5632 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5633 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5634 ]
5635 );
5636 });
5637
5638 let editor = cx.add_window(|window, cx| {
5639 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5640 build_editor(buffer, window, cx)
5641 });
5642 _ = editor.update(cx, |editor, window, cx| {
5643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5644 s.select_display_ranges([
5645 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5646 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5647 ])
5648 });
5649 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5650 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5651 assert_eq!(
5652 display_ranges(editor, cx),
5653 vec![
5654 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5655 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5656 ]
5657 );
5658 });
5659
5660 // With `duplicate_line_up` the selections move to the duplicated lines,
5661 // which are inserted above the original lines
5662 let editor = cx.add_window(|window, cx| {
5663 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5664 build_editor(buffer, window, cx)
5665 });
5666 _ = editor.update(cx, |editor, window, cx| {
5667 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5668 s.select_display_ranges([
5669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5671 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5672 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5673 ])
5674 });
5675 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5676 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5677 assert_eq!(
5678 display_ranges(editor, cx),
5679 vec![
5680 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5682 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5683 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5684 ]
5685 );
5686 });
5687
5688 let editor = cx.add_window(|window, cx| {
5689 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5690 build_editor(buffer, window, cx)
5691 });
5692 _ = editor.update(cx, |editor, window, cx| {
5693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5694 s.select_display_ranges([
5695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5696 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5697 ])
5698 });
5699 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5700 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5701 assert_eq!(
5702 display_ranges(editor, cx),
5703 vec![
5704 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5705 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5706 ]
5707 );
5708 });
5709
5710 let editor = cx.add_window(|window, cx| {
5711 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5712 build_editor(buffer, window, cx)
5713 });
5714 _ = editor.update(cx, |editor, window, cx| {
5715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5716 s.select_display_ranges([
5717 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5718 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5719 ])
5720 });
5721 editor.duplicate_selection(&DuplicateSelection, window, cx);
5722 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5723 assert_eq!(
5724 display_ranges(editor, cx),
5725 vec![
5726 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5727 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5728 ]
5729 );
5730 });
5731}
5732
5733#[gpui::test]
5734fn test_move_line_up_down(cx: &mut TestAppContext) {
5735 init_test(cx, |_| {});
5736
5737 let editor = cx.add_window(|window, cx| {
5738 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5739 build_editor(buffer, window, cx)
5740 });
5741 _ = editor.update(cx, |editor, window, cx| {
5742 editor.fold_creases(
5743 vec![
5744 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5745 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5746 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5747 ],
5748 true,
5749 window,
5750 cx,
5751 );
5752 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5753 s.select_display_ranges([
5754 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5755 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5756 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5757 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5758 ])
5759 });
5760 assert_eq!(
5761 editor.display_text(cx),
5762 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5763 );
5764
5765 editor.move_line_up(&MoveLineUp, window, cx);
5766 assert_eq!(
5767 editor.display_text(cx),
5768 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5769 );
5770 assert_eq!(
5771 display_ranges(editor, cx),
5772 vec![
5773 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5774 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5775 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5776 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5777 ]
5778 );
5779 });
5780
5781 _ = editor.update(cx, |editor, window, cx| {
5782 editor.move_line_down(&MoveLineDown, window, cx);
5783 assert_eq!(
5784 editor.display_text(cx),
5785 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5786 );
5787 assert_eq!(
5788 display_ranges(editor, cx),
5789 vec![
5790 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5791 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5792 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5793 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5794 ]
5795 );
5796 });
5797
5798 _ = editor.update(cx, |editor, window, cx| {
5799 editor.move_line_down(&MoveLineDown, window, cx);
5800 assert_eq!(
5801 editor.display_text(cx),
5802 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5803 );
5804 assert_eq!(
5805 display_ranges(editor, cx),
5806 vec![
5807 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5808 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5809 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5810 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5811 ]
5812 );
5813 });
5814
5815 _ = editor.update(cx, |editor, window, cx| {
5816 editor.move_line_up(&MoveLineUp, window, cx);
5817 assert_eq!(
5818 editor.display_text(cx),
5819 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5820 );
5821 assert_eq!(
5822 display_ranges(editor, cx),
5823 vec![
5824 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5825 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5826 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5827 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5828 ]
5829 );
5830 });
5831}
5832
5833#[gpui::test]
5834fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5835 init_test(cx, |_| {});
5836 let editor = cx.add_window(|window, cx| {
5837 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5838 build_editor(buffer, window, cx)
5839 });
5840 _ = editor.update(cx, |editor, window, cx| {
5841 editor.fold_creases(
5842 vec![Crease::simple(
5843 Point::new(6, 4)..Point::new(7, 4),
5844 FoldPlaceholder::test(),
5845 )],
5846 true,
5847 window,
5848 cx,
5849 );
5850 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5851 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5852 });
5853 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5854 editor.move_line_up(&MoveLineUp, window, cx);
5855 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5856 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5857 });
5858}
5859
5860#[gpui::test]
5861fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5862 init_test(cx, |_| {});
5863
5864 let editor = cx.add_window(|window, cx| {
5865 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5866 build_editor(buffer, window, cx)
5867 });
5868 _ = editor.update(cx, |editor, window, cx| {
5869 let snapshot = editor.buffer.read(cx).snapshot(cx);
5870 editor.insert_blocks(
5871 [BlockProperties {
5872 style: BlockStyle::Fixed,
5873 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5874 height: Some(1),
5875 render: Arc::new(|_| div().into_any()),
5876 priority: 0,
5877 }],
5878 Some(Autoscroll::fit()),
5879 cx,
5880 );
5881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5882 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5883 });
5884 editor.move_line_down(&MoveLineDown, window, cx);
5885 });
5886}
5887
5888#[gpui::test]
5889async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5890 init_test(cx, |_| {});
5891
5892 let mut cx = EditorTestContext::new(cx).await;
5893 cx.set_state(
5894 &"
5895 ˇzero
5896 one
5897 two
5898 three
5899 four
5900 five
5901 "
5902 .unindent(),
5903 );
5904
5905 // Create a four-line block that replaces three lines of text.
5906 cx.update_editor(|editor, window, cx| {
5907 let snapshot = editor.snapshot(window, cx);
5908 let snapshot = &snapshot.buffer_snapshot();
5909 let placement = BlockPlacement::Replace(
5910 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5911 );
5912 editor.insert_blocks(
5913 [BlockProperties {
5914 placement,
5915 height: Some(4),
5916 style: BlockStyle::Sticky,
5917 render: Arc::new(|_| gpui::div().into_any_element()),
5918 priority: 0,
5919 }],
5920 None,
5921 cx,
5922 );
5923 });
5924
5925 // Move down so that the cursor touches the block.
5926 cx.update_editor(|editor, window, cx| {
5927 editor.move_down(&Default::default(), window, cx);
5928 });
5929 cx.assert_editor_state(
5930 &"
5931 zero
5932 «one
5933 two
5934 threeˇ»
5935 four
5936 five
5937 "
5938 .unindent(),
5939 );
5940
5941 // Move down past the block.
5942 cx.update_editor(|editor, window, cx| {
5943 editor.move_down(&Default::default(), window, cx);
5944 });
5945 cx.assert_editor_state(
5946 &"
5947 zero
5948 one
5949 two
5950 three
5951 ˇfour
5952 five
5953 "
5954 .unindent(),
5955 );
5956}
5957
5958#[gpui::test]
5959fn test_transpose(cx: &mut TestAppContext) {
5960 init_test(cx, |_| {});
5961
5962 _ = cx.add_window(|window, cx| {
5963 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5964 editor.set_style(EditorStyle::default(), window, cx);
5965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5966 s.select_ranges([1..1])
5967 });
5968 editor.transpose(&Default::default(), window, cx);
5969 assert_eq!(editor.text(cx), "bac");
5970 assert_eq!(
5971 editor.selections.ranges(&editor.display_snapshot(cx)),
5972 [2..2]
5973 );
5974
5975 editor.transpose(&Default::default(), window, cx);
5976 assert_eq!(editor.text(cx), "bca");
5977 assert_eq!(
5978 editor.selections.ranges(&editor.display_snapshot(cx)),
5979 [3..3]
5980 );
5981
5982 editor.transpose(&Default::default(), window, cx);
5983 assert_eq!(editor.text(cx), "bac");
5984 assert_eq!(
5985 editor.selections.ranges(&editor.display_snapshot(cx)),
5986 [3..3]
5987 );
5988
5989 editor
5990 });
5991
5992 _ = cx.add_window(|window, cx| {
5993 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5994 editor.set_style(EditorStyle::default(), window, cx);
5995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5996 s.select_ranges([3..3])
5997 });
5998 editor.transpose(&Default::default(), window, cx);
5999 assert_eq!(editor.text(cx), "acb\nde");
6000 assert_eq!(
6001 editor.selections.ranges(&editor.display_snapshot(cx)),
6002 [3..3]
6003 );
6004
6005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6006 s.select_ranges([4..4])
6007 });
6008 editor.transpose(&Default::default(), window, cx);
6009 assert_eq!(editor.text(cx), "acbd\ne");
6010 assert_eq!(
6011 editor.selections.ranges(&editor.display_snapshot(cx)),
6012 [5..5]
6013 );
6014
6015 editor.transpose(&Default::default(), window, cx);
6016 assert_eq!(editor.text(cx), "acbde\n");
6017 assert_eq!(
6018 editor.selections.ranges(&editor.display_snapshot(cx)),
6019 [6..6]
6020 );
6021
6022 editor.transpose(&Default::default(), window, cx);
6023 assert_eq!(editor.text(cx), "acbd\ne");
6024 assert_eq!(
6025 editor.selections.ranges(&editor.display_snapshot(cx)),
6026 [6..6]
6027 );
6028
6029 editor
6030 });
6031
6032 _ = cx.add_window(|window, cx| {
6033 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6034 editor.set_style(EditorStyle::default(), window, cx);
6035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6036 s.select_ranges([1..1, 2..2, 4..4])
6037 });
6038 editor.transpose(&Default::default(), window, cx);
6039 assert_eq!(editor.text(cx), "bacd\ne");
6040 assert_eq!(
6041 editor.selections.ranges(&editor.display_snapshot(cx)),
6042 [2..2, 3..3, 5..5]
6043 );
6044
6045 editor.transpose(&Default::default(), window, cx);
6046 assert_eq!(editor.text(cx), "bcade\n");
6047 assert_eq!(
6048 editor.selections.ranges(&editor.display_snapshot(cx)),
6049 [3..3, 4..4, 6..6]
6050 );
6051
6052 editor.transpose(&Default::default(), window, cx);
6053 assert_eq!(editor.text(cx), "bcda\ne");
6054 assert_eq!(
6055 editor.selections.ranges(&editor.display_snapshot(cx)),
6056 [4..4, 6..6]
6057 );
6058
6059 editor.transpose(&Default::default(), window, cx);
6060 assert_eq!(editor.text(cx), "bcade\n");
6061 assert_eq!(
6062 editor.selections.ranges(&editor.display_snapshot(cx)),
6063 [4..4, 6..6]
6064 );
6065
6066 editor.transpose(&Default::default(), window, cx);
6067 assert_eq!(editor.text(cx), "bcaed\n");
6068 assert_eq!(
6069 editor.selections.ranges(&editor.display_snapshot(cx)),
6070 [5..5, 6..6]
6071 );
6072
6073 editor
6074 });
6075
6076 _ = cx.add_window(|window, cx| {
6077 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6078 editor.set_style(EditorStyle::default(), window, cx);
6079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6080 s.select_ranges([4..4])
6081 });
6082 editor.transpose(&Default::default(), window, cx);
6083 assert_eq!(editor.text(cx), "🏀🍐✋");
6084 assert_eq!(
6085 editor.selections.ranges(&editor.display_snapshot(cx)),
6086 [8..8]
6087 );
6088
6089 editor.transpose(&Default::default(), window, cx);
6090 assert_eq!(editor.text(cx), "🏀✋🍐");
6091 assert_eq!(
6092 editor.selections.ranges(&editor.display_snapshot(cx)),
6093 [11..11]
6094 );
6095
6096 editor.transpose(&Default::default(), window, cx);
6097 assert_eq!(editor.text(cx), "🏀🍐✋");
6098 assert_eq!(
6099 editor.selections.ranges(&editor.display_snapshot(cx)),
6100 [11..11]
6101 );
6102
6103 editor
6104 });
6105}
6106
6107#[gpui::test]
6108async fn test_rewrap(cx: &mut TestAppContext) {
6109 init_test(cx, |settings| {
6110 settings.languages.0.extend([
6111 (
6112 "Markdown".into(),
6113 LanguageSettingsContent {
6114 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6115 preferred_line_length: Some(40),
6116 ..Default::default()
6117 },
6118 ),
6119 (
6120 "Plain Text".into(),
6121 LanguageSettingsContent {
6122 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6123 preferred_line_length: Some(40),
6124 ..Default::default()
6125 },
6126 ),
6127 (
6128 "C++".into(),
6129 LanguageSettingsContent {
6130 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6131 preferred_line_length: Some(40),
6132 ..Default::default()
6133 },
6134 ),
6135 (
6136 "Python".into(),
6137 LanguageSettingsContent {
6138 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6139 preferred_line_length: Some(40),
6140 ..Default::default()
6141 },
6142 ),
6143 (
6144 "Rust".into(),
6145 LanguageSettingsContent {
6146 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6147 preferred_line_length: Some(40),
6148 ..Default::default()
6149 },
6150 ),
6151 ])
6152 });
6153
6154 let mut cx = EditorTestContext::new(cx).await;
6155
6156 let cpp_language = Arc::new(Language::new(
6157 LanguageConfig {
6158 name: "C++".into(),
6159 line_comments: vec!["// ".into()],
6160 ..LanguageConfig::default()
6161 },
6162 None,
6163 ));
6164 let python_language = Arc::new(Language::new(
6165 LanguageConfig {
6166 name: "Python".into(),
6167 line_comments: vec!["# ".into()],
6168 ..LanguageConfig::default()
6169 },
6170 None,
6171 ));
6172 let markdown_language = Arc::new(Language::new(
6173 LanguageConfig {
6174 name: "Markdown".into(),
6175 rewrap_prefixes: vec![
6176 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6177 regex::Regex::new("[-*+]\\s+").unwrap(),
6178 ],
6179 ..LanguageConfig::default()
6180 },
6181 None,
6182 ));
6183 let rust_language = Arc::new(
6184 Language::new(
6185 LanguageConfig {
6186 name: "Rust".into(),
6187 line_comments: vec!["// ".into(), "/// ".into()],
6188 ..LanguageConfig::default()
6189 },
6190 Some(tree_sitter_rust::LANGUAGE.into()),
6191 )
6192 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6193 .unwrap(),
6194 );
6195
6196 let plaintext_language = Arc::new(Language::new(
6197 LanguageConfig {
6198 name: "Plain Text".into(),
6199 ..LanguageConfig::default()
6200 },
6201 None,
6202 ));
6203
6204 // Test basic rewrapping of a long line with a cursor
6205 assert_rewrap(
6206 indoc! {"
6207 // ˇThis is a long comment that needs to be wrapped.
6208 "},
6209 indoc! {"
6210 // ˇThis is a long comment that needs to
6211 // be wrapped.
6212 "},
6213 cpp_language.clone(),
6214 &mut cx,
6215 );
6216
6217 // Test rewrapping a full selection
6218 assert_rewrap(
6219 indoc! {"
6220 «// This selected long comment needs to be wrapped.ˇ»"
6221 },
6222 indoc! {"
6223 «// This selected long comment needs to
6224 // be wrapped.ˇ»"
6225 },
6226 cpp_language.clone(),
6227 &mut cx,
6228 );
6229
6230 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6231 assert_rewrap(
6232 indoc! {"
6233 // ˇThis is the first line.
6234 // Thisˇ is the second line.
6235 // This is the thirdˇ line, all part of one paragraph.
6236 "},
6237 indoc! {"
6238 // ˇThis is the first line. Thisˇ is the
6239 // second line. This is the thirdˇ line,
6240 // all part of one paragraph.
6241 "},
6242 cpp_language.clone(),
6243 &mut cx,
6244 );
6245
6246 // Test multiple cursors in different paragraphs trigger separate rewraps
6247 assert_rewrap(
6248 indoc! {"
6249 // ˇThis is the first paragraph, first line.
6250 // ˇThis is the first paragraph, second line.
6251
6252 // ˇThis is the second paragraph, first line.
6253 // ˇThis is the second paragraph, second line.
6254 "},
6255 indoc! {"
6256 // ˇThis is the first paragraph, first
6257 // line. ˇThis is the first paragraph,
6258 // second line.
6259
6260 // ˇThis is the second paragraph, first
6261 // line. ˇThis is the second paragraph,
6262 // second line.
6263 "},
6264 cpp_language.clone(),
6265 &mut cx,
6266 );
6267
6268 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6269 assert_rewrap(
6270 indoc! {"
6271 «// A regular long long comment to be wrapped.
6272 /// A documentation long comment to be wrapped.ˇ»
6273 "},
6274 indoc! {"
6275 «// A regular long long comment to be
6276 // wrapped.
6277 /// A documentation long comment to be
6278 /// wrapped.ˇ»
6279 "},
6280 rust_language.clone(),
6281 &mut cx,
6282 );
6283
6284 // Test that change in indentation level trigger seperate rewraps
6285 assert_rewrap(
6286 indoc! {"
6287 fn foo() {
6288 «// This is a long comment at the base indent.
6289 // This is a long comment at the next indent.ˇ»
6290 }
6291 "},
6292 indoc! {"
6293 fn foo() {
6294 «// This is a long comment at the
6295 // base indent.
6296 // This is a long comment at the
6297 // next indent.ˇ»
6298 }
6299 "},
6300 rust_language.clone(),
6301 &mut cx,
6302 );
6303
6304 // Test that different comment prefix characters (e.g., '#') are handled correctly
6305 assert_rewrap(
6306 indoc! {"
6307 # ˇThis is a long comment using a pound sign.
6308 "},
6309 indoc! {"
6310 # ˇThis is a long comment using a pound
6311 # sign.
6312 "},
6313 python_language,
6314 &mut cx,
6315 );
6316
6317 // Test rewrapping only affects comments, not code even when selected
6318 assert_rewrap(
6319 indoc! {"
6320 «/// This doc comment is long and should be wrapped.
6321 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6322 "},
6323 indoc! {"
6324 «/// This doc comment is long and should
6325 /// be wrapped.
6326 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6327 "},
6328 rust_language.clone(),
6329 &mut cx,
6330 );
6331
6332 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6333 assert_rewrap(
6334 indoc! {"
6335 # Header
6336
6337 A long long long line of markdown text to wrap.ˇ
6338 "},
6339 indoc! {"
6340 # Header
6341
6342 A long long long line of markdown text
6343 to wrap.ˇ
6344 "},
6345 markdown_language.clone(),
6346 &mut cx,
6347 );
6348
6349 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6350 assert_rewrap(
6351 indoc! {"
6352 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6353 2. This is a numbered list item that is very long and needs to be wrapped properly.
6354 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6355 "},
6356 indoc! {"
6357 «1. This is a numbered list item that is
6358 very long and needs to be wrapped
6359 properly.
6360 2. This is a numbered list item that is
6361 very long and needs to be wrapped
6362 properly.
6363 - This is an unordered list item that is
6364 also very long and should not merge
6365 with the numbered item.ˇ»
6366 "},
6367 markdown_language.clone(),
6368 &mut cx,
6369 );
6370
6371 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6372 assert_rewrap(
6373 indoc! {"
6374 «1. This is a numbered list item that is
6375 very long and needs to be wrapped
6376 properly.
6377 2. This is a numbered list item that is
6378 very long and needs to be wrapped
6379 properly.
6380 - This is an unordered list item that is
6381 also very long and should not merge with
6382 the numbered item.ˇ»
6383 "},
6384 indoc! {"
6385 «1. This is a numbered list item that is
6386 very long and needs to be wrapped
6387 properly.
6388 2. This is a numbered list item that is
6389 very long and needs to be wrapped
6390 properly.
6391 - This is an unordered list item that is
6392 also very long and should not merge
6393 with the numbered item.ˇ»
6394 "},
6395 markdown_language.clone(),
6396 &mut cx,
6397 );
6398
6399 // Test that rewrapping maintain indents even when they already exists.
6400 assert_rewrap(
6401 indoc! {"
6402 «1. This is a numbered list
6403 item that is very long and needs to be wrapped properly.
6404 2. This is a numbered list
6405 item that is very long and needs to be wrapped properly.
6406 - This is an unordered list item that is also very long and
6407 should not merge with the numbered item.ˇ»
6408 "},
6409 indoc! {"
6410 «1. This is a numbered list item that is
6411 very long and needs to be wrapped
6412 properly.
6413 2. This is a numbered list item that is
6414 very long and needs to be wrapped
6415 properly.
6416 - This is an unordered list item that is
6417 also very long and should not merge
6418 with the numbered item.ˇ»
6419 "},
6420 markdown_language,
6421 &mut cx,
6422 );
6423
6424 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6425 assert_rewrap(
6426 indoc! {"
6427 ˇThis is a very long line of plain text that will be wrapped.
6428 "},
6429 indoc! {"
6430 ˇThis is a very long line of plain text
6431 that will be wrapped.
6432 "},
6433 plaintext_language.clone(),
6434 &mut cx,
6435 );
6436
6437 // Test that non-commented code acts as a paragraph boundary within a selection
6438 assert_rewrap(
6439 indoc! {"
6440 «// This is the first long comment block to be wrapped.
6441 fn my_func(a: u32);
6442 // This is the second long comment block to be wrapped.ˇ»
6443 "},
6444 indoc! {"
6445 «// This is the first long comment block
6446 // to be wrapped.
6447 fn my_func(a: u32);
6448 // This is the second long comment block
6449 // to be wrapped.ˇ»
6450 "},
6451 rust_language,
6452 &mut cx,
6453 );
6454
6455 // Test rewrapping multiple selections, including ones with blank lines or tabs
6456 assert_rewrap(
6457 indoc! {"
6458 «ˇThis is a very long line that will be wrapped.
6459
6460 This is another paragraph in the same selection.»
6461
6462 «\tThis is a very long indented line that will be wrapped.ˇ»
6463 "},
6464 indoc! {"
6465 «ˇThis is a very long line that will be
6466 wrapped.
6467
6468 This is another paragraph in the same
6469 selection.»
6470
6471 «\tThis is a very long indented line
6472 \tthat will be wrapped.ˇ»
6473 "},
6474 plaintext_language,
6475 &mut cx,
6476 );
6477
6478 // Test that an empty comment line acts as a paragraph boundary
6479 assert_rewrap(
6480 indoc! {"
6481 // ˇThis is a long comment that will be wrapped.
6482 //
6483 // And this is another long comment that will also be wrapped.ˇ
6484 "},
6485 indoc! {"
6486 // ˇThis is a long comment that will be
6487 // wrapped.
6488 //
6489 // And this is another long comment that
6490 // will also be wrapped.ˇ
6491 "},
6492 cpp_language,
6493 &mut cx,
6494 );
6495
6496 #[track_caller]
6497 fn assert_rewrap(
6498 unwrapped_text: &str,
6499 wrapped_text: &str,
6500 language: Arc<Language>,
6501 cx: &mut EditorTestContext,
6502 ) {
6503 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6504 cx.set_state(unwrapped_text);
6505 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6506 cx.assert_editor_state(wrapped_text);
6507 }
6508}
6509
6510#[gpui::test]
6511async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6512 init_test(cx, |settings| {
6513 settings.languages.0.extend([(
6514 "Rust".into(),
6515 LanguageSettingsContent {
6516 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6517 preferred_line_length: Some(40),
6518 ..Default::default()
6519 },
6520 )])
6521 });
6522
6523 let mut cx = EditorTestContext::new(cx).await;
6524
6525 let rust_lang = Arc::new(
6526 Language::new(
6527 LanguageConfig {
6528 name: "Rust".into(),
6529 line_comments: vec!["// ".into()],
6530 block_comment: Some(BlockCommentConfig {
6531 start: "/*".into(),
6532 end: "*/".into(),
6533 prefix: "* ".into(),
6534 tab_size: 1,
6535 }),
6536 documentation_comment: Some(BlockCommentConfig {
6537 start: "/**".into(),
6538 end: "*/".into(),
6539 prefix: "* ".into(),
6540 tab_size: 1,
6541 }),
6542
6543 ..LanguageConfig::default()
6544 },
6545 Some(tree_sitter_rust::LANGUAGE.into()),
6546 )
6547 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6548 .unwrap(),
6549 );
6550
6551 // regular block comment
6552 assert_rewrap(
6553 indoc! {"
6554 /*
6555 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6556 */
6557 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6558 "},
6559 indoc! {"
6560 /*
6561 *ˇ Lorem ipsum dolor sit amet,
6562 * consectetur adipiscing elit.
6563 */
6564 /*
6565 *ˇ Lorem ipsum dolor sit amet,
6566 * consectetur adipiscing elit.
6567 */
6568 "},
6569 rust_lang.clone(),
6570 &mut cx,
6571 );
6572
6573 // indent is respected
6574 assert_rewrap(
6575 indoc! {"
6576 {}
6577 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6578 "},
6579 indoc! {"
6580 {}
6581 /*
6582 *ˇ Lorem ipsum dolor sit amet,
6583 * consectetur adipiscing elit.
6584 */
6585 "},
6586 rust_lang.clone(),
6587 &mut cx,
6588 );
6589
6590 // short block comments with inline delimiters
6591 assert_rewrap(
6592 indoc! {"
6593 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6594 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6595 */
6596 /*
6597 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6598 "},
6599 indoc! {"
6600 /*
6601 *ˇ Lorem ipsum dolor sit amet,
6602 * consectetur adipiscing elit.
6603 */
6604 /*
6605 *ˇ Lorem ipsum dolor sit amet,
6606 * consectetur adipiscing elit.
6607 */
6608 /*
6609 *ˇ Lorem ipsum dolor sit amet,
6610 * consectetur adipiscing elit.
6611 */
6612 "},
6613 rust_lang.clone(),
6614 &mut cx,
6615 );
6616
6617 // multiline block comment with inline start/end delimiters
6618 assert_rewrap(
6619 indoc! {"
6620 /*ˇ Lorem ipsum dolor sit amet,
6621 * consectetur adipiscing elit. */
6622 "},
6623 indoc! {"
6624 /*
6625 *ˇ Lorem ipsum dolor sit amet,
6626 * consectetur adipiscing elit.
6627 */
6628 "},
6629 rust_lang.clone(),
6630 &mut cx,
6631 );
6632
6633 // block comment rewrap still respects paragraph bounds
6634 assert_rewrap(
6635 indoc! {"
6636 /*
6637 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6638 *
6639 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6640 */
6641 "},
6642 indoc! {"
6643 /*
6644 *ˇ Lorem ipsum dolor sit amet,
6645 * consectetur adipiscing elit.
6646 *
6647 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6648 */
6649 "},
6650 rust_lang.clone(),
6651 &mut cx,
6652 );
6653
6654 // documentation comments
6655 assert_rewrap(
6656 indoc! {"
6657 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6658 /**
6659 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6660 */
6661 "},
6662 indoc! {"
6663 /**
6664 *ˇ Lorem ipsum dolor sit amet,
6665 * consectetur adipiscing elit.
6666 */
6667 /**
6668 *ˇ Lorem ipsum dolor sit amet,
6669 * consectetur adipiscing elit.
6670 */
6671 "},
6672 rust_lang.clone(),
6673 &mut cx,
6674 );
6675
6676 // different, adjacent comments
6677 assert_rewrap(
6678 indoc! {"
6679 /**
6680 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6681 */
6682 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6683 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6684 "},
6685 indoc! {"
6686 /**
6687 *ˇ Lorem ipsum dolor sit amet,
6688 * consectetur adipiscing elit.
6689 */
6690 /*
6691 *ˇ Lorem ipsum dolor sit amet,
6692 * consectetur adipiscing elit.
6693 */
6694 //ˇ Lorem ipsum dolor sit amet,
6695 // consectetur adipiscing elit.
6696 "},
6697 rust_lang.clone(),
6698 &mut cx,
6699 );
6700
6701 // selection w/ single short block comment
6702 assert_rewrap(
6703 indoc! {"
6704 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6705 "},
6706 indoc! {"
6707 «/*
6708 * Lorem ipsum dolor sit amet,
6709 * consectetur adipiscing elit.
6710 */ˇ»
6711 "},
6712 rust_lang.clone(),
6713 &mut cx,
6714 );
6715
6716 // rewrapping a single comment w/ abutting comments
6717 assert_rewrap(
6718 indoc! {"
6719 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6720 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6721 "},
6722 indoc! {"
6723 /*
6724 * ˇLorem ipsum dolor sit amet,
6725 * consectetur adipiscing elit.
6726 */
6727 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6728 "},
6729 rust_lang.clone(),
6730 &mut cx,
6731 );
6732
6733 // selection w/ non-abutting short block comments
6734 assert_rewrap(
6735 indoc! {"
6736 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6737
6738 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6739 "},
6740 indoc! {"
6741 «/*
6742 * Lorem ipsum dolor sit amet,
6743 * consectetur adipiscing elit.
6744 */
6745
6746 /*
6747 * Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit.
6749 */ˇ»
6750 "},
6751 rust_lang.clone(),
6752 &mut cx,
6753 );
6754
6755 // selection of multiline block comments
6756 assert_rewrap(
6757 indoc! {"
6758 «/* Lorem ipsum dolor sit amet,
6759 * consectetur adipiscing elit. */ˇ»
6760 "},
6761 indoc! {"
6762 «/*
6763 * Lorem ipsum dolor sit amet,
6764 * consectetur adipiscing elit.
6765 */ˇ»
6766 "},
6767 rust_lang.clone(),
6768 &mut cx,
6769 );
6770
6771 // partial selection of multiline block comments
6772 assert_rewrap(
6773 indoc! {"
6774 «/* Lorem ipsum dolor sit amet,ˇ»
6775 * consectetur adipiscing elit. */
6776 /* Lorem ipsum dolor sit amet,
6777 «* consectetur adipiscing elit. */ˇ»
6778 "},
6779 indoc! {"
6780 «/*
6781 * Lorem ipsum dolor sit amet,ˇ»
6782 * consectetur adipiscing elit. */
6783 /* Lorem ipsum dolor sit amet,
6784 «* consectetur adipiscing elit.
6785 */ˇ»
6786 "},
6787 rust_lang.clone(),
6788 &mut cx,
6789 );
6790
6791 // selection w/ abutting short block comments
6792 // TODO: should not be combined; should rewrap as 2 comments
6793 assert_rewrap(
6794 indoc! {"
6795 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6796 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6797 "},
6798 // desired behavior:
6799 // indoc! {"
6800 // «/*
6801 // * Lorem ipsum dolor sit amet,
6802 // * consectetur adipiscing elit.
6803 // */
6804 // /*
6805 // * Lorem ipsum dolor sit amet,
6806 // * consectetur adipiscing elit.
6807 // */ˇ»
6808 // "},
6809 // actual behaviour:
6810 indoc! {"
6811 «/*
6812 * Lorem ipsum dolor sit amet,
6813 * consectetur adipiscing elit. Lorem
6814 * ipsum dolor sit amet, consectetur
6815 * adipiscing elit.
6816 */ˇ»
6817 "},
6818 rust_lang.clone(),
6819 &mut cx,
6820 );
6821
6822 // TODO: same as above, but with delimiters on separate line
6823 // assert_rewrap(
6824 // indoc! {"
6825 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6826 // */
6827 // /*
6828 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6829 // "},
6830 // // desired:
6831 // // indoc! {"
6832 // // «/*
6833 // // * Lorem ipsum dolor sit amet,
6834 // // * consectetur adipiscing elit.
6835 // // */
6836 // // /*
6837 // // * Lorem ipsum dolor sit amet,
6838 // // * consectetur adipiscing elit.
6839 // // */ˇ»
6840 // // "},
6841 // // actual: (but with trailing w/s on the empty lines)
6842 // indoc! {"
6843 // «/*
6844 // * Lorem ipsum dolor sit amet,
6845 // * consectetur adipiscing elit.
6846 // *
6847 // */
6848 // /*
6849 // *
6850 // * Lorem ipsum dolor sit amet,
6851 // * consectetur adipiscing elit.
6852 // */ˇ»
6853 // "},
6854 // rust_lang.clone(),
6855 // &mut cx,
6856 // );
6857
6858 // TODO these are unhandled edge cases; not correct, just documenting known issues
6859 assert_rewrap(
6860 indoc! {"
6861 /*
6862 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6863 */
6864 /*
6865 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6866 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6867 "},
6868 // desired:
6869 // indoc! {"
6870 // /*
6871 // *ˇ Lorem ipsum dolor sit amet,
6872 // * consectetur adipiscing elit.
6873 // */
6874 // /*
6875 // *ˇ Lorem ipsum dolor sit amet,
6876 // * consectetur adipiscing elit.
6877 // */
6878 // /*
6879 // *ˇ Lorem ipsum dolor sit amet
6880 // */ /* consectetur adipiscing elit. */
6881 // "},
6882 // actual:
6883 indoc! {"
6884 /*
6885 //ˇ Lorem ipsum dolor sit amet,
6886 // consectetur adipiscing elit.
6887 */
6888 /*
6889 * //ˇ Lorem ipsum dolor sit amet,
6890 * consectetur adipiscing elit.
6891 */
6892 /*
6893 *ˇ Lorem ipsum dolor sit amet */ /*
6894 * consectetur adipiscing elit.
6895 */
6896 "},
6897 rust_lang,
6898 &mut cx,
6899 );
6900
6901 #[track_caller]
6902 fn assert_rewrap(
6903 unwrapped_text: &str,
6904 wrapped_text: &str,
6905 language: Arc<Language>,
6906 cx: &mut EditorTestContext,
6907 ) {
6908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6909 cx.set_state(unwrapped_text);
6910 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6911 cx.assert_editor_state(wrapped_text);
6912 }
6913}
6914
6915#[gpui::test]
6916async fn test_hard_wrap(cx: &mut TestAppContext) {
6917 init_test(cx, |_| {});
6918 let mut cx = EditorTestContext::new(cx).await;
6919
6920 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6921 cx.update_editor(|editor, _, cx| {
6922 editor.set_hard_wrap(Some(14), cx);
6923 });
6924
6925 cx.set_state(indoc!(
6926 "
6927 one two three ˇ
6928 "
6929 ));
6930 cx.simulate_input("four");
6931 cx.run_until_parked();
6932
6933 cx.assert_editor_state(indoc!(
6934 "
6935 one two three
6936 fourˇ
6937 "
6938 ));
6939
6940 cx.update_editor(|editor, window, cx| {
6941 editor.newline(&Default::default(), window, cx);
6942 });
6943 cx.run_until_parked();
6944 cx.assert_editor_state(indoc!(
6945 "
6946 one two three
6947 four
6948 ˇ
6949 "
6950 ));
6951
6952 cx.simulate_input("five");
6953 cx.run_until_parked();
6954 cx.assert_editor_state(indoc!(
6955 "
6956 one two three
6957 four
6958 fiveˇ
6959 "
6960 ));
6961
6962 cx.update_editor(|editor, window, cx| {
6963 editor.newline(&Default::default(), window, cx);
6964 });
6965 cx.run_until_parked();
6966 cx.simulate_input("# ");
6967 cx.run_until_parked();
6968 cx.assert_editor_state(indoc!(
6969 "
6970 one two three
6971 four
6972 five
6973 # ˇ
6974 "
6975 ));
6976
6977 cx.update_editor(|editor, window, cx| {
6978 editor.newline(&Default::default(), window, cx);
6979 });
6980 cx.run_until_parked();
6981 cx.assert_editor_state(indoc!(
6982 "
6983 one two three
6984 four
6985 five
6986 #\x20
6987 #ˇ
6988 "
6989 ));
6990
6991 cx.simulate_input(" 6");
6992 cx.run_until_parked();
6993 cx.assert_editor_state(indoc!(
6994 "
6995 one two three
6996 four
6997 five
6998 #
6999 # 6ˇ
7000 "
7001 ));
7002}
7003
7004#[gpui::test]
7005async fn test_cut_line_ends(cx: &mut TestAppContext) {
7006 init_test(cx, |_| {});
7007
7008 let mut cx = EditorTestContext::new(cx).await;
7009
7010 cx.set_state(indoc! {"The quick brownˇ"});
7011 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7012 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7013
7014 cx.set_state(indoc! {"The emacs foxˇ"});
7015 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7016 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7017
7018 cx.set_state(indoc! {"
7019 The quick« brownˇ»
7020 fox jumps overˇ
7021 the lazy dog"});
7022 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7023 cx.assert_editor_state(indoc! {"
7024 The quickˇ
7025 ˇthe lazy dog"});
7026
7027 cx.set_state(indoc! {"
7028 The quick« brownˇ»
7029 fox jumps overˇ
7030 the lazy dog"});
7031 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7032 cx.assert_editor_state(indoc! {"
7033 The quickˇ
7034 fox jumps overˇthe lazy dog"});
7035
7036 cx.set_state(indoc! {"
7037 The quick« brownˇ»
7038 fox jumps overˇ
7039 the lazy dog"});
7040 cx.update_editor(|e, window, cx| {
7041 e.cut_to_end_of_line(
7042 &CutToEndOfLine {
7043 stop_at_newlines: true,
7044 },
7045 window,
7046 cx,
7047 )
7048 });
7049 cx.assert_editor_state(indoc! {"
7050 The quickˇ
7051 fox jumps overˇ
7052 the lazy dog"});
7053
7054 cx.set_state(indoc! {"
7055 The quick« brownˇ»
7056 fox jumps overˇ
7057 the lazy dog"});
7058 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7059 cx.assert_editor_state(indoc! {"
7060 The quickˇ
7061 fox jumps overˇthe lazy dog"});
7062}
7063
7064#[gpui::test]
7065async fn test_clipboard(cx: &mut TestAppContext) {
7066 init_test(cx, |_| {});
7067
7068 let mut cx = EditorTestContext::new(cx).await;
7069
7070 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7071 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7072 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7073
7074 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7075 cx.set_state("two ˇfour ˇsix ˇ");
7076 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7077 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7078
7079 // Paste again but with only two cursors. Since the number of cursors doesn't
7080 // match the number of slices in the clipboard, the entire clipboard text
7081 // is pasted at each cursor.
7082 cx.set_state("ˇtwo one✅ four three six five ˇ");
7083 cx.update_editor(|e, window, cx| {
7084 e.handle_input("( ", window, cx);
7085 e.paste(&Paste, window, cx);
7086 e.handle_input(") ", window, cx);
7087 });
7088 cx.assert_editor_state(
7089 &([
7090 "( one✅ ",
7091 "three ",
7092 "five ) ˇtwo one✅ four three six five ( one✅ ",
7093 "three ",
7094 "five ) ˇ",
7095 ]
7096 .join("\n")),
7097 );
7098
7099 // Cut with three selections, one of which is full-line.
7100 cx.set_state(indoc! {"
7101 1«2ˇ»3
7102 4ˇ567
7103 «8ˇ»9"});
7104 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7105 cx.assert_editor_state(indoc! {"
7106 1ˇ3
7107 ˇ9"});
7108
7109 // Paste with three selections, noticing how the copied selection that was full-line
7110 // gets inserted before the second cursor.
7111 cx.set_state(indoc! {"
7112 1ˇ3
7113 9ˇ
7114 «oˇ»ne"});
7115 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7116 cx.assert_editor_state(indoc! {"
7117 12ˇ3
7118 4567
7119 9ˇ
7120 8ˇne"});
7121
7122 // Copy with a single cursor only, which writes the whole line into the clipboard.
7123 cx.set_state(indoc! {"
7124 The quick brown
7125 fox juˇmps over
7126 the lazy dog"});
7127 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7128 assert_eq!(
7129 cx.read_from_clipboard()
7130 .and_then(|item| item.text().as_deref().map(str::to_string)),
7131 Some("fox jumps over\n".to_string())
7132 );
7133
7134 // Paste with three selections, noticing how the copied full-line selection is inserted
7135 // before the empty selections but replaces the selection that is non-empty.
7136 cx.set_state(indoc! {"
7137 Tˇhe quick brown
7138 «foˇ»x jumps over
7139 tˇhe lazy dog"});
7140 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7141 cx.assert_editor_state(indoc! {"
7142 fox jumps over
7143 Tˇhe quick brown
7144 fox jumps over
7145 ˇx jumps over
7146 fox jumps over
7147 tˇhe lazy dog"});
7148}
7149
7150#[gpui::test]
7151async fn test_copy_trim(cx: &mut TestAppContext) {
7152 init_test(cx, |_| {});
7153
7154 let mut cx = EditorTestContext::new(cx).await;
7155 cx.set_state(
7156 r#" «for selection in selections.iter() {
7157 let mut start = selection.start;
7158 let mut end = selection.end;
7159 let is_entire_line = selection.is_empty();
7160 if is_entire_line {
7161 start = Point::new(start.row, 0);ˇ»
7162 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7163 }
7164 "#,
7165 );
7166 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7167 assert_eq!(
7168 cx.read_from_clipboard()
7169 .and_then(|item| item.text().as_deref().map(str::to_string)),
7170 Some(
7171 "for selection in selections.iter() {
7172 let mut start = selection.start;
7173 let mut end = selection.end;
7174 let is_entire_line = selection.is_empty();
7175 if is_entire_line {
7176 start = Point::new(start.row, 0);"
7177 .to_string()
7178 ),
7179 "Regular copying preserves all indentation selected",
7180 );
7181 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7182 assert_eq!(
7183 cx.read_from_clipboard()
7184 .and_then(|item| item.text().as_deref().map(str::to_string)),
7185 Some(
7186 "for selection in selections.iter() {
7187let mut start = selection.start;
7188let mut end = selection.end;
7189let is_entire_line = selection.is_empty();
7190if is_entire_line {
7191 start = Point::new(start.row, 0);"
7192 .to_string()
7193 ),
7194 "Copying with stripping should strip all leading whitespaces"
7195 );
7196
7197 cx.set_state(
7198 r#" « for selection in selections.iter() {
7199 let mut start = selection.start;
7200 let mut end = selection.end;
7201 let is_entire_line = selection.is_empty();
7202 if is_entire_line {
7203 start = Point::new(start.row, 0);ˇ»
7204 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7205 }
7206 "#,
7207 );
7208 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7209 assert_eq!(
7210 cx.read_from_clipboard()
7211 .and_then(|item| item.text().as_deref().map(str::to_string)),
7212 Some(
7213 " for selection in selections.iter() {
7214 let mut start = selection.start;
7215 let mut end = selection.end;
7216 let is_entire_line = selection.is_empty();
7217 if is_entire_line {
7218 start = Point::new(start.row, 0);"
7219 .to_string()
7220 ),
7221 "Regular copying preserves all indentation selected",
7222 );
7223 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7224 assert_eq!(
7225 cx.read_from_clipboard()
7226 .and_then(|item| item.text().as_deref().map(str::to_string)),
7227 Some(
7228 "for selection in selections.iter() {
7229let mut start = selection.start;
7230let mut end = selection.end;
7231let is_entire_line = selection.is_empty();
7232if is_entire_line {
7233 start = Point::new(start.row, 0);"
7234 .to_string()
7235 ),
7236 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7237 );
7238
7239 cx.set_state(
7240 r#" «ˇ for selection in selections.iter() {
7241 let mut start = selection.start;
7242 let mut end = selection.end;
7243 let is_entire_line = selection.is_empty();
7244 if is_entire_line {
7245 start = Point::new(start.row, 0);»
7246 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7247 }
7248 "#,
7249 );
7250 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7251 assert_eq!(
7252 cx.read_from_clipboard()
7253 .and_then(|item| item.text().as_deref().map(str::to_string)),
7254 Some(
7255 " for selection in selections.iter() {
7256 let mut start = selection.start;
7257 let mut end = selection.end;
7258 let is_entire_line = selection.is_empty();
7259 if is_entire_line {
7260 start = Point::new(start.row, 0);"
7261 .to_string()
7262 ),
7263 "Regular copying for reverse selection works the same",
7264 );
7265 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7266 assert_eq!(
7267 cx.read_from_clipboard()
7268 .and_then(|item| item.text().as_deref().map(str::to_string)),
7269 Some(
7270 "for selection in selections.iter() {
7271let mut start = selection.start;
7272let mut end = selection.end;
7273let is_entire_line = selection.is_empty();
7274if is_entire_line {
7275 start = Point::new(start.row, 0);"
7276 .to_string()
7277 ),
7278 "Copying with stripping for reverse selection works the same"
7279 );
7280
7281 cx.set_state(
7282 r#" for selection «in selections.iter() {
7283 let mut start = selection.start;
7284 let mut end = selection.end;
7285 let is_entire_line = selection.is_empty();
7286 if is_entire_line {
7287 start = Point::new(start.row, 0);ˇ»
7288 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7289 }
7290 "#,
7291 );
7292 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7293 assert_eq!(
7294 cx.read_from_clipboard()
7295 .and_then(|item| item.text().as_deref().map(str::to_string)),
7296 Some(
7297 "in selections.iter() {
7298 let mut start = selection.start;
7299 let mut end = selection.end;
7300 let is_entire_line = selection.is_empty();
7301 if is_entire_line {
7302 start = Point::new(start.row, 0);"
7303 .to_string()
7304 ),
7305 "When selecting past the indent, the copying works as usual",
7306 );
7307 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7308 assert_eq!(
7309 cx.read_from_clipboard()
7310 .and_then(|item| item.text().as_deref().map(str::to_string)),
7311 Some(
7312 "in selections.iter() {
7313 let mut start = selection.start;
7314 let mut end = selection.end;
7315 let is_entire_line = selection.is_empty();
7316 if is_entire_line {
7317 start = Point::new(start.row, 0);"
7318 .to_string()
7319 ),
7320 "When selecting past the indent, nothing is trimmed"
7321 );
7322
7323 cx.set_state(
7324 r#" «for selection in selections.iter() {
7325 let mut start = selection.start;
7326
7327 let mut end = selection.end;
7328 let is_entire_line = selection.is_empty();
7329 if is_entire_line {
7330 start = Point::new(start.row, 0);
7331ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7332 }
7333 "#,
7334 );
7335 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7336 assert_eq!(
7337 cx.read_from_clipboard()
7338 .and_then(|item| item.text().as_deref().map(str::to_string)),
7339 Some(
7340 "for selection in selections.iter() {
7341let mut start = selection.start;
7342
7343let mut end = selection.end;
7344let is_entire_line = selection.is_empty();
7345if is_entire_line {
7346 start = Point::new(start.row, 0);
7347"
7348 .to_string()
7349 ),
7350 "Copying with stripping should ignore empty lines"
7351 );
7352}
7353
7354#[gpui::test]
7355async fn test_paste_multiline(cx: &mut TestAppContext) {
7356 init_test(cx, |_| {});
7357
7358 let mut cx = EditorTestContext::new(cx).await;
7359 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7360
7361 // Cut an indented block, without the leading whitespace.
7362 cx.set_state(indoc! {"
7363 const a: B = (
7364 c(),
7365 «d(
7366 e,
7367 f
7368 )ˇ»
7369 );
7370 "});
7371 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7372 cx.assert_editor_state(indoc! {"
7373 const a: B = (
7374 c(),
7375 ˇ
7376 );
7377 "});
7378
7379 // Paste it at the same position.
7380 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7381 cx.assert_editor_state(indoc! {"
7382 const a: B = (
7383 c(),
7384 d(
7385 e,
7386 f
7387 )ˇ
7388 );
7389 "});
7390
7391 // Paste it at a line with a lower indent level.
7392 cx.set_state(indoc! {"
7393 ˇ
7394 const a: B = (
7395 c(),
7396 );
7397 "});
7398 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7399 cx.assert_editor_state(indoc! {"
7400 d(
7401 e,
7402 f
7403 )ˇ
7404 const a: B = (
7405 c(),
7406 );
7407 "});
7408
7409 // Cut an indented block, with the leading whitespace.
7410 cx.set_state(indoc! {"
7411 const a: B = (
7412 c(),
7413 « d(
7414 e,
7415 f
7416 )
7417 ˇ»);
7418 "});
7419 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7420 cx.assert_editor_state(indoc! {"
7421 const a: B = (
7422 c(),
7423 ˇ);
7424 "});
7425
7426 // Paste it at the same position.
7427 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7428 cx.assert_editor_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 d(
7432 e,
7433 f
7434 )
7435 ˇ);
7436 "});
7437
7438 // Paste it at a line with a higher indent level.
7439 cx.set_state(indoc! {"
7440 const a: B = (
7441 c(),
7442 d(
7443 e,
7444 fˇ
7445 )
7446 );
7447 "});
7448 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7449 cx.assert_editor_state(indoc! {"
7450 const a: B = (
7451 c(),
7452 d(
7453 e,
7454 f d(
7455 e,
7456 f
7457 )
7458 ˇ
7459 )
7460 );
7461 "});
7462
7463 // Copy an indented block, starting mid-line
7464 cx.set_state(indoc! {"
7465 const a: B = (
7466 c(),
7467 somethin«g(
7468 e,
7469 f
7470 )ˇ»
7471 );
7472 "});
7473 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7474
7475 // Paste it on a line with a lower indent level
7476 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7477 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7478 cx.assert_editor_state(indoc! {"
7479 const a: B = (
7480 c(),
7481 something(
7482 e,
7483 f
7484 )
7485 );
7486 g(
7487 e,
7488 f
7489 )ˇ"});
7490}
7491
7492#[gpui::test]
7493async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7494 init_test(cx, |_| {});
7495
7496 cx.write_to_clipboard(ClipboardItem::new_string(
7497 " d(\n e\n );\n".into(),
7498 ));
7499
7500 let mut cx = EditorTestContext::new(cx).await;
7501 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7502
7503 cx.set_state(indoc! {"
7504 fn a() {
7505 b();
7506 if c() {
7507 ˇ
7508 }
7509 }
7510 "});
7511
7512 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7513 cx.assert_editor_state(indoc! {"
7514 fn a() {
7515 b();
7516 if c() {
7517 d(
7518 e
7519 );
7520 ˇ
7521 }
7522 }
7523 "});
7524
7525 cx.set_state(indoc! {"
7526 fn a() {
7527 b();
7528 ˇ
7529 }
7530 "});
7531
7532 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7533 cx.assert_editor_state(indoc! {"
7534 fn a() {
7535 b();
7536 d(
7537 e
7538 );
7539 ˇ
7540 }
7541 "});
7542}
7543
7544#[gpui::test]
7545fn test_select_all(cx: &mut TestAppContext) {
7546 init_test(cx, |_| {});
7547
7548 let editor = cx.add_window(|window, cx| {
7549 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7550 build_editor(buffer, window, cx)
7551 });
7552 _ = editor.update(cx, |editor, window, cx| {
7553 editor.select_all(&SelectAll, window, cx);
7554 assert_eq!(
7555 display_ranges(editor, cx),
7556 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7557 );
7558 });
7559}
7560
7561#[gpui::test]
7562fn test_select_line(cx: &mut TestAppContext) {
7563 init_test(cx, |_| {});
7564
7565 let editor = cx.add_window(|window, cx| {
7566 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7567 build_editor(buffer, window, cx)
7568 });
7569 _ = editor.update(cx, |editor, window, cx| {
7570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7571 s.select_display_ranges([
7572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7573 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7574 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7575 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7576 ])
7577 });
7578 editor.select_line(&SelectLine, window, cx);
7579 assert_eq!(
7580 display_ranges(editor, cx),
7581 vec![
7582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7584 ]
7585 );
7586 });
7587
7588 _ = editor.update(cx, |editor, window, cx| {
7589 editor.select_line(&SelectLine, window, cx);
7590 assert_eq!(
7591 display_ranges(editor, cx),
7592 vec![
7593 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7594 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7595 ]
7596 );
7597 });
7598
7599 _ = editor.update(cx, |editor, window, cx| {
7600 editor.select_line(&SelectLine, window, cx);
7601 assert_eq!(
7602 display_ranges(editor, cx),
7603 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7604 );
7605 });
7606}
7607
7608#[gpui::test]
7609async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7610 init_test(cx, |_| {});
7611 let mut cx = EditorTestContext::new(cx).await;
7612
7613 #[track_caller]
7614 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7615 cx.set_state(initial_state);
7616 cx.update_editor(|e, window, cx| {
7617 e.split_selection_into_lines(&Default::default(), window, cx)
7618 });
7619 cx.assert_editor_state(expected_state);
7620 }
7621
7622 // Selection starts and ends at the middle of lines, left-to-right
7623 test(
7624 &mut cx,
7625 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7626 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7627 );
7628 // Same thing, right-to-left
7629 test(
7630 &mut cx,
7631 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7632 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7633 );
7634
7635 // Whole buffer, left-to-right, last line *doesn't* end with newline
7636 test(
7637 &mut cx,
7638 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7639 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7640 );
7641 // Same thing, right-to-left
7642 test(
7643 &mut cx,
7644 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7645 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7646 );
7647
7648 // Whole buffer, left-to-right, last line ends with newline
7649 test(
7650 &mut cx,
7651 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7652 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7653 );
7654 // Same thing, right-to-left
7655 test(
7656 &mut cx,
7657 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7658 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7659 );
7660
7661 // Starts at the end of a line, ends at the start of another
7662 test(
7663 &mut cx,
7664 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7665 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7666 );
7667}
7668
7669#[gpui::test]
7670async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7671 init_test(cx, |_| {});
7672
7673 let editor = cx.add_window(|window, cx| {
7674 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7675 build_editor(buffer, window, cx)
7676 });
7677
7678 // setup
7679 _ = editor.update(cx, |editor, window, cx| {
7680 editor.fold_creases(
7681 vec![
7682 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7683 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7684 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7685 ],
7686 true,
7687 window,
7688 cx,
7689 );
7690 assert_eq!(
7691 editor.display_text(cx),
7692 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7693 );
7694 });
7695
7696 _ = editor.update(cx, |editor, window, cx| {
7697 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7698 s.select_display_ranges([
7699 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7700 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7701 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7702 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7703 ])
7704 });
7705 editor.split_selection_into_lines(&Default::default(), window, cx);
7706 assert_eq!(
7707 editor.display_text(cx),
7708 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7709 );
7710 });
7711 EditorTestContext::for_editor(editor, cx)
7712 .await
7713 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7714
7715 _ = editor.update(cx, |editor, window, cx| {
7716 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7717 s.select_display_ranges([
7718 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7719 ])
7720 });
7721 editor.split_selection_into_lines(&Default::default(), window, cx);
7722 assert_eq!(
7723 editor.display_text(cx),
7724 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7725 );
7726 assert_eq!(
7727 display_ranges(editor, cx),
7728 [
7729 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7730 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7731 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7732 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7733 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7734 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7735 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7736 ]
7737 );
7738 });
7739 EditorTestContext::for_editor(editor, cx)
7740 .await
7741 .assert_editor_state(
7742 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7743 );
7744}
7745
7746#[gpui::test]
7747async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7748 init_test(cx, |_| {});
7749
7750 let mut cx = EditorTestContext::new(cx).await;
7751
7752 cx.set_state(indoc!(
7753 r#"abc
7754 defˇghi
7755
7756 jk
7757 nlmo
7758 "#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.add_selection_above(&Default::default(), window, cx);
7763 });
7764
7765 cx.assert_editor_state(indoc!(
7766 r#"abcˇ
7767 defˇghi
7768
7769 jk
7770 nlmo
7771 "#
7772 ));
7773
7774 cx.update_editor(|editor, window, cx| {
7775 editor.add_selection_above(&Default::default(), window, cx);
7776 });
7777
7778 cx.assert_editor_state(indoc!(
7779 r#"abcˇ
7780 defˇghi
7781
7782 jk
7783 nlmo
7784 "#
7785 ));
7786
7787 cx.update_editor(|editor, window, cx| {
7788 editor.add_selection_below(&Default::default(), window, cx);
7789 });
7790
7791 cx.assert_editor_state(indoc!(
7792 r#"abc
7793 defˇghi
7794
7795 jk
7796 nlmo
7797 "#
7798 ));
7799
7800 cx.update_editor(|editor, window, cx| {
7801 editor.undo_selection(&Default::default(), window, cx);
7802 });
7803
7804 cx.assert_editor_state(indoc!(
7805 r#"abcˇ
7806 defˇghi
7807
7808 jk
7809 nlmo
7810 "#
7811 ));
7812
7813 cx.update_editor(|editor, window, cx| {
7814 editor.redo_selection(&Default::default(), window, cx);
7815 });
7816
7817 cx.assert_editor_state(indoc!(
7818 r#"abc
7819 defˇghi
7820
7821 jk
7822 nlmo
7823 "#
7824 ));
7825
7826 cx.update_editor(|editor, window, cx| {
7827 editor.add_selection_below(&Default::default(), window, cx);
7828 });
7829
7830 cx.assert_editor_state(indoc!(
7831 r#"abc
7832 defˇghi
7833 ˇ
7834 jk
7835 nlmo
7836 "#
7837 ));
7838
7839 cx.update_editor(|editor, window, cx| {
7840 editor.add_selection_below(&Default::default(), window, cx);
7841 });
7842
7843 cx.assert_editor_state(indoc!(
7844 r#"abc
7845 defˇghi
7846 ˇ
7847 jkˇ
7848 nlmo
7849 "#
7850 ));
7851
7852 cx.update_editor(|editor, window, cx| {
7853 editor.add_selection_below(&Default::default(), window, cx);
7854 });
7855
7856 cx.assert_editor_state(indoc!(
7857 r#"abc
7858 defˇghi
7859 ˇ
7860 jkˇ
7861 nlmˇo
7862 "#
7863 ));
7864
7865 cx.update_editor(|editor, window, cx| {
7866 editor.add_selection_below(&Default::default(), window, cx);
7867 });
7868
7869 cx.assert_editor_state(indoc!(
7870 r#"abc
7871 defˇghi
7872 ˇ
7873 jkˇ
7874 nlmˇo
7875 ˇ"#
7876 ));
7877
7878 // change selections
7879 cx.set_state(indoc!(
7880 r#"abc
7881 def«ˇg»hi
7882
7883 jk
7884 nlmo
7885 "#
7886 ));
7887
7888 cx.update_editor(|editor, window, cx| {
7889 editor.add_selection_below(&Default::default(), window, cx);
7890 });
7891
7892 cx.assert_editor_state(indoc!(
7893 r#"abc
7894 def«ˇg»hi
7895
7896 jk
7897 nlm«ˇo»
7898 "#
7899 ));
7900
7901 cx.update_editor(|editor, window, cx| {
7902 editor.add_selection_below(&Default::default(), window, cx);
7903 });
7904
7905 cx.assert_editor_state(indoc!(
7906 r#"abc
7907 def«ˇg»hi
7908
7909 jk
7910 nlm«ˇo»
7911 "#
7912 ));
7913
7914 cx.update_editor(|editor, window, cx| {
7915 editor.add_selection_above(&Default::default(), window, cx);
7916 });
7917
7918 cx.assert_editor_state(indoc!(
7919 r#"abc
7920 def«ˇg»hi
7921
7922 jk
7923 nlmo
7924 "#
7925 ));
7926
7927 cx.update_editor(|editor, window, cx| {
7928 editor.add_selection_above(&Default::default(), window, cx);
7929 });
7930
7931 cx.assert_editor_state(indoc!(
7932 r#"abc
7933 def«ˇg»hi
7934
7935 jk
7936 nlmo
7937 "#
7938 ));
7939
7940 // Change selections again
7941 cx.set_state(indoc!(
7942 r#"a«bc
7943 defgˇ»hi
7944
7945 jk
7946 nlmo
7947 "#
7948 ));
7949
7950 cx.update_editor(|editor, window, cx| {
7951 editor.add_selection_below(&Default::default(), window, cx);
7952 });
7953
7954 cx.assert_editor_state(indoc!(
7955 r#"a«bcˇ»
7956 d«efgˇ»hi
7957
7958 j«kˇ»
7959 nlmo
7960 "#
7961 ));
7962
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_below(&Default::default(), window, cx);
7965 });
7966 cx.assert_editor_state(indoc!(
7967 r#"a«bcˇ»
7968 d«efgˇ»hi
7969
7970 j«kˇ»
7971 n«lmoˇ»
7972 "#
7973 ));
7974 cx.update_editor(|editor, window, cx| {
7975 editor.add_selection_above(&Default::default(), window, cx);
7976 });
7977
7978 cx.assert_editor_state(indoc!(
7979 r#"a«bcˇ»
7980 d«efgˇ»hi
7981
7982 j«kˇ»
7983 nlmo
7984 "#
7985 ));
7986
7987 // Change selections again
7988 cx.set_state(indoc!(
7989 r#"abc
7990 d«ˇefghi
7991
7992 jk
7993 nlm»o
7994 "#
7995 ));
7996
7997 cx.update_editor(|editor, window, cx| {
7998 editor.add_selection_above(&Default::default(), window, cx);
7999 });
8000
8001 cx.assert_editor_state(indoc!(
8002 r#"a«ˇbc»
8003 d«ˇef»ghi
8004
8005 j«ˇk»
8006 n«ˇlm»o
8007 "#
8008 ));
8009
8010 cx.update_editor(|editor, window, cx| {
8011 editor.add_selection_below(&Default::default(), window, cx);
8012 });
8013
8014 cx.assert_editor_state(indoc!(
8015 r#"abc
8016 d«ˇef»ghi
8017
8018 j«ˇk»
8019 n«ˇlm»o
8020 "#
8021 ));
8022}
8023
8024#[gpui::test]
8025async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8026 init_test(cx, |_| {});
8027 let mut cx = EditorTestContext::new(cx).await;
8028
8029 cx.set_state(indoc!(
8030 r#"line onˇe
8031 liˇne two
8032 line three
8033 line four"#
8034 ));
8035
8036 cx.update_editor(|editor, window, cx| {
8037 editor.add_selection_below(&Default::default(), window, cx);
8038 });
8039
8040 // test multiple cursors expand in the same direction
8041 cx.assert_editor_state(indoc!(
8042 r#"line onˇe
8043 liˇne twˇo
8044 liˇne three
8045 line four"#
8046 ));
8047
8048 cx.update_editor(|editor, window, cx| {
8049 editor.add_selection_below(&Default::default(), window, cx);
8050 });
8051
8052 cx.update_editor(|editor, window, cx| {
8053 editor.add_selection_below(&Default::default(), window, cx);
8054 });
8055
8056 // test multiple cursors expand below overflow
8057 cx.assert_editor_state(indoc!(
8058 r#"line onˇe
8059 liˇne twˇo
8060 liˇne thˇree
8061 liˇne foˇur"#
8062 ));
8063
8064 cx.update_editor(|editor, window, cx| {
8065 editor.add_selection_above(&Default::default(), window, cx);
8066 });
8067
8068 // test multiple cursors retrieves back correctly
8069 cx.assert_editor_state(indoc!(
8070 r#"line onˇe
8071 liˇne twˇo
8072 liˇne thˇree
8073 line four"#
8074 ));
8075
8076 cx.update_editor(|editor, window, cx| {
8077 editor.add_selection_above(&Default::default(), window, cx);
8078 });
8079
8080 cx.update_editor(|editor, window, cx| {
8081 editor.add_selection_above(&Default::default(), window, cx);
8082 });
8083
8084 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8085 cx.assert_editor_state(indoc!(
8086 r#"liˇne onˇe
8087 liˇne two
8088 line three
8089 line four"#
8090 ));
8091
8092 cx.update_editor(|editor, window, cx| {
8093 editor.undo_selection(&Default::default(), window, cx);
8094 });
8095
8096 // test undo
8097 cx.assert_editor_state(indoc!(
8098 r#"line onˇe
8099 liˇne twˇo
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.redo_selection(&Default::default(), window, cx);
8106 });
8107
8108 // test redo
8109 cx.assert_editor_state(indoc!(
8110 r#"liˇne onˇe
8111 liˇne two
8112 line three
8113 line four"#
8114 ));
8115
8116 cx.set_state(indoc!(
8117 r#"abcd
8118 ef«ghˇ»
8119 ijkl
8120 «mˇ»nop"#
8121 ));
8122
8123 cx.update_editor(|editor, window, cx| {
8124 editor.add_selection_above(&Default::default(), window, cx);
8125 });
8126
8127 // test multiple selections expand in the same direction
8128 cx.assert_editor_state(indoc!(
8129 r#"ab«cdˇ»
8130 ef«ghˇ»
8131 «iˇ»jkl
8132 «mˇ»nop"#
8133 ));
8134
8135 cx.update_editor(|editor, window, cx| {
8136 editor.add_selection_above(&Default::default(), window, cx);
8137 });
8138
8139 // test multiple selection upward overflow
8140 cx.assert_editor_state(indoc!(
8141 r#"ab«cdˇ»
8142 «eˇ»f«ghˇ»
8143 «iˇ»jkl
8144 «mˇ»nop"#
8145 ));
8146
8147 cx.update_editor(|editor, window, cx| {
8148 editor.add_selection_below(&Default::default(), window, cx);
8149 });
8150
8151 // test multiple selection retrieves back correctly
8152 cx.assert_editor_state(indoc!(
8153 r#"abcd
8154 ef«ghˇ»
8155 «iˇ»jkl
8156 «mˇ»nop"#
8157 ));
8158
8159 cx.update_editor(|editor, window, cx| {
8160 editor.add_selection_below(&Default::default(), window, cx);
8161 });
8162
8163 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8164 cx.assert_editor_state(indoc!(
8165 r#"abcd
8166 ef«ghˇ»
8167 ij«klˇ»
8168 «mˇ»nop"#
8169 ));
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.undo_selection(&Default::default(), window, cx);
8173 });
8174
8175 // test undo
8176 cx.assert_editor_state(indoc!(
8177 r#"abcd
8178 ef«ghˇ»
8179 «iˇ»jkl
8180 «mˇ»nop"#
8181 ));
8182
8183 cx.update_editor(|editor, window, cx| {
8184 editor.redo_selection(&Default::default(), window, cx);
8185 });
8186
8187 // test redo
8188 cx.assert_editor_state(indoc!(
8189 r#"abcd
8190 ef«ghˇ»
8191 ij«klˇ»
8192 «mˇ»nop"#
8193 ));
8194}
8195
8196#[gpui::test]
8197async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8198 init_test(cx, |_| {});
8199 let mut cx = EditorTestContext::new(cx).await;
8200
8201 cx.set_state(indoc!(
8202 r#"line onˇe
8203 liˇne two
8204 line three
8205 line four"#
8206 ));
8207
8208 cx.update_editor(|editor, window, cx| {
8209 editor.add_selection_below(&Default::default(), window, cx);
8210 editor.add_selection_below(&Default::default(), window, cx);
8211 editor.add_selection_below(&Default::default(), window, cx);
8212 });
8213
8214 // initial state with two multi cursor groups
8215 cx.assert_editor_state(indoc!(
8216 r#"line onˇe
8217 liˇne twˇo
8218 liˇne thˇree
8219 liˇne foˇur"#
8220 ));
8221
8222 // add single cursor in middle - simulate opt click
8223 cx.update_editor(|editor, window, cx| {
8224 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8225 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8226 editor.end_selection(window, cx);
8227 });
8228
8229 cx.assert_editor_state(indoc!(
8230 r#"line onˇe
8231 liˇne twˇo
8232 liˇneˇ thˇree
8233 liˇne foˇur"#
8234 ));
8235
8236 cx.update_editor(|editor, window, cx| {
8237 editor.add_selection_above(&Default::default(), window, cx);
8238 });
8239
8240 // test new added selection expands above and existing selection shrinks
8241 cx.assert_editor_state(indoc!(
8242 r#"line onˇe
8243 liˇneˇ twˇo
8244 liˇneˇ thˇree
8245 line four"#
8246 ));
8247
8248 cx.update_editor(|editor, window, cx| {
8249 editor.add_selection_above(&Default::default(), window, cx);
8250 });
8251
8252 // test new added selection expands above and existing selection shrinks
8253 cx.assert_editor_state(indoc!(
8254 r#"lineˇ onˇe
8255 liˇneˇ twˇo
8256 lineˇ three
8257 line four"#
8258 ));
8259
8260 // intial state with two selection groups
8261 cx.set_state(indoc!(
8262 r#"abcd
8263 ef«ghˇ»
8264 ijkl
8265 «mˇ»nop"#
8266 ));
8267
8268 cx.update_editor(|editor, window, cx| {
8269 editor.add_selection_above(&Default::default(), window, cx);
8270 editor.add_selection_above(&Default::default(), window, cx);
8271 });
8272
8273 cx.assert_editor_state(indoc!(
8274 r#"ab«cdˇ»
8275 «eˇ»f«ghˇ»
8276 «iˇ»jkl
8277 «mˇ»nop"#
8278 ));
8279
8280 // add single selection in middle - simulate opt drag
8281 cx.update_editor(|editor, window, cx| {
8282 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8283 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8284 editor.update_selection(
8285 DisplayPoint::new(DisplayRow(2), 4),
8286 0,
8287 gpui::Point::<f32>::default(),
8288 window,
8289 cx,
8290 );
8291 editor.end_selection(window, cx);
8292 });
8293
8294 cx.assert_editor_state(indoc!(
8295 r#"ab«cdˇ»
8296 «eˇ»f«ghˇ»
8297 «iˇ»jk«lˇ»
8298 «mˇ»nop"#
8299 ));
8300
8301 cx.update_editor(|editor, window, cx| {
8302 editor.add_selection_below(&Default::default(), window, cx);
8303 });
8304
8305 // test new added selection expands below, others shrinks from above
8306 cx.assert_editor_state(indoc!(
8307 r#"abcd
8308 ef«ghˇ»
8309 «iˇ»jk«lˇ»
8310 «mˇ»no«pˇ»"#
8311 ));
8312}
8313
8314#[gpui::test]
8315async fn test_select_next(cx: &mut TestAppContext) {
8316 init_test(cx, |_| {});
8317 let mut cx = EditorTestContext::new(cx).await;
8318
8319 // Enable case sensitive search.
8320 update_test_editor_settings(&mut cx, |settings| {
8321 let mut search_settings = SearchSettingsContent::default();
8322 search_settings.case_sensitive = Some(true);
8323 settings.search = Some(search_settings);
8324 });
8325
8326 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8327
8328 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8329 .unwrap();
8330 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8331
8332 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8333 .unwrap();
8334 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8335
8336 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8337 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8338
8339 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8340 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8341
8342 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8343 .unwrap();
8344 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8345
8346 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8347 .unwrap();
8348 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8349
8350 // Test selection direction should be preserved
8351 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8352
8353 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8354 .unwrap();
8355 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8356
8357 // Test case sensitivity
8358 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8359 cx.update_editor(|e, window, cx| {
8360 e.select_next(&SelectNext::default(), window, cx).unwrap();
8361 });
8362 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8363
8364 // Disable case sensitive search.
8365 update_test_editor_settings(&mut cx, |settings| {
8366 let mut search_settings = SearchSettingsContent::default();
8367 search_settings.case_sensitive = Some(false);
8368 settings.search = Some(search_settings);
8369 });
8370
8371 cx.set_state("«ˇfoo»\nFOO\nFoo");
8372 cx.update_editor(|e, window, cx| {
8373 e.select_next(&SelectNext::default(), window, cx).unwrap();
8374 e.select_next(&SelectNext::default(), window, cx).unwrap();
8375 });
8376 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8377}
8378
8379#[gpui::test]
8380async fn test_select_all_matches(cx: &mut TestAppContext) {
8381 init_test(cx, |_| {});
8382 let mut cx = EditorTestContext::new(cx).await;
8383
8384 // Enable case sensitive search.
8385 update_test_editor_settings(&mut cx, |settings| {
8386 let mut search_settings = SearchSettingsContent::default();
8387 search_settings.case_sensitive = Some(true);
8388 settings.search = Some(search_settings);
8389 });
8390
8391 // Test caret-only selections
8392 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8393 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8394 .unwrap();
8395 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8396
8397 // Test left-to-right selections
8398 cx.set_state("abc\n«abcˇ»\nabc");
8399 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8400 .unwrap();
8401 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8402
8403 // Test right-to-left selections
8404 cx.set_state("abc\n«ˇabc»\nabc");
8405 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8406 .unwrap();
8407 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8408
8409 // Test selecting whitespace with caret selection
8410 cx.set_state("abc\nˇ abc\nabc");
8411 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8412 .unwrap();
8413 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8414
8415 // Test selecting whitespace with left-to-right selection
8416 cx.set_state("abc\n«ˇ »abc\nabc");
8417 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8418 .unwrap();
8419 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8420
8421 // Test no matches with right-to-left selection
8422 cx.set_state("abc\n« ˇ»abc\nabc");
8423 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8424 .unwrap();
8425 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8426
8427 // Test with a single word and clip_at_line_ends=true (#29823)
8428 cx.set_state("aˇbc");
8429 cx.update_editor(|e, window, cx| {
8430 e.set_clip_at_line_ends(true, cx);
8431 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8432 e.set_clip_at_line_ends(false, cx);
8433 });
8434 cx.assert_editor_state("«abcˇ»");
8435
8436 // Test case sensitivity
8437 cx.set_state("fˇoo\nFOO\nFoo");
8438 cx.update_editor(|e, window, cx| {
8439 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8440 });
8441 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8442
8443 // Disable case sensitive search.
8444 update_test_editor_settings(&mut cx, |settings| {
8445 let mut search_settings = SearchSettingsContent::default();
8446 search_settings.case_sensitive = Some(false);
8447 settings.search = Some(search_settings);
8448 });
8449
8450 cx.set_state("fˇoo\nFOO\nFoo");
8451 cx.update_editor(|e, window, cx| {
8452 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8453 });
8454 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8455}
8456
8457#[gpui::test]
8458async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8459 init_test(cx, |_| {});
8460
8461 let mut cx = EditorTestContext::new(cx).await;
8462
8463 let large_body_1 = "\nd".repeat(200);
8464 let large_body_2 = "\ne".repeat(200);
8465
8466 cx.set_state(&format!(
8467 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8468 ));
8469 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8470 let scroll_position = editor.scroll_position(cx);
8471 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8472 scroll_position
8473 });
8474
8475 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8476 .unwrap();
8477 cx.assert_editor_state(&format!(
8478 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8479 ));
8480 let scroll_position_after_selection =
8481 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8482 assert_eq!(
8483 initial_scroll_position, scroll_position_after_selection,
8484 "Scroll position should not change after selecting all matches"
8485 );
8486}
8487
8488#[gpui::test]
8489async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8490 init_test(cx, |_| {});
8491
8492 let mut cx = EditorLspTestContext::new_rust(
8493 lsp::ServerCapabilities {
8494 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8495 ..Default::default()
8496 },
8497 cx,
8498 )
8499 .await;
8500
8501 cx.set_state(indoc! {"
8502 line 1
8503 line 2
8504 linˇe 3
8505 line 4
8506 line 5
8507 "});
8508
8509 // Make an edit
8510 cx.update_editor(|editor, window, cx| {
8511 editor.handle_input("X", window, cx);
8512 });
8513
8514 // Move cursor to a different position
8515 cx.update_editor(|editor, window, cx| {
8516 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8517 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8518 });
8519 });
8520
8521 cx.assert_editor_state(indoc! {"
8522 line 1
8523 line 2
8524 linXe 3
8525 line 4
8526 liˇne 5
8527 "});
8528
8529 cx.lsp
8530 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8531 Ok(Some(vec![lsp::TextEdit::new(
8532 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8533 "PREFIX ".to_string(),
8534 )]))
8535 });
8536
8537 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8538 .unwrap()
8539 .await
8540 .unwrap();
8541
8542 cx.assert_editor_state(indoc! {"
8543 PREFIX line 1
8544 line 2
8545 linXe 3
8546 line 4
8547 liˇne 5
8548 "});
8549
8550 // Undo formatting
8551 cx.update_editor(|editor, window, cx| {
8552 editor.undo(&Default::default(), window, cx);
8553 });
8554
8555 // Verify cursor moved back to position after edit
8556 cx.assert_editor_state(indoc! {"
8557 line 1
8558 line 2
8559 linXˇe 3
8560 line 4
8561 line 5
8562 "});
8563}
8564
8565#[gpui::test]
8566async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8567 init_test(cx, |_| {});
8568
8569 let mut cx = EditorTestContext::new(cx).await;
8570
8571 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8572 cx.update_editor(|editor, window, cx| {
8573 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8574 });
8575
8576 cx.set_state(indoc! {"
8577 line 1
8578 line 2
8579 linˇe 3
8580 line 4
8581 line 5
8582 line 6
8583 line 7
8584 line 8
8585 line 9
8586 line 10
8587 "});
8588
8589 let snapshot = cx.buffer_snapshot();
8590 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8591
8592 cx.update(|_, cx| {
8593 provider.update(cx, |provider, _| {
8594 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8595 id: None,
8596 edits: vec![(edit_position..edit_position, "X".into())],
8597 edit_preview: None,
8598 }))
8599 })
8600 });
8601
8602 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8603 cx.update_editor(|editor, window, cx| {
8604 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8605 });
8606
8607 cx.assert_editor_state(indoc! {"
8608 line 1
8609 line 2
8610 lineXˇ 3
8611 line 4
8612 line 5
8613 line 6
8614 line 7
8615 line 8
8616 line 9
8617 line 10
8618 "});
8619
8620 cx.update_editor(|editor, window, cx| {
8621 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8622 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8623 });
8624 });
8625
8626 cx.assert_editor_state(indoc! {"
8627 line 1
8628 line 2
8629 lineX 3
8630 line 4
8631 line 5
8632 line 6
8633 line 7
8634 line 8
8635 line 9
8636 liˇne 10
8637 "});
8638
8639 cx.update_editor(|editor, window, cx| {
8640 editor.undo(&Default::default(), window, cx);
8641 });
8642
8643 cx.assert_editor_state(indoc! {"
8644 line 1
8645 line 2
8646 lineˇ 3
8647 line 4
8648 line 5
8649 line 6
8650 line 7
8651 line 8
8652 line 9
8653 line 10
8654 "});
8655}
8656
8657#[gpui::test]
8658async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8659 init_test(cx, |_| {});
8660
8661 let mut cx = EditorTestContext::new(cx).await;
8662 cx.set_state(
8663 r#"let foo = 2;
8664lˇet foo = 2;
8665let fooˇ = 2;
8666let foo = 2;
8667let foo = ˇ2;"#,
8668 );
8669
8670 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8671 .unwrap();
8672 cx.assert_editor_state(
8673 r#"let foo = 2;
8674«letˇ» foo = 2;
8675let «fooˇ» = 2;
8676let foo = 2;
8677let foo = «2ˇ»;"#,
8678 );
8679
8680 // noop for multiple selections with different contents
8681 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8682 .unwrap();
8683 cx.assert_editor_state(
8684 r#"let foo = 2;
8685«letˇ» foo = 2;
8686let «fooˇ» = 2;
8687let foo = 2;
8688let foo = «2ˇ»;"#,
8689 );
8690
8691 // Test last selection direction should be preserved
8692 cx.set_state(
8693 r#"let foo = 2;
8694let foo = 2;
8695let «fooˇ» = 2;
8696let «ˇfoo» = 2;
8697let foo = 2;"#,
8698 );
8699
8700 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8701 .unwrap();
8702 cx.assert_editor_state(
8703 r#"let foo = 2;
8704let foo = 2;
8705let «fooˇ» = 2;
8706let «ˇfoo» = 2;
8707let «ˇfoo» = 2;"#,
8708 );
8709}
8710
8711#[gpui::test]
8712async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8713 init_test(cx, |_| {});
8714
8715 let mut cx =
8716 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8717
8718 cx.assert_editor_state(indoc! {"
8719 ˇbbb
8720 ccc
8721
8722 bbb
8723 ccc
8724 "});
8725 cx.dispatch_action(SelectPrevious::default());
8726 cx.assert_editor_state(indoc! {"
8727 «bbbˇ»
8728 ccc
8729
8730 bbb
8731 ccc
8732 "});
8733 cx.dispatch_action(SelectPrevious::default());
8734 cx.assert_editor_state(indoc! {"
8735 «bbbˇ»
8736 ccc
8737
8738 «bbbˇ»
8739 ccc
8740 "});
8741}
8742
8743#[gpui::test]
8744async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8745 init_test(cx, |_| {});
8746
8747 let mut cx = EditorTestContext::new(cx).await;
8748 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8749
8750 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8751 .unwrap();
8752 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8753
8754 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8755 .unwrap();
8756 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8757
8758 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8759 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8760
8761 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8762 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8763
8764 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8765 .unwrap();
8766 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8767
8768 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8769 .unwrap();
8770 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8771}
8772
8773#[gpui::test]
8774async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8775 init_test(cx, |_| {});
8776
8777 let mut cx = EditorTestContext::new(cx).await;
8778 cx.set_state("aˇ");
8779
8780 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8781 .unwrap();
8782 cx.assert_editor_state("«aˇ»");
8783 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8784 .unwrap();
8785 cx.assert_editor_state("«aˇ»");
8786}
8787
8788#[gpui::test]
8789async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8790 init_test(cx, |_| {});
8791
8792 let mut cx = EditorTestContext::new(cx).await;
8793 cx.set_state(
8794 r#"let foo = 2;
8795lˇet foo = 2;
8796let fooˇ = 2;
8797let foo = 2;
8798let foo = ˇ2;"#,
8799 );
8800
8801 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8802 .unwrap();
8803 cx.assert_editor_state(
8804 r#"let foo = 2;
8805«letˇ» foo = 2;
8806let «fooˇ» = 2;
8807let foo = 2;
8808let foo = «2ˇ»;"#,
8809 );
8810
8811 // noop for multiple selections with different contents
8812 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8813 .unwrap();
8814 cx.assert_editor_state(
8815 r#"let foo = 2;
8816«letˇ» foo = 2;
8817let «fooˇ» = 2;
8818let foo = 2;
8819let foo = «2ˇ»;"#,
8820 );
8821}
8822
8823#[gpui::test]
8824async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8825 init_test(cx, |_| {});
8826 let mut cx = EditorTestContext::new(cx).await;
8827
8828 // Enable case sensitive search.
8829 update_test_editor_settings(&mut cx, |settings| {
8830 let mut search_settings = SearchSettingsContent::default();
8831 search_settings.case_sensitive = Some(true);
8832 settings.search = Some(search_settings);
8833 });
8834
8835 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8836
8837 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8838 .unwrap();
8839 // selection direction is preserved
8840 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8841
8842 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8843 .unwrap();
8844 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8845
8846 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8847 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8848
8849 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8850 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8851
8852 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8853 .unwrap();
8854 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8855
8856 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8857 .unwrap();
8858 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8859
8860 // Test case sensitivity
8861 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
8862 cx.update_editor(|e, window, cx| {
8863 e.select_previous(&SelectPrevious::default(), window, cx)
8864 .unwrap();
8865 e.select_previous(&SelectPrevious::default(), window, cx)
8866 .unwrap();
8867 });
8868 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8869
8870 // Disable case sensitive search.
8871 update_test_editor_settings(&mut cx, |settings| {
8872 let mut search_settings = SearchSettingsContent::default();
8873 search_settings.case_sensitive = Some(false);
8874 settings.search = Some(search_settings);
8875 });
8876
8877 cx.set_state("foo\nFOO\n«ˇFoo»");
8878 cx.update_editor(|e, window, cx| {
8879 e.select_previous(&SelectPrevious::default(), window, cx)
8880 .unwrap();
8881 e.select_previous(&SelectPrevious::default(), window, cx)
8882 .unwrap();
8883 });
8884 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8885}
8886
8887#[gpui::test]
8888async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8889 init_test(cx, |_| {});
8890
8891 let language = Arc::new(Language::new(
8892 LanguageConfig::default(),
8893 Some(tree_sitter_rust::LANGUAGE.into()),
8894 ));
8895
8896 let text = r#"
8897 use mod1::mod2::{mod3, mod4};
8898
8899 fn fn_1(param1: bool, param2: &str) {
8900 let var1 = "text";
8901 }
8902 "#
8903 .unindent();
8904
8905 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8906 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8907 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8908
8909 editor
8910 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8911 .await;
8912
8913 editor.update_in(cx, |editor, window, cx| {
8914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8915 s.select_display_ranges([
8916 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8917 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8918 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8919 ]);
8920 });
8921 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8922 });
8923 editor.update(cx, |editor, cx| {
8924 assert_text_with_selections(
8925 editor,
8926 indoc! {r#"
8927 use mod1::mod2::{mod3, «mod4ˇ»};
8928
8929 fn fn_1«ˇ(param1: bool, param2: &str)» {
8930 let var1 = "«ˇtext»";
8931 }
8932 "#},
8933 cx,
8934 );
8935 });
8936
8937 editor.update_in(cx, |editor, window, cx| {
8938 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8939 });
8940 editor.update(cx, |editor, cx| {
8941 assert_text_with_selections(
8942 editor,
8943 indoc! {r#"
8944 use mod1::mod2::«{mod3, mod4}ˇ»;
8945
8946 «ˇfn fn_1(param1: bool, param2: &str) {
8947 let var1 = "text";
8948 }»
8949 "#},
8950 cx,
8951 );
8952 });
8953
8954 editor.update_in(cx, |editor, window, cx| {
8955 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8956 });
8957 assert_eq!(
8958 editor.update(cx, |editor, cx| editor
8959 .selections
8960 .display_ranges(&editor.display_snapshot(cx))),
8961 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8962 );
8963
8964 // Trying to expand the selected syntax node one more time has no effect.
8965 editor.update_in(cx, |editor, window, cx| {
8966 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8967 });
8968 assert_eq!(
8969 editor.update(cx, |editor, cx| editor
8970 .selections
8971 .display_ranges(&editor.display_snapshot(cx))),
8972 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8973 );
8974
8975 editor.update_in(cx, |editor, window, cx| {
8976 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8977 });
8978 editor.update(cx, |editor, cx| {
8979 assert_text_with_selections(
8980 editor,
8981 indoc! {r#"
8982 use mod1::mod2::«{mod3, mod4}ˇ»;
8983
8984 «ˇfn fn_1(param1: bool, param2: &str) {
8985 let var1 = "text";
8986 }»
8987 "#},
8988 cx,
8989 );
8990 });
8991
8992 editor.update_in(cx, |editor, window, cx| {
8993 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8994 });
8995 editor.update(cx, |editor, cx| {
8996 assert_text_with_selections(
8997 editor,
8998 indoc! {r#"
8999 use mod1::mod2::{mod3, «mod4ˇ»};
9000
9001 fn fn_1«ˇ(param1: bool, param2: &str)» {
9002 let var1 = "«ˇtext»";
9003 }
9004 "#},
9005 cx,
9006 );
9007 });
9008
9009 editor.update_in(cx, |editor, window, cx| {
9010 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9011 });
9012 editor.update(cx, |editor, cx| {
9013 assert_text_with_selections(
9014 editor,
9015 indoc! {r#"
9016 use mod1::mod2::{mod3, moˇd4};
9017
9018 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9019 let var1 = "teˇxt";
9020 }
9021 "#},
9022 cx,
9023 );
9024 });
9025
9026 // Trying to shrink the selected syntax node one more time has no effect.
9027 editor.update_in(cx, |editor, window, cx| {
9028 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9029 });
9030 editor.update_in(cx, |editor, _, cx| {
9031 assert_text_with_selections(
9032 editor,
9033 indoc! {r#"
9034 use mod1::mod2::{mod3, moˇd4};
9035
9036 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9037 let var1 = "teˇxt";
9038 }
9039 "#},
9040 cx,
9041 );
9042 });
9043
9044 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9045 // a fold.
9046 editor.update_in(cx, |editor, window, cx| {
9047 editor.fold_creases(
9048 vec![
9049 Crease::simple(
9050 Point::new(0, 21)..Point::new(0, 24),
9051 FoldPlaceholder::test(),
9052 ),
9053 Crease::simple(
9054 Point::new(3, 20)..Point::new(3, 22),
9055 FoldPlaceholder::test(),
9056 ),
9057 ],
9058 true,
9059 window,
9060 cx,
9061 );
9062 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9063 });
9064 editor.update(cx, |editor, cx| {
9065 assert_text_with_selections(
9066 editor,
9067 indoc! {r#"
9068 use mod1::mod2::«{mod3, mod4}ˇ»;
9069
9070 fn fn_1«ˇ(param1: bool, param2: &str)» {
9071 let var1 = "«ˇtext»";
9072 }
9073 "#},
9074 cx,
9075 );
9076 });
9077}
9078
9079#[gpui::test]
9080async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9081 init_test(cx, |_| {});
9082
9083 let language = Arc::new(Language::new(
9084 LanguageConfig::default(),
9085 Some(tree_sitter_rust::LANGUAGE.into()),
9086 ));
9087
9088 let text = "let a = 2;";
9089
9090 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9091 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9092 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9093
9094 editor
9095 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9096 .await;
9097
9098 // Test case 1: Cursor at end of word
9099 editor.update_in(cx, |editor, window, cx| {
9100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9101 s.select_display_ranges([
9102 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9103 ]);
9104 });
9105 });
9106 editor.update(cx, |editor, cx| {
9107 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9108 });
9109 editor.update_in(cx, |editor, window, cx| {
9110 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9111 });
9112 editor.update(cx, |editor, cx| {
9113 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9114 });
9115 editor.update_in(cx, |editor, window, cx| {
9116 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9117 });
9118 editor.update(cx, |editor, cx| {
9119 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9120 });
9121
9122 // Test case 2: Cursor at end of statement
9123 editor.update_in(cx, |editor, window, cx| {
9124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9125 s.select_display_ranges([
9126 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9127 ]);
9128 });
9129 });
9130 editor.update(cx, |editor, cx| {
9131 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
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(editor, "«ˇlet a = 2;»", cx);
9138 });
9139}
9140
9141#[gpui::test]
9142async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9143 init_test(cx, |_| {});
9144
9145 let language = Arc::new(Language::new(
9146 LanguageConfig {
9147 name: "JavaScript".into(),
9148 ..Default::default()
9149 },
9150 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9151 ));
9152
9153 let text = r#"
9154 let a = {
9155 key: "value",
9156 };
9157 "#
9158 .unindent();
9159
9160 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9161 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9162 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9163
9164 editor
9165 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9166 .await;
9167
9168 // Test case 1: Cursor after '{'
9169 editor.update_in(cx, |editor, window, cx| {
9170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9171 s.select_display_ranges([
9172 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9173 ]);
9174 });
9175 });
9176 editor.update(cx, |editor, cx| {
9177 assert_text_with_selections(
9178 editor,
9179 indoc! {r#"
9180 let a = {ˇ
9181 key: "value",
9182 };
9183 "#},
9184 cx,
9185 );
9186 });
9187 editor.update_in(cx, |editor, window, cx| {
9188 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9189 });
9190 editor.update(cx, |editor, cx| {
9191 assert_text_with_selections(
9192 editor,
9193 indoc! {r#"
9194 let a = «ˇ{
9195 key: "value",
9196 }»;
9197 "#},
9198 cx,
9199 );
9200 });
9201
9202 // Test case 2: Cursor after ':'
9203 editor.update_in(cx, |editor, window, cx| {
9204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9205 s.select_display_ranges([
9206 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9207 ]);
9208 });
9209 });
9210 editor.update(cx, |editor, cx| {
9211 assert_text_with_selections(
9212 editor,
9213 indoc! {r#"
9214 let a = {
9215 key:ˇ "value",
9216 };
9217 "#},
9218 cx,
9219 );
9220 });
9221 editor.update_in(cx, |editor, window, cx| {
9222 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9223 });
9224 editor.update(cx, |editor, cx| {
9225 assert_text_with_selections(
9226 editor,
9227 indoc! {r#"
9228 let a = {
9229 «ˇkey: "value"»,
9230 };
9231 "#},
9232 cx,
9233 );
9234 });
9235 editor.update_in(cx, |editor, window, cx| {
9236 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9237 });
9238 editor.update(cx, |editor, cx| {
9239 assert_text_with_selections(
9240 editor,
9241 indoc! {r#"
9242 let a = «ˇ{
9243 key: "value",
9244 }»;
9245 "#},
9246 cx,
9247 );
9248 });
9249
9250 // Test case 3: Cursor after ','
9251 editor.update_in(cx, |editor, window, cx| {
9252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9253 s.select_display_ranges([
9254 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9255 ]);
9256 });
9257 });
9258 editor.update(cx, |editor, cx| {
9259 assert_text_with_selections(
9260 editor,
9261 indoc! {r#"
9262 let a = {
9263 key: "value",ˇ
9264 };
9265 "#},
9266 cx,
9267 );
9268 });
9269 editor.update_in(cx, |editor, window, cx| {
9270 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9271 });
9272 editor.update(cx, |editor, cx| {
9273 assert_text_with_selections(
9274 editor,
9275 indoc! {r#"
9276 let a = «ˇ{
9277 key: "value",
9278 }»;
9279 "#},
9280 cx,
9281 );
9282 });
9283
9284 // Test case 4: Cursor after ';'
9285 editor.update_in(cx, |editor, window, cx| {
9286 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9287 s.select_display_ranges([
9288 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9289 ]);
9290 });
9291 });
9292 editor.update(cx, |editor, cx| {
9293 assert_text_with_selections(
9294 editor,
9295 indoc! {r#"
9296 let a = {
9297 key: "value",
9298 };ˇ
9299 "#},
9300 cx,
9301 );
9302 });
9303 editor.update_in(cx, |editor, window, cx| {
9304 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9305 });
9306 editor.update(cx, |editor, cx| {
9307 assert_text_with_selections(
9308 editor,
9309 indoc! {r#"
9310 «ˇlet a = {
9311 key: "value",
9312 };
9313 »"#},
9314 cx,
9315 );
9316 });
9317}
9318
9319#[gpui::test]
9320async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9321 init_test(cx, |_| {});
9322
9323 let language = Arc::new(Language::new(
9324 LanguageConfig::default(),
9325 Some(tree_sitter_rust::LANGUAGE.into()),
9326 ));
9327
9328 let text = r#"
9329 use mod1::mod2::{mod3, mod4};
9330
9331 fn fn_1(param1: bool, param2: &str) {
9332 let var1 = "hello world";
9333 }
9334 "#
9335 .unindent();
9336
9337 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9339 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9340
9341 editor
9342 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9343 .await;
9344
9345 // Test 1: Cursor on a letter of a string word
9346 editor.update_in(cx, |editor, window, cx| {
9347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9348 s.select_display_ranges([
9349 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9350 ]);
9351 });
9352 });
9353 editor.update_in(cx, |editor, window, cx| {
9354 assert_text_with_selections(
9355 editor,
9356 indoc! {r#"
9357 use mod1::mod2::{mod3, mod4};
9358
9359 fn fn_1(param1: bool, param2: &str) {
9360 let var1 = "hˇello world";
9361 }
9362 "#},
9363 cx,
9364 );
9365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9366 assert_text_with_selections(
9367 editor,
9368 indoc! {r#"
9369 use mod1::mod2::{mod3, mod4};
9370
9371 fn fn_1(param1: bool, param2: &str) {
9372 let var1 = "«ˇhello» world";
9373 }
9374 "#},
9375 cx,
9376 );
9377 });
9378
9379 // Test 2: Partial selection within a word
9380 editor.update_in(cx, |editor, window, cx| {
9381 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9382 s.select_display_ranges([
9383 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9384 ]);
9385 });
9386 });
9387 editor.update_in(cx, |editor, window, cx| {
9388 assert_text_with_selections(
9389 editor,
9390 indoc! {r#"
9391 use mod1::mod2::{mod3, mod4};
9392
9393 fn fn_1(param1: bool, param2: &str) {
9394 let var1 = "h«elˇ»lo world";
9395 }
9396 "#},
9397 cx,
9398 );
9399 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9400 assert_text_with_selections(
9401 editor,
9402 indoc! {r#"
9403 use mod1::mod2::{mod3, mod4};
9404
9405 fn fn_1(param1: bool, param2: &str) {
9406 let var1 = "«ˇhello» world";
9407 }
9408 "#},
9409 cx,
9410 );
9411 });
9412
9413 // Test 3: Complete word already selected
9414 editor.update_in(cx, |editor, window, cx| {
9415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9416 s.select_display_ranges([
9417 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9418 ]);
9419 });
9420 });
9421 editor.update_in(cx, |editor, window, cx| {
9422 assert_text_with_selections(
9423 editor,
9424 indoc! {r#"
9425 use mod1::mod2::{mod3, mod4};
9426
9427 fn fn_1(param1: bool, param2: &str) {
9428 let var1 = "«helloˇ» world";
9429 }
9430 "#},
9431 cx,
9432 );
9433 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9434 assert_text_with_selections(
9435 editor,
9436 indoc! {r#"
9437 use mod1::mod2::{mod3, mod4};
9438
9439 fn fn_1(param1: bool, param2: &str) {
9440 let var1 = "«hello worldˇ»";
9441 }
9442 "#},
9443 cx,
9444 );
9445 });
9446
9447 // Test 4: Selection spanning across words
9448 editor.update_in(cx, |editor, window, cx| {
9449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9450 s.select_display_ranges([
9451 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9452 ]);
9453 });
9454 });
9455 editor.update_in(cx, |editor, window, cx| {
9456 assert_text_with_selections(
9457 editor,
9458 indoc! {r#"
9459 use mod1::mod2::{mod3, mod4};
9460
9461 fn fn_1(param1: bool, param2: &str) {
9462 let var1 = "hel«lo woˇ»rld";
9463 }
9464 "#},
9465 cx,
9466 );
9467 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9468 assert_text_with_selections(
9469 editor,
9470 indoc! {r#"
9471 use mod1::mod2::{mod3, mod4};
9472
9473 fn fn_1(param1: bool, param2: &str) {
9474 let var1 = "«ˇhello world»";
9475 }
9476 "#},
9477 cx,
9478 );
9479 });
9480
9481 // Test 5: Expansion beyond string
9482 editor.update_in(cx, |editor, window, cx| {
9483 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9485 assert_text_with_selections(
9486 editor,
9487 indoc! {r#"
9488 use mod1::mod2::{mod3, mod4};
9489
9490 fn fn_1(param1: bool, param2: &str) {
9491 «ˇlet var1 = "hello world";»
9492 }
9493 "#},
9494 cx,
9495 );
9496 });
9497}
9498
9499#[gpui::test]
9500async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9501 init_test(cx, |_| {});
9502
9503 let mut cx = EditorTestContext::new(cx).await;
9504
9505 let language = Arc::new(Language::new(
9506 LanguageConfig::default(),
9507 Some(tree_sitter_rust::LANGUAGE.into()),
9508 ));
9509
9510 cx.update_buffer(|buffer, cx| {
9511 buffer.set_language(Some(language), cx);
9512 });
9513
9514 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9515 cx.update_editor(|editor, window, cx| {
9516 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9517 });
9518
9519 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9520
9521 cx.set_state(indoc! { r#"fn a() {
9522 // what
9523 // a
9524 // ˇlong
9525 // method
9526 // I
9527 // sure
9528 // hope
9529 // it
9530 // works
9531 }"# });
9532
9533 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9534 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9535 cx.update(|_, cx| {
9536 multi_buffer.update(cx, |multi_buffer, cx| {
9537 multi_buffer.set_excerpts_for_path(
9538 PathKey::for_buffer(&buffer, cx),
9539 buffer,
9540 [Point::new(1, 0)..Point::new(1, 0)],
9541 3,
9542 cx,
9543 );
9544 });
9545 });
9546
9547 let editor2 = cx.new_window_entity(|window, cx| {
9548 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9549 });
9550
9551 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9552 cx.update_editor(|editor, window, cx| {
9553 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9554 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9555 })
9556 });
9557
9558 cx.assert_editor_state(indoc! { "
9559 fn a() {
9560 // what
9561 // a
9562 ˇ // long
9563 // method"});
9564
9565 cx.update_editor(|editor, window, cx| {
9566 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9567 });
9568
9569 // Although we could potentially make the action work when the syntax node
9570 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9571 // did. Maybe we could also expand the excerpt to contain the range?
9572 cx.assert_editor_state(indoc! { "
9573 fn a() {
9574 // what
9575 // a
9576 ˇ // long
9577 // method"});
9578}
9579
9580#[gpui::test]
9581async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9582 init_test(cx, |_| {});
9583
9584 let base_text = r#"
9585 impl A {
9586 // this is an uncommitted comment
9587
9588 fn b() {
9589 c();
9590 }
9591
9592 // this is another uncommitted comment
9593
9594 fn d() {
9595 // e
9596 // f
9597 }
9598 }
9599
9600 fn g() {
9601 // h
9602 }
9603 "#
9604 .unindent();
9605
9606 let text = r#"
9607 ˇimpl A {
9608
9609 fn b() {
9610 c();
9611 }
9612
9613 fn d() {
9614 // e
9615 // f
9616 }
9617 }
9618
9619 fn g() {
9620 // h
9621 }
9622 "#
9623 .unindent();
9624
9625 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9626 cx.set_state(&text);
9627 cx.set_head_text(&base_text);
9628 cx.update_editor(|editor, window, cx| {
9629 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9630 });
9631
9632 cx.assert_state_with_diff(
9633 "
9634 ˇimpl A {
9635 - // this is an uncommitted comment
9636
9637 fn b() {
9638 c();
9639 }
9640
9641 - // this is another uncommitted comment
9642 -
9643 fn d() {
9644 // e
9645 // f
9646 }
9647 }
9648
9649 fn g() {
9650 // h
9651 }
9652 "
9653 .unindent(),
9654 );
9655
9656 let expected_display_text = "
9657 impl A {
9658 // this is an uncommitted comment
9659
9660 fn b() {
9661 ⋯
9662 }
9663
9664 // this is another uncommitted comment
9665
9666 fn d() {
9667 ⋯
9668 }
9669 }
9670
9671 fn g() {
9672 ⋯
9673 }
9674 "
9675 .unindent();
9676
9677 cx.update_editor(|editor, window, cx| {
9678 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9679 assert_eq!(editor.display_text(cx), expected_display_text);
9680 });
9681}
9682
9683#[gpui::test]
9684async fn test_autoindent(cx: &mut TestAppContext) {
9685 init_test(cx, |_| {});
9686
9687 let language = Arc::new(
9688 Language::new(
9689 LanguageConfig {
9690 brackets: BracketPairConfig {
9691 pairs: vec![
9692 BracketPair {
9693 start: "{".to_string(),
9694 end: "}".to_string(),
9695 close: false,
9696 surround: false,
9697 newline: true,
9698 },
9699 BracketPair {
9700 start: "(".to_string(),
9701 end: ")".to_string(),
9702 close: false,
9703 surround: false,
9704 newline: true,
9705 },
9706 ],
9707 ..Default::default()
9708 },
9709 ..Default::default()
9710 },
9711 Some(tree_sitter_rust::LANGUAGE.into()),
9712 )
9713 .with_indents_query(
9714 r#"
9715 (_ "(" ")" @end) @indent
9716 (_ "{" "}" @end) @indent
9717 "#,
9718 )
9719 .unwrap(),
9720 );
9721
9722 let text = "fn a() {}";
9723
9724 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9725 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9726 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9727 editor
9728 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9729 .await;
9730
9731 editor.update_in(cx, |editor, window, cx| {
9732 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9733 s.select_ranges([5..5, 8..8, 9..9])
9734 });
9735 editor.newline(&Newline, window, cx);
9736 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9737 assert_eq!(
9738 editor.selections.ranges(&editor.display_snapshot(cx)),
9739 &[
9740 Point::new(1, 4)..Point::new(1, 4),
9741 Point::new(3, 4)..Point::new(3, 4),
9742 Point::new(5, 0)..Point::new(5, 0)
9743 ]
9744 );
9745 });
9746}
9747
9748#[gpui::test]
9749async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9750 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9751
9752 let language = Arc::new(
9753 Language::new(
9754 LanguageConfig {
9755 brackets: BracketPairConfig {
9756 pairs: vec![
9757 BracketPair {
9758 start: "{".to_string(),
9759 end: "}".to_string(),
9760 close: false,
9761 surround: false,
9762 newline: true,
9763 },
9764 BracketPair {
9765 start: "(".to_string(),
9766 end: ")".to_string(),
9767 close: false,
9768 surround: false,
9769 newline: true,
9770 },
9771 ],
9772 ..Default::default()
9773 },
9774 ..Default::default()
9775 },
9776 Some(tree_sitter_rust::LANGUAGE.into()),
9777 )
9778 .with_indents_query(
9779 r#"
9780 (_ "(" ")" @end) @indent
9781 (_ "{" "}" @end) @indent
9782 "#,
9783 )
9784 .unwrap(),
9785 );
9786
9787 let text = "fn a() {}";
9788
9789 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9790 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9791 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9792 editor
9793 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9794 .await;
9795
9796 editor.update_in(cx, |editor, window, cx| {
9797 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9798 s.select_ranges([5..5, 8..8, 9..9])
9799 });
9800 editor.newline(&Newline, window, cx);
9801 assert_eq!(
9802 editor.text(cx),
9803 indoc!(
9804 "
9805 fn a(
9806
9807 ) {
9808
9809 }
9810 "
9811 )
9812 );
9813 assert_eq!(
9814 editor.selections.ranges(&editor.display_snapshot(cx)),
9815 &[
9816 Point::new(1, 0)..Point::new(1, 0),
9817 Point::new(3, 0)..Point::new(3, 0),
9818 Point::new(5, 0)..Point::new(5, 0)
9819 ]
9820 );
9821 });
9822}
9823
9824#[gpui::test]
9825async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9826 init_test(cx, |settings| {
9827 settings.defaults.auto_indent = Some(true);
9828 settings.languages.0.insert(
9829 "python".into(),
9830 LanguageSettingsContent {
9831 auto_indent: Some(false),
9832 ..Default::default()
9833 },
9834 );
9835 });
9836
9837 let mut cx = EditorTestContext::new(cx).await;
9838
9839 let injected_language = Arc::new(
9840 Language::new(
9841 LanguageConfig {
9842 brackets: BracketPairConfig {
9843 pairs: vec![
9844 BracketPair {
9845 start: "{".to_string(),
9846 end: "}".to_string(),
9847 close: false,
9848 surround: false,
9849 newline: true,
9850 },
9851 BracketPair {
9852 start: "(".to_string(),
9853 end: ")".to_string(),
9854 close: true,
9855 surround: false,
9856 newline: true,
9857 },
9858 ],
9859 ..Default::default()
9860 },
9861 name: "python".into(),
9862 ..Default::default()
9863 },
9864 Some(tree_sitter_python::LANGUAGE.into()),
9865 )
9866 .with_indents_query(
9867 r#"
9868 (_ "(" ")" @end) @indent
9869 (_ "{" "}" @end) @indent
9870 "#,
9871 )
9872 .unwrap(),
9873 );
9874
9875 let language = Arc::new(
9876 Language::new(
9877 LanguageConfig {
9878 brackets: BracketPairConfig {
9879 pairs: vec![
9880 BracketPair {
9881 start: "{".to_string(),
9882 end: "}".to_string(),
9883 close: false,
9884 surround: false,
9885 newline: true,
9886 },
9887 BracketPair {
9888 start: "(".to_string(),
9889 end: ")".to_string(),
9890 close: true,
9891 surround: false,
9892 newline: true,
9893 },
9894 ],
9895 ..Default::default()
9896 },
9897 name: LanguageName::new("rust"),
9898 ..Default::default()
9899 },
9900 Some(tree_sitter_rust::LANGUAGE.into()),
9901 )
9902 .with_indents_query(
9903 r#"
9904 (_ "(" ")" @end) @indent
9905 (_ "{" "}" @end) @indent
9906 "#,
9907 )
9908 .unwrap()
9909 .with_injection_query(
9910 r#"
9911 (macro_invocation
9912 macro: (identifier) @_macro_name
9913 (token_tree) @injection.content
9914 (#set! injection.language "python"))
9915 "#,
9916 )
9917 .unwrap(),
9918 );
9919
9920 cx.language_registry().add(injected_language);
9921 cx.language_registry().add(language.clone());
9922
9923 cx.update_buffer(|buffer, cx| {
9924 buffer.set_language(Some(language), cx);
9925 });
9926
9927 cx.set_state(r#"struct A {ˇ}"#);
9928
9929 cx.update_editor(|editor, window, cx| {
9930 editor.newline(&Default::default(), window, cx);
9931 });
9932
9933 cx.assert_editor_state(indoc!(
9934 "struct A {
9935 ˇ
9936 }"
9937 ));
9938
9939 cx.set_state(r#"select_biased!(ˇ)"#);
9940
9941 cx.update_editor(|editor, window, cx| {
9942 editor.newline(&Default::default(), window, cx);
9943 editor.handle_input("def ", window, cx);
9944 editor.handle_input("(", window, cx);
9945 editor.newline(&Default::default(), window, cx);
9946 editor.handle_input("a", window, cx);
9947 });
9948
9949 cx.assert_editor_state(indoc!(
9950 "select_biased!(
9951 def (
9952 aˇ
9953 )
9954 )"
9955 ));
9956}
9957
9958#[gpui::test]
9959async fn test_autoindent_selections(cx: &mut TestAppContext) {
9960 init_test(cx, |_| {});
9961
9962 {
9963 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9964 cx.set_state(indoc! {"
9965 impl A {
9966
9967 fn b() {}
9968
9969 «fn c() {
9970
9971 }ˇ»
9972 }
9973 "});
9974
9975 cx.update_editor(|editor, window, cx| {
9976 editor.autoindent(&Default::default(), window, cx);
9977 });
9978
9979 cx.assert_editor_state(indoc! {"
9980 impl A {
9981
9982 fn b() {}
9983
9984 «fn c() {
9985
9986 }ˇ»
9987 }
9988 "});
9989 }
9990
9991 {
9992 let mut cx = EditorTestContext::new_multibuffer(
9993 cx,
9994 [indoc! { "
9995 impl A {
9996 «
9997 // a
9998 fn b(){}
9999 »
10000 «
10001 }
10002 fn c(){}
10003 »
10004 "}],
10005 );
10006
10007 let buffer = cx.update_editor(|editor, _, cx| {
10008 let buffer = editor.buffer().update(cx, |buffer, _| {
10009 buffer.all_buffers().iter().next().unwrap().clone()
10010 });
10011 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10012 buffer
10013 });
10014
10015 cx.run_until_parked();
10016 cx.update_editor(|editor, window, cx| {
10017 editor.select_all(&Default::default(), window, cx);
10018 editor.autoindent(&Default::default(), window, cx)
10019 });
10020 cx.run_until_parked();
10021
10022 cx.update(|_, cx| {
10023 assert_eq!(
10024 buffer.read(cx).text(),
10025 indoc! { "
10026 impl A {
10027
10028 // a
10029 fn b(){}
10030
10031
10032 }
10033 fn c(){}
10034
10035 " }
10036 )
10037 });
10038 }
10039}
10040
10041#[gpui::test]
10042async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10043 init_test(cx, |_| {});
10044
10045 let mut cx = EditorTestContext::new(cx).await;
10046
10047 let language = Arc::new(Language::new(
10048 LanguageConfig {
10049 brackets: BracketPairConfig {
10050 pairs: vec![
10051 BracketPair {
10052 start: "{".to_string(),
10053 end: "}".to_string(),
10054 close: true,
10055 surround: true,
10056 newline: true,
10057 },
10058 BracketPair {
10059 start: "(".to_string(),
10060 end: ")".to_string(),
10061 close: true,
10062 surround: true,
10063 newline: true,
10064 },
10065 BracketPair {
10066 start: "/*".to_string(),
10067 end: " */".to_string(),
10068 close: true,
10069 surround: true,
10070 newline: true,
10071 },
10072 BracketPair {
10073 start: "[".to_string(),
10074 end: "]".to_string(),
10075 close: false,
10076 surround: false,
10077 newline: true,
10078 },
10079 BracketPair {
10080 start: "\"".to_string(),
10081 end: "\"".to_string(),
10082 close: true,
10083 surround: true,
10084 newline: false,
10085 },
10086 BracketPair {
10087 start: "<".to_string(),
10088 end: ">".to_string(),
10089 close: false,
10090 surround: true,
10091 newline: true,
10092 },
10093 ],
10094 ..Default::default()
10095 },
10096 autoclose_before: "})]".to_string(),
10097 ..Default::default()
10098 },
10099 Some(tree_sitter_rust::LANGUAGE.into()),
10100 ));
10101
10102 cx.language_registry().add(language.clone());
10103 cx.update_buffer(|buffer, cx| {
10104 buffer.set_language(Some(language), cx);
10105 });
10106
10107 cx.set_state(
10108 &r#"
10109 🏀ˇ
10110 εˇ
10111 ❤️ˇ
10112 "#
10113 .unindent(),
10114 );
10115
10116 // autoclose multiple nested brackets at multiple cursors
10117 cx.update_editor(|editor, window, cx| {
10118 editor.handle_input("{", window, cx);
10119 editor.handle_input("{", window, cx);
10120 editor.handle_input("{", window, cx);
10121 });
10122 cx.assert_editor_state(
10123 &"
10124 🏀{{{ˇ}}}
10125 ε{{{ˇ}}}
10126 ❤️{{{ˇ}}}
10127 "
10128 .unindent(),
10129 );
10130
10131 // insert a different closing bracket
10132 cx.update_editor(|editor, window, cx| {
10133 editor.handle_input(")", window, cx);
10134 });
10135 cx.assert_editor_state(
10136 &"
10137 🏀{{{)ˇ}}}
10138 ε{{{)ˇ}}}
10139 ❤️{{{)ˇ}}}
10140 "
10141 .unindent(),
10142 );
10143
10144 // skip over the auto-closed brackets when typing a closing bracket
10145 cx.update_editor(|editor, window, cx| {
10146 editor.move_right(&MoveRight, window, cx);
10147 editor.handle_input("}", window, cx);
10148 editor.handle_input("}", window, cx);
10149 editor.handle_input("}", window, cx);
10150 });
10151 cx.assert_editor_state(
10152 &"
10153 🏀{{{)}}}}ˇ
10154 ε{{{)}}}}ˇ
10155 ❤️{{{)}}}}ˇ
10156 "
10157 .unindent(),
10158 );
10159
10160 // autoclose multi-character pairs
10161 cx.set_state(
10162 &"
10163 ˇ
10164 ˇ
10165 "
10166 .unindent(),
10167 );
10168 cx.update_editor(|editor, window, cx| {
10169 editor.handle_input("/", window, cx);
10170 editor.handle_input("*", window, cx);
10171 });
10172 cx.assert_editor_state(
10173 &"
10174 /*ˇ */
10175 /*ˇ */
10176 "
10177 .unindent(),
10178 );
10179
10180 // one cursor autocloses a multi-character pair, one cursor
10181 // does not autoclose.
10182 cx.set_state(
10183 &"
10184 /ˇ
10185 ˇ
10186 "
10187 .unindent(),
10188 );
10189 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10190 cx.assert_editor_state(
10191 &"
10192 /*ˇ */
10193 *ˇ
10194 "
10195 .unindent(),
10196 );
10197
10198 // Don't autoclose if the next character isn't whitespace and isn't
10199 // listed in the language's "autoclose_before" section.
10200 cx.set_state("ˇa b");
10201 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10202 cx.assert_editor_state("{ˇa b");
10203
10204 // Don't autoclose if `close` is false for the bracket pair
10205 cx.set_state("ˇ");
10206 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10207 cx.assert_editor_state("[ˇ");
10208
10209 // Surround with brackets if text is selected
10210 cx.set_state("«aˇ» b");
10211 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10212 cx.assert_editor_state("{«aˇ»} b");
10213
10214 // Autoclose when not immediately after a word character
10215 cx.set_state("a ˇ");
10216 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10217 cx.assert_editor_state("a \"ˇ\"");
10218
10219 // Autoclose pair where the start and end characters are the same
10220 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10221 cx.assert_editor_state("a \"\"ˇ");
10222
10223 // Don't autoclose when immediately after a word character
10224 cx.set_state("aˇ");
10225 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10226 cx.assert_editor_state("a\"ˇ");
10227
10228 // Do autoclose when after a non-word character
10229 cx.set_state("{ˇ");
10230 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10231 cx.assert_editor_state("{\"ˇ\"");
10232
10233 // Non identical pairs autoclose regardless of preceding character
10234 cx.set_state("aˇ");
10235 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10236 cx.assert_editor_state("a{ˇ}");
10237
10238 // Don't autoclose pair if autoclose is disabled
10239 cx.set_state("ˇ");
10240 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10241 cx.assert_editor_state("<ˇ");
10242
10243 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10244 cx.set_state("«aˇ» b");
10245 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10246 cx.assert_editor_state("<«aˇ»> b");
10247}
10248
10249#[gpui::test]
10250async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10251 init_test(cx, |settings| {
10252 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10253 });
10254
10255 let mut cx = EditorTestContext::new(cx).await;
10256
10257 let language = Arc::new(Language::new(
10258 LanguageConfig {
10259 brackets: BracketPairConfig {
10260 pairs: vec![
10261 BracketPair {
10262 start: "{".to_string(),
10263 end: "}".to_string(),
10264 close: true,
10265 surround: true,
10266 newline: true,
10267 },
10268 BracketPair {
10269 start: "(".to_string(),
10270 end: ")".to_string(),
10271 close: true,
10272 surround: true,
10273 newline: true,
10274 },
10275 BracketPair {
10276 start: "[".to_string(),
10277 end: "]".to_string(),
10278 close: false,
10279 surround: false,
10280 newline: true,
10281 },
10282 ],
10283 ..Default::default()
10284 },
10285 autoclose_before: "})]".to_string(),
10286 ..Default::default()
10287 },
10288 Some(tree_sitter_rust::LANGUAGE.into()),
10289 ));
10290
10291 cx.language_registry().add(language.clone());
10292 cx.update_buffer(|buffer, cx| {
10293 buffer.set_language(Some(language), cx);
10294 });
10295
10296 cx.set_state(
10297 &"
10298 ˇ
10299 ˇ
10300 ˇ
10301 "
10302 .unindent(),
10303 );
10304
10305 // ensure only matching closing brackets are skipped over
10306 cx.update_editor(|editor, window, cx| {
10307 editor.handle_input("}", window, cx);
10308 editor.move_left(&MoveLeft, window, cx);
10309 editor.handle_input(")", window, cx);
10310 editor.move_left(&MoveLeft, window, cx);
10311 });
10312 cx.assert_editor_state(
10313 &"
10314 ˇ)}
10315 ˇ)}
10316 ˇ)}
10317 "
10318 .unindent(),
10319 );
10320
10321 // skip-over closing brackets at multiple cursors
10322 cx.update_editor(|editor, window, cx| {
10323 editor.handle_input(")", window, cx);
10324 editor.handle_input("}", window, cx);
10325 });
10326 cx.assert_editor_state(
10327 &"
10328 )}ˇ
10329 )}ˇ
10330 )}ˇ
10331 "
10332 .unindent(),
10333 );
10334
10335 // ignore non-close brackets
10336 cx.update_editor(|editor, window, cx| {
10337 editor.handle_input("]", window, cx);
10338 editor.move_left(&MoveLeft, window, cx);
10339 editor.handle_input("]", window, cx);
10340 });
10341 cx.assert_editor_state(
10342 &"
10343 )}]ˇ]
10344 )}]ˇ]
10345 )}]ˇ]
10346 "
10347 .unindent(),
10348 );
10349}
10350
10351#[gpui::test]
10352async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10353 init_test(cx, |_| {});
10354
10355 let mut cx = EditorTestContext::new(cx).await;
10356
10357 let html_language = Arc::new(
10358 Language::new(
10359 LanguageConfig {
10360 name: "HTML".into(),
10361 brackets: BracketPairConfig {
10362 pairs: vec![
10363 BracketPair {
10364 start: "<".into(),
10365 end: ">".into(),
10366 close: true,
10367 ..Default::default()
10368 },
10369 BracketPair {
10370 start: "{".into(),
10371 end: "}".into(),
10372 close: true,
10373 ..Default::default()
10374 },
10375 BracketPair {
10376 start: "(".into(),
10377 end: ")".into(),
10378 close: true,
10379 ..Default::default()
10380 },
10381 ],
10382 ..Default::default()
10383 },
10384 autoclose_before: "})]>".into(),
10385 ..Default::default()
10386 },
10387 Some(tree_sitter_html::LANGUAGE.into()),
10388 )
10389 .with_injection_query(
10390 r#"
10391 (script_element
10392 (raw_text) @injection.content
10393 (#set! injection.language "javascript"))
10394 "#,
10395 )
10396 .unwrap(),
10397 );
10398
10399 let javascript_language = Arc::new(Language::new(
10400 LanguageConfig {
10401 name: "JavaScript".into(),
10402 brackets: BracketPairConfig {
10403 pairs: vec![
10404 BracketPair {
10405 start: "/*".into(),
10406 end: " */".into(),
10407 close: true,
10408 ..Default::default()
10409 },
10410 BracketPair {
10411 start: "{".into(),
10412 end: "}".into(),
10413 close: true,
10414 ..Default::default()
10415 },
10416 BracketPair {
10417 start: "(".into(),
10418 end: ")".into(),
10419 close: true,
10420 ..Default::default()
10421 },
10422 ],
10423 ..Default::default()
10424 },
10425 autoclose_before: "})]>".into(),
10426 ..Default::default()
10427 },
10428 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10429 ));
10430
10431 cx.language_registry().add(html_language.clone());
10432 cx.language_registry().add(javascript_language);
10433 cx.executor().run_until_parked();
10434
10435 cx.update_buffer(|buffer, cx| {
10436 buffer.set_language(Some(html_language), cx);
10437 });
10438
10439 cx.set_state(
10440 &r#"
10441 <body>ˇ
10442 <script>
10443 var x = 1;ˇ
10444 </script>
10445 </body>ˇ
10446 "#
10447 .unindent(),
10448 );
10449
10450 // Precondition: different languages are active at different locations.
10451 cx.update_editor(|editor, window, cx| {
10452 let snapshot = editor.snapshot(window, cx);
10453 let cursors = editor
10454 .selections
10455 .ranges::<usize>(&editor.display_snapshot(cx));
10456 let languages = cursors
10457 .iter()
10458 .map(|c| snapshot.language_at(c.start).unwrap().name())
10459 .collect::<Vec<_>>();
10460 assert_eq!(
10461 languages,
10462 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10463 );
10464 });
10465
10466 // Angle brackets autoclose in HTML, but not JavaScript.
10467 cx.update_editor(|editor, window, cx| {
10468 editor.handle_input("<", window, cx);
10469 editor.handle_input("a", window, cx);
10470 });
10471 cx.assert_editor_state(
10472 &r#"
10473 <body><aˇ>
10474 <script>
10475 var x = 1;<aˇ
10476 </script>
10477 </body><aˇ>
10478 "#
10479 .unindent(),
10480 );
10481
10482 // Curly braces and parens autoclose in both HTML and JavaScript.
10483 cx.update_editor(|editor, window, cx| {
10484 editor.handle_input(" b=", window, cx);
10485 editor.handle_input("{", window, cx);
10486 editor.handle_input("c", window, cx);
10487 editor.handle_input("(", window, cx);
10488 });
10489 cx.assert_editor_state(
10490 &r#"
10491 <body><a b={c(ˇ)}>
10492 <script>
10493 var x = 1;<a b={c(ˇ)}
10494 </script>
10495 </body><a b={c(ˇ)}>
10496 "#
10497 .unindent(),
10498 );
10499
10500 // Brackets that were already autoclosed are skipped.
10501 cx.update_editor(|editor, window, cx| {
10502 editor.handle_input(")", window, cx);
10503 editor.handle_input("d", window, cx);
10504 editor.handle_input("}", window, cx);
10505 });
10506 cx.assert_editor_state(
10507 &r#"
10508 <body><a b={c()d}ˇ>
10509 <script>
10510 var x = 1;<a b={c()d}ˇ
10511 </script>
10512 </body><a b={c()d}ˇ>
10513 "#
10514 .unindent(),
10515 );
10516 cx.update_editor(|editor, window, cx| {
10517 editor.handle_input(">", window, cx);
10518 });
10519 cx.assert_editor_state(
10520 &r#"
10521 <body><a b={c()d}>ˇ
10522 <script>
10523 var x = 1;<a b={c()d}>ˇ
10524 </script>
10525 </body><a b={c()d}>ˇ
10526 "#
10527 .unindent(),
10528 );
10529
10530 // Reset
10531 cx.set_state(
10532 &r#"
10533 <body>ˇ
10534 <script>
10535 var x = 1;ˇ
10536 </script>
10537 </body>ˇ
10538 "#
10539 .unindent(),
10540 );
10541
10542 cx.update_editor(|editor, window, cx| {
10543 editor.handle_input("<", window, cx);
10544 });
10545 cx.assert_editor_state(
10546 &r#"
10547 <body><ˇ>
10548 <script>
10549 var x = 1;<ˇ
10550 </script>
10551 </body><ˇ>
10552 "#
10553 .unindent(),
10554 );
10555
10556 // When backspacing, the closing angle brackets are removed.
10557 cx.update_editor(|editor, window, cx| {
10558 editor.backspace(&Backspace, window, cx);
10559 });
10560 cx.assert_editor_state(
10561 &r#"
10562 <body>ˇ
10563 <script>
10564 var x = 1;ˇ
10565 </script>
10566 </body>ˇ
10567 "#
10568 .unindent(),
10569 );
10570
10571 // Block comments autoclose in JavaScript, but not HTML.
10572 cx.update_editor(|editor, window, cx| {
10573 editor.handle_input("/", window, cx);
10574 editor.handle_input("*", window, cx);
10575 });
10576 cx.assert_editor_state(
10577 &r#"
10578 <body>/*ˇ
10579 <script>
10580 var x = 1;/*ˇ */
10581 </script>
10582 </body>/*ˇ
10583 "#
10584 .unindent(),
10585 );
10586}
10587
10588#[gpui::test]
10589async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10590 init_test(cx, |_| {});
10591
10592 let mut cx = EditorTestContext::new(cx).await;
10593
10594 let rust_language = Arc::new(
10595 Language::new(
10596 LanguageConfig {
10597 name: "Rust".into(),
10598 brackets: serde_json::from_value(json!([
10599 { "start": "{", "end": "}", "close": true, "newline": true },
10600 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10601 ]))
10602 .unwrap(),
10603 autoclose_before: "})]>".into(),
10604 ..Default::default()
10605 },
10606 Some(tree_sitter_rust::LANGUAGE.into()),
10607 )
10608 .with_override_query("(string_literal) @string")
10609 .unwrap(),
10610 );
10611
10612 cx.language_registry().add(rust_language.clone());
10613 cx.update_buffer(|buffer, cx| {
10614 buffer.set_language(Some(rust_language), cx);
10615 });
10616
10617 cx.set_state(
10618 &r#"
10619 let x = ˇ
10620 "#
10621 .unindent(),
10622 );
10623
10624 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10625 cx.update_editor(|editor, window, cx| {
10626 editor.handle_input("\"", window, cx);
10627 });
10628 cx.assert_editor_state(
10629 &r#"
10630 let x = "ˇ"
10631 "#
10632 .unindent(),
10633 );
10634
10635 // Inserting another quotation mark. The cursor moves across the existing
10636 // automatically-inserted quotation mark.
10637 cx.update_editor(|editor, window, cx| {
10638 editor.handle_input("\"", window, cx);
10639 });
10640 cx.assert_editor_state(
10641 &r#"
10642 let x = ""ˇ
10643 "#
10644 .unindent(),
10645 );
10646
10647 // Reset
10648 cx.set_state(
10649 &r#"
10650 let x = ˇ
10651 "#
10652 .unindent(),
10653 );
10654
10655 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10656 cx.update_editor(|editor, window, cx| {
10657 editor.handle_input("\"", window, cx);
10658 editor.handle_input(" ", window, cx);
10659 editor.move_left(&Default::default(), window, cx);
10660 editor.handle_input("\\", window, cx);
10661 editor.handle_input("\"", window, cx);
10662 });
10663 cx.assert_editor_state(
10664 &r#"
10665 let x = "\"ˇ "
10666 "#
10667 .unindent(),
10668 );
10669
10670 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10671 // mark. Nothing is inserted.
10672 cx.update_editor(|editor, window, cx| {
10673 editor.move_right(&Default::default(), window, cx);
10674 editor.handle_input("\"", window, cx);
10675 });
10676 cx.assert_editor_state(
10677 &r#"
10678 let x = "\" "ˇ
10679 "#
10680 .unindent(),
10681 );
10682}
10683
10684#[gpui::test]
10685async fn test_surround_with_pair(cx: &mut TestAppContext) {
10686 init_test(cx, |_| {});
10687
10688 let language = Arc::new(Language::new(
10689 LanguageConfig {
10690 brackets: BracketPairConfig {
10691 pairs: vec![
10692 BracketPair {
10693 start: "{".to_string(),
10694 end: "}".to_string(),
10695 close: true,
10696 surround: true,
10697 newline: true,
10698 },
10699 BracketPair {
10700 start: "/* ".to_string(),
10701 end: "*/".to_string(),
10702 close: true,
10703 surround: true,
10704 ..Default::default()
10705 },
10706 ],
10707 ..Default::default()
10708 },
10709 ..Default::default()
10710 },
10711 Some(tree_sitter_rust::LANGUAGE.into()),
10712 ));
10713
10714 let text = r#"
10715 a
10716 b
10717 c
10718 "#
10719 .unindent();
10720
10721 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10722 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10723 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10724 editor
10725 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10726 .await;
10727
10728 editor.update_in(cx, |editor, window, cx| {
10729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10730 s.select_display_ranges([
10731 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10732 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10733 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10734 ])
10735 });
10736
10737 editor.handle_input("{", window, cx);
10738 editor.handle_input("{", window, cx);
10739 editor.handle_input("{", window, cx);
10740 assert_eq!(
10741 editor.text(cx),
10742 "
10743 {{{a}}}
10744 {{{b}}}
10745 {{{c}}}
10746 "
10747 .unindent()
10748 );
10749 assert_eq!(
10750 display_ranges(editor, cx),
10751 [
10752 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10753 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10754 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10755 ]
10756 );
10757
10758 editor.undo(&Undo, window, cx);
10759 editor.undo(&Undo, window, cx);
10760 editor.undo(&Undo, window, cx);
10761 assert_eq!(
10762 editor.text(cx),
10763 "
10764 a
10765 b
10766 c
10767 "
10768 .unindent()
10769 );
10770 assert_eq!(
10771 display_ranges(editor, cx),
10772 [
10773 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10774 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10775 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10776 ]
10777 );
10778
10779 // Ensure inserting the first character of a multi-byte bracket pair
10780 // doesn't surround the selections with the bracket.
10781 editor.handle_input("/", window, cx);
10782 assert_eq!(
10783 editor.text(cx),
10784 "
10785 /
10786 /
10787 /
10788 "
10789 .unindent()
10790 );
10791 assert_eq!(
10792 display_ranges(editor, cx),
10793 [
10794 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10795 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10796 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10797 ]
10798 );
10799
10800 editor.undo(&Undo, window, cx);
10801 assert_eq!(
10802 editor.text(cx),
10803 "
10804 a
10805 b
10806 c
10807 "
10808 .unindent()
10809 );
10810 assert_eq!(
10811 display_ranges(editor, cx),
10812 [
10813 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10814 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10815 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10816 ]
10817 );
10818
10819 // Ensure inserting the last character of a multi-byte bracket pair
10820 // doesn't surround the selections with the bracket.
10821 editor.handle_input("*", window, cx);
10822 assert_eq!(
10823 editor.text(cx),
10824 "
10825 *
10826 *
10827 *
10828 "
10829 .unindent()
10830 );
10831 assert_eq!(
10832 display_ranges(editor, cx),
10833 [
10834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10835 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10836 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10837 ]
10838 );
10839 });
10840}
10841
10842#[gpui::test]
10843async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10844 init_test(cx, |_| {});
10845
10846 let language = Arc::new(Language::new(
10847 LanguageConfig {
10848 brackets: BracketPairConfig {
10849 pairs: vec![BracketPair {
10850 start: "{".to_string(),
10851 end: "}".to_string(),
10852 close: true,
10853 surround: true,
10854 newline: true,
10855 }],
10856 ..Default::default()
10857 },
10858 autoclose_before: "}".to_string(),
10859 ..Default::default()
10860 },
10861 Some(tree_sitter_rust::LANGUAGE.into()),
10862 ));
10863
10864 let text = r#"
10865 a
10866 b
10867 c
10868 "#
10869 .unindent();
10870
10871 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10872 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10873 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10874 editor
10875 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10876 .await;
10877
10878 editor.update_in(cx, |editor, window, cx| {
10879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10880 s.select_ranges([
10881 Point::new(0, 1)..Point::new(0, 1),
10882 Point::new(1, 1)..Point::new(1, 1),
10883 Point::new(2, 1)..Point::new(2, 1),
10884 ])
10885 });
10886
10887 editor.handle_input("{", window, cx);
10888 editor.handle_input("{", window, cx);
10889 editor.handle_input("_", window, cx);
10890 assert_eq!(
10891 editor.text(cx),
10892 "
10893 a{{_}}
10894 b{{_}}
10895 c{{_}}
10896 "
10897 .unindent()
10898 );
10899 assert_eq!(
10900 editor
10901 .selections
10902 .ranges::<Point>(&editor.display_snapshot(cx)),
10903 [
10904 Point::new(0, 4)..Point::new(0, 4),
10905 Point::new(1, 4)..Point::new(1, 4),
10906 Point::new(2, 4)..Point::new(2, 4)
10907 ]
10908 );
10909
10910 editor.backspace(&Default::default(), window, cx);
10911 editor.backspace(&Default::default(), window, cx);
10912 assert_eq!(
10913 editor.text(cx),
10914 "
10915 a{}
10916 b{}
10917 c{}
10918 "
10919 .unindent()
10920 );
10921 assert_eq!(
10922 editor
10923 .selections
10924 .ranges::<Point>(&editor.display_snapshot(cx)),
10925 [
10926 Point::new(0, 2)..Point::new(0, 2),
10927 Point::new(1, 2)..Point::new(1, 2),
10928 Point::new(2, 2)..Point::new(2, 2)
10929 ]
10930 );
10931
10932 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10933 assert_eq!(
10934 editor.text(cx),
10935 "
10936 a
10937 b
10938 c
10939 "
10940 .unindent()
10941 );
10942 assert_eq!(
10943 editor
10944 .selections
10945 .ranges::<Point>(&editor.display_snapshot(cx)),
10946 [
10947 Point::new(0, 1)..Point::new(0, 1),
10948 Point::new(1, 1)..Point::new(1, 1),
10949 Point::new(2, 1)..Point::new(2, 1)
10950 ]
10951 );
10952 });
10953}
10954
10955#[gpui::test]
10956async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10957 init_test(cx, |settings| {
10958 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10959 });
10960
10961 let mut cx = EditorTestContext::new(cx).await;
10962
10963 let language = Arc::new(Language::new(
10964 LanguageConfig {
10965 brackets: BracketPairConfig {
10966 pairs: vec![
10967 BracketPair {
10968 start: "{".to_string(),
10969 end: "}".to_string(),
10970 close: true,
10971 surround: true,
10972 newline: true,
10973 },
10974 BracketPair {
10975 start: "(".to_string(),
10976 end: ")".to_string(),
10977 close: true,
10978 surround: true,
10979 newline: true,
10980 },
10981 BracketPair {
10982 start: "[".to_string(),
10983 end: "]".to_string(),
10984 close: false,
10985 surround: true,
10986 newline: true,
10987 },
10988 ],
10989 ..Default::default()
10990 },
10991 autoclose_before: "})]".to_string(),
10992 ..Default::default()
10993 },
10994 Some(tree_sitter_rust::LANGUAGE.into()),
10995 ));
10996
10997 cx.language_registry().add(language.clone());
10998 cx.update_buffer(|buffer, cx| {
10999 buffer.set_language(Some(language), cx);
11000 });
11001
11002 cx.set_state(
11003 &"
11004 {(ˇ)}
11005 [[ˇ]]
11006 {(ˇ)}
11007 "
11008 .unindent(),
11009 );
11010
11011 cx.update_editor(|editor, window, cx| {
11012 editor.backspace(&Default::default(), window, cx);
11013 editor.backspace(&Default::default(), window, cx);
11014 });
11015
11016 cx.assert_editor_state(
11017 &"
11018 ˇ
11019 ˇ]]
11020 ˇ
11021 "
11022 .unindent(),
11023 );
11024
11025 cx.update_editor(|editor, window, cx| {
11026 editor.handle_input("{", window, cx);
11027 editor.handle_input("{", window, cx);
11028 editor.move_right(&MoveRight, window, cx);
11029 editor.move_right(&MoveRight, window, cx);
11030 editor.move_left(&MoveLeft, window, cx);
11031 editor.move_left(&MoveLeft, window, cx);
11032 editor.backspace(&Default::default(), window, cx);
11033 });
11034
11035 cx.assert_editor_state(
11036 &"
11037 {ˇ}
11038 {ˇ}]]
11039 {ˇ}
11040 "
11041 .unindent(),
11042 );
11043
11044 cx.update_editor(|editor, window, cx| {
11045 editor.backspace(&Default::default(), window, cx);
11046 });
11047
11048 cx.assert_editor_state(
11049 &"
11050 ˇ
11051 ˇ]]
11052 ˇ
11053 "
11054 .unindent(),
11055 );
11056}
11057
11058#[gpui::test]
11059async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11060 init_test(cx, |_| {});
11061
11062 let language = Arc::new(Language::new(
11063 LanguageConfig::default(),
11064 Some(tree_sitter_rust::LANGUAGE.into()),
11065 ));
11066
11067 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11069 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11070 editor
11071 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11072 .await;
11073
11074 editor.update_in(cx, |editor, window, cx| {
11075 editor.set_auto_replace_emoji_shortcode(true);
11076
11077 editor.handle_input("Hello ", window, cx);
11078 editor.handle_input(":wave", window, cx);
11079 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11080
11081 editor.handle_input(":", window, cx);
11082 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11083
11084 editor.handle_input(" :smile", window, cx);
11085 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11086
11087 editor.handle_input(":", window, cx);
11088 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11089
11090 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11091 editor.handle_input(":wave", window, cx);
11092 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11093
11094 editor.handle_input(":", window, cx);
11095 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11096
11097 editor.handle_input(":1", window, cx);
11098 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11099
11100 editor.handle_input(":", window, cx);
11101 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11102
11103 // Ensure shortcode does not get replaced when it is part of a word
11104 editor.handle_input(" Test:wave", window, cx);
11105 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11106
11107 editor.handle_input(":", window, cx);
11108 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11109
11110 editor.set_auto_replace_emoji_shortcode(false);
11111
11112 // Ensure shortcode does not get replaced when auto replace is off
11113 editor.handle_input(" :wave", window, cx);
11114 assert_eq!(
11115 editor.text(cx),
11116 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11117 );
11118
11119 editor.handle_input(":", window, cx);
11120 assert_eq!(
11121 editor.text(cx),
11122 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11123 );
11124 });
11125}
11126
11127#[gpui::test]
11128async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11129 init_test(cx, |_| {});
11130
11131 let (text, insertion_ranges) = marked_text_ranges(
11132 indoc! {"
11133 ˇ
11134 "},
11135 false,
11136 );
11137
11138 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11139 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11140
11141 _ = editor.update_in(cx, |editor, window, cx| {
11142 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11143
11144 editor
11145 .insert_snippet(&insertion_ranges, snippet, window, cx)
11146 .unwrap();
11147
11148 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11149 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11150 assert_eq!(editor.text(cx), expected_text);
11151 assert_eq!(
11152 editor
11153 .selections
11154 .ranges::<usize>(&editor.display_snapshot(cx)),
11155 selection_ranges
11156 );
11157 }
11158
11159 assert(
11160 editor,
11161 cx,
11162 indoc! {"
11163 type «» =•
11164 "},
11165 );
11166
11167 assert!(editor.context_menu_visible(), "There should be a matches");
11168 });
11169}
11170
11171#[gpui::test]
11172async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11173 init_test(cx, |_| {});
11174
11175 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11176 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11177 assert_eq!(editor.text(cx), expected_text);
11178 assert_eq!(
11179 editor
11180 .selections
11181 .ranges::<usize>(&editor.display_snapshot(cx)),
11182 selection_ranges
11183 );
11184 }
11185
11186 let (text, insertion_ranges) = marked_text_ranges(
11187 indoc! {"
11188 ˇ
11189 "},
11190 false,
11191 );
11192
11193 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11194 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11195
11196 _ = editor.update_in(cx, |editor, window, cx| {
11197 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11198
11199 editor
11200 .insert_snippet(&insertion_ranges, snippet, window, cx)
11201 .unwrap();
11202
11203 assert_state(
11204 editor,
11205 cx,
11206 indoc! {"
11207 type «» = ;•
11208 "},
11209 );
11210
11211 assert!(
11212 editor.context_menu_visible(),
11213 "Context menu should be visible for placeholder choices"
11214 );
11215
11216 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11217
11218 assert_state(
11219 editor,
11220 cx,
11221 indoc! {"
11222 type = «»;•
11223 "},
11224 );
11225
11226 assert!(
11227 !editor.context_menu_visible(),
11228 "Context menu should be hidden after moving to next tabstop"
11229 );
11230
11231 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11232
11233 assert_state(
11234 editor,
11235 cx,
11236 indoc! {"
11237 type = ; ˇ
11238 "},
11239 );
11240
11241 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11242
11243 assert_state(
11244 editor,
11245 cx,
11246 indoc! {"
11247 type = ; ˇ
11248 "},
11249 );
11250 });
11251
11252 _ = editor.update_in(cx, |editor, window, cx| {
11253 editor.select_all(&SelectAll, window, cx);
11254 editor.backspace(&Backspace, window, cx);
11255
11256 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11257 let insertion_ranges = editor
11258 .selections
11259 .all(&editor.display_snapshot(cx))
11260 .iter()
11261 .map(|s| s.range())
11262 .collect::<Vec<_>>();
11263
11264 editor
11265 .insert_snippet(&insertion_ranges, snippet, window, cx)
11266 .unwrap();
11267
11268 assert_state(editor, cx, "fn «» = value;•");
11269
11270 assert!(
11271 editor.context_menu_visible(),
11272 "Context menu should be visible for placeholder choices"
11273 );
11274
11275 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11276
11277 assert_state(editor, cx, "fn = «valueˇ»;•");
11278
11279 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11280
11281 assert_state(editor, cx, "fn «» = value;•");
11282
11283 assert!(
11284 editor.context_menu_visible(),
11285 "Context menu should be visible again after returning to first tabstop"
11286 );
11287
11288 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11289
11290 assert_state(editor, cx, "fn «» = value;•");
11291 });
11292}
11293
11294#[gpui::test]
11295async fn test_snippets(cx: &mut TestAppContext) {
11296 init_test(cx, |_| {});
11297
11298 let mut cx = EditorTestContext::new(cx).await;
11299
11300 cx.set_state(indoc! {"
11301 a.ˇ b
11302 a.ˇ b
11303 a.ˇ b
11304 "});
11305
11306 cx.update_editor(|editor, window, cx| {
11307 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11308 let insertion_ranges = editor
11309 .selections
11310 .all(&editor.display_snapshot(cx))
11311 .iter()
11312 .map(|s| s.range())
11313 .collect::<Vec<_>>();
11314 editor
11315 .insert_snippet(&insertion_ranges, snippet, window, cx)
11316 .unwrap();
11317 });
11318
11319 cx.assert_editor_state(indoc! {"
11320 a.f(«oneˇ», two, «threeˇ») b
11321 a.f(«oneˇ», two, «threeˇ») b
11322 a.f(«oneˇ», two, «threeˇ») b
11323 "});
11324
11325 // Can't move earlier than the first tab stop
11326 cx.update_editor(|editor, window, cx| {
11327 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11328 });
11329 cx.assert_editor_state(indoc! {"
11330 a.f(«oneˇ», two, «threeˇ») b
11331 a.f(«oneˇ», two, «threeˇ») b
11332 a.f(«oneˇ», two, «threeˇ») b
11333 "});
11334
11335 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11336 cx.assert_editor_state(indoc! {"
11337 a.f(one, «twoˇ», three) b
11338 a.f(one, «twoˇ», three) b
11339 a.f(one, «twoˇ», three) b
11340 "});
11341
11342 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11343 cx.assert_editor_state(indoc! {"
11344 a.f(«oneˇ», two, «threeˇ») b
11345 a.f(«oneˇ», two, «threeˇ») b
11346 a.f(«oneˇ», two, «threeˇ») b
11347 "});
11348
11349 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11350 cx.assert_editor_state(indoc! {"
11351 a.f(one, «twoˇ», three) b
11352 a.f(one, «twoˇ», three) b
11353 a.f(one, «twoˇ», three) b
11354 "});
11355 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11356 cx.assert_editor_state(indoc! {"
11357 a.f(one, two, three)ˇ b
11358 a.f(one, two, three)ˇ b
11359 a.f(one, two, three)ˇ b
11360 "});
11361
11362 // As soon as the last tab stop is reached, snippet state is gone
11363 cx.update_editor(|editor, window, cx| {
11364 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11365 });
11366 cx.assert_editor_state(indoc! {"
11367 a.f(one, two, three)ˇ b
11368 a.f(one, two, three)ˇ b
11369 a.f(one, two, three)ˇ b
11370 "});
11371}
11372
11373#[gpui::test]
11374async fn test_snippet_indentation(cx: &mut TestAppContext) {
11375 init_test(cx, |_| {});
11376
11377 let mut cx = EditorTestContext::new(cx).await;
11378
11379 cx.update_editor(|editor, window, cx| {
11380 let snippet = Snippet::parse(indoc! {"
11381 /*
11382 * Multiline comment with leading indentation
11383 *
11384 * $1
11385 */
11386 $0"})
11387 .unwrap();
11388 let insertion_ranges = editor
11389 .selections
11390 .all(&editor.display_snapshot(cx))
11391 .iter()
11392 .map(|s| s.range())
11393 .collect::<Vec<_>>();
11394 editor
11395 .insert_snippet(&insertion_ranges, snippet, window, cx)
11396 .unwrap();
11397 });
11398
11399 cx.assert_editor_state(indoc! {"
11400 /*
11401 * Multiline comment with leading indentation
11402 *
11403 * ˇ
11404 */
11405 "});
11406
11407 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11408 cx.assert_editor_state(indoc! {"
11409 /*
11410 * Multiline comment with leading indentation
11411 *
11412 *•
11413 */
11414 ˇ"});
11415}
11416
11417#[gpui::test]
11418async fn test_document_format_during_save(cx: &mut TestAppContext) {
11419 init_test(cx, |_| {});
11420
11421 let fs = FakeFs::new(cx.executor());
11422 fs.insert_file(path!("/file.rs"), Default::default()).await;
11423
11424 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11425
11426 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11427 language_registry.add(rust_lang());
11428 let mut fake_servers = language_registry.register_fake_lsp(
11429 "Rust",
11430 FakeLspAdapter {
11431 capabilities: lsp::ServerCapabilities {
11432 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11433 ..Default::default()
11434 },
11435 ..Default::default()
11436 },
11437 );
11438
11439 let buffer = project
11440 .update(cx, |project, cx| {
11441 project.open_local_buffer(path!("/file.rs"), cx)
11442 })
11443 .await
11444 .unwrap();
11445
11446 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11447 let (editor, cx) = cx.add_window_view(|window, cx| {
11448 build_editor_with_project(project.clone(), buffer, window, cx)
11449 });
11450 editor.update_in(cx, |editor, window, cx| {
11451 editor.set_text("one\ntwo\nthree\n", window, cx)
11452 });
11453 assert!(cx.read(|cx| editor.is_dirty(cx)));
11454
11455 cx.executor().start_waiting();
11456 let fake_server = fake_servers.next().await.unwrap();
11457
11458 {
11459 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11460 move |params, _| async move {
11461 assert_eq!(
11462 params.text_document.uri,
11463 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11464 );
11465 assert_eq!(params.options.tab_size, 4);
11466 Ok(Some(vec![lsp::TextEdit::new(
11467 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11468 ", ".to_string(),
11469 )]))
11470 },
11471 );
11472 let save = editor
11473 .update_in(cx, |editor, window, cx| {
11474 editor.save(
11475 SaveOptions {
11476 format: true,
11477 autosave: false,
11478 },
11479 project.clone(),
11480 window,
11481 cx,
11482 )
11483 })
11484 .unwrap();
11485 cx.executor().start_waiting();
11486 save.await;
11487
11488 assert_eq!(
11489 editor.update(cx, |editor, cx| editor.text(cx)),
11490 "one, two\nthree\n"
11491 );
11492 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11493 }
11494
11495 {
11496 editor.update_in(cx, |editor, window, cx| {
11497 editor.set_text("one\ntwo\nthree\n", window, cx)
11498 });
11499 assert!(cx.read(|cx| editor.is_dirty(cx)));
11500
11501 // Ensure we can still save even if formatting hangs.
11502 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11503 move |params, _| async move {
11504 assert_eq!(
11505 params.text_document.uri,
11506 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11507 );
11508 futures::future::pending::<()>().await;
11509 unreachable!()
11510 },
11511 );
11512 let save = editor
11513 .update_in(cx, |editor, window, cx| {
11514 editor.save(
11515 SaveOptions {
11516 format: true,
11517 autosave: false,
11518 },
11519 project.clone(),
11520 window,
11521 cx,
11522 )
11523 })
11524 .unwrap();
11525 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11526 cx.executor().start_waiting();
11527 save.await;
11528 assert_eq!(
11529 editor.update(cx, |editor, cx| editor.text(cx)),
11530 "one\ntwo\nthree\n"
11531 );
11532 }
11533
11534 // Set rust language override and assert overridden tabsize is sent to language server
11535 update_test_language_settings(cx, |settings| {
11536 settings.languages.0.insert(
11537 "Rust".into(),
11538 LanguageSettingsContent {
11539 tab_size: NonZeroU32::new(8),
11540 ..Default::default()
11541 },
11542 );
11543 });
11544
11545 {
11546 editor.update_in(cx, |editor, window, cx| {
11547 editor.set_text("somehting_new\n", window, cx)
11548 });
11549 assert!(cx.read(|cx| editor.is_dirty(cx)));
11550 let _formatting_request_signal = fake_server
11551 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11552 assert_eq!(
11553 params.text_document.uri,
11554 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11555 );
11556 assert_eq!(params.options.tab_size, 8);
11557 Ok(Some(vec![]))
11558 });
11559 let save = editor
11560 .update_in(cx, |editor, window, cx| {
11561 editor.save(
11562 SaveOptions {
11563 format: true,
11564 autosave: false,
11565 },
11566 project.clone(),
11567 window,
11568 cx,
11569 )
11570 })
11571 .unwrap();
11572 cx.executor().start_waiting();
11573 save.await;
11574 }
11575}
11576
11577#[gpui::test]
11578async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11579 init_test(cx, |settings| {
11580 settings.defaults.ensure_final_newline_on_save = Some(false);
11581 });
11582
11583 let fs = FakeFs::new(cx.executor());
11584 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11585
11586 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11587
11588 let buffer = project
11589 .update(cx, |project, cx| {
11590 project.open_local_buffer(path!("/file.txt"), cx)
11591 })
11592 .await
11593 .unwrap();
11594
11595 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11596 let (editor, cx) = cx.add_window_view(|window, cx| {
11597 build_editor_with_project(project.clone(), buffer, window, cx)
11598 });
11599 editor.update_in(cx, |editor, window, cx| {
11600 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11601 s.select_ranges([0..0])
11602 });
11603 });
11604 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11605
11606 editor.update_in(cx, |editor, window, cx| {
11607 editor.handle_input("\n", window, cx)
11608 });
11609 cx.run_until_parked();
11610 save(&editor, &project, cx).await;
11611 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11612
11613 editor.update_in(cx, |editor, window, cx| {
11614 editor.undo(&Default::default(), window, cx);
11615 });
11616 save(&editor, &project, cx).await;
11617 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11618
11619 editor.update_in(cx, |editor, window, cx| {
11620 editor.redo(&Default::default(), window, cx);
11621 });
11622 cx.run_until_parked();
11623 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11624
11625 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11626 let save = editor
11627 .update_in(cx, |editor, window, cx| {
11628 editor.save(
11629 SaveOptions {
11630 format: true,
11631 autosave: false,
11632 },
11633 project.clone(),
11634 window,
11635 cx,
11636 )
11637 })
11638 .unwrap();
11639 cx.executor().start_waiting();
11640 save.await;
11641 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11642 }
11643}
11644
11645#[gpui::test]
11646async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11647 init_test(cx, |_| {});
11648
11649 let cols = 4;
11650 let rows = 10;
11651 let sample_text_1 = sample_text(rows, cols, 'a');
11652 assert_eq!(
11653 sample_text_1,
11654 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11655 );
11656 let sample_text_2 = sample_text(rows, cols, 'l');
11657 assert_eq!(
11658 sample_text_2,
11659 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11660 );
11661 let sample_text_3 = sample_text(rows, cols, 'v');
11662 assert_eq!(
11663 sample_text_3,
11664 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11665 );
11666
11667 let fs = FakeFs::new(cx.executor());
11668 fs.insert_tree(
11669 path!("/a"),
11670 json!({
11671 "main.rs": sample_text_1,
11672 "other.rs": sample_text_2,
11673 "lib.rs": sample_text_3,
11674 }),
11675 )
11676 .await;
11677
11678 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11679 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11680 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11681
11682 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11683 language_registry.add(rust_lang());
11684 let mut fake_servers = language_registry.register_fake_lsp(
11685 "Rust",
11686 FakeLspAdapter {
11687 capabilities: lsp::ServerCapabilities {
11688 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11689 ..Default::default()
11690 },
11691 ..Default::default()
11692 },
11693 );
11694
11695 let worktree = project.update(cx, |project, cx| {
11696 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11697 assert_eq!(worktrees.len(), 1);
11698 worktrees.pop().unwrap()
11699 });
11700 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11701
11702 let buffer_1 = project
11703 .update(cx, |project, cx| {
11704 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11705 })
11706 .await
11707 .unwrap();
11708 let buffer_2 = project
11709 .update(cx, |project, cx| {
11710 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11711 })
11712 .await
11713 .unwrap();
11714 let buffer_3 = project
11715 .update(cx, |project, cx| {
11716 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11717 })
11718 .await
11719 .unwrap();
11720
11721 let multi_buffer = cx.new(|cx| {
11722 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11723 multi_buffer.push_excerpts(
11724 buffer_1.clone(),
11725 [
11726 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11727 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11728 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11729 ],
11730 cx,
11731 );
11732 multi_buffer.push_excerpts(
11733 buffer_2.clone(),
11734 [
11735 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11736 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11737 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11738 ],
11739 cx,
11740 );
11741 multi_buffer.push_excerpts(
11742 buffer_3.clone(),
11743 [
11744 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11745 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11746 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11747 ],
11748 cx,
11749 );
11750 multi_buffer
11751 });
11752 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11753 Editor::new(
11754 EditorMode::full(),
11755 multi_buffer,
11756 Some(project.clone()),
11757 window,
11758 cx,
11759 )
11760 });
11761
11762 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11763 editor.change_selections(
11764 SelectionEffects::scroll(Autoscroll::Next),
11765 window,
11766 cx,
11767 |s| s.select_ranges(Some(1..2)),
11768 );
11769 editor.insert("|one|two|three|", window, cx);
11770 });
11771 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11772 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11773 editor.change_selections(
11774 SelectionEffects::scroll(Autoscroll::Next),
11775 window,
11776 cx,
11777 |s| s.select_ranges(Some(60..70)),
11778 );
11779 editor.insert("|four|five|six|", window, cx);
11780 });
11781 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11782
11783 // First two buffers should be edited, but not the third one.
11784 assert_eq!(
11785 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11786 "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}",
11787 );
11788 buffer_1.update(cx, |buffer, _| {
11789 assert!(buffer.is_dirty());
11790 assert_eq!(
11791 buffer.text(),
11792 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11793 )
11794 });
11795 buffer_2.update(cx, |buffer, _| {
11796 assert!(buffer.is_dirty());
11797 assert_eq!(
11798 buffer.text(),
11799 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11800 )
11801 });
11802 buffer_3.update(cx, |buffer, _| {
11803 assert!(!buffer.is_dirty());
11804 assert_eq!(buffer.text(), sample_text_3,)
11805 });
11806 cx.executor().run_until_parked();
11807
11808 cx.executor().start_waiting();
11809 let save = multi_buffer_editor
11810 .update_in(cx, |editor, window, cx| {
11811 editor.save(
11812 SaveOptions {
11813 format: true,
11814 autosave: false,
11815 },
11816 project.clone(),
11817 window,
11818 cx,
11819 )
11820 })
11821 .unwrap();
11822
11823 let fake_server = fake_servers.next().await.unwrap();
11824 fake_server
11825 .server
11826 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11827 Ok(Some(vec![lsp::TextEdit::new(
11828 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11829 format!("[{} formatted]", params.text_document.uri),
11830 )]))
11831 })
11832 .detach();
11833 save.await;
11834
11835 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11836 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11837 assert_eq!(
11838 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11839 uri!(
11840 "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}"
11841 ),
11842 );
11843 buffer_1.update(cx, |buffer, _| {
11844 assert!(!buffer.is_dirty());
11845 assert_eq!(
11846 buffer.text(),
11847 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11848 )
11849 });
11850 buffer_2.update(cx, |buffer, _| {
11851 assert!(!buffer.is_dirty());
11852 assert_eq!(
11853 buffer.text(),
11854 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11855 )
11856 });
11857 buffer_3.update(cx, |buffer, _| {
11858 assert!(!buffer.is_dirty());
11859 assert_eq!(buffer.text(), sample_text_3,)
11860 });
11861}
11862
11863#[gpui::test]
11864async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11865 init_test(cx, |_| {});
11866
11867 let fs = FakeFs::new(cx.executor());
11868 fs.insert_tree(
11869 path!("/dir"),
11870 json!({
11871 "file1.rs": "fn main() { println!(\"hello\"); }",
11872 "file2.rs": "fn test() { println!(\"test\"); }",
11873 "file3.rs": "fn other() { println!(\"other\"); }\n",
11874 }),
11875 )
11876 .await;
11877
11878 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11879 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11880 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11881
11882 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11883 language_registry.add(rust_lang());
11884
11885 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11886 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11887
11888 // Open three buffers
11889 let buffer_1 = project
11890 .update(cx, |project, cx| {
11891 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11892 })
11893 .await
11894 .unwrap();
11895 let buffer_2 = project
11896 .update(cx, |project, cx| {
11897 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11898 })
11899 .await
11900 .unwrap();
11901 let buffer_3 = project
11902 .update(cx, |project, cx| {
11903 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11904 })
11905 .await
11906 .unwrap();
11907
11908 // Create a multi-buffer with all three buffers
11909 let multi_buffer = cx.new(|cx| {
11910 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11911 multi_buffer.push_excerpts(
11912 buffer_1.clone(),
11913 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11914 cx,
11915 );
11916 multi_buffer.push_excerpts(
11917 buffer_2.clone(),
11918 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11919 cx,
11920 );
11921 multi_buffer.push_excerpts(
11922 buffer_3.clone(),
11923 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11924 cx,
11925 );
11926 multi_buffer
11927 });
11928
11929 let editor = cx.new_window_entity(|window, cx| {
11930 Editor::new(
11931 EditorMode::full(),
11932 multi_buffer,
11933 Some(project.clone()),
11934 window,
11935 cx,
11936 )
11937 });
11938
11939 // Edit only the first buffer
11940 editor.update_in(cx, |editor, window, cx| {
11941 editor.change_selections(
11942 SelectionEffects::scroll(Autoscroll::Next),
11943 window,
11944 cx,
11945 |s| s.select_ranges(Some(10..10)),
11946 );
11947 editor.insert("// edited", window, cx);
11948 });
11949
11950 // Verify that only buffer 1 is dirty
11951 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11952 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11953 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11954
11955 // Get write counts after file creation (files were created with initial content)
11956 // We expect each file to have been written once during creation
11957 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11958 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11959 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11960
11961 // Perform autosave
11962 let save_task = editor.update_in(cx, |editor, window, cx| {
11963 editor.save(
11964 SaveOptions {
11965 format: true,
11966 autosave: true,
11967 },
11968 project.clone(),
11969 window,
11970 cx,
11971 )
11972 });
11973 save_task.await.unwrap();
11974
11975 // Only the dirty buffer should have been saved
11976 assert_eq!(
11977 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11978 1,
11979 "Buffer 1 was dirty, so it should have been written once during autosave"
11980 );
11981 assert_eq!(
11982 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11983 0,
11984 "Buffer 2 was clean, so it should not have been written during autosave"
11985 );
11986 assert_eq!(
11987 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11988 0,
11989 "Buffer 3 was clean, so it should not have been written during autosave"
11990 );
11991
11992 // Verify buffer states after autosave
11993 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11994 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11995 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11996
11997 // Now perform a manual save (format = true)
11998 let save_task = editor.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 save_task.await.unwrap();
12010
12011 // During manual save, clean buffers don't get written to disk
12012 // They just get did_save called for language server notifications
12013 assert_eq!(
12014 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12015 1,
12016 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12017 );
12018 assert_eq!(
12019 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12020 0,
12021 "Buffer 2 should not have been written at all"
12022 );
12023 assert_eq!(
12024 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12025 0,
12026 "Buffer 3 should not have been written at all"
12027 );
12028}
12029
12030async fn setup_range_format_test(
12031 cx: &mut TestAppContext,
12032) -> (
12033 Entity<Project>,
12034 Entity<Editor>,
12035 &mut gpui::VisualTestContext,
12036 lsp::FakeLanguageServer,
12037) {
12038 init_test(cx, |_| {});
12039
12040 let fs = FakeFs::new(cx.executor());
12041 fs.insert_file(path!("/file.rs"), Default::default()).await;
12042
12043 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12044
12045 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12046 language_registry.add(rust_lang());
12047 let mut fake_servers = language_registry.register_fake_lsp(
12048 "Rust",
12049 FakeLspAdapter {
12050 capabilities: lsp::ServerCapabilities {
12051 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12052 ..lsp::ServerCapabilities::default()
12053 },
12054 ..FakeLspAdapter::default()
12055 },
12056 );
12057
12058 let buffer = project
12059 .update(cx, |project, cx| {
12060 project.open_local_buffer(path!("/file.rs"), cx)
12061 })
12062 .await
12063 .unwrap();
12064
12065 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12066 let (editor, cx) = cx.add_window_view(|window, cx| {
12067 build_editor_with_project(project.clone(), buffer, window, cx)
12068 });
12069
12070 cx.executor().start_waiting();
12071 let fake_server = fake_servers.next().await.unwrap();
12072
12073 (project, editor, cx, fake_server)
12074}
12075
12076#[gpui::test]
12077async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12078 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12079
12080 editor.update_in(cx, |editor, window, cx| {
12081 editor.set_text("one\ntwo\nthree\n", window, cx)
12082 });
12083 assert!(cx.read(|cx| editor.is_dirty(cx)));
12084
12085 let save = editor
12086 .update_in(cx, |editor, window, cx| {
12087 editor.save(
12088 SaveOptions {
12089 format: true,
12090 autosave: false,
12091 },
12092 project.clone(),
12093 window,
12094 cx,
12095 )
12096 })
12097 .unwrap();
12098 fake_server
12099 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12100 assert_eq!(
12101 params.text_document.uri,
12102 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12103 );
12104 assert_eq!(params.options.tab_size, 4);
12105 Ok(Some(vec![lsp::TextEdit::new(
12106 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12107 ", ".to_string(),
12108 )]))
12109 })
12110 .next()
12111 .await;
12112 cx.executor().start_waiting();
12113 save.await;
12114 assert_eq!(
12115 editor.update(cx, |editor, cx| editor.text(cx)),
12116 "one, two\nthree\n"
12117 );
12118 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12119}
12120
12121#[gpui::test]
12122async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12123 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12124
12125 editor.update_in(cx, |editor, window, cx| {
12126 editor.set_text("one\ntwo\nthree\n", window, cx)
12127 });
12128 assert!(cx.read(|cx| editor.is_dirty(cx)));
12129
12130 // Test that save still works when formatting hangs
12131 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12132 move |params, _| async move {
12133 assert_eq!(
12134 params.text_document.uri,
12135 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12136 );
12137 futures::future::pending::<()>().await;
12138 unreachable!()
12139 },
12140 );
12141 let save = editor
12142 .update_in(cx, |editor, window, cx| {
12143 editor.save(
12144 SaveOptions {
12145 format: true,
12146 autosave: false,
12147 },
12148 project.clone(),
12149 window,
12150 cx,
12151 )
12152 })
12153 .unwrap();
12154 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12155 cx.executor().start_waiting();
12156 save.await;
12157 assert_eq!(
12158 editor.update(cx, |editor, cx| editor.text(cx)),
12159 "one\ntwo\nthree\n"
12160 );
12161 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12162}
12163
12164#[gpui::test]
12165async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12166 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12167
12168 // Buffer starts clean, no formatting should be requested
12169 let save = editor
12170 .update_in(cx, |editor, window, cx| {
12171 editor.save(
12172 SaveOptions {
12173 format: false,
12174 autosave: false,
12175 },
12176 project.clone(),
12177 window,
12178 cx,
12179 )
12180 })
12181 .unwrap();
12182 let _pending_format_request = fake_server
12183 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12184 panic!("Should not be invoked");
12185 })
12186 .next();
12187 cx.executor().start_waiting();
12188 save.await;
12189 cx.run_until_parked();
12190}
12191
12192#[gpui::test]
12193async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12194 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12195
12196 // Set Rust language override and assert overridden tabsize is sent to language server
12197 update_test_language_settings(cx, |settings| {
12198 settings.languages.0.insert(
12199 "Rust".into(),
12200 LanguageSettingsContent {
12201 tab_size: NonZeroU32::new(8),
12202 ..Default::default()
12203 },
12204 );
12205 });
12206
12207 editor.update_in(cx, |editor, window, cx| {
12208 editor.set_text("something_new\n", window, cx)
12209 });
12210 assert!(cx.read(|cx| editor.is_dirty(cx)));
12211 let save = editor
12212 .update_in(cx, |editor, window, cx| {
12213 editor.save(
12214 SaveOptions {
12215 format: true,
12216 autosave: false,
12217 },
12218 project.clone(),
12219 window,
12220 cx,
12221 )
12222 })
12223 .unwrap();
12224 fake_server
12225 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12226 assert_eq!(
12227 params.text_document.uri,
12228 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12229 );
12230 assert_eq!(params.options.tab_size, 8);
12231 Ok(Some(Vec::new()))
12232 })
12233 .next()
12234 .await;
12235 save.await;
12236}
12237
12238#[gpui::test]
12239async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12240 init_test(cx, |settings| {
12241 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12242 settings::LanguageServerFormatterSpecifier::Current,
12243 )))
12244 });
12245
12246 let fs = FakeFs::new(cx.executor());
12247 fs.insert_file(path!("/file.rs"), Default::default()).await;
12248
12249 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12250
12251 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12252 language_registry.add(Arc::new(Language::new(
12253 LanguageConfig {
12254 name: "Rust".into(),
12255 matcher: LanguageMatcher {
12256 path_suffixes: vec!["rs".to_string()],
12257 ..Default::default()
12258 },
12259 ..LanguageConfig::default()
12260 },
12261 Some(tree_sitter_rust::LANGUAGE.into()),
12262 )));
12263 update_test_language_settings(cx, |settings| {
12264 // Enable Prettier formatting for the same buffer, and ensure
12265 // LSP is called instead of Prettier.
12266 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12267 });
12268 let mut fake_servers = language_registry.register_fake_lsp(
12269 "Rust",
12270 FakeLspAdapter {
12271 capabilities: lsp::ServerCapabilities {
12272 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12273 ..Default::default()
12274 },
12275 ..Default::default()
12276 },
12277 );
12278
12279 let buffer = project
12280 .update(cx, |project, cx| {
12281 project.open_local_buffer(path!("/file.rs"), cx)
12282 })
12283 .await
12284 .unwrap();
12285
12286 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12287 let (editor, cx) = cx.add_window_view(|window, cx| {
12288 build_editor_with_project(project.clone(), buffer, window, cx)
12289 });
12290 editor.update_in(cx, |editor, window, cx| {
12291 editor.set_text("one\ntwo\nthree\n", window, cx)
12292 });
12293
12294 cx.executor().start_waiting();
12295 let fake_server = fake_servers.next().await.unwrap();
12296
12297 let format = editor
12298 .update_in(cx, |editor, window, cx| {
12299 editor.perform_format(
12300 project.clone(),
12301 FormatTrigger::Manual,
12302 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12303 window,
12304 cx,
12305 )
12306 })
12307 .unwrap();
12308 fake_server
12309 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12310 assert_eq!(
12311 params.text_document.uri,
12312 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12313 );
12314 assert_eq!(params.options.tab_size, 4);
12315 Ok(Some(vec![lsp::TextEdit::new(
12316 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12317 ", ".to_string(),
12318 )]))
12319 })
12320 .next()
12321 .await;
12322 cx.executor().start_waiting();
12323 format.await;
12324 assert_eq!(
12325 editor.update(cx, |editor, cx| editor.text(cx)),
12326 "one, two\nthree\n"
12327 );
12328
12329 editor.update_in(cx, |editor, window, cx| {
12330 editor.set_text("one\ntwo\nthree\n", window, cx)
12331 });
12332 // Ensure we don't lock if formatting hangs.
12333 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12334 move |params, _| async move {
12335 assert_eq!(
12336 params.text_document.uri,
12337 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12338 );
12339 futures::future::pending::<()>().await;
12340 unreachable!()
12341 },
12342 );
12343 let format = editor
12344 .update_in(cx, |editor, window, cx| {
12345 editor.perform_format(
12346 project,
12347 FormatTrigger::Manual,
12348 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12349 window,
12350 cx,
12351 )
12352 })
12353 .unwrap();
12354 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12355 cx.executor().start_waiting();
12356 format.await;
12357 assert_eq!(
12358 editor.update(cx, |editor, cx| editor.text(cx)),
12359 "one\ntwo\nthree\n"
12360 );
12361}
12362
12363#[gpui::test]
12364async fn test_multiple_formatters(cx: &mut TestAppContext) {
12365 init_test(cx, |settings| {
12366 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12367 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12368 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12369 Formatter::CodeAction("code-action-1".into()),
12370 Formatter::CodeAction("code-action-2".into()),
12371 ]))
12372 });
12373
12374 let fs = FakeFs::new(cx.executor());
12375 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12376 .await;
12377
12378 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12379 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12380 language_registry.add(rust_lang());
12381
12382 let mut fake_servers = language_registry.register_fake_lsp(
12383 "Rust",
12384 FakeLspAdapter {
12385 capabilities: lsp::ServerCapabilities {
12386 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12387 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12388 commands: vec!["the-command-for-code-action-1".into()],
12389 ..Default::default()
12390 }),
12391 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12392 ..Default::default()
12393 },
12394 ..Default::default()
12395 },
12396 );
12397
12398 let buffer = project
12399 .update(cx, |project, cx| {
12400 project.open_local_buffer(path!("/file.rs"), cx)
12401 })
12402 .await
12403 .unwrap();
12404
12405 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12406 let (editor, cx) = cx.add_window_view(|window, cx| {
12407 build_editor_with_project(project.clone(), buffer, window, cx)
12408 });
12409
12410 cx.executor().start_waiting();
12411
12412 let fake_server = fake_servers.next().await.unwrap();
12413 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12414 move |_params, _| async move {
12415 Ok(Some(vec![lsp::TextEdit::new(
12416 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12417 "applied-formatting\n".to_string(),
12418 )]))
12419 },
12420 );
12421 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12422 move |params, _| async move {
12423 let requested_code_actions = params.context.only.expect("Expected code action request");
12424 assert_eq!(requested_code_actions.len(), 1);
12425
12426 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12427 let code_action = match requested_code_actions[0].as_str() {
12428 "code-action-1" => lsp::CodeAction {
12429 kind: Some("code-action-1".into()),
12430 edit: Some(lsp::WorkspaceEdit::new(
12431 [(
12432 uri,
12433 vec![lsp::TextEdit::new(
12434 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12435 "applied-code-action-1-edit\n".to_string(),
12436 )],
12437 )]
12438 .into_iter()
12439 .collect(),
12440 )),
12441 command: Some(lsp::Command {
12442 command: "the-command-for-code-action-1".into(),
12443 ..Default::default()
12444 }),
12445 ..Default::default()
12446 },
12447 "code-action-2" => lsp::CodeAction {
12448 kind: Some("code-action-2".into()),
12449 edit: Some(lsp::WorkspaceEdit::new(
12450 [(
12451 uri,
12452 vec![lsp::TextEdit::new(
12453 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12454 "applied-code-action-2-edit\n".to_string(),
12455 )],
12456 )]
12457 .into_iter()
12458 .collect(),
12459 )),
12460 ..Default::default()
12461 },
12462 req => panic!("Unexpected code action request: {:?}", req),
12463 };
12464 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12465 code_action,
12466 )]))
12467 },
12468 );
12469
12470 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12471 move |params, _| async move { Ok(params) }
12472 });
12473
12474 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12475 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12476 let fake = fake_server.clone();
12477 let lock = command_lock.clone();
12478 move |params, _| {
12479 assert_eq!(params.command, "the-command-for-code-action-1");
12480 let fake = fake.clone();
12481 let lock = lock.clone();
12482 async move {
12483 lock.lock().await;
12484 fake.server
12485 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12486 label: None,
12487 edit: lsp::WorkspaceEdit {
12488 changes: Some(
12489 [(
12490 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12491 vec![lsp::TextEdit {
12492 range: lsp::Range::new(
12493 lsp::Position::new(0, 0),
12494 lsp::Position::new(0, 0),
12495 ),
12496 new_text: "applied-code-action-1-command\n".into(),
12497 }],
12498 )]
12499 .into_iter()
12500 .collect(),
12501 ),
12502 ..Default::default()
12503 },
12504 })
12505 .await
12506 .into_response()
12507 .unwrap();
12508 Ok(Some(json!(null)))
12509 }
12510 }
12511 });
12512
12513 cx.executor().start_waiting();
12514 editor
12515 .update_in(cx, |editor, window, cx| {
12516 editor.perform_format(
12517 project.clone(),
12518 FormatTrigger::Manual,
12519 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12520 window,
12521 cx,
12522 )
12523 })
12524 .unwrap()
12525 .await;
12526 editor.update(cx, |editor, cx| {
12527 assert_eq!(
12528 editor.text(cx),
12529 r#"
12530 applied-code-action-2-edit
12531 applied-code-action-1-command
12532 applied-code-action-1-edit
12533 applied-formatting
12534 one
12535 two
12536 three
12537 "#
12538 .unindent()
12539 );
12540 });
12541
12542 editor.update_in(cx, |editor, window, cx| {
12543 editor.undo(&Default::default(), window, cx);
12544 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12545 });
12546
12547 // Perform a manual edit while waiting for an LSP command
12548 // that's being run as part of a formatting code action.
12549 let lock_guard = command_lock.lock().await;
12550 let format = editor
12551 .update_in(cx, |editor, window, cx| {
12552 editor.perform_format(
12553 project.clone(),
12554 FormatTrigger::Manual,
12555 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12556 window,
12557 cx,
12558 )
12559 })
12560 .unwrap();
12561 cx.run_until_parked();
12562 editor.update(cx, |editor, cx| {
12563 assert_eq!(
12564 editor.text(cx),
12565 r#"
12566 applied-code-action-1-edit
12567 applied-formatting
12568 one
12569 two
12570 three
12571 "#
12572 .unindent()
12573 );
12574
12575 editor.buffer.update(cx, |buffer, cx| {
12576 let ix = buffer.len(cx);
12577 buffer.edit([(ix..ix, "edited\n")], None, cx);
12578 });
12579 });
12580
12581 // Allow the LSP command to proceed. Because the buffer was edited,
12582 // the second code action will not be run.
12583 drop(lock_guard);
12584 format.await;
12585 editor.update_in(cx, |editor, window, cx| {
12586 assert_eq!(
12587 editor.text(cx),
12588 r#"
12589 applied-code-action-1-command
12590 applied-code-action-1-edit
12591 applied-formatting
12592 one
12593 two
12594 three
12595 edited
12596 "#
12597 .unindent()
12598 );
12599
12600 // The manual edit is undone first, because it is the last thing the user did
12601 // (even though the command completed afterwards).
12602 editor.undo(&Default::default(), window, cx);
12603 assert_eq!(
12604 editor.text(cx),
12605 r#"
12606 applied-code-action-1-command
12607 applied-code-action-1-edit
12608 applied-formatting
12609 one
12610 two
12611 three
12612 "#
12613 .unindent()
12614 );
12615
12616 // All the formatting (including the command, which completed after the manual edit)
12617 // is undone together.
12618 editor.undo(&Default::default(), window, cx);
12619 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12620 });
12621}
12622
12623#[gpui::test]
12624async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12625 init_test(cx, |settings| {
12626 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12627 settings::LanguageServerFormatterSpecifier::Current,
12628 )]))
12629 });
12630
12631 let fs = FakeFs::new(cx.executor());
12632 fs.insert_file(path!("/file.ts"), Default::default()).await;
12633
12634 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12635
12636 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12637 language_registry.add(Arc::new(Language::new(
12638 LanguageConfig {
12639 name: "TypeScript".into(),
12640 matcher: LanguageMatcher {
12641 path_suffixes: vec!["ts".to_string()],
12642 ..Default::default()
12643 },
12644 ..LanguageConfig::default()
12645 },
12646 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12647 )));
12648 update_test_language_settings(cx, |settings| {
12649 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12650 });
12651 let mut fake_servers = language_registry.register_fake_lsp(
12652 "TypeScript",
12653 FakeLspAdapter {
12654 capabilities: lsp::ServerCapabilities {
12655 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12656 ..Default::default()
12657 },
12658 ..Default::default()
12659 },
12660 );
12661
12662 let buffer = project
12663 .update(cx, |project, cx| {
12664 project.open_local_buffer(path!("/file.ts"), cx)
12665 })
12666 .await
12667 .unwrap();
12668
12669 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12670 let (editor, cx) = cx.add_window_view(|window, cx| {
12671 build_editor_with_project(project.clone(), buffer, window, cx)
12672 });
12673 editor.update_in(cx, |editor, window, cx| {
12674 editor.set_text(
12675 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12676 window,
12677 cx,
12678 )
12679 });
12680
12681 cx.executor().start_waiting();
12682 let fake_server = fake_servers.next().await.unwrap();
12683
12684 let format = editor
12685 .update_in(cx, |editor, window, cx| {
12686 editor.perform_code_action_kind(
12687 project.clone(),
12688 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12689 window,
12690 cx,
12691 )
12692 })
12693 .unwrap();
12694 fake_server
12695 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12696 assert_eq!(
12697 params.text_document.uri,
12698 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12699 );
12700 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12701 lsp::CodeAction {
12702 title: "Organize Imports".to_string(),
12703 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12704 edit: Some(lsp::WorkspaceEdit {
12705 changes: Some(
12706 [(
12707 params.text_document.uri.clone(),
12708 vec![lsp::TextEdit::new(
12709 lsp::Range::new(
12710 lsp::Position::new(1, 0),
12711 lsp::Position::new(2, 0),
12712 ),
12713 "".to_string(),
12714 )],
12715 )]
12716 .into_iter()
12717 .collect(),
12718 ),
12719 ..Default::default()
12720 }),
12721 ..Default::default()
12722 },
12723 )]))
12724 })
12725 .next()
12726 .await;
12727 cx.executor().start_waiting();
12728 format.await;
12729 assert_eq!(
12730 editor.update(cx, |editor, cx| editor.text(cx)),
12731 "import { a } from 'module';\n\nconst x = a;\n"
12732 );
12733
12734 editor.update_in(cx, |editor, window, cx| {
12735 editor.set_text(
12736 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12737 window,
12738 cx,
12739 )
12740 });
12741 // Ensure we don't lock if code action hangs.
12742 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12743 move |params, _| async move {
12744 assert_eq!(
12745 params.text_document.uri,
12746 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12747 );
12748 futures::future::pending::<()>().await;
12749 unreachable!()
12750 },
12751 );
12752 let format = editor
12753 .update_in(cx, |editor, window, cx| {
12754 editor.perform_code_action_kind(
12755 project,
12756 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12757 window,
12758 cx,
12759 )
12760 })
12761 .unwrap();
12762 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12763 cx.executor().start_waiting();
12764 format.await;
12765 assert_eq!(
12766 editor.update(cx, |editor, cx| editor.text(cx)),
12767 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12768 );
12769}
12770
12771#[gpui::test]
12772async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12773 init_test(cx, |_| {});
12774
12775 let mut cx = EditorLspTestContext::new_rust(
12776 lsp::ServerCapabilities {
12777 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12778 ..Default::default()
12779 },
12780 cx,
12781 )
12782 .await;
12783
12784 cx.set_state(indoc! {"
12785 one.twoˇ
12786 "});
12787
12788 // The format request takes a long time. When it completes, it inserts
12789 // a newline and an indent before the `.`
12790 cx.lsp
12791 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12792 let executor = cx.background_executor().clone();
12793 async move {
12794 executor.timer(Duration::from_millis(100)).await;
12795 Ok(Some(vec![lsp::TextEdit {
12796 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12797 new_text: "\n ".into(),
12798 }]))
12799 }
12800 });
12801
12802 // Submit a format request.
12803 let format_1 = cx
12804 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12805 .unwrap();
12806 cx.executor().run_until_parked();
12807
12808 // Submit a second format request.
12809 let format_2 = cx
12810 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12811 .unwrap();
12812 cx.executor().run_until_parked();
12813
12814 // Wait for both format requests to complete
12815 cx.executor().advance_clock(Duration::from_millis(200));
12816 cx.executor().start_waiting();
12817 format_1.await.unwrap();
12818 cx.executor().start_waiting();
12819 format_2.await.unwrap();
12820
12821 // The formatting edits only happens once.
12822 cx.assert_editor_state(indoc! {"
12823 one
12824 .twoˇ
12825 "});
12826}
12827
12828#[gpui::test]
12829async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12830 init_test(cx, |settings| {
12831 settings.defaults.formatter = Some(FormatterList::default())
12832 });
12833
12834 let mut cx = EditorLspTestContext::new_rust(
12835 lsp::ServerCapabilities {
12836 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12837 ..Default::default()
12838 },
12839 cx,
12840 )
12841 .await;
12842
12843 // Record which buffer changes have been sent to the language server
12844 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12845 cx.lsp
12846 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12847 let buffer_changes = buffer_changes.clone();
12848 move |params, _| {
12849 buffer_changes.lock().extend(
12850 params
12851 .content_changes
12852 .into_iter()
12853 .map(|e| (e.range.unwrap(), e.text)),
12854 );
12855 }
12856 });
12857 // Handle formatting requests to the language server.
12858 cx.lsp
12859 .set_request_handler::<lsp::request::Formatting, _, _>({
12860 let buffer_changes = buffer_changes.clone();
12861 move |_, _| {
12862 let buffer_changes = buffer_changes.clone();
12863 // Insert blank lines between each line of the buffer.
12864 async move {
12865 // When formatting is requested, trailing whitespace has already been stripped,
12866 // and the trailing newline has already been added.
12867 assert_eq!(
12868 &buffer_changes.lock()[1..],
12869 &[
12870 (
12871 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12872 "".into()
12873 ),
12874 (
12875 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12876 "".into()
12877 ),
12878 (
12879 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12880 "\n".into()
12881 ),
12882 ]
12883 );
12884
12885 Ok(Some(vec![
12886 lsp::TextEdit {
12887 range: lsp::Range::new(
12888 lsp::Position::new(1, 0),
12889 lsp::Position::new(1, 0),
12890 ),
12891 new_text: "\n".into(),
12892 },
12893 lsp::TextEdit {
12894 range: lsp::Range::new(
12895 lsp::Position::new(2, 0),
12896 lsp::Position::new(2, 0),
12897 ),
12898 new_text: "\n".into(),
12899 },
12900 ]))
12901 }
12902 }
12903 });
12904
12905 // Set up a buffer white some trailing whitespace and no trailing newline.
12906 cx.set_state(
12907 &[
12908 "one ", //
12909 "twoˇ", //
12910 "three ", //
12911 "four", //
12912 ]
12913 .join("\n"),
12914 );
12915 cx.run_until_parked();
12916
12917 // Submit a format request.
12918 let format = cx
12919 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12920 .unwrap();
12921
12922 cx.run_until_parked();
12923 // After formatting the buffer, the trailing whitespace is stripped,
12924 // a newline is appended, and the edits provided by the language server
12925 // have been applied.
12926 format.await.unwrap();
12927
12928 cx.assert_editor_state(
12929 &[
12930 "one", //
12931 "", //
12932 "twoˇ", //
12933 "", //
12934 "three", //
12935 "four", //
12936 "", //
12937 ]
12938 .join("\n"),
12939 );
12940
12941 // Undoing the formatting undoes the trailing whitespace removal, the
12942 // trailing newline, and the LSP edits.
12943 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12944 cx.assert_editor_state(
12945 &[
12946 "one ", //
12947 "twoˇ", //
12948 "three ", //
12949 "four", //
12950 ]
12951 .join("\n"),
12952 );
12953}
12954
12955#[gpui::test]
12956async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12957 cx: &mut TestAppContext,
12958) {
12959 init_test(cx, |_| {});
12960
12961 cx.update(|cx| {
12962 cx.update_global::<SettingsStore, _>(|settings, cx| {
12963 settings.update_user_settings(cx, |settings| {
12964 settings.editor.auto_signature_help = Some(true);
12965 });
12966 });
12967 });
12968
12969 let mut cx = EditorLspTestContext::new_rust(
12970 lsp::ServerCapabilities {
12971 signature_help_provider: Some(lsp::SignatureHelpOptions {
12972 ..Default::default()
12973 }),
12974 ..Default::default()
12975 },
12976 cx,
12977 )
12978 .await;
12979
12980 let language = Language::new(
12981 LanguageConfig {
12982 name: "Rust".into(),
12983 brackets: BracketPairConfig {
12984 pairs: vec![
12985 BracketPair {
12986 start: "{".to_string(),
12987 end: "}".to_string(),
12988 close: true,
12989 surround: true,
12990 newline: true,
12991 },
12992 BracketPair {
12993 start: "(".to_string(),
12994 end: ")".to_string(),
12995 close: true,
12996 surround: true,
12997 newline: true,
12998 },
12999 BracketPair {
13000 start: "/*".to_string(),
13001 end: " */".to_string(),
13002 close: true,
13003 surround: true,
13004 newline: true,
13005 },
13006 BracketPair {
13007 start: "[".to_string(),
13008 end: "]".to_string(),
13009 close: false,
13010 surround: false,
13011 newline: true,
13012 },
13013 BracketPair {
13014 start: "\"".to_string(),
13015 end: "\"".to_string(),
13016 close: true,
13017 surround: true,
13018 newline: false,
13019 },
13020 BracketPair {
13021 start: "<".to_string(),
13022 end: ">".to_string(),
13023 close: false,
13024 surround: true,
13025 newline: true,
13026 },
13027 ],
13028 ..Default::default()
13029 },
13030 autoclose_before: "})]".to_string(),
13031 ..Default::default()
13032 },
13033 Some(tree_sitter_rust::LANGUAGE.into()),
13034 );
13035 let language = Arc::new(language);
13036
13037 cx.language_registry().add(language.clone());
13038 cx.update_buffer(|buffer, cx| {
13039 buffer.set_language(Some(language), cx);
13040 });
13041
13042 cx.set_state(
13043 &r#"
13044 fn main() {
13045 sampleˇ
13046 }
13047 "#
13048 .unindent(),
13049 );
13050
13051 cx.update_editor(|editor, window, cx| {
13052 editor.handle_input("(", window, cx);
13053 });
13054 cx.assert_editor_state(
13055 &"
13056 fn main() {
13057 sample(ˇ)
13058 }
13059 "
13060 .unindent(),
13061 );
13062
13063 let mocked_response = lsp::SignatureHelp {
13064 signatures: vec![lsp::SignatureInformation {
13065 label: "fn sample(param1: u8, param2: u8)".to_string(),
13066 documentation: None,
13067 parameters: Some(vec![
13068 lsp::ParameterInformation {
13069 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13070 documentation: None,
13071 },
13072 lsp::ParameterInformation {
13073 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13074 documentation: None,
13075 },
13076 ]),
13077 active_parameter: None,
13078 }],
13079 active_signature: Some(0),
13080 active_parameter: Some(0),
13081 };
13082 handle_signature_help_request(&mut cx, mocked_response).await;
13083
13084 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13085 .await;
13086
13087 cx.editor(|editor, _, _| {
13088 let signature_help_state = editor.signature_help_state.popover().cloned();
13089 let signature = signature_help_state.unwrap();
13090 assert_eq!(
13091 signature.signatures[signature.current_signature].label,
13092 "fn sample(param1: u8, param2: u8)"
13093 );
13094 });
13095}
13096
13097#[gpui::test]
13098async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13099 init_test(cx, |_| {});
13100
13101 cx.update(|cx| {
13102 cx.update_global::<SettingsStore, _>(|settings, cx| {
13103 settings.update_user_settings(cx, |settings| {
13104 settings.editor.auto_signature_help = Some(false);
13105 settings.editor.show_signature_help_after_edits = Some(false);
13106 });
13107 });
13108 });
13109
13110 let mut cx = EditorLspTestContext::new_rust(
13111 lsp::ServerCapabilities {
13112 signature_help_provider: Some(lsp::SignatureHelpOptions {
13113 ..Default::default()
13114 }),
13115 ..Default::default()
13116 },
13117 cx,
13118 )
13119 .await;
13120
13121 let language = Language::new(
13122 LanguageConfig {
13123 name: "Rust".into(),
13124 brackets: BracketPairConfig {
13125 pairs: vec![
13126 BracketPair {
13127 start: "{".to_string(),
13128 end: "}".to_string(),
13129 close: true,
13130 surround: true,
13131 newline: true,
13132 },
13133 BracketPair {
13134 start: "(".to_string(),
13135 end: ")".to_string(),
13136 close: true,
13137 surround: true,
13138 newline: true,
13139 },
13140 BracketPair {
13141 start: "/*".to_string(),
13142 end: " */".to_string(),
13143 close: true,
13144 surround: true,
13145 newline: true,
13146 },
13147 BracketPair {
13148 start: "[".to_string(),
13149 end: "]".to_string(),
13150 close: false,
13151 surround: false,
13152 newline: true,
13153 },
13154 BracketPair {
13155 start: "\"".to_string(),
13156 end: "\"".to_string(),
13157 close: true,
13158 surround: true,
13159 newline: false,
13160 },
13161 BracketPair {
13162 start: "<".to_string(),
13163 end: ">".to_string(),
13164 close: false,
13165 surround: true,
13166 newline: true,
13167 },
13168 ],
13169 ..Default::default()
13170 },
13171 autoclose_before: "})]".to_string(),
13172 ..Default::default()
13173 },
13174 Some(tree_sitter_rust::LANGUAGE.into()),
13175 );
13176 let language = Arc::new(language);
13177
13178 cx.language_registry().add(language.clone());
13179 cx.update_buffer(|buffer, cx| {
13180 buffer.set_language(Some(language), cx);
13181 });
13182
13183 // Ensure that signature_help is not called when no signature help is enabled.
13184 cx.set_state(
13185 &r#"
13186 fn main() {
13187 sampleˇ
13188 }
13189 "#
13190 .unindent(),
13191 );
13192 cx.update_editor(|editor, window, cx| {
13193 editor.handle_input("(", window, cx);
13194 });
13195 cx.assert_editor_state(
13196 &"
13197 fn main() {
13198 sample(ˇ)
13199 }
13200 "
13201 .unindent(),
13202 );
13203 cx.editor(|editor, _, _| {
13204 assert!(editor.signature_help_state.task().is_none());
13205 });
13206
13207 let mocked_response = lsp::SignatureHelp {
13208 signatures: vec![lsp::SignatureInformation {
13209 label: "fn sample(param1: u8, param2: u8)".to_string(),
13210 documentation: None,
13211 parameters: Some(vec![
13212 lsp::ParameterInformation {
13213 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13214 documentation: None,
13215 },
13216 lsp::ParameterInformation {
13217 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13218 documentation: None,
13219 },
13220 ]),
13221 active_parameter: None,
13222 }],
13223 active_signature: Some(0),
13224 active_parameter: Some(0),
13225 };
13226
13227 // Ensure that signature_help is called when enabled afte edits
13228 cx.update(|_, cx| {
13229 cx.update_global::<SettingsStore, _>(|settings, cx| {
13230 settings.update_user_settings(cx, |settings| {
13231 settings.editor.auto_signature_help = Some(false);
13232 settings.editor.show_signature_help_after_edits = Some(true);
13233 });
13234 });
13235 });
13236 cx.set_state(
13237 &r#"
13238 fn main() {
13239 sampleˇ
13240 }
13241 "#
13242 .unindent(),
13243 );
13244 cx.update_editor(|editor, window, cx| {
13245 editor.handle_input("(", window, cx);
13246 });
13247 cx.assert_editor_state(
13248 &"
13249 fn main() {
13250 sample(ˇ)
13251 }
13252 "
13253 .unindent(),
13254 );
13255 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13256 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13257 .await;
13258 cx.update_editor(|editor, _, _| {
13259 let signature_help_state = editor.signature_help_state.popover().cloned();
13260 assert!(signature_help_state.is_some());
13261 let signature = signature_help_state.unwrap();
13262 assert_eq!(
13263 signature.signatures[signature.current_signature].label,
13264 "fn sample(param1: u8, param2: u8)"
13265 );
13266 editor.signature_help_state = SignatureHelpState::default();
13267 });
13268
13269 // Ensure that signature_help is called when auto signature help override is enabled
13270 cx.update(|_, cx| {
13271 cx.update_global::<SettingsStore, _>(|settings, cx| {
13272 settings.update_user_settings(cx, |settings| {
13273 settings.editor.auto_signature_help = Some(true);
13274 settings.editor.show_signature_help_after_edits = Some(false);
13275 });
13276 });
13277 });
13278 cx.set_state(
13279 &r#"
13280 fn main() {
13281 sampleˇ
13282 }
13283 "#
13284 .unindent(),
13285 );
13286 cx.update_editor(|editor, window, cx| {
13287 editor.handle_input("(", window, cx);
13288 });
13289 cx.assert_editor_state(
13290 &"
13291 fn main() {
13292 sample(ˇ)
13293 }
13294 "
13295 .unindent(),
13296 );
13297 handle_signature_help_request(&mut cx, mocked_response).await;
13298 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13299 .await;
13300 cx.editor(|editor, _, _| {
13301 let signature_help_state = editor.signature_help_state.popover().cloned();
13302 assert!(signature_help_state.is_some());
13303 let signature = signature_help_state.unwrap();
13304 assert_eq!(
13305 signature.signatures[signature.current_signature].label,
13306 "fn sample(param1: u8, param2: u8)"
13307 );
13308 });
13309}
13310
13311#[gpui::test]
13312async fn test_signature_help(cx: &mut TestAppContext) {
13313 init_test(cx, |_| {});
13314 cx.update(|cx| {
13315 cx.update_global::<SettingsStore, _>(|settings, cx| {
13316 settings.update_user_settings(cx, |settings| {
13317 settings.editor.auto_signature_help = Some(true);
13318 });
13319 });
13320 });
13321
13322 let mut cx = EditorLspTestContext::new_rust(
13323 lsp::ServerCapabilities {
13324 signature_help_provider: Some(lsp::SignatureHelpOptions {
13325 ..Default::default()
13326 }),
13327 ..Default::default()
13328 },
13329 cx,
13330 )
13331 .await;
13332
13333 // A test that directly calls `show_signature_help`
13334 cx.update_editor(|editor, window, cx| {
13335 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13336 });
13337
13338 let mocked_response = lsp::SignatureHelp {
13339 signatures: vec![lsp::SignatureInformation {
13340 label: "fn sample(param1: u8, param2: u8)".to_string(),
13341 documentation: None,
13342 parameters: Some(vec![
13343 lsp::ParameterInformation {
13344 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13345 documentation: None,
13346 },
13347 lsp::ParameterInformation {
13348 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13349 documentation: None,
13350 },
13351 ]),
13352 active_parameter: None,
13353 }],
13354 active_signature: Some(0),
13355 active_parameter: Some(0),
13356 };
13357 handle_signature_help_request(&mut cx, mocked_response).await;
13358
13359 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13360 .await;
13361
13362 cx.editor(|editor, _, _| {
13363 let signature_help_state = editor.signature_help_state.popover().cloned();
13364 assert!(signature_help_state.is_some());
13365 let signature = signature_help_state.unwrap();
13366 assert_eq!(
13367 signature.signatures[signature.current_signature].label,
13368 "fn sample(param1: u8, param2: u8)"
13369 );
13370 });
13371
13372 // When exiting outside from inside the brackets, `signature_help` is closed.
13373 cx.set_state(indoc! {"
13374 fn main() {
13375 sample(ˇ);
13376 }
13377
13378 fn sample(param1: u8, param2: u8) {}
13379 "});
13380
13381 cx.update_editor(|editor, window, cx| {
13382 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13383 s.select_ranges([0..0])
13384 });
13385 });
13386
13387 let mocked_response = lsp::SignatureHelp {
13388 signatures: Vec::new(),
13389 active_signature: None,
13390 active_parameter: None,
13391 };
13392 handle_signature_help_request(&mut cx, mocked_response).await;
13393
13394 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13395 .await;
13396
13397 cx.editor(|editor, _, _| {
13398 assert!(!editor.signature_help_state.is_shown());
13399 });
13400
13401 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13402 cx.set_state(indoc! {"
13403 fn main() {
13404 sample(ˇ);
13405 }
13406
13407 fn sample(param1: u8, param2: u8) {}
13408 "});
13409
13410 let mocked_response = lsp::SignatureHelp {
13411 signatures: vec![lsp::SignatureInformation {
13412 label: "fn sample(param1: u8, param2: u8)".to_string(),
13413 documentation: None,
13414 parameters: Some(vec![
13415 lsp::ParameterInformation {
13416 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13417 documentation: None,
13418 },
13419 lsp::ParameterInformation {
13420 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13421 documentation: None,
13422 },
13423 ]),
13424 active_parameter: None,
13425 }],
13426 active_signature: Some(0),
13427 active_parameter: Some(0),
13428 };
13429 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13430 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13431 .await;
13432 cx.editor(|editor, _, _| {
13433 assert!(editor.signature_help_state.is_shown());
13434 });
13435
13436 // Restore the popover with more parameter input
13437 cx.set_state(indoc! {"
13438 fn main() {
13439 sample(param1, param2ˇ);
13440 }
13441
13442 fn sample(param1: u8, param2: u8) {}
13443 "});
13444
13445 let mocked_response = lsp::SignatureHelp {
13446 signatures: vec![lsp::SignatureInformation {
13447 label: "fn sample(param1: u8, param2: u8)".to_string(),
13448 documentation: None,
13449 parameters: Some(vec![
13450 lsp::ParameterInformation {
13451 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13452 documentation: None,
13453 },
13454 lsp::ParameterInformation {
13455 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13456 documentation: None,
13457 },
13458 ]),
13459 active_parameter: None,
13460 }],
13461 active_signature: Some(0),
13462 active_parameter: Some(1),
13463 };
13464 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13465 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13466 .await;
13467
13468 // When selecting a range, the popover is gone.
13469 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13470 cx.update_editor(|editor, window, cx| {
13471 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13472 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13473 })
13474 });
13475 cx.assert_editor_state(indoc! {"
13476 fn main() {
13477 sample(param1, «ˇparam2»);
13478 }
13479
13480 fn sample(param1: u8, param2: u8) {}
13481 "});
13482 cx.editor(|editor, _, _| {
13483 assert!(!editor.signature_help_state.is_shown());
13484 });
13485
13486 // When unselecting again, the popover is back if within the brackets.
13487 cx.update_editor(|editor, window, cx| {
13488 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13489 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13490 })
13491 });
13492 cx.assert_editor_state(indoc! {"
13493 fn main() {
13494 sample(param1, ˇparam2);
13495 }
13496
13497 fn sample(param1: u8, param2: u8) {}
13498 "});
13499 handle_signature_help_request(&mut cx, mocked_response).await;
13500 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13501 .await;
13502 cx.editor(|editor, _, _| {
13503 assert!(editor.signature_help_state.is_shown());
13504 });
13505
13506 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13507 cx.update_editor(|editor, window, cx| {
13508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13509 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13510 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13511 })
13512 });
13513 cx.assert_editor_state(indoc! {"
13514 fn main() {
13515 sample(param1, ˇparam2);
13516 }
13517
13518 fn sample(param1: u8, param2: u8) {}
13519 "});
13520
13521 let mocked_response = lsp::SignatureHelp {
13522 signatures: vec![lsp::SignatureInformation {
13523 label: "fn sample(param1: u8, param2: u8)".to_string(),
13524 documentation: None,
13525 parameters: Some(vec![
13526 lsp::ParameterInformation {
13527 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13528 documentation: None,
13529 },
13530 lsp::ParameterInformation {
13531 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13532 documentation: None,
13533 },
13534 ]),
13535 active_parameter: None,
13536 }],
13537 active_signature: Some(0),
13538 active_parameter: Some(1),
13539 };
13540 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13541 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13542 .await;
13543 cx.update_editor(|editor, _, cx| {
13544 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13545 });
13546 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13547 .await;
13548 cx.update_editor(|editor, window, cx| {
13549 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13550 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13551 })
13552 });
13553 cx.assert_editor_state(indoc! {"
13554 fn main() {
13555 sample(param1, «ˇparam2»);
13556 }
13557
13558 fn sample(param1: u8, param2: u8) {}
13559 "});
13560 cx.update_editor(|editor, window, cx| {
13561 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13562 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13563 })
13564 });
13565 cx.assert_editor_state(indoc! {"
13566 fn main() {
13567 sample(param1, ˇparam2);
13568 }
13569
13570 fn sample(param1: u8, param2: u8) {}
13571 "});
13572 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13573 .await;
13574}
13575
13576#[gpui::test]
13577async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13578 init_test(cx, |_| {});
13579
13580 let mut cx = EditorLspTestContext::new_rust(
13581 lsp::ServerCapabilities {
13582 signature_help_provider: Some(lsp::SignatureHelpOptions {
13583 ..Default::default()
13584 }),
13585 ..Default::default()
13586 },
13587 cx,
13588 )
13589 .await;
13590
13591 cx.set_state(indoc! {"
13592 fn main() {
13593 overloadedˇ
13594 }
13595 "});
13596
13597 cx.update_editor(|editor, window, cx| {
13598 editor.handle_input("(", window, cx);
13599 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13600 });
13601
13602 // Mock response with 3 signatures
13603 let mocked_response = lsp::SignatureHelp {
13604 signatures: vec![
13605 lsp::SignatureInformation {
13606 label: "fn overloaded(x: i32)".to_string(),
13607 documentation: None,
13608 parameters: Some(vec![lsp::ParameterInformation {
13609 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13610 documentation: None,
13611 }]),
13612 active_parameter: None,
13613 },
13614 lsp::SignatureInformation {
13615 label: "fn overloaded(x: i32, y: i32)".to_string(),
13616 documentation: None,
13617 parameters: Some(vec![
13618 lsp::ParameterInformation {
13619 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13620 documentation: None,
13621 },
13622 lsp::ParameterInformation {
13623 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13624 documentation: None,
13625 },
13626 ]),
13627 active_parameter: None,
13628 },
13629 lsp::SignatureInformation {
13630 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13631 documentation: None,
13632 parameters: Some(vec![
13633 lsp::ParameterInformation {
13634 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13635 documentation: None,
13636 },
13637 lsp::ParameterInformation {
13638 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13639 documentation: None,
13640 },
13641 lsp::ParameterInformation {
13642 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13643 documentation: None,
13644 },
13645 ]),
13646 active_parameter: None,
13647 },
13648 ],
13649 active_signature: Some(1),
13650 active_parameter: Some(0),
13651 };
13652 handle_signature_help_request(&mut cx, mocked_response).await;
13653
13654 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13655 .await;
13656
13657 // Verify we have multiple signatures and the right one is selected
13658 cx.editor(|editor, _, _| {
13659 let popover = editor.signature_help_state.popover().cloned().unwrap();
13660 assert_eq!(popover.signatures.len(), 3);
13661 // active_signature was 1, so that should be the current
13662 assert_eq!(popover.current_signature, 1);
13663 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13664 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13665 assert_eq!(
13666 popover.signatures[2].label,
13667 "fn overloaded(x: i32, y: i32, z: i32)"
13668 );
13669 });
13670
13671 // Test navigation functionality
13672 cx.update_editor(|editor, window, cx| {
13673 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13674 });
13675
13676 cx.editor(|editor, _, _| {
13677 let popover = editor.signature_help_state.popover().cloned().unwrap();
13678 assert_eq!(popover.current_signature, 2);
13679 });
13680
13681 // Test wrap around
13682 cx.update_editor(|editor, window, cx| {
13683 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13684 });
13685
13686 cx.editor(|editor, _, _| {
13687 let popover = editor.signature_help_state.popover().cloned().unwrap();
13688 assert_eq!(popover.current_signature, 0);
13689 });
13690
13691 // Test previous navigation
13692 cx.update_editor(|editor, window, cx| {
13693 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13694 });
13695
13696 cx.editor(|editor, _, _| {
13697 let popover = editor.signature_help_state.popover().cloned().unwrap();
13698 assert_eq!(popover.current_signature, 2);
13699 });
13700}
13701
13702#[gpui::test]
13703async fn test_completion_mode(cx: &mut TestAppContext) {
13704 init_test(cx, |_| {});
13705 let mut cx = EditorLspTestContext::new_rust(
13706 lsp::ServerCapabilities {
13707 completion_provider: Some(lsp::CompletionOptions {
13708 resolve_provider: Some(true),
13709 ..Default::default()
13710 }),
13711 ..Default::default()
13712 },
13713 cx,
13714 )
13715 .await;
13716
13717 struct Run {
13718 run_description: &'static str,
13719 initial_state: String,
13720 buffer_marked_text: String,
13721 completion_label: &'static str,
13722 completion_text: &'static str,
13723 expected_with_insert_mode: String,
13724 expected_with_replace_mode: String,
13725 expected_with_replace_subsequence_mode: String,
13726 expected_with_replace_suffix_mode: String,
13727 }
13728
13729 let runs = [
13730 Run {
13731 run_description: "Start of word matches completion text",
13732 initial_state: "before ediˇ after".into(),
13733 buffer_marked_text: "before <edi|> after".into(),
13734 completion_label: "editor",
13735 completion_text: "editor",
13736 expected_with_insert_mode: "before editorˇ after".into(),
13737 expected_with_replace_mode: "before editorˇ after".into(),
13738 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13739 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13740 },
13741 Run {
13742 run_description: "Accept same text at the middle of the word",
13743 initial_state: "before ediˇtor after".into(),
13744 buffer_marked_text: "before <edi|tor> after".into(),
13745 completion_label: "editor",
13746 completion_text: "editor",
13747 expected_with_insert_mode: "before editorˇtor after".into(),
13748 expected_with_replace_mode: "before editorˇ after".into(),
13749 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13750 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13751 },
13752 Run {
13753 run_description: "End of word matches completion text -- cursor at end",
13754 initial_state: "before torˇ after".into(),
13755 buffer_marked_text: "before <tor|> after".into(),
13756 completion_label: "editor",
13757 completion_text: "editor",
13758 expected_with_insert_mode: "before editorˇ after".into(),
13759 expected_with_replace_mode: "before editorˇ after".into(),
13760 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13761 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13762 },
13763 Run {
13764 run_description: "End of word matches completion text -- cursor at start",
13765 initial_state: "before ˇtor after".into(),
13766 buffer_marked_text: "before <|tor> after".into(),
13767 completion_label: "editor",
13768 completion_text: "editor",
13769 expected_with_insert_mode: "before editorˇtor after".into(),
13770 expected_with_replace_mode: "before editorˇ after".into(),
13771 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13772 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13773 },
13774 Run {
13775 run_description: "Prepend text containing whitespace",
13776 initial_state: "pˇfield: bool".into(),
13777 buffer_marked_text: "<p|field>: bool".into(),
13778 completion_label: "pub ",
13779 completion_text: "pub ",
13780 expected_with_insert_mode: "pub ˇfield: bool".into(),
13781 expected_with_replace_mode: "pub ˇ: bool".into(),
13782 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13783 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13784 },
13785 Run {
13786 run_description: "Add element to start of list",
13787 initial_state: "[element_ˇelement_2]".into(),
13788 buffer_marked_text: "[<element_|element_2>]".into(),
13789 completion_label: "element_1",
13790 completion_text: "element_1",
13791 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13792 expected_with_replace_mode: "[element_1ˇ]".into(),
13793 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13794 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13795 },
13796 Run {
13797 run_description: "Add element to start of list -- first and second elements are equal",
13798 initial_state: "[elˇelement]".into(),
13799 buffer_marked_text: "[<el|element>]".into(),
13800 completion_label: "element",
13801 completion_text: "element",
13802 expected_with_insert_mode: "[elementˇelement]".into(),
13803 expected_with_replace_mode: "[elementˇ]".into(),
13804 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13805 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13806 },
13807 Run {
13808 run_description: "Ends with matching suffix",
13809 initial_state: "SubˇError".into(),
13810 buffer_marked_text: "<Sub|Error>".into(),
13811 completion_label: "SubscriptionError",
13812 completion_text: "SubscriptionError",
13813 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13814 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13815 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13816 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13817 },
13818 Run {
13819 run_description: "Suffix is a subsequence -- contiguous",
13820 initial_state: "SubˇErr".into(),
13821 buffer_marked_text: "<Sub|Err>".into(),
13822 completion_label: "SubscriptionError",
13823 completion_text: "SubscriptionError",
13824 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13825 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13826 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13827 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13828 },
13829 Run {
13830 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13831 initial_state: "Suˇscrirr".into(),
13832 buffer_marked_text: "<Su|scrirr>".into(),
13833 completion_label: "SubscriptionError",
13834 completion_text: "SubscriptionError",
13835 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13836 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13837 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13838 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13839 },
13840 Run {
13841 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13842 initial_state: "foo(indˇix)".into(),
13843 buffer_marked_text: "foo(<ind|ix>)".into(),
13844 completion_label: "node_index",
13845 completion_text: "node_index",
13846 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13847 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13848 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13849 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13850 },
13851 Run {
13852 run_description: "Replace range ends before cursor - should extend to cursor",
13853 initial_state: "before editˇo after".into(),
13854 buffer_marked_text: "before <{ed}>it|o after".into(),
13855 completion_label: "editor",
13856 completion_text: "editor",
13857 expected_with_insert_mode: "before editorˇo after".into(),
13858 expected_with_replace_mode: "before editorˇo after".into(),
13859 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13860 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13861 },
13862 Run {
13863 run_description: "Uses label for suffix matching",
13864 initial_state: "before ediˇtor after".into(),
13865 buffer_marked_text: "before <edi|tor> after".into(),
13866 completion_label: "editor",
13867 completion_text: "editor()",
13868 expected_with_insert_mode: "before editor()ˇtor after".into(),
13869 expected_with_replace_mode: "before editor()ˇ after".into(),
13870 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13871 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13872 },
13873 Run {
13874 run_description: "Case insensitive subsequence and suffix matching",
13875 initial_state: "before EDiˇtoR after".into(),
13876 buffer_marked_text: "before <EDi|toR> after".into(),
13877 completion_label: "editor",
13878 completion_text: "editor",
13879 expected_with_insert_mode: "before editorˇtoR after".into(),
13880 expected_with_replace_mode: "before editorˇ after".into(),
13881 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13882 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13883 },
13884 ];
13885
13886 for run in runs {
13887 let run_variations = [
13888 (LspInsertMode::Insert, run.expected_with_insert_mode),
13889 (LspInsertMode::Replace, run.expected_with_replace_mode),
13890 (
13891 LspInsertMode::ReplaceSubsequence,
13892 run.expected_with_replace_subsequence_mode,
13893 ),
13894 (
13895 LspInsertMode::ReplaceSuffix,
13896 run.expected_with_replace_suffix_mode,
13897 ),
13898 ];
13899
13900 for (lsp_insert_mode, expected_text) in run_variations {
13901 eprintln!(
13902 "run = {:?}, mode = {lsp_insert_mode:.?}",
13903 run.run_description,
13904 );
13905
13906 update_test_language_settings(&mut cx, |settings| {
13907 settings.defaults.completions = Some(CompletionSettingsContent {
13908 lsp_insert_mode: Some(lsp_insert_mode),
13909 words: Some(WordsCompletionMode::Disabled),
13910 words_min_length: Some(0),
13911 ..Default::default()
13912 });
13913 });
13914
13915 cx.set_state(&run.initial_state);
13916 cx.update_editor(|editor, window, cx| {
13917 editor.show_completions(&ShowCompletions, window, cx);
13918 });
13919
13920 let counter = Arc::new(AtomicUsize::new(0));
13921 handle_completion_request_with_insert_and_replace(
13922 &mut cx,
13923 &run.buffer_marked_text,
13924 vec![(run.completion_label, run.completion_text)],
13925 counter.clone(),
13926 )
13927 .await;
13928 cx.condition(|editor, _| editor.context_menu_visible())
13929 .await;
13930 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13931
13932 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13933 editor
13934 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13935 .unwrap()
13936 });
13937 cx.assert_editor_state(&expected_text);
13938 handle_resolve_completion_request(&mut cx, None).await;
13939 apply_additional_edits.await.unwrap();
13940 }
13941 }
13942}
13943
13944#[gpui::test]
13945async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13946 init_test(cx, |_| {});
13947 let mut cx = EditorLspTestContext::new_rust(
13948 lsp::ServerCapabilities {
13949 completion_provider: Some(lsp::CompletionOptions {
13950 resolve_provider: Some(true),
13951 ..Default::default()
13952 }),
13953 ..Default::default()
13954 },
13955 cx,
13956 )
13957 .await;
13958
13959 let initial_state = "SubˇError";
13960 let buffer_marked_text = "<Sub|Error>";
13961 let completion_text = "SubscriptionError";
13962 let expected_with_insert_mode = "SubscriptionErrorˇError";
13963 let expected_with_replace_mode = "SubscriptionErrorˇ";
13964
13965 update_test_language_settings(&mut cx, |settings| {
13966 settings.defaults.completions = Some(CompletionSettingsContent {
13967 words: Some(WordsCompletionMode::Disabled),
13968 words_min_length: Some(0),
13969 // set the opposite here to ensure that the action is overriding the default behavior
13970 lsp_insert_mode: Some(LspInsertMode::Insert),
13971 ..Default::default()
13972 });
13973 });
13974
13975 cx.set_state(initial_state);
13976 cx.update_editor(|editor, window, cx| {
13977 editor.show_completions(&ShowCompletions, window, cx);
13978 });
13979
13980 let counter = Arc::new(AtomicUsize::new(0));
13981 handle_completion_request_with_insert_and_replace(
13982 &mut cx,
13983 buffer_marked_text,
13984 vec![(completion_text, completion_text)],
13985 counter.clone(),
13986 )
13987 .await;
13988 cx.condition(|editor, _| editor.context_menu_visible())
13989 .await;
13990 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13991
13992 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13993 editor
13994 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13995 .unwrap()
13996 });
13997 cx.assert_editor_state(expected_with_replace_mode);
13998 handle_resolve_completion_request(&mut cx, None).await;
13999 apply_additional_edits.await.unwrap();
14000
14001 update_test_language_settings(&mut cx, |settings| {
14002 settings.defaults.completions = Some(CompletionSettingsContent {
14003 words: Some(WordsCompletionMode::Disabled),
14004 words_min_length: Some(0),
14005 // set the opposite here to ensure that the action is overriding the default behavior
14006 lsp_insert_mode: Some(LspInsertMode::Replace),
14007 ..Default::default()
14008 });
14009 });
14010
14011 cx.set_state(initial_state);
14012 cx.update_editor(|editor, window, cx| {
14013 editor.show_completions(&ShowCompletions, window, cx);
14014 });
14015 handle_completion_request_with_insert_and_replace(
14016 &mut cx,
14017 buffer_marked_text,
14018 vec![(completion_text, completion_text)],
14019 counter.clone(),
14020 )
14021 .await;
14022 cx.condition(|editor, _| editor.context_menu_visible())
14023 .await;
14024 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14025
14026 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14027 editor
14028 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14029 .unwrap()
14030 });
14031 cx.assert_editor_state(expected_with_insert_mode);
14032 handle_resolve_completion_request(&mut cx, None).await;
14033 apply_additional_edits.await.unwrap();
14034}
14035
14036#[gpui::test]
14037async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14038 init_test(cx, |_| {});
14039 let mut cx = EditorLspTestContext::new_rust(
14040 lsp::ServerCapabilities {
14041 completion_provider: Some(lsp::CompletionOptions {
14042 resolve_provider: Some(true),
14043 ..Default::default()
14044 }),
14045 ..Default::default()
14046 },
14047 cx,
14048 )
14049 .await;
14050
14051 // scenario: surrounding text matches completion text
14052 let completion_text = "to_offset";
14053 let initial_state = indoc! {"
14054 1. buf.to_offˇsuffix
14055 2. buf.to_offˇsuf
14056 3. buf.to_offˇfix
14057 4. buf.to_offˇ
14058 5. into_offˇensive
14059 6. ˇsuffix
14060 7. let ˇ //
14061 8. aaˇzz
14062 9. buf.to_off«zzzzzˇ»suffix
14063 10. buf.«ˇzzzzz»suffix
14064 11. to_off«ˇzzzzz»
14065
14066 buf.to_offˇsuffix // newest cursor
14067 "};
14068 let completion_marked_buffer = indoc! {"
14069 1. buf.to_offsuffix
14070 2. buf.to_offsuf
14071 3. buf.to_offfix
14072 4. buf.to_off
14073 5. into_offensive
14074 6. suffix
14075 7. let //
14076 8. aazz
14077 9. buf.to_offzzzzzsuffix
14078 10. buf.zzzzzsuffix
14079 11. to_offzzzzz
14080
14081 buf.<to_off|suffix> // newest cursor
14082 "};
14083 let expected = indoc! {"
14084 1. buf.to_offsetˇ
14085 2. buf.to_offsetˇsuf
14086 3. buf.to_offsetˇfix
14087 4. buf.to_offsetˇ
14088 5. into_offsetˇensive
14089 6. to_offsetˇsuffix
14090 7. let to_offsetˇ //
14091 8. aato_offsetˇzz
14092 9. buf.to_offsetˇ
14093 10. buf.to_offsetˇsuffix
14094 11. to_offsetˇ
14095
14096 buf.to_offsetˇ // newest cursor
14097 "};
14098 cx.set_state(initial_state);
14099 cx.update_editor(|editor, window, cx| {
14100 editor.show_completions(&ShowCompletions, window, cx);
14101 });
14102 handle_completion_request_with_insert_and_replace(
14103 &mut cx,
14104 completion_marked_buffer,
14105 vec![(completion_text, completion_text)],
14106 Arc::new(AtomicUsize::new(0)),
14107 )
14108 .await;
14109 cx.condition(|editor, _| editor.context_menu_visible())
14110 .await;
14111 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14112 editor
14113 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14114 .unwrap()
14115 });
14116 cx.assert_editor_state(expected);
14117 handle_resolve_completion_request(&mut cx, None).await;
14118 apply_additional_edits.await.unwrap();
14119
14120 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14121 let completion_text = "foo_and_bar";
14122 let initial_state = indoc! {"
14123 1. ooanbˇ
14124 2. zooanbˇ
14125 3. ooanbˇz
14126 4. zooanbˇz
14127 5. ooanˇ
14128 6. oanbˇ
14129
14130 ooanbˇ
14131 "};
14132 let completion_marked_buffer = indoc! {"
14133 1. ooanb
14134 2. zooanb
14135 3. ooanbz
14136 4. zooanbz
14137 5. ooan
14138 6. oanb
14139
14140 <ooanb|>
14141 "};
14142 let expected = indoc! {"
14143 1. foo_and_barˇ
14144 2. zfoo_and_barˇ
14145 3. foo_and_barˇz
14146 4. zfoo_and_barˇz
14147 5. ooanfoo_and_barˇ
14148 6. oanbfoo_and_barˇ
14149
14150 foo_and_barˇ
14151 "};
14152 cx.set_state(initial_state);
14153 cx.update_editor(|editor, window, cx| {
14154 editor.show_completions(&ShowCompletions, window, cx);
14155 });
14156 handle_completion_request_with_insert_and_replace(
14157 &mut cx,
14158 completion_marked_buffer,
14159 vec![(completion_text, completion_text)],
14160 Arc::new(AtomicUsize::new(0)),
14161 )
14162 .await;
14163 cx.condition(|editor, _| editor.context_menu_visible())
14164 .await;
14165 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14166 editor
14167 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14168 .unwrap()
14169 });
14170 cx.assert_editor_state(expected);
14171 handle_resolve_completion_request(&mut cx, None).await;
14172 apply_additional_edits.await.unwrap();
14173
14174 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14175 // (expects the same as if it was inserted at the end)
14176 let completion_text = "foo_and_bar";
14177 let initial_state = indoc! {"
14178 1. ooˇanb
14179 2. zooˇanb
14180 3. ooˇanbz
14181 4. zooˇanbz
14182
14183 ooˇanb
14184 "};
14185 let completion_marked_buffer = indoc! {"
14186 1. ooanb
14187 2. zooanb
14188 3. ooanbz
14189 4. zooanbz
14190
14191 <oo|anb>
14192 "};
14193 let expected = indoc! {"
14194 1. foo_and_barˇ
14195 2. zfoo_and_barˇ
14196 3. foo_and_barˇz
14197 4. zfoo_and_barˇz
14198
14199 foo_and_barˇ
14200 "};
14201 cx.set_state(initial_state);
14202 cx.update_editor(|editor, window, cx| {
14203 editor.show_completions(&ShowCompletions, window, cx);
14204 });
14205 handle_completion_request_with_insert_and_replace(
14206 &mut cx,
14207 completion_marked_buffer,
14208 vec![(completion_text, completion_text)],
14209 Arc::new(AtomicUsize::new(0)),
14210 )
14211 .await;
14212 cx.condition(|editor, _| editor.context_menu_visible())
14213 .await;
14214 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14215 editor
14216 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14217 .unwrap()
14218 });
14219 cx.assert_editor_state(expected);
14220 handle_resolve_completion_request(&mut cx, None).await;
14221 apply_additional_edits.await.unwrap();
14222}
14223
14224// This used to crash
14225#[gpui::test]
14226async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14227 init_test(cx, |_| {});
14228
14229 let buffer_text = indoc! {"
14230 fn main() {
14231 10.satu;
14232
14233 //
14234 // separate cursors so they open in different excerpts (manually reproducible)
14235 //
14236
14237 10.satu20;
14238 }
14239 "};
14240 let multibuffer_text_with_selections = indoc! {"
14241 fn main() {
14242 10.satuˇ;
14243
14244 //
14245
14246 //
14247
14248 10.satuˇ20;
14249 }
14250 "};
14251 let expected_multibuffer = indoc! {"
14252 fn main() {
14253 10.saturating_sub()ˇ;
14254
14255 //
14256
14257 //
14258
14259 10.saturating_sub()ˇ;
14260 }
14261 "};
14262
14263 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14264 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14265
14266 let fs = FakeFs::new(cx.executor());
14267 fs.insert_tree(
14268 path!("/a"),
14269 json!({
14270 "main.rs": buffer_text,
14271 }),
14272 )
14273 .await;
14274
14275 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14276 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14277 language_registry.add(rust_lang());
14278 let mut fake_servers = language_registry.register_fake_lsp(
14279 "Rust",
14280 FakeLspAdapter {
14281 capabilities: lsp::ServerCapabilities {
14282 completion_provider: Some(lsp::CompletionOptions {
14283 resolve_provider: None,
14284 ..lsp::CompletionOptions::default()
14285 }),
14286 ..lsp::ServerCapabilities::default()
14287 },
14288 ..FakeLspAdapter::default()
14289 },
14290 );
14291 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14292 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14293 let buffer = project
14294 .update(cx, |project, cx| {
14295 project.open_local_buffer(path!("/a/main.rs"), cx)
14296 })
14297 .await
14298 .unwrap();
14299
14300 let multi_buffer = cx.new(|cx| {
14301 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14302 multi_buffer.push_excerpts(
14303 buffer.clone(),
14304 [ExcerptRange::new(0..first_excerpt_end)],
14305 cx,
14306 );
14307 multi_buffer.push_excerpts(
14308 buffer.clone(),
14309 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14310 cx,
14311 );
14312 multi_buffer
14313 });
14314
14315 let editor = workspace
14316 .update(cx, |_, window, cx| {
14317 cx.new(|cx| {
14318 Editor::new(
14319 EditorMode::Full {
14320 scale_ui_elements_with_buffer_font_size: false,
14321 show_active_line_background: false,
14322 sizing_behavior: SizingBehavior::Default,
14323 },
14324 multi_buffer.clone(),
14325 Some(project.clone()),
14326 window,
14327 cx,
14328 )
14329 })
14330 })
14331 .unwrap();
14332
14333 let pane = workspace
14334 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14335 .unwrap();
14336 pane.update_in(cx, |pane, window, cx| {
14337 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14338 });
14339
14340 let fake_server = fake_servers.next().await.unwrap();
14341
14342 editor.update_in(cx, |editor, window, cx| {
14343 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14344 s.select_ranges([
14345 Point::new(1, 11)..Point::new(1, 11),
14346 Point::new(7, 11)..Point::new(7, 11),
14347 ])
14348 });
14349
14350 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14351 });
14352
14353 editor.update_in(cx, |editor, window, cx| {
14354 editor.show_completions(&ShowCompletions, window, cx);
14355 });
14356
14357 fake_server
14358 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14359 let completion_item = lsp::CompletionItem {
14360 label: "saturating_sub()".into(),
14361 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14362 lsp::InsertReplaceEdit {
14363 new_text: "saturating_sub()".to_owned(),
14364 insert: lsp::Range::new(
14365 lsp::Position::new(7, 7),
14366 lsp::Position::new(7, 11),
14367 ),
14368 replace: lsp::Range::new(
14369 lsp::Position::new(7, 7),
14370 lsp::Position::new(7, 13),
14371 ),
14372 },
14373 )),
14374 ..lsp::CompletionItem::default()
14375 };
14376
14377 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14378 })
14379 .next()
14380 .await
14381 .unwrap();
14382
14383 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14384 .await;
14385
14386 editor
14387 .update_in(cx, |editor, window, cx| {
14388 editor
14389 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14390 .unwrap()
14391 })
14392 .await
14393 .unwrap();
14394
14395 editor.update(cx, |editor, cx| {
14396 assert_text_with_selections(editor, expected_multibuffer, cx);
14397 })
14398}
14399
14400#[gpui::test]
14401async fn test_completion(cx: &mut TestAppContext) {
14402 init_test(cx, |_| {});
14403
14404 let mut cx = EditorLspTestContext::new_rust(
14405 lsp::ServerCapabilities {
14406 completion_provider: Some(lsp::CompletionOptions {
14407 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14408 resolve_provider: Some(true),
14409 ..Default::default()
14410 }),
14411 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14412 ..Default::default()
14413 },
14414 cx,
14415 )
14416 .await;
14417 let counter = Arc::new(AtomicUsize::new(0));
14418
14419 cx.set_state(indoc! {"
14420 oneˇ
14421 two
14422 three
14423 "});
14424 cx.simulate_keystroke(".");
14425 handle_completion_request(
14426 indoc! {"
14427 one.|<>
14428 two
14429 three
14430 "},
14431 vec!["first_completion", "second_completion"],
14432 true,
14433 counter.clone(),
14434 &mut cx,
14435 )
14436 .await;
14437 cx.condition(|editor, _| editor.context_menu_visible())
14438 .await;
14439 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14440
14441 let _handler = handle_signature_help_request(
14442 &mut cx,
14443 lsp::SignatureHelp {
14444 signatures: vec![lsp::SignatureInformation {
14445 label: "test signature".to_string(),
14446 documentation: None,
14447 parameters: Some(vec![lsp::ParameterInformation {
14448 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14449 documentation: None,
14450 }]),
14451 active_parameter: None,
14452 }],
14453 active_signature: None,
14454 active_parameter: None,
14455 },
14456 );
14457 cx.update_editor(|editor, window, cx| {
14458 assert!(
14459 !editor.signature_help_state.is_shown(),
14460 "No signature help was called for"
14461 );
14462 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14463 });
14464 cx.run_until_parked();
14465 cx.update_editor(|editor, _, _| {
14466 assert!(
14467 !editor.signature_help_state.is_shown(),
14468 "No signature help should be shown when completions menu is open"
14469 );
14470 });
14471
14472 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14473 editor.context_menu_next(&Default::default(), window, cx);
14474 editor
14475 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14476 .unwrap()
14477 });
14478 cx.assert_editor_state(indoc! {"
14479 one.second_completionˇ
14480 two
14481 three
14482 "});
14483
14484 handle_resolve_completion_request(
14485 &mut cx,
14486 Some(vec![
14487 (
14488 //This overlaps with the primary completion edit which is
14489 //misbehavior from the LSP spec, test that we filter it out
14490 indoc! {"
14491 one.second_ˇcompletion
14492 two
14493 threeˇ
14494 "},
14495 "overlapping additional edit",
14496 ),
14497 (
14498 indoc! {"
14499 one.second_completion
14500 two
14501 threeˇ
14502 "},
14503 "\nadditional edit",
14504 ),
14505 ]),
14506 )
14507 .await;
14508 apply_additional_edits.await.unwrap();
14509 cx.assert_editor_state(indoc! {"
14510 one.second_completionˇ
14511 two
14512 three
14513 additional edit
14514 "});
14515
14516 cx.set_state(indoc! {"
14517 one.second_completion
14518 twoˇ
14519 threeˇ
14520 additional edit
14521 "});
14522 cx.simulate_keystroke(" ");
14523 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14524 cx.simulate_keystroke("s");
14525 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14526
14527 cx.assert_editor_state(indoc! {"
14528 one.second_completion
14529 two sˇ
14530 three sˇ
14531 additional edit
14532 "});
14533 handle_completion_request(
14534 indoc! {"
14535 one.second_completion
14536 two s
14537 three <s|>
14538 additional edit
14539 "},
14540 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14541 true,
14542 counter.clone(),
14543 &mut cx,
14544 )
14545 .await;
14546 cx.condition(|editor, _| editor.context_menu_visible())
14547 .await;
14548 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14549
14550 cx.simulate_keystroke("i");
14551
14552 handle_completion_request(
14553 indoc! {"
14554 one.second_completion
14555 two si
14556 three <si|>
14557 additional edit
14558 "},
14559 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14560 true,
14561 counter.clone(),
14562 &mut cx,
14563 )
14564 .await;
14565 cx.condition(|editor, _| editor.context_menu_visible())
14566 .await;
14567 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14568
14569 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14570 editor
14571 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14572 .unwrap()
14573 });
14574 cx.assert_editor_state(indoc! {"
14575 one.second_completion
14576 two sixth_completionˇ
14577 three sixth_completionˇ
14578 additional edit
14579 "});
14580
14581 apply_additional_edits.await.unwrap();
14582
14583 update_test_language_settings(&mut cx, |settings| {
14584 settings.defaults.show_completions_on_input = Some(false);
14585 });
14586 cx.set_state("editorˇ");
14587 cx.simulate_keystroke(".");
14588 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14589 cx.simulate_keystrokes("c l o");
14590 cx.assert_editor_state("editor.cloˇ");
14591 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14592 cx.update_editor(|editor, window, cx| {
14593 editor.show_completions(&ShowCompletions, window, cx);
14594 });
14595 handle_completion_request(
14596 "editor.<clo|>",
14597 vec!["close", "clobber"],
14598 true,
14599 counter.clone(),
14600 &mut cx,
14601 )
14602 .await;
14603 cx.condition(|editor, _| editor.context_menu_visible())
14604 .await;
14605 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14606
14607 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14608 editor
14609 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14610 .unwrap()
14611 });
14612 cx.assert_editor_state("editor.clobberˇ");
14613 handle_resolve_completion_request(&mut cx, None).await;
14614 apply_additional_edits.await.unwrap();
14615}
14616
14617#[gpui::test]
14618async fn test_completion_reuse(cx: &mut TestAppContext) {
14619 init_test(cx, |_| {});
14620
14621 let mut cx = EditorLspTestContext::new_rust(
14622 lsp::ServerCapabilities {
14623 completion_provider: Some(lsp::CompletionOptions {
14624 trigger_characters: Some(vec![".".to_string()]),
14625 ..Default::default()
14626 }),
14627 ..Default::default()
14628 },
14629 cx,
14630 )
14631 .await;
14632
14633 let counter = Arc::new(AtomicUsize::new(0));
14634 cx.set_state("objˇ");
14635 cx.simulate_keystroke(".");
14636
14637 // Initial completion request returns complete results
14638 let is_incomplete = false;
14639 handle_completion_request(
14640 "obj.|<>",
14641 vec!["a", "ab", "abc"],
14642 is_incomplete,
14643 counter.clone(),
14644 &mut cx,
14645 )
14646 .await;
14647 cx.run_until_parked();
14648 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14649 cx.assert_editor_state("obj.ˇ");
14650 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14651
14652 // Type "a" - filters existing completions
14653 cx.simulate_keystroke("a");
14654 cx.run_until_parked();
14655 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14656 cx.assert_editor_state("obj.aˇ");
14657 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14658
14659 // Type "b" - filters existing completions
14660 cx.simulate_keystroke("b");
14661 cx.run_until_parked();
14662 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14663 cx.assert_editor_state("obj.abˇ");
14664 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14665
14666 // Type "c" - filters existing completions
14667 cx.simulate_keystroke("c");
14668 cx.run_until_parked();
14669 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14670 cx.assert_editor_state("obj.abcˇ");
14671 check_displayed_completions(vec!["abc"], &mut cx);
14672
14673 // Backspace to delete "c" - filters existing completions
14674 cx.update_editor(|editor, window, cx| {
14675 editor.backspace(&Backspace, window, cx);
14676 });
14677 cx.run_until_parked();
14678 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14679 cx.assert_editor_state("obj.abˇ");
14680 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14681
14682 // Moving cursor to the left dismisses menu.
14683 cx.update_editor(|editor, window, cx| {
14684 editor.move_left(&MoveLeft, window, cx);
14685 });
14686 cx.run_until_parked();
14687 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14688 cx.assert_editor_state("obj.aˇb");
14689 cx.update_editor(|editor, _, _| {
14690 assert_eq!(editor.context_menu_visible(), false);
14691 });
14692
14693 // Type "b" - new request
14694 cx.simulate_keystroke("b");
14695 let is_incomplete = false;
14696 handle_completion_request(
14697 "obj.<ab|>a",
14698 vec!["ab", "abc"],
14699 is_incomplete,
14700 counter.clone(),
14701 &mut cx,
14702 )
14703 .await;
14704 cx.run_until_parked();
14705 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14706 cx.assert_editor_state("obj.abˇb");
14707 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14708
14709 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14710 cx.update_editor(|editor, window, cx| {
14711 editor.backspace(&Backspace, window, cx);
14712 });
14713 let is_incomplete = false;
14714 handle_completion_request(
14715 "obj.<a|>b",
14716 vec!["a", "ab", "abc"],
14717 is_incomplete,
14718 counter.clone(),
14719 &mut cx,
14720 )
14721 .await;
14722 cx.run_until_parked();
14723 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14724 cx.assert_editor_state("obj.aˇb");
14725 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14726
14727 // Backspace to delete "a" - dismisses menu.
14728 cx.update_editor(|editor, window, cx| {
14729 editor.backspace(&Backspace, window, cx);
14730 });
14731 cx.run_until_parked();
14732 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14733 cx.assert_editor_state("obj.ˇb");
14734 cx.update_editor(|editor, _, _| {
14735 assert_eq!(editor.context_menu_visible(), false);
14736 });
14737}
14738
14739#[gpui::test]
14740async fn test_word_completion(cx: &mut TestAppContext) {
14741 let lsp_fetch_timeout_ms = 10;
14742 init_test(cx, |language_settings| {
14743 language_settings.defaults.completions = Some(CompletionSettingsContent {
14744 words_min_length: Some(0),
14745 lsp_fetch_timeout_ms: Some(10),
14746 lsp_insert_mode: Some(LspInsertMode::Insert),
14747 ..Default::default()
14748 });
14749 });
14750
14751 let mut cx = EditorLspTestContext::new_rust(
14752 lsp::ServerCapabilities {
14753 completion_provider: Some(lsp::CompletionOptions {
14754 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14755 ..lsp::CompletionOptions::default()
14756 }),
14757 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14758 ..lsp::ServerCapabilities::default()
14759 },
14760 cx,
14761 )
14762 .await;
14763
14764 let throttle_completions = Arc::new(AtomicBool::new(false));
14765
14766 let lsp_throttle_completions = throttle_completions.clone();
14767 let _completion_requests_handler =
14768 cx.lsp
14769 .server
14770 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14771 let lsp_throttle_completions = lsp_throttle_completions.clone();
14772 let cx = cx.clone();
14773 async move {
14774 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14775 cx.background_executor()
14776 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14777 .await;
14778 }
14779 Ok(Some(lsp::CompletionResponse::Array(vec![
14780 lsp::CompletionItem {
14781 label: "first".into(),
14782 ..lsp::CompletionItem::default()
14783 },
14784 lsp::CompletionItem {
14785 label: "last".into(),
14786 ..lsp::CompletionItem::default()
14787 },
14788 ])))
14789 }
14790 });
14791
14792 cx.set_state(indoc! {"
14793 oneˇ
14794 two
14795 three
14796 "});
14797 cx.simulate_keystroke(".");
14798 cx.executor().run_until_parked();
14799 cx.condition(|editor, _| editor.context_menu_visible())
14800 .await;
14801 cx.update_editor(|editor, window, cx| {
14802 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14803 {
14804 assert_eq!(
14805 completion_menu_entries(menu),
14806 &["first", "last"],
14807 "When LSP server is fast to reply, no fallback word completions are used"
14808 );
14809 } else {
14810 panic!("expected completion menu to be open");
14811 }
14812 editor.cancel(&Cancel, window, cx);
14813 });
14814 cx.executor().run_until_parked();
14815 cx.condition(|editor, _| !editor.context_menu_visible())
14816 .await;
14817
14818 throttle_completions.store(true, atomic::Ordering::Release);
14819 cx.simulate_keystroke(".");
14820 cx.executor()
14821 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14822 cx.executor().run_until_parked();
14823 cx.condition(|editor, _| editor.context_menu_visible())
14824 .await;
14825 cx.update_editor(|editor, _, _| {
14826 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14827 {
14828 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14829 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14830 } else {
14831 panic!("expected completion menu to be open");
14832 }
14833 });
14834}
14835
14836#[gpui::test]
14837async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14838 init_test(cx, |language_settings| {
14839 language_settings.defaults.completions = Some(CompletionSettingsContent {
14840 words: Some(WordsCompletionMode::Enabled),
14841 words_min_length: Some(0),
14842 lsp_insert_mode: Some(LspInsertMode::Insert),
14843 ..Default::default()
14844 });
14845 });
14846
14847 let mut cx = EditorLspTestContext::new_rust(
14848 lsp::ServerCapabilities {
14849 completion_provider: Some(lsp::CompletionOptions {
14850 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14851 ..lsp::CompletionOptions::default()
14852 }),
14853 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14854 ..lsp::ServerCapabilities::default()
14855 },
14856 cx,
14857 )
14858 .await;
14859
14860 let _completion_requests_handler =
14861 cx.lsp
14862 .server
14863 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14864 Ok(Some(lsp::CompletionResponse::Array(vec![
14865 lsp::CompletionItem {
14866 label: "first".into(),
14867 ..lsp::CompletionItem::default()
14868 },
14869 lsp::CompletionItem {
14870 label: "last".into(),
14871 ..lsp::CompletionItem::default()
14872 },
14873 ])))
14874 });
14875
14876 cx.set_state(indoc! {"ˇ
14877 first
14878 last
14879 second
14880 "});
14881 cx.simulate_keystroke(".");
14882 cx.executor().run_until_parked();
14883 cx.condition(|editor, _| editor.context_menu_visible())
14884 .await;
14885 cx.update_editor(|editor, _, _| {
14886 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14887 {
14888 assert_eq!(
14889 completion_menu_entries(menu),
14890 &["first", "last", "second"],
14891 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14892 );
14893 } else {
14894 panic!("expected completion menu to be open");
14895 }
14896 });
14897}
14898
14899#[gpui::test]
14900async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14901 init_test(cx, |language_settings| {
14902 language_settings.defaults.completions = Some(CompletionSettingsContent {
14903 words: Some(WordsCompletionMode::Disabled),
14904 words_min_length: Some(0),
14905 lsp_insert_mode: Some(LspInsertMode::Insert),
14906 ..Default::default()
14907 });
14908 });
14909
14910 let mut cx = EditorLspTestContext::new_rust(
14911 lsp::ServerCapabilities {
14912 completion_provider: Some(lsp::CompletionOptions {
14913 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14914 ..lsp::CompletionOptions::default()
14915 }),
14916 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14917 ..lsp::ServerCapabilities::default()
14918 },
14919 cx,
14920 )
14921 .await;
14922
14923 let _completion_requests_handler =
14924 cx.lsp
14925 .server
14926 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14927 panic!("LSP completions should not be queried when dealing with word completions")
14928 });
14929
14930 cx.set_state(indoc! {"ˇ
14931 first
14932 last
14933 second
14934 "});
14935 cx.update_editor(|editor, window, cx| {
14936 editor.show_word_completions(&ShowWordCompletions, window, cx);
14937 });
14938 cx.executor().run_until_parked();
14939 cx.condition(|editor, _| editor.context_menu_visible())
14940 .await;
14941 cx.update_editor(|editor, _, _| {
14942 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14943 {
14944 assert_eq!(
14945 completion_menu_entries(menu),
14946 &["first", "last", "second"],
14947 "`ShowWordCompletions` action should show word completions"
14948 );
14949 } else {
14950 panic!("expected completion menu to be open");
14951 }
14952 });
14953
14954 cx.simulate_keystroke("l");
14955 cx.executor().run_until_parked();
14956 cx.condition(|editor, _| editor.context_menu_visible())
14957 .await;
14958 cx.update_editor(|editor, _, _| {
14959 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14960 {
14961 assert_eq!(
14962 completion_menu_entries(menu),
14963 &["last"],
14964 "After showing word completions, further editing should filter them and not query the LSP"
14965 );
14966 } else {
14967 panic!("expected completion menu to be open");
14968 }
14969 });
14970}
14971
14972#[gpui::test]
14973async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14974 init_test(cx, |language_settings| {
14975 language_settings.defaults.completions = Some(CompletionSettingsContent {
14976 words_min_length: Some(0),
14977 lsp: Some(false),
14978 lsp_insert_mode: Some(LspInsertMode::Insert),
14979 ..Default::default()
14980 });
14981 });
14982
14983 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14984
14985 cx.set_state(indoc! {"ˇ
14986 0_usize
14987 let
14988 33
14989 4.5f32
14990 "});
14991 cx.update_editor(|editor, window, cx| {
14992 editor.show_completions(&ShowCompletions, window, cx);
14993 });
14994 cx.executor().run_until_parked();
14995 cx.condition(|editor, _| editor.context_menu_visible())
14996 .await;
14997 cx.update_editor(|editor, window, cx| {
14998 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14999 {
15000 assert_eq!(
15001 completion_menu_entries(menu),
15002 &["let"],
15003 "With no digits in the completion query, no digits should be in the word completions"
15004 );
15005 } else {
15006 panic!("expected completion menu to be open");
15007 }
15008 editor.cancel(&Cancel, window, cx);
15009 });
15010
15011 cx.set_state(indoc! {"3ˇ
15012 0_usize
15013 let
15014 3
15015 33.35f32
15016 "});
15017 cx.update_editor(|editor, window, cx| {
15018 editor.show_completions(&ShowCompletions, window, cx);
15019 });
15020 cx.executor().run_until_parked();
15021 cx.condition(|editor, _| editor.context_menu_visible())
15022 .await;
15023 cx.update_editor(|editor, _, _| {
15024 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15025 {
15026 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15027 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15028 } else {
15029 panic!("expected completion menu to be open");
15030 }
15031 });
15032}
15033
15034#[gpui::test]
15035async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15036 init_test(cx, |language_settings| {
15037 language_settings.defaults.completions = Some(CompletionSettingsContent {
15038 words: Some(WordsCompletionMode::Enabled),
15039 words_min_length: Some(3),
15040 lsp_insert_mode: Some(LspInsertMode::Insert),
15041 ..Default::default()
15042 });
15043 });
15044
15045 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15046 cx.set_state(indoc! {"ˇ
15047 wow
15048 wowen
15049 wowser
15050 "});
15051 cx.simulate_keystroke("w");
15052 cx.executor().run_until_parked();
15053 cx.update_editor(|editor, _, _| {
15054 if editor.context_menu.borrow_mut().is_some() {
15055 panic!(
15056 "expected completion menu to be hidden, as words completion threshold is not met"
15057 );
15058 }
15059 });
15060
15061 cx.update_editor(|editor, window, cx| {
15062 editor.show_word_completions(&ShowWordCompletions, window, cx);
15063 });
15064 cx.executor().run_until_parked();
15065 cx.update_editor(|editor, window, cx| {
15066 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15067 {
15068 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");
15069 } else {
15070 panic!("expected completion menu to be open after the word completions are called with an action");
15071 }
15072
15073 editor.cancel(&Cancel, window, cx);
15074 });
15075 cx.update_editor(|editor, _, _| {
15076 if editor.context_menu.borrow_mut().is_some() {
15077 panic!("expected completion menu to be hidden after canceling");
15078 }
15079 });
15080
15081 cx.simulate_keystroke("o");
15082 cx.executor().run_until_parked();
15083 cx.update_editor(|editor, _, _| {
15084 if editor.context_menu.borrow_mut().is_some() {
15085 panic!(
15086 "expected completion menu to be hidden, as words completion threshold is not met still"
15087 );
15088 }
15089 });
15090
15091 cx.simulate_keystroke("w");
15092 cx.executor().run_until_parked();
15093 cx.update_editor(|editor, _, _| {
15094 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15095 {
15096 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15097 } else {
15098 panic!("expected completion menu to be open after the word completions threshold is met");
15099 }
15100 });
15101}
15102
15103#[gpui::test]
15104async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15105 init_test(cx, |language_settings| {
15106 language_settings.defaults.completions = Some(CompletionSettingsContent {
15107 words: Some(WordsCompletionMode::Enabled),
15108 words_min_length: Some(0),
15109 lsp_insert_mode: Some(LspInsertMode::Insert),
15110 ..Default::default()
15111 });
15112 });
15113
15114 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15115 cx.update_editor(|editor, _, _| {
15116 editor.disable_word_completions();
15117 });
15118 cx.set_state(indoc! {"ˇ
15119 wow
15120 wowen
15121 wowser
15122 "});
15123 cx.simulate_keystroke("w");
15124 cx.executor().run_until_parked();
15125 cx.update_editor(|editor, _, _| {
15126 if editor.context_menu.borrow_mut().is_some() {
15127 panic!(
15128 "expected completion menu to be hidden, as words completion are disabled for this editor"
15129 );
15130 }
15131 });
15132
15133 cx.update_editor(|editor, window, cx| {
15134 editor.show_word_completions(&ShowWordCompletions, window, cx);
15135 });
15136 cx.executor().run_until_parked();
15137 cx.update_editor(|editor, _, _| {
15138 if editor.context_menu.borrow_mut().is_some() {
15139 panic!(
15140 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15141 );
15142 }
15143 });
15144}
15145
15146#[gpui::test]
15147async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15148 init_test(cx, |language_settings| {
15149 language_settings.defaults.completions = Some(CompletionSettingsContent {
15150 words: Some(WordsCompletionMode::Disabled),
15151 words_min_length: Some(0),
15152 lsp_insert_mode: Some(LspInsertMode::Insert),
15153 ..Default::default()
15154 });
15155 });
15156
15157 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15158 cx.update_editor(|editor, _, _| {
15159 editor.set_completion_provider(None);
15160 });
15161 cx.set_state(indoc! {"ˇ
15162 wow
15163 wowen
15164 wowser
15165 "});
15166 cx.simulate_keystroke("w");
15167 cx.executor().run_until_parked();
15168 cx.update_editor(|editor, _, _| {
15169 if editor.context_menu.borrow_mut().is_some() {
15170 panic!("expected completion menu to be hidden, as disabled in settings");
15171 }
15172 });
15173}
15174
15175fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15176 let position = || lsp::Position {
15177 line: params.text_document_position.position.line,
15178 character: params.text_document_position.position.character,
15179 };
15180 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15181 range: lsp::Range {
15182 start: position(),
15183 end: position(),
15184 },
15185 new_text: text.to_string(),
15186 }))
15187}
15188
15189#[gpui::test]
15190async fn test_multiline_completion(cx: &mut TestAppContext) {
15191 init_test(cx, |_| {});
15192
15193 let fs = FakeFs::new(cx.executor());
15194 fs.insert_tree(
15195 path!("/a"),
15196 json!({
15197 "main.ts": "a",
15198 }),
15199 )
15200 .await;
15201
15202 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15203 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15204 let typescript_language = Arc::new(Language::new(
15205 LanguageConfig {
15206 name: "TypeScript".into(),
15207 matcher: LanguageMatcher {
15208 path_suffixes: vec!["ts".to_string()],
15209 ..LanguageMatcher::default()
15210 },
15211 line_comments: vec!["// ".into()],
15212 ..LanguageConfig::default()
15213 },
15214 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15215 ));
15216 language_registry.add(typescript_language.clone());
15217 let mut fake_servers = language_registry.register_fake_lsp(
15218 "TypeScript",
15219 FakeLspAdapter {
15220 capabilities: lsp::ServerCapabilities {
15221 completion_provider: Some(lsp::CompletionOptions {
15222 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15223 ..lsp::CompletionOptions::default()
15224 }),
15225 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15226 ..lsp::ServerCapabilities::default()
15227 },
15228 // Emulate vtsls label generation
15229 label_for_completion: Some(Box::new(|item, _| {
15230 let text = if let Some(description) = item
15231 .label_details
15232 .as_ref()
15233 .and_then(|label_details| label_details.description.as_ref())
15234 {
15235 format!("{} {}", item.label, description)
15236 } else if let Some(detail) = &item.detail {
15237 format!("{} {}", item.label, detail)
15238 } else {
15239 item.label.clone()
15240 };
15241 Some(language::CodeLabel::plain(text, None))
15242 })),
15243 ..FakeLspAdapter::default()
15244 },
15245 );
15246 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15247 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15248 let worktree_id = workspace
15249 .update(cx, |workspace, _window, cx| {
15250 workspace.project().update(cx, |project, cx| {
15251 project.worktrees(cx).next().unwrap().read(cx).id()
15252 })
15253 })
15254 .unwrap();
15255 let _buffer = project
15256 .update(cx, |project, cx| {
15257 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15258 })
15259 .await
15260 .unwrap();
15261 let editor = workspace
15262 .update(cx, |workspace, window, cx| {
15263 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15264 })
15265 .unwrap()
15266 .await
15267 .unwrap()
15268 .downcast::<Editor>()
15269 .unwrap();
15270 let fake_server = fake_servers.next().await.unwrap();
15271
15272 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15273 let multiline_label_2 = "a\nb\nc\n";
15274 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15275 let multiline_description = "d\ne\nf\n";
15276 let multiline_detail_2 = "g\nh\ni\n";
15277
15278 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15279 move |params, _| async move {
15280 Ok(Some(lsp::CompletionResponse::Array(vec![
15281 lsp::CompletionItem {
15282 label: multiline_label.to_string(),
15283 text_edit: gen_text_edit(¶ms, "new_text_1"),
15284 ..lsp::CompletionItem::default()
15285 },
15286 lsp::CompletionItem {
15287 label: "single line label 1".to_string(),
15288 detail: Some(multiline_detail.to_string()),
15289 text_edit: gen_text_edit(¶ms, "new_text_2"),
15290 ..lsp::CompletionItem::default()
15291 },
15292 lsp::CompletionItem {
15293 label: "single line label 2".to_string(),
15294 label_details: Some(lsp::CompletionItemLabelDetails {
15295 description: Some(multiline_description.to_string()),
15296 detail: None,
15297 }),
15298 text_edit: gen_text_edit(¶ms, "new_text_2"),
15299 ..lsp::CompletionItem::default()
15300 },
15301 lsp::CompletionItem {
15302 label: multiline_label_2.to_string(),
15303 detail: Some(multiline_detail_2.to_string()),
15304 text_edit: gen_text_edit(¶ms, "new_text_3"),
15305 ..lsp::CompletionItem::default()
15306 },
15307 lsp::CompletionItem {
15308 label: "Label with many spaces and \t but without newlines".to_string(),
15309 detail: Some(
15310 "Details with many spaces and \t but without newlines".to_string(),
15311 ),
15312 text_edit: gen_text_edit(¶ms, "new_text_4"),
15313 ..lsp::CompletionItem::default()
15314 },
15315 ])))
15316 },
15317 );
15318
15319 editor.update_in(cx, |editor, window, cx| {
15320 cx.focus_self(window);
15321 editor.move_to_end(&MoveToEnd, window, cx);
15322 editor.handle_input(".", window, cx);
15323 });
15324 cx.run_until_parked();
15325 completion_handle.next().await.unwrap();
15326
15327 editor.update(cx, |editor, _| {
15328 assert!(editor.context_menu_visible());
15329 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15330 {
15331 let completion_labels = menu
15332 .completions
15333 .borrow()
15334 .iter()
15335 .map(|c| c.label.text.clone())
15336 .collect::<Vec<_>>();
15337 assert_eq!(
15338 completion_labels,
15339 &[
15340 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15341 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15342 "single line label 2 d e f ",
15343 "a b c g h i ",
15344 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15345 ],
15346 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15347 );
15348
15349 for completion in menu
15350 .completions
15351 .borrow()
15352 .iter() {
15353 assert_eq!(
15354 completion.label.filter_range,
15355 0..completion.label.text.len(),
15356 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15357 );
15358 }
15359 } else {
15360 panic!("expected completion menu to be open");
15361 }
15362 });
15363}
15364
15365#[gpui::test]
15366async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15367 init_test(cx, |_| {});
15368 let mut cx = EditorLspTestContext::new_rust(
15369 lsp::ServerCapabilities {
15370 completion_provider: Some(lsp::CompletionOptions {
15371 trigger_characters: Some(vec![".".to_string()]),
15372 ..Default::default()
15373 }),
15374 ..Default::default()
15375 },
15376 cx,
15377 )
15378 .await;
15379 cx.lsp
15380 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15381 Ok(Some(lsp::CompletionResponse::Array(vec![
15382 lsp::CompletionItem {
15383 label: "first".into(),
15384 ..Default::default()
15385 },
15386 lsp::CompletionItem {
15387 label: "last".into(),
15388 ..Default::default()
15389 },
15390 ])))
15391 });
15392 cx.set_state("variableˇ");
15393 cx.simulate_keystroke(".");
15394 cx.executor().run_until_parked();
15395
15396 cx.update_editor(|editor, _, _| {
15397 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15398 {
15399 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15400 } else {
15401 panic!("expected completion menu to be open");
15402 }
15403 });
15404
15405 cx.update_editor(|editor, window, cx| {
15406 editor.move_page_down(&MovePageDown::default(), window, cx);
15407 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15408 {
15409 assert!(
15410 menu.selected_item == 1,
15411 "expected PageDown to select the last item from the context menu"
15412 );
15413 } else {
15414 panic!("expected completion menu to stay open after PageDown");
15415 }
15416 });
15417
15418 cx.update_editor(|editor, window, cx| {
15419 editor.move_page_up(&MovePageUp::default(), window, cx);
15420 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15421 {
15422 assert!(
15423 menu.selected_item == 0,
15424 "expected PageUp to select the first item from the context menu"
15425 );
15426 } else {
15427 panic!("expected completion menu to stay open after PageUp");
15428 }
15429 });
15430}
15431
15432#[gpui::test]
15433async fn test_as_is_completions(cx: &mut TestAppContext) {
15434 init_test(cx, |_| {});
15435 let mut cx = EditorLspTestContext::new_rust(
15436 lsp::ServerCapabilities {
15437 completion_provider: Some(lsp::CompletionOptions {
15438 ..Default::default()
15439 }),
15440 ..Default::default()
15441 },
15442 cx,
15443 )
15444 .await;
15445 cx.lsp
15446 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15447 Ok(Some(lsp::CompletionResponse::Array(vec![
15448 lsp::CompletionItem {
15449 label: "unsafe".into(),
15450 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15451 range: lsp::Range {
15452 start: lsp::Position {
15453 line: 1,
15454 character: 2,
15455 },
15456 end: lsp::Position {
15457 line: 1,
15458 character: 3,
15459 },
15460 },
15461 new_text: "unsafe".to_string(),
15462 })),
15463 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15464 ..Default::default()
15465 },
15466 ])))
15467 });
15468 cx.set_state("fn a() {}\n nˇ");
15469 cx.executor().run_until_parked();
15470 cx.update_editor(|editor, window, cx| {
15471 editor.trigger_completion_on_input("n", true, window, cx)
15472 });
15473 cx.executor().run_until_parked();
15474
15475 cx.update_editor(|editor, window, cx| {
15476 editor.confirm_completion(&Default::default(), window, cx)
15477 });
15478 cx.executor().run_until_parked();
15479 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15480}
15481
15482#[gpui::test]
15483async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15484 init_test(cx, |_| {});
15485 let language =
15486 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15487 let mut cx = EditorLspTestContext::new(
15488 language,
15489 lsp::ServerCapabilities {
15490 completion_provider: Some(lsp::CompletionOptions {
15491 ..lsp::CompletionOptions::default()
15492 }),
15493 ..lsp::ServerCapabilities::default()
15494 },
15495 cx,
15496 )
15497 .await;
15498
15499 cx.set_state(
15500 "#ifndef BAR_H
15501#define BAR_H
15502
15503#include <stdbool.h>
15504
15505int fn_branch(bool do_branch1, bool do_branch2);
15506
15507#endif // BAR_H
15508ˇ",
15509 );
15510 cx.executor().run_until_parked();
15511 cx.update_editor(|editor, window, cx| {
15512 editor.handle_input("#", window, cx);
15513 });
15514 cx.executor().run_until_parked();
15515 cx.update_editor(|editor, window, cx| {
15516 editor.handle_input("i", window, cx);
15517 });
15518 cx.executor().run_until_parked();
15519 cx.update_editor(|editor, window, cx| {
15520 editor.handle_input("n", window, cx);
15521 });
15522 cx.executor().run_until_parked();
15523 cx.assert_editor_state(
15524 "#ifndef BAR_H
15525#define BAR_H
15526
15527#include <stdbool.h>
15528
15529int fn_branch(bool do_branch1, bool do_branch2);
15530
15531#endif // BAR_H
15532#inˇ",
15533 );
15534
15535 cx.lsp
15536 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15537 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15538 is_incomplete: false,
15539 item_defaults: None,
15540 items: vec![lsp::CompletionItem {
15541 kind: Some(lsp::CompletionItemKind::SNIPPET),
15542 label_details: Some(lsp::CompletionItemLabelDetails {
15543 detail: Some("header".to_string()),
15544 description: None,
15545 }),
15546 label: " include".to_string(),
15547 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15548 range: lsp::Range {
15549 start: lsp::Position {
15550 line: 8,
15551 character: 1,
15552 },
15553 end: lsp::Position {
15554 line: 8,
15555 character: 1,
15556 },
15557 },
15558 new_text: "include \"$0\"".to_string(),
15559 })),
15560 sort_text: Some("40b67681include".to_string()),
15561 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15562 filter_text: Some("include".to_string()),
15563 insert_text: Some("include \"$0\"".to_string()),
15564 ..lsp::CompletionItem::default()
15565 }],
15566 })))
15567 });
15568 cx.update_editor(|editor, window, cx| {
15569 editor.show_completions(&ShowCompletions, window, cx);
15570 });
15571 cx.executor().run_until_parked();
15572 cx.update_editor(|editor, window, cx| {
15573 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15574 });
15575 cx.executor().run_until_parked();
15576 cx.assert_editor_state(
15577 "#ifndef BAR_H
15578#define BAR_H
15579
15580#include <stdbool.h>
15581
15582int fn_branch(bool do_branch1, bool do_branch2);
15583
15584#endif // BAR_H
15585#include \"ˇ\"",
15586 );
15587
15588 cx.lsp
15589 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15590 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15591 is_incomplete: true,
15592 item_defaults: None,
15593 items: vec![lsp::CompletionItem {
15594 kind: Some(lsp::CompletionItemKind::FILE),
15595 label: "AGL/".to_string(),
15596 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15597 range: lsp::Range {
15598 start: lsp::Position {
15599 line: 8,
15600 character: 10,
15601 },
15602 end: lsp::Position {
15603 line: 8,
15604 character: 11,
15605 },
15606 },
15607 new_text: "AGL/".to_string(),
15608 })),
15609 sort_text: Some("40b67681AGL/".to_string()),
15610 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15611 filter_text: Some("AGL/".to_string()),
15612 insert_text: Some("AGL/".to_string()),
15613 ..lsp::CompletionItem::default()
15614 }],
15615 })))
15616 });
15617 cx.update_editor(|editor, window, cx| {
15618 editor.show_completions(&ShowCompletions, window, cx);
15619 });
15620 cx.executor().run_until_parked();
15621 cx.update_editor(|editor, window, cx| {
15622 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15623 });
15624 cx.executor().run_until_parked();
15625 cx.assert_editor_state(
15626 r##"#ifndef BAR_H
15627#define BAR_H
15628
15629#include <stdbool.h>
15630
15631int fn_branch(bool do_branch1, bool do_branch2);
15632
15633#endif // BAR_H
15634#include "AGL/ˇ"##,
15635 );
15636
15637 cx.update_editor(|editor, window, cx| {
15638 editor.handle_input("\"", window, cx);
15639 });
15640 cx.executor().run_until_parked();
15641 cx.assert_editor_state(
15642 r##"#ifndef BAR_H
15643#define BAR_H
15644
15645#include <stdbool.h>
15646
15647int fn_branch(bool do_branch1, bool do_branch2);
15648
15649#endif // BAR_H
15650#include "AGL/"ˇ"##,
15651 );
15652}
15653
15654#[gpui::test]
15655async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15656 init_test(cx, |_| {});
15657
15658 let mut cx = EditorLspTestContext::new_rust(
15659 lsp::ServerCapabilities {
15660 completion_provider: Some(lsp::CompletionOptions {
15661 trigger_characters: Some(vec![".".to_string()]),
15662 resolve_provider: Some(true),
15663 ..Default::default()
15664 }),
15665 ..Default::default()
15666 },
15667 cx,
15668 )
15669 .await;
15670
15671 cx.set_state("fn main() { let a = 2ˇ; }");
15672 cx.simulate_keystroke(".");
15673 let completion_item = lsp::CompletionItem {
15674 label: "Some".into(),
15675 kind: Some(lsp::CompletionItemKind::SNIPPET),
15676 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15677 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15678 kind: lsp::MarkupKind::Markdown,
15679 value: "```rust\nSome(2)\n```".to_string(),
15680 })),
15681 deprecated: Some(false),
15682 sort_text: Some("Some".to_string()),
15683 filter_text: Some("Some".to_string()),
15684 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15685 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15686 range: lsp::Range {
15687 start: lsp::Position {
15688 line: 0,
15689 character: 22,
15690 },
15691 end: lsp::Position {
15692 line: 0,
15693 character: 22,
15694 },
15695 },
15696 new_text: "Some(2)".to_string(),
15697 })),
15698 additional_text_edits: Some(vec![lsp::TextEdit {
15699 range: lsp::Range {
15700 start: lsp::Position {
15701 line: 0,
15702 character: 20,
15703 },
15704 end: lsp::Position {
15705 line: 0,
15706 character: 22,
15707 },
15708 },
15709 new_text: "".to_string(),
15710 }]),
15711 ..Default::default()
15712 };
15713
15714 let closure_completion_item = completion_item.clone();
15715 let counter = Arc::new(AtomicUsize::new(0));
15716 let counter_clone = counter.clone();
15717 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15718 let task_completion_item = closure_completion_item.clone();
15719 counter_clone.fetch_add(1, atomic::Ordering::Release);
15720 async move {
15721 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15722 is_incomplete: true,
15723 item_defaults: None,
15724 items: vec![task_completion_item],
15725 })))
15726 }
15727 });
15728
15729 cx.condition(|editor, _| editor.context_menu_visible())
15730 .await;
15731 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15732 assert!(request.next().await.is_some());
15733 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15734
15735 cx.simulate_keystrokes("S o m");
15736 cx.condition(|editor, _| editor.context_menu_visible())
15737 .await;
15738 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15739 assert!(request.next().await.is_some());
15740 assert!(request.next().await.is_some());
15741 assert!(request.next().await.is_some());
15742 request.close();
15743 assert!(request.next().await.is_none());
15744 assert_eq!(
15745 counter.load(atomic::Ordering::Acquire),
15746 4,
15747 "With the completions menu open, only one LSP request should happen per input"
15748 );
15749}
15750
15751#[gpui::test]
15752async fn test_toggle_comment(cx: &mut TestAppContext) {
15753 init_test(cx, |_| {});
15754 let mut cx = EditorTestContext::new(cx).await;
15755 let language = Arc::new(Language::new(
15756 LanguageConfig {
15757 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15758 ..Default::default()
15759 },
15760 Some(tree_sitter_rust::LANGUAGE.into()),
15761 ));
15762 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15763
15764 // If multiple selections intersect a line, the line is only toggled once.
15765 cx.set_state(indoc! {"
15766 fn a() {
15767 «//b();
15768 ˇ»// «c();
15769 //ˇ» d();
15770 }
15771 "});
15772
15773 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15774
15775 cx.assert_editor_state(indoc! {"
15776 fn a() {
15777 «b();
15778 c();
15779 ˇ» d();
15780 }
15781 "});
15782
15783 // The comment prefix is inserted at the same column for every line in a
15784 // selection.
15785 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15786
15787 cx.assert_editor_state(indoc! {"
15788 fn a() {
15789 // «b();
15790 // c();
15791 ˇ»// d();
15792 }
15793 "});
15794
15795 // If a selection ends at the beginning of a line, that line is not toggled.
15796 cx.set_selections_state(indoc! {"
15797 fn a() {
15798 // b();
15799 «// c();
15800 ˇ» // d();
15801 }
15802 "});
15803
15804 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15805
15806 cx.assert_editor_state(indoc! {"
15807 fn a() {
15808 // b();
15809 «c();
15810 ˇ» // d();
15811 }
15812 "});
15813
15814 // If a selection span a single line and is empty, the line is toggled.
15815 cx.set_state(indoc! {"
15816 fn a() {
15817 a();
15818 b();
15819 ˇ
15820 }
15821 "});
15822
15823 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15824
15825 cx.assert_editor_state(indoc! {"
15826 fn a() {
15827 a();
15828 b();
15829 //•ˇ
15830 }
15831 "});
15832
15833 // If a selection span multiple lines, empty lines are not toggled.
15834 cx.set_state(indoc! {"
15835 fn a() {
15836 «a();
15837
15838 c();ˇ»
15839 }
15840 "});
15841
15842 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15843
15844 cx.assert_editor_state(indoc! {"
15845 fn a() {
15846 // «a();
15847
15848 // c();ˇ»
15849 }
15850 "});
15851
15852 // If a selection includes multiple comment prefixes, all lines are uncommented.
15853 cx.set_state(indoc! {"
15854 fn a() {
15855 «// a();
15856 /// b();
15857 //! c();ˇ»
15858 }
15859 "});
15860
15861 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15862
15863 cx.assert_editor_state(indoc! {"
15864 fn a() {
15865 «a();
15866 b();
15867 c();ˇ»
15868 }
15869 "});
15870}
15871
15872#[gpui::test]
15873async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15874 init_test(cx, |_| {});
15875 let mut cx = EditorTestContext::new(cx).await;
15876 let language = Arc::new(Language::new(
15877 LanguageConfig {
15878 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15879 ..Default::default()
15880 },
15881 Some(tree_sitter_rust::LANGUAGE.into()),
15882 ));
15883 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15884
15885 let toggle_comments = &ToggleComments {
15886 advance_downwards: false,
15887 ignore_indent: true,
15888 };
15889
15890 // If multiple selections intersect a line, the line is only toggled once.
15891 cx.set_state(indoc! {"
15892 fn a() {
15893 // «b();
15894 // c();
15895 // ˇ» d();
15896 }
15897 "});
15898
15899 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15900
15901 cx.assert_editor_state(indoc! {"
15902 fn a() {
15903 «b();
15904 c();
15905 ˇ» d();
15906 }
15907 "});
15908
15909 // The comment prefix is inserted at the beginning of each line
15910 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15911
15912 cx.assert_editor_state(indoc! {"
15913 fn a() {
15914 // «b();
15915 // c();
15916 // ˇ» d();
15917 }
15918 "});
15919
15920 // If a selection ends at the beginning of a line, that line is not toggled.
15921 cx.set_selections_state(indoc! {"
15922 fn a() {
15923 // b();
15924 // «c();
15925 ˇ»// d();
15926 }
15927 "});
15928
15929 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15930
15931 cx.assert_editor_state(indoc! {"
15932 fn a() {
15933 // b();
15934 «c();
15935 ˇ»// d();
15936 }
15937 "});
15938
15939 // If a selection span a single line and is empty, the line is toggled.
15940 cx.set_state(indoc! {"
15941 fn a() {
15942 a();
15943 b();
15944 ˇ
15945 }
15946 "});
15947
15948 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15949
15950 cx.assert_editor_state(indoc! {"
15951 fn a() {
15952 a();
15953 b();
15954 //ˇ
15955 }
15956 "});
15957
15958 // If a selection span multiple lines, empty lines are not toggled.
15959 cx.set_state(indoc! {"
15960 fn a() {
15961 «a();
15962
15963 c();ˇ»
15964 }
15965 "});
15966
15967 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15968
15969 cx.assert_editor_state(indoc! {"
15970 fn a() {
15971 // «a();
15972
15973 // c();ˇ»
15974 }
15975 "});
15976
15977 // If a selection includes multiple comment prefixes, all lines are uncommented.
15978 cx.set_state(indoc! {"
15979 fn a() {
15980 // «a();
15981 /// b();
15982 //! c();ˇ»
15983 }
15984 "});
15985
15986 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15987
15988 cx.assert_editor_state(indoc! {"
15989 fn a() {
15990 «a();
15991 b();
15992 c();ˇ»
15993 }
15994 "});
15995}
15996
15997#[gpui::test]
15998async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15999 init_test(cx, |_| {});
16000
16001 let language = Arc::new(Language::new(
16002 LanguageConfig {
16003 line_comments: vec!["// ".into()],
16004 ..Default::default()
16005 },
16006 Some(tree_sitter_rust::LANGUAGE.into()),
16007 ));
16008
16009 let mut cx = EditorTestContext::new(cx).await;
16010
16011 cx.language_registry().add(language.clone());
16012 cx.update_buffer(|buffer, cx| {
16013 buffer.set_language(Some(language), cx);
16014 });
16015
16016 let toggle_comments = &ToggleComments {
16017 advance_downwards: true,
16018 ignore_indent: false,
16019 };
16020
16021 // Single cursor on one line -> advance
16022 // Cursor moves horizontally 3 characters as well on non-blank line
16023 cx.set_state(indoc!(
16024 "fn a() {
16025 ˇdog();
16026 cat();
16027 }"
16028 ));
16029 cx.update_editor(|editor, window, cx| {
16030 editor.toggle_comments(toggle_comments, window, cx);
16031 });
16032 cx.assert_editor_state(indoc!(
16033 "fn a() {
16034 // dog();
16035 catˇ();
16036 }"
16037 ));
16038
16039 // Single selection on one line -> don't advance
16040 cx.set_state(indoc!(
16041 "fn a() {
16042 «dog()ˇ»;
16043 cat();
16044 }"
16045 ));
16046 cx.update_editor(|editor, window, cx| {
16047 editor.toggle_comments(toggle_comments, window, cx);
16048 });
16049 cx.assert_editor_state(indoc!(
16050 "fn a() {
16051 // «dog()ˇ»;
16052 cat();
16053 }"
16054 ));
16055
16056 // Multiple cursors on one line -> advance
16057 cx.set_state(indoc!(
16058 "fn a() {
16059 ˇdˇog();
16060 cat();
16061 }"
16062 ));
16063 cx.update_editor(|editor, window, cx| {
16064 editor.toggle_comments(toggle_comments, window, cx);
16065 });
16066 cx.assert_editor_state(indoc!(
16067 "fn a() {
16068 // dog();
16069 catˇ(ˇ);
16070 }"
16071 ));
16072
16073 // Multiple cursors on one line, with selection -> don't advance
16074 cx.set_state(indoc!(
16075 "fn a() {
16076 ˇdˇog«()ˇ»;
16077 cat();
16078 }"
16079 ));
16080 cx.update_editor(|editor, window, cx| {
16081 editor.toggle_comments(toggle_comments, window, cx);
16082 });
16083 cx.assert_editor_state(indoc!(
16084 "fn a() {
16085 // ˇdˇog«()ˇ»;
16086 cat();
16087 }"
16088 ));
16089
16090 // Single cursor on one line -> advance
16091 // Cursor moves to column 0 on blank line
16092 cx.set_state(indoc!(
16093 "fn a() {
16094 ˇdog();
16095
16096 cat();
16097 }"
16098 ));
16099 cx.update_editor(|editor, window, cx| {
16100 editor.toggle_comments(toggle_comments, window, cx);
16101 });
16102 cx.assert_editor_state(indoc!(
16103 "fn a() {
16104 // dog();
16105 ˇ
16106 cat();
16107 }"
16108 ));
16109
16110 // Single cursor on one line -> advance
16111 // Cursor starts and ends at column 0
16112 cx.set_state(indoc!(
16113 "fn a() {
16114 ˇ dog();
16115 cat();
16116 }"
16117 ));
16118 cx.update_editor(|editor, window, cx| {
16119 editor.toggle_comments(toggle_comments, window, cx);
16120 });
16121 cx.assert_editor_state(indoc!(
16122 "fn a() {
16123 // dog();
16124 ˇ cat();
16125 }"
16126 ));
16127}
16128
16129#[gpui::test]
16130async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16131 init_test(cx, |_| {});
16132
16133 let mut cx = EditorTestContext::new(cx).await;
16134
16135 let html_language = Arc::new(
16136 Language::new(
16137 LanguageConfig {
16138 name: "HTML".into(),
16139 block_comment: Some(BlockCommentConfig {
16140 start: "<!-- ".into(),
16141 prefix: "".into(),
16142 end: " -->".into(),
16143 tab_size: 0,
16144 }),
16145 ..Default::default()
16146 },
16147 Some(tree_sitter_html::LANGUAGE.into()),
16148 )
16149 .with_injection_query(
16150 r#"
16151 (script_element
16152 (raw_text) @injection.content
16153 (#set! injection.language "javascript"))
16154 "#,
16155 )
16156 .unwrap(),
16157 );
16158
16159 let javascript_language = Arc::new(Language::new(
16160 LanguageConfig {
16161 name: "JavaScript".into(),
16162 line_comments: vec!["// ".into()],
16163 ..Default::default()
16164 },
16165 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16166 ));
16167
16168 cx.language_registry().add(html_language.clone());
16169 cx.language_registry().add(javascript_language);
16170 cx.update_buffer(|buffer, cx| {
16171 buffer.set_language(Some(html_language), cx);
16172 });
16173
16174 // Toggle comments for empty selections
16175 cx.set_state(
16176 &r#"
16177 <p>A</p>ˇ
16178 <p>B</p>ˇ
16179 <p>C</p>ˇ
16180 "#
16181 .unindent(),
16182 );
16183 cx.update_editor(|editor, window, cx| {
16184 editor.toggle_comments(&ToggleComments::default(), window, cx)
16185 });
16186 cx.assert_editor_state(
16187 &r#"
16188 <!-- <p>A</p>ˇ -->
16189 <!-- <p>B</p>ˇ -->
16190 <!-- <p>C</p>ˇ -->
16191 "#
16192 .unindent(),
16193 );
16194 cx.update_editor(|editor, window, cx| {
16195 editor.toggle_comments(&ToggleComments::default(), window, cx)
16196 });
16197 cx.assert_editor_state(
16198 &r#"
16199 <p>A</p>ˇ
16200 <p>B</p>ˇ
16201 <p>C</p>ˇ
16202 "#
16203 .unindent(),
16204 );
16205
16206 // Toggle comments for mixture of empty and non-empty selections, where
16207 // multiple selections occupy a given line.
16208 cx.set_state(
16209 &r#"
16210 <p>A«</p>
16211 <p>ˇ»B</p>ˇ
16212 <p>C«</p>
16213 <p>ˇ»D</p>ˇ
16214 "#
16215 .unindent(),
16216 );
16217
16218 cx.update_editor(|editor, window, cx| {
16219 editor.toggle_comments(&ToggleComments::default(), window, cx)
16220 });
16221 cx.assert_editor_state(
16222 &r#"
16223 <!-- <p>A«</p>
16224 <p>ˇ»B</p>ˇ -->
16225 <!-- <p>C«</p>
16226 <p>ˇ»D</p>ˇ -->
16227 "#
16228 .unindent(),
16229 );
16230 cx.update_editor(|editor, window, cx| {
16231 editor.toggle_comments(&ToggleComments::default(), window, cx)
16232 });
16233 cx.assert_editor_state(
16234 &r#"
16235 <p>A«</p>
16236 <p>ˇ»B</p>ˇ
16237 <p>C«</p>
16238 <p>ˇ»D</p>ˇ
16239 "#
16240 .unindent(),
16241 );
16242
16243 // Toggle comments when different languages are active for different
16244 // selections.
16245 cx.set_state(
16246 &r#"
16247 ˇ<script>
16248 ˇvar x = new Y();
16249 ˇ</script>
16250 "#
16251 .unindent(),
16252 );
16253 cx.executor().run_until_parked();
16254 cx.update_editor(|editor, window, cx| {
16255 editor.toggle_comments(&ToggleComments::default(), window, cx)
16256 });
16257 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16258 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16259 cx.assert_editor_state(
16260 &r#"
16261 <!-- ˇ<script> -->
16262 // ˇvar x = new Y();
16263 <!-- ˇ</script> -->
16264 "#
16265 .unindent(),
16266 );
16267}
16268
16269#[gpui::test]
16270fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16271 init_test(cx, |_| {});
16272
16273 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16274 let multibuffer = cx.new(|cx| {
16275 let mut multibuffer = MultiBuffer::new(ReadWrite);
16276 multibuffer.push_excerpts(
16277 buffer.clone(),
16278 [
16279 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16280 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16281 ],
16282 cx,
16283 );
16284 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16285 multibuffer
16286 });
16287
16288 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16289 editor.update_in(cx, |editor, window, cx| {
16290 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16291 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16292 s.select_ranges([
16293 Point::new(0, 0)..Point::new(0, 0),
16294 Point::new(1, 0)..Point::new(1, 0),
16295 ])
16296 });
16297
16298 editor.handle_input("X", window, cx);
16299 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16300 assert_eq!(
16301 editor.selections.ranges(&editor.display_snapshot(cx)),
16302 [
16303 Point::new(0, 1)..Point::new(0, 1),
16304 Point::new(1, 1)..Point::new(1, 1),
16305 ]
16306 );
16307
16308 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16310 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16311 });
16312 editor.backspace(&Default::default(), window, cx);
16313 assert_eq!(editor.text(cx), "Xa\nbbb");
16314 assert_eq!(
16315 editor.selections.ranges(&editor.display_snapshot(cx)),
16316 [Point::new(1, 0)..Point::new(1, 0)]
16317 );
16318
16319 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16320 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16321 });
16322 editor.backspace(&Default::default(), window, cx);
16323 assert_eq!(editor.text(cx), "X\nbb");
16324 assert_eq!(
16325 editor.selections.ranges(&editor.display_snapshot(cx)),
16326 [Point::new(0, 1)..Point::new(0, 1)]
16327 );
16328 });
16329}
16330
16331#[gpui::test]
16332fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16333 init_test(cx, |_| {});
16334
16335 let markers = vec![('[', ']').into(), ('(', ')').into()];
16336 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16337 indoc! {"
16338 [aaaa
16339 (bbbb]
16340 cccc)",
16341 },
16342 markers.clone(),
16343 );
16344 let excerpt_ranges = markers.into_iter().map(|marker| {
16345 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16346 ExcerptRange::new(context)
16347 });
16348 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16349 let multibuffer = cx.new(|cx| {
16350 let mut multibuffer = MultiBuffer::new(ReadWrite);
16351 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16352 multibuffer
16353 });
16354
16355 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16356 editor.update_in(cx, |editor, window, cx| {
16357 let (expected_text, selection_ranges) = marked_text_ranges(
16358 indoc! {"
16359 aaaa
16360 bˇbbb
16361 bˇbbˇb
16362 cccc"
16363 },
16364 true,
16365 );
16366 assert_eq!(editor.text(cx), expected_text);
16367 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16368 s.select_ranges(selection_ranges)
16369 });
16370
16371 editor.handle_input("X", window, cx);
16372
16373 let (expected_text, expected_selections) = marked_text_ranges(
16374 indoc! {"
16375 aaaa
16376 bXˇbbXb
16377 bXˇbbXˇb
16378 cccc"
16379 },
16380 false,
16381 );
16382 assert_eq!(editor.text(cx), expected_text);
16383 assert_eq!(
16384 editor.selections.ranges(&editor.display_snapshot(cx)),
16385 expected_selections
16386 );
16387
16388 editor.newline(&Newline, window, cx);
16389 let (expected_text, expected_selections) = marked_text_ranges(
16390 indoc! {"
16391 aaaa
16392 bX
16393 ˇbbX
16394 b
16395 bX
16396 ˇbbX
16397 ˇb
16398 cccc"
16399 },
16400 false,
16401 );
16402 assert_eq!(editor.text(cx), expected_text);
16403 assert_eq!(
16404 editor.selections.ranges(&editor.display_snapshot(cx)),
16405 expected_selections
16406 );
16407 });
16408}
16409
16410#[gpui::test]
16411fn test_refresh_selections(cx: &mut TestAppContext) {
16412 init_test(cx, |_| {});
16413
16414 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16415 let mut excerpt1_id = None;
16416 let multibuffer = cx.new(|cx| {
16417 let mut multibuffer = MultiBuffer::new(ReadWrite);
16418 excerpt1_id = multibuffer
16419 .push_excerpts(
16420 buffer.clone(),
16421 [
16422 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16423 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16424 ],
16425 cx,
16426 )
16427 .into_iter()
16428 .next();
16429 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16430 multibuffer
16431 });
16432
16433 let editor = cx.add_window(|window, cx| {
16434 let mut editor = build_editor(multibuffer.clone(), window, cx);
16435 let snapshot = editor.snapshot(window, cx);
16436 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16437 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16438 });
16439 editor.begin_selection(
16440 Point::new(2, 1).to_display_point(&snapshot),
16441 true,
16442 1,
16443 window,
16444 cx,
16445 );
16446 assert_eq!(
16447 editor.selections.ranges(&editor.display_snapshot(cx)),
16448 [
16449 Point::new(1, 3)..Point::new(1, 3),
16450 Point::new(2, 1)..Point::new(2, 1),
16451 ]
16452 );
16453 editor
16454 });
16455
16456 // Refreshing selections is a no-op when excerpts haven't changed.
16457 _ = editor.update(cx, |editor, window, cx| {
16458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16459 assert_eq!(
16460 editor.selections.ranges(&editor.display_snapshot(cx)),
16461 [
16462 Point::new(1, 3)..Point::new(1, 3),
16463 Point::new(2, 1)..Point::new(2, 1),
16464 ]
16465 );
16466 });
16467
16468 multibuffer.update(cx, |multibuffer, cx| {
16469 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16470 });
16471 _ = editor.update(cx, |editor, window, cx| {
16472 // Removing an excerpt causes the first selection to become degenerate.
16473 assert_eq!(
16474 editor.selections.ranges(&editor.display_snapshot(cx)),
16475 [
16476 Point::new(0, 0)..Point::new(0, 0),
16477 Point::new(0, 1)..Point::new(0, 1)
16478 ]
16479 );
16480
16481 // Refreshing selections will relocate the first selection to the original buffer
16482 // location.
16483 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16484 assert_eq!(
16485 editor.selections.ranges(&editor.display_snapshot(cx)),
16486 [
16487 Point::new(0, 1)..Point::new(0, 1),
16488 Point::new(0, 3)..Point::new(0, 3)
16489 ]
16490 );
16491 assert!(editor.selections.pending_anchor().is_some());
16492 });
16493}
16494
16495#[gpui::test]
16496fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16497 init_test(cx, |_| {});
16498
16499 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16500 let mut excerpt1_id = None;
16501 let multibuffer = cx.new(|cx| {
16502 let mut multibuffer = MultiBuffer::new(ReadWrite);
16503 excerpt1_id = multibuffer
16504 .push_excerpts(
16505 buffer.clone(),
16506 [
16507 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16508 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16509 ],
16510 cx,
16511 )
16512 .into_iter()
16513 .next();
16514 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16515 multibuffer
16516 });
16517
16518 let editor = cx.add_window(|window, cx| {
16519 let mut editor = build_editor(multibuffer.clone(), window, cx);
16520 let snapshot = editor.snapshot(window, cx);
16521 editor.begin_selection(
16522 Point::new(1, 3).to_display_point(&snapshot),
16523 false,
16524 1,
16525 window,
16526 cx,
16527 );
16528 assert_eq!(
16529 editor.selections.ranges(&editor.display_snapshot(cx)),
16530 [Point::new(1, 3)..Point::new(1, 3)]
16531 );
16532 editor
16533 });
16534
16535 multibuffer.update(cx, |multibuffer, cx| {
16536 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16537 });
16538 _ = editor.update(cx, |editor, window, cx| {
16539 assert_eq!(
16540 editor.selections.ranges(&editor.display_snapshot(cx)),
16541 [Point::new(0, 0)..Point::new(0, 0)]
16542 );
16543
16544 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16545 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16546 assert_eq!(
16547 editor.selections.ranges(&editor.display_snapshot(cx)),
16548 [Point::new(0, 3)..Point::new(0, 3)]
16549 );
16550 assert!(editor.selections.pending_anchor().is_some());
16551 });
16552}
16553
16554#[gpui::test]
16555async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16556 init_test(cx, |_| {});
16557
16558 let language = Arc::new(
16559 Language::new(
16560 LanguageConfig {
16561 brackets: BracketPairConfig {
16562 pairs: vec![
16563 BracketPair {
16564 start: "{".to_string(),
16565 end: "}".to_string(),
16566 close: true,
16567 surround: true,
16568 newline: true,
16569 },
16570 BracketPair {
16571 start: "/* ".to_string(),
16572 end: " */".to_string(),
16573 close: true,
16574 surround: true,
16575 newline: true,
16576 },
16577 ],
16578 ..Default::default()
16579 },
16580 ..Default::default()
16581 },
16582 Some(tree_sitter_rust::LANGUAGE.into()),
16583 )
16584 .with_indents_query("")
16585 .unwrap(),
16586 );
16587
16588 let text = concat!(
16589 "{ }\n", //
16590 " x\n", //
16591 " /* */\n", //
16592 "x\n", //
16593 "{{} }\n", //
16594 );
16595
16596 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16597 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16598 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16599 editor
16600 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16601 .await;
16602
16603 editor.update_in(cx, |editor, window, cx| {
16604 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16605 s.select_display_ranges([
16606 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16607 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16608 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16609 ])
16610 });
16611 editor.newline(&Newline, window, cx);
16612
16613 assert_eq!(
16614 editor.buffer().read(cx).read(cx).text(),
16615 concat!(
16616 "{ \n", // Suppress rustfmt
16617 "\n", //
16618 "}\n", //
16619 " x\n", //
16620 " /* \n", //
16621 " \n", //
16622 " */\n", //
16623 "x\n", //
16624 "{{} \n", //
16625 "}\n", //
16626 )
16627 );
16628 });
16629}
16630
16631#[gpui::test]
16632fn test_highlighted_ranges(cx: &mut TestAppContext) {
16633 init_test(cx, |_| {});
16634
16635 let editor = cx.add_window(|window, cx| {
16636 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16637 build_editor(buffer, window, cx)
16638 });
16639
16640 _ = editor.update(cx, |editor, window, cx| {
16641 struct Type1;
16642 struct Type2;
16643
16644 let buffer = editor.buffer.read(cx).snapshot(cx);
16645
16646 let anchor_range =
16647 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16648
16649 editor.highlight_background::<Type1>(
16650 &[
16651 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16652 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16653 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16654 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16655 ],
16656 |_| Hsla::red(),
16657 cx,
16658 );
16659 editor.highlight_background::<Type2>(
16660 &[
16661 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16662 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16663 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16664 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16665 ],
16666 |_| Hsla::green(),
16667 cx,
16668 );
16669
16670 let snapshot = editor.snapshot(window, cx);
16671 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16672 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16673 &snapshot,
16674 cx.theme(),
16675 );
16676 assert_eq!(
16677 highlighted_ranges,
16678 &[
16679 (
16680 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16681 Hsla::green(),
16682 ),
16683 (
16684 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16685 Hsla::red(),
16686 ),
16687 (
16688 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16689 Hsla::green(),
16690 ),
16691 (
16692 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16693 Hsla::red(),
16694 ),
16695 ]
16696 );
16697 assert_eq!(
16698 editor.sorted_background_highlights_in_range(
16699 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16700 &snapshot,
16701 cx.theme(),
16702 ),
16703 &[(
16704 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16705 Hsla::red(),
16706 )]
16707 );
16708 });
16709}
16710
16711#[gpui::test]
16712async fn test_following(cx: &mut TestAppContext) {
16713 init_test(cx, |_| {});
16714
16715 let fs = FakeFs::new(cx.executor());
16716 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16717
16718 let buffer = project.update(cx, |project, cx| {
16719 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16720 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16721 });
16722 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16723 let follower = cx.update(|cx| {
16724 cx.open_window(
16725 WindowOptions {
16726 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16727 gpui::Point::new(px(0.), px(0.)),
16728 gpui::Point::new(px(10.), px(80.)),
16729 ))),
16730 ..Default::default()
16731 },
16732 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16733 )
16734 .unwrap()
16735 });
16736
16737 let is_still_following = Rc::new(RefCell::new(true));
16738 let follower_edit_event_count = Rc::new(RefCell::new(0));
16739 let pending_update = Rc::new(RefCell::new(None));
16740 let leader_entity = leader.root(cx).unwrap();
16741 let follower_entity = follower.root(cx).unwrap();
16742 _ = follower.update(cx, {
16743 let update = pending_update.clone();
16744 let is_still_following = is_still_following.clone();
16745 let follower_edit_event_count = follower_edit_event_count.clone();
16746 |_, window, cx| {
16747 cx.subscribe_in(
16748 &leader_entity,
16749 window,
16750 move |_, leader, event, window, cx| {
16751 leader.read(cx).add_event_to_update_proto(
16752 event,
16753 &mut update.borrow_mut(),
16754 window,
16755 cx,
16756 );
16757 },
16758 )
16759 .detach();
16760
16761 cx.subscribe_in(
16762 &follower_entity,
16763 window,
16764 move |_, _, event: &EditorEvent, _window, _cx| {
16765 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16766 *is_still_following.borrow_mut() = false;
16767 }
16768
16769 if let EditorEvent::BufferEdited = event {
16770 *follower_edit_event_count.borrow_mut() += 1;
16771 }
16772 },
16773 )
16774 .detach();
16775 }
16776 });
16777
16778 // Update the selections only
16779 _ = leader.update(cx, |leader, window, cx| {
16780 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16781 s.select_ranges([1..1])
16782 });
16783 });
16784 follower
16785 .update(cx, |follower, window, cx| {
16786 follower.apply_update_proto(
16787 &project,
16788 pending_update.borrow_mut().take().unwrap(),
16789 window,
16790 cx,
16791 )
16792 })
16793 .unwrap()
16794 .await
16795 .unwrap();
16796 _ = follower.update(cx, |follower, _, cx| {
16797 assert_eq!(
16798 follower.selections.ranges(&follower.display_snapshot(cx)),
16799 vec![1..1]
16800 );
16801 });
16802 assert!(*is_still_following.borrow());
16803 assert_eq!(*follower_edit_event_count.borrow(), 0);
16804
16805 // Update the scroll position only
16806 _ = leader.update(cx, |leader, window, cx| {
16807 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16808 });
16809 follower
16810 .update(cx, |follower, window, cx| {
16811 follower.apply_update_proto(
16812 &project,
16813 pending_update.borrow_mut().take().unwrap(),
16814 window,
16815 cx,
16816 )
16817 })
16818 .unwrap()
16819 .await
16820 .unwrap();
16821 assert_eq!(
16822 follower
16823 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16824 .unwrap(),
16825 gpui::Point::new(1.5, 3.5)
16826 );
16827 assert!(*is_still_following.borrow());
16828 assert_eq!(*follower_edit_event_count.borrow(), 0);
16829
16830 // Update the selections and scroll position. The follower's scroll position is updated
16831 // via autoscroll, not via the leader's exact scroll position.
16832 _ = leader.update(cx, |leader, window, cx| {
16833 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16834 s.select_ranges([0..0])
16835 });
16836 leader.request_autoscroll(Autoscroll::newest(), cx);
16837 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16838 });
16839 follower
16840 .update(cx, |follower, window, cx| {
16841 follower.apply_update_proto(
16842 &project,
16843 pending_update.borrow_mut().take().unwrap(),
16844 window,
16845 cx,
16846 )
16847 })
16848 .unwrap()
16849 .await
16850 .unwrap();
16851 _ = follower.update(cx, |follower, _, cx| {
16852 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16853 assert_eq!(
16854 follower.selections.ranges(&follower.display_snapshot(cx)),
16855 vec![0..0]
16856 );
16857 });
16858 assert!(*is_still_following.borrow());
16859
16860 // Creating a pending selection that precedes another selection
16861 _ = leader.update(cx, |leader, window, cx| {
16862 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16863 s.select_ranges([1..1])
16864 });
16865 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16866 });
16867 follower
16868 .update(cx, |follower, window, cx| {
16869 follower.apply_update_proto(
16870 &project,
16871 pending_update.borrow_mut().take().unwrap(),
16872 window,
16873 cx,
16874 )
16875 })
16876 .unwrap()
16877 .await
16878 .unwrap();
16879 _ = follower.update(cx, |follower, _, cx| {
16880 assert_eq!(
16881 follower.selections.ranges(&follower.display_snapshot(cx)),
16882 vec![0..0, 1..1]
16883 );
16884 });
16885 assert!(*is_still_following.borrow());
16886
16887 // Extend the pending selection so that it surrounds another selection
16888 _ = leader.update(cx, |leader, window, cx| {
16889 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16890 });
16891 follower
16892 .update(cx, |follower, window, cx| {
16893 follower.apply_update_proto(
16894 &project,
16895 pending_update.borrow_mut().take().unwrap(),
16896 window,
16897 cx,
16898 )
16899 })
16900 .unwrap()
16901 .await
16902 .unwrap();
16903 _ = follower.update(cx, |follower, _, cx| {
16904 assert_eq!(
16905 follower.selections.ranges(&follower.display_snapshot(cx)),
16906 vec![0..2]
16907 );
16908 });
16909
16910 // Scrolling locally breaks the follow
16911 _ = follower.update(cx, |follower, window, cx| {
16912 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16913 follower.set_scroll_anchor(
16914 ScrollAnchor {
16915 anchor: top_anchor,
16916 offset: gpui::Point::new(0.0, 0.5),
16917 },
16918 window,
16919 cx,
16920 );
16921 });
16922 assert!(!(*is_still_following.borrow()));
16923}
16924
16925#[gpui::test]
16926async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16927 init_test(cx, |_| {});
16928
16929 let fs = FakeFs::new(cx.executor());
16930 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16931 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16932 let pane = workspace
16933 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16934 .unwrap();
16935
16936 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16937
16938 let leader = pane.update_in(cx, |_, window, cx| {
16939 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16940 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16941 });
16942
16943 // Start following the editor when it has no excerpts.
16944 let mut state_message =
16945 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16946 let workspace_entity = workspace.root(cx).unwrap();
16947 let follower_1 = cx
16948 .update_window(*workspace.deref(), |_, window, cx| {
16949 Editor::from_state_proto(
16950 workspace_entity,
16951 ViewId {
16952 creator: CollaboratorId::PeerId(PeerId::default()),
16953 id: 0,
16954 },
16955 &mut state_message,
16956 window,
16957 cx,
16958 )
16959 })
16960 .unwrap()
16961 .unwrap()
16962 .await
16963 .unwrap();
16964
16965 let update_message = Rc::new(RefCell::new(None));
16966 follower_1.update_in(cx, {
16967 let update = update_message.clone();
16968 |_, window, cx| {
16969 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16970 leader.read(cx).add_event_to_update_proto(
16971 event,
16972 &mut update.borrow_mut(),
16973 window,
16974 cx,
16975 );
16976 })
16977 .detach();
16978 }
16979 });
16980
16981 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16982 (
16983 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16984 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16985 )
16986 });
16987
16988 // Insert some excerpts.
16989 leader.update(cx, |leader, cx| {
16990 leader.buffer.update(cx, |multibuffer, cx| {
16991 multibuffer.set_excerpts_for_path(
16992 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16993 buffer_1.clone(),
16994 vec![
16995 Point::row_range(0..3),
16996 Point::row_range(1..6),
16997 Point::row_range(12..15),
16998 ],
16999 0,
17000 cx,
17001 );
17002 multibuffer.set_excerpts_for_path(
17003 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17004 buffer_2.clone(),
17005 vec![Point::row_range(0..6), Point::row_range(8..12)],
17006 0,
17007 cx,
17008 );
17009 });
17010 });
17011
17012 // Apply the update of adding the excerpts.
17013 follower_1
17014 .update_in(cx, |follower, window, cx| {
17015 follower.apply_update_proto(
17016 &project,
17017 update_message.borrow().clone().unwrap(),
17018 window,
17019 cx,
17020 )
17021 })
17022 .await
17023 .unwrap();
17024 assert_eq!(
17025 follower_1.update(cx, |editor, cx| editor.text(cx)),
17026 leader.update(cx, |editor, cx| editor.text(cx))
17027 );
17028 update_message.borrow_mut().take();
17029
17030 // Start following separately after it already has excerpts.
17031 let mut state_message =
17032 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17033 let workspace_entity = workspace.root(cx).unwrap();
17034 let follower_2 = cx
17035 .update_window(*workspace.deref(), |_, window, cx| {
17036 Editor::from_state_proto(
17037 workspace_entity,
17038 ViewId {
17039 creator: CollaboratorId::PeerId(PeerId::default()),
17040 id: 0,
17041 },
17042 &mut state_message,
17043 window,
17044 cx,
17045 )
17046 })
17047 .unwrap()
17048 .unwrap()
17049 .await
17050 .unwrap();
17051 assert_eq!(
17052 follower_2.update(cx, |editor, cx| editor.text(cx)),
17053 leader.update(cx, |editor, cx| editor.text(cx))
17054 );
17055
17056 // Remove some excerpts.
17057 leader.update(cx, |leader, cx| {
17058 leader.buffer.update(cx, |multibuffer, cx| {
17059 let excerpt_ids = multibuffer.excerpt_ids();
17060 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17061 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17062 });
17063 });
17064
17065 // Apply the update of removing the excerpts.
17066 follower_1
17067 .update_in(cx, |follower, window, cx| {
17068 follower.apply_update_proto(
17069 &project,
17070 update_message.borrow().clone().unwrap(),
17071 window,
17072 cx,
17073 )
17074 })
17075 .await
17076 .unwrap();
17077 follower_2
17078 .update_in(cx, |follower, window, cx| {
17079 follower.apply_update_proto(
17080 &project,
17081 update_message.borrow().clone().unwrap(),
17082 window,
17083 cx,
17084 )
17085 })
17086 .await
17087 .unwrap();
17088 update_message.borrow_mut().take();
17089 assert_eq!(
17090 follower_1.update(cx, |editor, cx| editor.text(cx)),
17091 leader.update(cx, |editor, cx| editor.text(cx))
17092 );
17093}
17094
17095#[gpui::test]
17096async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17097 init_test(cx, |_| {});
17098
17099 let mut cx = EditorTestContext::new(cx).await;
17100 let lsp_store =
17101 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17102
17103 cx.set_state(indoc! {"
17104 ˇfn func(abc def: i32) -> u32 {
17105 }
17106 "});
17107
17108 cx.update(|_, cx| {
17109 lsp_store.update(cx, |lsp_store, cx| {
17110 lsp_store
17111 .update_diagnostics(
17112 LanguageServerId(0),
17113 lsp::PublishDiagnosticsParams {
17114 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17115 version: None,
17116 diagnostics: vec![
17117 lsp::Diagnostic {
17118 range: lsp::Range::new(
17119 lsp::Position::new(0, 11),
17120 lsp::Position::new(0, 12),
17121 ),
17122 severity: Some(lsp::DiagnosticSeverity::ERROR),
17123 ..Default::default()
17124 },
17125 lsp::Diagnostic {
17126 range: lsp::Range::new(
17127 lsp::Position::new(0, 12),
17128 lsp::Position::new(0, 15),
17129 ),
17130 severity: Some(lsp::DiagnosticSeverity::ERROR),
17131 ..Default::default()
17132 },
17133 lsp::Diagnostic {
17134 range: lsp::Range::new(
17135 lsp::Position::new(0, 25),
17136 lsp::Position::new(0, 28),
17137 ),
17138 severity: Some(lsp::DiagnosticSeverity::ERROR),
17139 ..Default::default()
17140 },
17141 ],
17142 },
17143 None,
17144 DiagnosticSourceKind::Pushed,
17145 &[],
17146 cx,
17147 )
17148 .unwrap()
17149 });
17150 });
17151
17152 executor.run_until_parked();
17153
17154 cx.update_editor(|editor, window, cx| {
17155 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17156 });
17157
17158 cx.assert_editor_state(indoc! {"
17159 fn func(abc def: i32) -> ˇu32 {
17160 }
17161 "});
17162
17163 cx.update_editor(|editor, window, cx| {
17164 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17165 });
17166
17167 cx.assert_editor_state(indoc! {"
17168 fn func(abc ˇdef: i32) -> u32 {
17169 }
17170 "});
17171
17172 cx.update_editor(|editor, window, cx| {
17173 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17174 });
17175
17176 cx.assert_editor_state(indoc! {"
17177 fn func(abcˇ def: i32) -> u32 {
17178 }
17179 "});
17180
17181 cx.update_editor(|editor, window, cx| {
17182 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17183 });
17184
17185 cx.assert_editor_state(indoc! {"
17186 fn func(abc def: i32) -> ˇu32 {
17187 }
17188 "});
17189}
17190
17191#[gpui::test]
17192async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17193 init_test(cx, |_| {});
17194
17195 let mut cx = EditorTestContext::new(cx).await;
17196
17197 let diff_base = r#"
17198 use some::mod;
17199
17200 const A: u32 = 42;
17201
17202 fn main() {
17203 println!("hello");
17204
17205 println!("world");
17206 }
17207 "#
17208 .unindent();
17209
17210 // Edits are modified, removed, modified, added
17211 cx.set_state(
17212 &r#"
17213 use some::modified;
17214
17215 ˇ
17216 fn main() {
17217 println!("hello there");
17218
17219 println!("around the");
17220 println!("world");
17221 }
17222 "#
17223 .unindent(),
17224 );
17225
17226 cx.set_head_text(&diff_base);
17227 executor.run_until_parked();
17228
17229 cx.update_editor(|editor, window, cx| {
17230 //Wrap around the bottom of the buffer
17231 for _ in 0..3 {
17232 editor.go_to_next_hunk(&GoToHunk, window, cx);
17233 }
17234 });
17235
17236 cx.assert_editor_state(
17237 &r#"
17238 ˇuse some::modified;
17239
17240
17241 fn main() {
17242 println!("hello there");
17243
17244 println!("around the");
17245 println!("world");
17246 }
17247 "#
17248 .unindent(),
17249 );
17250
17251 cx.update_editor(|editor, window, cx| {
17252 //Wrap around the top of the buffer
17253 for _ in 0..2 {
17254 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17255 }
17256 });
17257
17258 cx.assert_editor_state(
17259 &r#"
17260 use some::modified;
17261
17262
17263 fn main() {
17264 ˇ println!("hello there");
17265
17266 println!("around the");
17267 println!("world");
17268 }
17269 "#
17270 .unindent(),
17271 );
17272
17273 cx.update_editor(|editor, window, cx| {
17274 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17275 });
17276
17277 cx.assert_editor_state(
17278 &r#"
17279 use some::modified;
17280
17281 ˇ
17282 fn main() {
17283 println!("hello there");
17284
17285 println!("around the");
17286 println!("world");
17287 }
17288 "#
17289 .unindent(),
17290 );
17291
17292 cx.update_editor(|editor, window, cx| {
17293 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17294 });
17295
17296 cx.assert_editor_state(
17297 &r#"
17298 ˇuse some::modified;
17299
17300
17301 fn main() {
17302 println!("hello there");
17303
17304 println!("around the");
17305 println!("world");
17306 }
17307 "#
17308 .unindent(),
17309 );
17310
17311 cx.update_editor(|editor, window, cx| {
17312 for _ in 0..2 {
17313 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17314 }
17315 });
17316
17317 cx.assert_editor_state(
17318 &r#"
17319 use some::modified;
17320
17321
17322 fn main() {
17323 ˇ println!("hello there");
17324
17325 println!("around the");
17326 println!("world");
17327 }
17328 "#
17329 .unindent(),
17330 );
17331
17332 cx.update_editor(|editor, window, cx| {
17333 editor.fold(&Fold, window, cx);
17334 });
17335
17336 cx.update_editor(|editor, window, cx| {
17337 editor.go_to_next_hunk(&GoToHunk, window, cx);
17338 });
17339
17340 cx.assert_editor_state(
17341 &r#"
17342 ˇuse some::modified;
17343
17344
17345 fn main() {
17346 println!("hello there");
17347
17348 println!("around the");
17349 println!("world");
17350 }
17351 "#
17352 .unindent(),
17353 );
17354}
17355
17356#[test]
17357fn test_split_words() {
17358 fn split(text: &str) -> Vec<&str> {
17359 split_words(text).collect()
17360 }
17361
17362 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17363 assert_eq!(split("hello_world"), &["hello_", "world"]);
17364 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17365 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17366 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17367 assert_eq!(split("helloworld"), &["helloworld"]);
17368
17369 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17370}
17371
17372#[gpui::test]
17373async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17374 init_test(cx, |_| {});
17375
17376 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17377 let mut assert = |before, after| {
17378 let _state_context = cx.set_state(before);
17379 cx.run_until_parked();
17380 cx.update_editor(|editor, window, cx| {
17381 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17382 });
17383 cx.run_until_parked();
17384 cx.assert_editor_state(after);
17385 };
17386
17387 // Outside bracket jumps to outside of matching bracket
17388 assert("console.logˇ(var);", "console.log(var)ˇ;");
17389 assert("console.log(var)ˇ;", "console.logˇ(var);");
17390
17391 // Inside bracket jumps to inside of matching bracket
17392 assert("console.log(ˇvar);", "console.log(varˇ);");
17393 assert("console.log(varˇ);", "console.log(ˇvar);");
17394
17395 // When outside a bracket and inside, favor jumping to the inside bracket
17396 assert(
17397 "console.log('foo', [1, 2, 3]ˇ);",
17398 "console.log(ˇ'foo', [1, 2, 3]);",
17399 );
17400 assert(
17401 "console.log(ˇ'foo', [1, 2, 3]);",
17402 "console.log('foo', [1, 2, 3]ˇ);",
17403 );
17404
17405 // Bias forward if two options are equally likely
17406 assert(
17407 "let result = curried_fun()ˇ();",
17408 "let result = curried_fun()()ˇ;",
17409 );
17410
17411 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17412 assert(
17413 indoc! {"
17414 function test() {
17415 console.log('test')ˇ
17416 }"},
17417 indoc! {"
17418 function test() {
17419 console.logˇ('test')
17420 }"},
17421 );
17422}
17423
17424#[gpui::test]
17425async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17426 init_test(cx, |_| {});
17427
17428 let fs = FakeFs::new(cx.executor());
17429 fs.insert_tree(
17430 path!("/a"),
17431 json!({
17432 "main.rs": "fn main() { let a = 5; }",
17433 "other.rs": "// Test file",
17434 }),
17435 )
17436 .await;
17437 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17438
17439 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17440 language_registry.add(Arc::new(Language::new(
17441 LanguageConfig {
17442 name: "Rust".into(),
17443 matcher: LanguageMatcher {
17444 path_suffixes: vec!["rs".to_string()],
17445 ..Default::default()
17446 },
17447 brackets: BracketPairConfig {
17448 pairs: vec![BracketPair {
17449 start: "{".to_string(),
17450 end: "}".to_string(),
17451 close: true,
17452 surround: true,
17453 newline: true,
17454 }],
17455 disabled_scopes_by_bracket_ix: Vec::new(),
17456 },
17457 ..Default::default()
17458 },
17459 Some(tree_sitter_rust::LANGUAGE.into()),
17460 )));
17461 let mut fake_servers = language_registry.register_fake_lsp(
17462 "Rust",
17463 FakeLspAdapter {
17464 capabilities: lsp::ServerCapabilities {
17465 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17466 first_trigger_character: "{".to_string(),
17467 more_trigger_character: None,
17468 }),
17469 ..Default::default()
17470 },
17471 ..Default::default()
17472 },
17473 );
17474
17475 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17476
17477 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17478
17479 let worktree_id = workspace
17480 .update(cx, |workspace, _, cx| {
17481 workspace.project().update(cx, |project, cx| {
17482 project.worktrees(cx).next().unwrap().read(cx).id()
17483 })
17484 })
17485 .unwrap();
17486
17487 let buffer = project
17488 .update(cx, |project, cx| {
17489 project.open_local_buffer(path!("/a/main.rs"), cx)
17490 })
17491 .await
17492 .unwrap();
17493 let editor_handle = workspace
17494 .update(cx, |workspace, window, cx| {
17495 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17496 })
17497 .unwrap()
17498 .await
17499 .unwrap()
17500 .downcast::<Editor>()
17501 .unwrap();
17502
17503 cx.executor().start_waiting();
17504 let fake_server = fake_servers.next().await.unwrap();
17505
17506 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17507 |params, _| async move {
17508 assert_eq!(
17509 params.text_document_position.text_document.uri,
17510 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17511 );
17512 assert_eq!(
17513 params.text_document_position.position,
17514 lsp::Position::new(0, 21),
17515 );
17516
17517 Ok(Some(vec![lsp::TextEdit {
17518 new_text: "]".to_string(),
17519 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17520 }]))
17521 },
17522 );
17523
17524 editor_handle.update_in(cx, |editor, window, cx| {
17525 window.focus(&editor.focus_handle(cx));
17526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17527 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17528 });
17529 editor.handle_input("{", window, cx);
17530 });
17531
17532 cx.executor().run_until_parked();
17533
17534 buffer.update(cx, |buffer, _| {
17535 assert_eq!(
17536 buffer.text(),
17537 "fn main() { let a = {5}; }",
17538 "No extra braces from on type formatting should appear in the buffer"
17539 )
17540 });
17541}
17542
17543#[gpui::test(iterations = 20, seeds(31))]
17544async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17545 init_test(cx, |_| {});
17546
17547 let mut cx = EditorLspTestContext::new_rust(
17548 lsp::ServerCapabilities {
17549 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17550 first_trigger_character: ".".to_string(),
17551 more_trigger_character: None,
17552 }),
17553 ..Default::default()
17554 },
17555 cx,
17556 )
17557 .await;
17558
17559 cx.update_buffer(|buffer, _| {
17560 // This causes autoindent to be async.
17561 buffer.set_sync_parse_timeout(Duration::ZERO)
17562 });
17563
17564 cx.set_state("fn c() {\n d()ˇ\n}\n");
17565 cx.simulate_keystroke("\n");
17566 cx.run_until_parked();
17567
17568 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17569 let mut request =
17570 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17571 let buffer_cloned = buffer_cloned.clone();
17572 async move {
17573 buffer_cloned.update(&mut cx, |buffer, _| {
17574 assert_eq!(
17575 buffer.text(),
17576 "fn c() {\n d()\n .\n}\n",
17577 "OnTypeFormatting should triggered after autoindent applied"
17578 )
17579 })?;
17580
17581 Ok(Some(vec![]))
17582 }
17583 });
17584
17585 cx.simulate_keystroke(".");
17586 cx.run_until_parked();
17587
17588 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17589 assert!(request.next().await.is_some());
17590 request.close();
17591 assert!(request.next().await.is_none());
17592}
17593
17594#[gpui::test]
17595async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17596 init_test(cx, |_| {});
17597
17598 let fs = FakeFs::new(cx.executor());
17599 fs.insert_tree(
17600 path!("/a"),
17601 json!({
17602 "main.rs": "fn main() { let a = 5; }",
17603 "other.rs": "// Test file",
17604 }),
17605 )
17606 .await;
17607
17608 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17609
17610 let server_restarts = Arc::new(AtomicUsize::new(0));
17611 let closure_restarts = Arc::clone(&server_restarts);
17612 let language_server_name = "test language server";
17613 let language_name: LanguageName = "Rust".into();
17614
17615 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17616 language_registry.add(Arc::new(Language::new(
17617 LanguageConfig {
17618 name: language_name.clone(),
17619 matcher: LanguageMatcher {
17620 path_suffixes: vec!["rs".to_string()],
17621 ..Default::default()
17622 },
17623 ..Default::default()
17624 },
17625 Some(tree_sitter_rust::LANGUAGE.into()),
17626 )));
17627 let mut fake_servers = language_registry.register_fake_lsp(
17628 "Rust",
17629 FakeLspAdapter {
17630 name: language_server_name,
17631 initialization_options: Some(json!({
17632 "testOptionValue": true
17633 })),
17634 initializer: Some(Box::new(move |fake_server| {
17635 let task_restarts = Arc::clone(&closure_restarts);
17636 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17637 task_restarts.fetch_add(1, atomic::Ordering::Release);
17638 futures::future::ready(Ok(()))
17639 });
17640 })),
17641 ..Default::default()
17642 },
17643 );
17644
17645 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17646 let _buffer = project
17647 .update(cx, |project, cx| {
17648 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17649 })
17650 .await
17651 .unwrap();
17652 let _fake_server = fake_servers.next().await.unwrap();
17653 update_test_language_settings(cx, |language_settings| {
17654 language_settings.languages.0.insert(
17655 language_name.clone().0,
17656 LanguageSettingsContent {
17657 tab_size: NonZeroU32::new(8),
17658 ..Default::default()
17659 },
17660 );
17661 });
17662 cx.executor().run_until_parked();
17663 assert_eq!(
17664 server_restarts.load(atomic::Ordering::Acquire),
17665 0,
17666 "Should not restart LSP server on an unrelated change"
17667 );
17668
17669 update_test_project_settings(cx, |project_settings| {
17670 project_settings.lsp.insert(
17671 "Some other server name".into(),
17672 LspSettings {
17673 binary: None,
17674 settings: None,
17675 initialization_options: Some(json!({
17676 "some other init value": false
17677 })),
17678 enable_lsp_tasks: false,
17679 fetch: None,
17680 },
17681 );
17682 });
17683 cx.executor().run_until_parked();
17684 assert_eq!(
17685 server_restarts.load(atomic::Ordering::Acquire),
17686 0,
17687 "Should not restart LSP server on an unrelated LSP settings change"
17688 );
17689
17690 update_test_project_settings(cx, |project_settings| {
17691 project_settings.lsp.insert(
17692 language_server_name.into(),
17693 LspSettings {
17694 binary: None,
17695 settings: None,
17696 initialization_options: Some(json!({
17697 "anotherInitValue": false
17698 })),
17699 enable_lsp_tasks: false,
17700 fetch: None,
17701 },
17702 );
17703 });
17704 cx.executor().run_until_parked();
17705 assert_eq!(
17706 server_restarts.load(atomic::Ordering::Acquire),
17707 1,
17708 "Should restart LSP server on a related LSP settings change"
17709 );
17710
17711 update_test_project_settings(cx, |project_settings| {
17712 project_settings.lsp.insert(
17713 language_server_name.into(),
17714 LspSettings {
17715 binary: None,
17716 settings: None,
17717 initialization_options: Some(json!({
17718 "anotherInitValue": false
17719 })),
17720 enable_lsp_tasks: false,
17721 fetch: None,
17722 },
17723 );
17724 });
17725 cx.executor().run_until_parked();
17726 assert_eq!(
17727 server_restarts.load(atomic::Ordering::Acquire),
17728 1,
17729 "Should not restart LSP server on a related LSP settings change that is the same"
17730 );
17731
17732 update_test_project_settings(cx, |project_settings| {
17733 project_settings.lsp.insert(
17734 language_server_name.into(),
17735 LspSettings {
17736 binary: None,
17737 settings: None,
17738 initialization_options: None,
17739 enable_lsp_tasks: false,
17740 fetch: None,
17741 },
17742 );
17743 });
17744 cx.executor().run_until_parked();
17745 assert_eq!(
17746 server_restarts.load(atomic::Ordering::Acquire),
17747 2,
17748 "Should restart LSP server on another related LSP settings change"
17749 );
17750}
17751
17752#[gpui::test]
17753async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17754 init_test(cx, |_| {});
17755
17756 let mut cx = EditorLspTestContext::new_rust(
17757 lsp::ServerCapabilities {
17758 completion_provider: Some(lsp::CompletionOptions {
17759 trigger_characters: Some(vec![".".to_string()]),
17760 resolve_provider: Some(true),
17761 ..Default::default()
17762 }),
17763 ..Default::default()
17764 },
17765 cx,
17766 )
17767 .await;
17768
17769 cx.set_state("fn main() { let a = 2ˇ; }");
17770 cx.simulate_keystroke(".");
17771 let completion_item = lsp::CompletionItem {
17772 label: "some".into(),
17773 kind: Some(lsp::CompletionItemKind::SNIPPET),
17774 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17775 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17776 kind: lsp::MarkupKind::Markdown,
17777 value: "```rust\nSome(2)\n```".to_string(),
17778 })),
17779 deprecated: Some(false),
17780 sort_text: Some("fffffff2".to_string()),
17781 filter_text: Some("some".to_string()),
17782 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17783 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17784 range: lsp::Range {
17785 start: lsp::Position {
17786 line: 0,
17787 character: 22,
17788 },
17789 end: lsp::Position {
17790 line: 0,
17791 character: 22,
17792 },
17793 },
17794 new_text: "Some(2)".to_string(),
17795 })),
17796 additional_text_edits: Some(vec![lsp::TextEdit {
17797 range: lsp::Range {
17798 start: lsp::Position {
17799 line: 0,
17800 character: 20,
17801 },
17802 end: lsp::Position {
17803 line: 0,
17804 character: 22,
17805 },
17806 },
17807 new_text: "".to_string(),
17808 }]),
17809 ..Default::default()
17810 };
17811
17812 let closure_completion_item = completion_item.clone();
17813 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17814 let task_completion_item = closure_completion_item.clone();
17815 async move {
17816 Ok(Some(lsp::CompletionResponse::Array(vec![
17817 task_completion_item,
17818 ])))
17819 }
17820 });
17821
17822 request.next().await;
17823
17824 cx.condition(|editor, _| editor.context_menu_visible())
17825 .await;
17826 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17827 editor
17828 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17829 .unwrap()
17830 });
17831 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17832
17833 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17834 let task_completion_item = completion_item.clone();
17835 async move { Ok(task_completion_item) }
17836 })
17837 .next()
17838 .await
17839 .unwrap();
17840 apply_additional_edits.await.unwrap();
17841 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17842}
17843
17844#[gpui::test]
17845async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17846 init_test(cx, |_| {});
17847
17848 let mut cx = EditorLspTestContext::new_rust(
17849 lsp::ServerCapabilities {
17850 completion_provider: Some(lsp::CompletionOptions {
17851 trigger_characters: Some(vec![".".to_string()]),
17852 resolve_provider: Some(true),
17853 ..Default::default()
17854 }),
17855 ..Default::default()
17856 },
17857 cx,
17858 )
17859 .await;
17860
17861 cx.set_state("fn main() { let a = 2ˇ; }");
17862 cx.simulate_keystroke(".");
17863
17864 let item1 = lsp::CompletionItem {
17865 label: "method id()".to_string(),
17866 filter_text: Some("id".to_string()),
17867 detail: None,
17868 documentation: None,
17869 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17870 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17871 new_text: ".id".to_string(),
17872 })),
17873 ..lsp::CompletionItem::default()
17874 };
17875
17876 let item2 = lsp::CompletionItem {
17877 label: "other".to_string(),
17878 filter_text: Some("other".to_string()),
17879 detail: None,
17880 documentation: None,
17881 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17882 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17883 new_text: ".other".to_string(),
17884 })),
17885 ..lsp::CompletionItem::default()
17886 };
17887
17888 let item1 = item1.clone();
17889 cx.set_request_handler::<lsp::request::Completion, _, _>({
17890 let item1 = item1.clone();
17891 move |_, _, _| {
17892 let item1 = item1.clone();
17893 let item2 = item2.clone();
17894 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17895 }
17896 })
17897 .next()
17898 .await;
17899
17900 cx.condition(|editor, _| editor.context_menu_visible())
17901 .await;
17902 cx.update_editor(|editor, _, _| {
17903 let context_menu = editor.context_menu.borrow_mut();
17904 let context_menu = context_menu
17905 .as_ref()
17906 .expect("Should have the context menu deployed");
17907 match context_menu {
17908 CodeContextMenu::Completions(completions_menu) => {
17909 let completions = completions_menu.completions.borrow_mut();
17910 assert_eq!(
17911 completions
17912 .iter()
17913 .map(|completion| &completion.label.text)
17914 .collect::<Vec<_>>(),
17915 vec!["method id()", "other"]
17916 )
17917 }
17918 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17919 }
17920 });
17921
17922 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17923 let item1 = item1.clone();
17924 move |_, item_to_resolve, _| {
17925 let item1 = item1.clone();
17926 async move {
17927 if item1 == item_to_resolve {
17928 Ok(lsp::CompletionItem {
17929 label: "method id()".to_string(),
17930 filter_text: Some("id".to_string()),
17931 detail: Some("Now resolved!".to_string()),
17932 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17933 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17934 range: lsp::Range::new(
17935 lsp::Position::new(0, 22),
17936 lsp::Position::new(0, 22),
17937 ),
17938 new_text: ".id".to_string(),
17939 })),
17940 ..lsp::CompletionItem::default()
17941 })
17942 } else {
17943 Ok(item_to_resolve)
17944 }
17945 }
17946 }
17947 })
17948 .next()
17949 .await
17950 .unwrap();
17951 cx.run_until_parked();
17952
17953 cx.update_editor(|editor, window, cx| {
17954 editor.context_menu_next(&Default::default(), window, cx);
17955 });
17956
17957 cx.update_editor(|editor, _, _| {
17958 let context_menu = editor.context_menu.borrow_mut();
17959 let context_menu = context_menu
17960 .as_ref()
17961 .expect("Should have the context menu deployed");
17962 match context_menu {
17963 CodeContextMenu::Completions(completions_menu) => {
17964 let completions = completions_menu.completions.borrow_mut();
17965 assert_eq!(
17966 completions
17967 .iter()
17968 .map(|completion| &completion.label.text)
17969 .collect::<Vec<_>>(),
17970 vec!["method id() Now resolved!", "other"],
17971 "Should update first completion label, but not second as the filter text did not match."
17972 );
17973 }
17974 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17975 }
17976 });
17977}
17978
17979#[gpui::test]
17980async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17981 init_test(cx, |_| {});
17982 let mut cx = EditorLspTestContext::new_rust(
17983 lsp::ServerCapabilities {
17984 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17985 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17986 completion_provider: Some(lsp::CompletionOptions {
17987 resolve_provider: Some(true),
17988 ..Default::default()
17989 }),
17990 ..Default::default()
17991 },
17992 cx,
17993 )
17994 .await;
17995 cx.set_state(indoc! {"
17996 struct TestStruct {
17997 field: i32
17998 }
17999
18000 fn mainˇ() {
18001 let unused_var = 42;
18002 let test_struct = TestStruct { field: 42 };
18003 }
18004 "});
18005 let symbol_range = cx.lsp_range(indoc! {"
18006 struct TestStruct {
18007 field: i32
18008 }
18009
18010 «fn main»() {
18011 let unused_var = 42;
18012 let test_struct = TestStruct { field: 42 };
18013 }
18014 "});
18015 let mut hover_requests =
18016 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18017 Ok(Some(lsp::Hover {
18018 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18019 kind: lsp::MarkupKind::Markdown,
18020 value: "Function documentation".to_string(),
18021 }),
18022 range: Some(symbol_range),
18023 }))
18024 });
18025
18026 // Case 1: Test that code action menu hide hover popover
18027 cx.dispatch_action(Hover);
18028 hover_requests.next().await;
18029 cx.condition(|editor, _| editor.hover_state.visible()).await;
18030 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18031 move |_, _, _| async move {
18032 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18033 lsp::CodeAction {
18034 title: "Remove unused variable".to_string(),
18035 kind: Some(CodeActionKind::QUICKFIX),
18036 edit: Some(lsp::WorkspaceEdit {
18037 changes: Some(
18038 [(
18039 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18040 vec![lsp::TextEdit {
18041 range: lsp::Range::new(
18042 lsp::Position::new(5, 4),
18043 lsp::Position::new(5, 27),
18044 ),
18045 new_text: "".to_string(),
18046 }],
18047 )]
18048 .into_iter()
18049 .collect(),
18050 ),
18051 ..Default::default()
18052 }),
18053 ..Default::default()
18054 },
18055 )]))
18056 },
18057 );
18058 cx.update_editor(|editor, window, cx| {
18059 editor.toggle_code_actions(
18060 &ToggleCodeActions {
18061 deployed_from: None,
18062 quick_launch: false,
18063 },
18064 window,
18065 cx,
18066 );
18067 });
18068 code_action_requests.next().await;
18069 cx.run_until_parked();
18070 cx.condition(|editor, _| editor.context_menu_visible())
18071 .await;
18072 cx.update_editor(|editor, _, _| {
18073 assert!(
18074 !editor.hover_state.visible(),
18075 "Hover popover should be hidden when code action menu is shown"
18076 );
18077 // Hide code actions
18078 editor.context_menu.take();
18079 });
18080
18081 // Case 2: Test that code completions hide hover popover
18082 cx.dispatch_action(Hover);
18083 hover_requests.next().await;
18084 cx.condition(|editor, _| editor.hover_state.visible()).await;
18085 let counter = Arc::new(AtomicUsize::new(0));
18086 let mut completion_requests =
18087 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18088 let counter = counter.clone();
18089 async move {
18090 counter.fetch_add(1, atomic::Ordering::Release);
18091 Ok(Some(lsp::CompletionResponse::Array(vec![
18092 lsp::CompletionItem {
18093 label: "main".into(),
18094 kind: Some(lsp::CompletionItemKind::FUNCTION),
18095 detail: Some("() -> ()".to_string()),
18096 ..Default::default()
18097 },
18098 lsp::CompletionItem {
18099 label: "TestStruct".into(),
18100 kind: Some(lsp::CompletionItemKind::STRUCT),
18101 detail: Some("struct TestStruct".to_string()),
18102 ..Default::default()
18103 },
18104 ])))
18105 }
18106 });
18107 cx.update_editor(|editor, window, cx| {
18108 editor.show_completions(&ShowCompletions, window, cx);
18109 });
18110 completion_requests.next().await;
18111 cx.condition(|editor, _| editor.context_menu_visible())
18112 .await;
18113 cx.update_editor(|editor, _, _| {
18114 assert!(
18115 !editor.hover_state.visible(),
18116 "Hover popover should be hidden when completion menu is shown"
18117 );
18118 });
18119}
18120
18121#[gpui::test]
18122async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18123 init_test(cx, |_| {});
18124
18125 let mut cx = EditorLspTestContext::new_rust(
18126 lsp::ServerCapabilities {
18127 completion_provider: Some(lsp::CompletionOptions {
18128 trigger_characters: Some(vec![".".to_string()]),
18129 resolve_provider: Some(true),
18130 ..Default::default()
18131 }),
18132 ..Default::default()
18133 },
18134 cx,
18135 )
18136 .await;
18137
18138 cx.set_state("fn main() { let a = 2ˇ; }");
18139 cx.simulate_keystroke(".");
18140
18141 let unresolved_item_1 = lsp::CompletionItem {
18142 label: "id".to_string(),
18143 filter_text: Some("id".to_string()),
18144 detail: None,
18145 documentation: None,
18146 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18147 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18148 new_text: ".id".to_string(),
18149 })),
18150 ..lsp::CompletionItem::default()
18151 };
18152 let resolved_item_1 = lsp::CompletionItem {
18153 additional_text_edits: Some(vec![lsp::TextEdit {
18154 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18155 new_text: "!!".to_string(),
18156 }]),
18157 ..unresolved_item_1.clone()
18158 };
18159 let unresolved_item_2 = lsp::CompletionItem {
18160 label: "other".to_string(),
18161 filter_text: Some("other".to_string()),
18162 detail: None,
18163 documentation: None,
18164 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18165 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18166 new_text: ".other".to_string(),
18167 })),
18168 ..lsp::CompletionItem::default()
18169 };
18170 let resolved_item_2 = lsp::CompletionItem {
18171 additional_text_edits: Some(vec![lsp::TextEdit {
18172 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18173 new_text: "??".to_string(),
18174 }]),
18175 ..unresolved_item_2.clone()
18176 };
18177
18178 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18179 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18180 cx.lsp
18181 .server
18182 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18183 let unresolved_item_1 = unresolved_item_1.clone();
18184 let resolved_item_1 = resolved_item_1.clone();
18185 let unresolved_item_2 = unresolved_item_2.clone();
18186 let resolved_item_2 = resolved_item_2.clone();
18187 let resolve_requests_1 = resolve_requests_1.clone();
18188 let resolve_requests_2 = resolve_requests_2.clone();
18189 move |unresolved_request, _| {
18190 let unresolved_item_1 = unresolved_item_1.clone();
18191 let resolved_item_1 = resolved_item_1.clone();
18192 let unresolved_item_2 = unresolved_item_2.clone();
18193 let resolved_item_2 = resolved_item_2.clone();
18194 let resolve_requests_1 = resolve_requests_1.clone();
18195 let resolve_requests_2 = resolve_requests_2.clone();
18196 async move {
18197 if unresolved_request == unresolved_item_1 {
18198 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18199 Ok(resolved_item_1.clone())
18200 } else if unresolved_request == unresolved_item_2 {
18201 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18202 Ok(resolved_item_2.clone())
18203 } else {
18204 panic!("Unexpected completion item {unresolved_request:?}")
18205 }
18206 }
18207 }
18208 })
18209 .detach();
18210
18211 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18212 let unresolved_item_1 = unresolved_item_1.clone();
18213 let unresolved_item_2 = unresolved_item_2.clone();
18214 async move {
18215 Ok(Some(lsp::CompletionResponse::Array(vec![
18216 unresolved_item_1,
18217 unresolved_item_2,
18218 ])))
18219 }
18220 })
18221 .next()
18222 .await;
18223
18224 cx.condition(|editor, _| editor.context_menu_visible())
18225 .await;
18226 cx.update_editor(|editor, _, _| {
18227 let context_menu = editor.context_menu.borrow_mut();
18228 let context_menu = context_menu
18229 .as_ref()
18230 .expect("Should have the context menu deployed");
18231 match context_menu {
18232 CodeContextMenu::Completions(completions_menu) => {
18233 let completions = completions_menu.completions.borrow_mut();
18234 assert_eq!(
18235 completions
18236 .iter()
18237 .map(|completion| &completion.label.text)
18238 .collect::<Vec<_>>(),
18239 vec!["id", "other"]
18240 )
18241 }
18242 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18243 }
18244 });
18245 cx.run_until_parked();
18246
18247 cx.update_editor(|editor, window, cx| {
18248 editor.context_menu_next(&ContextMenuNext, window, cx);
18249 });
18250 cx.run_until_parked();
18251 cx.update_editor(|editor, window, cx| {
18252 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18253 });
18254 cx.run_until_parked();
18255 cx.update_editor(|editor, window, cx| {
18256 editor.context_menu_next(&ContextMenuNext, window, cx);
18257 });
18258 cx.run_until_parked();
18259 cx.update_editor(|editor, window, cx| {
18260 editor
18261 .compose_completion(&ComposeCompletion::default(), window, cx)
18262 .expect("No task returned")
18263 })
18264 .await
18265 .expect("Completion failed");
18266 cx.run_until_parked();
18267
18268 cx.update_editor(|editor, _, cx| {
18269 assert_eq!(
18270 resolve_requests_1.load(atomic::Ordering::Acquire),
18271 1,
18272 "Should always resolve once despite multiple selections"
18273 );
18274 assert_eq!(
18275 resolve_requests_2.load(atomic::Ordering::Acquire),
18276 1,
18277 "Should always resolve once after multiple selections and applying the completion"
18278 );
18279 assert_eq!(
18280 editor.text(cx),
18281 "fn main() { let a = ??.other; }",
18282 "Should use resolved data when applying the completion"
18283 );
18284 });
18285}
18286
18287#[gpui::test]
18288async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18289 init_test(cx, |_| {});
18290
18291 let item_0 = lsp::CompletionItem {
18292 label: "abs".into(),
18293 insert_text: Some("abs".into()),
18294 data: Some(json!({ "very": "special"})),
18295 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18296 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18297 lsp::InsertReplaceEdit {
18298 new_text: "abs".to_string(),
18299 insert: lsp::Range::default(),
18300 replace: lsp::Range::default(),
18301 },
18302 )),
18303 ..lsp::CompletionItem::default()
18304 };
18305 let items = iter::once(item_0.clone())
18306 .chain((11..51).map(|i| lsp::CompletionItem {
18307 label: format!("item_{}", i),
18308 insert_text: Some(format!("item_{}", i)),
18309 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18310 ..lsp::CompletionItem::default()
18311 }))
18312 .collect::<Vec<_>>();
18313
18314 let default_commit_characters = vec!["?".to_string()];
18315 let default_data = json!({ "default": "data"});
18316 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18317 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18318 let default_edit_range = lsp::Range {
18319 start: lsp::Position {
18320 line: 0,
18321 character: 5,
18322 },
18323 end: lsp::Position {
18324 line: 0,
18325 character: 5,
18326 },
18327 };
18328
18329 let mut cx = EditorLspTestContext::new_rust(
18330 lsp::ServerCapabilities {
18331 completion_provider: Some(lsp::CompletionOptions {
18332 trigger_characters: Some(vec![".".to_string()]),
18333 resolve_provider: Some(true),
18334 ..Default::default()
18335 }),
18336 ..Default::default()
18337 },
18338 cx,
18339 )
18340 .await;
18341
18342 cx.set_state("fn main() { let a = 2ˇ; }");
18343 cx.simulate_keystroke(".");
18344
18345 let completion_data = default_data.clone();
18346 let completion_characters = default_commit_characters.clone();
18347 let completion_items = items.clone();
18348 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18349 let default_data = completion_data.clone();
18350 let default_commit_characters = completion_characters.clone();
18351 let items = completion_items.clone();
18352 async move {
18353 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18354 items,
18355 item_defaults: Some(lsp::CompletionListItemDefaults {
18356 data: Some(default_data.clone()),
18357 commit_characters: Some(default_commit_characters.clone()),
18358 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18359 default_edit_range,
18360 )),
18361 insert_text_format: Some(default_insert_text_format),
18362 insert_text_mode: Some(default_insert_text_mode),
18363 }),
18364 ..lsp::CompletionList::default()
18365 })))
18366 }
18367 })
18368 .next()
18369 .await;
18370
18371 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18372 cx.lsp
18373 .server
18374 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18375 let closure_resolved_items = resolved_items.clone();
18376 move |item_to_resolve, _| {
18377 let closure_resolved_items = closure_resolved_items.clone();
18378 async move {
18379 closure_resolved_items.lock().push(item_to_resolve.clone());
18380 Ok(item_to_resolve)
18381 }
18382 }
18383 })
18384 .detach();
18385
18386 cx.condition(|editor, _| editor.context_menu_visible())
18387 .await;
18388 cx.run_until_parked();
18389 cx.update_editor(|editor, _, _| {
18390 let menu = editor.context_menu.borrow_mut();
18391 match menu.as_ref().expect("should have the completions menu") {
18392 CodeContextMenu::Completions(completions_menu) => {
18393 assert_eq!(
18394 completions_menu
18395 .entries
18396 .borrow()
18397 .iter()
18398 .map(|mat| mat.string.clone())
18399 .collect::<Vec<String>>(),
18400 items
18401 .iter()
18402 .map(|completion| completion.label.clone())
18403 .collect::<Vec<String>>()
18404 );
18405 }
18406 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18407 }
18408 });
18409 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18410 // with 4 from the end.
18411 assert_eq!(
18412 *resolved_items.lock(),
18413 [&items[0..16], &items[items.len() - 4..items.len()]]
18414 .concat()
18415 .iter()
18416 .cloned()
18417 .map(|mut item| {
18418 if item.data.is_none() {
18419 item.data = Some(default_data.clone());
18420 }
18421 item
18422 })
18423 .collect::<Vec<lsp::CompletionItem>>(),
18424 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18425 );
18426 resolved_items.lock().clear();
18427
18428 cx.update_editor(|editor, window, cx| {
18429 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18430 });
18431 cx.run_until_parked();
18432 // Completions that have already been resolved are skipped.
18433 assert_eq!(
18434 *resolved_items.lock(),
18435 items[items.len() - 17..items.len() - 4]
18436 .iter()
18437 .cloned()
18438 .map(|mut item| {
18439 if item.data.is_none() {
18440 item.data = Some(default_data.clone());
18441 }
18442 item
18443 })
18444 .collect::<Vec<lsp::CompletionItem>>()
18445 );
18446 resolved_items.lock().clear();
18447}
18448
18449#[gpui::test]
18450async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18451 init_test(cx, |_| {});
18452
18453 let mut cx = EditorLspTestContext::new(
18454 Language::new(
18455 LanguageConfig {
18456 matcher: LanguageMatcher {
18457 path_suffixes: vec!["jsx".into()],
18458 ..Default::default()
18459 },
18460 overrides: [(
18461 "element".into(),
18462 LanguageConfigOverride {
18463 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18464 ..Default::default()
18465 },
18466 )]
18467 .into_iter()
18468 .collect(),
18469 ..Default::default()
18470 },
18471 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18472 )
18473 .with_override_query("(jsx_self_closing_element) @element")
18474 .unwrap(),
18475 lsp::ServerCapabilities {
18476 completion_provider: Some(lsp::CompletionOptions {
18477 trigger_characters: Some(vec![":".to_string()]),
18478 ..Default::default()
18479 }),
18480 ..Default::default()
18481 },
18482 cx,
18483 )
18484 .await;
18485
18486 cx.lsp
18487 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18488 Ok(Some(lsp::CompletionResponse::Array(vec![
18489 lsp::CompletionItem {
18490 label: "bg-blue".into(),
18491 ..Default::default()
18492 },
18493 lsp::CompletionItem {
18494 label: "bg-red".into(),
18495 ..Default::default()
18496 },
18497 lsp::CompletionItem {
18498 label: "bg-yellow".into(),
18499 ..Default::default()
18500 },
18501 ])))
18502 });
18503
18504 cx.set_state(r#"<p class="bgˇ" />"#);
18505
18506 // Trigger completion when typing a dash, because the dash is an extra
18507 // word character in the 'element' scope, which contains the cursor.
18508 cx.simulate_keystroke("-");
18509 cx.executor().run_until_parked();
18510 cx.update_editor(|editor, _, _| {
18511 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18512 {
18513 assert_eq!(
18514 completion_menu_entries(menu),
18515 &["bg-blue", "bg-red", "bg-yellow"]
18516 );
18517 } else {
18518 panic!("expected completion menu to be open");
18519 }
18520 });
18521
18522 cx.simulate_keystroke("l");
18523 cx.executor().run_until_parked();
18524 cx.update_editor(|editor, _, _| {
18525 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18526 {
18527 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18528 } else {
18529 panic!("expected completion menu to be open");
18530 }
18531 });
18532
18533 // When filtering completions, consider the character after the '-' to
18534 // be the start of a subword.
18535 cx.set_state(r#"<p class="yelˇ" />"#);
18536 cx.simulate_keystroke("l");
18537 cx.executor().run_until_parked();
18538 cx.update_editor(|editor, _, _| {
18539 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18540 {
18541 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18542 } else {
18543 panic!("expected completion menu to be open");
18544 }
18545 });
18546}
18547
18548fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18549 let entries = menu.entries.borrow();
18550 entries.iter().map(|mat| mat.string.clone()).collect()
18551}
18552
18553#[gpui::test]
18554async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18555 init_test(cx, |settings| {
18556 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18557 });
18558
18559 let fs = FakeFs::new(cx.executor());
18560 fs.insert_file(path!("/file.ts"), Default::default()).await;
18561
18562 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18563 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18564
18565 language_registry.add(Arc::new(Language::new(
18566 LanguageConfig {
18567 name: "TypeScript".into(),
18568 matcher: LanguageMatcher {
18569 path_suffixes: vec!["ts".to_string()],
18570 ..Default::default()
18571 },
18572 ..Default::default()
18573 },
18574 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18575 )));
18576 update_test_language_settings(cx, |settings| {
18577 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18578 });
18579
18580 let test_plugin = "test_plugin";
18581 let _ = language_registry.register_fake_lsp(
18582 "TypeScript",
18583 FakeLspAdapter {
18584 prettier_plugins: vec![test_plugin],
18585 ..Default::default()
18586 },
18587 );
18588
18589 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18590 let buffer = project
18591 .update(cx, |project, cx| {
18592 project.open_local_buffer(path!("/file.ts"), cx)
18593 })
18594 .await
18595 .unwrap();
18596
18597 let buffer_text = "one\ntwo\nthree\n";
18598 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18599 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18600 editor.update_in(cx, |editor, window, cx| {
18601 editor.set_text(buffer_text, window, cx)
18602 });
18603
18604 editor
18605 .update_in(cx, |editor, window, cx| {
18606 editor.perform_format(
18607 project.clone(),
18608 FormatTrigger::Manual,
18609 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18610 window,
18611 cx,
18612 )
18613 })
18614 .unwrap()
18615 .await;
18616 assert_eq!(
18617 editor.update(cx, |editor, cx| editor.text(cx)),
18618 buffer_text.to_string() + prettier_format_suffix,
18619 "Test prettier formatting was not applied to the original buffer text",
18620 );
18621
18622 update_test_language_settings(cx, |settings| {
18623 settings.defaults.formatter = Some(FormatterList::default())
18624 });
18625 let format = editor.update_in(cx, |editor, window, cx| {
18626 editor.perform_format(
18627 project.clone(),
18628 FormatTrigger::Manual,
18629 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18630 window,
18631 cx,
18632 )
18633 });
18634 format.await.unwrap();
18635 assert_eq!(
18636 editor.update(cx, |editor, cx| editor.text(cx)),
18637 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18638 "Autoformatting (via test prettier) was not applied to the original buffer text",
18639 );
18640}
18641
18642#[gpui::test]
18643async fn test_addition_reverts(cx: &mut TestAppContext) {
18644 init_test(cx, |_| {});
18645 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18646 let base_text = indoc! {r#"
18647 struct Row;
18648 struct Row1;
18649 struct Row2;
18650
18651 struct Row4;
18652 struct Row5;
18653 struct Row6;
18654
18655 struct Row8;
18656 struct Row9;
18657 struct Row10;"#};
18658
18659 // When addition hunks are not adjacent to carets, no hunk revert is performed
18660 assert_hunk_revert(
18661 indoc! {r#"struct Row;
18662 struct Row1;
18663 struct Row1.1;
18664 struct Row1.2;
18665 struct Row2;ˇ
18666
18667 struct Row4;
18668 struct Row5;
18669 struct Row6;
18670
18671 struct Row8;
18672 ˇstruct Row9;
18673 struct Row9.1;
18674 struct Row9.2;
18675 struct Row9.3;
18676 struct Row10;"#},
18677 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18678 indoc! {r#"struct Row;
18679 struct Row1;
18680 struct Row1.1;
18681 struct Row1.2;
18682 struct Row2;ˇ
18683
18684 struct Row4;
18685 struct Row5;
18686 struct Row6;
18687
18688 struct Row8;
18689 ˇstruct Row9;
18690 struct Row9.1;
18691 struct Row9.2;
18692 struct Row9.3;
18693 struct Row10;"#},
18694 base_text,
18695 &mut cx,
18696 );
18697 // Same for selections
18698 assert_hunk_revert(
18699 indoc! {r#"struct Row;
18700 struct Row1;
18701 struct Row2;
18702 struct Row2.1;
18703 struct Row2.2;
18704 «ˇ
18705 struct Row4;
18706 struct» Row5;
18707 «struct Row6;
18708 ˇ»
18709 struct Row9.1;
18710 struct Row9.2;
18711 struct Row9.3;
18712 struct Row8;
18713 struct Row9;
18714 struct Row10;"#},
18715 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18716 indoc! {r#"struct Row;
18717 struct Row1;
18718 struct Row2;
18719 struct Row2.1;
18720 struct Row2.2;
18721 «ˇ
18722 struct Row4;
18723 struct» Row5;
18724 «struct Row6;
18725 ˇ»
18726 struct Row9.1;
18727 struct Row9.2;
18728 struct Row9.3;
18729 struct Row8;
18730 struct Row9;
18731 struct Row10;"#},
18732 base_text,
18733 &mut cx,
18734 );
18735
18736 // When carets and selections intersect the addition hunks, those are reverted.
18737 // Adjacent carets got merged.
18738 assert_hunk_revert(
18739 indoc! {r#"struct Row;
18740 ˇ// something on the top
18741 struct Row1;
18742 struct Row2;
18743 struct Roˇw3.1;
18744 struct Row2.2;
18745 struct Row2.3;ˇ
18746
18747 struct Row4;
18748 struct ˇRow5.1;
18749 struct Row5.2;
18750 struct «Rowˇ»5.3;
18751 struct Row5;
18752 struct Row6;
18753 ˇ
18754 struct Row9.1;
18755 struct «Rowˇ»9.2;
18756 struct «ˇRow»9.3;
18757 struct Row8;
18758 struct Row9;
18759 «ˇ// something on bottom»
18760 struct Row10;"#},
18761 vec![
18762 DiffHunkStatusKind::Added,
18763 DiffHunkStatusKind::Added,
18764 DiffHunkStatusKind::Added,
18765 DiffHunkStatusKind::Added,
18766 DiffHunkStatusKind::Added,
18767 ],
18768 indoc! {r#"struct Row;
18769 ˇstruct Row1;
18770 struct Row2;
18771 ˇ
18772 struct Row4;
18773 ˇstruct Row5;
18774 struct Row6;
18775 ˇ
18776 ˇstruct Row8;
18777 struct Row9;
18778 ˇstruct Row10;"#},
18779 base_text,
18780 &mut cx,
18781 );
18782}
18783
18784#[gpui::test]
18785async fn test_modification_reverts(cx: &mut TestAppContext) {
18786 init_test(cx, |_| {});
18787 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18788 let base_text = indoc! {r#"
18789 struct Row;
18790 struct Row1;
18791 struct Row2;
18792
18793 struct Row4;
18794 struct Row5;
18795 struct Row6;
18796
18797 struct Row8;
18798 struct Row9;
18799 struct Row10;"#};
18800
18801 // Modification hunks behave the same as the addition ones.
18802 assert_hunk_revert(
18803 indoc! {r#"struct Row;
18804 struct Row1;
18805 struct Row33;
18806 ˇ
18807 struct Row4;
18808 struct Row5;
18809 struct Row6;
18810 ˇ
18811 struct Row99;
18812 struct Row9;
18813 struct Row10;"#},
18814 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18815 indoc! {r#"struct Row;
18816 struct Row1;
18817 struct Row33;
18818 ˇ
18819 struct Row4;
18820 struct Row5;
18821 struct Row6;
18822 ˇ
18823 struct Row99;
18824 struct Row9;
18825 struct Row10;"#},
18826 base_text,
18827 &mut cx,
18828 );
18829 assert_hunk_revert(
18830 indoc! {r#"struct Row;
18831 struct Row1;
18832 struct Row33;
18833 «ˇ
18834 struct Row4;
18835 struct» Row5;
18836 «struct Row6;
18837 ˇ»
18838 struct Row99;
18839 struct Row9;
18840 struct Row10;"#},
18841 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18842 indoc! {r#"struct Row;
18843 struct Row1;
18844 struct Row33;
18845 «ˇ
18846 struct Row4;
18847 struct» Row5;
18848 «struct Row6;
18849 ˇ»
18850 struct Row99;
18851 struct Row9;
18852 struct Row10;"#},
18853 base_text,
18854 &mut cx,
18855 );
18856
18857 assert_hunk_revert(
18858 indoc! {r#"ˇstruct Row1.1;
18859 struct Row1;
18860 «ˇstr»uct Row22;
18861
18862 struct ˇRow44;
18863 struct Row5;
18864 struct «Rˇ»ow66;ˇ
18865
18866 «struˇ»ct Row88;
18867 struct Row9;
18868 struct Row1011;ˇ"#},
18869 vec![
18870 DiffHunkStatusKind::Modified,
18871 DiffHunkStatusKind::Modified,
18872 DiffHunkStatusKind::Modified,
18873 DiffHunkStatusKind::Modified,
18874 DiffHunkStatusKind::Modified,
18875 DiffHunkStatusKind::Modified,
18876 ],
18877 indoc! {r#"struct Row;
18878 ˇstruct Row1;
18879 struct Row2;
18880 ˇ
18881 struct Row4;
18882 ˇstruct Row5;
18883 struct Row6;
18884 ˇ
18885 struct Row8;
18886 ˇstruct Row9;
18887 struct Row10;ˇ"#},
18888 base_text,
18889 &mut cx,
18890 );
18891}
18892
18893#[gpui::test]
18894async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18895 init_test(cx, |_| {});
18896 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18897 let base_text = indoc! {r#"
18898 one
18899
18900 two
18901 three
18902 "#};
18903
18904 cx.set_head_text(base_text);
18905 cx.set_state("\nˇ\n");
18906 cx.executor().run_until_parked();
18907 cx.update_editor(|editor, _window, cx| {
18908 editor.expand_selected_diff_hunks(cx);
18909 });
18910 cx.executor().run_until_parked();
18911 cx.update_editor(|editor, window, cx| {
18912 editor.backspace(&Default::default(), window, cx);
18913 });
18914 cx.run_until_parked();
18915 cx.assert_state_with_diff(
18916 indoc! {r#"
18917
18918 - two
18919 - threeˇ
18920 +
18921 "#}
18922 .to_string(),
18923 );
18924}
18925
18926#[gpui::test]
18927async fn test_deletion_reverts(cx: &mut TestAppContext) {
18928 init_test(cx, |_| {});
18929 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18930 let base_text = indoc! {r#"struct Row;
18931struct Row1;
18932struct Row2;
18933
18934struct Row4;
18935struct Row5;
18936struct Row6;
18937
18938struct Row8;
18939struct Row9;
18940struct Row10;"#};
18941
18942 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18943 assert_hunk_revert(
18944 indoc! {r#"struct Row;
18945 struct Row2;
18946
18947 ˇstruct Row4;
18948 struct Row5;
18949 struct Row6;
18950 ˇ
18951 struct Row8;
18952 struct Row10;"#},
18953 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18954 indoc! {r#"struct Row;
18955 struct Row2;
18956
18957 ˇstruct Row4;
18958 struct Row5;
18959 struct Row6;
18960 ˇ
18961 struct Row8;
18962 struct Row10;"#},
18963 base_text,
18964 &mut cx,
18965 );
18966 assert_hunk_revert(
18967 indoc! {r#"struct Row;
18968 struct Row2;
18969
18970 «ˇstruct Row4;
18971 struct» Row5;
18972 «struct Row6;
18973 ˇ»
18974 struct Row8;
18975 struct Row10;"#},
18976 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18977 indoc! {r#"struct Row;
18978 struct Row2;
18979
18980 «ˇstruct Row4;
18981 struct» Row5;
18982 «struct Row6;
18983 ˇ»
18984 struct Row8;
18985 struct Row10;"#},
18986 base_text,
18987 &mut cx,
18988 );
18989
18990 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18991 assert_hunk_revert(
18992 indoc! {r#"struct Row;
18993 ˇstruct Row2;
18994
18995 struct Row4;
18996 struct Row5;
18997 struct Row6;
18998
18999 struct Row8;ˇ
19000 struct Row10;"#},
19001 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19002 indoc! {r#"struct Row;
19003 struct Row1;
19004 ˇstruct Row2;
19005
19006 struct Row4;
19007 struct Row5;
19008 struct Row6;
19009
19010 struct Row8;ˇ
19011 struct Row9;
19012 struct Row10;"#},
19013 base_text,
19014 &mut cx,
19015 );
19016 assert_hunk_revert(
19017 indoc! {r#"struct Row;
19018 struct Row2«ˇ;
19019 struct Row4;
19020 struct» Row5;
19021 «struct Row6;
19022
19023 struct Row8;ˇ»
19024 struct Row10;"#},
19025 vec![
19026 DiffHunkStatusKind::Deleted,
19027 DiffHunkStatusKind::Deleted,
19028 DiffHunkStatusKind::Deleted,
19029 ],
19030 indoc! {r#"struct Row;
19031 struct Row1;
19032 struct Row2«ˇ;
19033
19034 struct Row4;
19035 struct» Row5;
19036 «struct Row6;
19037
19038 struct Row8;ˇ»
19039 struct Row9;
19040 struct Row10;"#},
19041 base_text,
19042 &mut cx,
19043 );
19044}
19045
19046#[gpui::test]
19047async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19048 init_test(cx, |_| {});
19049
19050 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19051 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19052 let base_text_3 =
19053 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19054
19055 let text_1 = edit_first_char_of_every_line(base_text_1);
19056 let text_2 = edit_first_char_of_every_line(base_text_2);
19057 let text_3 = edit_first_char_of_every_line(base_text_3);
19058
19059 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19060 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19061 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19062
19063 let multibuffer = cx.new(|cx| {
19064 let mut multibuffer = MultiBuffer::new(ReadWrite);
19065 multibuffer.push_excerpts(
19066 buffer_1.clone(),
19067 [
19068 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19069 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19070 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19071 ],
19072 cx,
19073 );
19074 multibuffer.push_excerpts(
19075 buffer_2.clone(),
19076 [
19077 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19078 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19079 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19080 ],
19081 cx,
19082 );
19083 multibuffer.push_excerpts(
19084 buffer_3.clone(),
19085 [
19086 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19087 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19088 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19089 ],
19090 cx,
19091 );
19092 multibuffer
19093 });
19094
19095 let fs = FakeFs::new(cx.executor());
19096 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19097 let (editor, cx) = cx
19098 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19099 editor.update_in(cx, |editor, _window, cx| {
19100 for (buffer, diff_base) in [
19101 (buffer_1.clone(), base_text_1),
19102 (buffer_2.clone(), base_text_2),
19103 (buffer_3.clone(), base_text_3),
19104 ] {
19105 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19106 editor
19107 .buffer
19108 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19109 }
19110 });
19111 cx.executor().run_until_parked();
19112
19113 editor.update_in(cx, |editor, window, cx| {
19114 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}");
19115 editor.select_all(&SelectAll, window, cx);
19116 editor.git_restore(&Default::default(), window, cx);
19117 });
19118 cx.executor().run_until_parked();
19119
19120 // When all ranges are selected, all buffer hunks are reverted.
19121 editor.update(cx, |editor, cx| {
19122 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");
19123 });
19124 buffer_1.update(cx, |buffer, _| {
19125 assert_eq!(buffer.text(), base_text_1);
19126 });
19127 buffer_2.update(cx, |buffer, _| {
19128 assert_eq!(buffer.text(), base_text_2);
19129 });
19130 buffer_3.update(cx, |buffer, _| {
19131 assert_eq!(buffer.text(), base_text_3);
19132 });
19133
19134 editor.update_in(cx, |editor, window, cx| {
19135 editor.undo(&Default::default(), window, cx);
19136 });
19137
19138 editor.update_in(cx, |editor, window, cx| {
19139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19140 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19141 });
19142 editor.git_restore(&Default::default(), window, cx);
19143 });
19144
19145 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19146 // but not affect buffer_2 and its related excerpts.
19147 editor.update(cx, |editor, cx| {
19148 assert_eq!(
19149 editor.text(cx),
19150 "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}"
19151 );
19152 });
19153 buffer_1.update(cx, |buffer, _| {
19154 assert_eq!(buffer.text(), base_text_1);
19155 });
19156 buffer_2.update(cx, |buffer, _| {
19157 assert_eq!(
19158 buffer.text(),
19159 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19160 );
19161 });
19162 buffer_3.update(cx, |buffer, _| {
19163 assert_eq!(
19164 buffer.text(),
19165 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19166 );
19167 });
19168
19169 fn edit_first_char_of_every_line(text: &str) -> String {
19170 text.split('\n')
19171 .map(|line| format!("X{}", &line[1..]))
19172 .collect::<Vec<_>>()
19173 .join("\n")
19174 }
19175}
19176
19177#[gpui::test]
19178async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19179 init_test(cx, |_| {});
19180
19181 let cols = 4;
19182 let rows = 10;
19183 let sample_text_1 = sample_text(rows, cols, 'a');
19184 assert_eq!(
19185 sample_text_1,
19186 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19187 );
19188 let sample_text_2 = sample_text(rows, cols, 'l');
19189 assert_eq!(
19190 sample_text_2,
19191 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19192 );
19193 let sample_text_3 = sample_text(rows, cols, 'v');
19194 assert_eq!(
19195 sample_text_3,
19196 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19197 );
19198
19199 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19200 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19201 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19202
19203 let multi_buffer = cx.new(|cx| {
19204 let mut multibuffer = MultiBuffer::new(ReadWrite);
19205 multibuffer.push_excerpts(
19206 buffer_1.clone(),
19207 [
19208 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19209 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19210 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19211 ],
19212 cx,
19213 );
19214 multibuffer.push_excerpts(
19215 buffer_2.clone(),
19216 [
19217 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19218 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19219 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19220 ],
19221 cx,
19222 );
19223 multibuffer.push_excerpts(
19224 buffer_3.clone(),
19225 [
19226 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19227 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19228 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19229 ],
19230 cx,
19231 );
19232 multibuffer
19233 });
19234
19235 let fs = FakeFs::new(cx.executor());
19236 fs.insert_tree(
19237 "/a",
19238 json!({
19239 "main.rs": sample_text_1,
19240 "other.rs": sample_text_2,
19241 "lib.rs": sample_text_3,
19242 }),
19243 )
19244 .await;
19245 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19246 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19247 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19248 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19249 Editor::new(
19250 EditorMode::full(),
19251 multi_buffer,
19252 Some(project.clone()),
19253 window,
19254 cx,
19255 )
19256 });
19257 let multibuffer_item_id = workspace
19258 .update(cx, |workspace, window, cx| {
19259 assert!(
19260 workspace.active_item(cx).is_none(),
19261 "active item should be None before the first item is added"
19262 );
19263 workspace.add_item_to_active_pane(
19264 Box::new(multi_buffer_editor.clone()),
19265 None,
19266 true,
19267 window,
19268 cx,
19269 );
19270 let active_item = workspace
19271 .active_item(cx)
19272 .expect("should have an active item after adding the multi buffer");
19273 assert_eq!(
19274 active_item.buffer_kind(cx),
19275 ItemBufferKind::Multibuffer,
19276 "A multi buffer was expected to active after adding"
19277 );
19278 active_item.item_id()
19279 })
19280 .unwrap();
19281 cx.executor().run_until_parked();
19282
19283 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19284 editor.change_selections(
19285 SelectionEffects::scroll(Autoscroll::Next),
19286 window,
19287 cx,
19288 |s| s.select_ranges(Some(1..2)),
19289 );
19290 editor.open_excerpts(&OpenExcerpts, window, cx);
19291 });
19292 cx.executor().run_until_parked();
19293 let first_item_id = workspace
19294 .update(cx, |workspace, window, cx| {
19295 let active_item = workspace
19296 .active_item(cx)
19297 .expect("should have an active item after navigating into the 1st buffer");
19298 let first_item_id = active_item.item_id();
19299 assert_ne!(
19300 first_item_id, multibuffer_item_id,
19301 "Should navigate into the 1st buffer and activate it"
19302 );
19303 assert_eq!(
19304 active_item.buffer_kind(cx),
19305 ItemBufferKind::Singleton,
19306 "New active item should be a singleton buffer"
19307 );
19308 assert_eq!(
19309 active_item
19310 .act_as::<Editor>(cx)
19311 .expect("should have navigated into an editor for the 1st buffer")
19312 .read(cx)
19313 .text(cx),
19314 sample_text_1
19315 );
19316
19317 workspace
19318 .go_back(workspace.active_pane().downgrade(), window, cx)
19319 .detach_and_log_err(cx);
19320
19321 first_item_id
19322 })
19323 .unwrap();
19324 cx.executor().run_until_parked();
19325 workspace
19326 .update(cx, |workspace, _, cx| {
19327 let active_item = workspace
19328 .active_item(cx)
19329 .expect("should have an active item after navigating back");
19330 assert_eq!(
19331 active_item.item_id(),
19332 multibuffer_item_id,
19333 "Should navigate back to the multi buffer"
19334 );
19335 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19336 })
19337 .unwrap();
19338
19339 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19340 editor.change_selections(
19341 SelectionEffects::scroll(Autoscroll::Next),
19342 window,
19343 cx,
19344 |s| s.select_ranges(Some(39..40)),
19345 );
19346 editor.open_excerpts(&OpenExcerpts, window, cx);
19347 });
19348 cx.executor().run_until_parked();
19349 let second_item_id = workspace
19350 .update(cx, |workspace, window, cx| {
19351 let active_item = workspace
19352 .active_item(cx)
19353 .expect("should have an active item after navigating into the 2nd buffer");
19354 let second_item_id = active_item.item_id();
19355 assert_ne!(
19356 second_item_id, multibuffer_item_id,
19357 "Should navigate away from the multibuffer"
19358 );
19359 assert_ne!(
19360 second_item_id, first_item_id,
19361 "Should navigate into the 2nd buffer and activate it"
19362 );
19363 assert_eq!(
19364 active_item.buffer_kind(cx),
19365 ItemBufferKind::Singleton,
19366 "New active item should be a singleton buffer"
19367 );
19368 assert_eq!(
19369 active_item
19370 .act_as::<Editor>(cx)
19371 .expect("should have navigated into an editor")
19372 .read(cx)
19373 .text(cx),
19374 sample_text_2
19375 );
19376
19377 workspace
19378 .go_back(workspace.active_pane().downgrade(), window, cx)
19379 .detach_and_log_err(cx);
19380
19381 second_item_id
19382 })
19383 .unwrap();
19384 cx.executor().run_until_parked();
19385 workspace
19386 .update(cx, |workspace, _, cx| {
19387 let active_item = workspace
19388 .active_item(cx)
19389 .expect("should have an active item after navigating back from the 2nd buffer");
19390 assert_eq!(
19391 active_item.item_id(),
19392 multibuffer_item_id,
19393 "Should navigate back from the 2nd buffer to the multi buffer"
19394 );
19395 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19396 })
19397 .unwrap();
19398
19399 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19400 editor.change_selections(
19401 SelectionEffects::scroll(Autoscroll::Next),
19402 window,
19403 cx,
19404 |s| s.select_ranges(Some(70..70)),
19405 );
19406 editor.open_excerpts(&OpenExcerpts, window, cx);
19407 });
19408 cx.executor().run_until_parked();
19409 workspace
19410 .update(cx, |workspace, window, cx| {
19411 let active_item = workspace
19412 .active_item(cx)
19413 .expect("should have an active item after navigating into the 3rd buffer");
19414 let third_item_id = active_item.item_id();
19415 assert_ne!(
19416 third_item_id, multibuffer_item_id,
19417 "Should navigate into the 3rd buffer and activate it"
19418 );
19419 assert_ne!(third_item_id, first_item_id);
19420 assert_ne!(third_item_id, second_item_id);
19421 assert_eq!(
19422 active_item.buffer_kind(cx),
19423 ItemBufferKind::Singleton,
19424 "New active item should be a singleton buffer"
19425 );
19426 assert_eq!(
19427 active_item
19428 .act_as::<Editor>(cx)
19429 .expect("should have navigated into an editor")
19430 .read(cx)
19431 .text(cx),
19432 sample_text_3
19433 );
19434
19435 workspace
19436 .go_back(workspace.active_pane().downgrade(), window, cx)
19437 .detach_and_log_err(cx);
19438 })
19439 .unwrap();
19440 cx.executor().run_until_parked();
19441 workspace
19442 .update(cx, |workspace, _, cx| {
19443 let active_item = workspace
19444 .active_item(cx)
19445 .expect("should have an active item after navigating back from the 3rd buffer");
19446 assert_eq!(
19447 active_item.item_id(),
19448 multibuffer_item_id,
19449 "Should navigate back from the 3rd buffer to the multi buffer"
19450 );
19451 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19452 })
19453 .unwrap();
19454}
19455
19456#[gpui::test]
19457async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19458 init_test(cx, |_| {});
19459
19460 let mut cx = EditorTestContext::new(cx).await;
19461
19462 let diff_base = r#"
19463 use some::mod;
19464
19465 const A: u32 = 42;
19466
19467 fn main() {
19468 println!("hello");
19469
19470 println!("world");
19471 }
19472 "#
19473 .unindent();
19474
19475 cx.set_state(
19476 &r#"
19477 use some::modified;
19478
19479 ˇ
19480 fn main() {
19481 println!("hello there");
19482
19483 println!("around the");
19484 println!("world");
19485 }
19486 "#
19487 .unindent(),
19488 );
19489
19490 cx.set_head_text(&diff_base);
19491 executor.run_until_parked();
19492
19493 cx.update_editor(|editor, window, cx| {
19494 editor.go_to_next_hunk(&GoToHunk, window, cx);
19495 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19496 });
19497 executor.run_until_parked();
19498 cx.assert_state_with_diff(
19499 r#"
19500 use some::modified;
19501
19502
19503 fn main() {
19504 - println!("hello");
19505 + ˇ println!("hello there");
19506
19507 println!("around the");
19508 println!("world");
19509 }
19510 "#
19511 .unindent(),
19512 );
19513
19514 cx.update_editor(|editor, window, cx| {
19515 for _ in 0..2 {
19516 editor.go_to_next_hunk(&GoToHunk, window, cx);
19517 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19518 }
19519 });
19520 executor.run_until_parked();
19521 cx.assert_state_with_diff(
19522 r#"
19523 - use some::mod;
19524 + ˇuse some::modified;
19525
19526
19527 fn main() {
19528 - println!("hello");
19529 + println!("hello there");
19530
19531 + println!("around the");
19532 println!("world");
19533 }
19534 "#
19535 .unindent(),
19536 );
19537
19538 cx.update_editor(|editor, window, cx| {
19539 editor.go_to_next_hunk(&GoToHunk, window, cx);
19540 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19541 });
19542 executor.run_until_parked();
19543 cx.assert_state_with_diff(
19544 r#"
19545 - use some::mod;
19546 + use some::modified;
19547
19548 - const A: u32 = 42;
19549 ˇ
19550 fn main() {
19551 - println!("hello");
19552 + println!("hello there");
19553
19554 + println!("around the");
19555 println!("world");
19556 }
19557 "#
19558 .unindent(),
19559 );
19560
19561 cx.update_editor(|editor, window, cx| {
19562 editor.cancel(&Cancel, window, cx);
19563 });
19564
19565 cx.assert_state_with_diff(
19566 r#"
19567 use some::modified;
19568
19569 ˇ
19570 fn main() {
19571 println!("hello there");
19572
19573 println!("around the");
19574 println!("world");
19575 }
19576 "#
19577 .unindent(),
19578 );
19579}
19580
19581#[gpui::test]
19582async fn test_diff_base_change_with_expanded_diff_hunks(
19583 executor: BackgroundExecutor,
19584 cx: &mut TestAppContext,
19585) {
19586 init_test(cx, |_| {});
19587
19588 let mut cx = EditorTestContext::new(cx).await;
19589
19590 let diff_base = r#"
19591 use some::mod1;
19592 use some::mod2;
19593
19594 const A: u32 = 42;
19595 const B: u32 = 42;
19596 const C: u32 = 42;
19597
19598 fn main() {
19599 println!("hello");
19600
19601 println!("world");
19602 }
19603 "#
19604 .unindent();
19605
19606 cx.set_state(
19607 &r#"
19608 use some::mod2;
19609
19610 const A: u32 = 42;
19611 const C: u32 = 42;
19612
19613 fn main(ˇ) {
19614 //println!("hello");
19615
19616 println!("world");
19617 //
19618 //
19619 }
19620 "#
19621 .unindent(),
19622 );
19623
19624 cx.set_head_text(&diff_base);
19625 executor.run_until_parked();
19626
19627 cx.update_editor(|editor, window, cx| {
19628 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19629 });
19630 executor.run_until_parked();
19631 cx.assert_state_with_diff(
19632 r#"
19633 - use some::mod1;
19634 use some::mod2;
19635
19636 const A: u32 = 42;
19637 - const B: u32 = 42;
19638 const C: u32 = 42;
19639
19640 fn main(ˇ) {
19641 - println!("hello");
19642 + //println!("hello");
19643
19644 println!("world");
19645 + //
19646 + //
19647 }
19648 "#
19649 .unindent(),
19650 );
19651
19652 cx.set_head_text("new diff base!");
19653 executor.run_until_parked();
19654 cx.assert_state_with_diff(
19655 r#"
19656 - new diff base!
19657 + use some::mod2;
19658 +
19659 + const A: u32 = 42;
19660 + const C: u32 = 42;
19661 +
19662 + fn main(ˇ) {
19663 + //println!("hello");
19664 +
19665 + println!("world");
19666 + //
19667 + //
19668 + }
19669 "#
19670 .unindent(),
19671 );
19672}
19673
19674#[gpui::test]
19675async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19676 init_test(cx, |_| {});
19677
19678 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19679 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19680 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19681 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19682 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19683 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19684
19685 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19686 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19687 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19688
19689 let multi_buffer = cx.new(|cx| {
19690 let mut multibuffer = MultiBuffer::new(ReadWrite);
19691 multibuffer.push_excerpts(
19692 buffer_1.clone(),
19693 [
19694 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19695 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19696 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19697 ],
19698 cx,
19699 );
19700 multibuffer.push_excerpts(
19701 buffer_2.clone(),
19702 [
19703 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19704 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19705 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19706 ],
19707 cx,
19708 );
19709 multibuffer.push_excerpts(
19710 buffer_3.clone(),
19711 [
19712 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19713 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19714 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
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 for (buffer, diff_base) in [
19726 (buffer_1.clone(), file_1_old),
19727 (buffer_2.clone(), file_2_old),
19728 (buffer_3.clone(), file_3_old),
19729 ] {
19730 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19731 editor
19732 .buffer
19733 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19734 }
19735 })
19736 .unwrap();
19737
19738 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19739 cx.run_until_parked();
19740
19741 cx.assert_editor_state(
19742 &"
19743 ˇaaa
19744 ccc
19745 ddd
19746
19747 ggg
19748 hhh
19749
19750
19751 lll
19752 mmm
19753 NNN
19754
19755 qqq
19756 rrr
19757
19758 uuu
19759 111
19760 222
19761 333
19762
19763 666
19764 777
19765
19766 000
19767 !!!"
19768 .unindent(),
19769 );
19770
19771 cx.update_editor(|editor, window, cx| {
19772 editor.select_all(&SelectAll, window, cx);
19773 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19774 });
19775 cx.executor().run_until_parked();
19776
19777 cx.assert_state_with_diff(
19778 "
19779 «aaa
19780 - bbb
19781 ccc
19782 ddd
19783
19784 ggg
19785 hhh
19786
19787
19788 lll
19789 mmm
19790 - nnn
19791 + NNN
19792
19793 qqq
19794 rrr
19795
19796 uuu
19797 111
19798 222
19799 333
19800
19801 + 666
19802 777
19803
19804 000
19805 !!!ˇ»"
19806 .unindent(),
19807 );
19808}
19809
19810#[gpui::test]
19811async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19812 init_test(cx, |_| {});
19813
19814 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19815 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19816
19817 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19818 let multi_buffer = cx.new(|cx| {
19819 let mut multibuffer = MultiBuffer::new(ReadWrite);
19820 multibuffer.push_excerpts(
19821 buffer.clone(),
19822 [
19823 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19824 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19825 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19826 ],
19827 cx,
19828 );
19829 multibuffer
19830 });
19831
19832 let editor =
19833 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19834 editor
19835 .update(cx, |editor, _window, cx| {
19836 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19837 editor
19838 .buffer
19839 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19840 })
19841 .unwrap();
19842
19843 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19844 cx.run_until_parked();
19845
19846 cx.update_editor(|editor, window, cx| {
19847 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19848 });
19849 cx.executor().run_until_parked();
19850
19851 // When the start of a hunk coincides with the start of its excerpt,
19852 // the hunk is expanded. When the start of a hunk is earlier than
19853 // the start of its excerpt, the hunk is not expanded.
19854 cx.assert_state_with_diff(
19855 "
19856 ˇaaa
19857 - bbb
19858 + BBB
19859
19860 - ddd
19861 - eee
19862 + DDD
19863 + EEE
19864 fff
19865
19866 iii
19867 "
19868 .unindent(),
19869 );
19870}
19871
19872#[gpui::test]
19873async fn test_edits_around_expanded_insertion_hunks(
19874 executor: BackgroundExecutor,
19875 cx: &mut TestAppContext,
19876) {
19877 init_test(cx, |_| {});
19878
19879 let mut cx = EditorTestContext::new(cx).await;
19880
19881 let diff_base = r#"
19882 use some::mod1;
19883 use some::mod2;
19884
19885 const A: u32 = 42;
19886
19887 fn main() {
19888 println!("hello");
19889
19890 println!("world");
19891 }
19892 "#
19893 .unindent();
19894 executor.run_until_parked();
19895 cx.set_state(
19896 &r#"
19897 use some::mod1;
19898 use some::mod2;
19899
19900 const A: u32 = 42;
19901 const B: u32 = 42;
19902 const C: u32 = 42;
19903 ˇ
19904
19905 fn main() {
19906 println!("hello");
19907
19908 println!("world");
19909 }
19910 "#
19911 .unindent(),
19912 );
19913
19914 cx.set_head_text(&diff_base);
19915 executor.run_until_parked();
19916
19917 cx.update_editor(|editor, window, cx| {
19918 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19919 });
19920 executor.run_until_parked();
19921
19922 cx.assert_state_with_diff(
19923 r#"
19924 use some::mod1;
19925 use some::mod2;
19926
19927 const A: u32 = 42;
19928 + const B: u32 = 42;
19929 + const C: u32 = 42;
19930 + ˇ
19931
19932 fn main() {
19933 println!("hello");
19934
19935 println!("world");
19936 }
19937 "#
19938 .unindent(),
19939 );
19940
19941 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19942 executor.run_until_parked();
19943
19944 cx.assert_state_with_diff(
19945 r#"
19946 use some::mod1;
19947 use some::mod2;
19948
19949 const A: u32 = 42;
19950 + const B: u32 = 42;
19951 + const C: u32 = 42;
19952 + const D: u32 = 42;
19953 + ˇ
19954
19955 fn main() {
19956 println!("hello");
19957
19958 println!("world");
19959 }
19960 "#
19961 .unindent(),
19962 );
19963
19964 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19965 executor.run_until_parked();
19966
19967 cx.assert_state_with_diff(
19968 r#"
19969 use some::mod1;
19970 use some::mod2;
19971
19972 const A: u32 = 42;
19973 + const B: u32 = 42;
19974 + const C: u32 = 42;
19975 + const D: u32 = 42;
19976 + const E: u32 = 42;
19977 + ˇ
19978
19979 fn main() {
19980 println!("hello");
19981
19982 println!("world");
19983 }
19984 "#
19985 .unindent(),
19986 );
19987
19988 cx.update_editor(|editor, window, cx| {
19989 editor.delete_line(&DeleteLine, window, cx);
19990 });
19991 executor.run_until_parked();
19992
19993 cx.assert_state_with_diff(
19994 r#"
19995 use some::mod1;
19996 use some::mod2;
19997
19998 const A: u32 = 42;
19999 + const B: u32 = 42;
20000 + const C: u32 = 42;
20001 + const D: u32 = 42;
20002 + const E: u32 = 42;
20003 ˇ
20004 fn main() {
20005 println!("hello");
20006
20007 println!("world");
20008 }
20009 "#
20010 .unindent(),
20011 );
20012
20013 cx.update_editor(|editor, window, cx| {
20014 editor.move_up(&MoveUp, window, cx);
20015 editor.delete_line(&DeleteLine, window, cx);
20016 editor.move_up(&MoveUp, window, cx);
20017 editor.delete_line(&DeleteLine, window, cx);
20018 editor.move_up(&MoveUp, window, cx);
20019 editor.delete_line(&DeleteLine, window, cx);
20020 });
20021 executor.run_until_parked();
20022 cx.assert_state_with_diff(
20023 r#"
20024 use some::mod1;
20025 use some::mod2;
20026
20027 const A: u32 = 42;
20028 + const B: u32 = 42;
20029 ˇ
20030 fn main() {
20031 println!("hello");
20032
20033 println!("world");
20034 }
20035 "#
20036 .unindent(),
20037 );
20038
20039 cx.update_editor(|editor, window, cx| {
20040 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20041 editor.delete_line(&DeleteLine, window, cx);
20042 });
20043 executor.run_until_parked();
20044 cx.assert_state_with_diff(
20045 r#"
20046 ˇ
20047 fn main() {
20048 println!("hello");
20049
20050 println!("world");
20051 }
20052 "#
20053 .unindent(),
20054 );
20055}
20056
20057#[gpui::test]
20058async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20059 init_test(cx, |_| {});
20060
20061 let mut cx = EditorTestContext::new(cx).await;
20062 cx.set_head_text(indoc! { "
20063 one
20064 two
20065 three
20066 four
20067 five
20068 "
20069 });
20070 cx.set_state(indoc! { "
20071 one
20072 ˇthree
20073 five
20074 "});
20075 cx.run_until_parked();
20076 cx.update_editor(|editor, window, cx| {
20077 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20078 });
20079 cx.assert_state_with_diff(
20080 indoc! { "
20081 one
20082 - two
20083 ˇthree
20084 - four
20085 five
20086 "}
20087 .to_string(),
20088 );
20089 cx.update_editor(|editor, window, cx| {
20090 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20091 });
20092
20093 cx.assert_state_with_diff(
20094 indoc! { "
20095 one
20096 ˇthree
20097 five
20098 "}
20099 .to_string(),
20100 );
20101
20102 cx.set_state(indoc! { "
20103 one
20104 ˇTWO
20105 three
20106 four
20107 five
20108 "});
20109 cx.run_until_parked();
20110 cx.update_editor(|editor, window, cx| {
20111 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20112 });
20113
20114 cx.assert_state_with_diff(
20115 indoc! { "
20116 one
20117 - two
20118 + ˇTWO
20119 three
20120 four
20121 five
20122 "}
20123 .to_string(),
20124 );
20125 cx.update_editor(|editor, window, cx| {
20126 editor.move_up(&Default::default(), window, cx);
20127 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20128 });
20129 cx.assert_state_with_diff(
20130 indoc! { "
20131 one
20132 ˇTWO
20133 three
20134 four
20135 five
20136 "}
20137 .to_string(),
20138 );
20139}
20140
20141#[gpui::test]
20142async fn test_edits_around_expanded_deletion_hunks(
20143 executor: BackgroundExecutor,
20144 cx: &mut TestAppContext,
20145) {
20146 init_test(cx, |_| {});
20147
20148 let mut cx = EditorTestContext::new(cx).await;
20149
20150 let diff_base = r#"
20151 use some::mod1;
20152 use some::mod2;
20153
20154 const A: u32 = 42;
20155 const B: u32 = 42;
20156 const C: u32 = 42;
20157
20158
20159 fn main() {
20160 println!("hello");
20161
20162 println!("world");
20163 }
20164 "#
20165 .unindent();
20166 executor.run_until_parked();
20167 cx.set_state(
20168 &r#"
20169 use some::mod1;
20170 use some::mod2;
20171
20172 ˇconst B: u32 = 42;
20173 const C: u32 = 42;
20174
20175
20176 fn main() {
20177 println!("hello");
20178
20179 println!("world");
20180 }
20181 "#
20182 .unindent(),
20183 );
20184
20185 cx.set_head_text(&diff_base);
20186 executor.run_until_parked();
20187
20188 cx.update_editor(|editor, window, cx| {
20189 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20190 });
20191 executor.run_until_parked();
20192
20193 cx.assert_state_with_diff(
20194 r#"
20195 use some::mod1;
20196 use some::mod2;
20197
20198 - const A: u32 = 42;
20199 ˇconst B: u32 = 42;
20200 const C: u32 = 42;
20201
20202
20203 fn main() {
20204 println!("hello");
20205
20206 println!("world");
20207 }
20208 "#
20209 .unindent(),
20210 );
20211
20212 cx.update_editor(|editor, window, cx| {
20213 editor.delete_line(&DeleteLine, window, cx);
20214 });
20215 executor.run_until_parked();
20216 cx.assert_state_with_diff(
20217 r#"
20218 use some::mod1;
20219 use some::mod2;
20220
20221 - const A: u32 = 42;
20222 - const B: u32 = 42;
20223 ˇconst C: u32 = 42;
20224
20225
20226 fn main() {
20227 println!("hello");
20228
20229 println!("world");
20230 }
20231 "#
20232 .unindent(),
20233 );
20234
20235 cx.update_editor(|editor, window, cx| {
20236 editor.delete_line(&DeleteLine, window, cx);
20237 });
20238 executor.run_until_parked();
20239 cx.assert_state_with_diff(
20240 r#"
20241 use some::mod1;
20242 use some::mod2;
20243
20244 - const A: u32 = 42;
20245 - const B: u32 = 42;
20246 - const C: u32 = 42;
20247 ˇ
20248
20249 fn main() {
20250 println!("hello");
20251
20252 println!("world");
20253 }
20254 "#
20255 .unindent(),
20256 );
20257
20258 cx.update_editor(|editor, window, cx| {
20259 editor.handle_input("replacement", window, cx);
20260 });
20261 executor.run_until_parked();
20262 cx.assert_state_with_diff(
20263 r#"
20264 use some::mod1;
20265 use some::mod2;
20266
20267 - const A: u32 = 42;
20268 - const B: u32 = 42;
20269 - const C: u32 = 42;
20270 -
20271 + replacementˇ
20272
20273 fn main() {
20274 println!("hello");
20275
20276 println!("world");
20277 }
20278 "#
20279 .unindent(),
20280 );
20281}
20282
20283#[gpui::test]
20284async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20285 init_test(cx, |_| {});
20286
20287 let mut cx = EditorTestContext::new(cx).await;
20288
20289 let base_text = r#"
20290 one
20291 two
20292 three
20293 four
20294 five
20295 "#
20296 .unindent();
20297 executor.run_until_parked();
20298 cx.set_state(
20299 &r#"
20300 one
20301 two
20302 fˇour
20303 five
20304 "#
20305 .unindent(),
20306 );
20307
20308 cx.set_head_text(&base_text);
20309 executor.run_until_parked();
20310
20311 cx.update_editor(|editor, window, cx| {
20312 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20313 });
20314 executor.run_until_parked();
20315
20316 cx.assert_state_with_diff(
20317 r#"
20318 one
20319 two
20320 - three
20321 fˇour
20322 five
20323 "#
20324 .unindent(),
20325 );
20326
20327 cx.update_editor(|editor, window, cx| {
20328 editor.backspace(&Backspace, window, cx);
20329 editor.backspace(&Backspace, window, cx);
20330 });
20331 executor.run_until_parked();
20332 cx.assert_state_with_diff(
20333 r#"
20334 one
20335 two
20336 - threeˇ
20337 - four
20338 + our
20339 five
20340 "#
20341 .unindent(),
20342 );
20343}
20344
20345#[gpui::test]
20346async fn test_edit_after_expanded_modification_hunk(
20347 executor: BackgroundExecutor,
20348 cx: &mut TestAppContext,
20349) {
20350 init_test(cx, |_| {});
20351
20352 let mut cx = EditorTestContext::new(cx).await;
20353
20354 let diff_base = r#"
20355 use some::mod1;
20356 use some::mod2;
20357
20358 const A: u32 = 42;
20359 const B: u32 = 42;
20360 const C: u32 = 42;
20361 const D: u32 = 42;
20362
20363
20364 fn main() {
20365 println!("hello");
20366
20367 println!("world");
20368 }"#
20369 .unindent();
20370
20371 cx.set_state(
20372 &r#"
20373 use some::mod1;
20374 use some::mod2;
20375
20376 const A: u32 = 42;
20377 const B: u32 = 42;
20378 const C: u32 = 43ˇ
20379 const D: u32 = 42;
20380
20381
20382 fn main() {
20383 println!("hello");
20384
20385 println!("world");
20386 }"#
20387 .unindent(),
20388 );
20389
20390 cx.set_head_text(&diff_base);
20391 executor.run_until_parked();
20392 cx.update_editor(|editor, window, cx| {
20393 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20394 });
20395 executor.run_until_parked();
20396
20397 cx.assert_state_with_diff(
20398 r#"
20399 use some::mod1;
20400 use some::mod2;
20401
20402 const A: u32 = 42;
20403 const B: u32 = 42;
20404 - const C: u32 = 42;
20405 + const C: u32 = 43ˇ
20406 const D: u32 = 42;
20407
20408
20409 fn main() {
20410 println!("hello");
20411
20412 println!("world");
20413 }"#
20414 .unindent(),
20415 );
20416
20417 cx.update_editor(|editor, window, cx| {
20418 editor.handle_input("\nnew_line\n", window, cx);
20419 });
20420 executor.run_until_parked();
20421
20422 cx.assert_state_with_diff(
20423 r#"
20424 use some::mod1;
20425 use some::mod2;
20426
20427 const A: u32 = 42;
20428 const B: u32 = 42;
20429 - const C: u32 = 42;
20430 + const C: u32 = 43
20431 + new_line
20432 + ˇ
20433 const D: u32 = 42;
20434
20435
20436 fn main() {
20437 println!("hello");
20438
20439 println!("world");
20440 }"#
20441 .unindent(),
20442 );
20443}
20444
20445#[gpui::test]
20446async fn test_stage_and_unstage_added_file_hunk(
20447 executor: BackgroundExecutor,
20448 cx: &mut TestAppContext,
20449) {
20450 init_test(cx, |_| {});
20451
20452 let mut cx = EditorTestContext::new(cx).await;
20453 cx.update_editor(|editor, _, cx| {
20454 editor.set_expand_all_diff_hunks(cx);
20455 });
20456
20457 let working_copy = r#"
20458 ˇfn main() {
20459 println!("hello, world!");
20460 }
20461 "#
20462 .unindent();
20463
20464 cx.set_state(&working_copy);
20465 executor.run_until_parked();
20466
20467 cx.assert_state_with_diff(
20468 r#"
20469 + ˇfn main() {
20470 + println!("hello, world!");
20471 + }
20472 "#
20473 .unindent(),
20474 );
20475 cx.assert_index_text(None);
20476
20477 cx.update_editor(|editor, window, cx| {
20478 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20479 });
20480 executor.run_until_parked();
20481 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20482 cx.assert_state_with_diff(
20483 r#"
20484 + ˇfn main() {
20485 + println!("hello, world!");
20486 + }
20487 "#
20488 .unindent(),
20489 );
20490
20491 cx.update_editor(|editor, window, cx| {
20492 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20493 });
20494 executor.run_until_parked();
20495 cx.assert_index_text(None);
20496}
20497
20498async fn setup_indent_guides_editor(
20499 text: &str,
20500 cx: &mut TestAppContext,
20501) -> (BufferId, EditorTestContext) {
20502 init_test(cx, |_| {});
20503
20504 let mut cx = EditorTestContext::new(cx).await;
20505
20506 let buffer_id = cx.update_editor(|editor, window, cx| {
20507 editor.set_text(text, window, cx);
20508 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20509
20510 buffer_ids[0]
20511 });
20512
20513 (buffer_id, cx)
20514}
20515
20516fn assert_indent_guides(
20517 range: Range<u32>,
20518 expected: Vec<IndentGuide>,
20519 active_indices: Option<Vec<usize>>,
20520 cx: &mut EditorTestContext,
20521) {
20522 let indent_guides = cx.update_editor(|editor, window, cx| {
20523 let snapshot = editor.snapshot(window, cx).display_snapshot;
20524 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20525 editor,
20526 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20527 true,
20528 &snapshot,
20529 cx,
20530 );
20531
20532 indent_guides.sort_by(|a, b| {
20533 a.depth.cmp(&b.depth).then(
20534 a.start_row
20535 .cmp(&b.start_row)
20536 .then(a.end_row.cmp(&b.end_row)),
20537 )
20538 });
20539 indent_guides
20540 });
20541
20542 if let Some(expected) = active_indices {
20543 let active_indices = cx.update_editor(|editor, window, cx| {
20544 let snapshot = editor.snapshot(window, cx).display_snapshot;
20545 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20546 });
20547
20548 assert_eq!(
20549 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20550 expected,
20551 "Active indent guide indices do not match"
20552 );
20553 }
20554
20555 assert_eq!(indent_guides, expected, "Indent guides do not match");
20556}
20557
20558fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20559 IndentGuide {
20560 buffer_id,
20561 start_row: MultiBufferRow(start_row),
20562 end_row: MultiBufferRow(end_row),
20563 depth,
20564 tab_size: 4,
20565 settings: IndentGuideSettings {
20566 enabled: true,
20567 line_width: 1,
20568 active_line_width: 1,
20569 coloring: IndentGuideColoring::default(),
20570 background_coloring: IndentGuideBackgroundColoring::default(),
20571 },
20572 }
20573}
20574
20575#[gpui::test]
20576async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20577 let (buffer_id, mut cx) = setup_indent_guides_editor(
20578 &"
20579 fn main() {
20580 let a = 1;
20581 }"
20582 .unindent(),
20583 cx,
20584 )
20585 .await;
20586
20587 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20588}
20589
20590#[gpui::test]
20591async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20592 let (buffer_id, mut cx) = setup_indent_guides_editor(
20593 &"
20594 fn main() {
20595 let a = 1;
20596 let b = 2;
20597 }"
20598 .unindent(),
20599 cx,
20600 )
20601 .await;
20602
20603 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20604}
20605
20606#[gpui::test]
20607async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20608 let (buffer_id, mut cx) = setup_indent_guides_editor(
20609 &"
20610 fn main() {
20611 let a = 1;
20612 if a == 3 {
20613 let b = 2;
20614 } else {
20615 let c = 3;
20616 }
20617 }"
20618 .unindent(),
20619 cx,
20620 )
20621 .await;
20622
20623 assert_indent_guides(
20624 0..8,
20625 vec![
20626 indent_guide(buffer_id, 1, 6, 0),
20627 indent_guide(buffer_id, 3, 3, 1),
20628 indent_guide(buffer_id, 5, 5, 1),
20629 ],
20630 None,
20631 &mut cx,
20632 );
20633}
20634
20635#[gpui::test]
20636async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20637 let (buffer_id, mut cx) = setup_indent_guides_editor(
20638 &"
20639 fn main() {
20640 let a = 1;
20641 let b = 2;
20642 let c = 3;
20643 }"
20644 .unindent(),
20645 cx,
20646 )
20647 .await;
20648
20649 assert_indent_guides(
20650 0..5,
20651 vec![
20652 indent_guide(buffer_id, 1, 3, 0),
20653 indent_guide(buffer_id, 2, 2, 1),
20654 ],
20655 None,
20656 &mut cx,
20657 );
20658}
20659
20660#[gpui::test]
20661async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20662 let (buffer_id, mut cx) = setup_indent_guides_editor(
20663 &"
20664 fn main() {
20665 let a = 1;
20666
20667 let c = 3;
20668 }"
20669 .unindent(),
20670 cx,
20671 )
20672 .await;
20673
20674 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20675}
20676
20677#[gpui::test]
20678async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20679 let (buffer_id, mut cx) = setup_indent_guides_editor(
20680 &"
20681 fn main() {
20682 let a = 1;
20683
20684 let c = 3;
20685
20686 if a == 3 {
20687 let b = 2;
20688 } else {
20689 let c = 3;
20690 }
20691 }"
20692 .unindent(),
20693 cx,
20694 )
20695 .await;
20696
20697 assert_indent_guides(
20698 0..11,
20699 vec![
20700 indent_guide(buffer_id, 1, 9, 0),
20701 indent_guide(buffer_id, 6, 6, 1),
20702 indent_guide(buffer_id, 8, 8, 1),
20703 ],
20704 None,
20705 &mut cx,
20706 );
20707}
20708
20709#[gpui::test]
20710async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20711 let (buffer_id, mut cx) = setup_indent_guides_editor(
20712 &"
20713 fn main() {
20714 let a = 1;
20715
20716 let c = 3;
20717
20718 if a == 3 {
20719 let b = 2;
20720 } else {
20721 let c = 3;
20722 }
20723 }"
20724 .unindent(),
20725 cx,
20726 )
20727 .await;
20728
20729 assert_indent_guides(
20730 1..11,
20731 vec![
20732 indent_guide(buffer_id, 1, 9, 0),
20733 indent_guide(buffer_id, 6, 6, 1),
20734 indent_guide(buffer_id, 8, 8, 1),
20735 ],
20736 None,
20737 &mut cx,
20738 );
20739}
20740
20741#[gpui::test]
20742async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20743 let (buffer_id, mut cx) = setup_indent_guides_editor(
20744 &"
20745 fn main() {
20746 let a = 1;
20747
20748 let c = 3;
20749
20750 if a == 3 {
20751 let b = 2;
20752 } else {
20753 let c = 3;
20754 }
20755 }"
20756 .unindent(),
20757 cx,
20758 )
20759 .await;
20760
20761 assert_indent_guides(
20762 1..10,
20763 vec![
20764 indent_guide(buffer_id, 1, 9, 0),
20765 indent_guide(buffer_id, 6, 6, 1),
20766 indent_guide(buffer_id, 8, 8, 1),
20767 ],
20768 None,
20769 &mut cx,
20770 );
20771}
20772
20773#[gpui::test]
20774async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20775 let (buffer_id, mut cx) = setup_indent_guides_editor(
20776 &"
20777 fn main() {
20778 if a {
20779 b(
20780 c,
20781 d,
20782 )
20783 } else {
20784 e(
20785 f
20786 )
20787 }
20788 }"
20789 .unindent(),
20790 cx,
20791 )
20792 .await;
20793
20794 assert_indent_guides(
20795 0..11,
20796 vec![
20797 indent_guide(buffer_id, 1, 10, 0),
20798 indent_guide(buffer_id, 2, 5, 1),
20799 indent_guide(buffer_id, 7, 9, 1),
20800 indent_guide(buffer_id, 3, 4, 2),
20801 indent_guide(buffer_id, 8, 8, 2),
20802 ],
20803 None,
20804 &mut cx,
20805 );
20806
20807 cx.update_editor(|editor, window, cx| {
20808 editor.fold_at(MultiBufferRow(2), window, cx);
20809 assert_eq!(
20810 editor.display_text(cx),
20811 "
20812 fn main() {
20813 if a {
20814 b(⋯
20815 )
20816 } else {
20817 e(
20818 f
20819 )
20820 }
20821 }"
20822 .unindent()
20823 );
20824 });
20825
20826 assert_indent_guides(
20827 0..11,
20828 vec![
20829 indent_guide(buffer_id, 1, 10, 0),
20830 indent_guide(buffer_id, 2, 5, 1),
20831 indent_guide(buffer_id, 7, 9, 1),
20832 indent_guide(buffer_id, 8, 8, 2),
20833 ],
20834 None,
20835 &mut cx,
20836 );
20837}
20838
20839#[gpui::test]
20840async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20841 let (buffer_id, mut cx) = setup_indent_guides_editor(
20842 &"
20843 block1
20844 block2
20845 block3
20846 block4
20847 block2
20848 block1
20849 block1"
20850 .unindent(),
20851 cx,
20852 )
20853 .await;
20854
20855 assert_indent_guides(
20856 1..10,
20857 vec![
20858 indent_guide(buffer_id, 1, 4, 0),
20859 indent_guide(buffer_id, 2, 3, 1),
20860 indent_guide(buffer_id, 3, 3, 2),
20861 ],
20862 None,
20863 &mut cx,
20864 );
20865}
20866
20867#[gpui::test]
20868async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20869 let (buffer_id, mut cx) = setup_indent_guides_editor(
20870 &"
20871 block1
20872 block2
20873 block3
20874
20875 block1
20876 block1"
20877 .unindent(),
20878 cx,
20879 )
20880 .await;
20881
20882 assert_indent_guides(
20883 0..6,
20884 vec![
20885 indent_guide(buffer_id, 1, 2, 0),
20886 indent_guide(buffer_id, 2, 2, 1),
20887 ],
20888 None,
20889 &mut cx,
20890 );
20891}
20892
20893#[gpui::test]
20894async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20895 let (buffer_id, mut cx) = setup_indent_guides_editor(
20896 &"
20897 function component() {
20898 \treturn (
20899 \t\t\t
20900 \t\t<div>
20901 \t\t\t<abc></abc>
20902 \t\t</div>
20903 \t)
20904 }"
20905 .unindent(),
20906 cx,
20907 )
20908 .await;
20909
20910 assert_indent_guides(
20911 0..8,
20912 vec![
20913 indent_guide(buffer_id, 1, 6, 0),
20914 indent_guide(buffer_id, 2, 5, 1),
20915 indent_guide(buffer_id, 4, 4, 2),
20916 ],
20917 None,
20918 &mut cx,
20919 );
20920}
20921
20922#[gpui::test]
20923async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20924 let (buffer_id, mut cx) = setup_indent_guides_editor(
20925 &"
20926 function component() {
20927 \treturn (
20928 \t
20929 \t\t<div>
20930 \t\t\t<abc></abc>
20931 \t\t</div>
20932 \t)
20933 }"
20934 .unindent(),
20935 cx,
20936 )
20937 .await;
20938
20939 assert_indent_guides(
20940 0..8,
20941 vec![
20942 indent_guide(buffer_id, 1, 6, 0),
20943 indent_guide(buffer_id, 2, 5, 1),
20944 indent_guide(buffer_id, 4, 4, 2),
20945 ],
20946 None,
20947 &mut cx,
20948 );
20949}
20950
20951#[gpui::test]
20952async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20953 let (buffer_id, mut cx) = setup_indent_guides_editor(
20954 &"
20955 block1
20956
20957
20958
20959 block2
20960 "
20961 .unindent(),
20962 cx,
20963 )
20964 .await;
20965
20966 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20967}
20968
20969#[gpui::test]
20970async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20971 let (buffer_id, mut cx) = setup_indent_guides_editor(
20972 &"
20973 def a:
20974 \tb = 3
20975 \tif True:
20976 \t\tc = 4
20977 \t\td = 5
20978 \tprint(b)
20979 "
20980 .unindent(),
20981 cx,
20982 )
20983 .await;
20984
20985 assert_indent_guides(
20986 0..6,
20987 vec![
20988 indent_guide(buffer_id, 1, 5, 0),
20989 indent_guide(buffer_id, 3, 4, 1),
20990 ],
20991 None,
20992 &mut cx,
20993 );
20994}
20995
20996#[gpui::test]
20997async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20998 let (buffer_id, mut cx) = setup_indent_guides_editor(
20999 &"
21000 fn main() {
21001 let a = 1;
21002 }"
21003 .unindent(),
21004 cx,
21005 )
21006 .await;
21007
21008 cx.update_editor(|editor, window, cx| {
21009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21010 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21011 });
21012 });
21013
21014 assert_indent_guides(
21015 0..3,
21016 vec![indent_guide(buffer_id, 1, 1, 0)],
21017 Some(vec![0]),
21018 &mut cx,
21019 );
21020}
21021
21022#[gpui::test]
21023async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21024 let (buffer_id, mut cx) = setup_indent_guides_editor(
21025 &"
21026 fn main() {
21027 if 1 == 2 {
21028 let a = 1;
21029 }
21030 }"
21031 .unindent(),
21032 cx,
21033 )
21034 .await;
21035
21036 cx.update_editor(|editor, window, cx| {
21037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21038 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21039 });
21040 });
21041
21042 assert_indent_guides(
21043 0..4,
21044 vec![
21045 indent_guide(buffer_id, 1, 3, 0),
21046 indent_guide(buffer_id, 2, 2, 1),
21047 ],
21048 Some(vec![1]),
21049 &mut cx,
21050 );
21051
21052 cx.update_editor(|editor, window, cx| {
21053 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21054 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21055 });
21056 });
21057
21058 assert_indent_guides(
21059 0..4,
21060 vec![
21061 indent_guide(buffer_id, 1, 3, 0),
21062 indent_guide(buffer_id, 2, 2, 1),
21063 ],
21064 Some(vec![1]),
21065 &mut cx,
21066 );
21067
21068 cx.update_editor(|editor, window, cx| {
21069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21070 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21071 });
21072 });
21073
21074 assert_indent_guides(
21075 0..4,
21076 vec![
21077 indent_guide(buffer_id, 1, 3, 0),
21078 indent_guide(buffer_id, 2, 2, 1),
21079 ],
21080 Some(vec![0]),
21081 &mut cx,
21082 );
21083}
21084
21085#[gpui::test]
21086async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21087 let (buffer_id, mut cx) = setup_indent_guides_editor(
21088 &"
21089 fn main() {
21090 let a = 1;
21091
21092 let b = 2;
21093 }"
21094 .unindent(),
21095 cx,
21096 )
21097 .await;
21098
21099 cx.update_editor(|editor, window, cx| {
21100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21101 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21102 });
21103 });
21104
21105 assert_indent_guides(
21106 0..5,
21107 vec![indent_guide(buffer_id, 1, 3, 0)],
21108 Some(vec![0]),
21109 &mut cx,
21110 );
21111}
21112
21113#[gpui::test]
21114async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21115 let (buffer_id, mut cx) = setup_indent_guides_editor(
21116 &"
21117 def m:
21118 a = 1
21119 pass"
21120 .unindent(),
21121 cx,
21122 )
21123 .await;
21124
21125 cx.update_editor(|editor, window, cx| {
21126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21127 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21128 });
21129 });
21130
21131 assert_indent_guides(
21132 0..3,
21133 vec![indent_guide(buffer_id, 1, 2, 0)],
21134 Some(vec![0]),
21135 &mut cx,
21136 );
21137}
21138
21139#[gpui::test]
21140async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21141 init_test(cx, |_| {});
21142 let mut cx = EditorTestContext::new(cx).await;
21143 let text = indoc! {
21144 "
21145 impl A {
21146 fn b() {
21147 0;
21148 3;
21149 5;
21150 6;
21151 7;
21152 }
21153 }
21154 "
21155 };
21156 let base_text = indoc! {
21157 "
21158 impl A {
21159 fn b() {
21160 0;
21161 1;
21162 2;
21163 3;
21164 4;
21165 }
21166 fn c() {
21167 5;
21168 6;
21169 7;
21170 }
21171 }
21172 "
21173 };
21174
21175 cx.update_editor(|editor, window, cx| {
21176 editor.set_text(text, window, cx);
21177
21178 editor.buffer().update(cx, |multibuffer, cx| {
21179 let buffer = multibuffer.as_singleton().unwrap();
21180 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21181
21182 multibuffer.set_all_diff_hunks_expanded(cx);
21183 multibuffer.add_diff(diff, cx);
21184
21185 buffer.read(cx).remote_id()
21186 })
21187 });
21188 cx.run_until_parked();
21189
21190 cx.assert_state_with_diff(
21191 indoc! { "
21192 impl A {
21193 fn b() {
21194 0;
21195 - 1;
21196 - 2;
21197 3;
21198 - 4;
21199 - }
21200 - fn c() {
21201 5;
21202 6;
21203 7;
21204 }
21205 }
21206 ˇ"
21207 }
21208 .to_string(),
21209 );
21210
21211 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21212 editor
21213 .snapshot(window, cx)
21214 .buffer_snapshot()
21215 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21216 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21217 .collect::<Vec<_>>()
21218 });
21219 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21220 assert_eq!(
21221 actual_guides,
21222 vec![
21223 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21224 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21225 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21226 ]
21227 );
21228}
21229
21230#[gpui::test]
21231async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21232 init_test(cx, |_| {});
21233 let mut cx = EditorTestContext::new(cx).await;
21234
21235 let diff_base = r#"
21236 a
21237 b
21238 c
21239 "#
21240 .unindent();
21241
21242 cx.set_state(
21243 &r#"
21244 ˇA
21245 b
21246 C
21247 "#
21248 .unindent(),
21249 );
21250 cx.set_head_text(&diff_base);
21251 cx.update_editor(|editor, window, cx| {
21252 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21253 });
21254 executor.run_until_parked();
21255
21256 let both_hunks_expanded = r#"
21257 - a
21258 + ˇA
21259 b
21260 - c
21261 + C
21262 "#
21263 .unindent();
21264
21265 cx.assert_state_with_diff(both_hunks_expanded.clone());
21266
21267 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21268 let snapshot = editor.snapshot(window, cx);
21269 let hunks = editor
21270 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21271 .collect::<Vec<_>>();
21272 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21273 let buffer_id = hunks[0].buffer_id;
21274 hunks
21275 .into_iter()
21276 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21277 .collect::<Vec<_>>()
21278 });
21279 assert_eq!(hunk_ranges.len(), 2);
21280
21281 cx.update_editor(|editor, _, cx| {
21282 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21283 });
21284 executor.run_until_parked();
21285
21286 let second_hunk_expanded = r#"
21287 ˇA
21288 b
21289 - c
21290 + C
21291 "#
21292 .unindent();
21293
21294 cx.assert_state_with_diff(second_hunk_expanded);
21295
21296 cx.update_editor(|editor, _, cx| {
21297 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21298 });
21299 executor.run_until_parked();
21300
21301 cx.assert_state_with_diff(both_hunks_expanded.clone());
21302
21303 cx.update_editor(|editor, _, cx| {
21304 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21305 });
21306 executor.run_until_parked();
21307
21308 let first_hunk_expanded = r#"
21309 - a
21310 + ˇA
21311 b
21312 C
21313 "#
21314 .unindent();
21315
21316 cx.assert_state_with_diff(first_hunk_expanded);
21317
21318 cx.update_editor(|editor, _, cx| {
21319 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21320 });
21321 executor.run_until_parked();
21322
21323 cx.assert_state_with_diff(both_hunks_expanded);
21324
21325 cx.set_state(
21326 &r#"
21327 ˇA
21328 b
21329 "#
21330 .unindent(),
21331 );
21332 cx.run_until_parked();
21333
21334 // TODO this cursor position seems bad
21335 cx.assert_state_with_diff(
21336 r#"
21337 - ˇa
21338 + A
21339 b
21340 "#
21341 .unindent(),
21342 );
21343
21344 cx.update_editor(|editor, window, cx| {
21345 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21346 });
21347
21348 cx.assert_state_with_diff(
21349 r#"
21350 - ˇa
21351 + A
21352 b
21353 - c
21354 "#
21355 .unindent(),
21356 );
21357
21358 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21359 let snapshot = editor.snapshot(window, cx);
21360 let hunks = editor
21361 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21362 .collect::<Vec<_>>();
21363 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21364 let buffer_id = hunks[0].buffer_id;
21365 hunks
21366 .into_iter()
21367 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21368 .collect::<Vec<_>>()
21369 });
21370 assert_eq!(hunk_ranges.len(), 2);
21371
21372 cx.update_editor(|editor, _, cx| {
21373 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21374 });
21375 executor.run_until_parked();
21376
21377 cx.assert_state_with_diff(
21378 r#"
21379 - ˇa
21380 + A
21381 b
21382 "#
21383 .unindent(),
21384 );
21385}
21386
21387#[gpui::test]
21388async fn test_toggle_deletion_hunk_at_start_of_file(
21389 executor: BackgroundExecutor,
21390 cx: &mut TestAppContext,
21391) {
21392 init_test(cx, |_| {});
21393 let mut cx = EditorTestContext::new(cx).await;
21394
21395 let diff_base = r#"
21396 a
21397 b
21398 c
21399 "#
21400 .unindent();
21401
21402 cx.set_state(
21403 &r#"
21404 ˇb
21405 c
21406 "#
21407 .unindent(),
21408 );
21409 cx.set_head_text(&diff_base);
21410 cx.update_editor(|editor, window, cx| {
21411 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21412 });
21413 executor.run_until_parked();
21414
21415 let hunk_expanded = r#"
21416 - a
21417 ˇb
21418 c
21419 "#
21420 .unindent();
21421
21422 cx.assert_state_with_diff(hunk_expanded.clone());
21423
21424 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21425 let snapshot = editor.snapshot(window, cx);
21426 let hunks = editor
21427 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21428 .collect::<Vec<_>>();
21429 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21430 let buffer_id = hunks[0].buffer_id;
21431 hunks
21432 .into_iter()
21433 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21434 .collect::<Vec<_>>()
21435 });
21436 assert_eq!(hunk_ranges.len(), 1);
21437
21438 cx.update_editor(|editor, _, cx| {
21439 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21440 });
21441 executor.run_until_parked();
21442
21443 let hunk_collapsed = r#"
21444 ˇb
21445 c
21446 "#
21447 .unindent();
21448
21449 cx.assert_state_with_diff(hunk_collapsed);
21450
21451 cx.update_editor(|editor, _, cx| {
21452 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21453 });
21454 executor.run_until_parked();
21455
21456 cx.assert_state_with_diff(hunk_expanded);
21457}
21458
21459#[gpui::test]
21460async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21461 init_test(cx, |_| {});
21462
21463 let fs = FakeFs::new(cx.executor());
21464 fs.insert_tree(
21465 path!("/test"),
21466 json!({
21467 ".git": {},
21468 "file-1": "ONE\n",
21469 "file-2": "TWO\n",
21470 "file-3": "THREE\n",
21471 }),
21472 )
21473 .await;
21474
21475 fs.set_head_for_repo(
21476 path!("/test/.git").as_ref(),
21477 &[
21478 ("file-1", "one\n".into()),
21479 ("file-2", "two\n".into()),
21480 ("file-3", "three\n".into()),
21481 ],
21482 "deadbeef",
21483 );
21484
21485 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21486 let mut buffers = vec![];
21487 for i in 1..=3 {
21488 let buffer = project
21489 .update(cx, |project, cx| {
21490 let path = format!(path!("/test/file-{}"), i);
21491 project.open_local_buffer(path, cx)
21492 })
21493 .await
21494 .unwrap();
21495 buffers.push(buffer);
21496 }
21497
21498 let multibuffer = cx.new(|cx| {
21499 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21500 multibuffer.set_all_diff_hunks_expanded(cx);
21501 for buffer in &buffers {
21502 let snapshot = buffer.read(cx).snapshot();
21503 multibuffer.set_excerpts_for_path(
21504 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21505 buffer.clone(),
21506 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21507 2,
21508 cx,
21509 );
21510 }
21511 multibuffer
21512 });
21513
21514 let editor = cx.add_window(|window, cx| {
21515 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21516 });
21517 cx.run_until_parked();
21518
21519 let snapshot = editor
21520 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21521 .unwrap();
21522 let hunks = snapshot
21523 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21524 .map(|hunk| match hunk {
21525 DisplayDiffHunk::Unfolded {
21526 display_row_range, ..
21527 } => display_row_range,
21528 DisplayDiffHunk::Folded { .. } => unreachable!(),
21529 })
21530 .collect::<Vec<_>>();
21531 assert_eq!(
21532 hunks,
21533 [
21534 DisplayRow(2)..DisplayRow(4),
21535 DisplayRow(7)..DisplayRow(9),
21536 DisplayRow(12)..DisplayRow(14),
21537 ]
21538 );
21539}
21540
21541#[gpui::test]
21542async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21543 init_test(cx, |_| {});
21544
21545 let mut cx = EditorTestContext::new(cx).await;
21546 cx.set_head_text(indoc! { "
21547 one
21548 two
21549 three
21550 four
21551 five
21552 "
21553 });
21554 cx.set_index_text(indoc! { "
21555 one
21556 two
21557 three
21558 four
21559 five
21560 "
21561 });
21562 cx.set_state(indoc! {"
21563 one
21564 TWO
21565 ˇTHREE
21566 FOUR
21567 five
21568 "});
21569 cx.run_until_parked();
21570 cx.update_editor(|editor, window, cx| {
21571 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21572 });
21573 cx.run_until_parked();
21574 cx.assert_index_text(Some(indoc! {"
21575 one
21576 TWO
21577 THREE
21578 FOUR
21579 five
21580 "}));
21581 cx.set_state(indoc! { "
21582 one
21583 TWO
21584 ˇTHREE-HUNDRED
21585 FOUR
21586 five
21587 "});
21588 cx.run_until_parked();
21589 cx.update_editor(|editor, window, cx| {
21590 let snapshot = editor.snapshot(window, cx);
21591 let hunks = editor
21592 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21593 .collect::<Vec<_>>();
21594 assert_eq!(hunks.len(), 1);
21595 assert_eq!(
21596 hunks[0].status(),
21597 DiffHunkStatus {
21598 kind: DiffHunkStatusKind::Modified,
21599 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21600 }
21601 );
21602
21603 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21604 });
21605 cx.run_until_parked();
21606 cx.assert_index_text(Some(indoc! {"
21607 one
21608 TWO
21609 THREE-HUNDRED
21610 FOUR
21611 five
21612 "}));
21613}
21614
21615#[gpui::test]
21616fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21617 init_test(cx, |_| {});
21618
21619 let editor = cx.add_window(|window, cx| {
21620 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21621 build_editor(buffer, window, cx)
21622 });
21623
21624 let render_args = Arc::new(Mutex::new(None));
21625 let snapshot = editor
21626 .update(cx, |editor, window, cx| {
21627 let snapshot = editor.buffer().read(cx).snapshot(cx);
21628 let range =
21629 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21630
21631 struct RenderArgs {
21632 row: MultiBufferRow,
21633 folded: bool,
21634 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21635 }
21636
21637 let crease = Crease::inline(
21638 range,
21639 FoldPlaceholder::test(),
21640 {
21641 let toggle_callback = render_args.clone();
21642 move |row, folded, callback, _window, _cx| {
21643 *toggle_callback.lock() = Some(RenderArgs {
21644 row,
21645 folded,
21646 callback,
21647 });
21648 div()
21649 }
21650 },
21651 |_row, _folded, _window, _cx| div(),
21652 );
21653
21654 editor.insert_creases(Some(crease), cx);
21655 let snapshot = editor.snapshot(window, cx);
21656 let _div =
21657 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21658 snapshot
21659 })
21660 .unwrap();
21661
21662 let render_args = render_args.lock().take().unwrap();
21663 assert_eq!(render_args.row, MultiBufferRow(1));
21664 assert!(!render_args.folded);
21665 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21666
21667 cx.update_window(*editor, |_, window, cx| {
21668 (render_args.callback)(true, window, cx)
21669 })
21670 .unwrap();
21671 let snapshot = editor
21672 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21673 .unwrap();
21674 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21675
21676 cx.update_window(*editor, |_, window, cx| {
21677 (render_args.callback)(false, window, cx)
21678 })
21679 .unwrap();
21680 let snapshot = editor
21681 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21682 .unwrap();
21683 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21684}
21685
21686#[gpui::test]
21687async fn test_input_text(cx: &mut TestAppContext) {
21688 init_test(cx, |_| {});
21689 let mut cx = EditorTestContext::new(cx).await;
21690
21691 cx.set_state(
21692 &r#"ˇone
21693 two
21694
21695 three
21696 fourˇ
21697 five
21698
21699 siˇx"#
21700 .unindent(),
21701 );
21702
21703 cx.dispatch_action(HandleInput(String::new()));
21704 cx.assert_editor_state(
21705 &r#"ˇone
21706 two
21707
21708 three
21709 fourˇ
21710 five
21711
21712 siˇx"#
21713 .unindent(),
21714 );
21715
21716 cx.dispatch_action(HandleInput("AAAA".to_string()));
21717 cx.assert_editor_state(
21718 &r#"AAAAˇone
21719 two
21720
21721 three
21722 fourAAAAˇ
21723 five
21724
21725 siAAAAˇx"#
21726 .unindent(),
21727 );
21728}
21729
21730#[gpui::test]
21731async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21732 init_test(cx, |_| {});
21733
21734 let mut cx = EditorTestContext::new(cx).await;
21735 cx.set_state(
21736 r#"let foo = 1;
21737let foo = 2;
21738let foo = 3;
21739let fooˇ = 4;
21740let foo = 5;
21741let foo = 6;
21742let foo = 7;
21743let foo = 8;
21744let foo = 9;
21745let foo = 10;
21746let foo = 11;
21747let foo = 12;
21748let foo = 13;
21749let foo = 14;
21750let foo = 15;"#,
21751 );
21752
21753 cx.update_editor(|e, window, cx| {
21754 assert_eq!(
21755 e.next_scroll_position,
21756 NextScrollCursorCenterTopBottom::Center,
21757 "Default next scroll direction is center",
21758 );
21759
21760 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21761 assert_eq!(
21762 e.next_scroll_position,
21763 NextScrollCursorCenterTopBottom::Top,
21764 "After center, next scroll direction should be top",
21765 );
21766
21767 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21768 assert_eq!(
21769 e.next_scroll_position,
21770 NextScrollCursorCenterTopBottom::Bottom,
21771 "After top, next scroll direction should be bottom",
21772 );
21773
21774 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21775 assert_eq!(
21776 e.next_scroll_position,
21777 NextScrollCursorCenterTopBottom::Center,
21778 "After bottom, scrolling should start over",
21779 );
21780
21781 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21782 assert_eq!(
21783 e.next_scroll_position,
21784 NextScrollCursorCenterTopBottom::Top,
21785 "Scrolling continues if retriggered fast enough"
21786 );
21787 });
21788
21789 cx.executor()
21790 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21791 cx.executor().run_until_parked();
21792 cx.update_editor(|e, _, _| {
21793 assert_eq!(
21794 e.next_scroll_position,
21795 NextScrollCursorCenterTopBottom::Center,
21796 "If scrolling is not triggered fast enough, it should reset"
21797 );
21798 });
21799}
21800
21801#[gpui::test]
21802async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21803 init_test(cx, |_| {});
21804 let mut cx = EditorLspTestContext::new_rust(
21805 lsp::ServerCapabilities {
21806 definition_provider: Some(lsp::OneOf::Left(true)),
21807 references_provider: Some(lsp::OneOf::Left(true)),
21808 ..lsp::ServerCapabilities::default()
21809 },
21810 cx,
21811 )
21812 .await;
21813
21814 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21815 let go_to_definition = cx
21816 .lsp
21817 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21818 move |params, _| async move {
21819 if empty_go_to_definition {
21820 Ok(None)
21821 } else {
21822 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21823 uri: params.text_document_position_params.text_document.uri,
21824 range: lsp::Range::new(
21825 lsp::Position::new(4, 3),
21826 lsp::Position::new(4, 6),
21827 ),
21828 })))
21829 }
21830 },
21831 );
21832 let references = cx
21833 .lsp
21834 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21835 Ok(Some(vec![lsp::Location {
21836 uri: params.text_document_position.text_document.uri,
21837 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21838 }]))
21839 });
21840 (go_to_definition, references)
21841 };
21842
21843 cx.set_state(
21844 &r#"fn one() {
21845 let mut a = ˇtwo();
21846 }
21847
21848 fn two() {}"#
21849 .unindent(),
21850 );
21851 set_up_lsp_handlers(false, &mut cx);
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 definition");
21856 assert_eq!(
21857 navigated,
21858 Navigated::Yes,
21859 "Should have navigated to definition from the GetDefinition response"
21860 );
21861 cx.assert_editor_state(
21862 &r#"fn one() {
21863 let mut a = two();
21864 }
21865
21866 fn «twoˇ»() {}"#
21867 .unindent(),
21868 );
21869
21870 let editors = cx.update_workspace(|workspace, _, cx| {
21871 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21872 });
21873 cx.update_editor(|_, _, test_editor_cx| {
21874 assert_eq!(
21875 editors.len(),
21876 1,
21877 "Initially, only one, test, editor should be open in the workspace"
21878 );
21879 assert_eq!(
21880 test_editor_cx.entity(),
21881 editors.last().expect("Asserted len is 1").clone()
21882 );
21883 });
21884
21885 set_up_lsp_handlers(true, &mut cx);
21886 let navigated = cx
21887 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21888 .await
21889 .expect("Failed to navigate to lookup references");
21890 assert_eq!(
21891 navigated,
21892 Navigated::Yes,
21893 "Should have navigated to references as a fallback after empty GoToDefinition response"
21894 );
21895 // We should not change the selections in the existing file,
21896 // if opening another milti buffer with the references
21897 cx.assert_editor_state(
21898 &r#"fn one() {
21899 let mut a = two();
21900 }
21901
21902 fn «twoˇ»() {}"#
21903 .unindent(),
21904 );
21905 let editors = cx.update_workspace(|workspace, _, cx| {
21906 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21907 });
21908 cx.update_editor(|_, _, test_editor_cx| {
21909 assert_eq!(
21910 editors.len(),
21911 2,
21912 "After falling back to references search, we open a new editor with the results"
21913 );
21914 let references_fallback_text = editors
21915 .into_iter()
21916 .find(|new_editor| *new_editor != test_editor_cx.entity())
21917 .expect("Should have one non-test editor now")
21918 .read(test_editor_cx)
21919 .text(test_editor_cx);
21920 assert_eq!(
21921 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21922 "Should use the range from the references response and not the GoToDefinition one"
21923 );
21924 });
21925}
21926
21927#[gpui::test]
21928async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21929 init_test(cx, |_| {});
21930 cx.update(|cx| {
21931 let mut editor_settings = EditorSettings::get_global(cx).clone();
21932 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21933 EditorSettings::override_global(editor_settings, cx);
21934 });
21935 let mut cx = EditorLspTestContext::new_rust(
21936 lsp::ServerCapabilities {
21937 definition_provider: Some(lsp::OneOf::Left(true)),
21938 references_provider: Some(lsp::OneOf::Left(true)),
21939 ..lsp::ServerCapabilities::default()
21940 },
21941 cx,
21942 )
21943 .await;
21944 let original_state = r#"fn one() {
21945 let mut a = ˇtwo();
21946 }
21947
21948 fn two() {}"#
21949 .unindent();
21950 cx.set_state(&original_state);
21951
21952 let mut go_to_definition = cx
21953 .lsp
21954 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21955 move |_, _| async move { Ok(None) },
21956 );
21957 let _references = cx
21958 .lsp
21959 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21960 panic!("Should not call for references with no go to definition fallback")
21961 });
21962
21963 let navigated = cx
21964 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21965 .await
21966 .expect("Failed to navigate to lookup references");
21967 go_to_definition
21968 .next()
21969 .await
21970 .expect("Should have called the go_to_definition handler");
21971
21972 assert_eq!(
21973 navigated,
21974 Navigated::No,
21975 "Should have navigated to references as a fallback after empty GoToDefinition response"
21976 );
21977 cx.assert_editor_state(&original_state);
21978 let editors = cx.update_workspace(|workspace, _, cx| {
21979 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21980 });
21981 cx.update_editor(|_, _, _| {
21982 assert_eq!(
21983 editors.len(),
21984 1,
21985 "After unsuccessful fallback, no other editor should have been opened"
21986 );
21987 });
21988}
21989
21990#[gpui::test]
21991async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21992 init_test(cx, |_| {});
21993 let mut cx = EditorLspTestContext::new_rust(
21994 lsp::ServerCapabilities {
21995 references_provider: Some(lsp::OneOf::Left(true)),
21996 ..lsp::ServerCapabilities::default()
21997 },
21998 cx,
21999 )
22000 .await;
22001
22002 cx.set_state(
22003 &r#"
22004 fn one() {
22005 let mut a = two();
22006 }
22007
22008 fn ˇtwo() {}"#
22009 .unindent(),
22010 );
22011 cx.lsp
22012 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22013 Ok(Some(vec![
22014 lsp::Location {
22015 uri: params.text_document_position.text_document.uri.clone(),
22016 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22017 },
22018 lsp::Location {
22019 uri: params.text_document_position.text_document.uri,
22020 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22021 },
22022 ]))
22023 });
22024 let navigated = cx
22025 .update_editor(|editor, window, cx| {
22026 editor.find_all_references(&FindAllReferences, window, cx)
22027 })
22028 .unwrap()
22029 .await
22030 .expect("Failed to navigate to references");
22031 assert_eq!(
22032 navigated,
22033 Navigated::Yes,
22034 "Should have navigated to references from the FindAllReferences response"
22035 );
22036 cx.assert_editor_state(
22037 &r#"fn one() {
22038 let mut a = two();
22039 }
22040
22041 fn ˇtwo() {}"#
22042 .unindent(),
22043 );
22044
22045 let editors = cx.update_workspace(|workspace, _, cx| {
22046 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22047 });
22048 cx.update_editor(|_, _, _| {
22049 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22050 });
22051
22052 cx.set_state(
22053 &r#"fn one() {
22054 let mut a = ˇtwo();
22055 }
22056
22057 fn two() {}"#
22058 .unindent(),
22059 );
22060 let navigated = cx
22061 .update_editor(|editor, window, cx| {
22062 editor.find_all_references(&FindAllReferences, window, cx)
22063 })
22064 .unwrap()
22065 .await
22066 .expect("Failed to navigate to references");
22067 assert_eq!(
22068 navigated,
22069 Navigated::Yes,
22070 "Should have navigated to references from the FindAllReferences response"
22071 );
22072 cx.assert_editor_state(
22073 &r#"fn one() {
22074 let mut a = ˇtwo();
22075 }
22076
22077 fn two() {}"#
22078 .unindent(),
22079 );
22080 let editors = cx.update_workspace(|workspace, _, cx| {
22081 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22082 });
22083 cx.update_editor(|_, _, _| {
22084 assert_eq!(
22085 editors.len(),
22086 2,
22087 "should have re-used the previous multibuffer"
22088 );
22089 });
22090
22091 cx.set_state(
22092 &r#"fn one() {
22093 let mut a = ˇtwo();
22094 }
22095 fn three() {}
22096 fn two() {}"#
22097 .unindent(),
22098 );
22099 cx.lsp
22100 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22101 Ok(Some(vec![
22102 lsp::Location {
22103 uri: params.text_document_position.text_document.uri.clone(),
22104 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22105 },
22106 lsp::Location {
22107 uri: params.text_document_position.text_document.uri,
22108 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22109 },
22110 ]))
22111 });
22112 let navigated = cx
22113 .update_editor(|editor, window, cx| {
22114 editor.find_all_references(&FindAllReferences, window, cx)
22115 })
22116 .unwrap()
22117 .await
22118 .expect("Failed to navigate to references");
22119 assert_eq!(
22120 navigated,
22121 Navigated::Yes,
22122 "Should have navigated to references from the FindAllReferences response"
22123 );
22124 cx.assert_editor_state(
22125 &r#"fn one() {
22126 let mut a = ˇtwo();
22127 }
22128 fn three() {}
22129 fn two() {}"#
22130 .unindent(),
22131 );
22132 let editors = cx.update_workspace(|workspace, _, cx| {
22133 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22134 });
22135 cx.update_editor(|_, _, _| {
22136 assert_eq!(
22137 editors.len(),
22138 3,
22139 "should have used a new multibuffer as offsets changed"
22140 );
22141 });
22142}
22143#[gpui::test]
22144async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22145 init_test(cx, |_| {});
22146
22147 let language = Arc::new(Language::new(
22148 LanguageConfig::default(),
22149 Some(tree_sitter_rust::LANGUAGE.into()),
22150 ));
22151
22152 let text = r#"
22153 #[cfg(test)]
22154 mod tests() {
22155 #[test]
22156 fn runnable_1() {
22157 let a = 1;
22158 }
22159
22160 #[test]
22161 fn runnable_2() {
22162 let a = 1;
22163 let b = 2;
22164 }
22165 }
22166 "#
22167 .unindent();
22168
22169 let fs = FakeFs::new(cx.executor());
22170 fs.insert_file("/file.rs", Default::default()).await;
22171
22172 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22173 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22174 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22175 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22176 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22177
22178 let editor = cx.new_window_entity(|window, cx| {
22179 Editor::new(
22180 EditorMode::full(),
22181 multi_buffer,
22182 Some(project.clone()),
22183 window,
22184 cx,
22185 )
22186 });
22187
22188 editor.update_in(cx, |editor, window, cx| {
22189 let snapshot = editor.buffer().read(cx).snapshot(cx);
22190 editor.tasks.insert(
22191 (buffer.read(cx).remote_id(), 3),
22192 RunnableTasks {
22193 templates: vec![],
22194 offset: snapshot.anchor_before(43),
22195 column: 0,
22196 extra_variables: HashMap::default(),
22197 context_range: BufferOffset(43)..BufferOffset(85),
22198 },
22199 );
22200 editor.tasks.insert(
22201 (buffer.read(cx).remote_id(), 8),
22202 RunnableTasks {
22203 templates: vec![],
22204 offset: snapshot.anchor_before(86),
22205 column: 0,
22206 extra_variables: HashMap::default(),
22207 context_range: BufferOffset(86)..BufferOffset(191),
22208 },
22209 );
22210
22211 // Test finding task when cursor is inside function body
22212 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22213 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22214 });
22215 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22216 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22217
22218 // Test finding task when cursor is on function name
22219 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22220 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22221 });
22222 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22223 assert_eq!(row, 8, "Should find task when cursor is on function name");
22224 });
22225}
22226
22227#[gpui::test]
22228async fn test_folding_buffers(cx: &mut TestAppContext) {
22229 init_test(cx, |_| {});
22230
22231 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22232 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22233 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22234
22235 let fs = FakeFs::new(cx.executor());
22236 fs.insert_tree(
22237 path!("/a"),
22238 json!({
22239 "first.rs": sample_text_1,
22240 "second.rs": sample_text_2,
22241 "third.rs": sample_text_3,
22242 }),
22243 )
22244 .await;
22245 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22246 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22247 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22248 let worktree = project.update(cx, |project, cx| {
22249 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22250 assert_eq!(worktrees.len(), 1);
22251 worktrees.pop().unwrap()
22252 });
22253 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22254
22255 let buffer_1 = project
22256 .update(cx, |project, cx| {
22257 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22258 })
22259 .await
22260 .unwrap();
22261 let buffer_2 = project
22262 .update(cx, |project, cx| {
22263 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22264 })
22265 .await
22266 .unwrap();
22267 let buffer_3 = project
22268 .update(cx, |project, cx| {
22269 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22270 })
22271 .await
22272 .unwrap();
22273
22274 let multi_buffer = cx.new(|cx| {
22275 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22276 multi_buffer.push_excerpts(
22277 buffer_1.clone(),
22278 [
22279 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22280 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22281 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22282 ],
22283 cx,
22284 );
22285 multi_buffer.push_excerpts(
22286 buffer_2.clone(),
22287 [
22288 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22289 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22290 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22291 ],
22292 cx,
22293 );
22294 multi_buffer.push_excerpts(
22295 buffer_3.clone(),
22296 [
22297 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22298 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22299 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22300 ],
22301 cx,
22302 );
22303 multi_buffer
22304 });
22305 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22306 Editor::new(
22307 EditorMode::full(),
22308 multi_buffer.clone(),
22309 Some(project.clone()),
22310 window,
22311 cx,
22312 )
22313 });
22314
22315 assert_eq!(
22316 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22317 "\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",
22318 );
22319
22320 multi_buffer_editor.update(cx, |editor, cx| {
22321 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22322 });
22323 assert_eq!(
22324 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22325 "\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",
22326 "After folding the first buffer, its text should not be displayed"
22327 );
22328
22329 multi_buffer_editor.update(cx, |editor, cx| {
22330 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22331 });
22332 assert_eq!(
22333 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22334 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22335 "After folding the second buffer, its text should not be displayed"
22336 );
22337
22338 multi_buffer_editor.update(cx, |editor, cx| {
22339 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22340 });
22341 assert_eq!(
22342 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22343 "\n\n\n\n\n",
22344 "After folding the third buffer, its text should not be displayed"
22345 );
22346
22347 // Emulate selection inside the fold logic, that should work
22348 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22349 editor
22350 .snapshot(window, cx)
22351 .next_line_boundary(Point::new(0, 4));
22352 });
22353
22354 multi_buffer_editor.update(cx, |editor, cx| {
22355 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22356 });
22357 assert_eq!(
22358 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22359 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22360 "After unfolding the second buffer, its text should be displayed"
22361 );
22362
22363 // Typing inside of buffer 1 causes that buffer to be unfolded.
22364 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22365 assert_eq!(
22366 multi_buffer
22367 .read(cx)
22368 .snapshot(cx)
22369 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22370 .collect::<String>(),
22371 "bbbb"
22372 );
22373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22374 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22375 });
22376 editor.handle_input("B", window, cx);
22377 });
22378
22379 assert_eq!(
22380 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22381 "\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22382 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22383 );
22384
22385 multi_buffer_editor.update(cx, |editor, cx| {
22386 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22387 });
22388 assert_eq!(
22389 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22390 "\n\naaaa\nBbbbb\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",
22391 "After unfolding the all buffers, all original text should be displayed"
22392 );
22393}
22394
22395#[gpui::test]
22396async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22397 init_test(cx, |_| {});
22398
22399 let sample_text_1 = "1111\n2222\n3333".to_string();
22400 let sample_text_2 = "4444\n5555\n6666".to_string();
22401 let sample_text_3 = "7777\n8888\n9999".to_string();
22402
22403 let fs = FakeFs::new(cx.executor());
22404 fs.insert_tree(
22405 path!("/a"),
22406 json!({
22407 "first.rs": sample_text_1,
22408 "second.rs": sample_text_2,
22409 "third.rs": sample_text_3,
22410 }),
22411 )
22412 .await;
22413 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22414 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22415 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22416 let worktree = project.update(cx, |project, cx| {
22417 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22418 assert_eq!(worktrees.len(), 1);
22419 worktrees.pop().unwrap()
22420 });
22421 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22422
22423 let buffer_1 = project
22424 .update(cx, |project, cx| {
22425 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22426 })
22427 .await
22428 .unwrap();
22429 let buffer_2 = project
22430 .update(cx, |project, cx| {
22431 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22432 })
22433 .await
22434 .unwrap();
22435 let buffer_3 = project
22436 .update(cx, |project, cx| {
22437 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22438 })
22439 .await
22440 .unwrap();
22441
22442 let multi_buffer = cx.new(|cx| {
22443 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22444 multi_buffer.push_excerpts(
22445 buffer_1.clone(),
22446 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22447 cx,
22448 );
22449 multi_buffer.push_excerpts(
22450 buffer_2.clone(),
22451 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22452 cx,
22453 );
22454 multi_buffer.push_excerpts(
22455 buffer_3.clone(),
22456 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22457 cx,
22458 );
22459 multi_buffer
22460 });
22461
22462 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22463 Editor::new(
22464 EditorMode::full(),
22465 multi_buffer,
22466 Some(project.clone()),
22467 window,
22468 cx,
22469 )
22470 });
22471
22472 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22473 assert_eq!(
22474 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22475 full_text,
22476 );
22477
22478 multi_buffer_editor.update(cx, |editor, cx| {
22479 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22480 });
22481 assert_eq!(
22482 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22483 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22484 "After folding the first buffer, its text should not be displayed"
22485 );
22486
22487 multi_buffer_editor.update(cx, |editor, cx| {
22488 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22489 });
22490
22491 assert_eq!(
22492 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22493 "\n\n\n\n\n\n7777\n8888\n9999",
22494 "After folding the second buffer, its text should not be displayed"
22495 );
22496
22497 multi_buffer_editor.update(cx, |editor, cx| {
22498 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22499 });
22500 assert_eq!(
22501 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22502 "\n\n\n\n\n",
22503 "After folding the third buffer, its text should not be displayed"
22504 );
22505
22506 multi_buffer_editor.update(cx, |editor, cx| {
22507 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22508 });
22509 assert_eq!(
22510 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22511 "\n\n\n\n4444\n5555\n6666\n\n",
22512 "After unfolding the second buffer, its text should be displayed"
22513 );
22514
22515 multi_buffer_editor.update(cx, |editor, cx| {
22516 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22517 });
22518 assert_eq!(
22519 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22520 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22521 "After unfolding the first buffer, its text should be displayed"
22522 );
22523
22524 multi_buffer_editor.update(cx, |editor, cx| {
22525 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22526 });
22527 assert_eq!(
22528 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22529 full_text,
22530 "After unfolding all buffers, all original text should be displayed"
22531 );
22532}
22533
22534#[gpui::test]
22535async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22536 init_test(cx, |_| {});
22537
22538 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22539
22540 let fs = FakeFs::new(cx.executor());
22541 fs.insert_tree(
22542 path!("/a"),
22543 json!({
22544 "main.rs": sample_text,
22545 }),
22546 )
22547 .await;
22548 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22549 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22550 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22551 let worktree = project.update(cx, |project, cx| {
22552 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22553 assert_eq!(worktrees.len(), 1);
22554 worktrees.pop().unwrap()
22555 });
22556 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22557
22558 let buffer_1 = project
22559 .update(cx, |project, cx| {
22560 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22561 })
22562 .await
22563 .unwrap();
22564
22565 let multi_buffer = cx.new(|cx| {
22566 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22567 multi_buffer.push_excerpts(
22568 buffer_1.clone(),
22569 [ExcerptRange::new(
22570 Point::new(0, 0)
22571 ..Point::new(
22572 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22573 0,
22574 ),
22575 )],
22576 cx,
22577 );
22578 multi_buffer
22579 });
22580 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22581 Editor::new(
22582 EditorMode::full(),
22583 multi_buffer,
22584 Some(project.clone()),
22585 window,
22586 cx,
22587 )
22588 });
22589
22590 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22591 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22592 enum TestHighlight {}
22593 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22594 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22595 editor.highlight_text::<TestHighlight>(
22596 vec![highlight_range.clone()],
22597 HighlightStyle::color(Hsla::green()),
22598 cx,
22599 );
22600 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22601 s.select_ranges(Some(highlight_range))
22602 });
22603 });
22604
22605 let full_text = format!("\n\n{sample_text}");
22606 assert_eq!(
22607 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22608 full_text,
22609 );
22610}
22611
22612#[gpui::test]
22613async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22614 init_test(cx, |_| {});
22615 cx.update(|cx| {
22616 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22617 "keymaps/default-linux.json",
22618 cx,
22619 )
22620 .unwrap();
22621 cx.bind_keys(default_key_bindings);
22622 });
22623
22624 let (editor, cx) = cx.add_window_view(|window, cx| {
22625 let multi_buffer = MultiBuffer::build_multi(
22626 [
22627 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22628 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22629 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22630 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22631 ],
22632 cx,
22633 );
22634 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22635
22636 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22637 // fold all but the second buffer, so that we test navigating between two
22638 // adjacent folded buffers, as well as folded buffers at the start and
22639 // end the multibuffer
22640 editor.fold_buffer(buffer_ids[0], cx);
22641 editor.fold_buffer(buffer_ids[2], cx);
22642 editor.fold_buffer(buffer_ids[3], cx);
22643
22644 editor
22645 });
22646 cx.simulate_resize(size(px(1000.), px(1000.)));
22647
22648 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22649 cx.assert_excerpts_with_selections(indoc! {"
22650 [EXCERPT]
22651 ˇ[FOLDED]
22652 [EXCERPT]
22653 a1
22654 b1
22655 [EXCERPT]
22656 [FOLDED]
22657 [EXCERPT]
22658 [FOLDED]
22659 "
22660 });
22661 cx.simulate_keystroke("down");
22662 cx.assert_excerpts_with_selections(indoc! {"
22663 [EXCERPT]
22664 [FOLDED]
22665 [EXCERPT]
22666 ˇa1
22667 b1
22668 [EXCERPT]
22669 [FOLDED]
22670 [EXCERPT]
22671 [FOLDED]
22672 "
22673 });
22674 cx.simulate_keystroke("down");
22675 cx.assert_excerpts_with_selections(indoc! {"
22676 [EXCERPT]
22677 [FOLDED]
22678 [EXCERPT]
22679 a1
22680 ˇb1
22681 [EXCERPT]
22682 [FOLDED]
22683 [EXCERPT]
22684 [FOLDED]
22685 "
22686 });
22687 cx.simulate_keystroke("down");
22688 cx.assert_excerpts_with_selections(indoc! {"
22689 [EXCERPT]
22690 [FOLDED]
22691 [EXCERPT]
22692 a1
22693 b1
22694 ˇ[EXCERPT]
22695 [FOLDED]
22696 [EXCERPT]
22697 [FOLDED]
22698 "
22699 });
22700 cx.simulate_keystroke("down");
22701 cx.assert_excerpts_with_selections(indoc! {"
22702 [EXCERPT]
22703 [FOLDED]
22704 [EXCERPT]
22705 a1
22706 b1
22707 [EXCERPT]
22708 ˇ[FOLDED]
22709 [EXCERPT]
22710 [FOLDED]
22711 "
22712 });
22713 for _ in 0..5 {
22714 cx.simulate_keystroke("down");
22715 cx.assert_excerpts_with_selections(indoc! {"
22716 [EXCERPT]
22717 [FOLDED]
22718 [EXCERPT]
22719 a1
22720 b1
22721 [EXCERPT]
22722 [FOLDED]
22723 [EXCERPT]
22724 ˇ[FOLDED]
22725 "
22726 });
22727 }
22728
22729 cx.simulate_keystroke("up");
22730 cx.assert_excerpts_with_selections(indoc! {"
22731 [EXCERPT]
22732 [FOLDED]
22733 [EXCERPT]
22734 a1
22735 b1
22736 [EXCERPT]
22737 ˇ[FOLDED]
22738 [EXCERPT]
22739 [FOLDED]
22740 "
22741 });
22742 cx.simulate_keystroke("up");
22743 cx.assert_excerpts_with_selections(indoc! {"
22744 [EXCERPT]
22745 [FOLDED]
22746 [EXCERPT]
22747 a1
22748 b1
22749 ˇ[EXCERPT]
22750 [FOLDED]
22751 [EXCERPT]
22752 [FOLDED]
22753 "
22754 });
22755 cx.simulate_keystroke("up");
22756 cx.assert_excerpts_with_selections(indoc! {"
22757 [EXCERPT]
22758 [FOLDED]
22759 [EXCERPT]
22760 a1
22761 ˇb1
22762 [EXCERPT]
22763 [FOLDED]
22764 [EXCERPT]
22765 [FOLDED]
22766 "
22767 });
22768 cx.simulate_keystroke("up");
22769 cx.assert_excerpts_with_selections(indoc! {"
22770 [EXCERPT]
22771 [FOLDED]
22772 [EXCERPT]
22773 ˇa1
22774 b1
22775 [EXCERPT]
22776 [FOLDED]
22777 [EXCERPT]
22778 [FOLDED]
22779 "
22780 });
22781 for _ in 0..5 {
22782 cx.simulate_keystroke("up");
22783 cx.assert_excerpts_with_selections(indoc! {"
22784 [EXCERPT]
22785 ˇ[FOLDED]
22786 [EXCERPT]
22787 a1
22788 b1
22789 [EXCERPT]
22790 [FOLDED]
22791 [EXCERPT]
22792 [FOLDED]
22793 "
22794 });
22795 }
22796}
22797
22798#[gpui::test]
22799async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22800 init_test(cx, |_| {});
22801
22802 // Simple insertion
22803 assert_highlighted_edits(
22804 "Hello, world!",
22805 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22806 true,
22807 cx,
22808 |highlighted_edits, cx| {
22809 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22810 assert_eq!(highlighted_edits.highlights.len(), 1);
22811 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22812 assert_eq!(
22813 highlighted_edits.highlights[0].1.background_color,
22814 Some(cx.theme().status().created_background)
22815 );
22816 },
22817 )
22818 .await;
22819
22820 // Replacement
22821 assert_highlighted_edits(
22822 "This is a test.",
22823 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22824 false,
22825 cx,
22826 |highlighted_edits, cx| {
22827 assert_eq!(highlighted_edits.text, "That is a test.");
22828 assert_eq!(highlighted_edits.highlights.len(), 1);
22829 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22830 assert_eq!(
22831 highlighted_edits.highlights[0].1.background_color,
22832 Some(cx.theme().status().created_background)
22833 );
22834 },
22835 )
22836 .await;
22837
22838 // Multiple edits
22839 assert_highlighted_edits(
22840 "Hello, world!",
22841 vec![
22842 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22843 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22844 ],
22845 false,
22846 cx,
22847 |highlighted_edits, cx| {
22848 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22849 assert_eq!(highlighted_edits.highlights.len(), 2);
22850 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22851 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22852 assert_eq!(
22853 highlighted_edits.highlights[0].1.background_color,
22854 Some(cx.theme().status().created_background)
22855 );
22856 assert_eq!(
22857 highlighted_edits.highlights[1].1.background_color,
22858 Some(cx.theme().status().created_background)
22859 );
22860 },
22861 )
22862 .await;
22863
22864 // Multiple lines with edits
22865 assert_highlighted_edits(
22866 "First line\nSecond line\nThird line\nFourth line",
22867 vec![
22868 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22869 (
22870 Point::new(2, 0)..Point::new(2, 10),
22871 "New third line".to_string(),
22872 ),
22873 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22874 ],
22875 false,
22876 cx,
22877 |highlighted_edits, cx| {
22878 assert_eq!(
22879 highlighted_edits.text,
22880 "Second modified\nNew third line\nFourth updated line"
22881 );
22882 assert_eq!(highlighted_edits.highlights.len(), 3);
22883 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22884 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22885 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22886 for highlight in &highlighted_edits.highlights {
22887 assert_eq!(
22888 highlight.1.background_color,
22889 Some(cx.theme().status().created_background)
22890 );
22891 }
22892 },
22893 )
22894 .await;
22895}
22896
22897#[gpui::test]
22898async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22899 init_test(cx, |_| {});
22900
22901 // Deletion
22902 assert_highlighted_edits(
22903 "Hello, world!",
22904 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22905 true,
22906 cx,
22907 |highlighted_edits, cx| {
22908 assert_eq!(highlighted_edits.text, "Hello, world!");
22909 assert_eq!(highlighted_edits.highlights.len(), 1);
22910 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22911 assert_eq!(
22912 highlighted_edits.highlights[0].1.background_color,
22913 Some(cx.theme().status().deleted_background)
22914 );
22915 },
22916 )
22917 .await;
22918
22919 // Insertion
22920 assert_highlighted_edits(
22921 "Hello, world!",
22922 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22923 true,
22924 cx,
22925 |highlighted_edits, cx| {
22926 assert_eq!(highlighted_edits.highlights.len(), 1);
22927 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22928 assert_eq!(
22929 highlighted_edits.highlights[0].1.background_color,
22930 Some(cx.theme().status().created_background)
22931 );
22932 },
22933 )
22934 .await;
22935}
22936
22937async fn assert_highlighted_edits(
22938 text: &str,
22939 edits: Vec<(Range<Point>, String)>,
22940 include_deletions: bool,
22941 cx: &mut TestAppContext,
22942 assertion_fn: impl Fn(HighlightedText, &App),
22943) {
22944 let window = cx.add_window(|window, cx| {
22945 let buffer = MultiBuffer::build_simple(text, cx);
22946 Editor::new(EditorMode::full(), buffer, None, window, cx)
22947 });
22948 let cx = &mut VisualTestContext::from_window(*window, cx);
22949
22950 let (buffer, snapshot) = window
22951 .update(cx, |editor, _window, cx| {
22952 (
22953 editor.buffer().clone(),
22954 editor.buffer().read(cx).snapshot(cx),
22955 )
22956 })
22957 .unwrap();
22958
22959 let edits = edits
22960 .into_iter()
22961 .map(|(range, edit)| {
22962 (
22963 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22964 edit,
22965 )
22966 })
22967 .collect::<Vec<_>>();
22968
22969 let text_anchor_edits = edits
22970 .clone()
22971 .into_iter()
22972 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
22973 .collect::<Vec<_>>();
22974
22975 let edit_preview = window
22976 .update(cx, |_, _window, cx| {
22977 buffer
22978 .read(cx)
22979 .as_singleton()
22980 .unwrap()
22981 .read(cx)
22982 .preview_edits(text_anchor_edits.into(), cx)
22983 })
22984 .unwrap()
22985 .await;
22986
22987 cx.update(|_window, cx| {
22988 let highlighted_edits = edit_prediction_edit_text(
22989 snapshot.as_singleton().unwrap().2,
22990 &edits,
22991 &edit_preview,
22992 include_deletions,
22993 cx,
22994 );
22995 assertion_fn(highlighted_edits, cx)
22996 });
22997}
22998
22999#[track_caller]
23000fn assert_breakpoint(
23001 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23002 path: &Arc<Path>,
23003 expected: Vec<(u32, Breakpoint)>,
23004) {
23005 if expected.is_empty() {
23006 assert!(!breakpoints.contains_key(path), "{}", path.display());
23007 } else {
23008 let mut breakpoint = breakpoints
23009 .get(path)
23010 .unwrap()
23011 .iter()
23012 .map(|breakpoint| {
23013 (
23014 breakpoint.row,
23015 Breakpoint {
23016 message: breakpoint.message.clone(),
23017 state: breakpoint.state,
23018 condition: breakpoint.condition.clone(),
23019 hit_condition: breakpoint.hit_condition.clone(),
23020 },
23021 )
23022 })
23023 .collect::<Vec<_>>();
23024
23025 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23026
23027 assert_eq!(expected, breakpoint);
23028 }
23029}
23030
23031fn add_log_breakpoint_at_cursor(
23032 editor: &mut Editor,
23033 log_message: &str,
23034 window: &mut Window,
23035 cx: &mut Context<Editor>,
23036) {
23037 let (anchor, bp) = editor
23038 .breakpoints_at_cursors(window, cx)
23039 .first()
23040 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23041 .unwrap_or_else(|| {
23042 let snapshot = editor.snapshot(window, cx);
23043 let cursor_position: Point =
23044 editor.selections.newest(&snapshot.display_snapshot).head();
23045
23046 let breakpoint_position = snapshot
23047 .buffer_snapshot()
23048 .anchor_before(Point::new(cursor_position.row, 0));
23049
23050 (breakpoint_position, Breakpoint::new_log(log_message))
23051 });
23052
23053 editor.edit_breakpoint_at_anchor(
23054 anchor,
23055 bp,
23056 BreakpointEditAction::EditLogMessage(log_message.into()),
23057 cx,
23058 );
23059}
23060
23061#[gpui::test]
23062async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23063 init_test(cx, |_| {});
23064
23065 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23066 let fs = FakeFs::new(cx.executor());
23067 fs.insert_tree(
23068 path!("/a"),
23069 json!({
23070 "main.rs": sample_text,
23071 }),
23072 )
23073 .await;
23074 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23075 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23076 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23077
23078 let fs = FakeFs::new(cx.executor());
23079 fs.insert_tree(
23080 path!("/a"),
23081 json!({
23082 "main.rs": sample_text,
23083 }),
23084 )
23085 .await;
23086 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23087 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23088 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23089 let worktree_id = workspace
23090 .update(cx, |workspace, _window, cx| {
23091 workspace.project().update(cx, |project, cx| {
23092 project.worktrees(cx).next().unwrap().read(cx).id()
23093 })
23094 })
23095 .unwrap();
23096
23097 let buffer = project
23098 .update(cx, |project, cx| {
23099 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23100 })
23101 .await
23102 .unwrap();
23103
23104 let (editor, cx) = cx.add_window_view(|window, cx| {
23105 Editor::new(
23106 EditorMode::full(),
23107 MultiBuffer::build_from_buffer(buffer, cx),
23108 Some(project.clone()),
23109 window,
23110 cx,
23111 )
23112 });
23113
23114 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23115 let abs_path = project.read_with(cx, |project, cx| {
23116 project
23117 .absolute_path(&project_path, cx)
23118 .map(Arc::from)
23119 .unwrap()
23120 });
23121
23122 // assert we can add breakpoint on the first line
23123 editor.update_in(cx, |editor, window, cx| {
23124 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23125 editor.move_to_end(&MoveToEnd, window, cx);
23126 editor.toggle_breakpoint(&actions::ToggleBreakpoint, 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_eq!(1, breakpoints.len());
23139 assert_breakpoint(
23140 &breakpoints,
23141 &abs_path,
23142 vec![
23143 (0, Breakpoint::new_standard()),
23144 (3, Breakpoint::new_standard()),
23145 ],
23146 );
23147
23148 editor.update_in(cx, |editor, window, cx| {
23149 editor.move_to_beginning(&MoveToBeginning, window, cx);
23150 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23151 });
23152
23153 let breakpoints = editor.update(cx, |editor, cx| {
23154 editor
23155 .breakpoint_store()
23156 .as_ref()
23157 .unwrap()
23158 .read(cx)
23159 .all_source_breakpoints(cx)
23160 });
23161
23162 assert_eq!(1, breakpoints.len());
23163 assert_breakpoint(
23164 &breakpoints,
23165 &abs_path,
23166 vec![(3, Breakpoint::new_standard())],
23167 );
23168
23169 editor.update_in(cx, |editor, window, cx| {
23170 editor.move_to_end(&MoveToEnd, window, cx);
23171 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23172 });
23173
23174 let breakpoints = editor.update(cx, |editor, cx| {
23175 editor
23176 .breakpoint_store()
23177 .as_ref()
23178 .unwrap()
23179 .read(cx)
23180 .all_source_breakpoints(cx)
23181 });
23182
23183 assert_eq!(0, breakpoints.len());
23184 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23185}
23186
23187#[gpui::test]
23188async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23189 init_test(cx, |_| {});
23190
23191 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23192
23193 let fs = FakeFs::new(cx.executor());
23194 fs.insert_tree(
23195 path!("/a"),
23196 json!({
23197 "main.rs": sample_text,
23198 }),
23199 )
23200 .await;
23201 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23202 let (workspace, cx) =
23203 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23204
23205 let worktree_id = workspace.update(cx, |workspace, cx| {
23206 workspace.project().update(cx, |project, cx| {
23207 project.worktrees(cx).next().unwrap().read(cx).id()
23208 })
23209 });
23210
23211 let buffer = project
23212 .update(cx, |project, cx| {
23213 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23214 })
23215 .await
23216 .unwrap();
23217
23218 let (editor, cx) = cx.add_window_view(|window, cx| {
23219 Editor::new(
23220 EditorMode::full(),
23221 MultiBuffer::build_from_buffer(buffer, cx),
23222 Some(project.clone()),
23223 window,
23224 cx,
23225 )
23226 });
23227
23228 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23229 let abs_path = project.read_with(cx, |project, cx| {
23230 project
23231 .absolute_path(&project_path, cx)
23232 .map(Arc::from)
23233 .unwrap()
23234 });
23235
23236 editor.update_in(cx, |editor, window, cx| {
23237 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23238 });
23239
23240 let breakpoints = editor.update(cx, |editor, cx| {
23241 editor
23242 .breakpoint_store()
23243 .as_ref()
23244 .unwrap()
23245 .read(cx)
23246 .all_source_breakpoints(cx)
23247 });
23248
23249 assert_breakpoint(
23250 &breakpoints,
23251 &abs_path,
23252 vec![(0, Breakpoint::new_log("hello world"))],
23253 );
23254
23255 // Removing a log message from a log breakpoint should remove it
23256 editor.update_in(cx, |editor, window, cx| {
23257 add_log_breakpoint_at_cursor(editor, "", window, cx);
23258 });
23259
23260 let breakpoints = editor.update(cx, |editor, cx| {
23261 editor
23262 .breakpoint_store()
23263 .as_ref()
23264 .unwrap()
23265 .read(cx)
23266 .all_source_breakpoints(cx)
23267 });
23268
23269 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23270
23271 editor.update_in(cx, |editor, window, cx| {
23272 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23273 editor.move_to_end(&MoveToEnd, window, cx);
23274 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23275 // Not adding a log message to a standard breakpoint shouldn't remove it
23276 add_log_breakpoint_at_cursor(editor, "", window, cx);
23277 });
23278
23279 let breakpoints = editor.update(cx, |editor, cx| {
23280 editor
23281 .breakpoint_store()
23282 .as_ref()
23283 .unwrap()
23284 .read(cx)
23285 .all_source_breakpoints(cx)
23286 });
23287
23288 assert_breakpoint(
23289 &breakpoints,
23290 &abs_path,
23291 vec![
23292 (0, Breakpoint::new_standard()),
23293 (3, Breakpoint::new_standard()),
23294 ],
23295 );
23296
23297 editor.update_in(cx, |editor, window, cx| {
23298 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23299 });
23300
23301 let breakpoints = editor.update(cx, |editor, cx| {
23302 editor
23303 .breakpoint_store()
23304 .as_ref()
23305 .unwrap()
23306 .read(cx)
23307 .all_source_breakpoints(cx)
23308 });
23309
23310 assert_breakpoint(
23311 &breakpoints,
23312 &abs_path,
23313 vec![
23314 (0, Breakpoint::new_standard()),
23315 (3, Breakpoint::new_log("hello world")),
23316 ],
23317 );
23318
23319 editor.update_in(cx, |editor, window, cx| {
23320 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23321 });
23322
23323 let breakpoints = editor.update(cx, |editor, cx| {
23324 editor
23325 .breakpoint_store()
23326 .as_ref()
23327 .unwrap()
23328 .read(cx)
23329 .all_source_breakpoints(cx)
23330 });
23331
23332 assert_breakpoint(
23333 &breakpoints,
23334 &abs_path,
23335 vec![
23336 (0, Breakpoint::new_standard()),
23337 (3, Breakpoint::new_log("hello Earth!!")),
23338 ],
23339 );
23340}
23341
23342/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23343/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23344/// or when breakpoints were placed out of order. This tests for a regression too
23345#[gpui::test]
23346async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23347 init_test(cx, |_| {});
23348
23349 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23350 let fs = FakeFs::new(cx.executor());
23351 fs.insert_tree(
23352 path!("/a"),
23353 json!({
23354 "main.rs": sample_text,
23355 }),
23356 )
23357 .await;
23358 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23359 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23360 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23361
23362 let fs = FakeFs::new(cx.executor());
23363 fs.insert_tree(
23364 path!("/a"),
23365 json!({
23366 "main.rs": sample_text,
23367 }),
23368 )
23369 .await;
23370 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23371 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23372 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23373 let worktree_id = workspace
23374 .update(cx, |workspace, _window, cx| {
23375 workspace.project().update(cx, |project, cx| {
23376 project.worktrees(cx).next().unwrap().read(cx).id()
23377 })
23378 })
23379 .unwrap();
23380
23381 let buffer = project
23382 .update(cx, |project, cx| {
23383 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23384 })
23385 .await
23386 .unwrap();
23387
23388 let (editor, cx) = cx.add_window_view(|window, cx| {
23389 Editor::new(
23390 EditorMode::full(),
23391 MultiBuffer::build_from_buffer(buffer, cx),
23392 Some(project.clone()),
23393 window,
23394 cx,
23395 )
23396 });
23397
23398 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23399 let abs_path = project.read_with(cx, |project, cx| {
23400 project
23401 .absolute_path(&project_path, cx)
23402 .map(Arc::from)
23403 .unwrap()
23404 });
23405
23406 // assert we can add breakpoint on the first line
23407 editor.update_in(cx, |editor, window, cx| {
23408 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23409 editor.move_to_end(&MoveToEnd, window, cx);
23410 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23411 editor.move_up(&MoveUp, window, cx);
23412 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23413 });
23414
23415 let breakpoints = editor.update(cx, |editor, cx| {
23416 editor
23417 .breakpoint_store()
23418 .as_ref()
23419 .unwrap()
23420 .read(cx)
23421 .all_source_breakpoints(cx)
23422 });
23423
23424 assert_eq!(1, breakpoints.len());
23425 assert_breakpoint(
23426 &breakpoints,
23427 &abs_path,
23428 vec![
23429 (0, Breakpoint::new_standard()),
23430 (2, Breakpoint::new_standard()),
23431 (3, Breakpoint::new_standard()),
23432 ],
23433 );
23434
23435 editor.update_in(cx, |editor, window, cx| {
23436 editor.move_to_beginning(&MoveToBeginning, window, cx);
23437 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23438 editor.move_to_end(&MoveToEnd, window, cx);
23439 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23440 // Disabling a breakpoint that doesn't exist should do nothing
23441 editor.move_up(&MoveUp, window, cx);
23442 editor.move_up(&MoveUp, window, cx);
23443 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23444 });
23445
23446 let breakpoints = editor.update(cx, |editor, cx| {
23447 editor
23448 .breakpoint_store()
23449 .as_ref()
23450 .unwrap()
23451 .read(cx)
23452 .all_source_breakpoints(cx)
23453 });
23454
23455 let disable_breakpoint = {
23456 let mut bp = Breakpoint::new_standard();
23457 bp.state = BreakpointState::Disabled;
23458 bp
23459 };
23460
23461 assert_eq!(1, breakpoints.len());
23462 assert_breakpoint(
23463 &breakpoints,
23464 &abs_path,
23465 vec![
23466 (0, disable_breakpoint.clone()),
23467 (2, Breakpoint::new_standard()),
23468 (3, disable_breakpoint.clone()),
23469 ],
23470 );
23471
23472 editor.update_in(cx, |editor, window, cx| {
23473 editor.move_to_beginning(&MoveToBeginning, window, cx);
23474 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23475 editor.move_to_end(&MoveToEnd, window, cx);
23476 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23477 editor.move_up(&MoveUp, window, cx);
23478 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23479 });
23480
23481 let breakpoints = editor.update(cx, |editor, cx| {
23482 editor
23483 .breakpoint_store()
23484 .as_ref()
23485 .unwrap()
23486 .read(cx)
23487 .all_source_breakpoints(cx)
23488 });
23489
23490 assert_eq!(1, breakpoints.len());
23491 assert_breakpoint(
23492 &breakpoints,
23493 &abs_path,
23494 vec![
23495 (0, Breakpoint::new_standard()),
23496 (2, disable_breakpoint),
23497 (3, Breakpoint::new_standard()),
23498 ],
23499 );
23500}
23501
23502#[gpui::test]
23503async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23504 init_test(cx, |_| {});
23505 let capabilities = lsp::ServerCapabilities {
23506 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23507 prepare_provider: Some(true),
23508 work_done_progress_options: Default::default(),
23509 })),
23510 ..Default::default()
23511 };
23512 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23513
23514 cx.set_state(indoc! {"
23515 struct Fˇoo {}
23516 "});
23517
23518 cx.update_editor(|editor, _, cx| {
23519 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23520 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23521 editor.highlight_background::<DocumentHighlightRead>(
23522 &[highlight_range],
23523 |theme| theme.colors().editor_document_highlight_read_background,
23524 cx,
23525 );
23526 });
23527
23528 let mut prepare_rename_handler = cx
23529 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23530 move |_, _, _| async move {
23531 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23532 start: lsp::Position {
23533 line: 0,
23534 character: 7,
23535 },
23536 end: lsp::Position {
23537 line: 0,
23538 character: 10,
23539 },
23540 })))
23541 },
23542 );
23543 let prepare_rename_task = cx
23544 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23545 .expect("Prepare rename was not started");
23546 prepare_rename_handler.next().await.unwrap();
23547 prepare_rename_task.await.expect("Prepare rename failed");
23548
23549 let mut rename_handler =
23550 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23551 let edit = lsp::TextEdit {
23552 range: lsp::Range {
23553 start: lsp::Position {
23554 line: 0,
23555 character: 7,
23556 },
23557 end: lsp::Position {
23558 line: 0,
23559 character: 10,
23560 },
23561 },
23562 new_text: "FooRenamed".to_string(),
23563 };
23564 Ok(Some(lsp::WorkspaceEdit::new(
23565 // Specify the same edit twice
23566 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23567 )))
23568 });
23569 let rename_task = cx
23570 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23571 .expect("Confirm rename was not started");
23572 rename_handler.next().await.unwrap();
23573 rename_task.await.expect("Confirm rename failed");
23574 cx.run_until_parked();
23575
23576 // Despite two edits, only one is actually applied as those are identical
23577 cx.assert_editor_state(indoc! {"
23578 struct FooRenamedˇ {}
23579 "});
23580}
23581
23582#[gpui::test]
23583async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23584 init_test(cx, |_| {});
23585 // These capabilities indicate that the server does not support prepare rename.
23586 let capabilities = lsp::ServerCapabilities {
23587 rename_provider: Some(lsp::OneOf::Left(true)),
23588 ..Default::default()
23589 };
23590 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23591
23592 cx.set_state(indoc! {"
23593 struct Fˇoo {}
23594 "});
23595
23596 cx.update_editor(|editor, _window, cx| {
23597 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23598 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23599 editor.highlight_background::<DocumentHighlightRead>(
23600 &[highlight_range],
23601 |theme| theme.colors().editor_document_highlight_read_background,
23602 cx,
23603 );
23604 });
23605
23606 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23607 .expect("Prepare rename was not started")
23608 .await
23609 .expect("Prepare rename failed");
23610
23611 let mut rename_handler =
23612 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23613 let edit = lsp::TextEdit {
23614 range: lsp::Range {
23615 start: lsp::Position {
23616 line: 0,
23617 character: 7,
23618 },
23619 end: lsp::Position {
23620 line: 0,
23621 character: 10,
23622 },
23623 },
23624 new_text: "FooRenamed".to_string(),
23625 };
23626 Ok(Some(lsp::WorkspaceEdit::new(
23627 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23628 )))
23629 });
23630 let rename_task = cx
23631 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23632 .expect("Confirm rename was not started");
23633 rename_handler.next().await.unwrap();
23634 rename_task.await.expect("Confirm rename failed");
23635 cx.run_until_parked();
23636
23637 // Correct range is renamed, as `surrounding_word` is used to find it.
23638 cx.assert_editor_state(indoc! {"
23639 struct FooRenamedˇ {}
23640 "});
23641}
23642
23643#[gpui::test]
23644async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23645 init_test(cx, |_| {});
23646 let mut cx = EditorTestContext::new(cx).await;
23647
23648 let language = Arc::new(
23649 Language::new(
23650 LanguageConfig::default(),
23651 Some(tree_sitter_html::LANGUAGE.into()),
23652 )
23653 .with_brackets_query(
23654 r#"
23655 ("<" @open "/>" @close)
23656 ("</" @open ">" @close)
23657 ("<" @open ">" @close)
23658 ("\"" @open "\"" @close)
23659 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23660 "#,
23661 )
23662 .unwrap(),
23663 );
23664 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23665
23666 cx.set_state(indoc! {"
23667 <span>ˇ</span>
23668 "});
23669 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23670 cx.assert_editor_state(indoc! {"
23671 <span>
23672 ˇ
23673 </span>
23674 "});
23675
23676 cx.set_state(indoc! {"
23677 <span><span></span>ˇ</span>
23678 "});
23679 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23680 cx.assert_editor_state(indoc! {"
23681 <span><span></span>
23682 ˇ</span>
23683 "});
23684
23685 cx.set_state(indoc! {"
23686 <span>ˇ
23687 </span>
23688 "});
23689 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23690 cx.assert_editor_state(indoc! {"
23691 <span>
23692 ˇ
23693 </span>
23694 "});
23695}
23696
23697#[gpui::test(iterations = 10)]
23698async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23699 init_test(cx, |_| {});
23700
23701 let fs = FakeFs::new(cx.executor());
23702 fs.insert_tree(
23703 path!("/dir"),
23704 json!({
23705 "a.ts": "a",
23706 }),
23707 )
23708 .await;
23709
23710 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23712 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23713
23714 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23715 language_registry.add(Arc::new(Language::new(
23716 LanguageConfig {
23717 name: "TypeScript".into(),
23718 matcher: LanguageMatcher {
23719 path_suffixes: vec!["ts".to_string()],
23720 ..Default::default()
23721 },
23722 ..Default::default()
23723 },
23724 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23725 )));
23726 let mut fake_language_servers = language_registry.register_fake_lsp(
23727 "TypeScript",
23728 FakeLspAdapter {
23729 capabilities: lsp::ServerCapabilities {
23730 code_lens_provider: Some(lsp::CodeLensOptions {
23731 resolve_provider: Some(true),
23732 }),
23733 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23734 commands: vec!["_the/command".to_string()],
23735 ..lsp::ExecuteCommandOptions::default()
23736 }),
23737 ..lsp::ServerCapabilities::default()
23738 },
23739 ..FakeLspAdapter::default()
23740 },
23741 );
23742
23743 let editor = workspace
23744 .update(cx, |workspace, window, cx| {
23745 workspace.open_abs_path(
23746 PathBuf::from(path!("/dir/a.ts")),
23747 OpenOptions::default(),
23748 window,
23749 cx,
23750 )
23751 })
23752 .unwrap()
23753 .await
23754 .unwrap()
23755 .downcast::<Editor>()
23756 .unwrap();
23757 cx.executor().run_until_parked();
23758
23759 let fake_server = fake_language_servers.next().await.unwrap();
23760
23761 let buffer = editor.update(cx, |editor, cx| {
23762 editor
23763 .buffer()
23764 .read(cx)
23765 .as_singleton()
23766 .expect("have opened a single file by path")
23767 });
23768
23769 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23770 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23771 drop(buffer_snapshot);
23772 let actions = cx
23773 .update_window(*workspace, |_, window, cx| {
23774 project.code_actions(&buffer, anchor..anchor, window, cx)
23775 })
23776 .unwrap();
23777
23778 fake_server
23779 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23780 Ok(Some(vec![
23781 lsp::CodeLens {
23782 range: lsp::Range::default(),
23783 command: Some(lsp::Command {
23784 title: "Code lens command".to_owned(),
23785 command: "_the/command".to_owned(),
23786 arguments: None,
23787 }),
23788 data: None,
23789 },
23790 lsp::CodeLens {
23791 range: lsp::Range::default(),
23792 command: Some(lsp::Command {
23793 title: "Command not in capabilities".to_owned(),
23794 command: "not in capabilities".to_owned(),
23795 arguments: None,
23796 }),
23797 data: None,
23798 },
23799 lsp::CodeLens {
23800 range: lsp::Range {
23801 start: lsp::Position {
23802 line: 1,
23803 character: 1,
23804 },
23805 end: lsp::Position {
23806 line: 1,
23807 character: 1,
23808 },
23809 },
23810 command: Some(lsp::Command {
23811 title: "Command not in range".to_owned(),
23812 command: "_the/command".to_owned(),
23813 arguments: None,
23814 }),
23815 data: None,
23816 },
23817 ]))
23818 })
23819 .next()
23820 .await;
23821
23822 let actions = actions.await.unwrap();
23823 assert_eq!(
23824 actions.len(),
23825 1,
23826 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23827 );
23828 let action = actions[0].clone();
23829 let apply = project.update(cx, |project, cx| {
23830 project.apply_code_action(buffer.clone(), action, true, cx)
23831 });
23832
23833 // Resolving the code action does not populate its edits. In absence of
23834 // edits, we must execute the given command.
23835 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23836 |mut lens, _| async move {
23837 let lens_command = lens.command.as_mut().expect("should have a command");
23838 assert_eq!(lens_command.title, "Code lens command");
23839 lens_command.arguments = Some(vec![json!("the-argument")]);
23840 Ok(lens)
23841 },
23842 );
23843
23844 // While executing the command, the language server sends the editor
23845 // a `workspaceEdit` request.
23846 fake_server
23847 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23848 let fake = fake_server.clone();
23849 move |params, _| {
23850 assert_eq!(params.command, "_the/command");
23851 let fake = fake.clone();
23852 async move {
23853 fake.server
23854 .request::<lsp::request::ApplyWorkspaceEdit>(
23855 lsp::ApplyWorkspaceEditParams {
23856 label: None,
23857 edit: lsp::WorkspaceEdit {
23858 changes: Some(
23859 [(
23860 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23861 vec![lsp::TextEdit {
23862 range: lsp::Range::new(
23863 lsp::Position::new(0, 0),
23864 lsp::Position::new(0, 0),
23865 ),
23866 new_text: "X".into(),
23867 }],
23868 )]
23869 .into_iter()
23870 .collect(),
23871 ),
23872 ..lsp::WorkspaceEdit::default()
23873 },
23874 },
23875 )
23876 .await
23877 .into_response()
23878 .unwrap();
23879 Ok(Some(json!(null)))
23880 }
23881 }
23882 })
23883 .next()
23884 .await;
23885
23886 // Applying the code lens command returns a project transaction containing the edits
23887 // sent by the language server in its `workspaceEdit` request.
23888 let transaction = apply.await.unwrap();
23889 assert!(transaction.0.contains_key(&buffer));
23890 buffer.update(cx, |buffer, cx| {
23891 assert_eq!(buffer.text(), "Xa");
23892 buffer.undo(cx);
23893 assert_eq!(buffer.text(), "a");
23894 });
23895
23896 let actions_after_edits = cx
23897 .update_window(*workspace, |_, window, cx| {
23898 project.code_actions(&buffer, anchor..anchor, window, cx)
23899 })
23900 .unwrap()
23901 .await
23902 .unwrap();
23903 assert_eq!(
23904 actions, actions_after_edits,
23905 "For the same selection, same code lens actions should be returned"
23906 );
23907
23908 let _responses =
23909 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23910 panic!("No more code lens requests are expected");
23911 });
23912 editor.update_in(cx, |editor, window, cx| {
23913 editor.select_all(&SelectAll, window, cx);
23914 });
23915 cx.executor().run_until_parked();
23916 let new_actions = cx
23917 .update_window(*workspace, |_, window, cx| {
23918 project.code_actions(&buffer, anchor..anchor, window, cx)
23919 })
23920 .unwrap()
23921 .await
23922 .unwrap();
23923 assert_eq!(
23924 actions, new_actions,
23925 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23926 );
23927}
23928
23929#[gpui::test]
23930async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23931 init_test(cx, |_| {});
23932
23933 let fs = FakeFs::new(cx.executor());
23934 let main_text = r#"fn main() {
23935println!("1");
23936println!("2");
23937println!("3");
23938println!("4");
23939println!("5");
23940}"#;
23941 let lib_text = "mod foo {}";
23942 fs.insert_tree(
23943 path!("/a"),
23944 json!({
23945 "lib.rs": lib_text,
23946 "main.rs": main_text,
23947 }),
23948 )
23949 .await;
23950
23951 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23952 let (workspace, cx) =
23953 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23954 let worktree_id = workspace.update(cx, |workspace, cx| {
23955 workspace.project().update(cx, |project, cx| {
23956 project.worktrees(cx).next().unwrap().read(cx).id()
23957 })
23958 });
23959
23960 let expected_ranges = vec![
23961 Point::new(0, 0)..Point::new(0, 0),
23962 Point::new(1, 0)..Point::new(1, 1),
23963 Point::new(2, 0)..Point::new(2, 2),
23964 Point::new(3, 0)..Point::new(3, 3),
23965 ];
23966
23967 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23968 let editor_1 = workspace
23969 .update_in(cx, |workspace, window, cx| {
23970 workspace.open_path(
23971 (worktree_id, rel_path("main.rs")),
23972 Some(pane_1.downgrade()),
23973 true,
23974 window,
23975 cx,
23976 )
23977 })
23978 .unwrap()
23979 .await
23980 .downcast::<Editor>()
23981 .unwrap();
23982 pane_1.update(cx, |pane, cx| {
23983 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23984 open_editor.update(cx, |editor, cx| {
23985 assert_eq!(
23986 editor.display_text(cx),
23987 main_text,
23988 "Original main.rs text on initial open",
23989 );
23990 assert_eq!(
23991 editor
23992 .selections
23993 .all::<Point>(&editor.display_snapshot(cx))
23994 .into_iter()
23995 .map(|s| s.range())
23996 .collect::<Vec<_>>(),
23997 vec![Point::zero()..Point::zero()],
23998 "Default selections on initial open",
23999 );
24000 })
24001 });
24002 editor_1.update_in(cx, |editor, window, cx| {
24003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24004 s.select_ranges(expected_ranges.clone());
24005 });
24006 });
24007
24008 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24009 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24010 });
24011 let editor_2 = workspace
24012 .update_in(cx, |workspace, window, cx| {
24013 workspace.open_path(
24014 (worktree_id, rel_path("main.rs")),
24015 Some(pane_2.downgrade()),
24016 true,
24017 window,
24018 cx,
24019 )
24020 })
24021 .unwrap()
24022 .await
24023 .downcast::<Editor>()
24024 .unwrap();
24025 pane_2.update(cx, |pane, cx| {
24026 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24027 open_editor.update(cx, |editor, cx| {
24028 assert_eq!(
24029 editor.display_text(cx),
24030 main_text,
24031 "Original main.rs text on initial open in another panel",
24032 );
24033 assert_eq!(
24034 editor
24035 .selections
24036 .all::<Point>(&editor.display_snapshot(cx))
24037 .into_iter()
24038 .map(|s| s.range())
24039 .collect::<Vec<_>>(),
24040 vec![Point::zero()..Point::zero()],
24041 "Default selections on initial open in another panel",
24042 );
24043 })
24044 });
24045
24046 editor_2.update_in(cx, |editor, window, cx| {
24047 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24048 });
24049
24050 let _other_editor_1 = workspace
24051 .update_in(cx, |workspace, window, cx| {
24052 workspace.open_path(
24053 (worktree_id, rel_path("lib.rs")),
24054 Some(pane_1.downgrade()),
24055 true,
24056 window,
24057 cx,
24058 )
24059 })
24060 .unwrap()
24061 .await
24062 .downcast::<Editor>()
24063 .unwrap();
24064 pane_1
24065 .update_in(cx, |pane, window, cx| {
24066 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24067 })
24068 .await
24069 .unwrap();
24070 drop(editor_1);
24071 pane_1.update(cx, |pane, cx| {
24072 pane.active_item()
24073 .unwrap()
24074 .downcast::<Editor>()
24075 .unwrap()
24076 .update(cx, |editor, cx| {
24077 assert_eq!(
24078 editor.display_text(cx),
24079 lib_text,
24080 "Other file should be open and active",
24081 );
24082 });
24083 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24084 });
24085
24086 let _other_editor_2 = workspace
24087 .update_in(cx, |workspace, window, cx| {
24088 workspace.open_path(
24089 (worktree_id, rel_path("lib.rs")),
24090 Some(pane_2.downgrade()),
24091 true,
24092 window,
24093 cx,
24094 )
24095 })
24096 .unwrap()
24097 .await
24098 .downcast::<Editor>()
24099 .unwrap();
24100 pane_2
24101 .update_in(cx, |pane, window, cx| {
24102 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24103 })
24104 .await
24105 .unwrap();
24106 drop(editor_2);
24107 pane_2.update(cx, |pane, cx| {
24108 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24109 open_editor.update(cx, |editor, cx| {
24110 assert_eq!(
24111 editor.display_text(cx),
24112 lib_text,
24113 "Other file should be open and active in another panel too",
24114 );
24115 });
24116 assert_eq!(
24117 pane.items().count(),
24118 1,
24119 "No other editors should be open in another pane",
24120 );
24121 });
24122
24123 let _editor_1_reopened = workspace
24124 .update_in(cx, |workspace, window, cx| {
24125 workspace.open_path(
24126 (worktree_id, rel_path("main.rs")),
24127 Some(pane_1.downgrade()),
24128 true,
24129 window,
24130 cx,
24131 )
24132 })
24133 .unwrap()
24134 .await
24135 .downcast::<Editor>()
24136 .unwrap();
24137 let _editor_2_reopened = workspace
24138 .update_in(cx, |workspace, window, cx| {
24139 workspace.open_path(
24140 (worktree_id, rel_path("main.rs")),
24141 Some(pane_2.downgrade()),
24142 true,
24143 window,
24144 cx,
24145 )
24146 })
24147 .unwrap()
24148 .await
24149 .downcast::<Editor>()
24150 .unwrap();
24151 pane_1.update(cx, |pane, cx| {
24152 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24153 open_editor.update(cx, |editor, cx| {
24154 assert_eq!(
24155 editor.display_text(cx),
24156 main_text,
24157 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24158 );
24159 assert_eq!(
24160 editor
24161 .selections
24162 .all::<Point>(&editor.display_snapshot(cx))
24163 .into_iter()
24164 .map(|s| s.range())
24165 .collect::<Vec<_>>(),
24166 expected_ranges,
24167 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24168 );
24169 })
24170 });
24171 pane_2.update(cx, |pane, cx| {
24172 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24173 open_editor.update(cx, |editor, cx| {
24174 assert_eq!(
24175 editor.display_text(cx),
24176 r#"fn main() {
24177⋯rintln!("1");
24178⋯intln!("2");
24179⋯ntln!("3");
24180println!("4");
24181println!("5");
24182}"#,
24183 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24184 );
24185 assert_eq!(
24186 editor
24187 .selections
24188 .all::<Point>(&editor.display_snapshot(cx))
24189 .into_iter()
24190 .map(|s| s.range())
24191 .collect::<Vec<_>>(),
24192 vec![Point::zero()..Point::zero()],
24193 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24194 );
24195 })
24196 });
24197}
24198
24199#[gpui::test]
24200async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24201 init_test(cx, |_| {});
24202
24203 let fs = FakeFs::new(cx.executor());
24204 let main_text = r#"fn main() {
24205println!("1");
24206println!("2");
24207println!("3");
24208println!("4");
24209println!("5");
24210}"#;
24211 let lib_text = "mod foo {}";
24212 fs.insert_tree(
24213 path!("/a"),
24214 json!({
24215 "lib.rs": lib_text,
24216 "main.rs": main_text,
24217 }),
24218 )
24219 .await;
24220
24221 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24222 let (workspace, cx) =
24223 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24224 let worktree_id = workspace.update(cx, |workspace, cx| {
24225 workspace.project().update(cx, |project, cx| {
24226 project.worktrees(cx).next().unwrap().read(cx).id()
24227 })
24228 });
24229
24230 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24231 let editor = workspace
24232 .update_in(cx, |workspace, window, cx| {
24233 workspace.open_path(
24234 (worktree_id, rel_path("main.rs")),
24235 Some(pane.downgrade()),
24236 true,
24237 window,
24238 cx,
24239 )
24240 })
24241 .unwrap()
24242 .await
24243 .downcast::<Editor>()
24244 .unwrap();
24245 pane.update(cx, |pane, cx| {
24246 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24247 open_editor.update(cx, |editor, cx| {
24248 assert_eq!(
24249 editor.display_text(cx),
24250 main_text,
24251 "Original main.rs text on initial open",
24252 );
24253 })
24254 });
24255 editor.update_in(cx, |editor, window, cx| {
24256 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24257 });
24258
24259 cx.update_global(|store: &mut SettingsStore, cx| {
24260 store.update_user_settings(cx, |s| {
24261 s.workspace.restore_on_file_reopen = Some(false);
24262 });
24263 });
24264 editor.update_in(cx, |editor, window, cx| {
24265 editor.fold_ranges(
24266 vec![
24267 Point::new(1, 0)..Point::new(1, 1),
24268 Point::new(2, 0)..Point::new(2, 2),
24269 Point::new(3, 0)..Point::new(3, 3),
24270 ],
24271 false,
24272 window,
24273 cx,
24274 );
24275 });
24276 pane.update_in(cx, |pane, window, cx| {
24277 pane.close_all_items(&CloseAllItems::default(), window, cx)
24278 })
24279 .await
24280 .unwrap();
24281 pane.update(cx, |pane, _| {
24282 assert!(pane.active_item().is_none());
24283 });
24284 cx.update_global(|store: &mut SettingsStore, cx| {
24285 store.update_user_settings(cx, |s| {
24286 s.workspace.restore_on_file_reopen = Some(true);
24287 });
24288 });
24289
24290 let _editor_reopened = workspace
24291 .update_in(cx, |workspace, window, cx| {
24292 workspace.open_path(
24293 (worktree_id, rel_path("main.rs")),
24294 Some(pane.downgrade()),
24295 true,
24296 window,
24297 cx,
24298 )
24299 })
24300 .unwrap()
24301 .await
24302 .downcast::<Editor>()
24303 .unwrap();
24304 pane.update(cx, |pane, cx| {
24305 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24306 open_editor.update(cx, |editor, cx| {
24307 assert_eq!(
24308 editor.display_text(cx),
24309 main_text,
24310 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24311 );
24312 })
24313 });
24314}
24315
24316#[gpui::test]
24317async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24318 struct EmptyModalView {
24319 focus_handle: gpui::FocusHandle,
24320 }
24321 impl EventEmitter<DismissEvent> for EmptyModalView {}
24322 impl Render for EmptyModalView {
24323 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24324 div()
24325 }
24326 }
24327 impl Focusable for EmptyModalView {
24328 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24329 self.focus_handle.clone()
24330 }
24331 }
24332 impl workspace::ModalView for EmptyModalView {}
24333 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24334 EmptyModalView {
24335 focus_handle: cx.focus_handle(),
24336 }
24337 }
24338
24339 init_test(cx, |_| {});
24340
24341 let fs = FakeFs::new(cx.executor());
24342 let project = Project::test(fs, [], cx).await;
24343 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24344 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24345 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24346 let editor = cx.new_window_entity(|window, cx| {
24347 Editor::new(
24348 EditorMode::full(),
24349 buffer,
24350 Some(project.clone()),
24351 window,
24352 cx,
24353 )
24354 });
24355 workspace
24356 .update(cx, |workspace, window, cx| {
24357 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24358 })
24359 .unwrap();
24360 editor.update_in(cx, |editor, window, cx| {
24361 editor.open_context_menu(&OpenContextMenu, window, cx);
24362 assert!(editor.mouse_context_menu.is_some());
24363 });
24364 workspace
24365 .update(cx, |workspace, window, cx| {
24366 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24367 })
24368 .unwrap();
24369 cx.read(|cx| {
24370 assert!(editor.read(cx).mouse_context_menu.is_none());
24371 });
24372}
24373
24374fn set_linked_edit_ranges(
24375 opening: (Point, Point),
24376 closing: (Point, Point),
24377 editor: &mut Editor,
24378 cx: &mut Context<Editor>,
24379) {
24380 let Some((buffer, _)) = editor
24381 .buffer
24382 .read(cx)
24383 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24384 else {
24385 panic!("Failed to get buffer for selection position");
24386 };
24387 let buffer = buffer.read(cx);
24388 let buffer_id = buffer.remote_id();
24389 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24390 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24391 let mut linked_ranges = HashMap::default();
24392 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24393 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24394}
24395
24396#[gpui::test]
24397async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24398 init_test(cx, |_| {});
24399
24400 let fs = FakeFs::new(cx.executor());
24401 fs.insert_file(path!("/file.html"), Default::default())
24402 .await;
24403
24404 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24405
24406 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24407 let html_language = Arc::new(Language::new(
24408 LanguageConfig {
24409 name: "HTML".into(),
24410 matcher: LanguageMatcher {
24411 path_suffixes: vec!["html".to_string()],
24412 ..LanguageMatcher::default()
24413 },
24414 brackets: BracketPairConfig {
24415 pairs: vec![BracketPair {
24416 start: "<".into(),
24417 end: ">".into(),
24418 close: true,
24419 ..Default::default()
24420 }],
24421 ..Default::default()
24422 },
24423 ..Default::default()
24424 },
24425 Some(tree_sitter_html::LANGUAGE.into()),
24426 ));
24427 language_registry.add(html_language);
24428 let mut fake_servers = language_registry.register_fake_lsp(
24429 "HTML",
24430 FakeLspAdapter {
24431 capabilities: lsp::ServerCapabilities {
24432 completion_provider: Some(lsp::CompletionOptions {
24433 resolve_provider: Some(true),
24434 ..Default::default()
24435 }),
24436 ..Default::default()
24437 },
24438 ..Default::default()
24439 },
24440 );
24441
24442 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24443 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24444
24445 let worktree_id = workspace
24446 .update(cx, |workspace, _window, cx| {
24447 workspace.project().update(cx, |project, cx| {
24448 project.worktrees(cx).next().unwrap().read(cx).id()
24449 })
24450 })
24451 .unwrap();
24452 project
24453 .update(cx, |project, cx| {
24454 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24455 })
24456 .await
24457 .unwrap();
24458 let editor = workspace
24459 .update(cx, |workspace, window, cx| {
24460 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24461 })
24462 .unwrap()
24463 .await
24464 .unwrap()
24465 .downcast::<Editor>()
24466 .unwrap();
24467
24468 let fake_server = fake_servers.next().await.unwrap();
24469 editor.update_in(cx, |editor, window, cx| {
24470 editor.set_text("<ad></ad>", window, cx);
24471 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24472 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24473 });
24474 set_linked_edit_ranges(
24475 (Point::new(0, 1), Point::new(0, 3)),
24476 (Point::new(0, 6), Point::new(0, 8)),
24477 editor,
24478 cx,
24479 );
24480 });
24481 let mut completion_handle =
24482 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24483 Ok(Some(lsp::CompletionResponse::Array(vec![
24484 lsp::CompletionItem {
24485 label: "head".to_string(),
24486 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24487 lsp::InsertReplaceEdit {
24488 new_text: "head".to_string(),
24489 insert: lsp::Range::new(
24490 lsp::Position::new(0, 1),
24491 lsp::Position::new(0, 3),
24492 ),
24493 replace: lsp::Range::new(
24494 lsp::Position::new(0, 1),
24495 lsp::Position::new(0, 3),
24496 ),
24497 },
24498 )),
24499 ..Default::default()
24500 },
24501 ])))
24502 });
24503 editor.update_in(cx, |editor, window, cx| {
24504 editor.show_completions(&ShowCompletions, window, cx);
24505 });
24506 cx.run_until_parked();
24507 completion_handle.next().await.unwrap();
24508 editor.update(cx, |editor, _| {
24509 assert!(
24510 editor.context_menu_visible(),
24511 "Completion menu should be visible"
24512 );
24513 });
24514 editor.update_in(cx, |editor, window, cx| {
24515 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24516 });
24517 cx.executor().run_until_parked();
24518 editor.update(cx, |editor, cx| {
24519 assert_eq!(editor.text(cx), "<head></head>");
24520 });
24521}
24522
24523#[gpui::test]
24524async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24525 init_test(cx, |_| {});
24526
24527 let mut cx = EditorTestContext::new(cx).await;
24528 let language = Arc::new(Language::new(
24529 LanguageConfig {
24530 name: "TSX".into(),
24531 matcher: LanguageMatcher {
24532 path_suffixes: vec!["tsx".to_string()],
24533 ..LanguageMatcher::default()
24534 },
24535 brackets: BracketPairConfig {
24536 pairs: vec![BracketPair {
24537 start: "<".into(),
24538 end: ">".into(),
24539 close: true,
24540 ..Default::default()
24541 }],
24542 ..Default::default()
24543 },
24544 linked_edit_characters: HashSet::from_iter(['.']),
24545 ..Default::default()
24546 },
24547 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24548 ));
24549 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24550
24551 // Test typing > does not extend linked pair
24552 cx.set_state("<divˇ<div></div>");
24553 cx.update_editor(|editor, _, cx| {
24554 set_linked_edit_ranges(
24555 (Point::new(0, 1), Point::new(0, 4)),
24556 (Point::new(0, 11), Point::new(0, 14)),
24557 editor,
24558 cx,
24559 );
24560 });
24561 cx.update_editor(|editor, window, cx| {
24562 editor.handle_input(">", window, cx);
24563 });
24564 cx.assert_editor_state("<div>ˇ<div></div>");
24565
24566 // Test typing . do extend linked pair
24567 cx.set_state("<Animatedˇ></Animated>");
24568 cx.update_editor(|editor, _, cx| {
24569 set_linked_edit_ranges(
24570 (Point::new(0, 1), Point::new(0, 9)),
24571 (Point::new(0, 12), Point::new(0, 20)),
24572 editor,
24573 cx,
24574 );
24575 });
24576 cx.update_editor(|editor, window, cx| {
24577 editor.handle_input(".", window, cx);
24578 });
24579 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24580 cx.update_editor(|editor, _, cx| {
24581 set_linked_edit_ranges(
24582 (Point::new(0, 1), Point::new(0, 10)),
24583 (Point::new(0, 13), Point::new(0, 21)),
24584 editor,
24585 cx,
24586 );
24587 });
24588 cx.update_editor(|editor, window, cx| {
24589 editor.handle_input("V", window, cx);
24590 });
24591 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24592}
24593
24594#[gpui::test]
24595async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24596 init_test(cx, |_| {});
24597
24598 let fs = FakeFs::new(cx.executor());
24599 fs.insert_tree(
24600 path!("/root"),
24601 json!({
24602 "a": {
24603 "main.rs": "fn main() {}",
24604 },
24605 "foo": {
24606 "bar": {
24607 "external_file.rs": "pub mod external {}",
24608 }
24609 }
24610 }),
24611 )
24612 .await;
24613
24614 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24615 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24616 language_registry.add(rust_lang());
24617 let _fake_servers = language_registry.register_fake_lsp(
24618 "Rust",
24619 FakeLspAdapter {
24620 ..FakeLspAdapter::default()
24621 },
24622 );
24623 let (workspace, cx) =
24624 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24625 let worktree_id = workspace.update(cx, |workspace, cx| {
24626 workspace.project().update(cx, |project, cx| {
24627 project.worktrees(cx).next().unwrap().read(cx).id()
24628 })
24629 });
24630
24631 let assert_language_servers_count =
24632 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24633 project.update(cx, |project, cx| {
24634 let current = project
24635 .lsp_store()
24636 .read(cx)
24637 .as_local()
24638 .unwrap()
24639 .language_servers
24640 .len();
24641 assert_eq!(expected, current, "{context}");
24642 });
24643 };
24644
24645 assert_language_servers_count(
24646 0,
24647 "No servers should be running before any file is open",
24648 cx,
24649 );
24650 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24651 let main_editor = workspace
24652 .update_in(cx, |workspace, window, cx| {
24653 workspace.open_path(
24654 (worktree_id, rel_path("main.rs")),
24655 Some(pane.downgrade()),
24656 true,
24657 window,
24658 cx,
24659 )
24660 })
24661 .unwrap()
24662 .await
24663 .downcast::<Editor>()
24664 .unwrap();
24665 pane.update(cx, |pane, cx| {
24666 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24667 open_editor.update(cx, |editor, cx| {
24668 assert_eq!(
24669 editor.display_text(cx),
24670 "fn main() {}",
24671 "Original main.rs text on initial open",
24672 );
24673 });
24674 assert_eq!(open_editor, main_editor);
24675 });
24676 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24677
24678 let external_editor = workspace
24679 .update_in(cx, |workspace, window, cx| {
24680 workspace.open_abs_path(
24681 PathBuf::from("/root/foo/bar/external_file.rs"),
24682 OpenOptions::default(),
24683 window,
24684 cx,
24685 )
24686 })
24687 .await
24688 .expect("opening external file")
24689 .downcast::<Editor>()
24690 .expect("downcasted external file's open element to editor");
24691 pane.update(cx, |pane, cx| {
24692 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24693 open_editor.update(cx, |editor, cx| {
24694 assert_eq!(
24695 editor.display_text(cx),
24696 "pub mod external {}",
24697 "External file is open now",
24698 );
24699 });
24700 assert_eq!(open_editor, external_editor);
24701 });
24702 assert_language_servers_count(
24703 1,
24704 "Second, external, *.rs file should join the existing server",
24705 cx,
24706 );
24707
24708 pane.update_in(cx, |pane, window, cx| {
24709 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24710 })
24711 .await
24712 .unwrap();
24713 pane.update_in(cx, |pane, window, cx| {
24714 pane.navigate_backward(&Default::default(), window, cx);
24715 });
24716 cx.run_until_parked();
24717 pane.update(cx, |pane, cx| {
24718 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24719 open_editor.update(cx, |editor, cx| {
24720 assert_eq!(
24721 editor.display_text(cx),
24722 "pub mod external {}",
24723 "External file is open now",
24724 );
24725 });
24726 });
24727 assert_language_servers_count(
24728 1,
24729 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24730 cx,
24731 );
24732
24733 cx.update(|_, cx| {
24734 workspace::reload(cx);
24735 });
24736 assert_language_servers_count(
24737 1,
24738 "After reloading the worktree with local and external files opened, only one project should be started",
24739 cx,
24740 );
24741}
24742
24743#[gpui::test]
24744async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24745 init_test(cx, |_| {});
24746
24747 let mut cx = EditorTestContext::new(cx).await;
24748 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24749 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24750
24751 // test cursor move to start of each line on tab
24752 // for `if`, `elif`, `else`, `while`, `with` and `for`
24753 cx.set_state(indoc! {"
24754 def main():
24755 ˇ for item in items:
24756 ˇ while item.active:
24757 ˇ if item.value > 10:
24758 ˇ continue
24759 ˇ elif item.value < 0:
24760 ˇ break
24761 ˇ else:
24762 ˇ with item.context() as ctx:
24763 ˇ yield count
24764 ˇ else:
24765 ˇ log('while else')
24766 ˇ else:
24767 ˇ log('for else')
24768 "});
24769 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24770 cx.assert_editor_state(indoc! {"
24771 def main():
24772 ˇfor item in items:
24773 ˇwhile item.active:
24774 ˇif item.value > 10:
24775 ˇcontinue
24776 ˇelif item.value < 0:
24777 ˇbreak
24778 ˇelse:
24779 ˇwith item.context() as ctx:
24780 ˇyield count
24781 ˇelse:
24782 ˇlog('while else')
24783 ˇelse:
24784 ˇlog('for else')
24785 "});
24786 // test relative indent is preserved when tab
24787 // for `if`, `elif`, `else`, `while`, `with` and `for`
24788 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24789 cx.assert_editor_state(indoc! {"
24790 def main():
24791 ˇfor item in items:
24792 ˇwhile item.active:
24793 ˇif item.value > 10:
24794 ˇcontinue
24795 ˇelif item.value < 0:
24796 ˇbreak
24797 ˇelse:
24798 ˇwith item.context() as ctx:
24799 ˇyield count
24800 ˇelse:
24801 ˇlog('while else')
24802 ˇelse:
24803 ˇlog('for else')
24804 "});
24805
24806 // test cursor move to start of each line on tab
24807 // for `try`, `except`, `else`, `finally`, `match` and `def`
24808 cx.set_state(indoc! {"
24809 def main():
24810 ˇ try:
24811 ˇ fetch()
24812 ˇ except ValueError:
24813 ˇ handle_error()
24814 ˇ else:
24815 ˇ match value:
24816 ˇ case _:
24817 ˇ finally:
24818 ˇ def status():
24819 ˇ return 0
24820 "});
24821 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24822 cx.assert_editor_state(indoc! {"
24823 def main():
24824 ˇtry:
24825 ˇfetch()
24826 ˇexcept ValueError:
24827 ˇhandle_error()
24828 ˇelse:
24829 ˇmatch value:
24830 ˇcase _:
24831 ˇfinally:
24832 ˇdef status():
24833 ˇreturn 0
24834 "});
24835 // test relative indent is preserved when tab
24836 // for `try`, `except`, `else`, `finally`, `match` and `def`
24837 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24838 cx.assert_editor_state(indoc! {"
24839 def main():
24840 ˇtry:
24841 ˇfetch()
24842 ˇexcept ValueError:
24843 ˇhandle_error()
24844 ˇelse:
24845 ˇmatch value:
24846 ˇcase _:
24847 ˇfinally:
24848 ˇdef status():
24849 ˇreturn 0
24850 "});
24851}
24852
24853#[gpui::test]
24854async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24855 init_test(cx, |_| {});
24856
24857 let mut cx = EditorTestContext::new(cx).await;
24858 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24859 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24860
24861 // test `else` auto outdents when typed inside `if` block
24862 cx.set_state(indoc! {"
24863 def main():
24864 if i == 2:
24865 return
24866 ˇ
24867 "});
24868 cx.update_editor(|editor, window, cx| {
24869 editor.handle_input("else:", window, cx);
24870 });
24871 cx.assert_editor_state(indoc! {"
24872 def main():
24873 if i == 2:
24874 return
24875 else:ˇ
24876 "});
24877
24878 // test `except` auto outdents when typed inside `try` block
24879 cx.set_state(indoc! {"
24880 def main():
24881 try:
24882 i = 2
24883 ˇ
24884 "});
24885 cx.update_editor(|editor, window, cx| {
24886 editor.handle_input("except:", window, cx);
24887 });
24888 cx.assert_editor_state(indoc! {"
24889 def main():
24890 try:
24891 i = 2
24892 except:ˇ
24893 "});
24894
24895 // test `else` auto outdents when typed inside `except` block
24896 cx.set_state(indoc! {"
24897 def main():
24898 try:
24899 i = 2
24900 except:
24901 j = 2
24902 ˇ
24903 "});
24904 cx.update_editor(|editor, window, cx| {
24905 editor.handle_input("else:", window, cx);
24906 });
24907 cx.assert_editor_state(indoc! {"
24908 def main():
24909 try:
24910 i = 2
24911 except:
24912 j = 2
24913 else:ˇ
24914 "});
24915
24916 // test `finally` auto outdents when typed inside `else` block
24917 cx.set_state(indoc! {"
24918 def main():
24919 try:
24920 i = 2
24921 except:
24922 j = 2
24923 else:
24924 k = 2
24925 ˇ
24926 "});
24927 cx.update_editor(|editor, window, cx| {
24928 editor.handle_input("finally:", window, cx);
24929 });
24930 cx.assert_editor_state(indoc! {"
24931 def main():
24932 try:
24933 i = 2
24934 except:
24935 j = 2
24936 else:
24937 k = 2
24938 finally:ˇ
24939 "});
24940
24941 // test `else` does not outdents when typed inside `except` block right after for block
24942 cx.set_state(indoc! {"
24943 def main():
24944 try:
24945 i = 2
24946 except:
24947 for i in range(n):
24948 pass
24949 ˇ
24950 "});
24951 cx.update_editor(|editor, window, cx| {
24952 editor.handle_input("else:", window, cx);
24953 });
24954 cx.assert_editor_state(indoc! {"
24955 def main():
24956 try:
24957 i = 2
24958 except:
24959 for i in range(n):
24960 pass
24961 else:ˇ
24962 "});
24963
24964 // test `finally` auto outdents when typed inside `else` block right after for block
24965 cx.set_state(indoc! {"
24966 def main():
24967 try:
24968 i = 2
24969 except:
24970 j = 2
24971 else:
24972 for i in range(n):
24973 pass
24974 ˇ
24975 "});
24976 cx.update_editor(|editor, window, cx| {
24977 editor.handle_input("finally:", window, cx);
24978 });
24979 cx.assert_editor_state(indoc! {"
24980 def main():
24981 try:
24982 i = 2
24983 except:
24984 j = 2
24985 else:
24986 for i in range(n):
24987 pass
24988 finally:ˇ
24989 "});
24990
24991 // test `except` outdents to inner "try" block
24992 cx.set_state(indoc! {"
24993 def main():
24994 try:
24995 i = 2
24996 if i == 2:
24997 try:
24998 i = 3
24999 ˇ
25000 "});
25001 cx.update_editor(|editor, window, cx| {
25002 editor.handle_input("except:", window, cx);
25003 });
25004 cx.assert_editor_state(indoc! {"
25005 def main():
25006 try:
25007 i = 2
25008 if i == 2:
25009 try:
25010 i = 3
25011 except:ˇ
25012 "});
25013
25014 // test `except` outdents to outer "try" block
25015 cx.set_state(indoc! {"
25016 def main():
25017 try:
25018 i = 2
25019 if i == 2:
25020 try:
25021 i = 3
25022 ˇ
25023 "});
25024 cx.update_editor(|editor, window, cx| {
25025 editor.handle_input("except:", window, cx);
25026 });
25027 cx.assert_editor_state(indoc! {"
25028 def main():
25029 try:
25030 i = 2
25031 if i == 2:
25032 try:
25033 i = 3
25034 except:ˇ
25035 "});
25036
25037 // test `else` stays at correct indent when typed after `for` block
25038 cx.set_state(indoc! {"
25039 def main():
25040 for i in range(10):
25041 if i == 3:
25042 break
25043 ˇ
25044 "});
25045 cx.update_editor(|editor, window, cx| {
25046 editor.handle_input("else:", window, cx);
25047 });
25048 cx.assert_editor_state(indoc! {"
25049 def main():
25050 for i in range(10):
25051 if i == 3:
25052 break
25053 else:ˇ
25054 "});
25055
25056 // test does not outdent on typing after line with square brackets
25057 cx.set_state(indoc! {"
25058 def f() -> list[str]:
25059 ˇ
25060 "});
25061 cx.update_editor(|editor, window, cx| {
25062 editor.handle_input("a", window, cx);
25063 });
25064 cx.assert_editor_state(indoc! {"
25065 def f() -> list[str]:
25066 aˇ
25067 "});
25068
25069 // test does not outdent on typing : after case keyword
25070 cx.set_state(indoc! {"
25071 match 1:
25072 caseˇ
25073 "});
25074 cx.update_editor(|editor, window, cx| {
25075 editor.handle_input(":", window, cx);
25076 });
25077 cx.assert_editor_state(indoc! {"
25078 match 1:
25079 case:ˇ
25080 "});
25081}
25082
25083#[gpui::test]
25084async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25085 init_test(cx, |_| {});
25086 update_test_language_settings(cx, |settings| {
25087 settings.defaults.extend_comment_on_newline = Some(false);
25088 });
25089 let mut cx = EditorTestContext::new(cx).await;
25090 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25091 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25092
25093 // test correct indent after newline on comment
25094 cx.set_state(indoc! {"
25095 # COMMENT:ˇ
25096 "});
25097 cx.update_editor(|editor, window, cx| {
25098 editor.newline(&Newline, window, cx);
25099 });
25100 cx.assert_editor_state(indoc! {"
25101 # COMMENT:
25102 ˇ
25103 "});
25104
25105 // test correct indent after newline in brackets
25106 cx.set_state(indoc! {"
25107 {ˇ}
25108 "});
25109 cx.update_editor(|editor, window, cx| {
25110 editor.newline(&Newline, window, cx);
25111 });
25112 cx.run_until_parked();
25113 cx.assert_editor_state(indoc! {"
25114 {
25115 ˇ
25116 }
25117 "});
25118
25119 cx.set_state(indoc! {"
25120 (ˇ)
25121 "});
25122 cx.update_editor(|editor, window, cx| {
25123 editor.newline(&Newline, window, cx);
25124 });
25125 cx.run_until_parked();
25126 cx.assert_editor_state(indoc! {"
25127 (
25128 ˇ
25129 )
25130 "});
25131
25132 // do not indent after empty lists or dictionaries
25133 cx.set_state(indoc! {"
25134 a = []ˇ
25135 "});
25136 cx.update_editor(|editor, window, cx| {
25137 editor.newline(&Newline, window, cx);
25138 });
25139 cx.run_until_parked();
25140 cx.assert_editor_state(indoc! {"
25141 a = []
25142 ˇ
25143 "});
25144}
25145
25146#[gpui::test]
25147async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25148 init_test(cx, |_| {});
25149
25150 let mut cx = EditorTestContext::new(cx).await;
25151 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25152 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25153
25154 // test cursor move to start of each line on tab
25155 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25156 cx.set_state(indoc! {"
25157 function main() {
25158 ˇ for item in $items; do
25159 ˇ while [ -n \"$item\" ]; do
25160 ˇ if [ \"$value\" -gt 10 ]; then
25161 ˇ continue
25162 ˇ elif [ \"$value\" -lt 0 ]; then
25163 ˇ break
25164 ˇ else
25165 ˇ echo \"$item\"
25166 ˇ fi
25167 ˇ done
25168 ˇ done
25169 ˇ}
25170 "});
25171 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25172 cx.assert_editor_state(indoc! {"
25173 function main() {
25174 ˇfor item in $items; do
25175 ˇwhile [ -n \"$item\" ]; do
25176 ˇif [ \"$value\" -gt 10 ]; then
25177 ˇcontinue
25178 ˇelif [ \"$value\" -lt 0 ]; then
25179 ˇbreak
25180 ˇelse
25181 ˇecho \"$item\"
25182 ˇfi
25183 ˇdone
25184 ˇdone
25185 ˇ}
25186 "});
25187 // test relative indent is preserved when tab
25188 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25189 cx.assert_editor_state(indoc! {"
25190 function main() {
25191 ˇfor item in $items; do
25192 ˇwhile [ -n \"$item\" ]; do
25193 ˇif [ \"$value\" -gt 10 ]; then
25194 ˇcontinue
25195 ˇelif [ \"$value\" -lt 0 ]; then
25196 ˇbreak
25197 ˇelse
25198 ˇecho \"$item\"
25199 ˇfi
25200 ˇdone
25201 ˇdone
25202 ˇ}
25203 "});
25204
25205 // test cursor move to start of each line on tab
25206 // for `case` statement with patterns
25207 cx.set_state(indoc! {"
25208 function handle() {
25209 ˇ case \"$1\" in
25210 ˇ start)
25211 ˇ echo \"a\"
25212 ˇ ;;
25213 ˇ stop)
25214 ˇ echo \"b\"
25215 ˇ ;;
25216 ˇ *)
25217 ˇ echo \"c\"
25218 ˇ ;;
25219 ˇ esac
25220 ˇ}
25221 "});
25222 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25223 cx.assert_editor_state(indoc! {"
25224 function handle() {
25225 ˇcase \"$1\" in
25226 ˇstart)
25227 ˇecho \"a\"
25228 ˇ;;
25229 ˇstop)
25230 ˇecho \"b\"
25231 ˇ;;
25232 ˇ*)
25233 ˇecho \"c\"
25234 ˇ;;
25235 ˇesac
25236 ˇ}
25237 "});
25238}
25239
25240#[gpui::test]
25241async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25242 init_test(cx, |_| {});
25243
25244 let mut cx = EditorTestContext::new(cx).await;
25245 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25246 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25247
25248 // test indents on comment insert
25249 cx.set_state(indoc! {"
25250 function main() {
25251 ˇ for item in $items; do
25252 ˇ while [ -n \"$item\" ]; do
25253 ˇ if [ \"$value\" -gt 10 ]; then
25254 ˇ continue
25255 ˇ elif [ \"$value\" -lt 0 ]; then
25256 ˇ break
25257 ˇ else
25258 ˇ echo \"$item\"
25259 ˇ fi
25260 ˇ done
25261 ˇ done
25262 ˇ}
25263 "});
25264 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25265 cx.assert_editor_state(indoc! {"
25266 function main() {
25267 #ˇ for item in $items; do
25268 #ˇ while [ -n \"$item\" ]; do
25269 #ˇ if [ \"$value\" -gt 10 ]; then
25270 #ˇ continue
25271 #ˇ elif [ \"$value\" -lt 0 ]; then
25272 #ˇ break
25273 #ˇ else
25274 #ˇ echo \"$item\"
25275 #ˇ fi
25276 #ˇ done
25277 #ˇ done
25278 #ˇ}
25279 "});
25280}
25281
25282#[gpui::test]
25283async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25284 init_test(cx, |_| {});
25285
25286 let mut cx = EditorTestContext::new(cx).await;
25287 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25288 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25289
25290 // test `else` auto outdents when typed inside `if` block
25291 cx.set_state(indoc! {"
25292 if [ \"$1\" = \"test\" ]; then
25293 echo \"foo bar\"
25294 ˇ
25295 "});
25296 cx.update_editor(|editor, window, cx| {
25297 editor.handle_input("else", window, cx);
25298 });
25299 cx.assert_editor_state(indoc! {"
25300 if [ \"$1\" = \"test\" ]; then
25301 echo \"foo bar\"
25302 elseˇ
25303 "});
25304
25305 // test `elif` auto outdents when typed inside `if` block
25306 cx.set_state(indoc! {"
25307 if [ \"$1\" = \"test\" ]; then
25308 echo \"foo bar\"
25309 ˇ
25310 "});
25311 cx.update_editor(|editor, window, cx| {
25312 editor.handle_input("elif", window, cx);
25313 });
25314 cx.assert_editor_state(indoc! {"
25315 if [ \"$1\" = \"test\" ]; then
25316 echo \"foo bar\"
25317 elifˇ
25318 "});
25319
25320 // test `fi` auto outdents when typed inside `else` block
25321 cx.set_state(indoc! {"
25322 if [ \"$1\" = \"test\" ]; then
25323 echo \"foo bar\"
25324 else
25325 echo \"bar baz\"
25326 ˇ
25327 "});
25328 cx.update_editor(|editor, window, cx| {
25329 editor.handle_input("fi", window, cx);
25330 });
25331 cx.assert_editor_state(indoc! {"
25332 if [ \"$1\" = \"test\" ]; then
25333 echo \"foo bar\"
25334 else
25335 echo \"bar baz\"
25336 fiˇ
25337 "});
25338
25339 // test `done` auto outdents when typed inside `while` block
25340 cx.set_state(indoc! {"
25341 while read line; do
25342 echo \"$line\"
25343 ˇ
25344 "});
25345 cx.update_editor(|editor, window, cx| {
25346 editor.handle_input("done", window, cx);
25347 });
25348 cx.assert_editor_state(indoc! {"
25349 while read line; do
25350 echo \"$line\"
25351 doneˇ
25352 "});
25353
25354 // test `done` auto outdents when typed inside `for` block
25355 cx.set_state(indoc! {"
25356 for file in *.txt; do
25357 cat \"$file\"
25358 ˇ
25359 "});
25360 cx.update_editor(|editor, window, cx| {
25361 editor.handle_input("done", window, cx);
25362 });
25363 cx.assert_editor_state(indoc! {"
25364 for file in *.txt; do
25365 cat \"$file\"
25366 doneˇ
25367 "});
25368
25369 // test `esac` auto outdents when typed inside `case` block
25370 cx.set_state(indoc! {"
25371 case \"$1\" in
25372 start)
25373 echo \"foo bar\"
25374 ;;
25375 stop)
25376 echo \"bar baz\"
25377 ;;
25378 ˇ
25379 "});
25380 cx.update_editor(|editor, window, cx| {
25381 editor.handle_input("esac", window, cx);
25382 });
25383 cx.assert_editor_state(indoc! {"
25384 case \"$1\" in
25385 start)
25386 echo \"foo bar\"
25387 ;;
25388 stop)
25389 echo \"bar baz\"
25390 ;;
25391 esacˇ
25392 "});
25393
25394 // test `*)` auto outdents when typed inside `case` block
25395 cx.set_state(indoc! {"
25396 case \"$1\" in
25397 start)
25398 echo \"foo bar\"
25399 ;;
25400 ˇ
25401 "});
25402 cx.update_editor(|editor, window, cx| {
25403 editor.handle_input("*)", window, cx);
25404 });
25405 cx.assert_editor_state(indoc! {"
25406 case \"$1\" in
25407 start)
25408 echo \"foo bar\"
25409 ;;
25410 *)ˇ
25411 "});
25412
25413 // test `fi` outdents to correct level with nested if blocks
25414 cx.set_state(indoc! {"
25415 if [ \"$1\" = \"test\" ]; then
25416 echo \"outer if\"
25417 if [ \"$2\" = \"debug\" ]; then
25418 echo \"inner if\"
25419 ˇ
25420 "});
25421 cx.update_editor(|editor, window, cx| {
25422 editor.handle_input("fi", window, cx);
25423 });
25424 cx.assert_editor_state(indoc! {"
25425 if [ \"$1\" = \"test\" ]; then
25426 echo \"outer if\"
25427 if [ \"$2\" = \"debug\" ]; then
25428 echo \"inner if\"
25429 fiˇ
25430 "});
25431}
25432
25433#[gpui::test]
25434async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25435 init_test(cx, |_| {});
25436 update_test_language_settings(cx, |settings| {
25437 settings.defaults.extend_comment_on_newline = Some(false);
25438 });
25439 let mut cx = EditorTestContext::new(cx).await;
25440 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25441 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25442
25443 // test correct indent after newline on comment
25444 cx.set_state(indoc! {"
25445 # COMMENT:ˇ
25446 "});
25447 cx.update_editor(|editor, window, cx| {
25448 editor.newline(&Newline, window, cx);
25449 });
25450 cx.assert_editor_state(indoc! {"
25451 # COMMENT:
25452 ˇ
25453 "});
25454
25455 // test correct indent after newline after `then`
25456 cx.set_state(indoc! {"
25457
25458 if [ \"$1\" = \"test\" ]; thenˇ
25459 "});
25460 cx.update_editor(|editor, window, cx| {
25461 editor.newline(&Newline, window, cx);
25462 });
25463 cx.run_until_parked();
25464 cx.assert_editor_state(indoc! {"
25465
25466 if [ \"$1\" = \"test\" ]; then
25467 ˇ
25468 "});
25469
25470 // test correct indent after newline after `else`
25471 cx.set_state(indoc! {"
25472 if [ \"$1\" = \"test\" ]; then
25473 elseˇ
25474 "});
25475 cx.update_editor(|editor, window, cx| {
25476 editor.newline(&Newline, window, cx);
25477 });
25478 cx.run_until_parked();
25479 cx.assert_editor_state(indoc! {"
25480 if [ \"$1\" = \"test\" ]; then
25481 else
25482 ˇ
25483 "});
25484
25485 // test correct indent after newline after `elif`
25486 cx.set_state(indoc! {"
25487 if [ \"$1\" = \"test\" ]; then
25488 elifˇ
25489 "});
25490 cx.update_editor(|editor, window, cx| {
25491 editor.newline(&Newline, window, cx);
25492 });
25493 cx.run_until_parked();
25494 cx.assert_editor_state(indoc! {"
25495 if [ \"$1\" = \"test\" ]; then
25496 elif
25497 ˇ
25498 "});
25499
25500 // test correct indent after newline after `do`
25501 cx.set_state(indoc! {"
25502 for file in *.txt; doˇ
25503 "});
25504 cx.update_editor(|editor, window, cx| {
25505 editor.newline(&Newline, window, cx);
25506 });
25507 cx.run_until_parked();
25508 cx.assert_editor_state(indoc! {"
25509 for file in *.txt; do
25510 ˇ
25511 "});
25512
25513 // test correct indent after newline after case pattern
25514 cx.set_state(indoc! {"
25515 case \"$1\" in
25516 start)ˇ
25517 "});
25518 cx.update_editor(|editor, window, cx| {
25519 editor.newline(&Newline, window, cx);
25520 });
25521 cx.run_until_parked();
25522 cx.assert_editor_state(indoc! {"
25523 case \"$1\" in
25524 start)
25525 ˇ
25526 "});
25527
25528 // test correct indent after newline after case pattern
25529 cx.set_state(indoc! {"
25530 case \"$1\" in
25531 start)
25532 ;;
25533 *)ˇ
25534 "});
25535 cx.update_editor(|editor, window, cx| {
25536 editor.newline(&Newline, window, cx);
25537 });
25538 cx.run_until_parked();
25539 cx.assert_editor_state(indoc! {"
25540 case \"$1\" in
25541 start)
25542 ;;
25543 *)
25544 ˇ
25545 "});
25546
25547 // test correct indent after newline after function opening brace
25548 cx.set_state(indoc! {"
25549 function test() {ˇ}
25550 "});
25551 cx.update_editor(|editor, window, cx| {
25552 editor.newline(&Newline, window, cx);
25553 });
25554 cx.run_until_parked();
25555 cx.assert_editor_state(indoc! {"
25556 function test() {
25557 ˇ
25558 }
25559 "});
25560
25561 // test no extra indent after semicolon on same line
25562 cx.set_state(indoc! {"
25563 echo \"test\";ˇ
25564 "});
25565 cx.update_editor(|editor, window, cx| {
25566 editor.newline(&Newline, window, cx);
25567 });
25568 cx.run_until_parked();
25569 cx.assert_editor_state(indoc! {"
25570 echo \"test\";
25571 ˇ
25572 "});
25573}
25574
25575fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25576 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25577 point..point
25578}
25579
25580#[track_caller]
25581fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25582 let (text, ranges) = marked_text_ranges(marked_text, true);
25583 assert_eq!(editor.text(cx), text);
25584 assert_eq!(
25585 editor.selections.ranges(&editor.display_snapshot(cx)),
25586 ranges,
25587 "Assert selections are {}",
25588 marked_text
25589 );
25590}
25591
25592pub fn handle_signature_help_request(
25593 cx: &mut EditorLspTestContext,
25594 mocked_response: lsp::SignatureHelp,
25595) -> impl Future<Output = ()> + use<> {
25596 let mut request =
25597 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25598 let mocked_response = mocked_response.clone();
25599 async move { Ok(Some(mocked_response)) }
25600 });
25601
25602 async move {
25603 request.next().await;
25604 }
25605}
25606
25607#[track_caller]
25608pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25609 cx.update_editor(|editor, _, _| {
25610 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25611 let entries = menu.entries.borrow();
25612 let entries = entries
25613 .iter()
25614 .map(|entry| entry.string.as_str())
25615 .collect::<Vec<_>>();
25616 assert_eq!(entries, expected);
25617 } else {
25618 panic!("Expected completions menu");
25619 }
25620 });
25621}
25622
25623/// Handle completion request passing a marked string specifying where the completion
25624/// should be triggered from using '|' character, what range should be replaced, and what completions
25625/// should be returned using '<' and '>' to delimit the range.
25626///
25627/// Also see `handle_completion_request_with_insert_and_replace`.
25628#[track_caller]
25629pub fn handle_completion_request(
25630 marked_string: &str,
25631 completions: Vec<&'static str>,
25632 is_incomplete: bool,
25633 counter: Arc<AtomicUsize>,
25634 cx: &mut EditorLspTestContext,
25635) -> impl Future<Output = ()> {
25636 let complete_from_marker: TextRangeMarker = '|'.into();
25637 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25638 let (_, mut marked_ranges) = marked_text_ranges_by(
25639 marked_string,
25640 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25641 );
25642
25643 let complete_from_position =
25644 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25645 let replace_range =
25646 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25647
25648 let mut request =
25649 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25650 let completions = completions.clone();
25651 counter.fetch_add(1, atomic::Ordering::Release);
25652 async move {
25653 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25654 assert_eq!(
25655 params.text_document_position.position,
25656 complete_from_position
25657 );
25658 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25659 is_incomplete,
25660 item_defaults: None,
25661 items: completions
25662 .iter()
25663 .map(|completion_text| lsp::CompletionItem {
25664 label: completion_text.to_string(),
25665 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25666 range: replace_range,
25667 new_text: completion_text.to_string(),
25668 })),
25669 ..Default::default()
25670 })
25671 .collect(),
25672 })))
25673 }
25674 });
25675
25676 async move {
25677 request.next().await;
25678 }
25679}
25680
25681/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25682/// given instead, which also contains an `insert` range.
25683///
25684/// This function uses markers to define ranges:
25685/// - `|` marks the cursor position
25686/// - `<>` marks the replace range
25687/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25688pub fn handle_completion_request_with_insert_and_replace(
25689 cx: &mut EditorLspTestContext,
25690 marked_string: &str,
25691 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25692 counter: Arc<AtomicUsize>,
25693) -> impl Future<Output = ()> {
25694 let complete_from_marker: TextRangeMarker = '|'.into();
25695 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25696 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25697
25698 let (_, mut marked_ranges) = marked_text_ranges_by(
25699 marked_string,
25700 vec![
25701 complete_from_marker.clone(),
25702 replace_range_marker.clone(),
25703 insert_range_marker.clone(),
25704 ],
25705 );
25706
25707 let complete_from_position =
25708 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25709 let replace_range =
25710 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25711
25712 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25713 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25714 _ => lsp::Range {
25715 start: replace_range.start,
25716 end: complete_from_position,
25717 },
25718 };
25719
25720 let mut request =
25721 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25722 let completions = completions.clone();
25723 counter.fetch_add(1, atomic::Ordering::Release);
25724 async move {
25725 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25726 assert_eq!(
25727 params.text_document_position.position, complete_from_position,
25728 "marker `|` position doesn't match",
25729 );
25730 Ok(Some(lsp::CompletionResponse::Array(
25731 completions
25732 .iter()
25733 .map(|(label, new_text)| lsp::CompletionItem {
25734 label: label.to_string(),
25735 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25736 lsp::InsertReplaceEdit {
25737 insert: insert_range,
25738 replace: replace_range,
25739 new_text: new_text.to_string(),
25740 },
25741 )),
25742 ..Default::default()
25743 })
25744 .collect(),
25745 )))
25746 }
25747 });
25748
25749 async move {
25750 request.next().await;
25751 }
25752}
25753
25754fn handle_resolve_completion_request(
25755 cx: &mut EditorLspTestContext,
25756 edits: Option<Vec<(&'static str, &'static str)>>,
25757) -> impl Future<Output = ()> {
25758 let edits = edits.map(|edits| {
25759 edits
25760 .iter()
25761 .map(|(marked_string, new_text)| {
25762 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25763 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25764 lsp::TextEdit::new(replace_range, new_text.to_string())
25765 })
25766 .collect::<Vec<_>>()
25767 });
25768
25769 let mut request =
25770 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25771 let edits = edits.clone();
25772 async move {
25773 Ok(lsp::CompletionItem {
25774 additional_text_edits: edits,
25775 ..Default::default()
25776 })
25777 }
25778 });
25779
25780 async move {
25781 request.next().await;
25782 }
25783}
25784
25785pub(crate) fn update_test_language_settings(
25786 cx: &mut TestAppContext,
25787 f: impl Fn(&mut AllLanguageSettingsContent),
25788) {
25789 cx.update(|cx| {
25790 SettingsStore::update_global(cx, |store, cx| {
25791 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25792 });
25793 });
25794}
25795
25796pub(crate) fn update_test_project_settings(
25797 cx: &mut TestAppContext,
25798 f: impl Fn(&mut ProjectSettingsContent),
25799) {
25800 cx.update(|cx| {
25801 SettingsStore::update_global(cx, |store, cx| {
25802 store.update_user_settings(cx, |settings| f(&mut settings.project));
25803 });
25804 });
25805}
25806
25807pub(crate) fn update_test_editor_settings(
25808 cx: &mut TestAppContext,
25809 f: impl Fn(&mut EditorSettingsContent),
25810) {
25811 cx.update(|cx| {
25812 SettingsStore::update_global(cx, |store, cx| {
25813 store.update_user_settings(cx, |settings| f(&mut settings.editor));
25814 })
25815 })
25816}
25817
25818pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25819 cx.update(|cx| {
25820 assets::Assets.load_test_fonts(cx);
25821 let store = SettingsStore::test(cx);
25822 cx.set_global(store);
25823 theme::init(theme::LoadThemes::JustBase, cx);
25824 release_channel::init(SemanticVersion::default(), cx);
25825 crate::init(cx);
25826 });
25827 zlog::init_test();
25828 update_test_language_settings(cx, f);
25829}
25830
25831#[track_caller]
25832fn assert_hunk_revert(
25833 not_reverted_text_with_selections: &str,
25834 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25835 expected_reverted_text_with_selections: &str,
25836 base_text: &str,
25837 cx: &mut EditorLspTestContext,
25838) {
25839 cx.set_state(not_reverted_text_with_selections);
25840 cx.set_head_text(base_text);
25841 cx.executor().run_until_parked();
25842
25843 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25844 let snapshot = editor.snapshot(window, cx);
25845 let reverted_hunk_statuses = snapshot
25846 .buffer_snapshot()
25847 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25848 .map(|hunk| hunk.status().kind)
25849 .collect::<Vec<_>>();
25850
25851 editor.git_restore(&Default::default(), window, cx);
25852 reverted_hunk_statuses
25853 });
25854 cx.executor().run_until_parked();
25855 cx.assert_editor_state(expected_reverted_text_with_selections);
25856 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25857}
25858
25859#[gpui::test(iterations = 10)]
25860async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25861 init_test(cx, |_| {});
25862
25863 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25864 let counter = diagnostic_requests.clone();
25865
25866 let fs = FakeFs::new(cx.executor());
25867 fs.insert_tree(
25868 path!("/a"),
25869 json!({
25870 "first.rs": "fn main() { let a = 5; }",
25871 "second.rs": "// Test file",
25872 }),
25873 )
25874 .await;
25875
25876 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25877 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25878 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25879
25880 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25881 language_registry.add(rust_lang());
25882 let mut fake_servers = language_registry.register_fake_lsp(
25883 "Rust",
25884 FakeLspAdapter {
25885 capabilities: lsp::ServerCapabilities {
25886 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25887 lsp::DiagnosticOptions {
25888 identifier: None,
25889 inter_file_dependencies: true,
25890 workspace_diagnostics: true,
25891 work_done_progress_options: Default::default(),
25892 },
25893 )),
25894 ..Default::default()
25895 },
25896 ..Default::default()
25897 },
25898 );
25899
25900 let editor = workspace
25901 .update(cx, |workspace, window, cx| {
25902 workspace.open_abs_path(
25903 PathBuf::from(path!("/a/first.rs")),
25904 OpenOptions::default(),
25905 window,
25906 cx,
25907 )
25908 })
25909 .unwrap()
25910 .await
25911 .unwrap()
25912 .downcast::<Editor>()
25913 .unwrap();
25914 let fake_server = fake_servers.next().await.unwrap();
25915 let server_id = fake_server.server.server_id();
25916 let mut first_request = fake_server
25917 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25918 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25919 let result_id = Some(new_result_id.to_string());
25920 assert_eq!(
25921 params.text_document.uri,
25922 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25923 );
25924 async move {
25925 Ok(lsp::DocumentDiagnosticReportResult::Report(
25926 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25927 related_documents: None,
25928 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25929 items: Vec::new(),
25930 result_id,
25931 },
25932 }),
25933 ))
25934 }
25935 });
25936
25937 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25938 project.update(cx, |project, cx| {
25939 let buffer_id = editor
25940 .read(cx)
25941 .buffer()
25942 .read(cx)
25943 .as_singleton()
25944 .expect("created a singleton buffer")
25945 .read(cx)
25946 .remote_id();
25947 let buffer_result_id = project
25948 .lsp_store()
25949 .read(cx)
25950 .result_id(server_id, buffer_id, cx);
25951 assert_eq!(expected, buffer_result_id);
25952 });
25953 };
25954
25955 ensure_result_id(None, cx);
25956 cx.executor().advance_clock(Duration::from_millis(60));
25957 cx.executor().run_until_parked();
25958 assert_eq!(
25959 diagnostic_requests.load(atomic::Ordering::Acquire),
25960 1,
25961 "Opening file should trigger diagnostic request"
25962 );
25963 first_request
25964 .next()
25965 .await
25966 .expect("should have sent the first diagnostics pull request");
25967 ensure_result_id(Some("1".to_string()), cx);
25968
25969 // Editing should trigger diagnostics
25970 editor.update_in(cx, |editor, window, cx| {
25971 editor.handle_input("2", window, cx)
25972 });
25973 cx.executor().advance_clock(Duration::from_millis(60));
25974 cx.executor().run_until_parked();
25975 assert_eq!(
25976 diagnostic_requests.load(atomic::Ordering::Acquire),
25977 2,
25978 "Editing should trigger diagnostic request"
25979 );
25980 ensure_result_id(Some("2".to_string()), cx);
25981
25982 // Moving cursor should not trigger diagnostic request
25983 editor.update_in(cx, |editor, window, cx| {
25984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25985 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25986 });
25987 });
25988 cx.executor().advance_clock(Duration::from_millis(60));
25989 cx.executor().run_until_parked();
25990 assert_eq!(
25991 diagnostic_requests.load(atomic::Ordering::Acquire),
25992 2,
25993 "Cursor movement should not trigger diagnostic request"
25994 );
25995 ensure_result_id(Some("2".to_string()), cx);
25996 // Multiple rapid edits should be debounced
25997 for _ in 0..5 {
25998 editor.update_in(cx, |editor, window, cx| {
25999 editor.handle_input("x", window, cx)
26000 });
26001 }
26002 cx.executor().advance_clock(Duration::from_millis(60));
26003 cx.executor().run_until_parked();
26004
26005 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26006 assert!(
26007 final_requests <= 4,
26008 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26009 );
26010 ensure_result_id(Some(final_requests.to_string()), cx);
26011}
26012
26013#[gpui::test]
26014async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26015 // Regression test for issue #11671
26016 // Previously, adding a cursor after moving multiple cursors would reset
26017 // the cursor count instead of adding to the existing cursors.
26018 init_test(cx, |_| {});
26019 let mut cx = EditorTestContext::new(cx).await;
26020
26021 // Create a simple buffer with cursor at start
26022 cx.set_state(indoc! {"
26023 ˇaaaa
26024 bbbb
26025 cccc
26026 dddd
26027 eeee
26028 ffff
26029 gggg
26030 hhhh"});
26031
26032 // Add 2 cursors below (so we have 3 total)
26033 cx.update_editor(|editor, window, cx| {
26034 editor.add_selection_below(&Default::default(), window, cx);
26035 editor.add_selection_below(&Default::default(), window, cx);
26036 });
26037
26038 // Verify we have 3 cursors
26039 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26040 assert_eq!(
26041 initial_count, 3,
26042 "Should have 3 cursors after adding 2 below"
26043 );
26044
26045 // Move down one line
26046 cx.update_editor(|editor, window, cx| {
26047 editor.move_down(&MoveDown, window, cx);
26048 });
26049
26050 // Add another cursor below
26051 cx.update_editor(|editor, window, cx| {
26052 editor.add_selection_below(&Default::default(), window, cx);
26053 });
26054
26055 // Should now have 4 cursors (3 original + 1 new)
26056 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26057 assert_eq!(
26058 final_count, 4,
26059 "Should have 4 cursors after moving and adding another"
26060 );
26061}
26062
26063#[gpui::test]
26064async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26065 init_test(cx, |_| {});
26066
26067 let mut cx = EditorTestContext::new(cx).await;
26068
26069 cx.set_state(indoc!(
26070 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26071 Second line here"#
26072 ));
26073
26074 cx.update_editor(|editor, window, cx| {
26075 // Enable soft wrapping with a narrow width to force soft wrapping and
26076 // confirm that more than 2 rows are being displayed.
26077 editor.set_wrap_width(Some(100.0.into()), cx);
26078 assert!(editor.display_text(cx).lines().count() > 2);
26079
26080 editor.add_selection_below(
26081 &AddSelectionBelow {
26082 skip_soft_wrap: true,
26083 },
26084 window,
26085 cx,
26086 );
26087
26088 assert_eq!(
26089 display_ranges(editor, cx),
26090 &[
26091 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26092 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26093 ]
26094 );
26095
26096 editor.add_selection_above(
26097 &AddSelectionAbove {
26098 skip_soft_wrap: true,
26099 },
26100 window,
26101 cx,
26102 );
26103
26104 assert_eq!(
26105 display_ranges(editor, cx),
26106 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26107 );
26108
26109 editor.add_selection_below(
26110 &AddSelectionBelow {
26111 skip_soft_wrap: false,
26112 },
26113 window,
26114 cx,
26115 );
26116
26117 assert_eq!(
26118 display_ranges(editor, cx),
26119 &[
26120 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26121 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26122 ]
26123 );
26124
26125 editor.add_selection_above(
26126 &AddSelectionAbove {
26127 skip_soft_wrap: false,
26128 },
26129 window,
26130 cx,
26131 );
26132
26133 assert_eq!(
26134 display_ranges(editor, cx),
26135 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26136 );
26137 });
26138}
26139
26140#[gpui::test(iterations = 10)]
26141async fn test_document_colors(cx: &mut TestAppContext) {
26142 let expected_color = Rgba {
26143 r: 0.33,
26144 g: 0.33,
26145 b: 0.33,
26146 a: 0.33,
26147 };
26148
26149 init_test(cx, |_| {});
26150
26151 let fs = FakeFs::new(cx.executor());
26152 fs.insert_tree(
26153 path!("/a"),
26154 json!({
26155 "first.rs": "fn main() { let a = 5; }",
26156 }),
26157 )
26158 .await;
26159
26160 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26161 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26162 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26163
26164 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26165 language_registry.add(rust_lang());
26166 let mut fake_servers = language_registry.register_fake_lsp(
26167 "Rust",
26168 FakeLspAdapter {
26169 capabilities: lsp::ServerCapabilities {
26170 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26171 ..lsp::ServerCapabilities::default()
26172 },
26173 name: "rust-analyzer",
26174 ..FakeLspAdapter::default()
26175 },
26176 );
26177 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26178 "Rust",
26179 FakeLspAdapter {
26180 capabilities: lsp::ServerCapabilities {
26181 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26182 ..lsp::ServerCapabilities::default()
26183 },
26184 name: "not-rust-analyzer",
26185 ..FakeLspAdapter::default()
26186 },
26187 );
26188
26189 let editor = workspace
26190 .update(cx, |workspace, window, cx| {
26191 workspace.open_abs_path(
26192 PathBuf::from(path!("/a/first.rs")),
26193 OpenOptions::default(),
26194 window,
26195 cx,
26196 )
26197 })
26198 .unwrap()
26199 .await
26200 .unwrap()
26201 .downcast::<Editor>()
26202 .unwrap();
26203 let fake_language_server = fake_servers.next().await.unwrap();
26204 let fake_language_server_without_capabilities =
26205 fake_servers_without_capabilities.next().await.unwrap();
26206 let requests_made = Arc::new(AtomicUsize::new(0));
26207 let closure_requests_made = Arc::clone(&requests_made);
26208 let mut color_request_handle = fake_language_server
26209 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26210 let requests_made = Arc::clone(&closure_requests_made);
26211 async move {
26212 assert_eq!(
26213 params.text_document.uri,
26214 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26215 );
26216 requests_made.fetch_add(1, atomic::Ordering::Release);
26217 Ok(vec![
26218 lsp::ColorInformation {
26219 range: lsp::Range {
26220 start: lsp::Position {
26221 line: 0,
26222 character: 0,
26223 },
26224 end: lsp::Position {
26225 line: 0,
26226 character: 1,
26227 },
26228 },
26229 color: lsp::Color {
26230 red: 0.33,
26231 green: 0.33,
26232 blue: 0.33,
26233 alpha: 0.33,
26234 },
26235 },
26236 lsp::ColorInformation {
26237 range: lsp::Range {
26238 start: lsp::Position {
26239 line: 0,
26240 character: 0,
26241 },
26242 end: lsp::Position {
26243 line: 0,
26244 character: 1,
26245 },
26246 },
26247 color: lsp::Color {
26248 red: 0.33,
26249 green: 0.33,
26250 blue: 0.33,
26251 alpha: 0.33,
26252 },
26253 },
26254 ])
26255 }
26256 });
26257
26258 let _handle = fake_language_server_without_capabilities
26259 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26260 panic!("Should not be called");
26261 });
26262 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26263 color_request_handle.next().await.unwrap();
26264 cx.run_until_parked();
26265 assert_eq!(
26266 1,
26267 requests_made.load(atomic::Ordering::Acquire),
26268 "Should query for colors once per editor open"
26269 );
26270 editor.update_in(cx, |editor, _, cx| {
26271 assert_eq!(
26272 vec![expected_color],
26273 extract_color_inlays(editor, cx),
26274 "Should have an initial inlay"
26275 );
26276 });
26277
26278 // opening another file in a split should not influence the LSP query counter
26279 workspace
26280 .update(cx, |workspace, window, cx| {
26281 assert_eq!(
26282 workspace.panes().len(),
26283 1,
26284 "Should have one pane with one editor"
26285 );
26286 workspace.move_item_to_pane_in_direction(
26287 &MoveItemToPaneInDirection {
26288 direction: SplitDirection::Right,
26289 focus: false,
26290 clone: true,
26291 },
26292 window,
26293 cx,
26294 );
26295 })
26296 .unwrap();
26297 cx.run_until_parked();
26298 workspace
26299 .update(cx, |workspace, _, cx| {
26300 let panes = workspace.panes();
26301 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26302 for pane in panes {
26303 let editor = pane
26304 .read(cx)
26305 .active_item()
26306 .and_then(|item| item.downcast::<Editor>())
26307 .expect("Should have opened an editor in each split");
26308 let editor_file = editor
26309 .read(cx)
26310 .buffer()
26311 .read(cx)
26312 .as_singleton()
26313 .expect("test deals with singleton buffers")
26314 .read(cx)
26315 .file()
26316 .expect("test buffese should have a file")
26317 .path();
26318 assert_eq!(
26319 editor_file.as_ref(),
26320 rel_path("first.rs"),
26321 "Both editors should be opened for the same file"
26322 )
26323 }
26324 })
26325 .unwrap();
26326
26327 cx.executor().advance_clock(Duration::from_millis(500));
26328 let save = editor.update_in(cx, |editor, window, cx| {
26329 editor.move_to_end(&MoveToEnd, window, cx);
26330 editor.handle_input("dirty", window, cx);
26331 editor.save(
26332 SaveOptions {
26333 format: true,
26334 autosave: true,
26335 },
26336 project.clone(),
26337 window,
26338 cx,
26339 )
26340 });
26341 save.await.unwrap();
26342
26343 color_request_handle.next().await.unwrap();
26344 cx.run_until_parked();
26345 assert_eq!(
26346 2,
26347 requests_made.load(atomic::Ordering::Acquire),
26348 "Should query for colors once per save (deduplicated) and once per formatting after save"
26349 );
26350
26351 drop(editor);
26352 let close = workspace
26353 .update(cx, |workspace, window, cx| {
26354 workspace.active_pane().update(cx, |pane, cx| {
26355 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26356 })
26357 })
26358 .unwrap();
26359 close.await.unwrap();
26360 let close = workspace
26361 .update(cx, |workspace, window, cx| {
26362 workspace.active_pane().update(cx, |pane, cx| {
26363 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26364 })
26365 })
26366 .unwrap();
26367 close.await.unwrap();
26368 assert_eq!(
26369 2,
26370 requests_made.load(atomic::Ordering::Acquire),
26371 "After saving and closing all editors, no extra requests should be made"
26372 );
26373 workspace
26374 .update(cx, |workspace, _, cx| {
26375 assert!(
26376 workspace.active_item(cx).is_none(),
26377 "Should close all editors"
26378 )
26379 })
26380 .unwrap();
26381
26382 workspace
26383 .update(cx, |workspace, window, cx| {
26384 workspace.active_pane().update(cx, |pane, cx| {
26385 pane.navigate_backward(&workspace::GoBack, window, cx);
26386 })
26387 })
26388 .unwrap();
26389 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26390 cx.run_until_parked();
26391 let editor = workspace
26392 .update(cx, |workspace, _, cx| {
26393 workspace
26394 .active_item(cx)
26395 .expect("Should have reopened the editor again after navigating back")
26396 .downcast::<Editor>()
26397 .expect("Should be an editor")
26398 })
26399 .unwrap();
26400
26401 assert_eq!(
26402 2,
26403 requests_made.load(atomic::Ordering::Acquire),
26404 "Cache should be reused on buffer close and reopen"
26405 );
26406 editor.update(cx, |editor, cx| {
26407 assert_eq!(
26408 vec![expected_color],
26409 extract_color_inlays(editor, cx),
26410 "Should have an initial inlay"
26411 );
26412 });
26413
26414 drop(color_request_handle);
26415 let closure_requests_made = Arc::clone(&requests_made);
26416 let mut empty_color_request_handle = fake_language_server
26417 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26418 let requests_made = Arc::clone(&closure_requests_made);
26419 async move {
26420 assert_eq!(
26421 params.text_document.uri,
26422 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26423 );
26424 requests_made.fetch_add(1, atomic::Ordering::Release);
26425 Ok(Vec::new())
26426 }
26427 });
26428 let save = editor.update_in(cx, |editor, window, cx| {
26429 editor.move_to_end(&MoveToEnd, window, cx);
26430 editor.handle_input("dirty_again", window, cx);
26431 editor.save(
26432 SaveOptions {
26433 format: false,
26434 autosave: true,
26435 },
26436 project.clone(),
26437 window,
26438 cx,
26439 )
26440 });
26441 save.await.unwrap();
26442
26443 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26444 empty_color_request_handle.next().await.unwrap();
26445 cx.run_until_parked();
26446 assert_eq!(
26447 3,
26448 requests_made.load(atomic::Ordering::Acquire),
26449 "Should query for colors once per save only, as formatting was not requested"
26450 );
26451 editor.update(cx, |editor, cx| {
26452 assert_eq!(
26453 Vec::<Rgba>::new(),
26454 extract_color_inlays(editor, cx),
26455 "Should clear all colors when the server returns an empty response"
26456 );
26457 });
26458}
26459
26460#[gpui::test]
26461async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26462 init_test(cx, |_| {});
26463 let (editor, cx) = cx.add_window_view(Editor::single_line);
26464 editor.update_in(cx, |editor, window, cx| {
26465 editor.set_text("oops\n\nwow\n", window, cx)
26466 });
26467 cx.run_until_parked();
26468 editor.update(cx, |editor, cx| {
26469 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26470 });
26471 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26472 cx.run_until_parked();
26473 editor.update(cx, |editor, cx| {
26474 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26475 });
26476}
26477
26478#[gpui::test]
26479async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26480 init_test(cx, |_| {});
26481
26482 cx.update(|cx| {
26483 register_project_item::<Editor>(cx);
26484 });
26485
26486 let fs = FakeFs::new(cx.executor());
26487 fs.insert_tree("/root1", json!({})).await;
26488 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26489 .await;
26490
26491 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26492 let (workspace, cx) =
26493 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26494
26495 let worktree_id = project.update(cx, |project, cx| {
26496 project.worktrees(cx).next().unwrap().read(cx).id()
26497 });
26498
26499 let handle = workspace
26500 .update_in(cx, |workspace, window, cx| {
26501 let project_path = (worktree_id, rel_path("one.pdf"));
26502 workspace.open_path(project_path, None, true, window, cx)
26503 })
26504 .await
26505 .unwrap();
26506
26507 assert_eq!(
26508 handle.to_any().entity_type(),
26509 TypeId::of::<InvalidItemView>()
26510 );
26511}
26512
26513#[gpui::test]
26514async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26515 init_test(cx, |_| {});
26516
26517 let language = Arc::new(Language::new(
26518 LanguageConfig::default(),
26519 Some(tree_sitter_rust::LANGUAGE.into()),
26520 ));
26521
26522 // Test hierarchical sibling navigation
26523 let text = r#"
26524 fn outer() {
26525 if condition {
26526 let a = 1;
26527 }
26528 let b = 2;
26529 }
26530
26531 fn another() {
26532 let c = 3;
26533 }
26534 "#;
26535
26536 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26537 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26538 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26539
26540 // Wait for parsing to complete
26541 editor
26542 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26543 .await;
26544
26545 editor.update_in(cx, |editor, window, cx| {
26546 // Start by selecting "let a = 1;" inside the if block
26547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26548 s.select_display_ranges([
26549 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26550 ]);
26551 });
26552
26553 let initial_selection = editor
26554 .selections
26555 .display_ranges(&editor.display_snapshot(cx));
26556 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26557
26558 // Test select next sibling - should move up levels to find the next sibling
26559 // Since "let a = 1;" has no siblings in the if block, it should move up
26560 // to find "let b = 2;" which is a sibling of the if block
26561 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26562 let next_selection = editor
26563 .selections
26564 .display_ranges(&editor.display_snapshot(cx));
26565
26566 // Should have a selection and it should be different from the initial
26567 assert_eq!(
26568 next_selection.len(),
26569 1,
26570 "Should have one selection after next"
26571 );
26572 assert_ne!(
26573 next_selection[0], initial_selection[0],
26574 "Next sibling selection should be different"
26575 );
26576
26577 // Test hierarchical navigation by going to the end of the current function
26578 // and trying to navigate to the next function
26579 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26580 s.select_display_ranges([
26581 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26582 ]);
26583 });
26584
26585 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26586 let function_next_selection = editor
26587 .selections
26588 .display_ranges(&editor.display_snapshot(cx));
26589
26590 // Should move to the next function
26591 assert_eq!(
26592 function_next_selection.len(),
26593 1,
26594 "Should have one selection after function next"
26595 );
26596
26597 // Test select previous sibling navigation
26598 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26599 let prev_selection = editor
26600 .selections
26601 .display_ranges(&editor.display_snapshot(cx));
26602
26603 // Should have a selection and it should be different
26604 assert_eq!(
26605 prev_selection.len(),
26606 1,
26607 "Should have one selection after prev"
26608 );
26609 assert_ne!(
26610 prev_selection[0], function_next_selection[0],
26611 "Previous sibling selection should be different from next"
26612 );
26613 });
26614}
26615
26616#[gpui::test]
26617async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26618 init_test(cx, |_| {});
26619
26620 let mut cx = EditorTestContext::new(cx).await;
26621 cx.set_state(
26622 "let ˇvariable = 42;
26623let another = variable + 1;
26624let result = variable * 2;",
26625 );
26626
26627 // Set up document highlights manually (simulating LSP response)
26628 cx.update_editor(|editor, _window, cx| {
26629 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26630
26631 // Create highlights for "variable" occurrences
26632 let highlight_ranges = [
26633 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26634 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26635 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26636 ];
26637
26638 let anchor_ranges: Vec<_> = highlight_ranges
26639 .iter()
26640 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26641 .collect();
26642
26643 editor.highlight_background::<DocumentHighlightRead>(
26644 &anchor_ranges,
26645 |theme| theme.colors().editor_document_highlight_read_background,
26646 cx,
26647 );
26648 });
26649
26650 // Go to next highlight - should move to second "variable"
26651 cx.update_editor(|editor, window, cx| {
26652 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26653 });
26654 cx.assert_editor_state(
26655 "let variable = 42;
26656let another = ˇvariable + 1;
26657let result = variable * 2;",
26658 );
26659
26660 // Go to next highlight - should move to third "variable"
26661 cx.update_editor(|editor, window, cx| {
26662 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26663 });
26664 cx.assert_editor_state(
26665 "let variable = 42;
26666let another = variable + 1;
26667let result = ˇvariable * 2;",
26668 );
26669
26670 // Go to next highlight - should stay at third "variable" (no wrap-around)
26671 cx.update_editor(|editor, window, cx| {
26672 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26673 });
26674 cx.assert_editor_state(
26675 "let variable = 42;
26676let another = variable + 1;
26677let result = ˇvariable * 2;",
26678 );
26679
26680 // Now test going backwards from third position
26681 cx.update_editor(|editor, window, cx| {
26682 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26683 });
26684 cx.assert_editor_state(
26685 "let variable = 42;
26686let another = ˇvariable + 1;
26687let result = variable * 2;",
26688 );
26689
26690 // Go to previous highlight - should move to first "variable"
26691 cx.update_editor(|editor, window, cx| {
26692 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26693 });
26694 cx.assert_editor_state(
26695 "let ˇvariable = 42;
26696let another = variable + 1;
26697let result = variable * 2;",
26698 );
26699
26700 // Go to previous highlight - should stay on first "variable"
26701 cx.update_editor(|editor, window, cx| {
26702 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26703 });
26704 cx.assert_editor_state(
26705 "let ˇvariable = 42;
26706let another = variable + 1;
26707let result = variable * 2;",
26708 );
26709}
26710
26711#[gpui::test]
26712async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26713 cx: &mut gpui::TestAppContext,
26714) {
26715 init_test(cx, |_| {});
26716
26717 let url = "https://zed.dev";
26718
26719 let markdown_language = Arc::new(Language::new(
26720 LanguageConfig {
26721 name: "Markdown".into(),
26722 ..LanguageConfig::default()
26723 },
26724 None,
26725 ));
26726
26727 let mut cx = EditorTestContext::new(cx).await;
26728 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26729 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26730
26731 cx.update_editor(|editor, window, cx| {
26732 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26733 editor.paste(&Paste, window, cx);
26734 });
26735
26736 cx.assert_editor_state(&format!(
26737 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26738 ));
26739}
26740
26741#[gpui::test]
26742async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26743 cx: &mut gpui::TestAppContext,
26744) {
26745 init_test(cx, |_| {});
26746
26747 let url = "https://zed.dev";
26748
26749 let markdown_language = Arc::new(Language::new(
26750 LanguageConfig {
26751 name: "Markdown".into(),
26752 ..LanguageConfig::default()
26753 },
26754 None,
26755 ));
26756
26757 let mut cx = EditorTestContext::new(cx).await;
26758 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26759 cx.set_state(&format!(
26760 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26761 ));
26762
26763 cx.update_editor(|editor, window, cx| {
26764 editor.copy(&Copy, window, cx);
26765 });
26766
26767 cx.set_state(&format!(
26768 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26769 ));
26770
26771 cx.update_editor(|editor, window, cx| {
26772 editor.paste(&Paste, window, cx);
26773 });
26774
26775 cx.assert_editor_state(&format!(
26776 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26777 ));
26778}
26779
26780#[gpui::test]
26781async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26782 cx: &mut gpui::TestAppContext,
26783) {
26784 init_test(cx, |_| {});
26785
26786 let url = "https://zed.dev";
26787
26788 let markdown_language = Arc::new(Language::new(
26789 LanguageConfig {
26790 name: "Markdown".into(),
26791 ..LanguageConfig::default()
26792 },
26793 None,
26794 ));
26795
26796 let mut cx = EditorTestContext::new(cx).await;
26797 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26798 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26799
26800 cx.update_editor(|editor, window, cx| {
26801 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26802 editor.paste(&Paste, window, cx);
26803 });
26804
26805 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26806}
26807
26808#[gpui::test]
26809async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26810 cx: &mut gpui::TestAppContext,
26811) {
26812 init_test(cx, |_| {});
26813
26814 let text = "Awesome";
26815
26816 let markdown_language = Arc::new(Language::new(
26817 LanguageConfig {
26818 name: "Markdown".into(),
26819 ..LanguageConfig::default()
26820 },
26821 None,
26822 ));
26823
26824 let mut cx = EditorTestContext::new(cx).await;
26825 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26826 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26827
26828 cx.update_editor(|editor, window, cx| {
26829 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26830 editor.paste(&Paste, window, cx);
26831 });
26832
26833 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26834}
26835
26836#[gpui::test]
26837async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26838 cx: &mut gpui::TestAppContext,
26839) {
26840 init_test(cx, |_| {});
26841
26842 let url = "https://zed.dev";
26843
26844 let markdown_language = Arc::new(Language::new(
26845 LanguageConfig {
26846 name: "Rust".into(),
26847 ..LanguageConfig::default()
26848 },
26849 None,
26850 ));
26851
26852 let mut cx = EditorTestContext::new(cx).await;
26853 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26854 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26855
26856 cx.update_editor(|editor, window, cx| {
26857 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26858 editor.paste(&Paste, window, cx);
26859 });
26860
26861 cx.assert_editor_state(&format!(
26862 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26863 ));
26864}
26865
26866#[gpui::test]
26867async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26868 cx: &mut TestAppContext,
26869) {
26870 init_test(cx, |_| {});
26871
26872 let url = "https://zed.dev";
26873
26874 let markdown_language = Arc::new(Language::new(
26875 LanguageConfig {
26876 name: "Markdown".into(),
26877 ..LanguageConfig::default()
26878 },
26879 None,
26880 ));
26881
26882 let (editor, cx) = cx.add_window_view(|window, cx| {
26883 let multi_buffer = MultiBuffer::build_multi(
26884 [
26885 ("this will embed -> link", vec![Point::row_range(0..1)]),
26886 ("this will replace -> link", vec![Point::row_range(0..1)]),
26887 ],
26888 cx,
26889 );
26890 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26892 s.select_ranges(vec![
26893 Point::new(0, 19)..Point::new(0, 23),
26894 Point::new(1, 21)..Point::new(1, 25),
26895 ])
26896 });
26897 let first_buffer_id = multi_buffer
26898 .read(cx)
26899 .excerpt_buffer_ids()
26900 .into_iter()
26901 .next()
26902 .unwrap();
26903 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26904 first_buffer.update(cx, |buffer, cx| {
26905 buffer.set_language(Some(markdown_language.clone()), cx);
26906 });
26907
26908 editor
26909 });
26910 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26911
26912 cx.update_editor(|editor, window, cx| {
26913 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26914 editor.paste(&Paste, window, cx);
26915 });
26916
26917 cx.assert_editor_state(&format!(
26918 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26919 ));
26920}
26921
26922#[gpui::test]
26923async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26924 init_test(cx, |_| {});
26925
26926 let fs = FakeFs::new(cx.executor());
26927 fs.insert_tree(
26928 path!("/project"),
26929 json!({
26930 "first.rs": "# First Document\nSome content here.",
26931 "second.rs": "Plain text content for second file.",
26932 }),
26933 )
26934 .await;
26935
26936 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26937 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26938 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26939
26940 let language = rust_lang();
26941 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26942 language_registry.add(language.clone());
26943 let mut fake_servers = language_registry.register_fake_lsp(
26944 "Rust",
26945 FakeLspAdapter {
26946 ..FakeLspAdapter::default()
26947 },
26948 );
26949
26950 let buffer1 = project
26951 .update(cx, |project, cx| {
26952 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26953 })
26954 .await
26955 .unwrap();
26956 let buffer2 = project
26957 .update(cx, |project, cx| {
26958 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26959 })
26960 .await
26961 .unwrap();
26962
26963 let multi_buffer = cx.new(|cx| {
26964 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26965 multi_buffer.set_excerpts_for_path(
26966 PathKey::for_buffer(&buffer1, cx),
26967 buffer1.clone(),
26968 [Point::zero()..buffer1.read(cx).max_point()],
26969 3,
26970 cx,
26971 );
26972 multi_buffer.set_excerpts_for_path(
26973 PathKey::for_buffer(&buffer2, cx),
26974 buffer2.clone(),
26975 [Point::zero()..buffer1.read(cx).max_point()],
26976 3,
26977 cx,
26978 );
26979 multi_buffer
26980 });
26981
26982 let (editor, cx) = cx.add_window_view(|window, cx| {
26983 Editor::new(
26984 EditorMode::full(),
26985 multi_buffer,
26986 Some(project.clone()),
26987 window,
26988 cx,
26989 )
26990 });
26991
26992 let fake_language_server = fake_servers.next().await.unwrap();
26993
26994 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26995
26996 let save = editor.update_in(cx, |editor, window, cx| {
26997 assert!(editor.is_dirty(cx));
26998
26999 editor.save(
27000 SaveOptions {
27001 format: true,
27002 autosave: true,
27003 },
27004 project,
27005 window,
27006 cx,
27007 )
27008 });
27009 let (start_edit_tx, start_edit_rx) = oneshot::channel();
27010 let (done_edit_tx, done_edit_rx) = oneshot::channel();
27011 let mut done_edit_rx = Some(done_edit_rx);
27012 let mut start_edit_tx = Some(start_edit_tx);
27013
27014 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
27015 start_edit_tx.take().unwrap().send(()).unwrap();
27016 let done_edit_rx = done_edit_rx.take().unwrap();
27017 async move {
27018 done_edit_rx.await.unwrap();
27019 Ok(None)
27020 }
27021 });
27022
27023 start_edit_rx.await.unwrap();
27024 buffer2
27025 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
27026 .unwrap();
27027
27028 done_edit_tx.send(()).unwrap();
27029
27030 save.await.unwrap();
27031 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
27032}
27033
27034#[track_caller]
27035fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
27036 editor
27037 .all_inlays(cx)
27038 .into_iter()
27039 .filter_map(|inlay| inlay.get_color())
27040 .map(Rgba::from)
27041 .collect()
27042}
27043
27044#[gpui::test]
27045fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
27046 init_test(cx, |_| {});
27047
27048 let editor = cx.add_window(|window, cx| {
27049 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
27050 build_editor(buffer, window, cx)
27051 });
27052
27053 editor
27054 .update(cx, |editor, window, cx| {
27055 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27056 s.select_display_ranges([
27057 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
27058 ])
27059 });
27060
27061 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
27062
27063 assert_eq!(
27064 editor.display_text(cx),
27065 "line1\nline2\nline2",
27066 "Duplicating last line upward should create duplicate above, not on same line"
27067 );
27068
27069 assert_eq!(
27070 editor
27071 .selections
27072 .display_ranges(&editor.display_snapshot(cx)),
27073 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27074 "Selection should move to the duplicated line"
27075 );
27076 })
27077 .unwrap();
27078}
27079
27080#[gpui::test]
27081async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27082 init_test(cx, |_| {});
27083
27084 let mut cx = EditorTestContext::new(cx).await;
27085
27086 cx.set_state("line1\nline2ˇ");
27087
27088 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27089
27090 let clipboard_text = cx
27091 .read_from_clipboard()
27092 .and_then(|item| item.text().as_deref().map(str::to_string));
27093
27094 assert_eq!(
27095 clipboard_text,
27096 Some("line2\n".to_string()),
27097 "Copying a line without trailing newline should include a newline"
27098 );
27099
27100 cx.set_state("line1\nˇ");
27101
27102 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27103
27104 cx.assert_editor_state("line1\nline2\nˇ");
27105}
27106
27107#[gpui::test]
27108async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27109 init_test(cx, |_| {});
27110
27111 let mut cx = EditorTestContext::new(cx).await;
27112
27113 cx.set_state("line1\nline2ˇ");
27114 cx.update_editor(|e, window, cx| {
27115 e.set_mode(EditorMode::SingleLine);
27116 assert!(e.key_context(window, cx).contains("end_of_input"));
27117 });
27118 cx.set_state("ˇline1\nline2");
27119 cx.update_editor(|e, window, cx| {
27120 assert!(!e.key_context(window, cx).contains("end_of_input"));
27121 });
27122 cx.set_state("line1ˇ\nline2");
27123 cx.update_editor(|e, window, cx| {
27124 assert!(!e.key_context(window, cx).contains("end_of_input"));
27125 });
27126}
27127
27128#[gpui::test]
27129async fn test_sticky_scroll(cx: &mut TestAppContext) {
27130 init_test(cx, |_| {});
27131 let mut cx = EditorTestContext::new(cx).await;
27132
27133 let buffer = indoc! {"
27134 ˇfn foo() {
27135 let abc = 123;
27136 }
27137 struct Bar;
27138 impl Bar {
27139 fn new() -> Self {
27140 Self
27141 }
27142 }
27143 fn baz() {
27144 }
27145 "};
27146 cx.set_state(&buffer);
27147
27148 cx.update_editor(|e, _, cx| {
27149 e.buffer()
27150 .read(cx)
27151 .as_singleton()
27152 .unwrap()
27153 .update(cx, |buffer, cx| {
27154 buffer.set_language(Some(rust_lang()), cx);
27155 })
27156 });
27157
27158 let mut sticky_headers = |offset: ScrollOffset| {
27159 cx.update_editor(|e, window, cx| {
27160 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27161 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27162 .into_iter()
27163 .map(
27164 |StickyHeader {
27165 start_point,
27166 offset,
27167 ..
27168 }| { (start_point, offset) },
27169 )
27170 .collect::<Vec<_>>()
27171 })
27172 };
27173
27174 let fn_foo = Point { row: 0, column: 0 };
27175 let impl_bar = Point { row: 4, column: 0 };
27176 let fn_new = Point { row: 5, column: 4 };
27177
27178 assert_eq!(sticky_headers(0.0), vec![]);
27179 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27180 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27181 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27182 assert_eq!(sticky_headers(2.0), vec![]);
27183 assert_eq!(sticky_headers(2.5), vec![]);
27184 assert_eq!(sticky_headers(3.0), vec![]);
27185 assert_eq!(sticky_headers(3.5), vec![]);
27186 assert_eq!(sticky_headers(4.0), vec![]);
27187 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27188 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27189 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27190 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27191 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27192 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27193 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27194 assert_eq!(sticky_headers(8.0), vec![]);
27195 assert_eq!(sticky_headers(8.5), vec![]);
27196 assert_eq!(sticky_headers(9.0), vec![]);
27197 assert_eq!(sticky_headers(9.5), vec![]);
27198 assert_eq!(sticky_headers(10.0), vec![]);
27199}
27200
27201#[gpui::test]
27202async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27203 init_test(cx, |_| {});
27204 cx.update(|cx| {
27205 SettingsStore::update_global(cx, |store, cx| {
27206 store.update_user_settings(cx, |settings| {
27207 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27208 enabled: Some(true),
27209 })
27210 });
27211 });
27212 });
27213 let mut cx = EditorTestContext::new(cx).await;
27214
27215 let line_height = cx.editor(|editor, window, _cx| {
27216 editor
27217 .style()
27218 .unwrap()
27219 .text
27220 .line_height_in_pixels(window.rem_size())
27221 });
27222
27223 let buffer = indoc! {"
27224 ˇfn foo() {
27225 let abc = 123;
27226 }
27227 struct Bar;
27228 impl Bar {
27229 fn new() -> Self {
27230 Self
27231 }
27232 }
27233 fn baz() {
27234 }
27235 "};
27236 cx.set_state(&buffer);
27237
27238 cx.update_editor(|e, _, cx| {
27239 e.buffer()
27240 .read(cx)
27241 .as_singleton()
27242 .unwrap()
27243 .update(cx, |buffer, cx| {
27244 buffer.set_language(Some(rust_lang()), cx);
27245 })
27246 });
27247
27248 let fn_foo = || empty_range(0, 0);
27249 let impl_bar = || empty_range(4, 0);
27250 let fn_new = || empty_range(5, 4);
27251
27252 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27253 cx.update_editor(|e, window, cx| {
27254 e.scroll(
27255 gpui::Point {
27256 x: 0.,
27257 y: scroll_offset,
27258 },
27259 None,
27260 window,
27261 cx,
27262 );
27263 });
27264 cx.simulate_click(
27265 gpui::Point {
27266 x: px(0.),
27267 y: click_offset as f32 * line_height,
27268 },
27269 Modifiers::none(),
27270 );
27271 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27272 };
27273
27274 assert_eq!(
27275 scroll_and_click(
27276 4.5, // impl Bar is halfway off the screen
27277 0.0 // click top of screen
27278 ),
27279 // scrolled to impl Bar
27280 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27281 );
27282
27283 assert_eq!(
27284 scroll_and_click(
27285 4.5, // impl Bar is halfway off the screen
27286 0.25 // click middle of impl Bar
27287 ),
27288 // scrolled to impl Bar
27289 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27290 );
27291
27292 assert_eq!(
27293 scroll_and_click(
27294 4.5, // impl Bar is halfway off the screen
27295 1.5 // click below impl Bar (e.g. fn new())
27296 ),
27297 // scrolled to fn new() - this is below the impl Bar header which has persisted
27298 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27299 );
27300
27301 assert_eq!(
27302 scroll_and_click(
27303 5.5, // fn new is halfway underneath impl Bar
27304 0.75 // click on the overlap of impl Bar and fn new()
27305 ),
27306 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27307 );
27308
27309 assert_eq!(
27310 scroll_and_click(
27311 5.5, // fn new is halfway underneath impl Bar
27312 1.25 // click on the visible part of fn new()
27313 ),
27314 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27315 );
27316
27317 assert_eq!(
27318 scroll_and_click(
27319 1.5, // fn foo is halfway off the screen
27320 0.0 // click top of screen
27321 ),
27322 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
27323 );
27324
27325 assert_eq!(
27326 scroll_and_click(
27327 1.5, // fn foo is halfway off the screen
27328 0.75 // click visible part of let abc...
27329 )
27330 .0,
27331 // no change in scroll
27332 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
27333 (gpui::Point { x: 0., y: 1.5 })
27334 );
27335}
27336
27337#[gpui::test]
27338async fn test_next_prev_reference(cx: &mut TestAppContext) {
27339 const CYCLE_POSITIONS: &[&'static str] = &[
27340 indoc! {"
27341 fn foo() {
27342 let ˇabc = 123;
27343 let x = abc + 1;
27344 let y = abc + 2;
27345 let z = abc + 2;
27346 }
27347 "},
27348 indoc! {"
27349 fn foo() {
27350 let abc = 123;
27351 let x = ˇabc + 1;
27352 let y = abc + 2;
27353 let z = abc + 2;
27354 }
27355 "},
27356 indoc! {"
27357 fn foo() {
27358 let abc = 123;
27359 let x = abc + 1;
27360 let y = ˇabc + 2;
27361 let z = abc + 2;
27362 }
27363 "},
27364 indoc! {"
27365 fn foo() {
27366 let abc = 123;
27367 let x = abc + 1;
27368 let y = abc + 2;
27369 let z = ˇabc + 2;
27370 }
27371 "},
27372 ];
27373
27374 init_test(cx, |_| {});
27375
27376 let mut cx = EditorLspTestContext::new_rust(
27377 lsp::ServerCapabilities {
27378 references_provider: Some(lsp::OneOf::Left(true)),
27379 ..Default::default()
27380 },
27381 cx,
27382 )
27383 .await;
27384
27385 // importantly, the cursor is in the middle
27386 cx.set_state(indoc! {"
27387 fn foo() {
27388 let aˇbc = 123;
27389 let x = abc + 1;
27390 let y = abc + 2;
27391 let z = abc + 2;
27392 }
27393 "});
27394
27395 let reference_ranges = [
27396 lsp::Position::new(1, 8),
27397 lsp::Position::new(2, 12),
27398 lsp::Position::new(3, 12),
27399 lsp::Position::new(4, 12),
27400 ]
27401 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27402
27403 cx.lsp
27404 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27405 Ok(Some(
27406 reference_ranges
27407 .map(|range| lsp::Location {
27408 uri: params.text_document_position.text_document.uri.clone(),
27409 range,
27410 })
27411 .to_vec(),
27412 ))
27413 });
27414
27415 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27416 cx.update_editor(|editor, window, cx| {
27417 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27418 })
27419 .unwrap()
27420 .await
27421 .unwrap()
27422 };
27423
27424 _move(Direction::Next, 1, &mut cx).await;
27425 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27426
27427 _move(Direction::Next, 1, &mut cx).await;
27428 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27429
27430 _move(Direction::Next, 1, &mut cx).await;
27431 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27432
27433 // loops back to the start
27434 _move(Direction::Next, 1, &mut cx).await;
27435 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27436
27437 // loops back to the end
27438 _move(Direction::Prev, 1, &mut cx).await;
27439 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27440
27441 _move(Direction::Prev, 1, &mut cx).await;
27442 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27443
27444 _move(Direction::Prev, 1, &mut cx).await;
27445 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27446
27447 _move(Direction::Prev, 1, &mut cx).await;
27448 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27449
27450 _move(Direction::Next, 3, &mut cx).await;
27451 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27452
27453 _move(Direction::Prev, 2, &mut cx).await;
27454 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27455}
27456
27457#[gpui::test]
27458async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
27459 init_test(cx, |_| {});
27460
27461 let (editor, cx) = cx.add_window_view(|window, cx| {
27462 let multi_buffer = MultiBuffer::build_multi(
27463 [
27464 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
27465 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
27466 ],
27467 cx,
27468 );
27469 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
27470 });
27471
27472 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27473 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
27474
27475 cx.assert_excerpts_with_selections(indoc! {"
27476 [EXCERPT]
27477 ˇ1
27478 2
27479 3
27480 [EXCERPT]
27481 1
27482 2
27483 3
27484 "});
27485
27486 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
27487 cx.update_editor(|editor, window, cx| {
27488 editor.change_selections(None.into(), window, cx, |s| {
27489 s.select_ranges([2..3]);
27490 });
27491 });
27492 cx.assert_excerpts_with_selections(indoc! {"
27493 [EXCERPT]
27494 1
27495 2ˇ
27496 3
27497 [EXCERPT]
27498 1
27499 2
27500 3
27501 "});
27502
27503 cx.update_editor(|editor, window, cx| {
27504 editor
27505 .select_all_matches(&SelectAllMatches, window, cx)
27506 .unwrap();
27507 });
27508 cx.assert_excerpts_with_selections(indoc! {"
27509 [EXCERPT]
27510 1
27511 2ˇ
27512 3
27513 [EXCERPT]
27514 1
27515 2ˇ
27516 3
27517 "});
27518
27519 cx.update_editor(|editor, window, cx| {
27520 editor.handle_input("X", window, cx);
27521 });
27522 cx.assert_excerpts_with_selections(indoc! {"
27523 [EXCERPT]
27524 1
27525 Xˇ
27526 3
27527 [EXCERPT]
27528 1
27529 Xˇ
27530 3
27531 "});
27532
27533 // Scenario 2: Select "2", then fold second buffer before insertion
27534 cx.update_multibuffer(|mb, cx| {
27535 for buffer_id in buffer_ids.iter() {
27536 let buffer = mb.buffer(*buffer_id).unwrap();
27537 buffer.update(cx, |buffer, cx| {
27538 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
27539 });
27540 }
27541 });
27542
27543 // Select "2" and select all matches
27544 cx.update_editor(|editor, window, cx| {
27545 editor.change_selections(None.into(), window, cx, |s| {
27546 s.select_ranges([2..3]);
27547 });
27548 editor
27549 .select_all_matches(&SelectAllMatches, window, cx)
27550 .unwrap();
27551 });
27552
27553 // Fold second buffer - should remove selections from folded buffer
27554 cx.update_editor(|editor, _, cx| {
27555 editor.fold_buffer(buffer_ids[1], cx);
27556 });
27557 cx.assert_excerpts_with_selections(indoc! {"
27558 [EXCERPT]
27559 1
27560 2ˇ
27561 3
27562 [EXCERPT]
27563 [FOLDED]
27564 "});
27565
27566 // Insert text - should only affect first buffer
27567 cx.update_editor(|editor, window, cx| {
27568 editor.handle_input("Y", window, cx);
27569 });
27570 cx.update_editor(|editor, _, cx| {
27571 editor.unfold_buffer(buffer_ids[1], cx);
27572 });
27573 cx.assert_excerpts_with_selections(indoc! {"
27574 [EXCERPT]
27575 1
27576 Yˇ
27577 3
27578 [EXCERPT]
27579 1
27580 2
27581 3
27582 "});
27583
27584 // Scenario 3: Select "2", then fold first buffer before insertion
27585 cx.update_multibuffer(|mb, cx| {
27586 for buffer_id in buffer_ids.iter() {
27587 let buffer = mb.buffer(*buffer_id).unwrap();
27588 buffer.update(cx, |buffer, cx| {
27589 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
27590 });
27591 }
27592 });
27593
27594 // Select "2" and select all matches
27595 cx.update_editor(|editor, window, cx| {
27596 editor.change_selections(None.into(), window, cx, |s| {
27597 s.select_ranges([2..3]);
27598 });
27599 editor
27600 .select_all_matches(&SelectAllMatches, window, cx)
27601 .unwrap();
27602 });
27603
27604 // Fold first buffer - should remove selections from folded buffer
27605 cx.update_editor(|editor, _, cx| {
27606 editor.fold_buffer(buffer_ids[0], cx);
27607 });
27608 cx.assert_excerpts_with_selections(indoc! {"
27609 [EXCERPT]
27610 [FOLDED]
27611 [EXCERPT]
27612 1
27613 2ˇ
27614 3
27615 "});
27616
27617 // Insert text - should only affect second buffer
27618 cx.update_editor(|editor, window, cx| {
27619 editor.handle_input("Z", window, cx);
27620 });
27621 cx.update_editor(|editor, _, cx| {
27622 editor.unfold_buffer(buffer_ids[0], cx);
27623 });
27624 cx.assert_excerpts_with_selections(indoc! {"
27625 [EXCERPT]
27626 1
27627 2
27628 3
27629 [EXCERPT]
27630 1
27631 Zˇ
27632 3
27633 "});
27634
27635 // Edge case scenario: fold all buffers, then try to insert
27636 cx.update_editor(|editor, _, cx| {
27637 editor.fold_buffer(buffer_ids[0], cx);
27638 editor.fold_buffer(buffer_ids[1], cx);
27639 });
27640 cx.assert_excerpts_with_selections(indoc! {"
27641 [EXCERPT]
27642 ˇ[FOLDED]
27643 [EXCERPT]
27644 [FOLDED]
27645 "});
27646
27647 // Insert should work via default selection
27648 cx.update_editor(|editor, window, cx| {
27649 editor.handle_input("W", window, cx);
27650 });
27651 cx.update_editor(|editor, _, cx| {
27652 editor.unfold_buffer(buffer_ids[0], cx);
27653 editor.unfold_buffer(buffer_ids[1], cx);
27654 });
27655 cx.assert_excerpts_with_selections(indoc! {"
27656 [EXCERPT]
27657 Wˇ1
27658 2
27659 3
27660 [EXCERPT]
27661 1
27662 Z
27663 3
27664 "});
27665}