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, TestAppContext, UpdateGlobal, VisualTestContext,
21 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::markdown_lang;
36use languages::rust_lang;
37use lsp::CompletionParams;
38use multi_buffer::{IndentGuide, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey};
39use parking_lot::Mutex;
40use pretty_assertions::{assert_eq, assert_ne};
41use project::{
42 FakeFs,
43 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
44 project_settings::LspSettings,
45};
46use serde_json::{self, json};
47use settings::{
48 AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
49 IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
50};
51use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
52use std::{
53 iter,
54 sync::atomic::{self, AtomicUsize},
55};
56use test::build_editor_with_project;
57use text::ToPoint as _;
58use unindent::Unindent;
59use util::{
60 assert_set_eq, path,
61 rel_path::rel_path,
62 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
63 uri,
64};
65use workspace::{
66 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
67 OpenOptions, ViewId,
68 invalid_item_view::InvalidItemView,
69 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
70 register_project_item,
71};
72
73fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
74 editor
75 .selections
76 .display_ranges(&editor.display_snapshot(cx))
77}
78
79#[gpui::test]
80fn test_edit_events(cx: &mut TestAppContext) {
81 init_test(cx, |_| {});
82
83 let buffer = cx.new(|cx| {
84 let mut buffer = language::Buffer::local("123456", cx);
85 buffer.set_group_interval(Duration::from_secs(1));
86 buffer
87 });
88
89 let events = Rc::new(RefCell::new(Vec::new()));
90 let editor1 = cx.add_window({
91 let events = events.clone();
92 |window, cx| {
93 let entity = cx.entity();
94 cx.subscribe_in(
95 &entity,
96 window,
97 move |_, _, event: &EditorEvent, _, _| match event {
98 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
99 EditorEvent::BufferEdited => {
100 events.borrow_mut().push(("editor1", "buffer edited"))
101 }
102 _ => {}
103 },
104 )
105 .detach();
106 Editor::for_buffer(buffer.clone(), None, window, cx)
107 }
108 });
109
110 let editor2 = cx.add_window({
111 let events = events.clone();
112 |window, cx| {
113 cx.subscribe_in(
114 &cx.entity(),
115 window,
116 move |_, _, event: &EditorEvent, _, _| match event {
117 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
118 EditorEvent::BufferEdited => {
119 events.borrow_mut().push(("editor2", "buffer edited"))
120 }
121 _ => {}
122 },
123 )
124 .detach();
125 Editor::for_buffer(buffer.clone(), None, window, cx)
126 }
127 });
128
129 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
130
131 // Mutating editor 1 will emit an `Edited` event only for that editor.
132 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
133 assert_eq!(
134 mem::take(&mut *events.borrow_mut()),
135 [
136 ("editor1", "edited"),
137 ("editor1", "buffer edited"),
138 ("editor2", "buffer edited"),
139 ]
140 );
141
142 // Mutating editor 2 will emit an `Edited` event only for that editor.
143 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
144 assert_eq!(
145 mem::take(&mut *events.borrow_mut()),
146 [
147 ("editor2", "edited"),
148 ("editor1", "buffer edited"),
149 ("editor2", "buffer edited"),
150 ]
151 );
152
153 // Undoing on editor 1 will emit an `Edited` event only for that editor.
154 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
155 assert_eq!(
156 mem::take(&mut *events.borrow_mut()),
157 [
158 ("editor1", "edited"),
159 ("editor1", "buffer edited"),
160 ("editor2", "buffer edited"),
161 ]
162 );
163
164 // Redoing on editor 1 will emit an `Edited` event only for that editor.
165 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
166 assert_eq!(
167 mem::take(&mut *events.borrow_mut()),
168 [
169 ("editor1", "edited"),
170 ("editor1", "buffer edited"),
171 ("editor2", "buffer edited"),
172 ]
173 );
174
175 // Undoing on editor 2 will emit an `Edited` event only for that editor.
176 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
177 assert_eq!(
178 mem::take(&mut *events.borrow_mut()),
179 [
180 ("editor2", "edited"),
181 ("editor1", "buffer edited"),
182 ("editor2", "buffer edited"),
183 ]
184 );
185
186 // Redoing on editor 2 will emit an `Edited` event only for that editor.
187 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
188 assert_eq!(
189 mem::take(&mut *events.borrow_mut()),
190 [
191 ("editor2", "edited"),
192 ("editor1", "buffer edited"),
193 ("editor2", "buffer edited"),
194 ]
195 );
196
197 // No event is emitted when the mutation is a no-op.
198 _ = editor2.update(cx, |editor, window, cx| {
199 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
200 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
201 });
202
203 editor.backspace(&Backspace, window, cx);
204 });
205 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
206}
207
208#[gpui::test]
209fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
210 init_test(cx, |_| {});
211
212 let mut now = Instant::now();
213 let group_interval = Duration::from_millis(1);
214 let buffer = cx.new(|cx| {
215 let mut buf = language::Buffer::local("123456", cx);
216 buf.set_group_interval(group_interval);
217 buf
218 });
219 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
220 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
221
222 _ = editor.update(cx, |editor, window, cx| {
223 editor.start_transaction_at(now, window, cx);
224 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
225 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
226 });
227
228 editor.insert("cd", window, cx);
229 editor.end_transaction_at(now, cx);
230 assert_eq!(editor.text(cx), "12cd56");
231 assert_eq!(
232 editor.selections.ranges(&editor.display_snapshot(cx)),
233 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
234 );
235
236 editor.start_transaction_at(now, window, cx);
237 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
238 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
239 });
240 editor.insert("e", window, cx);
241 editor.end_transaction_at(now, cx);
242 assert_eq!(editor.text(cx), "12cde6");
243 assert_eq!(
244 editor.selections.ranges(&editor.display_snapshot(cx)),
245 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
246 );
247
248 now += group_interval + Duration::from_millis(1);
249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
250 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
251 });
252
253 // Simulate an edit in another editor
254 buffer.update(cx, |buffer, cx| {
255 buffer.start_transaction_at(now, cx);
256 buffer.edit(
257 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
258 None,
259 cx,
260 );
261 buffer.edit(
262 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
263 None,
264 cx,
265 );
266 buffer.end_transaction_at(now, cx);
267 });
268
269 assert_eq!(editor.text(cx), "ab2cde6");
270 assert_eq!(
271 editor.selections.ranges(&editor.display_snapshot(cx)),
272 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
273 );
274
275 // Last transaction happened past the group interval in a different editor.
276 // Undo it individually and don't restore selections.
277 editor.undo(&Undo, window, cx);
278 assert_eq!(editor.text(cx), "12cde6");
279 assert_eq!(
280 editor.selections.ranges(&editor.display_snapshot(cx)),
281 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
282 );
283
284 // First two transactions happened within the group interval in this editor.
285 // Undo them together and restore selections.
286 editor.undo(&Undo, window, cx);
287 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
288 assert_eq!(editor.text(cx), "123456");
289 assert_eq!(
290 editor.selections.ranges(&editor.display_snapshot(cx)),
291 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
292 );
293
294 // Redo the first two transactions together.
295 editor.redo(&Redo, window, cx);
296 assert_eq!(editor.text(cx), "12cde6");
297 assert_eq!(
298 editor.selections.ranges(&editor.display_snapshot(cx)),
299 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
300 );
301
302 // Redo the last transaction on its own.
303 editor.redo(&Redo, window, cx);
304 assert_eq!(editor.text(cx), "ab2cde6");
305 assert_eq!(
306 editor.selections.ranges(&editor.display_snapshot(cx)),
307 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
308 );
309
310 // Test empty transactions.
311 editor.start_transaction_at(now, window, cx);
312 editor.end_transaction_at(now, cx);
313 editor.undo(&Undo, window, cx);
314 assert_eq!(editor.text(cx), "12cde6");
315 });
316}
317
318#[gpui::test]
319fn test_ime_composition(cx: &mut TestAppContext) {
320 init_test(cx, |_| {});
321
322 let buffer = cx.new(|cx| {
323 let mut buffer = language::Buffer::local("abcde", cx);
324 // Ensure automatic grouping doesn't occur.
325 buffer.set_group_interval(Duration::ZERO);
326 buffer
327 });
328
329 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
330 cx.add_window(|window, cx| {
331 let mut editor = build_editor(buffer.clone(), window, cx);
332
333 // Start a new IME composition.
334 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
335 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
336 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
337 assert_eq!(editor.text(cx), "äbcde");
338 assert_eq!(
339 editor.marked_text_ranges(cx),
340 Some(vec![
341 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
342 ])
343 );
344
345 // Finalize IME composition.
346 editor.replace_text_in_range(None, "ā", window, cx);
347 assert_eq!(editor.text(cx), "ābcde");
348 assert_eq!(editor.marked_text_ranges(cx), None);
349
350 // IME composition edits are grouped and are undone/redone at once.
351 editor.undo(&Default::default(), window, cx);
352 assert_eq!(editor.text(cx), "abcde");
353 assert_eq!(editor.marked_text_ranges(cx), None);
354 editor.redo(&Default::default(), window, cx);
355 assert_eq!(editor.text(cx), "ābcde");
356 assert_eq!(editor.marked_text_ranges(cx), None);
357
358 // Start a new IME composition.
359 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
360 assert_eq!(
361 editor.marked_text_ranges(cx),
362 Some(vec![
363 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
364 ])
365 );
366
367 // Undoing during an IME composition cancels it.
368 editor.undo(&Default::default(), window, cx);
369 assert_eq!(editor.text(cx), "ābcde");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
373 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
374 assert_eq!(editor.text(cx), "ābcdè");
375 assert_eq!(
376 editor.marked_text_ranges(cx),
377 Some(vec![
378 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
379 ])
380 );
381
382 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
383 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
384 assert_eq!(editor.text(cx), "ābcdę");
385 assert_eq!(editor.marked_text_ranges(cx), None);
386
387 // Start a new IME composition with multiple cursors.
388 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
389 s.select_ranges([
390 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
391 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
392 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
393 ])
394 });
395 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
396 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
397 assert_eq!(
398 editor.marked_text_ranges(cx),
399 Some(vec![
400 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
401 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
402 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
403 ])
404 );
405
406 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
407 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
408 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
409 assert_eq!(
410 editor.marked_text_ranges(cx),
411 Some(vec![
412 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
413 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
414 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
415 ])
416 );
417
418 // Finalize IME composition with multiple cursors.
419 editor.replace_text_in_range(Some(9..10), "2", window, cx);
420 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
421 assert_eq!(editor.marked_text_ranges(cx), None);
422
423 editor
424 });
425}
426
427#[gpui::test]
428fn test_selection_with_mouse(cx: &mut TestAppContext) {
429 init_test(cx, |_| {});
430
431 let editor = cx.add_window(|window, cx| {
432 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
433 build_editor(buffer, window, cx)
434 });
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
438 });
439 assert_eq!(
440 editor
441 .update(cx, |editor, _, cx| display_ranges(editor, cx))
442 .unwrap(),
443 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
444 );
445
446 _ = editor.update(cx, |editor, window, cx| {
447 editor.update_selection(
448 DisplayPoint::new(DisplayRow(3), 3),
449 0,
450 gpui::Point::<f32>::default(),
451 window,
452 cx,
453 );
454 });
455
456 assert_eq!(
457 editor
458 .update(cx, |editor, _, cx| display_ranges(editor, cx))
459 .unwrap(),
460 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
461 );
462
463 _ = editor.update(cx, |editor, window, cx| {
464 editor.update_selection(
465 DisplayPoint::new(DisplayRow(1), 1),
466 0,
467 gpui::Point::<f32>::default(),
468 window,
469 cx,
470 );
471 });
472
473 assert_eq!(
474 editor
475 .update(cx, |editor, _, cx| display_ranges(editor, cx))
476 .unwrap(),
477 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
478 );
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 editor.update_selection(
483 DisplayPoint::new(DisplayRow(3), 3),
484 0,
485 gpui::Point::<f32>::default(),
486 window,
487 cx,
488 );
489 });
490
491 assert_eq!(
492 editor
493 .update(cx, |editor, _, cx| display_ranges(editor, cx))
494 .unwrap(),
495 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
496 );
497
498 _ = editor.update(cx, |editor, window, cx| {
499 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
500 editor.update_selection(
501 DisplayPoint::new(DisplayRow(0), 0),
502 0,
503 gpui::Point::<f32>::default(),
504 window,
505 cx,
506 );
507 });
508
509 assert_eq!(
510 editor
511 .update(cx, |editor, _, cx| display_ranges(editor, cx))
512 .unwrap(),
513 [
514 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
515 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
516 ]
517 );
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| display_ranges(editor, cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
528 );
529}
530
531#[gpui::test]
532fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.end_selection(window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
550 });
551
552 _ = editor.update(cx, |editor, window, cx| {
553 editor.end_selection(window, cx);
554 });
555
556 assert_eq!(
557 editor
558 .update(cx, |editor, _, cx| display_ranges(editor, cx))
559 .unwrap(),
560 [
561 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
562 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
563 ]
564 );
565
566 _ = editor.update(cx, |editor, window, cx| {
567 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
568 });
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.end_selection(window, cx);
572 });
573
574 assert_eq!(
575 editor
576 .update(cx, |editor, _, cx| display_ranges(editor, cx))
577 .unwrap(),
578 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
579 );
580}
581
582#[gpui::test]
583fn test_canceling_pending_selection(cx: &mut TestAppContext) {
584 init_test(cx, |_| {});
585
586 let editor = cx.add_window(|window, cx| {
587 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
588 build_editor(buffer, window, cx)
589 });
590
591 _ = editor.update(cx, |editor, window, cx| {
592 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
593 assert_eq!(
594 display_ranges(editor, cx),
595 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
596 );
597 });
598
599 _ = editor.update(cx, |editor, window, cx| {
600 editor.update_selection(
601 DisplayPoint::new(DisplayRow(3), 3),
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 _ = editor.update(cx, |editor, window, cx| {
614 editor.cancel(&Cancel, window, cx);
615 editor.update_selection(
616 DisplayPoint::new(DisplayRow(1), 1),
617 0,
618 gpui::Point::<f32>::default(),
619 window,
620 cx,
621 );
622 assert_eq!(
623 display_ranges(editor, cx),
624 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
625 );
626 });
627}
628
629#[gpui::test]
630fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
631 init_test(cx, |_| {});
632
633 let editor = cx.add_window(|window, cx| {
634 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
635 build_editor(buffer, window, cx)
636 });
637
638 _ = editor.update(cx, |editor, window, cx| {
639 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
640 assert_eq!(
641 display_ranges(editor, cx),
642 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
643 );
644
645 editor.move_down(&Default::default(), window, cx);
646 assert_eq!(
647 display_ranges(editor, cx),
648 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
649 );
650
651 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
652 assert_eq!(
653 display_ranges(editor, cx),
654 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
655 );
656
657 editor.move_up(&Default::default(), window, cx);
658 assert_eq!(
659 display_ranges(editor, cx),
660 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
661 );
662 });
663}
664
665#[gpui::test]
666fn test_extending_selection(cx: &mut TestAppContext) {
667 init_test(cx, |_| {});
668
669 let editor = cx.add_window(|window, cx| {
670 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
671 build_editor(buffer, window, cx)
672 });
673
674 _ = editor.update(cx, |editor, window, cx| {
675 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
676 editor.end_selection(window, cx);
677 assert_eq!(
678 display_ranges(editor, cx),
679 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
680 );
681
682 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
683 editor.end_selection(window, cx);
684 assert_eq!(
685 display_ranges(editor, cx),
686 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
687 );
688
689 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
690 editor.end_selection(window, cx);
691 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
692 assert_eq!(
693 display_ranges(editor, cx),
694 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
695 );
696
697 editor.update_selection(
698 DisplayPoint::new(DisplayRow(0), 1),
699 0,
700 gpui::Point::<f32>::default(),
701 window,
702 cx,
703 );
704 editor.end_selection(window, cx);
705 assert_eq!(
706 display_ranges(editor, cx),
707 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
708 );
709
710 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
711 editor.end_selection(window, cx);
712 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
713 editor.end_selection(window, cx);
714 assert_eq!(
715 display_ranges(editor, cx),
716 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
717 );
718
719 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
720 assert_eq!(
721 display_ranges(editor, cx),
722 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
723 );
724
725 editor.update_selection(
726 DisplayPoint::new(DisplayRow(0), 6),
727 0,
728 gpui::Point::<f32>::default(),
729 window,
730 cx,
731 );
732 assert_eq!(
733 display_ranges(editor, cx),
734 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
735 );
736
737 editor.update_selection(
738 DisplayPoint::new(DisplayRow(0), 1),
739 0,
740 gpui::Point::<f32>::default(),
741 window,
742 cx,
743 );
744 editor.end_selection(window, cx);
745 assert_eq!(
746 display_ranges(editor, cx),
747 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
748 );
749 });
750}
751
752#[gpui::test]
753fn test_clone(cx: &mut TestAppContext) {
754 init_test(cx, |_| {});
755
756 let (text, selection_ranges) = marked_text_ranges(
757 indoc! {"
758 one
759 two
760 threeˇ
761 four
762 fiveˇ
763 "},
764 true,
765 );
766
767 let editor = cx.add_window(|window, cx| {
768 let buffer = MultiBuffer::build_simple(&text, cx);
769 build_editor(buffer, window, cx)
770 });
771
772 _ = editor.update(cx, |editor, window, cx| {
773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
774 s.select_ranges(
775 selection_ranges
776 .iter()
777 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
778 )
779 });
780 editor.fold_creases(
781 vec![
782 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
783 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
784 ],
785 true,
786 window,
787 cx,
788 );
789 });
790
791 let cloned_editor = editor
792 .update(cx, |editor, _, cx| {
793 cx.open_window(Default::default(), |window, cx| {
794 cx.new(|cx| editor.clone(window, cx))
795 })
796 })
797 .unwrap()
798 .unwrap();
799
800 let snapshot = editor
801 .update(cx, |e, window, cx| e.snapshot(window, cx))
802 .unwrap();
803 let cloned_snapshot = cloned_editor
804 .update(cx, |e, window, cx| e.snapshot(window, cx))
805 .unwrap();
806
807 assert_eq!(
808 cloned_editor
809 .update(cx, |e, _, cx| e.display_text(cx))
810 .unwrap(),
811 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
812 );
813 assert_eq!(
814 cloned_snapshot
815 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
816 .collect::<Vec<_>>(),
817 snapshot
818 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
819 .collect::<Vec<_>>(),
820 );
821 assert_set_eq!(
822 cloned_editor
823 .update(cx, |editor, _, cx| editor
824 .selections
825 .ranges::<Point>(&editor.display_snapshot(cx)))
826 .unwrap(),
827 editor
828 .update(cx, |editor, _, cx| editor
829 .selections
830 .ranges(&editor.display_snapshot(cx)))
831 .unwrap()
832 );
833 assert_set_eq!(
834 cloned_editor
835 .update(cx, |e, _window, cx| e
836 .selections
837 .display_ranges(&e.display_snapshot(cx)))
838 .unwrap(),
839 editor
840 .update(cx, |e, _, cx| e
841 .selections
842 .display_ranges(&e.display_snapshot(cx)))
843 .unwrap()
844 );
845}
846
847#[gpui::test]
848async fn test_navigation_history(cx: &mut TestAppContext) {
849 init_test(cx, |_| {});
850
851 use workspace::item::Item;
852
853 let fs = FakeFs::new(cx.executor());
854 let project = Project::test(fs, [], cx).await;
855 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
856 let pane = workspace
857 .update(cx, |workspace, _, _| workspace.active_pane().clone())
858 .unwrap();
859
860 _ = workspace.update(cx, |_v, window, cx| {
861 cx.new(|cx| {
862 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
863 let mut editor = build_editor(buffer, window, cx);
864 let handle = cx.entity();
865 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
866
867 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
868 editor.nav_history.as_mut().unwrap().pop_backward(cx)
869 }
870
871 // Move the cursor a small distance.
872 // Nothing is added to the navigation history.
873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
874 s.select_display_ranges([
875 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
876 ])
877 });
878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
879 s.select_display_ranges([
880 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
881 ])
882 });
883 assert!(pop_history(&mut editor, cx).is_none());
884
885 // Move the cursor a large distance.
886 // The history can jump back to the previous position.
887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
888 s.select_display_ranges([
889 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
890 ])
891 });
892 let nav_entry = pop_history(&mut editor, cx).unwrap();
893 editor.navigate(nav_entry.data.unwrap(), window, cx);
894 assert_eq!(nav_entry.item.id(), cx.entity_id());
895 assert_eq!(
896 editor
897 .selections
898 .display_ranges(&editor.display_snapshot(cx)),
899 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
900 );
901 assert!(pop_history(&mut editor, cx).is_none());
902
903 // Move the cursor a small distance via the mouse.
904 // Nothing is added to the navigation history.
905 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
906 editor.end_selection(window, cx);
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 // Move the cursor a large distance via the mouse.
916 // The history can jump back to the previous position.
917 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
918 editor.end_selection(window, cx);
919 assert_eq!(
920 editor
921 .selections
922 .display_ranges(&editor.display_snapshot(cx)),
923 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
924 );
925 let nav_entry = pop_history(&mut editor, cx).unwrap();
926 editor.navigate(nav_entry.data.unwrap(), window, cx);
927 assert_eq!(nav_entry.item.id(), cx.entity_id());
928 assert_eq!(
929 editor
930 .selections
931 .display_ranges(&editor.display_snapshot(cx)),
932 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
933 );
934 assert!(pop_history(&mut editor, cx).is_none());
935
936 // Set scroll position to check later
937 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
938 let original_scroll_position = editor.scroll_manager.anchor();
939
940 // Jump to the end of the document and adjust scroll
941 editor.move_to_end(&MoveToEnd, window, cx);
942 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
943 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
944
945 let nav_entry = pop_history(&mut editor, cx).unwrap();
946 editor.navigate(nav_entry.data.unwrap(), window, cx);
947 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
948
949 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
950 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
951 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
952 let invalid_point = Point::new(9999, 0);
953 editor.navigate(
954 Box::new(NavigationData {
955 cursor_anchor: invalid_anchor,
956 cursor_position: invalid_point,
957 scroll_anchor: ScrollAnchor {
958 anchor: invalid_anchor,
959 offset: Default::default(),
960 },
961 scroll_top_row: invalid_point.row,
962 }),
963 window,
964 cx,
965 );
966 assert_eq!(
967 editor
968 .selections
969 .display_ranges(&editor.display_snapshot(cx)),
970 &[editor.max_point(cx)..editor.max_point(cx)]
971 );
972 assert_eq!(
973 editor.scroll_position(cx),
974 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
975 );
976
977 editor
978 })
979 });
980}
981
982#[gpui::test]
983fn test_cancel(cx: &mut TestAppContext) {
984 init_test(cx, |_| {});
985
986 let editor = cx.add_window(|window, cx| {
987 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
988 build_editor(buffer, window, cx)
989 });
990
991 _ = editor.update(cx, |editor, window, cx| {
992 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
993 editor.update_selection(
994 DisplayPoint::new(DisplayRow(1), 1),
995 0,
996 gpui::Point::<f32>::default(),
997 window,
998 cx,
999 );
1000 editor.end_selection(window, cx);
1001
1002 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1003 editor.update_selection(
1004 DisplayPoint::new(DisplayRow(0), 3),
1005 0,
1006 gpui::Point::<f32>::default(),
1007 window,
1008 cx,
1009 );
1010 editor.end_selection(window, cx);
1011 assert_eq!(
1012 display_ranges(editor, cx),
1013 [
1014 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1015 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1016 ]
1017 );
1018 });
1019
1020 _ = editor.update(cx, |editor, window, cx| {
1021 editor.cancel(&Cancel, window, cx);
1022 assert_eq!(
1023 display_ranges(editor, cx),
1024 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1025 );
1026 });
1027
1028 _ = editor.update(cx, |editor, window, cx| {
1029 editor.cancel(&Cancel, window, cx);
1030 assert_eq!(
1031 display_ranges(editor, cx),
1032 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1033 );
1034 });
1035}
1036
1037#[gpui::test]
1038fn test_fold_action(cx: &mut TestAppContext) {
1039 init_test(cx, |_| {});
1040
1041 let editor = cx.add_window(|window, cx| {
1042 let buffer = MultiBuffer::build_simple(
1043 &"
1044 impl Foo {
1045 // Hello!
1046
1047 fn a() {
1048 1
1049 }
1050
1051 fn b() {
1052 2
1053 }
1054
1055 fn c() {
1056 3
1057 }
1058 }
1059 "
1060 .unindent(),
1061 cx,
1062 );
1063 build_editor(buffer, window, cx)
1064 });
1065
1066 _ = editor.update(cx, |editor, window, cx| {
1067 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1068 s.select_display_ranges([
1069 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1070 ]);
1071 });
1072 editor.fold(&Fold, window, cx);
1073 assert_eq!(
1074 editor.display_text(cx),
1075 "
1076 impl Foo {
1077 // Hello!
1078
1079 fn a() {
1080 1
1081 }
1082
1083 fn b() {⋯
1084 }
1085
1086 fn c() {⋯
1087 }
1088 }
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 impl Foo {⋯
1098 }
1099 "
1100 .unindent(),
1101 );
1102
1103 editor.unfold_lines(&UnfoldLines, window, cx);
1104 assert_eq!(
1105 editor.display_text(cx),
1106 "
1107 impl Foo {
1108 // Hello!
1109
1110 fn a() {
1111 1
1112 }
1113
1114 fn b() {⋯
1115 }
1116
1117 fn c() {⋯
1118 }
1119 }
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148 def c():
1149 print(3)
1150 "
1151 .unindent(),
1152 cx,
1153 );
1154 build_editor(buffer, window, cx)
1155 });
1156
1157 _ = editor.update(cx, |editor, window, cx| {
1158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1159 s.select_display_ranges([
1160 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1161 ]);
1162 });
1163 editor.fold(&Fold, window, cx);
1164 assert_eq!(
1165 editor.display_text(cx),
1166 "
1167 class Foo:
1168 # Hello!
1169
1170 def a():
1171 print(1)
1172
1173 def b():⋯
1174
1175 def c():⋯
1176 "
1177 .unindent(),
1178 );
1179
1180 editor.fold(&Fold, window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:⋯
1185 "
1186 .unindent(),
1187 );
1188
1189 editor.unfold_lines(&UnfoldLines, window, cx);
1190 assert_eq!(
1191 editor.display_text(cx),
1192 "
1193 class Foo:
1194 # Hello!
1195
1196 def a():
1197 print(1)
1198
1199 def b():⋯
1200
1201 def c():⋯
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.unfold_lines(&UnfoldLines, window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 editor.buffer.read(cx).read(cx).text()
1210 );
1211 });
1212}
1213
1214#[gpui::test]
1215fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1216 init_test(cx, |_| {});
1217
1218 let editor = cx.add_window(|window, cx| {
1219 let buffer = MultiBuffer::build_simple(
1220 &"
1221 class Foo:
1222 # Hello!
1223
1224 def a():
1225 print(1)
1226
1227 def b():
1228 print(2)
1229
1230
1231 def c():
1232 print(3)
1233
1234
1235 "
1236 .unindent(),
1237 cx,
1238 );
1239 build_editor(buffer, window, cx)
1240 });
1241
1242 _ = editor.update(cx, |editor, window, cx| {
1243 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1244 s.select_display_ranges([
1245 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1246 ]);
1247 });
1248 editor.fold(&Fold, window, cx);
1249 assert_eq!(
1250 editor.display_text(cx),
1251 "
1252 class Foo:
1253 # Hello!
1254
1255 def a():
1256 print(1)
1257
1258 def b():⋯
1259
1260
1261 def c():⋯
1262
1263
1264 "
1265 .unindent(),
1266 );
1267
1268 editor.fold(&Fold, window, cx);
1269 assert_eq!(
1270 editor.display_text(cx),
1271 "
1272 class Foo:⋯
1273
1274
1275 "
1276 .unindent(),
1277 );
1278
1279 editor.unfold_lines(&UnfoldLines, window, cx);
1280 assert_eq!(
1281 editor.display_text(cx),
1282 "
1283 class Foo:
1284 # Hello!
1285
1286 def a():
1287 print(1)
1288
1289 def b():⋯
1290
1291
1292 def c():⋯
1293
1294
1295 "
1296 .unindent(),
1297 );
1298
1299 editor.unfold_lines(&UnfoldLines, window, cx);
1300 assert_eq!(
1301 editor.display_text(cx),
1302 editor.buffer.read(cx).read(cx).text()
1303 );
1304 });
1305}
1306
1307#[gpui::test]
1308fn test_fold_at_level(cx: &mut TestAppContext) {
1309 init_test(cx, |_| {});
1310
1311 let editor = cx.add_window(|window, cx| {
1312 let buffer = MultiBuffer::build_simple(
1313 &"
1314 class Foo:
1315 # Hello!
1316
1317 def a():
1318 print(1)
1319
1320 def b():
1321 print(2)
1322
1323
1324 class Bar:
1325 # World!
1326
1327 def a():
1328 print(1)
1329
1330 def b():
1331 print(2)
1332
1333
1334 "
1335 .unindent(),
1336 cx,
1337 );
1338 build_editor(buffer, window, cx)
1339 });
1340
1341 _ = editor.update(cx, |editor, window, cx| {
1342 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1343 assert_eq!(
1344 editor.display_text(cx),
1345 "
1346 class Foo:
1347 # Hello!
1348
1349 def a():⋯
1350
1351 def b():⋯
1352
1353
1354 class Bar:
1355 # World!
1356
1357 def a():⋯
1358
1359 def b():⋯
1360
1361
1362 "
1363 .unindent(),
1364 );
1365
1366 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1367 assert_eq!(
1368 editor.display_text(cx),
1369 "
1370 class Foo:⋯
1371
1372
1373 class Bar:⋯
1374
1375
1376 "
1377 .unindent(),
1378 );
1379
1380 editor.unfold_all(&UnfoldAll, window, cx);
1381 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1382 assert_eq!(
1383 editor.display_text(cx),
1384 "
1385 class Foo:
1386 # Hello!
1387
1388 def a():
1389 print(1)
1390
1391 def b():
1392 print(2)
1393
1394
1395 class Bar:
1396 # World!
1397
1398 def a():
1399 print(1)
1400
1401 def b():
1402 print(2)
1403
1404
1405 "
1406 .unindent(),
1407 );
1408
1409 assert_eq!(
1410 editor.display_text(cx),
1411 editor.buffer.read(cx).read(cx).text()
1412 );
1413 let (_, positions) = marked_text_ranges(
1414 &"
1415 class Foo:
1416 # Hello!
1417
1418 def a():
1419 print(1)
1420
1421 def b():
1422 p«riˇ»nt(2)
1423
1424
1425 class Bar:
1426 # World!
1427
1428 def a():
1429 «ˇprint(1)
1430
1431 def b():
1432 print(2)»
1433
1434
1435 "
1436 .unindent(),
1437 true,
1438 );
1439
1440 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1441 s.select_ranges(
1442 positions
1443 .iter()
1444 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1445 )
1446 });
1447
1448 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1449 assert_eq!(
1450 editor.display_text(cx),
1451 "
1452 class Foo:
1453 # Hello!
1454
1455 def a():⋯
1456
1457 def b():
1458 print(2)
1459
1460
1461 class Bar:
1462 # World!
1463
1464 def a():
1465 print(1)
1466
1467 def b():
1468 print(2)
1469
1470
1471 "
1472 .unindent(),
1473 );
1474 });
1475}
1476
1477#[gpui::test]
1478fn test_move_cursor(cx: &mut TestAppContext) {
1479 init_test(cx, |_| {});
1480
1481 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1482 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1483
1484 buffer.update(cx, |buffer, cx| {
1485 buffer.edit(
1486 vec![
1487 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1488 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1489 ],
1490 None,
1491 cx,
1492 );
1493 });
1494 _ = editor.update(cx, |editor, window, cx| {
1495 assert_eq!(
1496 display_ranges(editor, cx),
1497 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1498 );
1499
1500 editor.move_down(&MoveDown, window, cx);
1501 assert_eq!(
1502 display_ranges(editor, cx),
1503 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1504 );
1505
1506 editor.move_right(&MoveRight, window, cx);
1507 assert_eq!(
1508 display_ranges(editor, cx),
1509 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1510 );
1511
1512 editor.move_left(&MoveLeft, window, cx);
1513 assert_eq!(
1514 display_ranges(editor, cx),
1515 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1516 );
1517
1518 editor.move_up(&MoveUp, window, cx);
1519 assert_eq!(
1520 display_ranges(editor, cx),
1521 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1522 );
1523
1524 editor.move_to_end(&MoveToEnd, window, cx);
1525 assert_eq!(
1526 display_ranges(editor, cx),
1527 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1528 );
1529
1530 editor.move_to_beginning(&MoveToBeginning, window, cx);
1531 assert_eq!(
1532 display_ranges(editor, cx),
1533 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1534 );
1535
1536 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1537 s.select_display_ranges([
1538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1539 ]);
1540 });
1541 editor.select_to_beginning(&SelectToBeginning, window, cx);
1542 assert_eq!(
1543 display_ranges(editor, cx),
1544 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1545 );
1546
1547 editor.select_to_end(&SelectToEnd, window, cx);
1548 assert_eq!(
1549 display_ranges(editor, cx),
1550 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1551 );
1552 });
1553}
1554
1555#[gpui::test]
1556fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1557 init_test(cx, |_| {});
1558
1559 let editor = cx.add_window(|window, cx| {
1560 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1561 build_editor(buffer, window, cx)
1562 });
1563
1564 assert_eq!('🟥'.len_utf8(), 4);
1565 assert_eq!('α'.len_utf8(), 2);
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.fold_creases(
1569 vec![
1570 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1571 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1572 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1573 ],
1574 true,
1575 window,
1576 cx,
1577 );
1578 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1579
1580 editor.move_right(&MoveRight, window, cx);
1581 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1582 editor.move_right(&MoveRight, window, cx);
1583 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1584 editor.move_right(&MoveRight, window, cx);
1585 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1586
1587 editor.move_down(&MoveDown, window, cx);
1588 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1589 editor.move_left(&MoveLeft, window, cx);
1590 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1591 editor.move_left(&MoveLeft, window, cx);
1592 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1593 editor.move_left(&MoveLeft, window, cx);
1594 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1595
1596 editor.move_down(&MoveDown, window, cx);
1597 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1598 editor.move_right(&MoveRight, window, cx);
1599 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1600 editor.move_right(&MoveRight, window, cx);
1601 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1602 editor.move_right(&MoveRight, window, cx);
1603 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1604
1605 editor.move_up(&MoveUp, window, cx);
1606 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1607 editor.move_down(&MoveDown, window, cx);
1608 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1609 editor.move_up(&MoveUp, window, cx);
1610 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1611
1612 editor.move_up(&MoveUp, window, cx);
1613 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1614 editor.move_left(&MoveLeft, window, cx);
1615 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1616 editor.move_left(&MoveLeft, window, cx);
1617 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1618 });
1619}
1620
1621#[gpui::test]
1622fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1623 init_test(cx, |_| {});
1624
1625 let editor = cx.add_window(|window, cx| {
1626 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1627 build_editor(buffer, window, cx)
1628 });
1629 _ = editor.update(cx, |editor, window, cx| {
1630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1631 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1632 });
1633
1634 // moving above start of document should move selection to start of document,
1635 // but the next move down should still be at the original goal_x
1636 editor.move_up(&MoveUp, window, cx);
1637 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1638
1639 editor.move_down(&MoveDown, window, cx);
1640 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1641
1642 editor.move_down(&MoveDown, window, cx);
1643 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1644
1645 editor.move_down(&MoveDown, window, cx);
1646 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1647
1648 editor.move_down(&MoveDown, window, cx);
1649 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1650
1651 // moving past end of document should not change goal_x
1652 editor.move_down(&MoveDown, window, cx);
1653 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1654
1655 editor.move_down(&MoveDown, window, cx);
1656 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1657
1658 editor.move_up(&MoveUp, window, cx);
1659 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1660
1661 editor.move_up(&MoveUp, window, cx);
1662 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1663
1664 editor.move_up(&MoveUp, window, cx);
1665 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1666 });
1667}
1668
1669#[gpui::test]
1670fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1671 init_test(cx, |_| {});
1672 let move_to_beg = MoveToBeginningOfLine {
1673 stop_at_soft_wraps: true,
1674 stop_at_indent: true,
1675 };
1676
1677 let delete_to_beg = DeleteToBeginningOfLine {
1678 stop_at_indent: false,
1679 };
1680
1681 let move_to_end = MoveToEndOfLine {
1682 stop_at_soft_wraps: true,
1683 };
1684
1685 let editor = cx.add_window(|window, cx| {
1686 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1687 build_editor(buffer, window, cx)
1688 });
1689 _ = editor.update(cx, |editor, window, cx| {
1690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1691 s.select_display_ranges([
1692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1693 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1694 ]);
1695 });
1696 });
1697
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1700 assert_eq!(
1701 display_ranges(editor, cx),
1702 &[
1703 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1704 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1705 ]
1706 );
1707 });
1708
1709 _ = editor.update(cx, |editor, window, cx| {
1710 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1711 assert_eq!(
1712 display_ranges(editor, cx),
1713 &[
1714 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1715 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1716 ]
1717 );
1718 });
1719
1720 _ = editor.update(cx, |editor, window, cx| {
1721 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1722 assert_eq!(
1723 display_ranges(editor, cx),
1724 &[
1725 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1726 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1727 ]
1728 );
1729 });
1730
1731 _ = editor.update(cx, |editor, window, cx| {
1732 editor.move_to_end_of_line(&move_to_end, window, cx);
1733 assert_eq!(
1734 display_ranges(editor, cx),
1735 &[
1736 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1737 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1738 ]
1739 );
1740 });
1741
1742 // Moving to the end of line again is a no-op.
1743 _ = editor.update(cx, |editor, window, cx| {
1744 editor.move_to_end_of_line(&move_to_end, window, cx);
1745 assert_eq!(
1746 display_ranges(editor, cx),
1747 &[
1748 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1749 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1750 ]
1751 );
1752 });
1753
1754 _ = editor.update(cx, |editor, window, cx| {
1755 editor.move_left(&MoveLeft, window, cx);
1756 editor.select_to_beginning_of_line(
1757 &SelectToBeginningOfLine {
1758 stop_at_soft_wraps: true,
1759 stop_at_indent: true,
1760 },
1761 window,
1762 cx,
1763 );
1764 assert_eq!(
1765 display_ranges(editor, cx),
1766 &[
1767 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1768 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1769 ]
1770 );
1771 });
1772
1773 _ = editor.update(cx, |editor, window, cx| {
1774 editor.select_to_beginning_of_line(
1775 &SelectToBeginningOfLine {
1776 stop_at_soft_wraps: true,
1777 stop_at_indent: true,
1778 },
1779 window,
1780 cx,
1781 );
1782 assert_eq!(
1783 display_ranges(editor, cx),
1784 &[
1785 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1786 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1787 ]
1788 );
1789 });
1790
1791 _ = editor.update(cx, |editor, window, cx| {
1792 editor.select_to_beginning_of_line(
1793 &SelectToBeginningOfLine {
1794 stop_at_soft_wraps: true,
1795 stop_at_indent: true,
1796 },
1797 window,
1798 cx,
1799 );
1800 assert_eq!(
1801 display_ranges(editor, cx),
1802 &[
1803 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1804 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1805 ]
1806 );
1807 });
1808
1809 _ = editor.update(cx, |editor, window, cx| {
1810 editor.select_to_end_of_line(
1811 &SelectToEndOfLine {
1812 stop_at_soft_wraps: true,
1813 },
1814 window,
1815 cx,
1816 );
1817 assert_eq!(
1818 display_ranges(editor, cx),
1819 &[
1820 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1822 ]
1823 );
1824 });
1825
1826 _ = editor.update(cx, |editor, window, cx| {
1827 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1828 assert_eq!(editor.display_text(cx), "ab\n de");
1829 assert_eq!(
1830 display_ranges(editor, cx),
1831 &[
1832 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1833 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1834 ]
1835 );
1836 });
1837
1838 _ = editor.update(cx, |editor, window, cx| {
1839 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1840 assert_eq!(editor.display_text(cx), "\n");
1841 assert_eq!(
1842 display_ranges(editor, cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848 });
1849}
1850
1851#[gpui::test]
1852fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1853 init_test(cx, |_| {});
1854 let move_to_beg = MoveToBeginningOfLine {
1855 stop_at_soft_wraps: false,
1856 stop_at_indent: false,
1857 };
1858
1859 let move_to_end = MoveToEndOfLine {
1860 stop_at_soft_wraps: false,
1861 };
1862
1863 let editor = cx.add_window(|window, cx| {
1864 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1865 build_editor(buffer, window, cx)
1866 });
1867
1868 _ = editor.update(cx, |editor, window, cx| {
1869 editor.set_wrap_width(Some(140.0.into()), cx);
1870
1871 // We expect the following lines after wrapping
1872 // ```
1873 // thequickbrownfox
1874 // jumpedoverthelazydo
1875 // gs
1876 // ```
1877 // The final `gs` was soft-wrapped onto a new line.
1878 assert_eq!(
1879 "thequickbrownfox\njumpedoverthelaz\nydogs",
1880 editor.display_text(cx),
1881 );
1882
1883 // First, let's assert behavior on the first line, that was not soft-wrapped.
1884 // Start the cursor at the `k` on the first line
1885 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1886 s.select_display_ranges([
1887 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1888 ]);
1889 });
1890
1891 // Moving to the beginning of the line should put us at the beginning of the line.
1892 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1893 assert_eq!(
1894 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1895 display_ranges(editor, cx)
1896 );
1897
1898 // Moving to the end of the line should put us at the end of the line.
1899 editor.move_to_end_of_line(&move_to_end, window, cx);
1900 assert_eq!(
1901 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1902 display_ranges(editor, cx)
1903 );
1904
1905 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1906 // Start the cursor at the last line (`y` that was wrapped to a new line)
1907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1908 s.select_display_ranges([
1909 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1910 ]);
1911 });
1912
1913 // Moving to the beginning of the line should put us at the start of the second line of
1914 // display text, i.e., the `j`.
1915 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1916 assert_eq!(
1917 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1918 display_ranges(editor, cx)
1919 );
1920
1921 // Moving to the beginning of the line again should be a no-op.
1922 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1923 assert_eq!(
1924 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1925 display_ranges(editor, cx)
1926 );
1927
1928 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1929 // next display line.
1930 editor.move_to_end_of_line(&move_to_end, window, cx);
1931 assert_eq!(
1932 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1933 display_ranges(editor, cx)
1934 );
1935
1936 // Moving to the end of the line again should be a no-op.
1937 editor.move_to_end_of_line(&move_to_end, window, cx);
1938 assert_eq!(
1939 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1940 display_ranges(editor, cx)
1941 );
1942 });
1943}
1944
1945#[gpui::test]
1946fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1947 init_test(cx, |_| {});
1948
1949 let move_to_beg = MoveToBeginningOfLine {
1950 stop_at_soft_wraps: true,
1951 stop_at_indent: true,
1952 };
1953
1954 let select_to_beg = SelectToBeginningOfLine {
1955 stop_at_soft_wraps: true,
1956 stop_at_indent: true,
1957 };
1958
1959 let delete_to_beg = DeleteToBeginningOfLine {
1960 stop_at_indent: true,
1961 };
1962
1963 let move_to_end = MoveToEndOfLine {
1964 stop_at_soft_wraps: false,
1965 };
1966
1967 let editor = cx.add_window(|window, cx| {
1968 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1969 build_editor(buffer, window, cx)
1970 });
1971
1972 _ = editor.update(cx, |editor, window, cx| {
1973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1976 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1977 ]);
1978 });
1979
1980 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1981 // and the second cursor at the first non-whitespace character in the line.
1982 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1983 assert_eq!(
1984 display_ranges(editor, cx),
1985 &[
1986 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1987 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1988 ]
1989 );
1990
1991 // Moving to the beginning of the line again should be a no-op for the first cursor,
1992 // and should move the second cursor to the beginning of the line.
1993 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1994 assert_eq!(
1995 display_ranges(editor, cx),
1996 &[
1997 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1998 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1999 ]
2000 );
2001
2002 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2003 // and should move the second cursor back to the first non-whitespace character in the line.
2004 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2005 assert_eq!(
2006 display_ranges(editor, cx),
2007 &[
2008 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2009 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2010 ]
2011 );
2012
2013 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2014 // and to the first non-whitespace character in the line for the second cursor.
2015 editor.move_to_end_of_line(&move_to_end, window, cx);
2016 editor.move_left(&MoveLeft, window, cx);
2017 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2018 assert_eq!(
2019 display_ranges(editor, cx),
2020 &[
2021 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2022 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2023 ]
2024 );
2025
2026 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2027 // and should select to the beginning of the line for the second cursor.
2028 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2029 assert_eq!(
2030 display_ranges(editor, cx),
2031 &[
2032 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2033 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2034 ]
2035 );
2036
2037 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2038 // and should delete to the first non-whitespace character in the line for the second cursor.
2039 editor.move_to_end_of_line(&move_to_end, window, cx);
2040 editor.move_left(&MoveLeft, window, cx);
2041 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2042 assert_eq!(editor.text(cx), "c\n f");
2043 });
2044}
2045
2046#[gpui::test]
2047fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2048 init_test(cx, |_| {});
2049
2050 let move_to_beg = MoveToBeginningOfLine {
2051 stop_at_soft_wraps: true,
2052 stop_at_indent: true,
2053 };
2054
2055 let editor = cx.add_window(|window, cx| {
2056 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2057 build_editor(buffer, window, cx)
2058 });
2059
2060 _ = editor.update(cx, |editor, window, cx| {
2061 // test cursor between line_start and indent_start
2062 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2063 s.select_display_ranges([
2064 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2065 ]);
2066 });
2067
2068 // cursor should move to line_start
2069 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2070 assert_eq!(
2071 display_ranges(editor, cx),
2072 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2073 );
2074
2075 // cursor should move to indent_start
2076 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2077 assert_eq!(
2078 display_ranges(editor, cx),
2079 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2080 );
2081
2082 // cursor should move to back to line_start
2083 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2084 assert_eq!(
2085 display_ranges(editor, cx),
2086 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2087 );
2088 });
2089}
2090
2091#[gpui::test]
2092fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2093 init_test(cx, |_| {});
2094
2095 let editor = cx.add_window(|window, cx| {
2096 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2097 build_editor(buffer, window, cx)
2098 });
2099 _ = editor.update(cx, |editor, window, cx| {
2100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2101 s.select_display_ranges([
2102 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2103 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2104 ])
2105 });
2106 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2107 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2108
2109 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2110 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2111
2112 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2113 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2114
2115 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2116 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2117
2118 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2119 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2120
2121 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2122 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2123
2124 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2125 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2126
2127 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2128 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2129
2130 editor.move_right(&MoveRight, window, cx);
2131 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2132 assert_selection_ranges(
2133 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2134 editor,
2135 cx,
2136 );
2137
2138 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2139 assert_selection_ranges(
2140 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2141 editor,
2142 cx,
2143 );
2144
2145 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2146 assert_selection_ranges(
2147 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2148 editor,
2149 cx,
2150 );
2151 });
2152}
2153
2154#[gpui::test]
2155fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2156 init_test(cx, |_| {});
2157
2158 let editor = cx.add_window(|window, cx| {
2159 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2160 build_editor(buffer, window, cx)
2161 });
2162
2163 _ = editor.update(cx, |editor, window, cx| {
2164 editor.set_wrap_width(Some(140.0.into()), cx);
2165 assert_eq!(
2166 editor.display_text(cx),
2167 "use one::{\n two::three::\n four::five\n};"
2168 );
2169
2170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2171 s.select_display_ranges([
2172 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2173 ]);
2174 });
2175
2176 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2177 assert_eq!(
2178 display_ranges(editor, cx),
2179 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2180 );
2181
2182 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2183 assert_eq!(
2184 display_ranges(editor, cx),
2185 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2186 );
2187
2188 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2189 assert_eq!(
2190 display_ranges(editor, cx),
2191 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2192 );
2193
2194 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2195 assert_eq!(
2196 display_ranges(editor, cx),
2197 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2198 );
2199
2200 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2201 assert_eq!(
2202 display_ranges(editor, cx),
2203 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2204 );
2205
2206 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2207 assert_eq!(
2208 display_ranges(editor, cx),
2209 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2210 );
2211 });
2212}
2213
2214#[gpui::test]
2215async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2216 init_test(cx, |_| {});
2217 let mut cx = EditorTestContext::new(cx).await;
2218
2219 let line_height = cx.editor(|editor, window, _| {
2220 editor
2221 .style()
2222 .unwrap()
2223 .text
2224 .line_height_in_pixels(window.rem_size())
2225 });
2226 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2227
2228 cx.set_state(
2229 &r#"ˇone
2230 two
2231
2232 three
2233 fourˇ
2234 five
2235
2236 six"#
2237 .unindent(),
2238 );
2239
2240 cx.update_editor(|editor, window, cx| {
2241 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2242 });
2243 cx.assert_editor_state(
2244 &r#"one
2245 two
2246 ˇ
2247 three
2248 four
2249 five
2250 ˇ
2251 six"#
2252 .unindent(),
2253 );
2254
2255 cx.update_editor(|editor, window, cx| {
2256 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2257 });
2258 cx.assert_editor_state(
2259 &r#"one
2260 two
2261
2262 three
2263 four
2264 five
2265 ˇ
2266 sixˇ"#
2267 .unindent(),
2268 );
2269
2270 cx.update_editor(|editor, window, cx| {
2271 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2272 });
2273 cx.assert_editor_state(
2274 &r#"one
2275 two
2276
2277 three
2278 four
2279 five
2280
2281 sixˇ"#
2282 .unindent(),
2283 );
2284
2285 cx.update_editor(|editor, window, cx| {
2286 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2287 });
2288 cx.assert_editor_state(
2289 &r#"one
2290 two
2291
2292 three
2293 four
2294 five
2295 ˇ
2296 six"#
2297 .unindent(),
2298 );
2299
2300 cx.update_editor(|editor, window, cx| {
2301 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2302 });
2303 cx.assert_editor_state(
2304 &r#"one
2305 two
2306 ˇ
2307 three
2308 four
2309 five
2310
2311 six"#
2312 .unindent(),
2313 );
2314
2315 cx.update_editor(|editor, window, cx| {
2316 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2317 });
2318 cx.assert_editor_state(
2319 &r#"ˇone
2320 two
2321
2322 three
2323 four
2324 five
2325
2326 six"#
2327 .unindent(),
2328 );
2329}
2330
2331#[gpui::test]
2332async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2333 init_test(cx, |_| {});
2334 let mut cx = EditorTestContext::new(cx).await;
2335 let line_height = cx.editor(|editor, window, _| {
2336 editor
2337 .style()
2338 .unwrap()
2339 .text
2340 .line_height_in_pixels(window.rem_size())
2341 });
2342 let window = cx.window;
2343 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2344
2345 cx.set_state(
2346 r#"ˇone
2347 two
2348 three
2349 four
2350 five
2351 six
2352 seven
2353 eight
2354 nine
2355 ten
2356 "#,
2357 );
2358
2359 cx.update_editor(|editor, window, cx| {
2360 assert_eq!(
2361 editor.snapshot(window, cx).scroll_position(),
2362 gpui::Point::new(0., 0.)
2363 );
2364 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2365 assert_eq!(
2366 editor.snapshot(window, cx).scroll_position(),
2367 gpui::Point::new(0., 3.)
2368 );
2369 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2370 assert_eq!(
2371 editor.snapshot(window, cx).scroll_position(),
2372 gpui::Point::new(0., 6.)
2373 );
2374 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2375 assert_eq!(
2376 editor.snapshot(window, cx).scroll_position(),
2377 gpui::Point::new(0., 3.)
2378 );
2379
2380 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2381 assert_eq!(
2382 editor.snapshot(window, cx).scroll_position(),
2383 gpui::Point::new(0., 1.)
2384 );
2385 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2386 assert_eq!(
2387 editor.snapshot(window, cx).scroll_position(),
2388 gpui::Point::new(0., 3.)
2389 );
2390 });
2391}
2392
2393#[gpui::test]
2394async fn test_autoscroll(cx: &mut TestAppContext) {
2395 init_test(cx, |_| {});
2396 let mut cx = EditorTestContext::new(cx).await;
2397
2398 let line_height = cx.update_editor(|editor, window, cx| {
2399 editor.set_vertical_scroll_margin(2, cx);
2400 editor
2401 .style()
2402 .unwrap()
2403 .text
2404 .line_height_in_pixels(window.rem_size())
2405 });
2406 let window = cx.window;
2407 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2408
2409 cx.set_state(
2410 r#"ˇone
2411 two
2412 three
2413 four
2414 five
2415 six
2416 seven
2417 eight
2418 nine
2419 ten
2420 "#,
2421 );
2422 cx.update_editor(|editor, window, cx| {
2423 assert_eq!(
2424 editor.snapshot(window, cx).scroll_position(),
2425 gpui::Point::new(0., 0.0)
2426 );
2427 });
2428
2429 // Add a cursor below the visible area. Since both cursors cannot fit
2430 // on screen, the editor autoscrolls to reveal the newest cursor, and
2431 // allows the vertical scroll margin below that cursor.
2432 cx.update_editor(|editor, window, cx| {
2433 editor.change_selections(Default::default(), window, cx, |selections| {
2434 selections.select_ranges([
2435 Point::new(0, 0)..Point::new(0, 0),
2436 Point::new(6, 0)..Point::new(6, 0),
2437 ]);
2438 })
2439 });
2440 cx.update_editor(|editor, window, cx| {
2441 assert_eq!(
2442 editor.snapshot(window, cx).scroll_position(),
2443 gpui::Point::new(0., 3.0)
2444 );
2445 });
2446
2447 // Move down. The editor cursor scrolls down to track the newest cursor.
2448 cx.update_editor(|editor, window, cx| {
2449 editor.move_down(&Default::default(), window, cx);
2450 });
2451 cx.update_editor(|editor, window, cx| {
2452 assert_eq!(
2453 editor.snapshot(window, cx).scroll_position(),
2454 gpui::Point::new(0., 4.0)
2455 );
2456 });
2457
2458 // Add a cursor above the visible area. Since both cursors fit on screen,
2459 // the editor scrolls to show both.
2460 cx.update_editor(|editor, window, cx| {
2461 editor.change_selections(Default::default(), window, cx, |selections| {
2462 selections.select_ranges([
2463 Point::new(1, 0)..Point::new(1, 0),
2464 Point::new(6, 0)..Point::new(6, 0),
2465 ]);
2466 })
2467 });
2468 cx.update_editor(|editor, window, cx| {
2469 assert_eq!(
2470 editor.snapshot(window, cx).scroll_position(),
2471 gpui::Point::new(0., 1.0)
2472 );
2473 });
2474}
2475
2476#[gpui::test]
2477async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2478 init_test(cx, |_| {});
2479 let mut cx = EditorTestContext::new(cx).await;
2480
2481 let line_height = cx.editor(|editor, window, _cx| {
2482 editor
2483 .style()
2484 .unwrap()
2485 .text
2486 .line_height_in_pixels(window.rem_size())
2487 });
2488 let window = cx.window;
2489 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2490 cx.set_state(
2491 &r#"
2492 ˇone
2493 two
2494 threeˇ
2495 four
2496 five
2497 six
2498 seven
2499 eight
2500 nine
2501 ten
2502 "#
2503 .unindent(),
2504 );
2505
2506 cx.update_editor(|editor, window, cx| {
2507 editor.move_page_down(&MovePageDown::default(), window, cx)
2508 });
2509 cx.assert_editor_state(
2510 &r#"
2511 one
2512 two
2513 three
2514 ˇfour
2515 five
2516 sixˇ
2517 seven
2518 eight
2519 nine
2520 ten
2521 "#
2522 .unindent(),
2523 );
2524
2525 cx.update_editor(|editor, window, cx| {
2526 editor.move_page_down(&MovePageDown::default(), window, cx)
2527 });
2528 cx.assert_editor_state(
2529 &r#"
2530 one
2531 two
2532 three
2533 four
2534 five
2535 six
2536 ˇseven
2537 eight
2538 nineˇ
2539 ten
2540 "#
2541 .unindent(),
2542 );
2543
2544 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2545 cx.assert_editor_state(
2546 &r#"
2547 one
2548 two
2549 three
2550 ˇfour
2551 five
2552 sixˇ
2553 seven
2554 eight
2555 nine
2556 ten
2557 "#
2558 .unindent(),
2559 );
2560
2561 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2562 cx.assert_editor_state(
2563 &r#"
2564 ˇone
2565 two
2566 threeˇ
2567 four
2568 five
2569 six
2570 seven
2571 eight
2572 nine
2573 ten
2574 "#
2575 .unindent(),
2576 );
2577
2578 // Test select collapsing
2579 cx.update_editor(|editor, window, cx| {
2580 editor.move_page_down(&MovePageDown::default(), window, cx);
2581 editor.move_page_down(&MovePageDown::default(), window, cx);
2582 editor.move_page_down(&MovePageDown::default(), window, cx);
2583 });
2584 cx.assert_editor_state(
2585 &r#"
2586 one
2587 two
2588 three
2589 four
2590 five
2591 six
2592 seven
2593 eight
2594 nine
2595 ˇten
2596 ˇ"#
2597 .unindent(),
2598 );
2599}
2600
2601#[gpui::test]
2602async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2603 init_test(cx, |_| {});
2604 let mut cx = EditorTestContext::new(cx).await;
2605 cx.set_state("one «two threeˇ» four");
2606 cx.update_editor(|editor, window, cx| {
2607 editor.delete_to_beginning_of_line(
2608 &DeleteToBeginningOfLine {
2609 stop_at_indent: false,
2610 },
2611 window,
2612 cx,
2613 );
2614 assert_eq!(editor.text(cx), " four");
2615 });
2616}
2617
2618#[gpui::test]
2619async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2620 init_test(cx, |_| {});
2621
2622 let mut cx = EditorTestContext::new(cx).await;
2623
2624 // For an empty selection, the preceding word fragment is deleted.
2625 // For non-empty selections, only selected characters are deleted.
2626 cx.set_state("onˇe two t«hreˇ»e four");
2627 cx.update_editor(|editor, window, cx| {
2628 editor.delete_to_previous_word_start(
2629 &DeleteToPreviousWordStart {
2630 ignore_newlines: false,
2631 ignore_brackets: false,
2632 },
2633 window,
2634 cx,
2635 );
2636 });
2637 cx.assert_editor_state("ˇe two tˇe four");
2638
2639 cx.set_state("e tˇwo te «fˇ»our");
2640 cx.update_editor(|editor, window, cx| {
2641 editor.delete_to_next_word_end(
2642 &DeleteToNextWordEnd {
2643 ignore_newlines: false,
2644 ignore_brackets: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 });
2650 cx.assert_editor_state("e tˇ te ˇour");
2651}
2652
2653#[gpui::test]
2654async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2655 init_test(cx, |_| {});
2656
2657 let mut cx = EditorTestContext::new(cx).await;
2658
2659 cx.set_state("here is some text ˇwith a space");
2660 cx.update_editor(|editor, window, cx| {
2661 editor.delete_to_previous_word_start(
2662 &DeleteToPreviousWordStart {
2663 ignore_newlines: false,
2664 ignore_brackets: true,
2665 },
2666 window,
2667 cx,
2668 );
2669 });
2670 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2671 cx.assert_editor_state("here is some textˇwith a space");
2672
2673 cx.set_state("here is some text ˇwith a space");
2674 cx.update_editor(|editor, window, cx| {
2675 editor.delete_to_previous_word_start(
2676 &DeleteToPreviousWordStart {
2677 ignore_newlines: false,
2678 ignore_brackets: false,
2679 },
2680 window,
2681 cx,
2682 );
2683 });
2684 cx.assert_editor_state("here is some textˇwith a space");
2685
2686 cx.set_state("here is some textˇ with a space");
2687 cx.update_editor(|editor, window, cx| {
2688 editor.delete_to_next_word_end(
2689 &DeleteToNextWordEnd {
2690 ignore_newlines: false,
2691 ignore_brackets: true,
2692 },
2693 window,
2694 cx,
2695 );
2696 });
2697 // Same happens in the other direction.
2698 cx.assert_editor_state("here is some textˇwith a space");
2699
2700 cx.set_state("here is some textˇ with a space");
2701 cx.update_editor(|editor, window, cx| {
2702 editor.delete_to_next_word_end(
2703 &DeleteToNextWordEnd {
2704 ignore_newlines: false,
2705 ignore_brackets: false,
2706 },
2707 window,
2708 cx,
2709 );
2710 });
2711 cx.assert_editor_state("here is some textˇwith a space");
2712
2713 cx.set_state("here is some textˇ with a space");
2714 cx.update_editor(|editor, window, cx| {
2715 editor.delete_to_next_word_end(
2716 &DeleteToNextWordEnd {
2717 ignore_newlines: true,
2718 ignore_brackets: false,
2719 },
2720 window,
2721 cx,
2722 );
2723 });
2724 cx.assert_editor_state("here is some textˇwith a space");
2725 cx.update_editor(|editor, window, cx| {
2726 editor.delete_to_previous_word_start(
2727 &DeleteToPreviousWordStart {
2728 ignore_newlines: true,
2729 ignore_brackets: false,
2730 },
2731 window,
2732 cx,
2733 );
2734 });
2735 cx.assert_editor_state("here is some ˇwith a space");
2736 cx.update_editor(|editor, window, cx| {
2737 editor.delete_to_previous_word_start(
2738 &DeleteToPreviousWordStart {
2739 ignore_newlines: true,
2740 ignore_brackets: false,
2741 },
2742 window,
2743 cx,
2744 );
2745 });
2746 // Single whitespaces are removed with the word behind them.
2747 cx.assert_editor_state("here is ˇwith a space");
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_previous_word_start(
2750 &DeleteToPreviousWordStart {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 cx.assert_editor_state("here ˇwith a space");
2759 cx.update_editor(|editor, window, cx| {
2760 editor.delete_to_previous_word_start(
2761 &DeleteToPreviousWordStart {
2762 ignore_newlines: true,
2763 ignore_brackets: false,
2764 },
2765 window,
2766 cx,
2767 );
2768 });
2769 cx.assert_editor_state("ˇwith a space");
2770 cx.update_editor(|editor, window, cx| {
2771 editor.delete_to_previous_word_start(
2772 &DeleteToPreviousWordStart {
2773 ignore_newlines: true,
2774 ignore_brackets: false,
2775 },
2776 window,
2777 cx,
2778 );
2779 });
2780 cx.assert_editor_state("ˇwith a space");
2781 cx.update_editor(|editor, window, cx| {
2782 editor.delete_to_next_word_end(
2783 &DeleteToNextWordEnd {
2784 ignore_newlines: true,
2785 ignore_brackets: false,
2786 },
2787 window,
2788 cx,
2789 );
2790 });
2791 // Same happens in the other direction.
2792 cx.assert_editor_state("ˇ a space");
2793 cx.update_editor(|editor, window, cx| {
2794 editor.delete_to_next_word_end(
2795 &DeleteToNextWordEnd {
2796 ignore_newlines: true,
2797 ignore_brackets: false,
2798 },
2799 window,
2800 cx,
2801 );
2802 });
2803 cx.assert_editor_state("ˇ space");
2804 cx.update_editor(|editor, window, cx| {
2805 editor.delete_to_next_word_end(
2806 &DeleteToNextWordEnd {
2807 ignore_newlines: true,
2808 ignore_brackets: false,
2809 },
2810 window,
2811 cx,
2812 );
2813 });
2814 cx.assert_editor_state("ˇ");
2815 cx.update_editor(|editor, window, cx| {
2816 editor.delete_to_next_word_end(
2817 &DeleteToNextWordEnd {
2818 ignore_newlines: true,
2819 ignore_brackets: false,
2820 },
2821 window,
2822 cx,
2823 );
2824 });
2825 cx.assert_editor_state("ˇ");
2826 cx.update_editor(|editor, window, cx| {
2827 editor.delete_to_previous_word_start(
2828 &DeleteToPreviousWordStart {
2829 ignore_newlines: true,
2830 ignore_brackets: false,
2831 },
2832 window,
2833 cx,
2834 );
2835 });
2836 cx.assert_editor_state("ˇ");
2837}
2838
2839#[gpui::test]
2840async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2841 init_test(cx, |_| {});
2842
2843 let language = Arc::new(
2844 Language::new(
2845 LanguageConfig {
2846 brackets: BracketPairConfig {
2847 pairs: vec![
2848 BracketPair {
2849 start: "\"".to_string(),
2850 end: "\"".to_string(),
2851 close: true,
2852 surround: true,
2853 newline: false,
2854 },
2855 BracketPair {
2856 start: "(".to_string(),
2857 end: ")".to_string(),
2858 close: true,
2859 surround: true,
2860 newline: true,
2861 },
2862 ],
2863 ..BracketPairConfig::default()
2864 },
2865 ..LanguageConfig::default()
2866 },
2867 Some(tree_sitter_rust::LANGUAGE.into()),
2868 )
2869 .with_brackets_query(
2870 r#"
2871 ("(" @open ")" @close)
2872 ("\"" @open "\"" @close)
2873 "#,
2874 )
2875 .unwrap(),
2876 );
2877
2878 let mut cx = EditorTestContext::new(cx).await;
2879 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2880
2881 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
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 // Deletion stops before brackets if asked to not ignore them.
2893 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
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 // Deletion has to remove a single bracket and then stop again.
2905 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2906
2907 cx.update_editor(|editor, window, cx| {
2908 editor.delete_to_previous_word_start(
2909 &DeleteToPreviousWordStart {
2910 ignore_newlines: true,
2911 ignore_brackets: false,
2912 },
2913 window,
2914 cx,
2915 );
2916 });
2917 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2918
2919 cx.update_editor(|editor, window, cx| {
2920 editor.delete_to_previous_word_start(
2921 &DeleteToPreviousWordStart {
2922 ignore_newlines: true,
2923 ignore_brackets: false,
2924 },
2925 window,
2926 cx,
2927 );
2928 });
2929 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2930
2931 cx.update_editor(|editor, window, cx| {
2932 editor.delete_to_previous_word_start(
2933 &DeleteToPreviousWordStart {
2934 ignore_newlines: true,
2935 ignore_brackets: false,
2936 },
2937 window,
2938 cx,
2939 );
2940 });
2941 cx.assert_editor_state(r#"ˇCOMMENT");"#);
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 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2954 cx.assert_editor_state(r#"ˇ");"#);
2955
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_next_word_end(
2958 &DeleteToNextWordEnd {
2959 ignore_newlines: true,
2960 ignore_brackets: false,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 cx.assert_editor_state(r#"ˇ"#);
2967
2968 cx.update_editor(|editor, window, cx| {
2969 editor.delete_to_next_word_end(
2970 &DeleteToNextWordEnd {
2971 ignore_newlines: true,
2972 ignore_brackets: false,
2973 },
2974 window,
2975 cx,
2976 );
2977 });
2978 cx.assert_editor_state(r#"ˇ"#);
2979
2980 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2981 cx.update_editor(|editor, window, cx| {
2982 editor.delete_to_previous_word_start(
2983 &DeleteToPreviousWordStart {
2984 ignore_newlines: true,
2985 ignore_brackets: true,
2986 },
2987 window,
2988 cx,
2989 );
2990 });
2991 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2992}
2993
2994#[gpui::test]
2995fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2996 init_test(cx, |_| {});
2997
2998 let editor = cx.add_window(|window, cx| {
2999 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3000 build_editor(buffer, window, cx)
3001 });
3002 let del_to_prev_word_start = DeleteToPreviousWordStart {
3003 ignore_newlines: false,
3004 ignore_brackets: false,
3005 };
3006 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3007 ignore_newlines: true,
3008 ignore_brackets: false,
3009 };
3010
3011 _ = editor.update(cx, |editor, window, cx| {
3012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3013 s.select_display_ranges([
3014 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3015 ])
3016 });
3017 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3018 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3019 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3020 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3021 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3022 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3023 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3024 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3025 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3026 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3027 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3028 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3029 });
3030}
3031
3032#[gpui::test]
3033fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3034 init_test(cx, |_| {});
3035
3036 let editor = cx.add_window(|window, cx| {
3037 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3038 build_editor(buffer, window, cx)
3039 });
3040 let del_to_next_word_end = DeleteToNextWordEnd {
3041 ignore_newlines: false,
3042 ignore_brackets: false,
3043 };
3044 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3045 ignore_newlines: true,
3046 ignore_brackets: false,
3047 };
3048
3049 _ = editor.update(cx, |editor, window, cx| {
3050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3051 s.select_display_ranges([
3052 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3053 ])
3054 });
3055 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3056 assert_eq!(
3057 editor.buffer.read(cx).read(cx).text(),
3058 "one\n two\nthree\n four"
3059 );
3060 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3061 assert_eq!(
3062 editor.buffer.read(cx).read(cx).text(),
3063 "\n two\nthree\n four"
3064 );
3065 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3066 assert_eq!(
3067 editor.buffer.read(cx).read(cx).text(),
3068 "two\nthree\n four"
3069 );
3070 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3071 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3072 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3073 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3074 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3075 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3076 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3077 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3078 });
3079}
3080
3081#[gpui::test]
3082fn test_newline(cx: &mut TestAppContext) {
3083 init_test(cx, |_| {});
3084
3085 let editor = cx.add_window(|window, cx| {
3086 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3087 build_editor(buffer, window, cx)
3088 });
3089
3090 _ = editor.update(cx, |editor, window, cx| {
3091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3092 s.select_display_ranges([
3093 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3094 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3095 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3096 ])
3097 });
3098
3099 editor.newline(&Newline, window, cx);
3100 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_newline_yaml(cx: &mut TestAppContext) {
3106 init_test(cx, |_| {});
3107
3108 let mut cx = EditorTestContext::new(cx).await;
3109 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3110 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3111
3112 // Object (between 2 fields)
3113 cx.set_state(indoc! {"
3114 test:ˇ
3115 hello: bye"});
3116 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 test:
3119 ˇ
3120 hello: bye"});
3121
3122 // Object (first and single line)
3123 cx.set_state(indoc! {"
3124 test:ˇ"});
3125 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3126 cx.assert_editor_state(indoc! {"
3127 test:
3128 ˇ"});
3129
3130 // Array with objects (after first element)
3131 cx.set_state(indoc! {"
3132 test:
3133 - foo: barˇ"});
3134 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3135 cx.assert_editor_state(indoc! {"
3136 test:
3137 - foo: bar
3138 ˇ"});
3139
3140 // Array with objects and comment
3141 cx.set_state(indoc! {"
3142 test:
3143 - foo: bar
3144 - bar: # testˇ"});
3145 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3146 cx.assert_editor_state(indoc! {"
3147 test:
3148 - foo: bar
3149 - bar: # test
3150 ˇ"});
3151
3152 // Array with objects (after second element)
3153 cx.set_state(indoc! {"
3154 test:
3155 - foo: bar
3156 - bar: fooˇ"});
3157 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 test:
3160 - foo: bar
3161 - bar: foo
3162 ˇ"});
3163
3164 // Array with strings (after first element)
3165 cx.set_state(indoc! {"
3166 test:
3167 - fooˇ"});
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 test:
3171 - foo
3172 ˇ"});
3173}
3174
3175#[gpui::test]
3176fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3177 init_test(cx, |_| {});
3178
3179 let editor = cx.add_window(|window, cx| {
3180 let buffer = MultiBuffer::build_simple(
3181 "
3182 a
3183 b(
3184 X
3185 )
3186 c(
3187 X
3188 )
3189 "
3190 .unindent()
3191 .as_str(),
3192 cx,
3193 );
3194 let mut editor = build_editor(buffer, window, cx);
3195 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3196 s.select_ranges([
3197 Point::new(2, 4)..Point::new(2, 5),
3198 Point::new(5, 4)..Point::new(5, 5),
3199 ])
3200 });
3201 editor
3202 });
3203
3204 _ = editor.update(cx, |editor, window, cx| {
3205 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3206 editor.buffer.update(cx, |buffer, cx| {
3207 buffer.edit(
3208 [
3209 (Point::new(1, 2)..Point::new(3, 0), ""),
3210 (Point::new(4, 2)..Point::new(6, 0), ""),
3211 ],
3212 None,
3213 cx,
3214 );
3215 assert_eq!(
3216 buffer.read(cx).text(),
3217 "
3218 a
3219 b()
3220 c()
3221 "
3222 .unindent()
3223 );
3224 });
3225 assert_eq!(
3226 editor.selections.ranges(&editor.display_snapshot(cx)),
3227 &[
3228 Point::new(1, 2)..Point::new(1, 2),
3229 Point::new(2, 2)..Point::new(2, 2),
3230 ],
3231 );
3232
3233 editor.newline(&Newline, window, cx);
3234 assert_eq!(
3235 editor.text(cx),
3236 "
3237 a
3238 b(
3239 )
3240 c(
3241 )
3242 "
3243 .unindent()
3244 );
3245
3246 // The selections are moved after the inserted newlines
3247 assert_eq!(
3248 editor.selections.ranges(&editor.display_snapshot(cx)),
3249 &[
3250 Point::new(2, 0)..Point::new(2, 0),
3251 Point::new(4, 0)..Point::new(4, 0),
3252 ],
3253 );
3254 });
3255}
3256
3257#[gpui::test]
3258async fn test_newline_above(cx: &mut TestAppContext) {
3259 init_test(cx, |settings| {
3260 settings.defaults.tab_size = NonZeroU32::new(4)
3261 });
3262
3263 let language = Arc::new(
3264 Language::new(
3265 LanguageConfig::default(),
3266 Some(tree_sitter_rust::LANGUAGE.into()),
3267 )
3268 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3269 .unwrap(),
3270 );
3271
3272 let mut cx = EditorTestContext::new(cx).await;
3273 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3274 cx.set_state(indoc! {"
3275 const a: ˇA = (
3276 (ˇ
3277 «const_functionˇ»(ˇ),
3278 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3279 )ˇ
3280 ˇ);ˇ
3281 "});
3282
3283 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3284 cx.assert_editor_state(indoc! {"
3285 ˇ
3286 const a: A = (
3287 ˇ
3288 (
3289 ˇ
3290 ˇ
3291 const_function(),
3292 ˇ
3293 ˇ
3294 ˇ
3295 ˇ
3296 something_else,
3297 ˇ
3298 )
3299 ˇ
3300 ˇ
3301 );
3302 "});
3303}
3304
3305#[gpui::test]
3306async fn test_newline_below(cx: &mut TestAppContext) {
3307 init_test(cx, |settings| {
3308 settings.defaults.tab_size = NonZeroU32::new(4)
3309 });
3310
3311 let language = Arc::new(
3312 Language::new(
3313 LanguageConfig::default(),
3314 Some(tree_sitter_rust::LANGUAGE.into()),
3315 )
3316 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3317 .unwrap(),
3318 );
3319
3320 let mut cx = EditorTestContext::new(cx).await;
3321 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3322 cx.set_state(indoc! {"
3323 const a: ˇA = (
3324 (ˇ
3325 «const_functionˇ»(ˇ),
3326 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3327 )ˇ
3328 ˇ);ˇ
3329 "});
3330
3331 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3332 cx.assert_editor_state(indoc! {"
3333 const a: A = (
3334 ˇ
3335 (
3336 ˇ
3337 const_function(),
3338 ˇ
3339 ˇ
3340 something_else,
3341 ˇ
3342 ˇ
3343 ˇ
3344 ˇ
3345 )
3346 ˇ
3347 );
3348 ˇ
3349 ˇ
3350 "});
3351}
3352
3353#[gpui::test]
3354async fn test_newline_comments(cx: &mut TestAppContext) {
3355 init_test(cx, |settings| {
3356 settings.defaults.tab_size = NonZeroU32::new(4)
3357 });
3358
3359 let language = Arc::new(Language::new(
3360 LanguageConfig {
3361 line_comments: vec!["// ".into()],
3362 ..LanguageConfig::default()
3363 },
3364 None,
3365 ));
3366 {
3367 let mut cx = EditorTestContext::new(cx).await;
3368 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3369 cx.set_state(indoc! {"
3370 // Fooˇ
3371 "});
3372
3373 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3374 cx.assert_editor_state(indoc! {"
3375 // Foo
3376 // ˇ
3377 "});
3378 // Ensure that we add comment prefix when existing line contains space
3379 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3380 cx.assert_editor_state(
3381 indoc! {"
3382 // Foo
3383 //s
3384 // ˇ
3385 "}
3386 .replace("s", " ") // s is used as space placeholder to prevent format on save
3387 .as_str(),
3388 );
3389 // Ensure that we add comment prefix when existing line does not contain space
3390 cx.set_state(indoc! {"
3391 // Foo
3392 //ˇ
3393 "});
3394 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3395 cx.assert_editor_state(indoc! {"
3396 // Foo
3397 //
3398 // ˇ
3399 "});
3400 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3401 cx.set_state(indoc! {"
3402 ˇ// Foo
3403 "});
3404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406
3407 ˇ// Foo
3408 "});
3409 }
3410 // Ensure that comment continuations can be disabled.
3411 update_test_language_settings(cx, |settings| {
3412 settings.defaults.extend_comment_on_newline = Some(false);
3413 });
3414 let mut cx = EditorTestContext::new(cx).await;
3415 cx.set_state(indoc! {"
3416 // Fooˇ
3417 "});
3418 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 // Foo
3421 ˇ
3422 "});
3423}
3424
3425#[gpui::test]
3426async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3427 init_test(cx, |settings| {
3428 settings.defaults.tab_size = NonZeroU32::new(4)
3429 });
3430
3431 let language = Arc::new(Language::new(
3432 LanguageConfig {
3433 line_comments: vec!["// ".into(), "/// ".into()],
3434 ..LanguageConfig::default()
3435 },
3436 None,
3437 ));
3438 {
3439 let mut cx = EditorTestContext::new(cx).await;
3440 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3441 cx.set_state(indoc! {"
3442 //ˇ
3443 "});
3444 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3445 cx.assert_editor_state(indoc! {"
3446 //
3447 // ˇ
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 ///ˇ
3452 "});
3453 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3454 cx.assert_editor_state(indoc! {"
3455 ///
3456 /// ˇ
3457 "});
3458 }
3459}
3460
3461#[gpui::test]
3462async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3463 init_test(cx, |settings| {
3464 settings.defaults.tab_size = NonZeroU32::new(4)
3465 });
3466
3467 let language = Arc::new(
3468 Language::new(
3469 LanguageConfig {
3470 documentation_comment: Some(language::BlockCommentConfig {
3471 start: "/**".into(),
3472 end: "*/".into(),
3473 prefix: "* ".into(),
3474 tab_size: 1,
3475 }),
3476
3477 ..LanguageConfig::default()
3478 },
3479 Some(tree_sitter_rust::LANGUAGE.into()),
3480 )
3481 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3482 .unwrap(),
3483 );
3484
3485 {
3486 let mut cx = EditorTestContext::new(cx).await;
3487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3488 cx.set_state(indoc! {"
3489 /**ˇ
3490 "});
3491
3492 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3493 cx.assert_editor_state(indoc! {"
3494 /**
3495 * ˇ
3496 "});
3497 // Ensure that if cursor is before the comment start,
3498 // we do not actually insert a comment prefix.
3499 cx.set_state(indoc! {"
3500 ˇ/**
3501 "});
3502 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504
3505 ˇ/**
3506 "});
3507 // Ensure that if cursor is between it doesn't add comment prefix.
3508 cx.set_state(indoc! {"
3509 /*ˇ*
3510 "});
3511 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3512 cx.assert_editor_state(indoc! {"
3513 /*
3514 ˇ*
3515 "});
3516 // Ensure that if suffix exists on same line after cursor it adds new line.
3517 cx.set_state(indoc! {"
3518 /**ˇ*/
3519 "});
3520 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 /**
3523 * ˇ
3524 */
3525 "});
3526 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3527 cx.set_state(indoc! {"
3528 /**ˇ */
3529 "});
3530 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3531 cx.assert_editor_state(indoc! {"
3532 /**
3533 * ˇ
3534 */
3535 "});
3536 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3537 cx.set_state(indoc! {"
3538 /** ˇ*/
3539 "});
3540 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3541 cx.assert_editor_state(
3542 indoc! {"
3543 /**s
3544 * ˇ
3545 */
3546 "}
3547 .replace("s", " ") // s is used as space placeholder to prevent format on save
3548 .as_str(),
3549 );
3550 // Ensure that delimiter space is preserved when newline on already
3551 // spaced delimiter.
3552 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3553 cx.assert_editor_state(
3554 indoc! {"
3555 /**s
3556 *s
3557 * ˇ
3558 */
3559 "}
3560 .replace("s", " ") // s is used as space placeholder to prevent format on save
3561 .as_str(),
3562 );
3563 // Ensure that delimiter space is preserved when space is not
3564 // on existing delimiter.
3565 cx.set_state(indoc! {"
3566 /**
3567 *ˇ
3568 */
3569 "});
3570 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3571 cx.assert_editor_state(indoc! {"
3572 /**
3573 *
3574 * ˇ
3575 */
3576 "});
3577 // Ensure that if suffix exists on same line after cursor it
3578 // doesn't add extra new line if prefix is not on same line.
3579 cx.set_state(indoc! {"
3580 /**
3581 ˇ*/
3582 "});
3583 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3584 cx.assert_editor_state(indoc! {"
3585 /**
3586
3587 ˇ*/
3588 "});
3589 // Ensure that it detects suffix after existing prefix.
3590 cx.set_state(indoc! {"
3591 /**ˇ/
3592 "});
3593 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 /**
3596 ˇ/
3597 "});
3598 // Ensure that if suffix exists on same line before
3599 // cursor it does not add comment prefix.
3600 cx.set_state(indoc! {"
3601 /** */ˇ
3602 "});
3603 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3604 cx.assert_editor_state(indoc! {"
3605 /** */
3606 ˇ
3607 "});
3608 // Ensure that if suffix exists on same line before
3609 // cursor it does not add comment prefix.
3610 cx.set_state(indoc! {"
3611 /**
3612 *
3613 */ˇ
3614 "});
3615 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3616 cx.assert_editor_state(indoc! {"
3617 /**
3618 *
3619 */
3620 ˇ
3621 "});
3622
3623 // Ensure that inline comment followed by code
3624 // doesn't add comment prefix on newline
3625 cx.set_state(indoc! {"
3626 /** */ textˇ
3627 "});
3628 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3629 cx.assert_editor_state(indoc! {"
3630 /** */ text
3631 ˇ
3632 "});
3633
3634 // Ensure that text after comment end tag
3635 // doesn't add comment prefix on newline
3636 cx.set_state(indoc! {"
3637 /**
3638 *
3639 */ˇtext
3640 "});
3641 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3642 cx.assert_editor_state(indoc! {"
3643 /**
3644 *
3645 */
3646 ˇtext
3647 "});
3648
3649 // Ensure if not comment block it doesn't
3650 // add comment prefix on newline
3651 cx.set_state(indoc! {"
3652 * textˇ
3653 "});
3654 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3655 cx.assert_editor_state(indoc! {"
3656 * text
3657 ˇ
3658 "});
3659 }
3660 // Ensure that comment continuations can be disabled.
3661 update_test_language_settings(cx, |settings| {
3662 settings.defaults.extend_comment_on_newline = Some(false);
3663 });
3664 let mut cx = EditorTestContext::new(cx).await;
3665 cx.set_state(indoc! {"
3666 /**ˇ
3667 "});
3668 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3669 cx.assert_editor_state(indoc! {"
3670 /**
3671 ˇ
3672 "});
3673}
3674
3675#[gpui::test]
3676async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.defaults.tab_size = NonZeroU32::new(4)
3679 });
3680
3681 let lua_language = Arc::new(Language::new(
3682 LanguageConfig {
3683 line_comments: vec!["--".into()],
3684 block_comment: Some(language::BlockCommentConfig {
3685 start: "--[[".into(),
3686 prefix: "".into(),
3687 end: "]]".into(),
3688 tab_size: 0,
3689 }),
3690 ..LanguageConfig::default()
3691 },
3692 None,
3693 ));
3694
3695 let mut cx = EditorTestContext::new(cx).await;
3696 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3697
3698 // Line with line comment should extend
3699 cx.set_state(indoc! {"
3700 --ˇ
3701 "});
3702 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 --
3705 --ˇ
3706 "});
3707
3708 // Line with block comment that matches line comment should not extend
3709 cx.set_state(indoc! {"
3710 --[[ˇ
3711 "});
3712 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3713 cx.assert_editor_state(indoc! {"
3714 --[[
3715 ˇ
3716 "});
3717}
3718
3719#[gpui::test]
3720fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3721 init_test(cx, |_| {});
3722
3723 let editor = cx.add_window(|window, cx| {
3724 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3725 let mut editor = build_editor(buffer, window, cx);
3726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3727 s.select_ranges([
3728 MultiBufferOffset(3)..MultiBufferOffset(4),
3729 MultiBufferOffset(11)..MultiBufferOffset(12),
3730 MultiBufferOffset(19)..MultiBufferOffset(20),
3731 ])
3732 });
3733 editor
3734 });
3735
3736 _ = editor.update(cx, |editor, window, cx| {
3737 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3738 editor.buffer.update(cx, |buffer, cx| {
3739 buffer.edit(
3740 [
3741 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3742 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3743 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3744 ],
3745 None,
3746 cx,
3747 );
3748 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3749 });
3750 assert_eq!(
3751 editor.selections.ranges(&editor.display_snapshot(cx)),
3752 &[
3753 MultiBufferOffset(2)..MultiBufferOffset(2),
3754 MultiBufferOffset(7)..MultiBufferOffset(7),
3755 MultiBufferOffset(12)..MultiBufferOffset(12)
3756 ],
3757 );
3758
3759 editor.insert("Z", window, cx);
3760 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3761
3762 // The selections are moved after the inserted characters
3763 assert_eq!(
3764 editor.selections.ranges(&editor.display_snapshot(cx)),
3765 &[
3766 MultiBufferOffset(3)..MultiBufferOffset(3),
3767 MultiBufferOffset(9)..MultiBufferOffset(9),
3768 MultiBufferOffset(15)..MultiBufferOffset(15)
3769 ],
3770 );
3771 });
3772}
3773
3774#[gpui::test]
3775async fn test_tab(cx: &mut TestAppContext) {
3776 init_test(cx, |settings| {
3777 settings.defaults.tab_size = NonZeroU32::new(3)
3778 });
3779
3780 let mut cx = EditorTestContext::new(cx).await;
3781 cx.set_state(indoc! {"
3782 ˇabˇc
3783 ˇ🏀ˇ🏀ˇefg
3784 dˇ
3785 "});
3786 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3787 cx.assert_editor_state(indoc! {"
3788 ˇab ˇc
3789 ˇ🏀 ˇ🏀 ˇefg
3790 d ˇ
3791 "});
3792
3793 cx.set_state(indoc! {"
3794 a
3795 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3796 "});
3797 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3798 cx.assert_editor_state(indoc! {"
3799 a
3800 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3801 "});
3802}
3803
3804#[gpui::test]
3805async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3806 init_test(cx, |_| {});
3807
3808 let mut cx = EditorTestContext::new(cx).await;
3809 let language = Arc::new(
3810 Language::new(
3811 LanguageConfig::default(),
3812 Some(tree_sitter_rust::LANGUAGE.into()),
3813 )
3814 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3815 .unwrap(),
3816 );
3817 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3818
3819 // test when all cursors are not at suggested indent
3820 // then simply move to their suggested indent location
3821 cx.set_state(indoc! {"
3822 const a: B = (
3823 c(
3824 ˇ
3825 ˇ )
3826 );
3827 "});
3828 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3829 cx.assert_editor_state(indoc! {"
3830 const a: B = (
3831 c(
3832 ˇ
3833 ˇ)
3834 );
3835 "});
3836
3837 // test cursor already at suggested indent not moving when
3838 // other cursors are yet to reach their suggested indents
3839 cx.set_state(indoc! {"
3840 ˇ
3841 const a: B = (
3842 c(
3843 d(
3844 ˇ
3845 )
3846 ˇ
3847 ˇ )
3848 );
3849 "});
3850 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3851 cx.assert_editor_state(indoc! {"
3852 ˇ
3853 const a: B = (
3854 c(
3855 d(
3856 ˇ
3857 )
3858 ˇ
3859 ˇ)
3860 );
3861 "});
3862 // test when all cursors are at suggested indent then tab is inserted
3863 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3864 cx.assert_editor_state(indoc! {"
3865 ˇ
3866 const a: B = (
3867 c(
3868 d(
3869 ˇ
3870 )
3871 ˇ
3872 ˇ)
3873 );
3874 "});
3875
3876 // test when current indent is less than suggested indent,
3877 // we adjust line to match suggested indent and move cursor to it
3878 //
3879 // when no other cursor is at word boundary, all of them should move
3880 cx.set_state(indoc! {"
3881 const a: B = (
3882 c(
3883 d(
3884 ˇ
3885 ˇ )
3886 ˇ )
3887 );
3888 "});
3889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3890 cx.assert_editor_state(indoc! {"
3891 const a: B = (
3892 c(
3893 d(
3894 ˇ
3895 ˇ)
3896 ˇ)
3897 );
3898 "});
3899
3900 // test when current indent is less than suggested indent,
3901 // we adjust line to match suggested indent and move cursor to it
3902 //
3903 // when some other cursor is at word boundary, it should not move
3904 cx.set_state(indoc! {"
3905 const a: B = (
3906 c(
3907 d(
3908 ˇ
3909 ˇ )
3910 ˇ)
3911 );
3912 "});
3913 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3914 cx.assert_editor_state(indoc! {"
3915 const a: B = (
3916 c(
3917 d(
3918 ˇ
3919 ˇ)
3920 ˇ)
3921 );
3922 "});
3923
3924 // test when current indent is more than suggested indent,
3925 // we just move cursor to current indent instead of suggested indent
3926 //
3927 // when no other cursor is at word boundary, all of them should move
3928 cx.set_state(indoc! {"
3929 const a: B = (
3930 c(
3931 d(
3932 ˇ
3933 ˇ )
3934 ˇ )
3935 );
3936 "});
3937 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3938 cx.assert_editor_state(indoc! {"
3939 const a: B = (
3940 c(
3941 d(
3942 ˇ
3943 ˇ)
3944 ˇ)
3945 );
3946 "});
3947 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3948 cx.assert_editor_state(indoc! {"
3949 const a: B = (
3950 c(
3951 d(
3952 ˇ
3953 ˇ)
3954 ˇ)
3955 );
3956 "});
3957
3958 // test when current indent is more than suggested indent,
3959 // we just move cursor to current indent instead of suggested indent
3960 //
3961 // when some other cursor is at word boundary, it doesn't move
3962 cx.set_state(indoc! {"
3963 const a: B = (
3964 c(
3965 d(
3966 ˇ
3967 ˇ )
3968 ˇ)
3969 );
3970 "});
3971 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3972 cx.assert_editor_state(indoc! {"
3973 const a: B = (
3974 c(
3975 d(
3976 ˇ
3977 ˇ)
3978 ˇ)
3979 );
3980 "});
3981
3982 // handle auto-indent when there are multiple cursors on the same line
3983 cx.set_state(indoc! {"
3984 const a: B = (
3985 c(
3986 ˇ ˇ
3987 ˇ )
3988 );
3989 "});
3990 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3991 cx.assert_editor_state(indoc! {"
3992 const a: B = (
3993 c(
3994 ˇ
3995 ˇ)
3996 );
3997 "});
3998}
3999
4000#[gpui::test]
4001async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4002 init_test(cx, |settings| {
4003 settings.defaults.tab_size = NonZeroU32::new(3)
4004 });
4005
4006 let mut cx = EditorTestContext::new(cx).await;
4007 cx.set_state(indoc! {"
4008 ˇ
4009 \t ˇ
4010 \t ˇ
4011 \t ˇ
4012 \t \t\t \t \t\t \t\t \t \t ˇ
4013 "});
4014
4015 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4016 cx.assert_editor_state(indoc! {"
4017 ˇ
4018 \t ˇ
4019 \t ˇ
4020 \t ˇ
4021 \t \t\t \t \t\t \t\t \t \t ˇ
4022 "});
4023}
4024
4025#[gpui::test]
4026async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4027 init_test(cx, |settings| {
4028 settings.defaults.tab_size = NonZeroU32::new(4)
4029 });
4030
4031 let language = Arc::new(
4032 Language::new(
4033 LanguageConfig::default(),
4034 Some(tree_sitter_rust::LANGUAGE.into()),
4035 )
4036 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4037 .unwrap(),
4038 );
4039
4040 let mut cx = EditorTestContext::new(cx).await;
4041 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4042 cx.set_state(indoc! {"
4043 fn a() {
4044 if b {
4045 \t ˇc
4046 }
4047 }
4048 "});
4049
4050 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4051 cx.assert_editor_state(indoc! {"
4052 fn a() {
4053 if b {
4054 ˇc
4055 }
4056 }
4057 "});
4058}
4059
4060#[gpui::test]
4061async fn test_indent_outdent(cx: &mut TestAppContext) {
4062 init_test(cx, |settings| {
4063 settings.defaults.tab_size = NonZeroU32::new(4);
4064 });
4065
4066 let mut cx = EditorTestContext::new(cx).await;
4067
4068 cx.set_state(indoc! {"
4069 «oneˇ» «twoˇ»
4070 three
4071 four
4072 "});
4073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4074 cx.assert_editor_state(indoc! {"
4075 «oneˇ» «twoˇ»
4076 three
4077 four
4078 "});
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 // select across line ending
4088 cx.set_state(indoc! {"
4089 one two
4090 t«hree
4091 ˇ» four
4092 "});
4093 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4094 cx.assert_editor_state(indoc! {"
4095 one two
4096 t«hree
4097 ˇ» four
4098 "});
4099
4100 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4101 cx.assert_editor_state(indoc! {"
4102 one two
4103 t«hree
4104 ˇ» four
4105 "});
4106
4107 // Ensure that indenting/outdenting works when the cursor is at column 0.
4108 cx.set_state(indoc! {"
4109 one two
4110 ˇthree
4111 four
4112 "});
4113 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4114 cx.assert_editor_state(indoc! {"
4115 one two
4116 ˇthree
4117 four
4118 "});
4119
4120 cx.set_state(indoc! {"
4121 one two
4122 ˇ three
4123 four
4124 "});
4125 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4126 cx.assert_editor_state(indoc! {"
4127 one two
4128 ˇthree
4129 four
4130 "});
4131}
4132
4133#[gpui::test]
4134async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4135 // This is a regression test for issue #33761
4136 init_test(cx, |_| {});
4137
4138 let mut cx = EditorTestContext::new(cx).await;
4139 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4140 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4141
4142 cx.set_state(
4143 r#"ˇ# ingress:
4144ˇ# api:
4145ˇ# enabled: false
4146ˇ# pathType: Prefix
4147ˇ# console:
4148ˇ# enabled: false
4149ˇ# pathType: Prefix
4150"#,
4151 );
4152
4153 // Press tab to indent all lines
4154 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4155
4156 cx.assert_editor_state(
4157 r#" ˇ# ingress:
4158 ˇ# api:
4159 ˇ# enabled: false
4160 ˇ# pathType: Prefix
4161 ˇ# console:
4162 ˇ# enabled: false
4163 ˇ# pathType: Prefix
4164"#,
4165 );
4166}
4167
4168#[gpui::test]
4169async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4170 // This is a test to make sure our fix for issue #33761 didn't break anything
4171 init_test(cx, |_| {});
4172
4173 let mut cx = EditorTestContext::new(cx).await;
4174 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4175 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4176
4177 cx.set_state(
4178 r#"ˇingress:
4179ˇ api:
4180ˇ enabled: false
4181ˇ pathType: Prefix
4182"#,
4183 );
4184
4185 // Press tab to indent all lines
4186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4187
4188 cx.assert_editor_state(
4189 r#"ˇingress:
4190 ˇapi:
4191 ˇenabled: false
4192 ˇpathType: Prefix
4193"#,
4194 );
4195}
4196
4197#[gpui::test]
4198async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4199 init_test(cx, |settings| {
4200 settings.defaults.hard_tabs = Some(true);
4201 });
4202
4203 let mut cx = EditorTestContext::new(cx).await;
4204
4205 // select two ranges on one line
4206 cx.set_state(indoc! {"
4207 «oneˇ» «twoˇ»
4208 three
4209 four
4210 "});
4211 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4212 cx.assert_editor_state(indoc! {"
4213 \t«oneˇ» «twoˇ»
4214 three
4215 four
4216 "});
4217 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 \t\t«oneˇ» «twoˇ»
4220 three
4221 four
4222 "});
4223 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4224 cx.assert_editor_state(indoc! {"
4225 \t«oneˇ» «twoˇ»
4226 three
4227 four
4228 "});
4229 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4230 cx.assert_editor_state(indoc! {"
4231 «oneˇ» «twoˇ»
4232 three
4233 four
4234 "});
4235
4236 // select across a line ending
4237 cx.set_state(indoc! {"
4238 one two
4239 t«hree
4240 ˇ»four
4241 "});
4242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4243 cx.assert_editor_state(indoc! {"
4244 one two
4245 \tt«hree
4246 ˇ»four
4247 "});
4248 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4249 cx.assert_editor_state(indoc! {"
4250 one two
4251 \t\tt«hree
4252 ˇ»four
4253 "});
4254 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4255 cx.assert_editor_state(indoc! {"
4256 one two
4257 \tt«hree
4258 ˇ»four
4259 "});
4260 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4261 cx.assert_editor_state(indoc! {"
4262 one two
4263 t«hree
4264 ˇ»four
4265 "});
4266
4267 // Ensure that indenting/outdenting works when the cursor is at column 0.
4268 cx.set_state(indoc! {"
4269 one two
4270 ˇthree
4271 four
4272 "});
4273 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4274 cx.assert_editor_state(indoc! {"
4275 one two
4276 ˇthree
4277 four
4278 "});
4279 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4280 cx.assert_editor_state(indoc! {"
4281 one two
4282 \tˇthree
4283 four
4284 "});
4285 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4286 cx.assert_editor_state(indoc! {"
4287 one two
4288 ˇthree
4289 four
4290 "});
4291}
4292
4293#[gpui::test]
4294fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4295 init_test(cx, |settings| {
4296 settings.languages.0.extend([
4297 (
4298 "TOML".into(),
4299 LanguageSettingsContent {
4300 tab_size: NonZeroU32::new(2),
4301 ..Default::default()
4302 },
4303 ),
4304 (
4305 "Rust".into(),
4306 LanguageSettingsContent {
4307 tab_size: NonZeroU32::new(4),
4308 ..Default::default()
4309 },
4310 ),
4311 ]);
4312 });
4313
4314 let toml_language = Arc::new(Language::new(
4315 LanguageConfig {
4316 name: "TOML".into(),
4317 ..Default::default()
4318 },
4319 None,
4320 ));
4321 let rust_language = Arc::new(Language::new(
4322 LanguageConfig {
4323 name: "Rust".into(),
4324 ..Default::default()
4325 },
4326 None,
4327 ));
4328
4329 let toml_buffer =
4330 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4331 let rust_buffer =
4332 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4333 let multibuffer = cx.new(|cx| {
4334 let mut multibuffer = MultiBuffer::new(ReadWrite);
4335 multibuffer.push_excerpts(
4336 toml_buffer.clone(),
4337 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4338 cx,
4339 );
4340 multibuffer.push_excerpts(
4341 rust_buffer.clone(),
4342 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4343 cx,
4344 );
4345 multibuffer
4346 });
4347
4348 cx.add_window(|window, cx| {
4349 let mut editor = build_editor(multibuffer, window, cx);
4350
4351 assert_eq!(
4352 editor.text(cx),
4353 indoc! {"
4354 a = 1
4355 b = 2
4356
4357 const c: usize = 3;
4358 "}
4359 );
4360
4361 select_ranges(
4362 &mut editor,
4363 indoc! {"
4364 «aˇ» = 1
4365 b = 2
4366
4367 «const c:ˇ» usize = 3;
4368 "},
4369 window,
4370 cx,
4371 );
4372
4373 editor.tab(&Tab, window, cx);
4374 assert_text_with_selections(
4375 &mut editor,
4376 indoc! {"
4377 «aˇ» = 1
4378 b = 2
4379
4380 «const c:ˇ» usize = 3;
4381 "},
4382 cx,
4383 );
4384 editor.backtab(&Backtab, window, cx);
4385 assert_text_with_selections(
4386 &mut editor,
4387 indoc! {"
4388 «aˇ» = 1
4389 b = 2
4390
4391 «const c:ˇ» usize = 3;
4392 "},
4393 cx,
4394 );
4395
4396 editor
4397 });
4398}
4399
4400#[gpui::test]
4401async fn test_backspace(cx: &mut TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let mut cx = EditorTestContext::new(cx).await;
4405
4406 // Basic backspace
4407 cx.set_state(indoc! {"
4408 onˇe two three
4409 fou«rˇ» five six
4410 seven «ˇeight nine
4411 »ten
4412 "});
4413 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4414 cx.assert_editor_state(indoc! {"
4415 oˇe two three
4416 fouˇ five six
4417 seven ˇten
4418 "});
4419
4420 // Test backspace inside and around indents
4421 cx.set_state(indoc! {"
4422 zero
4423 ˇone
4424 ˇtwo
4425 ˇ ˇ ˇ three
4426 ˇ ˇ four
4427 "});
4428 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 zero
4431 ˇone
4432 ˇtwo
4433 ˇ threeˇ four
4434 "});
4435}
4436
4437#[gpui::test]
4438async fn test_delete(cx: &mut TestAppContext) {
4439 init_test(cx, |_| {});
4440
4441 let mut cx = EditorTestContext::new(cx).await;
4442 cx.set_state(indoc! {"
4443 onˇe two three
4444 fou«rˇ» five six
4445 seven «ˇeight nine
4446 »ten
4447 "});
4448 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4449 cx.assert_editor_state(indoc! {"
4450 onˇ two three
4451 fouˇ five six
4452 seven ˇten
4453 "});
4454}
4455
4456#[gpui::test]
4457fn test_delete_line(cx: &mut TestAppContext) {
4458 init_test(cx, |_| {});
4459
4460 let editor = cx.add_window(|window, cx| {
4461 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4462 build_editor(buffer, window, cx)
4463 });
4464 _ = editor.update(cx, |editor, window, cx| {
4465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4466 s.select_display_ranges([
4467 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4468 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4469 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4470 ])
4471 });
4472 editor.delete_line(&DeleteLine, window, cx);
4473 assert_eq!(editor.display_text(cx), "ghi");
4474 assert_eq!(
4475 display_ranges(editor, cx),
4476 vec![
4477 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4478 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4479 ]
4480 );
4481 });
4482
4483 let editor = cx.add_window(|window, cx| {
4484 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4485 build_editor(buffer, window, cx)
4486 });
4487 _ = editor.update(cx, |editor, window, cx| {
4488 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4489 s.select_display_ranges([
4490 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4491 ])
4492 });
4493 editor.delete_line(&DeleteLine, window, cx);
4494 assert_eq!(editor.display_text(cx), "ghi\n");
4495 assert_eq!(
4496 display_ranges(editor, cx),
4497 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4498 );
4499 });
4500
4501 let editor = cx.add_window(|window, cx| {
4502 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4503 build_editor(buffer, window, cx)
4504 });
4505 _ = editor.update(cx, |editor, window, cx| {
4506 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4507 s.select_display_ranges([
4508 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4509 ])
4510 });
4511 editor.delete_line(&DeleteLine, window, cx);
4512 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4513 assert_eq!(
4514 display_ranges(editor, cx),
4515 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4516 );
4517 });
4518}
4519
4520#[gpui::test]
4521fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4522 init_test(cx, |_| {});
4523
4524 cx.add_window(|window, cx| {
4525 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4526 let mut editor = build_editor(buffer.clone(), window, cx);
4527 let buffer = buffer.read(cx).as_singleton().unwrap();
4528
4529 assert_eq!(
4530 editor
4531 .selections
4532 .ranges::<Point>(&editor.display_snapshot(cx)),
4533 &[Point::new(0, 0)..Point::new(0, 0)]
4534 );
4535
4536 // When on single line, replace newline at end by space
4537 editor.join_lines(&JoinLines, window, cx);
4538 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4539 assert_eq!(
4540 editor
4541 .selections
4542 .ranges::<Point>(&editor.display_snapshot(cx)),
4543 &[Point::new(0, 3)..Point::new(0, 3)]
4544 );
4545
4546 // When multiple lines are selected, remove newlines that are spanned by the selection
4547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4548 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4549 });
4550 editor.join_lines(&JoinLines, window, cx);
4551 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4552 assert_eq!(
4553 editor
4554 .selections
4555 .ranges::<Point>(&editor.display_snapshot(cx)),
4556 &[Point::new(0, 11)..Point::new(0, 11)]
4557 );
4558
4559 // Undo should be transactional
4560 editor.undo(&Undo, window, cx);
4561 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4562 assert_eq!(
4563 editor
4564 .selections
4565 .ranges::<Point>(&editor.display_snapshot(cx)),
4566 &[Point::new(0, 5)..Point::new(2, 2)]
4567 );
4568
4569 // When joining an empty line don't insert a space
4570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4571 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4572 });
4573 editor.join_lines(&JoinLines, window, cx);
4574 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4575 assert_eq!(
4576 editor
4577 .selections
4578 .ranges::<Point>(&editor.display_snapshot(cx)),
4579 [Point::new(2, 3)..Point::new(2, 3)]
4580 );
4581
4582 // We can remove trailing newlines
4583 editor.join_lines(&JoinLines, window, cx);
4584 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4585 assert_eq!(
4586 editor
4587 .selections
4588 .ranges::<Point>(&editor.display_snapshot(cx)),
4589 [Point::new(2, 3)..Point::new(2, 3)]
4590 );
4591
4592 // We don't blow up on the last line
4593 editor.join_lines(&JoinLines, window, cx);
4594 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4595 assert_eq!(
4596 editor
4597 .selections
4598 .ranges::<Point>(&editor.display_snapshot(cx)),
4599 [Point::new(2, 3)..Point::new(2, 3)]
4600 );
4601
4602 // reset to test indentation
4603 editor.buffer.update(cx, |buffer, cx| {
4604 buffer.edit(
4605 [
4606 (Point::new(1, 0)..Point::new(1, 2), " "),
4607 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4608 ],
4609 None,
4610 cx,
4611 )
4612 });
4613
4614 // We remove any leading spaces
4615 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4617 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4618 });
4619 editor.join_lines(&JoinLines, window, cx);
4620 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4621
4622 // We don't insert a space for a line containing only spaces
4623 editor.join_lines(&JoinLines, window, cx);
4624 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4625
4626 // We ignore any leading tabs
4627 editor.join_lines(&JoinLines, window, cx);
4628 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4629
4630 editor
4631 });
4632}
4633
4634#[gpui::test]
4635fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4636 init_test(cx, |_| {});
4637
4638 cx.add_window(|window, cx| {
4639 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4640 let mut editor = build_editor(buffer.clone(), window, cx);
4641 let buffer = buffer.read(cx).as_singleton().unwrap();
4642
4643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4644 s.select_ranges([
4645 Point::new(0, 2)..Point::new(1, 1),
4646 Point::new(1, 2)..Point::new(1, 2),
4647 Point::new(3, 1)..Point::new(3, 2),
4648 ])
4649 });
4650
4651 editor.join_lines(&JoinLines, window, cx);
4652 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4653
4654 assert_eq!(
4655 editor
4656 .selections
4657 .ranges::<Point>(&editor.display_snapshot(cx)),
4658 [
4659 Point::new(0, 7)..Point::new(0, 7),
4660 Point::new(1, 3)..Point::new(1, 3)
4661 ]
4662 );
4663 editor
4664 });
4665}
4666
4667#[gpui::test]
4668async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4669 init_test(cx, |_| {});
4670
4671 let mut cx = EditorTestContext::new(cx).await;
4672
4673 let diff_base = r#"
4674 Line 0
4675 Line 1
4676 Line 2
4677 Line 3
4678 "#
4679 .unindent();
4680
4681 cx.set_state(
4682 &r#"
4683 ˇLine 0
4684 Line 1
4685 Line 2
4686 Line 3
4687 "#
4688 .unindent(),
4689 );
4690
4691 cx.set_head_text(&diff_base);
4692 executor.run_until_parked();
4693
4694 // Join lines
4695 cx.update_editor(|editor, window, cx| {
4696 editor.join_lines(&JoinLines, window, cx);
4697 });
4698 executor.run_until_parked();
4699
4700 cx.assert_editor_state(
4701 &r#"
4702 Line 0ˇ Line 1
4703 Line 2
4704 Line 3
4705 "#
4706 .unindent(),
4707 );
4708 // Join again
4709 cx.update_editor(|editor, window, cx| {
4710 editor.join_lines(&JoinLines, window, cx);
4711 });
4712 executor.run_until_parked();
4713
4714 cx.assert_editor_state(
4715 &r#"
4716 Line 0 Line 1ˇ Line 2
4717 Line 3
4718 "#
4719 .unindent(),
4720 );
4721}
4722
4723#[gpui::test]
4724async fn test_custom_newlines_cause_no_false_positive_diffs(
4725 executor: BackgroundExecutor,
4726 cx: &mut TestAppContext,
4727) {
4728 init_test(cx, |_| {});
4729 let mut cx = EditorTestContext::new(cx).await;
4730 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4731 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4732 executor.run_until_parked();
4733
4734 cx.update_editor(|editor, window, cx| {
4735 let snapshot = editor.snapshot(window, cx);
4736 assert_eq!(
4737 snapshot
4738 .buffer_snapshot()
4739 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4740 .collect::<Vec<_>>(),
4741 Vec::new(),
4742 "Should not have any diffs for files with custom newlines"
4743 );
4744 });
4745}
4746
4747#[gpui::test]
4748async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4749 init_test(cx, |_| {});
4750
4751 let mut cx = EditorTestContext::new(cx).await;
4752
4753 // Test sort_lines_case_insensitive()
4754 cx.set_state(indoc! {"
4755 «z
4756 y
4757 x
4758 Z
4759 Y
4760 Xˇ»
4761 "});
4762 cx.update_editor(|e, window, cx| {
4763 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4764 });
4765 cx.assert_editor_state(indoc! {"
4766 «x
4767 X
4768 y
4769 Y
4770 z
4771 Zˇ»
4772 "});
4773
4774 // Test sort_lines_by_length()
4775 //
4776 // Demonstrates:
4777 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4778 // - sort is stable
4779 cx.set_state(indoc! {"
4780 «123
4781 æ
4782 12
4783 ∞
4784 1
4785 æˇ»
4786 "});
4787 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4788 cx.assert_editor_state(indoc! {"
4789 «æ
4790 ∞
4791 1
4792 æ
4793 12
4794 123ˇ»
4795 "});
4796
4797 // Test reverse_lines()
4798 cx.set_state(indoc! {"
4799 «5
4800 4
4801 3
4802 2
4803 1ˇ»
4804 "});
4805 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4806 cx.assert_editor_state(indoc! {"
4807 «1
4808 2
4809 3
4810 4
4811 5ˇ»
4812 "});
4813
4814 // Skip testing shuffle_line()
4815
4816 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4817 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4818
4819 // Don't manipulate when cursor is on single line, but expand the selection
4820 cx.set_state(indoc! {"
4821 ddˇdd
4822 ccc
4823 bb
4824 a
4825 "});
4826 cx.update_editor(|e, window, cx| {
4827 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4828 });
4829 cx.assert_editor_state(indoc! {"
4830 «ddddˇ»
4831 ccc
4832 bb
4833 a
4834 "});
4835
4836 // Basic manipulate case
4837 // Start selection moves to column 0
4838 // End of selection shrinks to fit shorter line
4839 cx.set_state(indoc! {"
4840 dd«d
4841 ccc
4842 bb
4843 aaaaaˇ»
4844 "});
4845 cx.update_editor(|e, window, cx| {
4846 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4847 });
4848 cx.assert_editor_state(indoc! {"
4849 «aaaaa
4850 bb
4851 ccc
4852 dddˇ»
4853 "});
4854
4855 // Manipulate case with newlines
4856 cx.set_state(indoc! {"
4857 dd«d
4858 ccc
4859
4860 bb
4861 aaaaa
4862
4863 ˇ»
4864 "});
4865 cx.update_editor(|e, window, cx| {
4866 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4867 });
4868 cx.assert_editor_state(indoc! {"
4869 «
4870
4871 aaaaa
4872 bb
4873 ccc
4874 dddˇ»
4875
4876 "});
4877
4878 // Adding new line
4879 cx.set_state(indoc! {"
4880 aa«a
4881 bbˇ»b
4882 "});
4883 cx.update_editor(|e, window, cx| {
4884 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4885 });
4886 cx.assert_editor_state(indoc! {"
4887 «aaa
4888 bbb
4889 added_lineˇ»
4890 "});
4891
4892 // Removing line
4893 cx.set_state(indoc! {"
4894 aa«a
4895 bbbˇ»
4896 "});
4897 cx.update_editor(|e, window, cx| {
4898 e.manipulate_immutable_lines(window, cx, |lines| {
4899 lines.pop();
4900 })
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «aaaˇ»
4904 "});
4905
4906 // Removing all lines
4907 cx.set_state(indoc! {"
4908 aa«a
4909 bbbˇ»
4910 "});
4911 cx.update_editor(|e, window, cx| {
4912 e.manipulate_immutable_lines(window, cx, |lines| {
4913 lines.drain(..);
4914 })
4915 });
4916 cx.assert_editor_state(indoc! {"
4917 ˇ
4918 "});
4919}
4920
4921#[gpui::test]
4922async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4923 init_test(cx, |_| {});
4924
4925 let mut cx = EditorTestContext::new(cx).await;
4926
4927 // Consider continuous selection as single selection
4928 cx.set_state(indoc! {"
4929 Aaa«aa
4930 cˇ»c«c
4931 bb
4932 aaaˇ»aa
4933 "});
4934 cx.update_editor(|e, window, cx| {
4935 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4936 });
4937 cx.assert_editor_state(indoc! {"
4938 «Aaaaa
4939 ccc
4940 bb
4941 aaaaaˇ»
4942 "});
4943
4944 cx.set_state(indoc! {"
4945 Aaa«aa
4946 cˇ»c«c
4947 bb
4948 aaaˇ»aa
4949 "});
4950 cx.update_editor(|e, window, cx| {
4951 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4952 });
4953 cx.assert_editor_state(indoc! {"
4954 «Aaaaa
4955 ccc
4956 bbˇ»
4957 "});
4958
4959 // Consider non continuous selection as distinct dedup operations
4960 cx.set_state(indoc! {"
4961 «aaaaa
4962 bb
4963 aaaaa
4964 aaaaaˇ»
4965
4966 aaa«aaˇ»
4967 "});
4968 cx.update_editor(|e, window, cx| {
4969 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4970 });
4971 cx.assert_editor_state(indoc! {"
4972 «aaaaa
4973 bbˇ»
4974
4975 «aaaaaˇ»
4976 "});
4977}
4978
4979#[gpui::test]
4980async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4981 init_test(cx, |_| {});
4982
4983 let mut cx = EditorTestContext::new(cx).await;
4984
4985 cx.set_state(indoc! {"
4986 «Aaa
4987 aAa
4988 Aaaˇ»
4989 "});
4990 cx.update_editor(|e, window, cx| {
4991 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4992 });
4993 cx.assert_editor_state(indoc! {"
4994 «Aaa
4995 aAaˇ»
4996 "});
4997
4998 cx.set_state(indoc! {"
4999 «Aaa
5000 aAa
5001 aaAˇ»
5002 "});
5003 cx.update_editor(|e, window, cx| {
5004 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5005 });
5006 cx.assert_editor_state(indoc! {"
5007 «Aaaˇ»
5008 "});
5009}
5010
5011#[gpui::test]
5012async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5013 init_test(cx, |_| {});
5014
5015 let mut cx = EditorTestContext::new(cx).await;
5016
5017 let js_language = Arc::new(Language::new(
5018 LanguageConfig {
5019 name: "JavaScript".into(),
5020 wrap_characters: Some(language::WrapCharactersConfig {
5021 start_prefix: "<".into(),
5022 start_suffix: ">".into(),
5023 end_prefix: "</".into(),
5024 end_suffix: ">".into(),
5025 }),
5026 ..LanguageConfig::default()
5027 },
5028 None,
5029 ));
5030
5031 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5032
5033 cx.set_state(indoc! {"
5034 «testˇ»
5035 "});
5036 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5037 cx.assert_editor_state(indoc! {"
5038 <«ˇ»>test</«ˇ»>
5039 "});
5040
5041 cx.set_state(indoc! {"
5042 «test
5043 testˇ»
5044 "});
5045 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5046 cx.assert_editor_state(indoc! {"
5047 <«ˇ»>test
5048 test</«ˇ»>
5049 "});
5050
5051 cx.set_state(indoc! {"
5052 teˇst
5053 "});
5054 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5055 cx.assert_editor_state(indoc! {"
5056 te<«ˇ»></«ˇ»>st
5057 "});
5058}
5059
5060#[gpui::test]
5061async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5062 init_test(cx, |_| {});
5063
5064 let mut cx = EditorTestContext::new(cx).await;
5065
5066 let js_language = Arc::new(Language::new(
5067 LanguageConfig {
5068 name: "JavaScript".into(),
5069 wrap_characters: Some(language::WrapCharactersConfig {
5070 start_prefix: "<".into(),
5071 start_suffix: ">".into(),
5072 end_prefix: "</".into(),
5073 end_suffix: ">".into(),
5074 }),
5075 ..LanguageConfig::default()
5076 },
5077 None,
5078 ));
5079
5080 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5081
5082 cx.set_state(indoc! {"
5083 «testˇ»
5084 «testˇ» «testˇ»
5085 «testˇ»
5086 "});
5087 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5088 cx.assert_editor_state(indoc! {"
5089 <«ˇ»>test</«ˇ»>
5090 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5091 <«ˇ»>test</«ˇ»>
5092 "});
5093
5094 cx.set_state(indoc! {"
5095 «test
5096 testˇ»
5097 «test
5098 testˇ»
5099 "});
5100 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5101 cx.assert_editor_state(indoc! {"
5102 <«ˇ»>test
5103 test</«ˇ»>
5104 <«ˇ»>test
5105 test</«ˇ»>
5106 "});
5107}
5108
5109#[gpui::test]
5110async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5111 init_test(cx, |_| {});
5112
5113 let mut cx = EditorTestContext::new(cx).await;
5114
5115 let plaintext_language = Arc::new(Language::new(
5116 LanguageConfig {
5117 name: "Plain Text".into(),
5118 ..LanguageConfig::default()
5119 },
5120 None,
5121 ));
5122
5123 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5124
5125 cx.set_state(indoc! {"
5126 «testˇ»
5127 "});
5128 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5129 cx.assert_editor_state(indoc! {"
5130 «testˇ»
5131 "});
5132}
5133
5134#[gpui::test]
5135async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5136 init_test(cx, |_| {});
5137
5138 let mut cx = EditorTestContext::new(cx).await;
5139
5140 // Manipulate with multiple selections on a single line
5141 cx.set_state(indoc! {"
5142 dd«dd
5143 cˇ»c«c
5144 bb
5145 aaaˇ»aa
5146 "});
5147 cx.update_editor(|e, window, cx| {
5148 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5149 });
5150 cx.assert_editor_state(indoc! {"
5151 «aaaaa
5152 bb
5153 ccc
5154 ddddˇ»
5155 "});
5156
5157 // Manipulate with multiple disjoin selections
5158 cx.set_state(indoc! {"
5159 5«
5160 4
5161 3
5162 2
5163 1ˇ»
5164
5165 dd«dd
5166 ccc
5167 bb
5168 aaaˇ»aa
5169 "});
5170 cx.update_editor(|e, window, cx| {
5171 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5172 });
5173 cx.assert_editor_state(indoc! {"
5174 «1
5175 2
5176 3
5177 4
5178 5ˇ»
5179
5180 «aaaaa
5181 bb
5182 ccc
5183 ddddˇ»
5184 "});
5185
5186 // Adding lines on each selection
5187 cx.set_state(indoc! {"
5188 2«
5189 1ˇ»
5190
5191 bb«bb
5192 aaaˇ»aa
5193 "});
5194 cx.update_editor(|e, window, cx| {
5195 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5196 });
5197 cx.assert_editor_state(indoc! {"
5198 «2
5199 1
5200 added lineˇ»
5201
5202 «bbbb
5203 aaaaa
5204 added lineˇ»
5205 "});
5206
5207 // Removing lines on each selection
5208 cx.set_state(indoc! {"
5209 2«
5210 1ˇ»
5211
5212 bb«bb
5213 aaaˇ»aa
5214 "});
5215 cx.update_editor(|e, window, cx| {
5216 e.manipulate_immutable_lines(window, cx, |lines| {
5217 lines.pop();
5218 })
5219 });
5220 cx.assert_editor_state(indoc! {"
5221 «2ˇ»
5222
5223 «bbbbˇ»
5224 "});
5225}
5226
5227#[gpui::test]
5228async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5229 init_test(cx, |settings| {
5230 settings.defaults.tab_size = NonZeroU32::new(3)
5231 });
5232
5233 let mut cx = EditorTestContext::new(cx).await;
5234
5235 // MULTI SELECTION
5236 // Ln.1 "«" tests empty lines
5237 // Ln.9 tests just leading whitespace
5238 cx.set_state(indoc! {"
5239 «
5240 abc // No indentationˇ»
5241 «\tabc // 1 tabˇ»
5242 \t\tabc « ˇ» // 2 tabs
5243 \t ab«c // Tab followed by space
5244 \tabc // Space followed by tab (3 spaces should be the result)
5245 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5246 abˇ»ˇc ˇ ˇ // Already space indented«
5247 \t
5248 \tabc\tdef // Only the leading tab is manipulatedˇ»
5249 "});
5250 cx.update_editor(|e, window, cx| {
5251 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5252 });
5253 cx.assert_editor_state(
5254 indoc! {"
5255 «
5256 abc // No indentation
5257 abc // 1 tab
5258 abc // 2 tabs
5259 abc // Tab followed by space
5260 abc // Space followed by tab (3 spaces should be the result)
5261 abc // Mixed indentation (tab conversion depends on the column)
5262 abc // Already space indented
5263 ·
5264 abc\tdef // Only the leading tab is manipulatedˇ»
5265 "}
5266 .replace("·", "")
5267 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5268 );
5269
5270 // Test on just a few lines, the others should remain unchanged
5271 // Only lines (3, 5, 10, 11) should change
5272 cx.set_state(
5273 indoc! {"
5274 ·
5275 abc // No indentation
5276 \tabcˇ // 1 tab
5277 \t\tabc // 2 tabs
5278 \t abcˇ // Tab followed by space
5279 \tabc // Space followed by tab (3 spaces should be the result)
5280 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5281 abc // Already space indented
5282 «\t
5283 \tabc\tdef // Only the leading tab is manipulatedˇ»
5284 "}
5285 .replace("·", "")
5286 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5287 );
5288 cx.update_editor(|e, window, cx| {
5289 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5290 });
5291 cx.assert_editor_state(
5292 indoc! {"
5293 ·
5294 abc // No indentation
5295 « abc // 1 tabˇ»
5296 \t\tabc // 2 tabs
5297 « abc // Tab followed by spaceˇ»
5298 \tabc // Space followed by tab (3 spaces should be the result)
5299 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5300 abc // Already space indented
5301 « ·
5302 abc\tdef // Only the leading tab is manipulatedˇ»
5303 "}
5304 .replace("·", "")
5305 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5306 );
5307
5308 // SINGLE SELECTION
5309 // Ln.1 "«" tests empty lines
5310 // Ln.9 tests just leading whitespace
5311 cx.set_state(indoc! {"
5312 «
5313 abc // No indentation
5314 \tabc // 1 tab
5315 \t\tabc // 2 tabs
5316 \t abc // Tab followed by space
5317 \tabc // Space followed by tab (3 spaces should be the result)
5318 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5319 abc // Already space indented
5320 \t
5321 \tabc\tdef // Only the leading tab is manipulatedˇ»
5322 "});
5323 cx.update_editor(|e, window, cx| {
5324 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5325 });
5326 cx.assert_editor_state(
5327 indoc! {"
5328 «
5329 abc // No indentation
5330 abc // 1 tab
5331 abc // 2 tabs
5332 abc // Tab followed by space
5333 abc // Space followed by tab (3 spaces should be the result)
5334 abc // Mixed indentation (tab conversion depends on the column)
5335 abc // Already space indented
5336 ·
5337 abc\tdef // Only the leading tab is manipulatedˇ»
5338 "}
5339 .replace("·", "")
5340 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5341 );
5342}
5343
5344#[gpui::test]
5345async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5346 init_test(cx, |settings| {
5347 settings.defaults.tab_size = NonZeroU32::new(3)
5348 });
5349
5350 let mut cx = EditorTestContext::new(cx).await;
5351
5352 // MULTI SELECTION
5353 // Ln.1 "«" tests empty lines
5354 // Ln.11 tests just leading whitespace
5355 cx.set_state(indoc! {"
5356 «
5357 abˇ»ˇc // No indentation
5358 abc ˇ ˇ // 1 space (< 3 so dont convert)
5359 abc « // 2 spaces (< 3 so dont convert)
5360 abc // 3 spaces (convert)
5361 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5362 «\tˇ»\t«\tˇ»abc // Already tab indented
5363 «\t abc // Tab followed by space
5364 \tabc // Space followed by tab (should be consumed due to tab)
5365 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5366 \tˇ» «\t
5367 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5368 "});
5369 cx.update_editor(|e, window, cx| {
5370 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5371 });
5372 cx.assert_editor_state(indoc! {"
5373 «
5374 abc // No indentation
5375 abc // 1 space (< 3 so dont convert)
5376 abc // 2 spaces (< 3 so dont convert)
5377 \tabc // 3 spaces (convert)
5378 \t abc // 5 spaces (1 tab + 2 spaces)
5379 \t\t\tabc // Already tab indented
5380 \t abc // Tab followed by space
5381 \tabc // Space followed by tab (should be consumed due to tab)
5382 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5383 \t\t\t
5384 \tabc \t // Only the leading spaces should be convertedˇ»
5385 "});
5386
5387 // Test on just a few lines, the other should remain unchanged
5388 // Only lines (4, 8, 11, 12) should change
5389 cx.set_state(
5390 indoc! {"
5391 ·
5392 abc // No indentation
5393 abc // 1 space (< 3 so dont convert)
5394 abc // 2 spaces (< 3 so dont convert)
5395 « abc // 3 spaces (convert)ˇ»
5396 abc // 5 spaces (1 tab + 2 spaces)
5397 \t\t\tabc // Already tab indented
5398 \t abc // Tab followed by space
5399 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5400 \t\t \tabc // Mixed indentation
5401 \t \t \t \tabc // Mixed indentation
5402 \t \tˇ
5403 « abc \t // Only the leading spaces should be convertedˇ»
5404 "}
5405 .replace("·", "")
5406 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5407 );
5408 cx.update_editor(|e, window, cx| {
5409 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5410 });
5411 cx.assert_editor_state(
5412 indoc! {"
5413 ·
5414 abc // No indentation
5415 abc // 1 space (< 3 so dont convert)
5416 abc // 2 spaces (< 3 so dont convert)
5417 «\tabc // 3 spaces (convert)ˇ»
5418 abc // 5 spaces (1 tab + 2 spaces)
5419 \t\t\tabc // Already tab indented
5420 \t abc // Tab followed by space
5421 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5422 \t\t \tabc // Mixed indentation
5423 \t \t \t \tabc // Mixed indentation
5424 «\t\t\t
5425 \tabc \t // Only the leading spaces should be convertedˇ»
5426 "}
5427 .replace("·", "")
5428 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5429 );
5430
5431 // SINGLE SELECTION
5432 // Ln.1 "«" tests empty lines
5433 // Ln.11 tests just leading whitespace
5434 cx.set_state(indoc! {"
5435 «
5436 abc // No indentation
5437 abc // 1 space (< 3 so dont convert)
5438 abc // 2 spaces (< 3 so dont convert)
5439 abc // 3 spaces (convert)
5440 abc // 5 spaces (1 tab + 2 spaces)
5441 \t\t\tabc // Already tab indented
5442 \t abc // Tab followed by space
5443 \tabc // Space followed by tab (should be consumed due to tab)
5444 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5445 \t \t
5446 abc \t // Only the leading spaces should be convertedˇ»
5447 "});
5448 cx.update_editor(|e, window, cx| {
5449 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5450 });
5451 cx.assert_editor_state(indoc! {"
5452 «
5453 abc // No indentation
5454 abc // 1 space (< 3 so dont convert)
5455 abc // 2 spaces (< 3 so dont convert)
5456 \tabc // 3 spaces (convert)
5457 \t abc // 5 spaces (1 tab + 2 spaces)
5458 \t\t\tabc // Already tab indented
5459 \t abc // Tab followed by space
5460 \tabc // Space followed by tab (should be consumed due to tab)
5461 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5462 \t\t\t
5463 \tabc \t // Only the leading spaces should be convertedˇ»
5464 "});
5465}
5466
5467#[gpui::test]
5468async fn test_toggle_case(cx: &mut TestAppContext) {
5469 init_test(cx, |_| {});
5470
5471 let mut cx = EditorTestContext::new(cx).await;
5472
5473 // If all lower case -> upper case
5474 cx.set_state(indoc! {"
5475 «hello worldˇ»
5476 "});
5477 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5478 cx.assert_editor_state(indoc! {"
5479 «HELLO WORLDˇ»
5480 "});
5481
5482 // If all upper case -> lower case
5483 cx.set_state(indoc! {"
5484 «HELLO WORLDˇ»
5485 "});
5486 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5487 cx.assert_editor_state(indoc! {"
5488 «hello worldˇ»
5489 "});
5490
5491 // If any upper case characters are identified -> lower case
5492 // This matches JetBrains IDEs
5493 cx.set_state(indoc! {"
5494 «hEllo worldˇ»
5495 "});
5496 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5497 cx.assert_editor_state(indoc! {"
5498 «hello worldˇ»
5499 "});
5500}
5501
5502#[gpui::test]
5503async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5504 init_test(cx, |_| {});
5505
5506 let mut cx = EditorTestContext::new(cx).await;
5507
5508 cx.set_state(indoc! {"
5509 «implement-windows-supportˇ»
5510 "});
5511 cx.update_editor(|e, window, cx| {
5512 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5513 });
5514 cx.assert_editor_state(indoc! {"
5515 «Implement windows supportˇ»
5516 "});
5517}
5518
5519#[gpui::test]
5520async fn test_manipulate_text(cx: &mut TestAppContext) {
5521 init_test(cx, |_| {});
5522
5523 let mut cx = EditorTestContext::new(cx).await;
5524
5525 // Test convert_to_upper_case()
5526 cx.set_state(indoc! {"
5527 «hello worldˇ»
5528 "});
5529 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5530 cx.assert_editor_state(indoc! {"
5531 «HELLO WORLDˇ»
5532 "});
5533
5534 // Test convert_to_lower_case()
5535 cx.set_state(indoc! {"
5536 «HELLO WORLDˇ»
5537 "});
5538 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5539 cx.assert_editor_state(indoc! {"
5540 «hello worldˇ»
5541 "});
5542
5543 // Test multiple line, single selection case
5544 cx.set_state(indoc! {"
5545 «The quick brown
5546 fox jumps over
5547 the lazy dogˇ»
5548 "});
5549 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5550 cx.assert_editor_state(indoc! {"
5551 «The Quick Brown
5552 Fox Jumps Over
5553 The Lazy Dogˇ»
5554 "});
5555
5556 // Test multiple line, single selection case
5557 cx.set_state(indoc! {"
5558 «The quick brown
5559 fox jumps over
5560 the lazy dogˇ»
5561 "});
5562 cx.update_editor(|e, window, cx| {
5563 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5564 });
5565 cx.assert_editor_state(indoc! {"
5566 «TheQuickBrown
5567 FoxJumpsOver
5568 TheLazyDogˇ»
5569 "});
5570
5571 // From here on out, test more complex cases of manipulate_text()
5572
5573 // Test no selection case - should affect words cursors are in
5574 // Cursor at beginning, middle, and end of word
5575 cx.set_state(indoc! {"
5576 ˇhello big beauˇtiful worldˇ
5577 "});
5578 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5579 cx.assert_editor_state(indoc! {"
5580 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5581 "});
5582
5583 // Test multiple selections on a single line and across multiple lines
5584 cx.set_state(indoc! {"
5585 «Theˇ» quick «brown
5586 foxˇ» jumps «overˇ»
5587 the «lazyˇ» dog
5588 "});
5589 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5590 cx.assert_editor_state(indoc! {"
5591 «THEˇ» quick «BROWN
5592 FOXˇ» jumps «OVERˇ»
5593 the «LAZYˇ» dog
5594 "});
5595
5596 // Test case where text length grows
5597 cx.set_state(indoc! {"
5598 «tschüߡ»
5599 "});
5600 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5601 cx.assert_editor_state(indoc! {"
5602 «TSCHÜSSˇ»
5603 "});
5604
5605 // Test to make sure we don't crash when text shrinks
5606 cx.set_state(indoc! {"
5607 aaa_bbbˇ
5608 "});
5609 cx.update_editor(|e, window, cx| {
5610 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5611 });
5612 cx.assert_editor_state(indoc! {"
5613 «aaaBbbˇ»
5614 "});
5615
5616 // Test to make sure we all aware of the fact that each word can grow and shrink
5617 // Final selections should be aware of this fact
5618 cx.set_state(indoc! {"
5619 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5620 "});
5621 cx.update_editor(|e, window, cx| {
5622 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5623 });
5624 cx.assert_editor_state(indoc! {"
5625 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5626 "});
5627
5628 cx.set_state(indoc! {"
5629 «hElLo, WoRld!ˇ»
5630 "});
5631 cx.update_editor(|e, window, cx| {
5632 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5633 });
5634 cx.assert_editor_state(indoc! {"
5635 «HeLlO, wOrLD!ˇ»
5636 "});
5637
5638 // Test selections with `line_mode() = true`.
5639 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5640 cx.set_state(indoc! {"
5641 «The quick brown
5642 fox jumps over
5643 tˇ»he lazy dog
5644 "});
5645 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5646 cx.assert_editor_state(indoc! {"
5647 «THE QUICK BROWN
5648 FOX JUMPS OVER
5649 THE LAZY DOGˇ»
5650 "});
5651}
5652
5653#[gpui::test]
5654fn test_duplicate_line(cx: &mut TestAppContext) {
5655 init_test(cx, |_| {});
5656
5657 let editor = cx.add_window(|window, cx| {
5658 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5659 build_editor(buffer, window, cx)
5660 });
5661 _ = editor.update(cx, |editor, window, cx| {
5662 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5663 s.select_display_ranges([
5664 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5665 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5666 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5667 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5668 ])
5669 });
5670 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5671 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5672 assert_eq!(
5673 display_ranges(editor, cx),
5674 vec![
5675 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5676 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5677 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5678 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5679 ]
5680 );
5681 });
5682
5683 let editor = cx.add_window(|window, cx| {
5684 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5685 build_editor(buffer, window, cx)
5686 });
5687 _ = editor.update(cx, |editor, window, cx| {
5688 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5689 s.select_display_ranges([
5690 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5691 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5692 ])
5693 });
5694 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5695 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5696 assert_eq!(
5697 display_ranges(editor, cx),
5698 vec![
5699 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5700 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5701 ]
5702 );
5703 });
5704
5705 // With `duplicate_line_up` the selections move to the duplicated lines,
5706 // which are inserted above the original lines
5707 let editor = cx.add_window(|window, cx| {
5708 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5709 build_editor(buffer, window, cx)
5710 });
5711 _ = editor.update(cx, |editor, window, cx| {
5712 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5713 s.select_display_ranges([
5714 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5715 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5716 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5717 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5718 ])
5719 });
5720 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5721 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5722 assert_eq!(
5723 display_ranges(editor, cx),
5724 vec![
5725 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5726 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5727 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5728 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5729 ]
5730 );
5731 });
5732
5733 let editor = cx.add_window(|window, cx| {
5734 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5735 build_editor(buffer, window, cx)
5736 });
5737 _ = editor.update(cx, |editor, window, cx| {
5738 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5739 s.select_display_ranges([
5740 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5741 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5742 ])
5743 });
5744 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5745 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5746 assert_eq!(
5747 display_ranges(editor, cx),
5748 vec![
5749 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5750 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5751 ]
5752 );
5753 });
5754
5755 let editor = cx.add_window(|window, cx| {
5756 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5757 build_editor(buffer, window, cx)
5758 });
5759 _ = editor.update(cx, |editor, window, cx| {
5760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5761 s.select_display_ranges([
5762 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5763 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5764 ])
5765 });
5766 editor.duplicate_selection(&DuplicateSelection, window, cx);
5767 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5768 assert_eq!(
5769 display_ranges(editor, cx),
5770 vec![
5771 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5772 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5773 ]
5774 );
5775 });
5776}
5777
5778#[gpui::test]
5779fn test_move_line_up_down(cx: &mut TestAppContext) {
5780 init_test(cx, |_| {});
5781
5782 let editor = cx.add_window(|window, cx| {
5783 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5784 build_editor(buffer, window, cx)
5785 });
5786 _ = editor.update(cx, |editor, window, cx| {
5787 editor.fold_creases(
5788 vec![
5789 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5790 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5791 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5792 ],
5793 true,
5794 window,
5795 cx,
5796 );
5797 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5798 s.select_display_ranges([
5799 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5800 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5801 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5802 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5803 ])
5804 });
5805 assert_eq!(
5806 editor.display_text(cx),
5807 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5808 );
5809
5810 editor.move_line_up(&MoveLineUp, window, cx);
5811 assert_eq!(
5812 editor.display_text(cx),
5813 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5814 );
5815 assert_eq!(
5816 display_ranges(editor, cx),
5817 vec![
5818 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5819 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5820 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5821 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5822 ]
5823 );
5824 });
5825
5826 _ = editor.update(cx, |editor, window, cx| {
5827 editor.move_line_down(&MoveLineDown, window, cx);
5828 assert_eq!(
5829 editor.display_text(cx),
5830 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5831 );
5832 assert_eq!(
5833 display_ranges(editor, cx),
5834 vec![
5835 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5836 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5837 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5838 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5839 ]
5840 );
5841 });
5842
5843 _ = editor.update(cx, |editor, window, cx| {
5844 editor.move_line_down(&MoveLineDown, window, cx);
5845 assert_eq!(
5846 editor.display_text(cx),
5847 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5848 );
5849 assert_eq!(
5850 display_ranges(editor, cx),
5851 vec![
5852 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5853 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5854 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5855 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5856 ]
5857 );
5858 });
5859
5860 _ = editor.update(cx, |editor, window, cx| {
5861 editor.move_line_up(&MoveLineUp, window, cx);
5862 assert_eq!(
5863 editor.display_text(cx),
5864 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5865 );
5866 assert_eq!(
5867 display_ranges(editor, cx),
5868 vec![
5869 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5870 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5871 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5872 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5873 ]
5874 );
5875 });
5876}
5877
5878#[gpui::test]
5879fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5880 init_test(cx, |_| {});
5881 let editor = cx.add_window(|window, cx| {
5882 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5883 build_editor(buffer, window, cx)
5884 });
5885 _ = editor.update(cx, |editor, window, cx| {
5886 editor.fold_creases(
5887 vec![Crease::simple(
5888 Point::new(6, 4)..Point::new(7, 4),
5889 FoldPlaceholder::test(),
5890 )],
5891 true,
5892 window,
5893 cx,
5894 );
5895 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5896 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5897 });
5898 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5899 editor.move_line_up(&MoveLineUp, window, cx);
5900 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5901 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5902 });
5903}
5904
5905#[gpui::test]
5906fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5907 init_test(cx, |_| {});
5908
5909 let editor = cx.add_window(|window, cx| {
5910 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5911 build_editor(buffer, window, cx)
5912 });
5913 _ = editor.update(cx, |editor, window, cx| {
5914 let snapshot = editor.buffer.read(cx).snapshot(cx);
5915 editor.insert_blocks(
5916 [BlockProperties {
5917 style: BlockStyle::Fixed,
5918 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5919 height: Some(1),
5920 render: Arc::new(|_| div().into_any()),
5921 priority: 0,
5922 }],
5923 Some(Autoscroll::fit()),
5924 cx,
5925 );
5926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5927 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5928 });
5929 editor.move_line_down(&MoveLineDown, window, cx);
5930 });
5931}
5932
5933#[gpui::test]
5934async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5935 init_test(cx, |_| {});
5936
5937 let mut cx = EditorTestContext::new(cx).await;
5938 cx.set_state(
5939 &"
5940 ˇzero
5941 one
5942 two
5943 three
5944 four
5945 five
5946 "
5947 .unindent(),
5948 );
5949
5950 // Create a four-line block that replaces three lines of text.
5951 cx.update_editor(|editor, window, cx| {
5952 let snapshot = editor.snapshot(window, cx);
5953 let snapshot = &snapshot.buffer_snapshot();
5954 let placement = BlockPlacement::Replace(
5955 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5956 );
5957 editor.insert_blocks(
5958 [BlockProperties {
5959 placement,
5960 height: Some(4),
5961 style: BlockStyle::Sticky,
5962 render: Arc::new(|_| gpui::div().into_any_element()),
5963 priority: 0,
5964 }],
5965 None,
5966 cx,
5967 );
5968 });
5969
5970 // Move down so that the cursor touches the block.
5971 cx.update_editor(|editor, window, cx| {
5972 editor.move_down(&Default::default(), window, cx);
5973 });
5974 cx.assert_editor_state(
5975 &"
5976 zero
5977 «one
5978 two
5979 threeˇ»
5980 four
5981 five
5982 "
5983 .unindent(),
5984 );
5985
5986 // Move down past the block.
5987 cx.update_editor(|editor, window, cx| {
5988 editor.move_down(&Default::default(), window, cx);
5989 });
5990 cx.assert_editor_state(
5991 &"
5992 zero
5993 one
5994 two
5995 three
5996 ˇfour
5997 five
5998 "
5999 .unindent(),
6000 );
6001}
6002
6003#[gpui::test]
6004fn test_transpose(cx: &mut TestAppContext) {
6005 init_test(cx, |_| {});
6006
6007 _ = cx.add_window(|window, cx| {
6008 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6009 editor.set_style(EditorStyle::default(), window, cx);
6010 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6011 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6012 });
6013 editor.transpose(&Default::default(), window, cx);
6014 assert_eq!(editor.text(cx), "bac");
6015 assert_eq!(
6016 editor.selections.ranges(&editor.display_snapshot(cx)),
6017 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6018 );
6019
6020 editor.transpose(&Default::default(), window, cx);
6021 assert_eq!(editor.text(cx), "bca");
6022 assert_eq!(
6023 editor.selections.ranges(&editor.display_snapshot(cx)),
6024 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6025 );
6026
6027 editor.transpose(&Default::default(), window, cx);
6028 assert_eq!(editor.text(cx), "bac");
6029 assert_eq!(
6030 editor.selections.ranges(&editor.display_snapshot(cx)),
6031 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6032 );
6033
6034 editor
6035 });
6036
6037 _ = cx.add_window(|window, cx| {
6038 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6039 editor.set_style(EditorStyle::default(), window, cx);
6040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6041 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6042 });
6043 editor.transpose(&Default::default(), window, cx);
6044 assert_eq!(editor.text(cx), "acb\nde");
6045 assert_eq!(
6046 editor.selections.ranges(&editor.display_snapshot(cx)),
6047 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6048 );
6049
6050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6051 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6052 });
6053 editor.transpose(&Default::default(), window, cx);
6054 assert_eq!(editor.text(cx), "acbd\ne");
6055 assert_eq!(
6056 editor.selections.ranges(&editor.display_snapshot(cx)),
6057 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6058 );
6059
6060 editor.transpose(&Default::default(), window, cx);
6061 assert_eq!(editor.text(cx), "acbde\n");
6062 assert_eq!(
6063 editor.selections.ranges(&editor.display_snapshot(cx)),
6064 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6065 );
6066
6067 editor.transpose(&Default::default(), window, cx);
6068 assert_eq!(editor.text(cx), "acbd\ne");
6069 assert_eq!(
6070 editor.selections.ranges(&editor.display_snapshot(cx)),
6071 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6072 );
6073
6074 editor
6075 });
6076
6077 _ = cx.add_window(|window, cx| {
6078 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6079 editor.set_style(EditorStyle::default(), window, cx);
6080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6081 s.select_ranges([
6082 MultiBufferOffset(1)..MultiBufferOffset(1),
6083 MultiBufferOffset(2)..MultiBufferOffset(2),
6084 MultiBufferOffset(4)..MultiBufferOffset(4),
6085 ])
6086 });
6087 editor.transpose(&Default::default(), window, cx);
6088 assert_eq!(editor.text(cx), "bacd\ne");
6089 assert_eq!(
6090 editor.selections.ranges(&editor.display_snapshot(cx)),
6091 [
6092 MultiBufferOffset(2)..MultiBufferOffset(2),
6093 MultiBufferOffset(3)..MultiBufferOffset(3),
6094 MultiBufferOffset(5)..MultiBufferOffset(5)
6095 ]
6096 );
6097
6098 editor.transpose(&Default::default(), window, cx);
6099 assert_eq!(editor.text(cx), "bcade\n");
6100 assert_eq!(
6101 editor.selections.ranges(&editor.display_snapshot(cx)),
6102 [
6103 MultiBufferOffset(3)..MultiBufferOffset(3),
6104 MultiBufferOffset(4)..MultiBufferOffset(4),
6105 MultiBufferOffset(6)..MultiBufferOffset(6)
6106 ]
6107 );
6108
6109 editor.transpose(&Default::default(), window, cx);
6110 assert_eq!(editor.text(cx), "bcda\ne");
6111 assert_eq!(
6112 editor.selections.ranges(&editor.display_snapshot(cx)),
6113 [
6114 MultiBufferOffset(4)..MultiBufferOffset(4),
6115 MultiBufferOffset(6)..MultiBufferOffset(6)
6116 ]
6117 );
6118
6119 editor.transpose(&Default::default(), window, cx);
6120 assert_eq!(editor.text(cx), "bcade\n");
6121 assert_eq!(
6122 editor.selections.ranges(&editor.display_snapshot(cx)),
6123 [
6124 MultiBufferOffset(4)..MultiBufferOffset(4),
6125 MultiBufferOffset(6)..MultiBufferOffset(6)
6126 ]
6127 );
6128
6129 editor.transpose(&Default::default(), window, cx);
6130 assert_eq!(editor.text(cx), "bcaed\n");
6131 assert_eq!(
6132 editor.selections.ranges(&editor.display_snapshot(cx)),
6133 [
6134 MultiBufferOffset(5)..MultiBufferOffset(5),
6135 MultiBufferOffset(6)..MultiBufferOffset(6)
6136 ]
6137 );
6138
6139 editor
6140 });
6141
6142 _ = cx.add_window(|window, cx| {
6143 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6144 editor.set_style(EditorStyle::default(), window, cx);
6145 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6146 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6147 });
6148 editor.transpose(&Default::default(), window, cx);
6149 assert_eq!(editor.text(cx), "🏀🍐✋");
6150 assert_eq!(
6151 editor.selections.ranges(&editor.display_snapshot(cx)),
6152 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6153 );
6154
6155 editor.transpose(&Default::default(), window, cx);
6156 assert_eq!(editor.text(cx), "🏀✋🍐");
6157 assert_eq!(
6158 editor.selections.ranges(&editor.display_snapshot(cx)),
6159 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6160 );
6161
6162 editor.transpose(&Default::default(), window, cx);
6163 assert_eq!(editor.text(cx), "🏀🍐✋");
6164 assert_eq!(
6165 editor.selections.ranges(&editor.display_snapshot(cx)),
6166 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6167 );
6168
6169 editor
6170 });
6171}
6172
6173#[gpui::test]
6174async fn test_rewrap(cx: &mut TestAppContext) {
6175 init_test(cx, |settings| {
6176 settings.languages.0.extend([
6177 (
6178 "Markdown".into(),
6179 LanguageSettingsContent {
6180 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6181 preferred_line_length: Some(40),
6182 ..Default::default()
6183 },
6184 ),
6185 (
6186 "Plain Text".into(),
6187 LanguageSettingsContent {
6188 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6189 preferred_line_length: Some(40),
6190 ..Default::default()
6191 },
6192 ),
6193 (
6194 "C++".into(),
6195 LanguageSettingsContent {
6196 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6197 preferred_line_length: Some(40),
6198 ..Default::default()
6199 },
6200 ),
6201 (
6202 "Python".into(),
6203 LanguageSettingsContent {
6204 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6205 preferred_line_length: Some(40),
6206 ..Default::default()
6207 },
6208 ),
6209 (
6210 "Rust".into(),
6211 LanguageSettingsContent {
6212 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6213 preferred_line_length: Some(40),
6214 ..Default::default()
6215 },
6216 ),
6217 ])
6218 });
6219
6220 let mut cx = EditorTestContext::new(cx).await;
6221
6222 let cpp_language = Arc::new(Language::new(
6223 LanguageConfig {
6224 name: "C++".into(),
6225 line_comments: vec!["// ".into()],
6226 ..LanguageConfig::default()
6227 },
6228 None,
6229 ));
6230 let python_language = Arc::new(Language::new(
6231 LanguageConfig {
6232 name: "Python".into(),
6233 line_comments: vec!["# ".into()],
6234 ..LanguageConfig::default()
6235 },
6236 None,
6237 ));
6238 let markdown_language = Arc::new(Language::new(
6239 LanguageConfig {
6240 name: "Markdown".into(),
6241 rewrap_prefixes: vec![
6242 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6243 regex::Regex::new("[-*+]\\s+").unwrap(),
6244 ],
6245 ..LanguageConfig::default()
6246 },
6247 None,
6248 ));
6249 let rust_language = Arc::new(
6250 Language::new(
6251 LanguageConfig {
6252 name: "Rust".into(),
6253 line_comments: vec!["// ".into(), "/// ".into()],
6254 ..LanguageConfig::default()
6255 },
6256 Some(tree_sitter_rust::LANGUAGE.into()),
6257 )
6258 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6259 .unwrap(),
6260 );
6261
6262 let plaintext_language = Arc::new(Language::new(
6263 LanguageConfig {
6264 name: "Plain Text".into(),
6265 ..LanguageConfig::default()
6266 },
6267 None,
6268 ));
6269
6270 // Test basic rewrapping of a long line with a cursor
6271 assert_rewrap(
6272 indoc! {"
6273 // ˇThis is a long comment that needs to be wrapped.
6274 "},
6275 indoc! {"
6276 // ˇThis is a long comment that needs to
6277 // be wrapped.
6278 "},
6279 cpp_language.clone(),
6280 &mut cx,
6281 );
6282
6283 // Test rewrapping a full selection
6284 assert_rewrap(
6285 indoc! {"
6286 «// This selected long comment needs to be wrapped.ˇ»"
6287 },
6288 indoc! {"
6289 «// This selected long comment needs to
6290 // be wrapped.ˇ»"
6291 },
6292 cpp_language.clone(),
6293 &mut cx,
6294 );
6295
6296 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6297 assert_rewrap(
6298 indoc! {"
6299 // ˇThis is the first line.
6300 // Thisˇ is the second line.
6301 // This is the thirdˇ line, all part of one paragraph.
6302 "},
6303 indoc! {"
6304 // ˇThis is the first line. Thisˇ is the
6305 // second line. This is the thirdˇ line,
6306 // all part of one paragraph.
6307 "},
6308 cpp_language.clone(),
6309 &mut cx,
6310 );
6311
6312 // Test multiple cursors in different paragraphs trigger separate rewraps
6313 assert_rewrap(
6314 indoc! {"
6315 // ˇThis is the first paragraph, first line.
6316 // ˇThis is the first paragraph, second line.
6317
6318 // ˇThis is the second paragraph, first line.
6319 // ˇThis is the second paragraph, second line.
6320 "},
6321 indoc! {"
6322 // ˇThis is the first paragraph, first
6323 // line. ˇThis is the first paragraph,
6324 // second line.
6325
6326 // ˇThis is the second paragraph, first
6327 // line. ˇThis is the second paragraph,
6328 // second line.
6329 "},
6330 cpp_language.clone(),
6331 &mut cx,
6332 );
6333
6334 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6335 assert_rewrap(
6336 indoc! {"
6337 «// A regular long long comment to be wrapped.
6338 /// A documentation long comment to be wrapped.ˇ»
6339 "},
6340 indoc! {"
6341 «// A regular long long comment to be
6342 // wrapped.
6343 /// A documentation long comment to be
6344 /// wrapped.ˇ»
6345 "},
6346 rust_language.clone(),
6347 &mut cx,
6348 );
6349
6350 // Test that change in indentation level trigger seperate rewraps
6351 assert_rewrap(
6352 indoc! {"
6353 fn foo() {
6354 «// This is a long comment at the base indent.
6355 // This is a long comment at the next indent.ˇ»
6356 }
6357 "},
6358 indoc! {"
6359 fn foo() {
6360 «// This is a long comment at the
6361 // base indent.
6362 // This is a long comment at the
6363 // next indent.ˇ»
6364 }
6365 "},
6366 rust_language.clone(),
6367 &mut cx,
6368 );
6369
6370 // Test that different comment prefix characters (e.g., '#') are handled correctly
6371 assert_rewrap(
6372 indoc! {"
6373 # ˇThis is a long comment using a pound sign.
6374 "},
6375 indoc! {"
6376 # ˇThis is a long comment using a pound
6377 # sign.
6378 "},
6379 python_language,
6380 &mut cx,
6381 );
6382
6383 // Test rewrapping only affects comments, not code even when selected
6384 assert_rewrap(
6385 indoc! {"
6386 «/// This doc comment is long and should be wrapped.
6387 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6388 "},
6389 indoc! {"
6390 «/// This doc comment is long and should
6391 /// be wrapped.
6392 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6393 "},
6394 rust_language.clone(),
6395 &mut cx,
6396 );
6397
6398 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6399 assert_rewrap(
6400 indoc! {"
6401 # Header
6402
6403 A long long long line of markdown text to wrap.ˇ
6404 "},
6405 indoc! {"
6406 # Header
6407
6408 A long long long line of markdown text
6409 to wrap.ˇ
6410 "},
6411 markdown_language.clone(),
6412 &mut cx,
6413 );
6414
6415 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6416 assert_rewrap(
6417 indoc! {"
6418 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6419 2. This is a numbered list item that is very long and needs to be wrapped properly.
6420 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6421 "},
6422 indoc! {"
6423 «1. This is a numbered list item that is
6424 very long and needs to be wrapped
6425 properly.
6426 2. This is a numbered list item that is
6427 very long and needs to be wrapped
6428 properly.
6429 - This is an unordered list item that is
6430 also very long and should not merge
6431 with the numbered item.ˇ»
6432 "},
6433 markdown_language.clone(),
6434 &mut cx,
6435 );
6436
6437 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6438 assert_rewrap(
6439 indoc! {"
6440 «1. This is a numbered list item that is
6441 very long and needs to be wrapped
6442 properly.
6443 2. This is a numbered list item that is
6444 very long and needs to be wrapped
6445 properly.
6446 - This is an unordered list item that is
6447 also very long and should not merge with
6448 the numbered item.ˇ»
6449 "},
6450 indoc! {"
6451 «1. This is a numbered list item that is
6452 very long and needs to be wrapped
6453 properly.
6454 2. This is a numbered list item that is
6455 very long and needs to be wrapped
6456 properly.
6457 - This is an unordered list item that is
6458 also very long and should not merge
6459 with the numbered item.ˇ»
6460 "},
6461 markdown_language.clone(),
6462 &mut cx,
6463 );
6464
6465 // Test that rewrapping maintain indents even when they already exists.
6466 assert_rewrap(
6467 indoc! {"
6468 «1. This is a numbered list
6469 item that is very long and needs to be wrapped properly.
6470 2. This is a numbered list
6471 item that is very long and needs to be wrapped properly.
6472 - This is an unordered list item that is also very long and
6473 should not merge with the numbered item.ˇ»
6474 "},
6475 indoc! {"
6476 «1. This is a numbered list item that is
6477 very long and needs to be wrapped
6478 properly.
6479 2. This is a numbered list item that is
6480 very long and needs to be wrapped
6481 properly.
6482 - This is an unordered list item that is
6483 also very long and should not merge
6484 with the numbered item.ˇ»
6485 "},
6486 markdown_language,
6487 &mut cx,
6488 );
6489
6490 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6491 assert_rewrap(
6492 indoc! {"
6493 ˇThis is a very long line of plain text that will be wrapped.
6494 "},
6495 indoc! {"
6496 ˇThis is a very long line of plain text
6497 that will be wrapped.
6498 "},
6499 plaintext_language.clone(),
6500 &mut cx,
6501 );
6502
6503 // Test that non-commented code acts as a paragraph boundary within a selection
6504 assert_rewrap(
6505 indoc! {"
6506 «// This is the first long comment block to be wrapped.
6507 fn my_func(a: u32);
6508 // This is the second long comment block to be wrapped.ˇ»
6509 "},
6510 indoc! {"
6511 «// This is the first long comment block
6512 // to be wrapped.
6513 fn my_func(a: u32);
6514 // This is the second long comment block
6515 // to be wrapped.ˇ»
6516 "},
6517 rust_language,
6518 &mut cx,
6519 );
6520
6521 // Test rewrapping multiple selections, including ones with blank lines or tabs
6522 assert_rewrap(
6523 indoc! {"
6524 «ˇThis is a very long line that will be wrapped.
6525
6526 This is another paragraph in the same selection.»
6527
6528 «\tThis is a very long indented line that will be wrapped.ˇ»
6529 "},
6530 indoc! {"
6531 «ˇThis is a very long line that will be
6532 wrapped.
6533
6534 This is another paragraph in the same
6535 selection.»
6536
6537 «\tThis is a very long indented line
6538 \tthat will be wrapped.ˇ»
6539 "},
6540 plaintext_language,
6541 &mut cx,
6542 );
6543
6544 // Test that an empty comment line acts as a paragraph boundary
6545 assert_rewrap(
6546 indoc! {"
6547 // ˇThis is a long comment that will be wrapped.
6548 //
6549 // And this is another long comment that will also be wrapped.ˇ
6550 "},
6551 indoc! {"
6552 // ˇThis is a long comment that will be
6553 // wrapped.
6554 //
6555 // And this is another long comment that
6556 // will also be wrapped.ˇ
6557 "},
6558 cpp_language,
6559 &mut cx,
6560 );
6561
6562 #[track_caller]
6563 fn assert_rewrap(
6564 unwrapped_text: &str,
6565 wrapped_text: &str,
6566 language: Arc<Language>,
6567 cx: &mut EditorTestContext,
6568 ) {
6569 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6570 cx.set_state(unwrapped_text);
6571 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6572 cx.assert_editor_state(wrapped_text);
6573 }
6574}
6575
6576#[gpui::test]
6577async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6578 init_test(cx, |settings| {
6579 settings.languages.0.extend([(
6580 "Rust".into(),
6581 LanguageSettingsContent {
6582 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6583 preferred_line_length: Some(40),
6584 ..Default::default()
6585 },
6586 )])
6587 });
6588
6589 let mut cx = EditorTestContext::new(cx).await;
6590
6591 let rust_lang = Arc::new(
6592 Language::new(
6593 LanguageConfig {
6594 name: "Rust".into(),
6595 line_comments: vec!["// ".into()],
6596 block_comment: Some(BlockCommentConfig {
6597 start: "/*".into(),
6598 end: "*/".into(),
6599 prefix: "* ".into(),
6600 tab_size: 1,
6601 }),
6602 documentation_comment: Some(BlockCommentConfig {
6603 start: "/**".into(),
6604 end: "*/".into(),
6605 prefix: "* ".into(),
6606 tab_size: 1,
6607 }),
6608
6609 ..LanguageConfig::default()
6610 },
6611 Some(tree_sitter_rust::LANGUAGE.into()),
6612 )
6613 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6614 .unwrap(),
6615 );
6616
6617 // regular block comment
6618 assert_rewrap(
6619 indoc! {"
6620 /*
6621 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6622 */
6623 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6624 "},
6625 indoc! {"
6626 /*
6627 *ˇ Lorem ipsum dolor sit amet,
6628 * consectetur adipiscing elit.
6629 */
6630 /*
6631 *ˇ Lorem ipsum dolor sit amet,
6632 * consectetur adipiscing elit.
6633 */
6634 "},
6635 rust_lang.clone(),
6636 &mut cx,
6637 );
6638
6639 // indent is respected
6640 assert_rewrap(
6641 indoc! {"
6642 {}
6643 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6644 "},
6645 indoc! {"
6646 {}
6647 /*
6648 *ˇ Lorem ipsum dolor sit amet,
6649 * consectetur adipiscing elit.
6650 */
6651 "},
6652 rust_lang.clone(),
6653 &mut cx,
6654 );
6655
6656 // short block comments with inline delimiters
6657 assert_rewrap(
6658 indoc! {"
6659 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6660 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6661 */
6662 /*
6663 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6664 "},
6665 indoc! {"
6666 /*
6667 *ˇ Lorem ipsum dolor sit amet,
6668 * consectetur adipiscing elit.
6669 */
6670 /*
6671 *ˇ Lorem ipsum dolor sit amet,
6672 * consectetur adipiscing elit.
6673 */
6674 /*
6675 *ˇ Lorem ipsum dolor sit amet,
6676 * consectetur adipiscing elit.
6677 */
6678 "},
6679 rust_lang.clone(),
6680 &mut cx,
6681 );
6682
6683 // multiline block comment with inline start/end delimiters
6684 assert_rewrap(
6685 indoc! {"
6686 /*ˇ Lorem ipsum dolor sit amet,
6687 * consectetur adipiscing elit. */
6688 "},
6689 indoc! {"
6690 /*
6691 *ˇ Lorem ipsum dolor sit amet,
6692 * consectetur adipiscing elit.
6693 */
6694 "},
6695 rust_lang.clone(),
6696 &mut cx,
6697 );
6698
6699 // block comment rewrap still respects paragraph bounds
6700 assert_rewrap(
6701 indoc! {"
6702 /*
6703 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6704 *
6705 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6706 */
6707 "},
6708 indoc! {"
6709 /*
6710 *ˇ Lorem ipsum dolor sit amet,
6711 * consectetur adipiscing elit.
6712 *
6713 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6714 */
6715 "},
6716 rust_lang.clone(),
6717 &mut cx,
6718 );
6719
6720 // documentation comments
6721 assert_rewrap(
6722 indoc! {"
6723 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6724 /**
6725 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6726 */
6727 "},
6728 indoc! {"
6729 /**
6730 *ˇ Lorem ipsum dolor sit amet,
6731 * consectetur adipiscing elit.
6732 */
6733 /**
6734 *ˇ Lorem ipsum dolor sit amet,
6735 * consectetur adipiscing elit.
6736 */
6737 "},
6738 rust_lang.clone(),
6739 &mut cx,
6740 );
6741
6742 // different, adjacent comments
6743 assert_rewrap(
6744 indoc! {"
6745 /**
6746 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6747 */
6748 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6749 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6750 "},
6751 indoc! {"
6752 /**
6753 *ˇ Lorem ipsum dolor sit amet,
6754 * consectetur adipiscing elit.
6755 */
6756 /*
6757 *ˇ Lorem ipsum dolor sit amet,
6758 * consectetur adipiscing elit.
6759 */
6760 //ˇ Lorem ipsum dolor sit amet,
6761 // consectetur adipiscing elit.
6762 "},
6763 rust_lang.clone(),
6764 &mut cx,
6765 );
6766
6767 // selection w/ single short block comment
6768 assert_rewrap(
6769 indoc! {"
6770 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6771 "},
6772 indoc! {"
6773 «/*
6774 * Lorem ipsum dolor sit amet,
6775 * consectetur adipiscing elit.
6776 */ˇ»
6777 "},
6778 rust_lang.clone(),
6779 &mut cx,
6780 );
6781
6782 // rewrapping a single comment w/ abutting comments
6783 assert_rewrap(
6784 indoc! {"
6785 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6786 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6787 "},
6788 indoc! {"
6789 /*
6790 * ˇLorem ipsum dolor sit amet,
6791 * consectetur adipiscing elit.
6792 */
6793 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6794 "},
6795 rust_lang.clone(),
6796 &mut cx,
6797 );
6798
6799 // selection w/ non-abutting short block comments
6800 assert_rewrap(
6801 indoc! {"
6802 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6803
6804 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6805 "},
6806 indoc! {"
6807 «/*
6808 * Lorem ipsum dolor sit amet,
6809 * consectetur adipiscing elit.
6810 */
6811
6812 /*
6813 * Lorem ipsum dolor sit amet,
6814 * consectetur adipiscing elit.
6815 */ˇ»
6816 "},
6817 rust_lang.clone(),
6818 &mut cx,
6819 );
6820
6821 // selection of multiline block comments
6822 assert_rewrap(
6823 indoc! {"
6824 «/* Lorem ipsum dolor sit amet,
6825 * consectetur adipiscing elit. */ˇ»
6826 "},
6827 indoc! {"
6828 «/*
6829 * Lorem ipsum dolor sit amet,
6830 * consectetur adipiscing elit.
6831 */ˇ»
6832 "},
6833 rust_lang.clone(),
6834 &mut cx,
6835 );
6836
6837 // partial selection of multiline block comments
6838 assert_rewrap(
6839 indoc! {"
6840 «/* Lorem ipsum dolor sit amet,ˇ»
6841 * consectetur adipiscing elit. */
6842 /* Lorem ipsum dolor sit amet,
6843 «* consectetur adipiscing elit. */ˇ»
6844 "},
6845 indoc! {"
6846 «/*
6847 * Lorem ipsum dolor sit amet,ˇ»
6848 * consectetur adipiscing elit. */
6849 /* Lorem ipsum dolor sit amet,
6850 «* consectetur adipiscing elit.
6851 */ˇ»
6852 "},
6853 rust_lang.clone(),
6854 &mut cx,
6855 );
6856
6857 // selection w/ abutting short block comments
6858 // TODO: should not be combined; should rewrap as 2 comments
6859 assert_rewrap(
6860 indoc! {"
6861 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6862 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6863 "},
6864 // desired behavior:
6865 // indoc! {"
6866 // «/*
6867 // * Lorem ipsum dolor sit amet,
6868 // * consectetur adipiscing elit.
6869 // */
6870 // /*
6871 // * Lorem ipsum dolor sit amet,
6872 // * consectetur adipiscing elit.
6873 // */ˇ»
6874 // "},
6875 // actual behaviour:
6876 indoc! {"
6877 «/*
6878 * Lorem ipsum dolor sit amet,
6879 * consectetur adipiscing elit. Lorem
6880 * ipsum dolor sit amet, consectetur
6881 * adipiscing elit.
6882 */ˇ»
6883 "},
6884 rust_lang.clone(),
6885 &mut cx,
6886 );
6887
6888 // TODO: same as above, but with delimiters on separate line
6889 // assert_rewrap(
6890 // indoc! {"
6891 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6892 // */
6893 // /*
6894 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6895 // "},
6896 // // desired:
6897 // // indoc! {"
6898 // // «/*
6899 // // * Lorem ipsum dolor sit amet,
6900 // // * consectetur adipiscing elit.
6901 // // */
6902 // // /*
6903 // // * Lorem ipsum dolor sit amet,
6904 // // * consectetur adipiscing elit.
6905 // // */ˇ»
6906 // // "},
6907 // // actual: (but with trailing w/s on the empty lines)
6908 // indoc! {"
6909 // «/*
6910 // * Lorem ipsum dolor sit amet,
6911 // * consectetur adipiscing elit.
6912 // *
6913 // */
6914 // /*
6915 // *
6916 // * Lorem ipsum dolor sit amet,
6917 // * consectetur adipiscing elit.
6918 // */ˇ»
6919 // "},
6920 // rust_lang.clone(),
6921 // &mut cx,
6922 // );
6923
6924 // TODO these are unhandled edge cases; not correct, just documenting known issues
6925 assert_rewrap(
6926 indoc! {"
6927 /*
6928 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6929 */
6930 /*
6931 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6932 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6933 "},
6934 // desired:
6935 // indoc! {"
6936 // /*
6937 // *ˇ Lorem ipsum dolor sit amet,
6938 // * consectetur adipiscing elit.
6939 // */
6940 // /*
6941 // *ˇ Lorem ipsum dolor sit amet,
6942 // * consectetur adipiscing elit.
6943 // */
6944 // /*
6945 // *ˇ Lorem ipsum dolor sit amet
6946 // */ /* consectetur adipiscing elit. */
6947 // "},
6948 // actual:
6949 indoc! {"
6950 /*
6951 //ˇ Lorem ipsum dolor sit amet,
6952 // consectetur adipiscing elit.
6953 */
6954 /*
6955 * //ˇ Lorem ipsum dolor sit amet,
6956 * consectetur adipiscing elit.
6957 */
6958 /*
6959 *ˇ Lorem ipsum dolor sit amet */ /*
6960 * consectetur adipiscing elit.
6961 */
6962 "},
6963 rust_lang,
6964 &mut cx,
6965 );
6966
6967 #[track_caller]
6968 fn assert_rewrap(
6969 unwrapped_text: &str,
6970 wrapped_text: &str,
6971 language: Arc<Language>,
6972 cx: &mut EditorTestContext,
6973 ) {
6974 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6975 cx.set_state(unwrapped_text);
6976 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6977 cx.assert_editor_state(wrapped_text);
6978 }
6979}
6980
6981#[gpui::test]
6982async fn test_hard_wrap(cx: &mut TestAppContext) {
6983 init_test(cx, |_| {});
6984 let mut cx = EditorTestContext::new(cx).await;
6985
6986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6987 cx.update_editor(|editor, _, cx| {
6988 editor.set_hard_wrap(Some(14), cx);
6989 });
6990
6991 cx.set_state(indoc!(
6992 "
6993 one two three ˇ
6994 "
6995 ));
6996 cx.simulate_input("four");
6997 cx.run_until_parked();
6998
6999 cx.assert_editor_state(indoc!(
7000 "
7001 one two three
7002 fourˇ
7003 "
7004 ));
7005
7006 cx.update_editor(|editor, window, cx| {
7007 editor.newline(&Default::default(), window, cx);
7008 });
7009 cx.run_until_parked();
7010 cx.assert_editor_state(indoc!(
7011 "
7012 one two three
7013 four
7014 ˇ
7015 "
7016 ));
7017
7018 cx.simulate_input("five");
7019 cx.run_until_parked();
7020 cx.assert_editor_state(indoc!(
7021 "
7022 one two three
7023 four
7024 fiveˇ
7025 "
7026 ));
7027
7028 cx.update_editor(|editor, window, cx| {
7029 editor.newline(&Default::default(), window, cx);
7030 });
7031 cx.run_until_parked();
7032 cx.simulate_input("# ");
7033 cx.run_until_parked();
7034 cx.assert_editor_state(indoc!(
7035 "
7036 one two three
7037 four
7038 five
7039 # ˇ
7040 "
7041 ));
7042
7043 cx.update_editor(|editor, window, cx| {
7044 editor.newline(&Default::default(), window, cx);
7045 });
7046 cx.run_until_parked();
7047 cx.assert_editor_state(indoc!(
7048 "
7049 one two three
7050 four
7051 five
7052 #\x20
7053 #ˇ
7054 "
7055 ));
7056
7057 cx.simulate_input(" 6");
7058 cx.run_until_parked();
7059 cx.assert_editor_state(indoc!(
7060 "
7061 one two three
7062 four
7063 five
7064 #
7065 # 6ˇ
7066 "
7067 ));
7068}
7069
7070#[gpui::test]
7071async fn test_cut_line_ends(cx: &mut TestAppContext) {
7072 init_test(cx, |_| {});
7073
7074 let mut cx = EditorTestContext::new(cx).await;
7075
7076 cx.set_state(indoc! {"The quick brownˇ"});
7077 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7078 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7079
7080 cx.set_state(indoc! {"The emacs foxˇ"});
7081 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7082 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7083
7084 cx.set_state(indoc! {"
7085 The quick« brownˇ»
7086 fox jumps overˇ
7087 the lazy dog"});
7088 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7089 cx.assert_editor_state(indoc! {"
7090 The quickˇ
7091 ˇthe lazy dog"});
7092
7093 cx.set_state(indoc! {"
7094 The quick« brownˇ»
7095 fox jumps overˇ
7096 the lazy dog"});
7097 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7098 cx.assert_editor_state(indoc! {"
7099 The quickˇ
7100 fox jumps overˇthe lazy dog"});
7101
7102 cx.set_state(indoc! {"
7103 The quick« brownˇ»
7104 fox jumps overˇ
7105 the lazy dog"});
7106 cx.update_editor(|e, window, cx| {
7107 e.cut_to_end_of_line(
7108 &CutToEndOfLine {
7109 stop_at_newlines: true,
7110 },
7111 window,
7112 cx,
7113 )
7114 });
7115 cx.assert_editor_state(indoc! {"
7116 The quickˇ
7117 fox jumps overˇ
7118 the lazy dog"});
7119
7120 cx.set_state(indoc! {"
7121 The quick« brownˇ»
7122 fox jumps overˇ
7123 the lazy dog"});
7124 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7125 cx.assert_editor_state(indoc! {"
7126 The quickˇ
7127 fox jumps overˇthe lazy dog"});
7128}
7129
7130#[gpui::test]
7131async fn test_clipboard(cx: &mut TestAppContext) {
7132 init_test(cx, |_| {});
7133
7134 let mut cx = EditorTestContext::new(cx).await;
7135
7136 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7137 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7138 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7139
7140 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7141 cx.set_state("two ˇfour ˇsix ˇ");
7142 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7143 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7144
7145 // Paste again but with only two cursors. Since the number of cursors doesn't
7146 // match the number of slices in the clipboard, the entire clipboard text
7147 // is pasted at each cursor.
7148 cx.set_state("ˇtwo one✅ four three six five ˇ");
7149 cx.update_editor(|e, window, cx| {
7150 e.handle_input("( ", window, cx);
7151 e.paste(&Paste, window, cx);
7152 e.handle_input(") ", window, cx);
7153 });
7154 cx.assert_editor_state(
7155 &([
7156 "( one✅ ",
7157 "three ",
7158 "five ) ˇtwo one✅ four three six five ( one✅ ",
7159 "three ",
7160 "five ) ˇ",
7161 ]
7162 .join("\n")),
7163 );
7164
7165 // Cut with three selections, one of which is full-line.
7166 cx.set_state(indoc! {"
7167 1«2ˇ»3
7168 4ˇ567
7169 «8ˇ»9"});
7170 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7171 cx.assert_editor_state(indoc! {"
7172 1ˇ3
7173 ˇ9"});
7174
7175 // Paste with three selections, noticing how the copied selection that was full-line
7176 // gets inserted before the second cursor.
7177 cx.set_state(indoc! {"
7178 1ˇ3
7179 9ˇ
7180 «oˇ»ne"});
7181 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7182 cx.assert_editor_state(indoc! {"
7183 12ˇ3
7184 4567
7185 9ˇ
7186 8ˇne"});
7187
7188 // Copy with a single cursor only, which writes the whole line into the clipboard.
7189 cx.set_state(indoc! {"
7190 The quick brown
7191 fox juˇmps over
7192 the lazy dog"});
7193 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7194 assert_eq!(
7195 cx.read_from_clipboard()
7196 .and_then(|item| item.text().as_deref().map(str::to_string)),
7197 Some("fox jumps over\n".to_string())
7198 );
7199
7200 // Paste with three selections, noticing how the copied full-line selection is inserted
7201 // before the empty selections but replaces the selection that is non-empty.
7202 cx.set_state(indoc! {"
7203 Tˇhe quick brown
7204 «foˇ»x jumps over
7205 tˇhe lazy dog"});
7206 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7207 cx.assert_editor_state(indoc! {"
7208 fox jumps over
7209 Tˇhe quick brown
7210 fox jumps over
7211 ˇx jumps over
7212 fox jumps over
7213 tˇhe lazy dog"});
7214}
7215
7216#[gpui::test]
7217async fn test_copy_trim(cx: &mut TestAppContext) {
7218 init_test(cx, |_| {});
7219
7220 let mut cx = EditorTestContext::new(cx).await;
7221 cx.set_state(
7222 r#" «for selection in selections.iter() {
7223 let mut start = selection.start;
7224 let mut end = selection.end;
7225 let is_entire_line = selection.is_empty();
7226 if is_entire_line {
7227 start = Point::new(start.row, 0);ˇ»
7228 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7229 }
7230 "#,
7231 );
7232 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7233 assert_eq!(
7234 cx.read_from_clipboard()
7235 .and_then(|item| item.text().as_deref().map(str::to_string)),
7236 Some(
7237 "for selection in selections.iter() {
7238 let mut start = selection.start;
7239 let mut end = selection.end;
7240 let is_entire_line = selection.is_empty();
7241 if is_entire_line {
7242 start = Point::new(start.row, 0);"
7243 .to_string()
7244 ),
7245 "Regular copying preserves all indentation selected",
7246 );
7247 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7248 assert_eq!(
7249 cx.read_from_clipboard()
7250 .and_then(|item| item.text().as_deref().map(str::to_string)),
7251 Some(
7252 "for selection in selections.iter() {
7253let mut start = selection.start;
7254let mut end = selection.end;
7255let is_entire_line = selection.is_empty();
7256if is_entire_line {
7257 start = Point::new(start.row, 0);"
7258 .to_string()
7259 ),
7260 "Copying with stripping should strip all leading whitespaces"
7261 );
7262
7263 cx.set_state(
7264 r#" « for selection in selections.iter() {
7265 let mut start = selection.start;
7266 let mut end = selection.end;
7267 let is_entire_line = selection.is_empty();
7268 if is_entire_line {
7269 start = Point::new(start.row, 0);ˇ»
7270 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7271 }
7272 "#,
7273 );
7274 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7275 assert_eq!(
7276 cx.read_from_clipboard()
7277 .and_then(|item| item.text().as_deref().map(str::to_string)),
7278 Some(
7279 " for selection in selections.iter() {
7280 let mut start = selection.start;
7281 let mut end = selection.end;
7282 let is_entire_line = selection.is_empty();
7283 if is_entire_line {
7284 start = Point::new(start.row, 0);"
7285 .to_string()
7286 ),
7287 "Regular copying preserves all indentation selected",
7288 );
7289 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7290 assert_eq!(
7291 cx.read_from_clipboard()
7292 .and_then(|item| item.text().as_deref().map(str::to_string)),
7293 Some(
7294 "for selection in selections.iter() {
7295let mut start = selection.start;
7296let mut end = selection.end;
7297let is_entire_line = selection.is_empty();
7298if is_entire_line {
7299 start = Point::new(start.row, 0);"
7300 .to_string()
7301 ),
7302 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7303 );
7304
7305 cx.set_state(
7306 r#" «ˇ for selection in selections.iter() {
7307 let mut start = selection.start;
7308 let mut end = selection.end;
7309 let is_entire_line = selection.is_empty();
7310 if is_entire_line {
7311 start = Point::new(start.row, 0);»
7312 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7313 }
7314 "#,
7315 );
7316 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7317 assert_eq!(
7318 cx.read_from_clipboard()
7319 .and_then(|item| item.text().as_deref().map(str::to_string)),
7320 Some(
7321 " for selection in selections.iter() {
7322 let mut start = selection.start;
7323 let mut end = selection.end;
7324 let is_entire_line = selection.is_empty();
7325 if is_entire_line {
7326 start = Point::new(start.row, 0);"
7327 .to_string()
7328 ),
7329 "Regular copying for reverse selection works the same",
7330 );
7331 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7332 assert_eq!(
7333 cx.read_from_clipboard()
7334 .and_then(|item| item.text().as_deref().map(str::to_string)),
7335 Some(
7336 "for selection in selections.iter() {
7337let mut start = selection.start;
7338let mut end = selection.end;
7339let is_entire_line = selection.is_empty();
7340if is_entire_line {
7341 start = Point::new(start.row, 0);"
7342 .to_string()
7343 ),
7344 "Copying with stripping for reverse selection works the same"
7345 );
7346
7347 cx.set_state(
7348 r#" for selection «in selections.iter() {
7349 let mut start = selection.start;
7350 let mut end = selection.end;
7351 let is_entire_line = selection.is_empty();
7352 if is_entire_line {
7353 start = Point::new(start.row, 0);ˇ»
7354 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7355 }
7356 "#,
7357 );
7358 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7359 assert_eq!(
7360 cx.read_from_clipboard()
7361 .and_then(|item| item.text().as_deref().map(str::to_string)),
7362 Some(
7363 "in selections.iter() {
7364 let mut start = selection.start;
7365 let mut end = selection.end;
7366 let is_entire_line = selection.is_empty();
7367 if is_entire_line {
7368 start = Point::new(start.row, 0);"
7369 .to_string()
7370 ),
7371 "When selecting past the indent, the copying works as usual",
7372 );
7373 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7374 assert_eq!(
7375 cx.read_from_clipboard()
7376 .and_then(|item| item.text().as_deref().map(str::to_string)),
7377 Some(
7378 "in selections.iter() {
7379 let mut start = selection.start;
7380 let mut end = selection.end;
7381 let is_entire_line = selection.is_empty();
7382 if is_entire_line {
7383 start = Point::new(start.row, 0);"
7384 .to_string()
7385 ),
7386 "When selecting past the indent, nothing is trimmed"
7387 );
7388
7389 cx.set_state(
7390 r#" «for selection in selections.iter() {
7391 let mut start = selection.start;
7392
7393 let mut end = selection.end;
7394 let is_entire_line = selection.is_empty();
7395 if is_entire_line {
7396 start = Point::new(start.row, 0);
7397ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7398 }
7399 "#,
7400 );
7401 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7402 assert_eq!(
7403 cx.read_from_clipboard()
7404 .and_then(|item| item.text().as_deref().map(str::to_string)),
7405 Some(
7406 "for selection in selections.iter() {
7407let mut start = selection.start;
7408
7409let mut end = selection.end;
7410let is_entire_line = selection.is_empty();
7411if is_entire_line {
7412 start = Point::new(start.row, 0);
7413"
7414 .to_string()
7415 ),
7416 "Copying with stripping should ignore empty lines"
7417 );
7418}
7419
7420#[gpui::test]
7421async fn test_paste_multiline(cx: &mut TestAppContext) {
7422 init_test(cx, |_| {});
7423
7424 let mut cx = EditorTestContext::new(cx).await;
7425 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7426
7427 // Cut an indented block, without the leading whitespace.
7428 cx.set_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 «d(
7432 e,
7433 f
7434 )ˇ»
7435 );
7436 "});
7437 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7438 cx.assert_editor_state(indoc! {"
7439 const a: B = (
7440 c(),
7441 ˇ
7442 );
7443 "});
7444
7445 // Paste it at the same position.
7446 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7447 cx.assert_editor_state(indoc! {"
7448 const a: B = (
7449 c(),
7450 d(
7451 e,
7452 f
7453 )ˇ
7454 );
7455 "});
7456
7457 // Paste it at a line with a lower indent level.
7458 cx.set_state(indoc! {"
7459 ˇ
7460 const a: B = (
7461 c(),
7462 );
7463 "});
7464 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7465 cx.assert_editor_state(indoc! {"
7466 d(
7467 e,
7468 f
7469 )ˇ
7470 const a: B = (
7471 c(),
7472 );
7473 "});
7474
7475 // Cut an indented block, with the leading whitespace.
7476 cx.set_state(indoc! {"
7477 const a: B = (
7478 c(),
7479 « d(
7480 e,
7481 f
7482 )
7483 ˇ»);
7484 "});
7485 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7486 cx.assert_editor_state(indoc! {"
7487 const a: B = (
7488 c(),
7489 ˇ);
7490 "});
7491
7492 // Paste it at the same position.
7493 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7494 cx.assert_editor_state(indoc! {"
7495 const a: B = (
7496 c(),
7497 d(
7498 e,
7499 f
7500 )
7501 ˇ);
7502 "});
7503
7504 // Paste it at a line with a higher indent level.
7505 cx.set_state(indoc! {"
7506 const a: B = (
7507 c(),
7508 d(
7509 e,
7510 fˇ
7511 )
7512 );
7513 "});
7514 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7515 cx.assert_editor_state(indoc! {"
7516 const a: B = (
7517 c(),
7518 d(
7519 e,
7520 f d(
7521 e,
7522 f
7523 )
7524 ˇ
7525 )
7526 );
7527 "});
7528
7529 // Copy an indented block, starting mid-line
7530 cx.set_state(indoc! {"
7531 const a: B = (
7532 c(),
7533 somethin«g(
7534 e,
7535 f
7536 )ˇ»
7537 );
7538 "});
7539 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7540
7541 // Paste it on a line with a lower indent level
7542 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7543 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7544 cx.assert_editor_state(indoc! {"
7545 const a: B = (
7546 c(),
7547 something(
7548 e,
7549 f
7550 )
7551 );
7552 g(
7553 e,
7554 f
7555 )ˇ"});
7556}
7557
7558#[gpui::test]
7559async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7560 init_test(cx, |_| {});
7561
7562 cx.write_to_clipboard(ClipboardItem::new_string(
7563 " d(\n e\n );\n".into(),
7564 ));
7565
7566 let mut cx = EditorTestContext::new(cx).await;
7567 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7568
7569 cx.set_state(indoc! {"
7570 fn a() {
7571 b();
7572 if c() {
7573 ˇ
7574 }
7575 }
7576 "});
7577
7578 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7579 cx.assert_editor_state(indoc! {"
7580 fn a() {
7581 b();
7582 if c() {
7583 d(
7584 e
7585 );
7586 ˇ
7587 }
7588 }
7589 "});
7590
7591 cx.set_state(indoc! {"
7592 fn a() {
7593 b();
7594 ˇ
7595 }
7596 "});
7597
7598 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7599 cx.assert_editor_state(indoc! {"
7600 fn a() {
7601 b();
7602 d(
7603 e
7604 );
7605 ˇ
7606 }
7607 "});
7608}
7609
7610#[gpui::test]
7611fn test_select_all(cx: &mut TestAppContext) {
7612 init_test(cx, |_| {});
7613
7614 let editor = cx.add_window(|window, cx| {
7615 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7616 build_editor(buffer, window, cx)
7617 });
7618 _ = editor.update(cx, |editor, window, cx| {
7619 editor.select_all(&SelectAll, window, cx);
7620 assert_eq!(
7621 display_ranges(editor, cx),
7622 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7623 );
7624 });
7625}
7626
7627#[gpui::test]
7628fn test_select_line(cx: &mut TestAppContext) {
7629 init_test(cx, |_| {});
7630
7631 let editor = cx.add_window(|window, cx| {
7632 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7633 build_editor(buffer, window, cx)
7634 });
7635 _ = editor.update(cx, |editor, window, cx| {
7636 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7637 s.select_display_ranges([
7638 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7639 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7640 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7641 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7642 ])
7643 });
7644 editor.select_line(&SelectLine, window, cx);
7645 assert_eq!(
7646 display_ranges(editor, cx),
7647 vec![
7648 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7649 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7650 ]
7651 );
7652 });
7653
7654 _ = editor.update(cx, |editor, window, cx| {
7655 editor.select_line(&SelectLine, window, cx);
7656 assert_eq!(
7657 display_ranges(editor, cx),
7658 vec![
7659 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7660 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7661 ]
7662 );
7663 });
7664
7665 _ = editor.update(cx, |editor, window, cx| {
7666 editor.select_line(&SelectLine, window, cx);
7667 assert_eq!(
7668 display_ranges(editor, cx),
7669 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7670 );
7671 });
7672}
7673
7674#[gpui::test]
7675async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7676 init_test(cx, |_| {});
7677 let mut cx = EditorTestContext::new(cx).await;
7678
7679 #[track_caller]
7680 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7681 cx.set_state(initial_state);
7682 cx.update_editor(|e, window, cx| {
7683 e.split_selection_into_lines(&Default::default(), window, cx)
7684 });
7685 cx.assert_editor_state(expected_state);
7686 }
7687
7688 // Selection starts and ends at the middle of lines, left-to-right
7689 test(
7690 &mut cx,
7691 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7692 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7693 );
7694 // Same thing, right-to-left
7695 test(
7696 &mut cx,
7697 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7698 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7699 );
7700
7701 // Whole buffer, left-to-right, last line *doesn't* end with newline
7702 test(
7703 &mut cx,
7704 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7705 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7706 );
7707 // Same thing, right-to-left
7708 test(
7709 &mut cx,
7710 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7711 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7712 );
7713
7714 // Whole buffer, left-to-right, last line ends with newline
7715 test(
7716 &mut cx,
7717 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7718 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7719 );
7720 // Same thing, right-to-left
7721 test(
7722 &mut cx,
7723 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7724 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7725 );
7726
7727 // Starts at the end of a line, ends at the start of another
7728 test(
7729 &mut cx,
7730 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7731 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7732 );
7733}
7734
7735#[gpui::test]
7736async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7737 init_test(cx, |_| {});
7738
7739 let editor = cx.add_window(|window, cx| {
7740 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7741 build_editor(buffer, window, cx)
7742 });
7743
7744 // setup
7745 _ = editor.update(cx, |editor, window, cx| {
7746 editor.fold_creases(
7747 vec![
7748 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7749 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7750 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7751 ],
7752 true,
7753 window,
7754 cx,
7755 );
7756 assert_eq!(
7757 editor.display_text(cx),
7758 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7759 );
7760 });
7761
7762 _ = editor.update(cx, |editor, window, cx| {
7763 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7764 s.select_display_ranges([
7765 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7766 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7767 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7768 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7769 ])
7770 });
7771 editor.split_selection_into_lines(&Default::default(), window, cx);
7772 assert_eq!(
7773 editor.display_text(cx),
7774 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7775 );
7776 });
7777 EditorTestContext::for_editor(editor, cx)
7778 .await
7779 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7780
7781 _ = editor.update(cx, |editor, window, cx| {
7782 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7783 s.select_display_ranges([
7784 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7785 ])
7786 });
7787 editor.split_selection_into_lines(&Default::default(), window, cx);
7788 assert_eq!(
7789 editor.display_text(cx),
7790 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7791 );
7792 assert_eq!(
7793 display_ranges(editor, cx),
7794 [
7795 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7796 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7797 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7798 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7799 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7800 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7801 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7802 ]
7803 );
7804 });
7805 EditorTestContext::for_editor(editor, cx)
7806 .await
7807 .assert_editor_state(
7808 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7809 );
7810}
7811
7812#[gpui::test]
7813async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7814 init_test(cx, |_| {});
7815
7816 let mut cx = EditorTestContext::new(cx).await;
7817
7818 cx.set_state(indoc!(
7819 r#"abc
7820 defˇghi
7821
7822 jk
7823 nlmo
7824 "#
7825 ));
7826
7827 cx.update_editor(|editor, window, cx| {
7828 editor.add_selection_above(&Default::default(), window, cx);
7829 });
7830
7831 cx.assert_editor_state(indoc!(
7832 r#"abcˇ
7833 defˇghi
7834
7835 jk
7836 nlmo
7837 "#
7838 ));
7839
7840 cx.update_editor(|editor, window, cx| {
7841 editor.add_selection_above(&Default::default(), window, cx);
7842 });
7843
7844 cx.assert_editor_state(indoc!(
7845 r#"abcˇ
7846 defˇghi
7847
7848 jk
7849 nlmo
7850 "#
7851 ));
7852
7853 cx.update_editor(|editor, window, cx| {
7854 editor.add_selection_below(&Default::default(), window, cx);
7855 });
7856
7857 cx.assert_editor_state(indoc!(
7858 r#"abc
7859 defˇghi
7860
7861 jk
7862 nlmo
7863 "#
7864 ));
7865
7866 cx.update_editor(|editor, window, cx| {
7867 editor.undo_selection(&Default::default(), window, cx);
7868 });
7869
7870 cx.assert_editor_state(indoc!(
7871 r#"abcˇ
7872 defˇghi
7873
7874 jk
7875 nlmo
7876 "#
7877 ));
7878
7879 cx.update_editor(|editor, window, cx| {
7880 editor.redo_selection(&Default::default(), window, cx);
7881 });
7882
7883 cx.assert_editor_state(indoc!(
7884 r#"abc
7885 defˇghi
7886
7887 jk
7888 nlmo
7889 "#
7890 ));
7891
7892 cx.update_editor(|editor, window, cx| {
7893 editor.add_selection_below(&Default::default(), window, cx);
7894 });
7895
7896 cx.assert_editor_state(indoc!(
7897 r#"abc
7898 defˇghi
7899 ˇ
7900 jk
7901 nlmo
7902 "#
7903 ));
7904
7905 cx.update_editor(|editor, window, cx| {
7906 editor.add_selection_below(&Default::default(), window, cx);
7907 });
7908
7909 cx.assert_editor_state(indoc!(
7910 r#"abc
7911 defˇghi
7912 ˇ
7913 jkˇ
7914 nlmo
7915 "#
7916 ));
7917
7918 cx.update_editor(|editor, window, cx| {
7919 editor.add_selection_below(&Default::default(), window, cx);
7920 });
7921
7922 cx.assert_editor_state(indoc!(
7923 r#"abc
7924 defˇghi
7925 ˇ
7926 jkˇ
7927 nlmˇo
7928 "#
7929 ));
7930
7931 cx.update_editor(|editor, window, cx| {
7932 editor.add_selection_below(&Default::default(), window, cx);
7933 });
7934
7935 cx.assert_editor_state(indoc!(
7936 r#"abc
7937 defˇghi
7938 ˇ
7939 jkˇ
7940 nlmˇo
7941 ˇ"#
7942 ));
7943
7944 // change selections
7945 cx.set_state(indoc!(
7946 r#"abc
7947 def«ˇg»hi
7948
7949 jk
7950 nlmo
7951 "#
7952 ));
7953
7954 cx.update_editor(|editor, window, cx| {
7955 editor.add_selection_below(&Default::default(), window, cx);
7956 });
7957
7958 cx.assert_editor_state(indoc!(
7959 r#"abc
7960 def«ˇg»hi
7961
7962 jk
7963 nlm«ˇo»
7964 "#
7965 ));
7966
7967 cx.update_editor(|editor, window, cx| {
7968 editor.add_selection_below(&Default::default(), window, cx);
7969 });
7970
7971 cx.assert_editor_state(indoc!(
7972 r#"abc
7973 def«ˇg»hi
7974
7975 jk
7976 nlm«ˇo»
7977 "#
7978 ));
7979
7980 cx.update_editor(|editor, window, cx| {
7981 editor.add_selection_above(&Default::default(), window, cx);
7982 });
7983
7984 cx.assert_editor_state(indoc!(
7985 r#"abc
7986 def«ˇg»hi
7987
7988 jk
7989 nlmo
7990 "#
7991 ));
7992
7993 cx.update_editor(|editor, window, cx| {
7994 editor.add_selection_above(&Default::default(), window, cx);
7995 });
7996
7997 cx.assert_editor_state(indoc!(
7998 r#"abc
7999 def«ˇg»hi
8000
8001 jk
8002 nlmo
8003 "#
8004 ));
8005
8006 // Change selections again
8007 cx.set_state(indoc!(
8008 r#"a«bc
8009 defgˇ»hi
8010
8011 jk
8012 nlmo
8013 "#
8014 ));
8015
8016 cx.update_editor(|editor, window, cx| {
8017 editor.add_selection_below(&Default::default(), window, cx);
8018 });
8019
8020 cx.assert_editor_state(indoc!(
8021 r#"a«bcˇ»
8022 d«efgˇ»hi
8023
8024 j«kˇ»
8025 nlmo
8026 "#
8027 ));
8028
8029 cx.update_editor(|editor, window, cx| {
8030 editor.add_selection_below(&Default::default(), window, cx);
8031 });
8032 cx.assert_editor_state(indoc!(
8033 r#"a«bcˇ»
8034 d«efgˇ»hi
8035
8036 j«kˇ»
8037 n«lmoˇ»
8038 "#
8039 ));
8040 cx.update_editor(|editor, window, cx| {
8041 editor.add_selection_above(&Default::default(), window, cx);
8042 });
8043
8044 cx.assert_editor_state(indoc!(
8045 r#"a«bcˇ»
8046 d«efgˇ»hi
8047
8048 j«kˇ»
8049 nlmo
8050 "#
8051 ));
8052
8053 // Change selections again
8054 cx.set_state(indoc!(
8055 r#"abc
8056 d«ˇefghi
8057
8058 jk
8059 nlm»o
8060 "#
8061 ));
8062
8063 cx.update_editor(|editor, window, cx| {
8064 editor.add_selection_above(&Default::default(), window, cx);
8065 });
8066
8067 cx.assert_editor_state(indoc!(
8068 r#"a«ˇbc»
8069 d«ˇef»ghi
8070
8071 j«ˇk»
8072 n«ˇlm»o
8073 "#
8074 ));
8075
8076 cx.update_editor(|editor, window, cx| {
8077 editor.add_selection_below(&Default::default(), window, cx);
8078 });
8079
8080 cx.assert_editor_state(indoc!(
8081 r#"abc
8082 d«ˇef»ghi
8083
8084 j«ˇk»
8085 n«ˇlm»o
8086 "#
8087 ));
8088}
8089
8090#[gpui::test]
8091async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8092 init_test(cx, |_| {});
8093 let mut cx = EditorTestContext::new(cx).await;
8094
8095 cx.set_state(indoc!(
8096 r#"line onˇe
8097 liˇne two
8098 line three
8099 line four"#
8100 ));
8101
8102 cx.update_editor(|editor, window, cx| {
8103 editor.add_selection_below(&Default::default(), window, cx);
8104 });
8105
8106 // test multiple cursors expand in the same direction
8107 cx.assert_editor_state(indoc!(
8108 r#"line onˇe
8109 liˇne twˇo
8110 liˇne three
8111 line four"#
8112 ));
8113
8114 cx.update_editor(|editor, window, cx| {
8115 editor.add_selection_below(&Default::default(), window, cx);
8116 });
8117
8118 cx.update_editor(|editor, window, cx| {
8119 editor.add_selection_below(&Default::default(), window, cx);
8120 });
8121
8122 // test multiple cursors expand below overflow
8123 cx.assert_editor_state(indoc!(
8124 r#"line onˇe
8125 liˇne twˇo
8126 liˇne thˇree
8127 liˇne foˇur"#
8128 ));
8129
8130 cx.update_editor(|editor, window, cx| {
8131 editor.add_selection_above(&Default::default(), window, cx);
8132 });
8133
8134 // test multiple cursors retrieves back correctly
8135 cx.assert_editor_state(indoc!(
8136 r#"line onˇe
8137 liˇne twˇo
8138 liˇne thˇree
8139 line four"#
8140 ));
8141
8142 cx.update_editor(|editor, window, cx| {
8143 editor.add_selection_above(&Default::default(), window, cx);
8144 });
8145
8146 cx.update_editor(|editor, window, cx| {
8147 editor.add_selection_above(&Default::default(), window, cx);
8148 });
8149
8150 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8151 cx.assert_editor_state(indoc!(
8152 r#"liˇne onˇe
8153 liˇne two
8154 line three
8155 line four"#
8156 ));
8157
8158 cx.update_editor(|editor, window, cx| {
8159 editor.undo_selection(&Default::default(), window, cx);
8160 });
8161
8162 // test undo
8163 cx.assert_editor_state(indoc!(
8164 r#"line onˇe
8165 liˇne twˇo
8166 line three
8167 line four"#
8168 ));
8169
8170 cx.update_editor(|editor, window, cx| {
8171 editor.redo_selection(&Default::default(), window, cx);
8172 });
8173
8174 // test redo
8175 cx.assert_editor_state(indoc!(
8176 r#"liˇne onˇe
8177 liˇne two
8178 line three
8179 line four"#
8180 ));
8181
8182 cx.set_state(indoc!(
8183 r#"abcd
8184 ef«ghˇ»
8185 ijkl
8186 «mˇ»nop"#
8187 ));
8188
8189 cx.update_editor(|editor, window, cx| {
8190 editor.add_selection_above(&Default::default(), window, cx);
8191 });
8192
8193 // test multiple selections expand in the same direction
8194 cx.assert_editor_state(indoc!(
8195 r#"ab«cdˇ»
8196 ef«ghˇ»
8197 «iˇ»jkl
8198 «mˇ»nop"#
8199 ));
8200
8201 cx.update_editor(|editor, window, cx| {
8202 editor.add_selection_above(&Default::default(), window, cx);
8203 });
8204
8205 // test multiple selection upward overflow
8206 cx.assert_editor_state(indoc!(
8207 r#"ab«cdˇ»
8208 «eˇ»f«ghˇ»
8209 «iˇ»jkl
8210 «mˇ»nop"#
8211 ));
8212
8213 cx.update_editor(|editor, window, cx| {
8214 editor.add_selection_below(&Default::default(), window, cx);
8215 });
8216
8217 // test multiple selection retrieves back correctly
8218 cx.assert_editor_state(indoc!(
8219 r#"abcd
8220 ef«ghˇ»
8221 «iˇ»jkl
8222 «mˇ»nop"#
8223 ));
8224
8225 cx.update_editor(|editor, window, cx| {
8226 editor.add_selection_below(&Default::default(), window, cx);
8227 });
8228
8229 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8230 cx.assert_editor_state(indoc!(
8231 r#"abcd
8232 ef«ghˇ»
8233 ij«klˇ»
8234 «mˇ»nop"#
8235 ));
8236
8237 cx.update_editor(|editor, window, cx| {
8238 editor.undo_selection(&Default::default(), window, cx);
8239 });
8240
8241 // test undo
8242 cx.assert_editor_state(indoc!(
8243 r#"abcd
8244 ef«ghˇ»
8245 «iˇ»jkl
8246 «mˇ»nop"#
8247 ));
8248
8249 cx.update_editor(|editor, window, cx| {
8250 editor.redo_selection(&Default::default(), window, cx);
8251 });
8252
8253 // test redo
8254 cx.assert_editor_state(indoc!(
8255 r#"abcd
8256 ef«ghˇ»
8257 ij«klˇ»
8258 «mˇ»nop"#
8259 ));
8260}
8261
8262#[gpui::test]
8263async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8264 init_test(cx, |_| {});
8265 let mut cx = EditorTestContext::new(cx).await;
8266
8267 cx.set_state(indoc!(
8268 r#"line onˇe
8269 liˇne two
8270 line three
8271 line four"#
8272 ));
8273
8274 cx.update_editor(|editor, window, cx| {
8275 editor.add_selection_below(&Default::default(), window, cx);
8276 editor.add_selection_below(&Default::default(), window, cx);
8277 editor.add_selection_below(&Default::default(), window, cx);
8278 });
8279
8280 // initial state with two multi cursor groups
8281 cx.assert_editor_state(indoc!(
8282 r#"line onˇe
8283 liˇne twˇo
8284 liˇne thˇree
8285 liˇne foˇur"#
8286 ));
8287
8288 // add single cursor in middle - simulate opt click
8289 cx.update_editor(|editor, window, cx| {
8290 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8291 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8292 editor.end_selection(window, cx);
8293 });
8294
8295 cx.assert_editor_state(indoc!(
8296 r#"line onˇe
8297 liˇne twˇo
8298 liˇneˇ thˇree
8299 liˇne foˇur"#
8300 ));
8301
8302 cx.update_editor(|editor, window, cx| {
8303 editor.add_selection_above(&Default::default(), window, cx);
8304 });
8305
8306 // test new added selection expands above and existing selection shrinks
8307 cx.assert_editor_state(indoc!(
8308 r#"line onˇe
8309 liˇneˇ twˇo
8310 liˇneˇ thˇree
8311 line four"#
8312 ));
8313
8314 cx.update_editor(|editor, window, cx| {
8315 editor.add_selection_above(&Default::default(), window, cx);
8316 });
8317
8318 // test new added selection expands above and existing selection shrinks
8319 cx.assert_editor_state(indoc!(
8320 r#"lineˇ onˇe
8321 liˇneˇ twˇo
8322 lineˇ three
8323 line four"#
8324 ));
8325
8326 // intial state with two selection groups
8327 cx.set_state(indoc!(
8328 r#"abcd
8329 ef«ghˇ»
8330 ijkl
8331 «mˇ»nop"#
8332 ));
8333
8334 cx.update_editor(|editor, window, cx| {
8335 editor.add_selection_above(&Default::default(), window, cx);
8336 editor.add_selection_above(&Default::default(), window, cx);
8337 });
8338
8339 cx.assert_editor_state(indoc!(
8340 r#"ab«cdˇ»
8341 «eˇ»f«ghˇ»
8342 «iˇ»jkl
8343 «mˇ»nop"#
8344 ));
8345
8346 // add single selection in middle - simulate opt drag
8347 cx.update_editor(|editor, window, cx| {
8348 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8349 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8350 editor.update_selection(
8351 DisplayPoint::new(DisplayRow(2), 4),
8352 0,
8353 gpui::Point::<f32>::default(),
8354 window,
8355 cx,
8356 );
8357 editor.end_selection(window, cx);
8358 });
8359
8360 cx.assert_editor_state(indoc!(
8361 r#"ab«cdˇ»
8362 «eˇ»f«ghˇ»
8363 «iˇ»jk«lˇ»
8364 «mˇ»nop"#
8365 ));
8366
8367 cx.update_editor(|editor, window, cx| {
8368 editor.add_selection_below(&Default::default(), window, cx);
8369 });
8370
8371 // test new added selection expands below, others shrinks from above
8372 cx.assert_editor_state(indoc!(
8373 r#"abcd
8374 ef«ghˇ»
8375 «iˇ»jk«lˇ»
8376 «mˇ»no«pˇ»"#
8377 ));
8378}
8379
8380#[gpui::test]
8381async fn test_select_next(cx: &mut TestAppContext) {
8382 init_test(cx, |_| {});
8383 let mut cx = EditorTestContext::new(cx).await;
8384
8385 // Enable case sensitive search.
8386 update_test_editor_settings(&mut cx, |settings| {
8387 let mut search_settings = SearchSettingsContent::default();
8388 search_settings.case_sensitive = Some(true);
8389 settings.search = Some(search_settings);
8390 });
8391
8392 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8393
8394 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8395 .unwrap();
8396 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8397
8398 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8399 .unwrap();
8400 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8401
8402 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8403 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8404
8405 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8406 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8407
8408 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8409 .unwrap();
8410 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8411
8412 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8413 .unwrap();
8414 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8415
8416 // Test selection direction should be preserved
8417 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8418
8419 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8420 .unwrap();
8421 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8422
8423 // Test case sensitivity
8424 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8425 cx.update_editor(|e, window, cx| {
8426 e.select_next(&SelectNext::default(), window, cx).unwrap();
8427 });
8428 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8429
8430 // Disable case sensitive search.
8431 update_test_editor_settings(&mut cx, |settings| {
8432 let mut search_settings = SearchSettingsContent::default();
8433 search_settings.case_sensitive = Some(false);
8434 settings.search = Some(search_settings);
8435 });
8436
8437 cx.set_state("«ˇfoo»\nFOO\nFoo");
8438 cx.update_editor(|e, window, cx| {
8439 e.select_next(&SelectNext::default(), window, cx).unwrap();
8440 e.select_next(&SelectNext::default(), window, cx).unwrap();
8441 });
8442 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8443}
8444
8445#[gpui::test]
8446async fn test_select_all_matches(cx: &mut TestAppContext) {
8447 init_test(cx, |_| {});
8448 let mut cx = EditorTestContext::new(cx).await;
8449
8450 // Enable case sensitive search.
8451 update_test_editor_settings(&mut cx, |settings| {
8452 let mut search_settings = SearchSettingsContent::default();
8453 search_settings.case_sensitive = Some(true);
8454 settings.search = Some(search_settings);
8455 });
8456
8457 // Test caret-only selections
8458 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8459 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8460 .unwrap();
8461 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8462
8463 // Test left-to-right selections
8464 cx.set_state("abc\n«abcˇ»\nabc");
8465 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8466 .unwrap();
8467 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8468
8469 // Test right-to-left selections
8470 cx.set_state("abc\n«ˇabc»\nabc");
8471 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8472 .unwrap();
8473 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8474
8475 // Test selecting whitespace with caret selection
8476 cx.set_state("abc\nˇ abc\nabc");
8477 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8478 .unwrap();
8479 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8480
8481 // Test selecting whitespace with left-to-right selection
8482 cx.set_state("abc\n«ˇ »abc\nabc");
8483 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8484 .unwrap();
8485 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8486
8487 // Test no matches with right-to-left selection
8488 cx.set_state("abc\n« ˇ»abc\nabc");
8489 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8490 .unwrap();
8491 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8492
8493 // Test with a single word and clip_at_line_ends=true (#29823)
8494 cx.set_state("aˇbc");
8495 cx.update_editor(|e, window, cx| {
8496 e.set_clip_at_line_ends(true, cx);
8497 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8498 e.set_clip_at_line_ends(false, cx);
8499 });
8500 cx.assert_editor_state("«abcˇ»");
8501
8502 // Test case sensitivity
8503 cx.set_state("fˇoo\nFOO\nFoo");
8504 cx.update_editor(|e, window, cx| {
8505 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8506 });
8507 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8508
8509 // Disable case sensitive search.
8510 update_test_editor_settings(&mut cx, |settings| {
8511 let mut search_settings = SearchSettingsContent::default();
8512 search_settings.case_sensitive = Some(false);
8513 settings.search = Some(search_settings);
8514 });
8515
8516 cx.set_state("fˇoo\nFOO\nFoo");
8517 cx.update_editor(|e, window, cx| {
8518 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8519 });
8520 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8521}
8522
8523#[gpui::test]
8524async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8525 init_test(cx, |_| {});
8526
8527 let mut cx = EditorTestContext::new(cx).await;
8528
8529 let large_body_1 = "\nd".repeat(200);
8530 let large_body_2 = "\ne".repeat(200);
8531
8532 cx.set_state(&format!(
8533 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8534 ));
8535 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8536 let scroll_position = editor.scroll_position(cx);
8537 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8538 scroll_position
8539 });
8540
8541 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8542 .unwrap();
8543 cx.assert_editor_state(&format!(
8544 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8545 ));
8546 let scroll_position_after_selection =
8547 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8548 assert_eq!(
8549 initial_scroll_position, scroll_position_after_selection,
8550 "Scroll position should not change after selecting all matches"
8551 );
8552}
8553
8554#[gpui::test]
8555async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8556 init_test(cx, |_| {});
8557
8558 let mut cx = EditorLspTestContext::new_rust(
8559 lsp::ServerCapabilities {
8560 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8561 ..Default::default()
8562 },
8563 cx,
8564 )
8565 .await;
8566
8567 cx.set_state(indoc! {"
8568 line 1
8569 line 2
8570 linˇe 3
8571 line 4
8572 line 5
8573 "});
8574
8575 // Make an edit
8576 cx.update_editor(|editor, window, cx| {
8577 editor.handle_input("X", window, cx);
8578 });
8579
8580 // Move cursor to a different position
8581 cx.update_editor(|editor, window, cx| {
8582 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8583 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8584 });
8585 });
8586
8587 cx.assert_editor_state(indoc! {"
8588 line 1
8589 line 2
8590 linXe 3
8591 line 4
8592 liˇne 5
8593 "});
8594
8595 cx.lsp
8596 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8597 Ok(Some(vec![lsp::TextEdit::new(
8598 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8599 "PREFIX ".to_string(),
8600 )]))
8601 });
8602
8603 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8604 .unwrap()
8605 .await
8606 .unwrap();
8607
8608 cx.assert_editor_state(indoc! {"
8609 PREFIX line 1
8610 line 2
8611 linXe 3
8612 line 4
8613 liˇne 5
8614 "});
8615
8616 // Undo formatting
8617 cx.update_editor(|editor, window, cx| {
8618 editor.undo(&Default::default(), window, cx);
8619 });
8620
8621 // Verify cursor moved back to position after edit
8622 cx.assert_editor_state(indoc! {"
8623 line 1
8624 line 2
8625 linXˇe 3
8626 line 4
8627 line 5
8628 "});
8629}
8630
8631#[gpui::test]
8632async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8633 init_test(cx, |_| {});
8634
8635 let mut cx = EditorTestContext::new(cx).await;
8636
8637 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8638 cx.update_editor(|editor, window, cx| {
8639 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8640 });
8641
8642 cx.set_state(indoc! {"
8643 line 1
8644 line 2
8645 linˇe 3
8646 line 4
8647 line 5
8648 line 6
8649 line 7
8650 line 8
8651 line 9
8652 line 10
8653 "});
8654
8655 let snapshot = cx.buffer_snapshot();
8656 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8657
8658 cx.update(|_, cx| {
8659 provider.update(cx, |provider, _| {
8660 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8661 id: None,
8662 edits: vec![(edit_position..edit_position, "X".into())],
8663 edit_preview: None,
8664 }))
8665 })
8666 });
8667
8668 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8669 cx.update_editor(|editor, window, cx| {
8670 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8671 });
8672
8673 cx.assert_editor_state(indoc! {"
8674 line 1
8675 line 2
8676 lineXˇ 3
8677 line 4
8678 line 5
8679 line 6
8680 line 7
8681 line 8
8682 line 9
8683 line 10
8684 "});
8685
8686 cx.update_editor(|editor, window, cx| {
8687 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8688 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8689 });
8690 });
8691
8692 cx.assert_editor_state(indoc! {"
8693 line 1
8694 line 2
8695 lineX 3
8696 line 4
8697 line 5
8698 line 6
8699 line 7
8700 line 8
8701 line 9
8702 liˇne 10
8703 "});
8704
8705 cx.update_editor(|editor, window, cx| {
8706 editor.undo(&Default::default(), window, cx);
8707 });
8708
8709 cx.assert_editor_state(indoc! {"
8710 line 1
8711 line 2
8712 lineˇ 3
8713 line 4
8714 line 5
8715 line 6
8716 line 7
8717 line 8
8718 line 9
8719 line 10
8720 "});
8721}
8722
8723#[gpui::test]
8724async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728 cx.set_state(
8729 r#"let foo = 2;
8730lˇet foo = 2;
8731let fooˇ = 2;
8732let foo = 2;
8733let foo = ˇ2;"#,
8734 );
8735
8736 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8737 .unwrap();
8738 cx.assert_editor_state(
8739 r#"let foo = 2;
8740«letˇ» foo = 2;
8741let «fooˇ» = 2;
8742let foo = 2;
8743let foo = «2ˇ»;"#,
8744 );
8745
8746 // noop for multiple selections with different contents
8747 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8748 .unwrap();
8749 cx.assert_editor_state(
8750 r#"let foo = 2;
8751«letˇ» foo = 2;
8752let «fooˇ» = 2;
8753let foo = 2;
8754let foo = «2ˇ»;"#,
8755 );
8756
8757 // Test last selection direction should be preserved
8758 cx.set_state(
8759 r#"let foo = 2;
8760let foo = 2;
8761let «fooˇ» = 2;
8762let «ˇfoo» = 2;
8763let foo = 2;"#,
8764 );
8765
8766 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8767 .unwrap();
8768 cx.assert_editor_state(
8769 r#"let foo = 2;
8770let foo = 2;
8771let «fooˇ» = 2;
8772let «ˇfoo» = 2;
8773let «ˇfoo» = 2;"#,
8774 );
8775}
8776
8777#[gpui::test]
8778async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8779 init_test(cx, |_| {});
8780
8781 let mut cx =
8782 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8783
8784 cx.assert_editor_state(indoc! {"
8785 ˇbbb
8786 ccc
8787
8788 bbb
8789 ccc
8790 "});
8791 cx.dispatch_action(SelectPrevious::default());
8792 cx.assert_editor_state(indoc! {"
8793 «bbbˇ»
8794 ccc
8795
8796 bbb
8797 ccc
8798 "});
8799 cx.dispatch_action(SelectPrevious::default());
8800 cx.assert_editor_state(indoc! {"
8801 «bbbˇ»
8802 ccc
8803
8804 «bbbˇ»
8805 ccc
8806 "});
8807}
8808
8809#[gpui::test]
8810async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8811 init_test(cx, |_| {});
8812
8813 let mut cx = EditorTestContext::new(cx).await;
8814 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8815
8816 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8817 .unwrap();
8818 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8819
8820 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8821 .unwrap();
8822 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8823
8824 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8825 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8826
8827 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8828 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8829
8830 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8831 .unwrap();
8832 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8833
8834 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8835 .unwrap();
8836 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8837}
8838
8839#[gpui::test]
8840async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8841 init_test(cx, |_| {});
8842
8843 let mut cx = EditorTestContext::new(cx).await;
8844 cx.set_state("aˇ");
8845
8846 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8847 .unwrap();
8848 cx.assert_editor_state("«aˇ»");
8849 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8850 .unwrap();
8851 cx.assert_editor_state("«aˇ»");
8852}
8853
8854#[gpui::test]
8855async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8856 init_test(cx, |_| {});
8857
8858 let mut cx = EditorTestContext::new(cx).await;
8859 cx.set_state(
8860 r#"let foo = 2;
8861lˇet foo = 2;
8862let fooˇ = 2;
8863let foo = 2;
8864let foo = ˇ2;"#,
8865 );
8866
8867 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8868 .unwrap();
8869 cx.assert_editor_state(
8870 r#"let foo = 2;
8871«letˇ» foo = 2;
8872let «fooˇ» = 2;
8873let foo = 2;
8874let foo = «2ˇ»;"#,
8875 );
8876
8877 // noop for multiple selections with different contents
8878 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8879 .unwrap();
8880 cx.assert_editor_state(
8881 r#"let foo = 2;
8882«letˇ» foo = 2;
8883let «fooˇ» = 2;
8884let foo = 2;
8885let foo = «2ˇ»;"#,
8886 );
8887}
8888
8889#[gpui::test]
8890async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8891 init_test(cx, |_| {});
8892 let mut cx = EditorTestContext::new(cx).await;
8893
8894 // Enable case sensitive search.
8895 update_test_editor_settings(&mut cx, |settings| {
8896 let mut search_settings = SearchSettingsContent::default();
8897 search_settings.case_sensitive = Some(true);
8898 settings.search = Some(search_settings);
8899 });
8900
8901 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8902
8903 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8904 .unwrap();
8905 // selection direction is preserved
8906 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8907
8908 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8909 .unwrap();
8910 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8911
8912 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8913 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8914
8915 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8916 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8917
8918 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8919 .unwrap();
8920 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8921
8922 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8923 .unwrap();
8924 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8925
8926 // Test case sensitivity
8927 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
8928 cx.update_editor(|e, window, cx| {
8929 e.select_previous(&SelectPrevious::default(), window, cx)
8930 .unwrap();
8931 e.select_previous(&SelectPrevious::default(), window, cx)
8932 .unwrap();
8933 });
8934 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8935
8936 // Disable case sensitive search.
8937 update_test_editor_settings(&mut cx, |settings| {
8938 let mut search_settings = SearchSettingsContent::default();
8939 search_settings.case_sensitive = Some(false);
8940 settings.search = Some(search_settings);
8941 });
8942
8943 cx.set_state("foo\nFOO\n«ˇFoo»");
8944 cx.update_editor(|e, window, cx| {
8945 e.select_previous(&SelectPrevious::default(), window, cx)
8946 .unwrap();
8947 e.select_previous(&SelectPrevious::default(), window, cx)
8948 .unwrap();
8949 });
8950 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8951}
8952
8953#[gpui::test]
8954async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8955 init_test(cx, |_| {});
8956
8957 let language = Arc::new(Language::new(
8958 LanguageConfig::default(),
8959 Some(tree_sitter_rust::LANGUAGE.into()),
8960 ));
8961
8962 let text = r#"
8963 use mod1::mod2::{mod3, mod4};
8964
8965 fn fn_1(param1: bool, param2: &str) {
8966 let var1 = "text";
8967 }
8968 "#
8969 .unindent();
8970
8971 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8972 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8973 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8974
8975 editor
8976 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8977 .await;
8978
8979 editor.update_in(cx, |editor, window, cx| {
8980 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8981 s.select_display_ranges([
8982 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8983 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8984 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8985 ]);
8986 });
8987 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8988 });
8989 editor.update(cx, |editor, cx| {
8990 assert_text_with_selections(
8991 editor,
8992 indoc! {r#"
8993 use mod1::mod2::{mod3, «mod4ˇ»};
8994
8995 fn fn_1«ˇ(param1: bool, param2: &str)» {
8996 let var1 = "«ˇtext»";
8997 }
8998 "#},
8999 cx,
9000 );
9001 });
9002
9003 editor.update_in(cx, |editor, window, cx| {
9004 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9005 });
9006 editor.update(cx, |editor, cx| {
9007 assert_text_with_selections(
9008 editor,
9009 indoc! {r#"
9010 use mod1::mod2::«{mod3, mod4}ˇ»;
9011
9012 «ˇfn fn_1(param1: bool, param2: &str) {
9013 let var1 = "text";
9014 }»
9015 "#},
9016 cx,
9017 );
9018 });
9019
9020 editor.update_in(cx, |editor, window, cx| {
9021 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9022 });
9023 assert_eq!(
9024 editor.update(cx, |editor, cx| editor
9025 .selections
9026 .display_ranges(&editor.display_snapshot(cx))),
9027 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9028 );
9029
9030 // Trying to expand the selected syntax node one more time has no effect.
9031 editor.update_in(cx, |editor, window, cx| {
9032 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9033 });
9034 assert_eq!(
9035 editor.update(cx, |editor, cx| editor
9036 .selections
9037 .display_ranges(&editor.display_snapshot(cx))),
9038 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9039 );
9040
9041 editor.update_in(cx, |editor, window, cx| {
9042 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9043 });
9044 editor.update(cx, |editor, cx| {
9045 assert_text_with_selections(
9046 editor,
9047 indoc! {r#"
9048 use mod1::mod2::«{mod3, mod4}ˇ»;
9049
9050 «ˇfn fn_1(param1: bool, param2: &str) {
9051 let var1 = "text";
9052 }»
9053 "#},
9054 cx,
9055 );
9056 });
9057
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9060 });
9061 editor.update(cx, |editor, cx| {
9062 assert_text_with_selections(
9063 editor,
9064 indoc! {r#"
9065 use mod1::mod2::{mod3, «mod4ˇ»};
9066
9067 fn fn_1«ˇ(param1: bool, param2: &str)» {
9068 let var1 = "«ˇtext»";
9069 }
9070 "#},
9071 cx,
9072 );
9073 });
9074
9075 editor.update_in(cx, |editor, window, cx| {
9076 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9077 });
9078 editor.update(cx, |editor, cx| {
9079 assert_text_with_selections(
9080 editor,
9081 indoc! {r#"
9082 use mod1::mod2::{mod3, moˇd4};
9083
9084 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9085 let var1 = "teˇxt";
9086 }
9087 "#},
9088 cx,
9089 );
9090 });
9091
9092 // Trying to shrink the selected syntax node one more time has no effect.
9093 editor.update_in(cx, |editor, window, cx| {
9094 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9095 });
9096 editor.update_in(cx, |editor, _, cx| {
9097 assert_text_with_selections(
9098 editor,
9099 indoc! {r#"
9100 use mod1::mod2::{mod3, moˇd4};
9101
9102 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9103 let var1 = "teˇxt";
9104 }
9105 "#},
9106 cx,
9107 );
9108 });
9109
9110 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9111 // a fold.
9112 editor.update_in(cx, |editor, window, cx| {
9113 editor.fold_creases(
9114 vec![
9115 Crease::simple(
9116 Point::new(0, 21)..Point::new(0, 24),
9117 FoldPlaceholder::test(),
9118 ),
9119 Crease::simple(
9120 Point::new(3, 20)..Point::new(3, 22),
9121 FoldPlaceholder::test(),
9122 ),
9123 ],
9124 true,
9125 window,
9126 cx,
9127 );
9128 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9129 });
9130 editor.update(cx, |editor, cx| {
9131 assert_text_with_selections(
9132 editor,
9133 indoc! {r#"
9134 use mod1::mod2::«{mod3, mod4}ˇ»;
9135
9136 fn fn_1«ˇ(param1: bool, param2: &str)» {
9137 let var1 = "«ˇtext»";
9138 }
9139 "#},
9140 cx,
9141 );
9142 });
9143}
9144
9145#[gpui::test]
9146async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9147 init_test(cx, |_| {});
9148
9149 let language = Arc::new(Language::new(
9150 LanguageConfig::default(),
9151 Some(tree_sitter_rust::LANGUAGE.into()),
9152 ));
9153
9154 let text = "let a = 2;";
9155
9156 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9157 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9158 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9159
9160 editor
9161 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9162 .await;
9163
9164 // Test case 1: Cursor at end of word
9165 editor.update_in(cx, |editor, window, cx| {
9166 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9167 s.select_display_ranges([
9168 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9169 ]);
9170 });
9171 });
9172 editor.update(cx, |editor, cx| {
9173 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9174 });
9175 editor.update_in(cx, |editor, window, cx| {
9176 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9177 });
9178 editor.update(cx, |editor, cx| {
9179 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9180 });
9181 editor.update_in(cx, |editor, window, cx| {
9182 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9183 });
9184 editor.update(cx, |editor, cx| {
9185 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9186 });
9187
9188 // Test case 2: Cursor at end of statement
9189 editor.update_in(cx, |editor, window, cx| {
9190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9191 s.select_display_ranges([
9192 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9193 ]);
9194 });
9195 });
9196 editor.update(cx, |editor, cx| {
9197 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9198 });
9199 editor.update_in(cx, |editor, window, cx| {
9200 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9201 });
9202 editor.update(cx, |editor, cx| {
9203 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9204 });
9205}
9206
9207#[gpui::test]
9208async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9209 init_test(cx, |_| {});
9210
9211 let language = Arc::new(Language::new(
9212 LanguageConfig {
9213 name: "JavaScript".into(),
9214 ..Default::default()
9215 },
9216 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9217 ));
9218
9219 let text = r#"
9220 let a = {
9221 key: "value",
9222 };
9223 "#
9224 .unindent();
9225
9226 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9227 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9228 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9229
9230 editor
9231 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9232 .await;
9233
9234 // Test case 1: Cursor after '{'
9235 editor.update_in(cx, |editor, window, cx| {
9236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9237 s.select_display_ranges([
9238 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9239 ]);
9240 });
9241 });
9242 editor.update(cx, |editor, cx| {
9243 assert_text_with_selections(
9244 editor,
9245 indoc! {r#"
9246 let a = {ˇ
9247 key: "value",
9248 };
9249 "#},
9250 cx,
9251 );
9252 });
9253 editor.update_in(cx, |editor, window, cx| {
9254 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9255 });
9256 editor.update(cx, |editor, cx| {
9257 assert_text_with_selections(
9258 editor,
9259 indoc! {r#"
9260 let a = «ˇ{
9261 key: "value",
9262 }»;
9263 "#},
9264 cx,
9265 );
9266 });
9267
9268 // Test case 2: Cursor after ':'
9269 editor.update_in(cx, |editor, window, cx| {
9270 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9271 s.select_display_ranges([
9272 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9273 ]);
9274 });
9275 });
9276 editor.update(cx, |editor, cx| {
9277 assert_text_with_selections(
9278 editor,
9279 indoc! {r#"
9280 let a = {
9281 key:ˇ "value",
9282 };
9283 "#},
9284 cx,
9285 );
9286 });
9287 editor.update_in(cx, |editor, window, cx| {
9288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9289 });
9290 editor.update(cx, |editor, cx| {
9291 assert_text_with_selections(
9292 editor,
9293 indoc! {r#"
9294 let a = {
9295 «ˇkey: "value"»,
9296 };
9297 "#},
9298 cx,
9299 );
9300 });
9301 editor.update_in(cx, |editor, window, cx| {
9302 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9303 });
9304 editor.update(cx, |editor, cx| {
9305 assert_text_with_selections(
9306 editor,
9307 indoc! {r#"
9308 let a = «ˇ{
9309 key: "value",
9310 }»;
9311 "#},
9312 cx,
9313 );
9314 });
9315
9316 // Test case 3: Cursor after ','
9317 editor.update_in(cx, |editor, window, cx| {
9318 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9319 s.select_display_ranges([
9320 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9321 ]);
9322 });
9323 });
9324 editor.update(cx, |editor, cx| {
9325 assert_text_with_selections(
9326 editor,
9327 indoc! {r#"
9328 let a = {
9329 key: "value",ˇ
9330 };
9331 "#},
9332 cx,
9333 );
9334 });
9335 editor.update_in(cx, |editor, window, cx| {
9336 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9337 });
9338 editor.update(cx, |editor, cx| {
9339 assert_text_with_selections(
9340 editor,
9341 indoc! {r#"
9342 let a = «ˇ{
9343 key: "value",
9344 }»;
9345 "#},
9346 cx,
9347 );
9348 });
9349
9350 // Test case 4: Cursor after ';'
9351 editor.update_in(cx, |editor, window, cx| {
9352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9353 s.select_display_ranges([
9354 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9355 ]);
9356 });
9357 });
9358 editor.update(cx, |editor, cx| {
9359 assert_text_with_selections(
9360 editor,
9361 indoc! {r#"
9362 let a = {
9363 key: "value",
9364 };ˇ
9365 "#},
9366 cx,
9367 );
9368 });
9369 editor.update_in(cx, |editor, window, cx| {
9370 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9371 });
9372 editor.update(cx, |editor, cx| {
9373 assert_text_with_selections(
9374 editor,
9375 indoc! {r#"
9376 «ˇlet a = {
9377 key: "value",
9378 };
9379 »"#},
9380 cx,
9381 );
9382 });
9383}
9384
9385#[gpui::test]
9386async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9387 init_test(cx, |_| {});
9388
9389 let language = Arc::new(Language::new(
9390 LanguageConfig::default(),
9391 Some(tree_sitter_rust::LANGUAGE.into()),
9392 ));
9393
9394 let text = r#"
9395 use mod1::mod2::{mod3, mod4};
9396
9397 fn fn_1(param1: bool, param2: &str) {
9398 let var1 = "hello world";
9399 }
9400 "#
9401 .unindent();
9402
9403 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9404 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9405 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9406
9407 editor
9408 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9409 .await;
9410
9411 // Test 1: Cursor on a letter of a string word
9412 editor.update_in(cx, |editor, window, cx| {
9413 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9414 s.select_display_ranges([
9415 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9416 ]);
9417 });
9418 });
9419 editor.update_in(cx, |editor, window, cx| {
9420 assert_text_with_selections(
9421 editor,
9422 indoc! {r#"
9423 use mod1::mod2::{mod3, mod4};
9424
9425 fn fn_1(param1: bool, param2: &str) {
9426 let var1 = "hˇello world";
9427 }
9428 "#},
9429 cx,
9430 );
9431 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9432 assert_text_with_selections(
9433 editor,
9434 indoc! {r#"
9435 use mod1::mod2::{mod3, mod4};
9436
9437 fn fn_1(param1: bool, param2: &str) {
9438 let var1 = "«ˇhello» world";
9439 }
9440 "#},
9441 cx,
9442 );
9443 });
9444
9445 // Test 2: Partial selection within a word
9446 editor.update_in(cx, |editor, window, cx| {
9447 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9448 s.select_display_ranges([
9449 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9450 ]);
9451 });
9452 });
9453 editor.update_in(cx, |editor, window, cx| {
9454 assert_text_with_selections(
9455 editor,
9456 indoc! {r#"
9457 use mod1::mod2::{mod3, mod4};
9458
9459 fn fn_1(param1: bool, param2: &str) {
9460 let var1 = "h«elˇ»lo world";
9461 }
9462 "#},
9463 cx,
9464 );
9465 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9466 assert_text_with_selections(
9467 editor,
9468 indoc! {r#"
9469 use mod1::mod2::{mod3, mod4};
9470
9471 fn fn_1(param1: bool, param2: &str) {
9472 let var1 = "«ˇhello» world";
9473 }
9474 "#},
9475 cx,
9476 );
9477 });
9478
9479 // Test 3: Complete word already selected
9480 editor.update_in(cx, |editor, window, cx| {
9481 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9482 s.select_display_ranges([
9483 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9484 ]);
9485 });
9486 });
9487 editor.update_in(cx, |editor, window, cx| {
9488 assert_text_with_selections(
9489 editor,
9490 indoc! {r#"
9491 use mod1::mod2::{mod3, mod4};
9492
9493 fn fn_1(param1: bool, param2: &str) {
9494 let var1 = "«helloˇ» world";
9495 }
9496 "#},
9497 cx,
9498 );
9499 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9500 assert_text_with_selections(
9501 editor,
9502 indoc! {r#"
9503 use mod1::mod2::{mod3, mod4};
9504
9505 fn fn_1(param1: bool, param2: &str) {
9506 let var1 = "«hello worldˇ»";
9507 }
9508 "#},
9509 cx,
9510 );
9511 });
9512
9513 // Test 4: Selection spanning across words
9514 editor.update_in(cx, |editor, window, cx| {
9515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9516 s.select_display_ranges([
9517 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9518 ]);
9519 });
9520 });
9521 editor.update_in(cx, |editor, window, cx| {
9522 assert_text_with_selections(
9523 editor,
9524 indoc! {r#"
9525 use mod1::mod2::{mod3, mod4};
9526
9527 fn fn_1(param1: bool, param2: &str) {
9528 let var1 = "hel«lo woˇ»rld";
9529 }
9530 "#},
9531 cx,
9532 );
9533 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9534 assert_text_with_selections(
9535 editor,
9536 indoc! {r#"
9537 use mod1::mod2::{mod3, mod4};
9538
9539 fn fn_1(param1: bool, param2: &str) {
9540 let var1 = "«ˇhello world»";
9541 }
9542 "#},
9543 cx,
9544 );
9545 });
9546
9547 // Test 5: Expansion beyond string
9548 editor.update_in(cx, |editor, window, cx| {
9549 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9550 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9551 assert_text_with_selections(
9552 editor,
9553 indoc! {r#"
9554 use mod1::mod2::{mod3, mod4};
9555
9556 fn fn_1(param1: bool, param2: &str) {
9557 «ˇlet var1 = "hello world";»
9558 }
9559 "#},
9560 cx,
9561 );
9562 });
9563}
9564
9565#[gpui::test]
9566async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9567 init_test(cx, |_| {});
9568
9569 let mut cx = EditorTestContext::new(cx).await;
9570
9571 let language = Arc::new(Language::new(
9572 LanguageConfig::default(),
9573 Some(tree_sitter_rust::LANGUAGE.into()),
9574 ));
9575
9576 cx.update_buffer(|buffer, cx| {
9577 buffer.set_language(Some(language), cx);
9578 });
9579
9580 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9581 cx.update_editor(|editor, window, cx| {
9582 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9583 });
9584
9585 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9586
9587 cx.set_state(indoc! { r#"fn a() {
9588 // what
9589 // a
9590 // ˇlong
9591 // method
9592 // I
9593 // sure
9594 // hope
9595 // it
9596 // works
9597 }"# });
9598
9599 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9600 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9601 cx.update(|_, cx| {
9602 multi_buffer.update(cx, |multi_buffer, cx| {
9603 multi_buffer.set_excerpts_for_path(
9604 PathKey::for_buffer(&buffer, cx),
9605 buffer,
9606 [Point::new(1, 0)..Point::new(1, 0)],
9607 3,
9608 cx,
9609 );
9610 });
9611 });
9612
9613 let editor2 = cx.new_window_entity(|window, cx| {
9614 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9615 });
9616
9617 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9618 cx.update_editor(|editor, window, cx| {
9619 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9620 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9621 })
9622 });
9623
9624 cx.assert_editor_state(indoc! { "
9625 fn a() {
9626 // what
9627 // a
9628 ˇ // long
9629 // method"});
9630
9631 cx.update_editor(|editor, window, cx| {
9632 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9633 });
9634
9635 // Although we could potentially make the action work when the syntax node
9636 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9637 // did. Maybe we could also expand the excerpt to contain the range?
9638 cx.assert_editor_state(indoc! { "
9639 fn a() {
9640 // what
9641 // a
9642 ˇ // long
9643 // method"});
9644}
9645
9646#[gpui::test]
9647async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9648 init_test(cx, |_| {});
9649
9650 let base_text = r#"
9651 impl A {
9652 // this is an uncommitted comment
9653
9654 fn b() {
9655 c();
9656 }
9657
9658 // this is another uncommitted comment
9659
9660 fn d() {
9661 // e
9662 // f
9663 }
9664 }
9665
9666 fn g() {
9667 // h
9668 }
9669 "#
9670 .unindent();
9671
9672 let text = r#"
9673 ˇimpl A {
9674
9675 fn b() {
9676 c();
9677 }
9678
9679 fn d() {
9680 // e
9681 // f
9682 }
9683 }
9684
9685 fn g() {
9686 // h
9687 }
9688 "#
9689 .unindent();
9690
9691 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9692 cx.set_state(&text);
9693 cx.set_head_text(&base_text);
9694 cx.update_editor(|editor, window, cx| {
9695 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9696 });
9697
9698 cx.assert_state_with_diff(
9699 "
9700 ˇimpl A {
9701 - // this is an uncommitted comment
9702
9703 fn b() {
9704 c();
9705 }
9706
9707 - // this is another uncommitted comment
9708 -
9709 fn d() {
9710 // e
9711 // f
9712 }
9713 }
9714
9715 fn g() {
9716 // h
9717 }
9718 "
9719 .unindent(),
9720 );
9721
9722 let expected_display_text = "
9723 impl A {
9724 // this is an uncommitted comment
9725
9726 fn b() {
9727 ⋯
9728 }
9729
9730 // this is another uncommitted comment
9731
9732 fn d() {
9733 ⋯
9734 }
9735 }
9736
9737 fn g() {
9738 ⋯
9739 }
9740 "
9741 .unindent();
9742
9743 cx.update_editor(|editor, window, cx| {
9744 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9745 assert_eq!(editor.display_text(cx), expected_display_text);
9746 });
9747}
9748
9749#[gpui::test]
9750async fn test_autoindent(cx: &mut TestAppContext) {
9751 init_test(cx, |_| {});
9752
9753 let language = Arc::new(
9754 Language::new(
9755 LanguageConfig {
9756 brackets: BracketPairConfig {
9757 pairs: vec![
9758 BracketPair {
9759 start: "{".to_string(),
9760 end: "}".to_string(),
9761 close: false,
9762 surround: false,
9763 newline: true,
9764 },
9765 BracketPair {
9766 start: "(".to_string(),
9767 end: ")".to_string(),
9768 close: false,
9769 surround: false,
9770 newline: true,
9771 },
9772 ],
9773 ..Default::default()
9774 },
9775 ..Default::default()
9776 },
9777 Some(tree_sitter_rust::LANGUAGE.into()),
9778 )
9779 .with_indents_query(
9780 r#"
9781 (_ "(" ")" @end) @indent
9782 (_ "{" "}" @end) @indent
9783 "#,
9784 )
9785 .unwrap(),
9786 );
9787
9788 let text = "fn a() {}";
9789
9790 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9791 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9792 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9793 editor
9794 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9795 .await;
9796
9797 editor.update_in(cx, |editor, window, cx| {
9798 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9799 s.select_ranges([
9800 MultiBufferOffset(5)..MultiBufferOffset(5),
9801 MultiBufferOffset(8)..MultiBufferOffset(8),
9802 MultiBufferOffset(9)..MultiBufferOffset(9),
9803 ])
9804 });
9805 editor.newline(&Newline, window, cx);
9806 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9807 assert_eq!(
9808 editor.selections.ranges(&editor.display_snapshot(cx)),
9809 &[
9810 Point::new(1, 4)..Point::new(1, 4),
9811 Point::new(3, 4)..Point::new(3, 4),
9812 Point::new(5, 0)..Point::new(5, 0)
9813 ]
9814 );
9815 });
9816}
9817
9818#[gpui::test]
9819async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9820 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9821
9822 let language = Arc::new(
9823 Language::new(
9824 LanguageConfig {
9825 brackets: BracketPairConfig {
9826 pairs: vec![
9827 BracketPair {
9828 start: "{".to_string(),
9829 end: "}".to_string(),
9830 close: false,
9831 surround: false,
9832 newline: true,
9833 },
9834 BracketPair {
9835 start: "(".to_string(),
9836 end: ")".to_string(),
9837 close: false,
9838 surround: false,
9839 newline: true,
9840 },
9841 ],
9842 ..Default::default()
9843 },
9844 ..Default::default()
9845 },
9846 Some(tree_sitter_rust::LANGUAGE.into()),
9847 )
9848 .with_indents_query(
9849 r#"
9850 (_ "(" ")" @end) @indent
9851 (_ "{" "}" @end) @indent
9852 "#,
9853 )
9854 .unwrap(),
9855 );
9856
9857 let text = "fn a() {}";
9858
9859 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9860 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9861 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9862 editor
9863 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9864 .await;
9865
9866 editor.update_in(cx, |editor, window, cx| {
9867 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9868 s.select_ranges([
9869 MultiBufferOffset(5)..MultiBufferOffset(5),
9870 MultiBufferOffset(8)..MultiBufferOffset(8),
9871 MultiBufferOffset(9)..MultiBufferOffset(9),
9872 ])
9873 });
9874 editor.newline(&Newline, window, cx);
9875 assert_eq!(
9876 editor.text(cx),
9877 indoc!(
9878 "
9879 fn a(
9880
9881 ) {
9882
9883 }
9884 "
9885 )
9886 );
9887 assert_eq!(
9888 editor.selections.ranges(&editor.display_snapshot(cx)),
9889 &[
9890 Point::new(1, 0)..Point::new(1, 0),
9891 Point::new(3, 0)..Point::new(3, 0),
9892 Point::new(5, 0)..Point::new(5, 0)
9893 ]
9894 );
9895 });
9896}
9897
9898#[gpui::test]
9899async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9900 init_test(cx, |settings| {
9901 settings.defaults.auto_indent = Some(true);
9902 settings.languages.0.insert(
9903 "python".into(),
9904 LanguageSettingsContent {
9905 auto_indent: Some(false),
9906 ..Default::default()
9907 },
9908 );
9909 });
9910
9911 let mut cx = EditorTestContext::new(cx).await;
9912
9913 let injected_language = Arc::new(
9914 Language::new(
9915 LanguageConfig {
9916 brackets: BracketPairConfig {
9917 pairs: vec![
9918 BracketPair {
9919 start: "{".to_string(),
9920 end: "}".to_string(),
9921 close: false,
9922 surround: false,
9923 newline: true,
9924 },
9925 BracketPair {
9926 start: "(".to_string(),
9927 end: ")".to_string(),
9928 close: true,
9929 surround: false,
9930 newline: true,
9931 },
9932 ],
9933 ..Default::default()
9934 },
9935 name: "python".into(),
9936 ..Default::default()
9937 },
9938 Some(tree_sitter_python::LANGUAGE.into()),
9939 )
9940 .with_indents_query(
9941 r#"
9942 (_ "(" ")" @end) @indent
9943 (_ "{" "}" @end) @indent
9944 "#,
9945 )
9946 .unwrap(),
9947 );
9948
9949 let language = Arc::new(
9950 Language::new(
9951 LanguageConfig {
9952 brackets: BracketPairConfig {
9953 pairs: vec![
9954 BracketPair {
9955 start: "{".to_string(),
9956 end: "}".to_string(),
9957 close: false,
9958 surround: false,
9959 newline: true,
9960 },
9961 BracketPair {
9962 start: "(".to_string(),
9963 end: ")".to_string(),
9964 close: true,
9965 surround: false,
9966 newline: true,
9967 },
9968 ],
9969 ..Default::default()
9970 },
9971 name: LanguageName::new("rust"),
9972 ..Default::default()
9973 },
9974 Some(tree_sitter_rust::LANGUAGE.into()),
9975 )
9976 .with_indents_query(
9977 r#"
9978 (_ "(" ")" @end) @indent
9979 (_ "{" "}" @end) @indent
9980 "#,
9981 )
9982 .unwrap()
9983 .with_injection_query(
9984 r#"
9985 (macro_invocation
9986 macro: (identifier) @_macro_name
9987 (token_tree) @injection.content
9988 (#set! injection.language "python"))
9989 "#,
9990 )
9991 .unwrap(),
9992 );
9993
9994 cx.language_registry().add(injected_language);
9995 cx.language_registry().add(language.clone());
9996
9997 cx.update_buffer(|buffer, cx| {
9998 buffer.set_language(Some(language), cx);
9999 });
10000
10001 cx.set_state(r#"struct A {ˇ}"#);
10002
10003 cx.update_editor(|editor, window, cx| {
10004 editor.newline(&Default::default(), window, cx);
10005 });
10006
10007 cx.assert_editor_state(indoc!(
10008 "struct A {
10009 ˇ
10010 }"
10011 ));
10012
10013 cx.set_state(r#"select_biased!(ˇ)"#);
10014
10015 cx.update_editor(|editor, window, cx| {
10016 editor.newline(&Default::default(), window, cx);
10017 editor.handle_input("def ", window, cx);
10018 editor.handle_input("(", window, cx);
10019 editor.newline(&Default::default(), window, cx);
10020 editor.handle_input("a", window, cx);
10021 });
10022
10023 cx.assert_editor_state(indoc!(
10024 "select_biased!(
10025 def (
10026 aˇ
10027 )
10028 )"
10029 ));
10030}
10031
10032#[gpui::test]
10033async fn test_autoindent_selections(cx: &mut TestAppContext) {
10034 init_test(cx, |_| {});
10035
10036 {
10037 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10038 cx.set_state(indoc! {"
10039 impl A {
10040
10041 fn b() {}
10042
10043 «fn c() {
10044
10045 }ˇ»
10046 }
10047 "});
10048
10049 cx.update_editor(|editor, window, cx| {
10050 editor.autoindent(&Default::default(), window, cx);
10051 });
10052
10053 cx.assert_editor_state(indoc! {"
10054 impl A {
10055
10056 fn b() {}
10057
10058 «fn c() {
10059
10060 }ˇ»
10061 }
10062 "});
10063 }
10064
10065 {
10066 let mut cx = EditorTestContext::new_multibuffer(
10067 cx,
10068 [indoc! { "
10069 impl A {
10070 «
10071 // a
10072 fn b(){}
10073 »
10074 «
10075 }
10076 fn c(){}
10077 »
10078 "}],
10079 );
10080
10081 let buffer = cx.update_editor(|editor, _, cx| {
10082 let buffer = editor.buffer().update(cx, |buffer, _| {
10083 buffer.all_buffers().iter().next().unwrap().clone()
10084 });
10085 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10086 buffer
10087 });
10088
10089 cx.run_until_parked();
10090 cx.update_editor(|editor, window, cx| {
10091 editor.select_all(&Default::default(), window, cx);
10092 editor.autoindent(&Default::default(), window, cx)
10093 });
10094 cx.run_until_parked();
10095
10096 cx.update(|_, cx| {
10097 assert_eq!(
10098 buffer.read(cx).text(),
10099 indoc! { "
10100 impl A {
10101
10102 // a
10103 fn b(){}
10104
10105
10106 }
10107 fn c(){}
10108
10109 " }
10110 )
10111 });
10112 }
10113}
10114
10115#[gpui::test]
10116async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10117 init_test(cx, |_| {});
10118
10119 let mut cx = EditorTestContext::new(cx).await;
10120
10121 let language = Arc::new(Language::new(
10122 LanguageConfig {
10123 brackets: BracketPairConfig {
10124 pairs: vec![
10125 BracketPair {
10126 start: "{".to_string(),
10127 end: "}".to_string(),
10128 close: true,
10129 surround: true,
10130 newline: true,
10131 },
10132 BracketPair {
10133 start: "(".to_string(),
10134 end: ")".to_string(),
10135 close: true,
10136 surround: true,
10137 newline: true,
10138 },
10139 BracketPair {
10140 start: "/*".to_string(),
10141 end: " */".to_string(),
10142 close: true,
10143 surround: true,
10144 newline: true,
10145 },
10146 BracketPair {
10147 start: "[".to_string(),
10148 end: "]".to_string(),
10149 close: false,
10150 surround: false,
10151 newline: true,
10152 },
10153 BracketPair {
10154 start: "\"".to_string(),
10155 end: "\"".to_string(),
10156 close: true,
10157 surround: true,
10158 newline: false,
10159 },
10160 BracketPair {
10161 start: "<".to_string(),
10162 end: ">".to_string(),
10163 close: false,
10164 surround: true,
10165 newline: true,
10166 },
10167 ],
10168 ..Default::default()
10169 },
10170 autoclose_before: "})]".to_string(),
10171 ..Default::default()
10172 },
10173 Some(tree_sitter_rust::LANGUAGE.into()),
10174 ));
10175
10176 cx.language_registry().add(language.clone());
10177 cx.update_buffer(|buffer, cx| {
10178 buffer.set_language(Some(language), cx);
10179 });
10180
10181 cx.set_state(
10182 &r#"
10183 🏀ˇ
10184 εˇ
10185 ❤️ˇ
10186 "#
10187 .unindent(),
10188 );
10189
10190 // autoclose multiple nested brackets at multiple cursors
10191 cx.update_editor(|editor, window, cx| {
10192 editor.handle_input("{", window, cx);
10193 editor.handle_input("{", window, cx);
10194 editor.handle_input("{", window, cx);
10195 });
10196 cx.assert_editor_state(
10197 &"
10198 🏀{{{ˇ}}}
10199 ε{{{ˇ}}}
10200 ❤️{{{ˇ}}}
10201 "
10202 .unindent(),
10203 );
10204
10205 // insert a different closing bracket
10206 cx.update_editor(|editor, window, cx| {
10207 editor.handle_input(")", window, cx);
10208 });
10209 cx.assert_editor_state(
10210 &"
10211 🏀{{{)ˇ}}}
10212 ε{{{)ˇ}}}
10213 ❤️{{{)ˇ}}}
10214 "
10215 .unindent(),
10216 );
10217
10218 // skip over the auto-closed brackets when typing a closing bracket
10219 cx.update_editor(|editor, window, cx| {
10220 editor.move_right(&MoveRight, window, cx);
10221 editor.handle_input("}", window, cx);
10222 editor.handle_input("}", window, cx);
10223 editor.handle_input("}", window, cx);
10224 });
10225 cx.assert_editor_state(
10226 &"
10227 🏀{{{)}}}}ˇ
10228 ε{{{)}}}}ˇ
10229 ❤️{{{)}}}}ˇ
10230 "
10231 .unindent(),
10232 );
10233
10234 // autoclose multi-character pairs
10235 cx.set_state(
10236 &"
10237 ˇ
10238 ˇ
10239 "
10240 .unindent(),
10241 );
10242 cx.update_editor(|editor, window, cx| {
10243 editor.handle_input("/", window, cx);
10244 editor.handle_input("*", window, cx);
10245 });
10246 cx.assert_editor_state(
10247 &"
10248 /*ˇ */
10249 /*ˇ */
10250 "
10251 .unindent(),
10252 );
10253
10254 // one cursor autocloses a multi-character pair, one cursor
10255 // does not autoclose.
10256 cx.set_state(
10257 &"
10258 /ˇ
10259 ˇ
10260 "
10261 .unindent(),
10262 );
10263 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10264 cx.assert_editor_state(
10265 &"
10266 /*ˇ */
10267 *ˇ
10268 "
10269 .unindent(),
10270 );
10271
10272 // Don't autoclose if the next character isn't whitespace and isn't
10273 // listed in the language's "autoclose_before" section.
10274 cx.set_state("ˇa b");
10275 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10276 cx.assert_editor_state("{ˇa b");
10277
10278 // Don't autoclose if `close` is false for the bracket pair
10279 cx.set_state("ˇ");
10280 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10281 cx.assert_editor_state("[ˇ");
10282
10283 // Surround with brackets if text is selected
10284 cx.set_state("«aˇ» b");
10285 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10286 cx.assert_editor_state("{«aˇ»} b");
10287
10288 // Autoclose when not immediately after a word character
10289 cx.set_state("a ˇ");
10290 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10291 cx.assert_editor_state("a \"ˇ\"");
10292
10293 // Autoclose pair where the start and end characters are the same
10294 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10295 cx.assert_editor_state("a \"\"ˇ");
10296
10297 // Don't autoclose when immediately after a word character
10298 cx.set_state("aˇ");
10299 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10300 cx.assert_editor_state("a\"ˇ");
10301
10302 // Do autoclose when after a non-word character
10303 cx.set_state("{ˇ");
10304 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10305 cx.assert_editor_state("{\"ˇ\"");
10306
10307 // Non identical pairs autoclose regardless of preceding character
10308 cx.set_state("aˇ");
10309 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10310 cx.assert_editor_state("a{ˇ}");
10311
10312 // Don't autoclose pair if autoclose is disabled
10313 cx.set_state("ˇ");
10314 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10315 cx.assert_editor_state("<ˇ");
10316
10317 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10318 cx.set_state("«aˇ» b");
10319 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10320 cx.assert_editor_state("<«aˇ»> b");
10321}
10322
10323#[gpui::test]
10324async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10325 init_test(cx, |settings| {
10326 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10327 });
10328
10329 let mut cx = EditorTestContext::new(cx).await;
10330
10331 let language = Arc::new(Language::new(
10332 LanguageConfig {
10333 brackets: BracketPairConfig {
10334 pairs: vec![
10335 BracketPair {
10336 start: "{".to_string(),
10337 end: "}".to_string(),
10338 close: true,
10339 surround: true,
10340 newline: true,
10341 },
10342 BracketPair {
10343 start: "(".to_string(),
10344 end: ")".to_string(),
10345 close: true,
10346 surround: true,
10347 newline: true,
10348 },
10349 BracketPair {
10350 start: "[".to_string(),
10351 end: "]".to_string(),
10352 close: false,
10353 surround: false,
10354 newline: true,
10355 },
10356 ],
10357 ..Default::default()
10358 },
10359 autoclose_before: "})]".to_string(),
10360 ..Default::default()
10361 },
10362 Some(tree_sitter_rust::LANGUAGE.into()),
10363 ));
10364
10365 cx.language_registry().add(language.clone());
10366 cx.update_buffer(|buffer, cx| {
10367 buffer.set_language(Some(language), cx);
10368 });
10369
10370 cx.set_state(
10371 &"
10372 ˇ
10373 ˇ
10374 ˇ
10375 "
10376 .unindent(),
10377 );
10378
10379 // ensure only matching closing brackets are skipped over
10380 cx.update_editor(|editor, window, cx| {
10381 editor.handle_input("}", window, cx);
10382 editor.move_left(&MoveLeft, window, cx);
10383 editor.handle_input(")", window, cx);
10384 editor.move_left(&MoveLeft, window, cx);
10385 });
10386 cx.assert_editor_state(
10387 &"
10388 ˇ)}
10389 ˇ)}
10390 ˇ)}
10391 "
10392 .unindent(),
10393 );
10394
10395 // skip-over closing brackets at multiple cursors
10396 cx.update_editor(|editor, window, cx| {
10397 editor.handle_input(")", window, cx);
10398 editor.handle_input("}", window, cx);
10399 });
10400 cx.assert_editor_state(
10401 &"
10402 )}ˇ
10403 )}ˇ
10404 )}ˇ
10405 "
10406 .unindent(),
10407 );
10408
10409 // ignore non-close brackets
10410 cx.update_editor(|editor, window, cx| {
10411 editor.handle_input("]", window, cx);
10412 editor.move_left(&MoveLeft, window, cx);
10413 editor.handle_input("]", window, cx);
10414 });
10415 cx.assert_editor_state(
10416 &"
10417 )}]ˇ]
10418 )}]ˇ]
10419 )}]ˇ]
10420 "
10421 .unindent(),
10422 );
10423}
10424
10425#[gpui::test]
10426async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10427 init_test(cx, |_| {});
10428
10429 let mut cx = EditorTestContext::new(cx).await;
10430
10431 let html_language = Arc::new(
10432 Language::new(
10433 LanguageConfig {
10434 name: "HTML".into(),
10435 brackets: BracketPairConfig {
10436 pairs: vec![
10437 BracketPair {
10438 start: "<".into(),
10439 end: ">".into(),
10440 close: true,
10441 ..Default::default()
10442 },
10443 BracketPair {
10444 start: "{".into(),
10445 end: "}".into(),
10446 close: true,
10447 ..Default::default()
10448 },
10449 BracketPair {
10450 start: "(".into(),
10451 end: ")".into(),
10452 close: true,
10453 ..Default::default()
10454 },
10455 ],
10456 ..Default::default()
10457 },
10458 autoclose_before: "})]>".into(),
10459 ..Default::default()
10460 },
10461 Some(tree_sitter_html::LANGUAGE.into()),
10462 )
10463 .with_injection_query(
10464 r#"
10465 (script_element
10466 (raw_text) @injection.content
10467 (#set! injection.language "javascript"))
10468 "#,
10469 )
10470 .unwrap(),
10471 );
10472
10473 let javascript_language = Arc::new(Language::new(
10474 LanguageConfig {
10475 name: "JavaScript".into(),
10476 brackets: BracketPairConfig {
10477 pairs: vec![
10478 BracketPair {
10479 start: "/*".into(),
10480 end: " */".into(),
10481 close: true,
10482 ..Default::default()
10483 },
10484 BracketPair {
10485 start: "{".into(),
10486 end: "}".into(),
10487 close: true,
10488 ..Default::default()
10489 },
10490 BracketPair {
10491 start: "(".into(),
10492 end: ")".into(),
10493 close: true,
10494 ..Default::default()
10495 },
10496 ],
10497 ..Default::default()
10498 },
10499 autoclose_before: "})]>".into(),
10500 ..Default::default()
10501 },
10502 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10503 ));
10504
10505 cx.language_registry().add(html_language.clone());
10506 cx.language_registry().add(javascript_language);
10507 cx.executor().run_until_parked();
10508
10509 cx.update_buffer(|buffer, cx| {
10510 buffer.set_language(Some(html_language), cx);
10511 });
10512
10513 cx.set_state(
10514 &r#"
10515 <body>ˇ
10516 <script>
10517 var x = 1;ˇ
10518 </script>
10519 </body>ˇ
10520 "#
10521 .unindent(),
10522 );
10523
10524 // Precondition: different languages are active at different locations.
10525 cx.update_editor(|editor, window, cx| {
10526 let snapshot = editor.snapshot(window, cx);
10527 let cursors = editor
10528 .selections
10529 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10530 let languages = cursors
10531 .iter()
10532 .map(|c| snapshot.language_at(c.start).unwrap().name())
10533 .collect::<Vec<_>>();
10534 assert_eq!(
10535 languages,
10536 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10537 );
10538 });
10539
10540 // Angle brackets autoclose in HTML, but not JavaScript.
10541 cx.update_editor(|editor, window, cx| {
10542 editor.handle_input("<", window, cx);
10543 editor.handle_input("a", window, cx);
10544 });
10545 cx.assert_editor_state(
10546 &r#"
10547 <body><aˇ>
10548 <script>
10549 var x = 1;<aˇ
10550 </script>
10551 </body><aˇ>
10552 "#
10553 .unindent(),
10554 );
10555
10556 // Curly braces and parens autoclose in both HTML and JavaScript.
10557 cx.update_editor(|editor, window, cx| {
10558 editor.handle_input(" b=", window, cx);
10559 editor.handle_input("{", window, cx);
10560 editor.handle_input("c", window, cx);
10561 editor.handle_input("(", window, cx);
10562 });
10563 cx.assert_editor_state(
10564 &r#"
10565 <body><a b={c(ˇ)}>
10566 <script>
10567 var x = 1;<a b={c(ˇ)}
10568 </script>
10569 </body><a b={c(ˇ)}>
10570 "#
10571 .unindent(),
10572 );
10573
10574 // Brackets that were already autoclosed are skipped.
10575 cx.update_editor(|editor, window, cx| {
10576 editor.handle_input(")", window, cx);
10577 editor.handle_input("d", window, cx);
10578 editor.handle_input("}", window, cx);
10579 });
10580 cx.assert_editor_state(
10581 &r#"
10582 <body><a b={c()d}ˇ>
10583 <script>
10584 var x = 1;<a b={c()d}ˇ
10585 </script>
10586 </body><a b={c()d}ˇ>
10587 "#
10588 .unindent(),
10589 );
10590 cx.update_editor(|editor, window, cx| {
10591 editor.handle_input(">", window, cx);
10592 });
10593 cx.assert_editor_state(
10594 &r#"
10595 <body><a b={c()d}>ˇ
10596 <script>
10597 var x = 1;<a b={c()d}>ˇ
10598 </script>
10599 </body><a b={c()d}>ˇ
10600 "#
10601 .unindent(),
10602 );
10603
10604 // Reset
10605 cx.set_state(
10606 &r#"
10607 <body>ˇ
10608 <script>
10609 var x = 1;ˇ
10610 </script>
10611 </body>ˇ
10612 "#
10613 .unindent(),
10614 );
10615
10616 cx.update_editor(|editor, window, cx| {
10617 editor.handle_input("<", window, cx);
10618 });
10619 cx.assert_editor_state(
10620 &r#"
10621 <body><ˇ>
10622 <script>
10623 var x = 1;<ˇ
10624 </script>
10625 </body><ˇ>
10626 "#
10627 .unindent(),
10628 );
10629
10630 // When backspacing, the closing angle brackets are removed.
10631 cx.update_editor(|editor, window, cx| {
10632 editor.backspace(&Backspace, window, cx);
10633 });
10634 cx.assert_editor_state(
10635 &r#"
10636 <body>ˇ
10637 <script>
10638 var x = 1;ˇ
10639 </script>
10640 </body>ˇ
10641 "#
10642 .unindent(),
10643 );
10644
10645 // Block comments autoclose in JavaScript, but not HTML.
10646 cx.update_editor(|editor, window, cx| {
10647 editor.handle_input("/", window, cx);
10648 editor.handle_input("*", window, cx);
10649 });
10650 cx.assert_editor_state(
10651 &r#"
10652 <body>/*ˇ
10653 <script>
10654 var x = 1;/*ˇ */
10655 </script>
10656 </body>/*ˇ
10657 "#
10658 .unindent(),
10659 );
10660}
10661
10662#[gpui::test]
10663async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10664 init_test(cx, |_| {});
10665
10666 let mut cx = EditorTestContext::new(cx).await;
10667
10668 let rust_language = Arc::new(
10669 Language::new(
10670 LanguageConfig {
10671 name: "Rust".into(),
10672 brackets: serde_json::from_value(json!([
10673 { "start": "{", "end": "}", "close": true, "newline": true },
10674 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10675 ]))
10676 .unwrap(),
10677 autoclose_before: "})]>".into(),
10678 ..Default::default()
10679 },
10680 Some(tree_sitter_rust::LANGUAGE.into()),
10681 )
10682 .with_override_query("(string_literal) @string")
10683 .unwrap(),
10684 );
10685
10686 cx.language_registry().add(rust_language.clone());
10687 cx.update_buffer(|buffer, cx| {
10688 buffer.set_language(Some(rust_language), cx);
10689 });
10690
10691 cx.set_state(
10692 &r#"
10693 let x = ˇ
10694 "#
10695 .unindent(),
10696 );
10697
10698 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10699 cx.update_editor(|editor, window, cx| {
10700 editor.handle_input("\"", window, cx);
10701 });
10702 cx.assert_editor_state(
10703 &r#"
10704 let x = "ˇ"
10705 "#
10706 .unindent(),
10707 );
10708
10709 // Inserting another quotation mark. The cursor moves across the existing
10710 // automatically-inserted quotation mark.
10711 cx.update_editor(|editor, window, cx| {
10712 editor.handle_input("\"", window, cx);
10713 });
10714 cx.assert_editor_state(
10715 &r#"
10716 let x = ""ˇ
10717 "#
10718 .unindent(),
10719 );
10720
10721 // Reset
10722 cx.set_state(
10723 &r#"
10724 let x = ˇ
10725 "#
10726 .unindent(),
10727 );
10728
10729 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10730 cx.update_editor(|editor, window, cx| {
10731 editor.handle_input("\"", window, cx);
10732 editor.handle_input(" ", window, cx);
10733 editor.move_left(&Default::default(), window, cx);
10734 editor.handle_input("\\", window, cx);
10735 editor.handle_input("\"", window, cx);
10736 });
10737 cx.assert_editor_state(
10738 &r#"
10739 let x = "\"ˇ "
10740 "#
10741 .unindent(),
10742 );
10743
10744 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10745 // mark. Nothing is inserted.
10746 cx.update_editor(|editor, window, cx| {
10747 editor.move_right(&Default::default(), window, cx);
10748 editor.handle_input("\"", window, cx);
10749 });
10750 cx.assert_editor_state(
10751 &r#"
10752 let x = "\" "ˇ
10753 "#
10754 .unindent(),
10755 );
10756}
10757
10758#[gpui::test]
10759async fn test_surround_with_pair(cx: &mut TestAppContext) {
10760 init_test(cx, |_| {});
10761
10762 let language = Arc::new(Language::new(
10763 LanguageConfig {
10764 brackets: BracketPairConfig {
10765 pairs: vec![
10766 BracketPair {
10767 start: "{".to_string(),
10768 end: "}".to_string(),
10769 close: true,
10770 surround: true,
10771 newline: true,
10772 },
10773 BracketPair {
10774 start: "/* ".to_string(),
10775 end: "*/".to_string(),
10776 close: true,
10777 surround: true,
10778 ..Default::default()
10779 },
10780 ],
10781 ..Default::default()
10782 },
10783 ..Default::default()
10784 },
10785 Some(tree_sitter_rust::LANGUAGE.into()),
10786 ));
10787
10788 let text = r#"
10789 a
10790 b
10791 c
10792 "#
10793 .unindent();
10794
10795 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10796 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10797 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10798 editor
10799 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10800 .await;
10801
10802 editor.update_in(cx, |editor, window, cx| {
10803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10804 s.select_display_ranges([
10805 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10806 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10807 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10808 ])
10809 });
10810
10811 editor.handle_input("{", window, cx);
10812 editor.handle_input("{", window, cx);
10813 editor.handle_input("{", window, cx);
10814 assert_eq!(
10815 editor.text(cx),
10816 "
10817 {{{a}}}
10818 {{{b}}}
10819 {{{c}}}
10820 "
10821 .unindent()
10822 );
10823 assert_eq!(
10824 display_ranges(editor, cx),
10825 [
10826 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10827 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10828 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10829 ]
10830 );
10831
10832 editor.undo(&Undo, window, cx);
10833 editor.undo(&Undo, window, cx);
10834 editor.undo(&Undo, window, cx);
10835 assert_eq!(
10836 editor.text(cx),
10837 "
10838 a
10839 b
10840 c
10841 "
10842 .unindent()
10843 );
10844 assert_eq!(
10845 display_ranges(editor, cx),
10846 [
10847 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10848 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10849 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10850 ]
10851 );
10852
10853 // Ensure inserting the first character of a multi-byte bracket pair
10854 // doesn't surround the selections with the bracket.
10855 editor.handle_input("/", window, cx);
10856 assert_eq!(
10857 editor.text(cx),
10858 "
10859 /
10860 /
10861 /
10862 "
10863 .unindent()
10864 );
10865 assert_eq!(
10866 display_ranges(editor, cx),
10867 [
10868 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10869 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10870 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10871 ]
10872 );
10873
10874 editor.undo(&Undo, window, cx);
10875 assert_eq!(
10876 editor.text(cx),
10877 "
10878 a
10879 b
10880 c
10881 "
10882 .unindent()
10883 );
10884 assert_eq!(
10885 display_ranges(editor, cx),
10886 [
10887 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10888 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10889 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10890 ]
10891 );
10892
10893 // Ensure inserting the last character of a multi-byte bracket pair
10894 // doesn't surround the selections with the bracket.
10895 editor.handle_input("*", window, cx);
10896 assert_eq!(
10897 editor.text(cx),
10898 "
10899 *
10900 *
10901 *
10902 "
10903 .unindent()
10904 );
10905 assert_eq!(
10906 display_ranges(editor, cx),
10907 [
10908 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10909 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10910 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10911 ]
10912 );
10913 });
10914}
10915
10916#[gpui::test]
10917async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10918 init_test(cx, |_| {});
10919
10920 let language = Arc::new(Language::new(
10921 LanguageConfig {
10922 brackets: BracketPairConfig {
10923 pairs: vec![BracketPair {
10924 start: "{".to_string(),
10925 end: "}".to_string(),
10926 close: true,
10927 surround: true,
10928 newline: true,
10929 }],
10930 ..Default::default()
10931 },
10932 autoclose_before: "}".to_string(),
10933 ..Default::default()
10934 },
10935 Some(tree_sitter_rust::LANGUAGE.into()),
10936 ));
10937
10938 let text = r#"
10939 a
10940 b
10941 c
10942 "#
10943 .unindent();
10944
10945 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10946 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10947 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10948 editor
10949 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10950 .await;
10951
10952 editor.update_in(cx, |editor, window, cx| {
10953 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10954 s.select_ranges([
10955 Point::new(0, 1)..Point::new(0, 1),
10956 Point::new(1, 1)..Point::new(1, 1),
10957 Point::new(2, 1)..Point::new(2, 1),
10958 ])
10959 });
10960
10961 editor.handle_input("{", window, cx);
10962 editor.handle_input("{", window, cx);
10963 editor.handle_input("_", window, cx);
10964 assert_eq!(
10965 editor.text(cx),
10966 "
10967 a{{_}}
10968 b{{_}}
10969 c{{_}}
10970 "
10971 .unindent()
10972 );
10973 assert_eq!(
10974 editor
10975 .selections
10976 .ranges::<Point>(&editor.display_snapshot(cx)),
10977 [
10978 Point::new(0, 4)..Point::new(0, 4),
10979 Point::new(1, 4)..Point::new(1, 4),
10980 Point::new(2, 4)..Point::new(2, 4)
10981 ]
10982 );
10983
10984 editor.backspace(&Default::default(), window, cx);
10985 editor.backspace(&Default::default(), window, cx);
10986 assert_eq!(
10987 editor.text(cx),
10988 "
10989 a{}
10990 b{}
10991 c{}
10992 "
10993 .unindent()
10994 );
10995 assert_eq!(
10996 editor
10997 .selections
10998 .ranges::<Point>(&editor.display_snapshot(cx)),
10999 [
11000 Point::new(0, 2)..Point::new(0, 2),
11001 Point::new(1, 2)..Point::new(1, 2),
11002 Point::new(2, 2)..Point::new(2, 2)
11003 ]
11004 );
11005
11006 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11007 assert_eq!(
11008 editor.text(cx),
11009 "
11010 a
11011 b
11012 c
11013 "
11014 .unindent()
11015 );
11016 assert_eq!(
11017 editor
11018 .selections
11019 .ranges::<Point>(&editor.display_snapshot(cx)),
11020 [
11021 Point::new(0, 1)..Point::new(0, 1),
11022 Point::new(1, 1)..Point::new(1, 1),
11023 Point::new(2, 1)..Point::new(2, 1)
11024 ]
11025 );
11026 });
11027}
11028
11029#[gpui::test]
11030async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11031 init_test(cx, |settings| {
11032 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11033 });
11034
11035 let mut cx = EditorTestContext::new(cx).await;
11036
11037 let language = Arc::new(Language::new(
11038 LanguageConfig {
11039 brackets: BracketPairConfig {
11040 pairs: vec![
11041 BracketPair {
11042 start: "{".to_string(),
11043 end: "}".to_string(),
11044 close: true,
11045 surround: true,
11046 newline: true,
11047 },
11048 BracketPair {
11049 start: "(".to_string(),
11050 end: ")".to_string(),
11051 close: true,
11052 surround: true,
11053 newline: true,
11054 },
11055 BracketPair {
11056 start: "[".to_string(),
11057 end: "]".to_string(),
11058 close: false,
11059 surround: true,
11060 newline: true,
11061 },
11062 ],
11063 ..Default::default()
11064 },
11065 autoclose_before: "})]".to_string(),
11066 ..Default::default()
11067 },
11068 Some(tree_sitter_rust::LANGUAGE.into()),
11069 ));
11070
11071 cx.language_registry().add(language.clone());
11072 cx.update_buffer(|buffer, cx| {
11073 buffer.set_language(Some(language), cx);
11074 });
11075
11076 cx.set_state(
11077 &"
11078 {(ˇ)}
11079 [[ˇ]]
11080 {(ˇ)}
11081 "
11082 .unindent(),
11083 );
11084
11085 cx.update_editor(|editor, window, cx| {
11086 editor.backspace(&Default::default(), window, cx);
11087 editor.backspace(&Default::default(), window, cx);
11088 });
11089
11090 cx.assert_editor_state(
11091 &"
11092 ˇ
11093 ˇ]]
11094 ˇ
11095 "
11096 .unindent(),
11097 );
11098
11099 cx.update_editor(|editor, window, cx| {
11100 editor.handle_input("{", window, cx);
11101 editor.handle_input("{", window, cx);
11102 editor.move_right(&MoveRight, window, cx);
11103 editor.move_right(&MoveRight, window, cx);
11104 editor.move_left(&MoveLeft, window, cx);
11105 editor.move_left(&MoveLeft, window, cx);
11106 editor.backspace(&Default::default(), window, cx);
11107 });
11108
11109 cx.assert_editor_state(
11110 &"
11111 {ˇ}
11112 {ˇ}]]
11113 {ˇ}
11114 "
11115 .unindent(),
11116 );
11117
11118 cx.update_editor(|editor, window, cx| {
11119 editor.backspace(&Default::default(), window, cx);
11120 });
11121
11122 cx.assert_editor_state(
11123 &"
11124 ˇ
11125 ˇ]]
11126 ˇ
11127 "
11128 .unindent(),
11129 );
11130}
11131
11132#[gpui::test]
11133async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11134 init_test(cx, |_| {});
11135
11136 let language = Arc::new(Language::new(
11137 LanguageConfig::default(),
11138 Some(tree_sitter_rust::LANGUAGE.into()),
11139 ));
11140
11141 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11142 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11143 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11144 editor
11145 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11146 .await;
11147
11148 editor.update_in(cx, |editor, window, cx| {
11149 editor.set_auto_replace_emoji_shortcode(true);
11150
11151 editor.handle_input("Hello ", window, cx);
11152 editor.handle_input(":wave", window, cx);
11153 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11154
11155 editor.handle_input(":", window, cx);
11156 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11157
11158 editor.handle_input(" :smile", window, cx);
11159 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11160
11161 editor.handle_input(":", window, cx);
11162 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11163
11164 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11165 editor.handle_input(":wave", window, cx);
11166 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11167
11168 editor.handle_input(":", window, cx);
11169 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11170
11171 editor.handle_input(":1", window, cx);
11172 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11173
11174 editor.handle_input(":", window, cx);
11175 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11176
11177 // Ensure shortcode does not get replaced when it is part of a word
11178 editor.handle_input(" Test:wave", window, cx);
11179 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11180
11181 editor.handle_input(":", window, cx);
11182 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11183
11184 editor.set_auto_replace_emoji_shortcode(false);
11185
11186 // Ensure shortcode does not get replaced when auto replace is off
11187 editor.handle_input(" :wave", window, cx);
11188 assert_eq!(
11189 editor.text(cx),
11190 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11191 );
11192
11193 editor.handle_input(":", window, cx);
11194 assert_eq!(
11195 editor.text(cx),
11196 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11197 );
11198 });
11199}
11200
11201#[gpui::test]
11202async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11203 init_test(cx, |_| {});
11204
11205 let (text, insertion_ranges) = marked_text_ranges(
11206 indoc! {"
11207 ˇ
11208 "},
11209 false,
11210 );
11211
11212 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11213 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11214
11215 _ = editor.update_in(cx, |editor, window, cx| {
11216 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11217
11218 editor
11219 .insert_snippet(
11220 &insertion_ranges
11221 .iter()
11222 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11223 .collect::<Vec<_>>(),
11224 snippet,
11225 window,
11226 cx,
11227 )
11228 .unwrap();
11229
11230 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11231 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11232 assert_eq!(editor.text(cx), expected_text);
11233 assert_eq!(
11234 editor.selections.ranges(&editor.display_snapshot(cx)),
11235 selection_ranges
11236 .iter()
11237 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11238 .collect::<Vec<_>>()
11239 );
11240 }
11241
11242 assert(
11243 editor,
11244 cx,
11245 indoc! {"
11246 type «» =•
11247 "},
11248 );
11249
11250 assert!(editor.context_menu_visible(), "There should be a matches");
11251 });
11252}
11253
11254#[gpui::test]
11255async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11256 init_test(cx, |_| {});
11257
11258 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11259 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11260 assert_eq!(editor.text(cx), expected_text);
11261 assert_eq!(
11262 editor.selections.ranges(&editor.display_snapshot(cx)),
11263 selection_ranges
11264 .iter()
11265 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11266 .collect::<Vec<_>>()
11267 );
11268 }
11269
11270 let (text, insertion_ranges) = marked_text_ranges(
11271 indoc! {"
11272 ˇ
11273 "},
11274 false,
11275 );
11276
11277 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11278 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11279
11280 _ = editor.update_in(cx, |editor, window, cx| {
11281 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11282
11283 editor
11284 .insert_snippet(
11285 &insertion_ranges
11286 .iter()
11287 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11288 .collect::<Vec<_>>(),
11289 snippet,
11290 window,
11291 cx,
11292 )
11293 .unwrap();
11294
11295 assert_state(
11296 editor,
11297 cx,
11298 indoc! {"
11299 type «» = ;•
11300 "},
11301 );
11302
11303 assert!(
11304 editor.context_menu_visible(),
11305 "Context menu should be visible for placeholder choices"
11306 );
11307
11308 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11309
11310 assert_state(
11311 editor,
11312 cx,
11313 indoc! {"
11314 type = «»;•
11315 "},
11316 );
11317
11318 assert!(
11319 !editor.context_menu_visible(),
11320 "Context menu should be hidden after moving to next tabstop"
11321 );
11322
11323 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11324
11325 assert_state(
11326 editor,
11327 cx,
11328 indoc! {"
11329 type = ; ˇ
11330 "},
11331 );
11332
11333 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11334
11335 assert_state(
11336 editor,
11337 cx,
11338 indoc! {"
11339 type = ; ˇ
11340 "},
11341 );
11342 });
11343
11344 _ = editor.update_in(cx, |editor, window, cx| {
11345 editor.select_all(&SelectAll, window, cx);
11346 editor.backspace(&Backspace, window, cx);
11347
11348 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11349 let insertion_ranges = editor
11350 .selections
11351 .all(&editor.display_snapshot(cx))
11352 .iter()
11353 .map(|s| s.range())
11354 .collect::<Vec<_>>();
11355
11356 editor
11357 .insert_snippet(&insertion_ranges, snippet, window, cx)
11358 .unwrap();
11359
11360 assert_state(editor, cx, "fn «» = value;•");
11361
11362 assert!(
11363 editor.context_menu_visible(),
11364 "Context menu should be visible for placeholder choices"
11365 );
11366
11367 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11368
11369 assert_state(editor, cx, "fn = «valueˇ»;•");
11370
11371 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11372
11373 assert_state(editor, cx, "fn «» = value;•");
11374
11375 assert!(
11376 editor.context_menu_visible(),
11377 "Context menu should be visible again after returning to first tabstop"
11378 );
11379
11380 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11381
11382 assert_state(editor, cx, "fn «» = value;•");
11383 });
11384}
11385
11386#[gpui::test]
11387async fn test_snippets(cx: &mut TestAppContext) {
11388 init_test(cx, |_| {});
11389
11390 let mut cx = EditorTestContext::new(cx).await;
11391
11392 cx.set_state(indoc! {"
11393 a.ˇ b
11394 a.ˇ b
11395 a.ˇ b
11396 "});
11397
11398 cx.update_editor(|editor, window, cx| {
11399 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11400 let insertion_ranges = editor
11401 .selections
11402 .all(&editor.display_snapshot(cx))
11403 .iter()
11404 .map(|s| s.range())
11405 .collect::<Vec<_>>();
11406 editor
11407 .insert_snippet(&insertion_ranges, snippet, window, cx)
11408 .unwrap();
11409 });
11410
11411 cx.assert_editor_state(indoc! {"
11412 a.f(«oneˇ», two, «threeˇ») b
11413 a.f(«oneˇ», two, «threeˇ») b
11414 a.f(«oneˇ», two, «threeˇ») b
11415 "});
11416
11417 // Can't move earlier than the first tab stop
11418 cx.update_editor(|editor, window, cx| {
11419 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11420 });
11421 cx.assert_editor_state(indoc! {"
11422 a.f(«oneˇ», two, «threeˇ») b
11423 a.f(«oneˇ», two, «threeˇ») b
11424 a.f(«oneˇ», two, «threeˇ») b
11425 "});
11426
11427 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11428 cx.assert_editor_state(indoc! {"
11429 a.f(one, «twoˇ», three) b
11430 a.f(one, «twoˇ», three) b
11431 a.f(one, «twoˇ», three) b
11432 "});
11433
11434 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11435 cx.assert_editor_state(indoc! {"
11436 a.f(«oneˇ», two, «threeˇ») b
11437 a.f(«oneˇ», two, «threeˇ») b
11438 a.f(«oneˇ», two, «threeˇ») b
11439 "});
11440
11441 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11442 cx.assert_editor_state(indoc! {"
11443 a.f(one, «twoˇ», three) b
11444 a.f(one, «twoˇ», three) b
11445 a.f(one, «twoˇ», three) b
11446 "});
11447 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11448 cx.assert_editor_state(indoc! {"
11449 a.f(one, two, three)ˇ b
11450 a.f(one, two, three)ˇ b
11451 a.f(one, two, three)ˇ b
11452 "});
11453
11454 // As soon as the last tab stop is reached, snippet state is gone
11455 cx.update_editor(|editor, window, cx| {
11456 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11457 });
11458 cx.assert_editor_state(indoc! {"
11459 a.f(one, two, three)ˇ b
11460 a.f(one, two, three)ˇ b
11461 a.f(one, two, three)ˇ b
11462 "});
11463}
11464
11465#[gpui::test]
11466async fn test_snippet_indentation(cx: &mut TestAppContext) {
11467 init_test(cx, |_| {});
11468
11469 let mut cx = EditorTestContext::new(cx).await;
11470
11471 cx.update_editor(|editor, window, cx| {
11472 let snippet = Snippet::parse(indoc! {"
11473 /*
11474 * Multiline comment with leading indentation
11475 *
11476 * $1
11477 */
11478 $0"})
11479 .unwrap();
11480 let insertion_ranges = editor
11481 .selections
11482 .all(&editor.display_snapshot(cx))
11483 .iter()
11484 .map(|s| s.range())
11485 .collect::<Vec<_>>();
11486 editor
11487 .insert_snippet(&insertion_ranges, snippet, window, cx)
11488 .unwrap();
11489 });
11490
11491 cx.assert_editor_state(indoc! {"
11492 /*
11493 * Multiline comment with leading indentation
11494 *
11495 * ˇ
11496 */
11497 "});
11498
11499 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11500 cx.assert_editor_state(indoc! {"
11501 /*
11502 * Multiline comment with leading indentation
11503 *
11504 *•
11505 */
11506 ˇ"});
11507}
11508
11509#[gpui::test]
11510async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11511 init_test(cx, |_| {});
11512
11513 let mut cx = EditorTestContext::new(cx).await;
11514 cx.update_editor(|editor, _, cx| {
11515 editor.project().unwrap().update(cx, |project, cx| {
11516 project.snippets().update(cx, |snippets, _cx| {
11517 let snippet = project::snippet_provider::Snippet {
11518 prefix: vec!["multi word".to_string()],
11519 body: "this is many words".to_string(),
11520 description: Some("description".to_string()),
11521 name: "multi-word snippet test".to_string(),
11522 };
11523 snippets.add_snippet_for_test(
11524 None,
11525 PathBuf::from("test_snippets.json"),
11526 vec![Arc::new(snippet)],
11527 );
11528 });
11529 })
11530 });
11531
11532 for (input_to_simulate, should_match_snippet) in [
11533 ("m", true),
11534 ("m ", true),
11535 ("m w", true),
11536 ("aa m w", true),
11537 ("aa m g", false),
11538 ] {
11539 cx.set_state("ˇ");
11540 cx.simulate_input(input_to_simulate); // fails correctly
11541
11542 cx.update_editor(|editor, _, _| {
11543 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11544 else {
11545 assert!(!should_match_snippet); // no completions! don't even show the menu
11546 return;
11547 };
11548 assert!(context_menu.visible());
11549 let completions = context_menu.completions.borrow();
11550
11551 assert_eq!(!completions.is_empty(), should_match_snippet);
11552 });
11553 }
11554}
11555
11556#[gpui::test]
11557async fn test_document_format_during_save(cx: &mut TestAppContext) {
11558 init_test(cx, |_| {});
11559
11560 let fs = FakeFs::new(cx.executor());
11561 fs.insert_file(path!("/file.rs"), Default::default()).await;
11562
11563 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11564
11565 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11566 language_registry.add(rust_lang());
11567 let mut fake_servers = language_registry.register_fake_lsp(
11568 "Rust",
11569 FakeLspAdapter {
11570 capabilities: lsp::ServerCapabilities {
11571 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11572 ..Default::default()
11573 },
11574 ..Default::default()
11575 },
11576 );
11577
11578 let buffer = project
11579 .update(cx, |project, cx| {
11580 project.open_local_buffer(path!("/file.rs"), cx)
11581 })
11582 .await
11583 .unwrap();
11584
11585 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11586 let (editor, cx) = cx.add_window_view(|window, cx| {
11587 build_editor_with_project(project.clone(), buffer, window, cx)
11588 });
11589 editor.update_in(cx, |editor, window, cx| {
11590 editor.set_text("one\ntwo\nthree\n", window, cx)
11591 });
11592 assert!(cx.read(|cx| editor.is_dirty(cx)));
11593
11594 cx.executor().start_waiting();
11595 let fake_server = fake_servers.next().await.unwrap();
11596
11597 {
11598 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11599 move |params, _| async move {
11600 assert_eq!(
11601 params.text_document.uri,
11602 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11603 );
11604 assert_eq!(params.options.tab_size, 4);
11605 Ok(Some(vec![lsp::TextEdit::new(
11606 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11607 ", ".to_string(),
11608 )]))
11609 },
11610 );
11611 let save = editor
11612 .update_in(cx, |editor, window, cx| {
11613 editor.save(
11614 SaveOptions {
11615 format: true,
11616 autosave: false,
11617 },
11618 project.clone(),
11619 window,
11620 cx,
11621 )
11622 })
11623 .unwrap();
11624 cx.executor().start_waiting();
11625 save.await;
11626
11627 assert_eq!(
11628 editor.update(cx, |editor, cx| editor.text(cx)),
11629 "one, two\nthree\n"
11630 );
11631 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11632 }
11633
11634 {
11635 editor.update_in(cx, |editor, window, cx| {
11636 editor.set_text("one\ntwo\nthree\n", window, cx)
11637 });
11638 assert!(cx.read(|cx| editor.is_dirty(cx)));
11639
11640 // Ensure we can still save even if formatting hangs.
11641 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11642 move |params, _| async move {
11643 assert_eq!(
11644 params.text_document.uri,
11645 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11646 );
11647 futures::future::pending::<()>().await;
11648 unreachable!()
11649 },
11650 );
11651 let save = editor
11652 .update_in(cx, |editor, window, cx| {
11653 editor.save(
11654 SaveOptions {
11655 format: true,
11656 autosave: false,
11657 },
11658 project.clone(),
11659 window,
11660 cx,
11661 )
11662 })
11663 .unwrap();
11664 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11665 cx.executor().start_waiting();
11666 save.await;
11667 assert_eq!(
11668 editor.update(cx, |editor, cx| editor.text(cx)),
11669 "one\ntwo\nthree\n"
11670 );
11671 }
11672
11673 // Set rust language override and assert overridden tabsize is sent to language server
11674 update_test_language_settings(cx, |settings| {
11675 settings.languages.0.insert(
11676 "Rust".into(),
11677 LanguageSettingsContent {
11678 tab_size: NonZeroU32::new(8),
11679 ..Default::default()
11680 },
11681 );
11682 });
11683
11684 {
11685 editor.update_in(cx, |editor, window, cx| {
11686 editor.set_text("somehting_new\n", window, cx)
11687 });
11688 assert!(cx.read(|cx| editor.is_dirty(cx)));
11689 let _formatting_request_signal = fake_server
11690 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11691 assert_eq!(
11692 params.text_document.uri,
11693 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11694 );
11695 assert_eq!(params.options.tab_size, 8);
11696 Ok(Some(vec![]))
11697 });
11698 let save = editor
11699 .update_in(cx, |editor, window, cx| {
11700 editor.save(
11701 SaveOptions {
11702 format: true,
11703 autosave: false,
11704 },
11705 project.clone(),
11706 window,
11707 cx,
11708 )
11709 })
11710 .unwrap();
11711 cx.executor().start_waiting();
11712 save.await;
11713 }
11714}
11715
11716#[gpui::test]
11717async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11718 init_test(cx, |settings| {
11719 settings.defaults.ensure_final_newline_on_save = Some(false);
11720 });
11721
11722 let fs = FakeFs::new(cx.executor());
11723 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11724
11725 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11726
11727 let buffer = project
11728 .update(cx, |project, cx| {
11729 project.open_local_buffer(path!("/file.txt"), cx)
11730 })
11731 .await
11732 .unwrap();
11733
11734 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11735 let (editor, cx) = cx.add_window_view(|window, cx| {
11736 build_editor_with_project(project.clone(), buffer, window, cx)
11737 });
11738 editor.update_in(cx, |editor, window, cx| {
11739 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11740 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11741 });
11742 });
11743 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11744
11745 editor.update_in(cx, |editor, window, cx| {
11746 editor.handle_input("\n", window, cx)
11747 });
11748 cx.run_until_parked();
11749 save(&editor, &project, cx).await;
11750 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11751
11752 editor.update_in(cx, |editor, window, cx| {
11753 editor.undo(&Default::default(), window, cx);
11754 });
11755 save(&editor, &project, cx).await;
11756 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11757
11758 editor.update_in(cx, |editor, window, cx| {
11759 editor.redo(&Default::default(), window, cx);
11760 });
11761 cx.run_until_parked();
11762 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11763
11764 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11765 let save = editor
11766 .update_in(cx, |editor, window, cx| {
11767 editor.save(
11768 SaveOptions {
11769 format: true,
11770 autosave: false,
11771 },
11772 project.clone(),
11773 window,
11774 cx,
11775 )
11776 })
11777 .unwrap();
11778 cx.executor().start_waiting();
11779 save.await;
11780 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11781 }
11782}
11783
11784#[gpui::test]
11785async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11786 init_test(cx, |_| {});
11787
11788 let cols = 4;
11789 let rows = 10;
11790 let sample_text_1 = sample_text(rows, cols, 'a');
11791 assert_eq!(
11792 sample_text_1,
11793 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11794 );
11795 let sample_text_2 = sample_text(rows, cols, 'l');
11796 assert_eq!(
11797 sample_text_2,
11798 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11799 );
11800 let sample_text_3 = sample_text(rows, cols, 'v');
11801 assert_eq!(
11802 sample_text_3,
11803 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11804 );
11805
11806 let fs = FakeFs::new(cx.executor());
11807 fs.insert_tree(
11808 path!("/a"),
11809 json!({
11810 "main.rs": sample_text_1,
11811 "other.rs": sample_text_2,
11812 "lib.rs": sample_text_3,
11813 }),
11814 )
11815 .await;
11816
11817 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11818 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11819 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11820
11821 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11822 language_registry.add(rust_lang());
11823 let mut fake_servers = language_registry.register_fake_lsp(
11824 "Rust",
11825 FakeLspAdapter {
11826 capabilities: lsp::ServerCapabilities {
11827 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11828 ..Default::default()
11829 },
11830 ..Default::default()
11831 },
11832 );
11833
11834 let worktree = project.update(cx, |project, cx| {
11835 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11836 assert_eq!(worktrees.len(), 1);
11837 worktrees.pop().unwrap()
11838 });
11839 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11840
11841 let buffer_1 = project
11842 .update(cx, |project, cx| {
11843 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11844 })
11845 .await
11846 .unwrap();
11847 let buffer_2 = project
11848 .update(cx, |project, cx| {
11849 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11850 })
11851 .await
11852 .unwrap();
11853 let buffer_3 = project
11854 .update(cx, |project, cx| {
11855 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11856 })
11857 .await
11858 .unwrap();
11859
11860 let multi_buffer = cx.new(|cx| {
11861 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11862 multi_buffer.push_excerpts(
11863 buffer_1.clone(),
11864 [
11865 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11866 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11867 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11868 ],
11869 cx,
11870 );
11871 multi_buffer.push_excerpts(
11872 buffer_2.clone(),
11873 [
11874 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11875 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11876 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11877 ],
11878 cx,
11879 );
11880 multi_buffer.push_excerpts(
11881 buffer_3.clone(),
11882 [
11883 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11884 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11885 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11886 ],
11887 cx,
11888 );
11889 multi_buffer
11890 });
11891 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11892 Editor::new(
11893 EditorMode::full(),
11894 multi_buffer,
11895 Some(project.clone()),
11896 window,
11897 cx,
11898 )
11899 });
11900
11901 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11902 editor.change_selections(
11903 SelectionEffects::scroll(Autoscroll::Next),
11904 window,
11905 cx,
11906 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
11907 );
11908 editor.insert("|one|two|three|", window, cx);
11909 });
11910 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11911 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11912 editor.change_selections(
11913 SelectionEffects::scroll(Autoscroll::Next),
11914 window,
11915 cx,
11916 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
11917 );
11918 editor.insert("|four|five|six|", window, cx);
11919 });
11920 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11921
11922 // First two buffers should be edited, but not the third one.
11923 assert_eq!(
11924 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11925 "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}",
11926 );
11927 buffer_1.update(cx, |buffer, _| {
11928 assert!(buffer.is_dirty());
11929 assert_eq!(
11930 buffer.text(),
11931 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11932 )
11933 });
11934 buffer_2.update(cx, |buffer, _| {
11935 assert!(buffer.is_dirty());
11936 assert_eq!(
11937 buffer.text(),
11938 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11939 )
11940 });
11941 buffer_3.update(cx, |buffer, _| {
11942 assert!(!buffer.is_dirty());
11943 assert_eq!(buffer.text(), sample_text_3,)
11944 });
11945 cx.executor().run_until_parked();
11946
11947 cx.executor().start_waiting();
11948 let save = multi_buffer_editor
11949 .update_in(cx, |editor, window, cx| {
11950 editor.save(
11951 SaveOptions {
11952 format: true,
11953 autosave: false,
11954 },
11955 project.clone(),
11956 window,
11957 cx,
11958 )
11959 })
11960 .unwrap();
11961
11962 let fake_server = fake_servers.next().await.unwrap();
11963 fake_server
11964 .server
11965 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11966 Ok(Some(vec![lsp::TextEdit::new(
11967 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11968 format!("[{} formatted]", params.text_document.uri),
11969 )]))
11970 })
11971 .detach();
11972 save.await;
11973
11974 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11975 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11976 assert_eq!(
11977 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11978 uri!(
11979 "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}"
11980 ),
11981 );
11982 buffer_1.update(cx, |buffer, _| {
11983 assert!(!buffer.is_dirty());
11984 assert_eq!(
11985 buffer.text(),
11986 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11987 )
11988 });
11989 buffer_2.update(cx, |buffer, _| {
11990 assert!(!buffer.is_dirty());
11991 assert_eq!(
11992 buffer.text(),
11993 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11994 )
11995 });
11996 buffer_3.update(cx, |buffer, _| {
11997 assert!(!buffer.is_dirty());
11998 assert_eq!(buffer.text(), sample_text_3,)
11999 });
12000}
12001
12002#[gpui::test]
12003async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12004 init_test(cx, |_| {});
12005
12006 let fs = FakeFs::new(cx.executor());
12007 fs.insert_tree(
12008 path!("/dir"),
12009 json!({
12010 "file1.rs": "fn main() { println!(\"hello\"); }",
12011 "file2.rs": "fn test() { println!(\"test\"); }",
12012 "file3.rs": "fn other() { println!(\"other\"); }\n",
12013 }),
12014 )
12015 .await;
12016
12017 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12018 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12019 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12020
12021 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12022 language_registry.add(rust_lang());
12023
12024 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12025 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12026
12027 // Open three buffers
12028 let buffer_1 = project
12029 .update(cx, |project, cx| {
12030 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12031 })
12032 .await
12033 .unwrap();
12034 let buffer_2 = project
12035 .update(cx, |project, cx| {
12036 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12037 })
12038 .await
12039 .unwrap();
12040 let buffer_3 = project
12041 .update(cx, |project, cx| {
12042 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12043 })
12044 .await
12045 .unwrap();
12046
12047 // Create a multi-buffer with all three buffers
12048 let multi_buffer = cx.new(|cx| {
12049 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12050 multi_buffer.push_excerpts(
12051 buffer_1.clone(),
12052 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12053 cx,
12054 );
12055 multi_buffer.push_excerpts(
12056 buffer_2.clone(),
12057 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12058 cx,
12059 );
12060 multi_buffer.push_excerpts(
12061 buffer_3.clone(),
12062 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12063 cx,
12064 );
12065 multi_buffer
12066 });
12067
12068 let editor = cx.new_window_entity(|window, cx| {
12069 Editor::new(
12070 EditorMode::full(),
12071 multi_buffer,
12072 Some(project.clone()),
12073 window,
12074 cx,
12075 )
12076 });
12077
12078 // Edit only the first buffer
12079 editor.update_in(cx, |editor, window, cx| {
12080 editor.change_selections(
12081 SelectionEffects::scroll(Autoscroll::Next),
12082 window,
12083 cx,
12084 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12085 );
12086 editor.insert("// edited", window, cx);
12087 });
12088
12089 // Verify that only buffer 1 is dirty
12090 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12091 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12092 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12093
12094 // Get write counts after file creation (files were created with initial content)
12095 // We expect each file to have been written once during creation
12096 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12097 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12098 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12099
12100 // Perform autosave
12101 let save_task = editor.update_in(cx, |editor, window, cx| {
12102 editor.save(
12103 SaveOptions {
12104 format: true,
12105 autosave: true,
12106 },
12107 project.clone(),
12108 window,
12109 cx,
12110 )
12111 });
12112 save_task.await.unwrap();
12113
12114 // Only the dirty buffer should have been saved
12115 assert_eq!(
12116 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12117 1,
12118 "Buffer 1 was dirty, so it should have been written once during autosave"
12119 );
12120 assert_eq!(
12121 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12122 0,
12123 "Buffer 2 was clean, so it should not have been written during autosave"
12124 );
12125 assert_eq!(
12126 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12127 0,
12128 "Buffer 3 was clean, so it should not have been written during autosave"
12129 );
12130
12131 // Verify buffer states after autosave
12132 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12133 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12134 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12135
12136 // Now perform a manual save (format = true)
12137 let save_task = editor.update_in(cx, |editor, window, cx| {
12138 editor.save(
12139 SaveOptions {
12140 format: true,
12141 autosave: false,
12142 },
12143 project.clone(),
12144 window,
12145 cx,
12146 )
12147 });
12148 save_task.await.unwrap();
12149
12150 // During manual save, clean buffers don't get written to disk
12151 // They just get did_save called for language server notifications
12152 assert_eq!(
12153 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12154 1,
12155 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12156 );
12157 assert_eq!(
12158 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12159 0,
12160 "Buffer 2 should not have been written at all"
12161 );
12162 assert_eq!(
12163 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12164 0,
12165 "Buffer 3 should not have been written at all"
12166 );
12167}
12168
12169async fn setup_range_format_test(
12170 cx: &mut TestAppContext,
12171) -> (
12172 Entity<Project>,
12173 Entity<Editor>,
12174 &mut gpui::VisualTestContext,
12175 lsp::FakeLanguageServer,
12176) {
12177 init_test(cx, |_| {});
12178
12179 let fs = FakeFs::new(cx.executor());
12180 fs.insert_file(path!("/file.rs"), Default::default()).await;
12181
12182 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12183
12184 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12185 language_registry.add(rust_lang());
12186 let mut fake_servers = language_registry.register_fake_lsp(
12187 "Rust",
12188 FakeLspAdapter {
12189 capabilities: lsp::ServerCapabilities {
12190 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12191 ..lsp::ServerCapabilities::default()
12192 },
12193 ..FakeLspAdapter::default()
12194 },
12195 );
12196
12197 let buffer = project
12198 .update(cx, |project, cx| {
12199 project.open_local_buffer(path!("/file.rs"), cx)
12200 })
12201 .await
12202 .unwrap();
12203
12204 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12205 let (editor, cx) = cx.add_window_view(|window, cx| {
12206 build_editor_with_project(project.clone(), buffer, window, cx)
12207 });
12208
12209 cx.executor().start_waiting();
12210 let fake_server = fake_servers.next().await.unwrap();
12211
12212 (project, editor, cx, fake_server)
12213}
12214
12215#[gpui::test]
12216async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12217 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12218
12219 editor.update_in(cx, |editor, window, cx| {
12220 editor.set_text("one\ntwo\nthree\n", window, cx)
12221 });
12222 assert!(cx.read(|cx| editor.is_dirty(cx)));
12223
12224 let save = editor
12225 .update_in(cx, |editor, window, cx| {
12226 editor.save(
12227 SaveOptions {
12228 format: true,
12229 autosave: false,
12230 },
12231 project.clone(),
12232 window,
12233 cx,
12234 )
12235 })
12236 .unwrap();
12237 fake_server
12238 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12239 assert_eq!(
12240 params.text_document.uri,
12241 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12242 );
12243 assert_eq!(params.options.tab_size, 4);
12244 Ok(Some(vec![lsp::TextEdit::new(
12245 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12246 ", ".to_string(),
12247 )]))
12248 })
12249 .next()
12250 .await;
12251 cx.executor().start_waiting();
12252 save.await;
12253 assert_eq!(
12254 editor.update(cx, |editor, cx| editor.text(cx)),
12255 "one, two\nthree\n"
12256 );
12257 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12258}
12259
12260#[gpui::test]
12261async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12262 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12263
12264 editor.update_in(cx, |editor, window, cx| {
12265 editor.set_text("one\ntwo\nthree\n", window, cx)
12266 });
12267 assert!(cx.read(|cx| editor.is_dirty(cx)));
12268
12269 // Test that save still works when formatting hangs
12270 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12271 move |params, _| async move {
12272 assert_eq!(
12273 params.text_document.uri,
12274 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12275 );
12276 futures::future::pending::<()>().await;
12277 unreachable!()
12278 },
12279 );
12280 let save = editor
12281 .update_in(cx, |editor, window, cx| {
12282 editor.save(
12283 SaveOptions {
12284 format: true,
12285 autosave: false,
12286 },
12287 project.clone(),
12288 window,
12289 cx,
12290 )
12291 })
12292 .unwrap();
12293 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12294 cx.executor().start_waiting();
12295 save.await;
12296 assert_eq!(
12297 editor.update(cx, |editor, cx| editor.text(cx)),
12298 "one\ntwo\nthree\n"
12299 );
12300 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12301}
12302
12303#[gpui::test]
12304async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12305 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12306
12307 // Buffer starts clean, no formatting should be requested
12308 let save = editor
12309 .update_in(cx, |editor, window, cx| {
12310 editor.save(
12311 SaveOptions {
12312 format: false,
12313 autosave: false,
12314 },
12315 project.clone(),
12316 window,
12317 cx,
12318 )
12319 })
12320 .unwrap();
12321 let _pending_format_request = fake_server
12322 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12323 panic!("Should not be invoked");
12324 })
12325 .next();
12326 cx.executor().start_waiting();
12327 save.await;
12328 cx.run_until_parked();
12329}
12330
12331#[gpui::test]
12332async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12333 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12334
12335 // Set Rust language override and assert overridden tabsize is sent to language server
12336 update_test_language_settings(cx, |settings| {
12337 settings.languages.0.insert(
12338 "Rust".into(),
12339 LanguageSettingsContent {
12340 tab_size: NonZeroU32::new(8),
12341 ..Default::default()
12342 },
12343 );
12344 });
12345
12346 editor.update_in(cx, |editor, window, cx| {
12347 editor.set_text("something_new\n", window, cx)
12348 });
12349 assert!(cx.read(|cx| editor.is_dirty(cx)));
12350 let save = editor
12351 .update_in(cx, |editor, window, cx| {
12352 editor.save(
12353 SaveOptions {
12354 format: true,
12355 autosave: false,
12356 },
12357 project.clone(),
12358 window,
12359 cx,
12360 )
12361 })
12362 .unwrap();
12363 fake_server
12364 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12365 assert_eq!(
12366 params.text_document.uri,
12367 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12368 );
12369 assert_eq!(params.options.tab_size, 8);
12370 Ok(Some(Vec::new()))
12371 })
12372 .next()
12373 .await;
12374 save.await;
12375}
12376
12377#[gpui::test]
12378async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12379 init_test(cx, |settings| {
12380 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12381 settings::LanguageServerFormatterSpecifier::Current,
12382 )))
12383 });
12384
12385 let fs = FakeFs::new(cx.executor());
12386 fs.insert_file(path!("/file.rs"), Default::default()).await;
12387
12388 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12389
12390 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12391 language_registry.add(Arc::new(Language::new(
12392 LanguageConfig {
12393 name: "Rust".into(),
12394 matcher: LanguageMatcher {
12395 path_suffixes: vec!["rs".to_string()],
12396 ..Default::default()
12397 },
12398 ..LanguageConfig::default()
12399 },
12400 Some(tree_sitter_rust::LANGUAGE.into()),
12401 )));
12402 update_test_language_settings(cx, |settings| {
12403 // Enable Prettier formatting for the same buffer, and ensure
12404 // LSP is called instead of Prettier.
12405 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12406 });
12407 let mut fake_servers = language_registry.register_fake_lsp(
12408 "Rust",
12409 FakeLspAdapter {
12410 capabilities: lsp::ServerCapabilities {
12411 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12412 ..Default::default()
12413 },
12414 ..Default::default()
12415 },
12416 );
12417
12418 let buffer = project
12419 .update(cx, |project, cx| {
12420 project.open_local_buffer(path!("/file.rs"), cx)
12421 })
12422 .await
12423 .unwrap();
12424
12425 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12426 let (editor, cx) = cx.add_window_view(|window, cx| {
12427 build_editor_with_project(project.clone(), buffer, window, cx)
12428 });
12429 editor.update_in(cx, |editor, window, cx| {
12430 editor.set_text("one\ntwo\nthree\n", window, cx)
12431 });
12432
12433 cx.executor().start_waiting();
12434 let fake_server = fake_servers.next().await.unwrap();
12435
12436 let format = editor
12437 .update_in(cx, |editor, window, cx| {
12438 editor.perform_format(
12439 project.clone(),
12440 FormatTrigger::Manual,
12441 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12442 window,
12443 cx,
12444 )
12445 })
12446 .unwrap();
12447 fake_server
12448 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12449 assert_eq!(
12450 params.text_document.uri,
12451 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12452 );
12453 assert_eq!(params.options.tab_size, 4);
12454 Ok(Some(vec![lsp::TextEdit::new(
12455 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12456 ", ".to_string(),
12457 )]))
12458 })
12459 .next()
12460 .await;
12461 cx.executor().start_waiting();
12462 format.await;
12463 assert_eq!(
12464 editor.update(cx, |editor, cx| editor.text(cx)),
12465 "one, two\nthree\n"
12466 );
12467
12468 editor.update_in(cx, |editor, window, cx| {
12469 editor.set_text("one\ntwo\nthree\n", window, cx)
12470 });
12471 // Ensure we don't lock if formatting hangs.
12472 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12473 move |params, _| async move {
12474 assert_eq!(
12475 params.text_document.uri,
12476 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12477 );
12478 futures::future::pending::<()>().await;
12479 unreachable!()
12480 },
12481 );
12482 let format = editor
12483 .update_in(cx, |editor, window, cx| {
12484 editor.perform_format(
12485 project,
12486 FormatTrigger::Manual,
12487 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12488 window,
12489 cx,
12490 )
12491 })
12492 .unwrap();
12493 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12494 cx.executor().start_waiting();
12495 format.await;
12496 assert_eq!(
12497 editor.update(cx, |editor, cx| editor.text(cx)),
12498 "one\ntwo\nthree\n"
12499 );
12500}
12501
12502#[gpui::test]
12503async fn test_multiple_formatters(cx: &mut TestAppContext) {
12504 init_test(cx, |settings| {
12505 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12506 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12507 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12508 Formatter::CodeAction("code-action-1".into()),
12509 Formatter::CodeAction("code-action-2".into()),
12510 ]))
12511 });
12512
12513 let fs = FakeFs::new(cx.executor());
12514 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12515 .await;
12516
12517 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12518 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12519 language_registry.add(rust_lang());
12520
12521 let mut fake_servers = language_registry.register_fake_lsp(
12522 "Rust",
12523 FakeLspAdapter {
12524 capabilities: lsp::ServerCapabilities {
12525 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12526 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12527 commands: vec!["the-command-for-code-action-1".into()],
12528 ..Default::default()
12529 }),
12530 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12531 ..Default::default()
12532 },
12533 ..Default::default()
12534 },
12535 );
12536
12537 let buffer = project
12538 .update(cx, |project, cx| {
12539 project.open_local_buffer(path!("/file.rs"), cx)
12540 })
12541 .await
12542 .unwrap();
12543
12544 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12545 let (editor, cx) = cx.add_window_view(|window, cx| {
12546 build_editor_with_project(project.clone(), buffer, window, cx)
12547 });
12548
12549 cx.executor().start_waiting();
12550
12551 let fake_server = fake_servers.next().await.unwrap();
12552 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12553 move |_params, _| async move {
12554 Ok(Some(vec![lsp::TextEdit::new(
12555 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12556 "applied-formatting\n".to_string(),
12557 )]))
12558 },
12559 );
12560 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12561 move |params, _| async move {
12562 let requested_code_actions = params.context.only.expect("Expected code action request");
12563 assert_eq!(requested_code_actions.len(), 1);
12564
12565 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12566 let code_action = match requested_code_actions[0].as_str() {
12567 "code-action-1" => lsp::CodeAction {
12568 kind: Some("code-action-1".into()),
12569 edit: Some(lsp::WorkspaceEdit::new(
12570 [(
12571 uri,
12572 vec![lsp::TextEdit::new(
12573 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12574 "applied-code-action-1-edit\n".to_string(),
12575 )],
12576 )]
12577 .into_iter()
12578 .collect(),
12579 )),
12580 command: Some(lsp::Command {
12581 command: "the-command-for-code-action-1".into(),
12582 ..Default::default()
12583 }),
12584 ..Default::default()
12585 },
12586 "code-action-2" => lsp::CodeAction {
12587 kind: Some("code-action-2".into()),
12588 edit: Some(lsp::WorkspaceEdit::new(
12589 [(
12590 uri,
12591 vec![lsp::TextEdit::new(
12592 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12593 "applied-code-action-2-edit\n".to_string(),
12594 )],
12595 )]
12596 .into_iter()
12597 .collect(),
12598 )),
12599 ..Default::default()
12600 },
12601 req => panic!("Unexpected code action request: {:?}", req),
12602 };
12603 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12604 code_action,
12605 )]))
12606 },
12607 );
12608
12609 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12610 move |params, _| async move { Ok(params) }
12611 });
12612
12613 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12614 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12615 let fake = fake_server.clone();
12616 let lock = command_lock.clone();
12617 move |params, _| {
12618 assert_eq!(params.command, "the-command-for-code-action-1");
12619 let fake = fake.clone();
12620 let lock = lock.clone();
12621 async move {
12622 lock.lock().await;
12623 fake.server
12624 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12625 label: None,
12626 edit: lsp::WorkspaceEdit {
12627 changes: Some(
12628 [(
12629 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12630 vec![lsp::TextEdit {
12631 range: lsp::Range::new(
12632 lsp::Position::new(0, 0),
12633 lsp::Position::new(0, 0),
12634 ),
12635 new_text: "applied-code-action-1-command\n".into(),
12636 }],
12637 )]
12638 .into_iter()
12639 .collect(),
12640 ),
12641 ..Default::default()
12642 },
12643 })
12644 .await
12645 .into_response()
12646 .unwrap();
12647 Ok(Some(json!(null)))
12648 }
12649 }
12650 });
12651
12652 cx.executor().start_waiting();
12653 editor
12654 .update_in(cx, |editor, window, cx| {
12655 editor.perform_format(
12656 project.clone(),
12657 FormatTrigger::Manual,
12658 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12659 window,
12660 cx,
12661 )
12662 })
12663 .unwrap()
12664 .await;
12665 editor.update(cx, |editor, cx| {
12666 assert_eq!(
12667 editor.text(cx),
12668 r#"
12669 applied-code-action-2-edit
12670 applied-code-action-1-command
12671 applied-code-action-1-edit
12672 applied-formatting
12673 one
12674 two
12675 three
12676 "#
12677 .unindent()
12678 );
12679 });
12680
12681 editor.update_in(cx, |editor, window, cx| {
12682 editor.undo(&Default::default(), window, cx);
12683 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12684 });
12685
12686 // Perform a manual edit while waiting for an LSP command
12687 // that's being run as part of a formatting code action.
12688 let lock_guard = command_lock.lock().await;
12689 let format = editor
12690 .update_in(cx, |editor, window, cx| {
12691 editor.perform_format(
12692 project.clone(),
12693 FormatTrigger::Manual,
12694 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12695 window,
12696 cx,
12697 )
12698 })
12699 .unwrap();
12700 cx.run_until_parked();
12701 editor.update(cx, |editor, cx| {
12702 assert_eq!(
12703 editor.text(cx),
12704 r#"
12705 applied-code-action-1-edit
12706 applied-formatting
12707 one
12708 two
12709 three
12710 "#
12711 .unindent()
12712 );
12713
12714 editor.buffer.update(cx, |buffer, cx| {
12715 let ix = buffer.len(cx);
12716 buffer.edit([(ix..ix, "edited\n")], None, cx);
12717 });
12718 });
12719
12720 // Allow the LSP command to proceed. Because the buffer was edited,
12721 // the second code action will not be run.
12722 drop(lock_guard);
12723 format.await;
12724 editor.update_in(cx, |editor, window, cx| {
12725 assert_eq!(
12726 editor.text(cx),
12727 r#"
12728 applied-code-action-1-command
12729 applied-code-action-1-edit
12730 applied-formatting
12731 one
12732 two
12733 three
12734 edited
12735 "#
12736 .unindent()
12737 );
12738
12739 // The manual edit is undone first, because it is the last thing the user did
12740 // (even though the command completed afterwards).
12741 editor.undo(&Default::default(), window, cx);
12742 assert_eq!(
12743 editor.text(cx),
12744 r#"
12745 applied-code-action-1-command
12746 applied-code-action-1-edit
12747 applied-formatting
12748 one
12749 two
12750 three
12751 "#
12752 .unindent()
12753 );
12754
12755 // All the formatting (including the command, which completed after the manual edit)
12756 // is undone together.
12757 editor.undo(&Default::default(), window, cx);
12758 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12759 });
12760}
12761
12762#[gpui::test]
12763async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12764 init_test(cx, |settings| {
12765 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12766 settings::LanguageServerFormatterSpecifier::Current,
12767 )]))
12768 });
12769
12770 let fs = FakeFs::new(cx.executor());
12771 fs.insert_file(path!("/file.ts"), Default::default()).await;
12772
12773 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12774
12775 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12776 language_registry.add(Arc::new(Language::new(
12777 LanguageConfig {
12778 name: "TypeScript".into(),
12779 matcher: LanguageMatcher {
12780 path_suffixes: vec!["ts".to_string()],
12781 ..Default::default()
12782 },
12783 ..LanguageConfig::default()
12784 },
12785 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12786 )));
12787 update_test_language_settings(cx, |settings| {
12788 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12789 });
12790 let mut fake_servers = language_registry.register_fake_lsp(
12791 "TypeScript",
12792 FakeLspAdapter {
12793 capabilities: lsp::ServerCapabilities {
12794 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12795 ..Default::default()
12796 },
12797 ..Default::default()
12798 },
12799 );
12800
12801 let buffer = project
12802 .update(cx, |project, cx| {
12803 project.open_local_buffer(path!("/file.ts"), cx)
12804 })
12805 .await
12806 .unwrap();
12807
12808 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12809 let (editor, cx) = cx.add_window_view(|window, cx| {
12810 build_editor_with_project(project.clone(), buffer, window, cx)
12811 });
12812 editor.update_in(cx, |editor, window, cx| {
12813 editor.set_text(
12814 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12815 window,
12816 cx,
12817 )
12818 });
12819
12820 cx.executor().start_waiting();
12821 let fake_server = fake_servers.next().await.unwrap();
12822
12823 let format = editor
12824 .update_in(cx, |editor, window, cx| {
12825 editor.perform_code_action_kind(
12826 project.clone(),
12827 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12828 window,
12829 cx,
12830 )
12831 })
12832 .unwrap();
12833 fake_server
12834 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12835 assert_eq!(
12836 params.text_document.uri,
12837 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12838 );
12839 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12840 lsp::CodeAction {
12841 title: "Organize Imports".to_string(),
12842 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12843 edit: Some(lsp::WorkspaceEdit {
12844 changes: Some(
12845 [(
12846 params.text_document.uri.clone(),
12847 vec![lsp::TextEdit::new(
12848 lsp::Range::new(
12849 lsp::Position::new(1, 0),
12850 lsp::Position::new(2, 0),
12851 ),
12852 "".to_string(),
12853 )],
12854 )]
12855 .into_iter()
12856 .collect(),
12857 ),
12858 ..Default::default()
12859 }),
12860 ..Default::default()
12861 },
12862 )]))
12863 })
12864 .next()
12865 .await;
12866 cx.executor().start_waiting();
12867 format.await;
12868 assert_eq!(
12869 editor.update(cx, |editor, cx| editor.text(cx)),
12870 "import { a } from 'module';\n\nconst x = a;\n"
12871 );
12872
12873 editor.update_in(cx, |editor, window, cx| {
12874 editor.set_text(
12875 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12876 window,
12877 cx,
12878 )
12879 });
12880 // Ensure we don't lock if code action hangs.
12881 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12882 move |params, _| async move {
12883 assert_eq!(
12884 params.text_document.uri,
12885 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12886 );
12887 futures::future::pending::<()>().await;
12888 unreachable!()
12889 },
12890 );
12891 let format = editor
12892 .update_in(cx, |editor, window, cx| {
12893 editor.perform_code_action_kind(
12894 project,
12895 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12896 window,
12897 cx,
12898 )
12899 })
12900 .unwrap();
12901 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12902 cx.executor().start_waiting();
12903 format.await;
12904 assert_eq!(
12905 editor.update(cx, |editor, cx| editor.text(cx)),
12906 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12907 );
12908}
12909
12910#[gpui::test]
12911async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12912 init_test(cx, |_| {});
12913
12914 let mut cx = EditorLspTestContext::new_rust(
12915 lsp::ServerCapabilities {
12916 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12917 ..Default::default()
12918 },
12919 cx,
12920 )
12921 .await;
12922
12923 cx.set_state(indoc! {"
12924 one.twoˇ
12925 "});
12926
12927 // The format request takes a long time. When it completes, it inserts
12928 // a newline and an indent before the `.`
12929 cx.lsp
12930 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12931 let executor = cx.background_executor().clone();
12932 async move {
12933 executor.timer(Duration::from_millis(100)).await;
12934 Ok(Some(vec![lsp::TextEdit {
12935 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12936 new_text: "\n ".into(),
12937 }]))
12938 }
12939 });
12940
12941 // Submit a format request.
12942 let format_1 = cx
12943 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12944 .unwrap();
12945 cx.executor().run_until_parked();
12946
12947 // Submit a second format request.
12948 let format_2 = cx
12949 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12950 .unwrap();
12951 cx.executor().run_until_parked();
12952
12953 // Wait for both format requests to complete
12954 cx.executor().advance_clock(Duration::from_millis(200));
12955 cx.executor().start_waiting();
12956 format_1.await.unwrap();
12957 cx.executor().start_waiting();
12958 format_2.await.unwrap();
12959
12960 // The formatting edits only happens once.
12961 cx.assert_editor_state(indoc! {"
12962 one
12963 .twoˇ
12964 "});
12965}
12966
12967#[gpui::test]
12968async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12969 init_test(cx, |settings| {
12970 settings.defaults.formatter = Some(FormatterList::default())
12971 });
12972
12973 let mut cx = EditorLspTestContext::new_rust(
12974 lsp::ServerCapabilities {
12975 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12976 ..Default::default()
12977 },
12978 cx,
12979 )
12980 .await;
12981
12982 // Record which buffer changes have been sent to the language server
12983 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12984 cx.lsp
12985 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12986 let buffer_changes = buffer_changes.clone();
12987 move |params, _| {
12988 buffer_changes.lock().extend(
12989 params
12990 .content_changes
12991 .into_iter()
12992 .map(|e| (e.range.unwrap(), e.text)),
12993 );
12994 }
12995 });
12996 // Handle formatting requests to the language server.
12997 cx.lsp
12998 .set_request_handler::<lsp::request::Formatting, _, _>({
12999 let buffer_changes = buffer_changes.clone();
13000 move |_, _| {
13001 let buffer_changes = buffer_changes.clone();
13002 // Insert blank lines between each line of the buffer.
13003 async move {
13004 // When formatting is requested, trailing whitespace has already been stripped,
13005 // and the trailing newline has already been added.
13006 assert_eq!(
13007 &buffer_changes.lock()[1..],
13008 &[
13009 (
13010 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13011 "".into()
13012 ),
13013 (
13014 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13015 "".into()
13016 ),
13017 (
13018 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13019 "\n".into()
13020 ),
13021 ]
13022 );
13023
13024 Ok(Some(vec![
13025 lsp::TextEdit {
13026 range: lsp::Range::new(
13027 lsp::Position::new(1, 0),
13028 lsp::Position::new(1, 0),
13029 ),
13030 new_text: "\n".into(),
13031 },
13032 lsp::TextEdit {
13033 range: lsp::Range::new(
13034 lsp::Position::new(2, 0),
13035 lsp::Position::new(2, 0),
13036 ),
13037 new_text: "\n".into(),
13038 },
13039 ]))
13040 }
13041 }
13042 });
13043
13044 // Set up a buffer white some trailing whitespace and no trailing newline.
13045 cx.set_state(
13046 &[
13047 "one ", //
13048 "twoˇ", //
13049 "three ", //
13050 "four", //
13051 ]
13052 .join("\n"),
13053 );
13054 cx.run_until_parked();
13055
13056 // Submit a format request.
13057 let format = cx
13058 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13059 .unwrap();
13060
13061 cx.run_until_parked();
13062 // After formatting the buffer, the trailing whitespace is stripped,
13063 // a newline is appended, and the edits provided by the language server
13064 // have been applied.
13065 format.await.unwrap();
13066
13067 cx.assert_editor_state(
13068 &[
13069 "one", //
13070 "", //
13071 "twoˇ", //
13072 "", //
13073 "three", //
13074 "four", //
13075 "", //
13076 ]
13077 .join("\n"),
13078 );
13079
13080 // Undoing the formatting undoes the trailing whitespace removal, the
13081 // trailing newline, and the LSP edits.
13082 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13083 cx.assert_editor_state(
13084 &[
13085 "one ", //
13086 "twoˇ", //
13087 "three ", //
13088 "four", //
13089 ]
13090 .join("\n"),
13091 );
13092}
13093
13094#[gpui::test]
13095async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13096 cx: &mut TestAppContext,
13097) {
13098 init_test(cx, |_| {});
13099
13100 cx.update(|cx| {
13101 cx.update_global::<SettingsStore, _>(|settings, cx| {
13102 settings.update_user_settings(cx, |settings| {
13103 settings.editor.auto_signature_help = Some(true);
13104 });
13105 });
13106 });
13107
13108 let mut cx = EditorLspTestContext::new_rust(
13109 lsp::ServerCapabilities {
13110 signature_help_provider: Some(lsp::SignatureHelpOptions {
13111 ..Default::default()
13112 }),
13113 ..Default::default()
13114 },
13115 cx,
13116 )
13117 .await;
13118
13119 let language = Language::new(
13120 LanguageConfig {
13121 name: "Rust".into(),
13122 brackets: BracketPairConfig {
13123 pairs: vec![
13124 BracketPair {
13125 start: "{".to_string(),
13126 end: "}".to_string(),
13127 close: true,
13128 surround: true,
13129 newline: true,
13130 },
13131 BracketPair {
13132 start: "(".to_string(),
13133 end: ")".to_string(),
13134 close: true,
13135 surround: true,
13136 newline: true,
13137 },
13138 BracketPair {
13139 start: "/*".to_string(),
13140 end: " */".to_string(),
13141 close: true,
13142 surround: true,
13143 newline: true,
13144 },
13145 BracketPair {
13146 start: "[".to_string(),
13147 end: "]".to_string(),
13148 close: false,
13149 surround: false,
13150 newline: true,
13151 },
13152 BracketPair {
13153 start: "\"".to_string(),
13154 end: "\"".to_string(),
13155 close: true,
13156 surround: true,
13157 newline: false,
13158 },
13159 BracketPair {
13160 start: "<".to_string(),
13161 end: ">".to_string(),
13162 close: false,
13163 surround: true,
13164 newline: true,
13165 },
13166 ],
13167 ..Default::default()
13168 },
13169 autoclose_before: "})]".to_string(),
13170 ..Default::default()
13171 },
13172 Some(tree_sitter_rust::LANGUAGE.into()),
13173 );
13174 let language = Arc::new(language);
13175
13176 cx.language_registry().add(language.clone());
13177 cx.update_buffer(|buffer, cx| {
13178 buffer.set_language(Some(language), cx);
13179 });
13180
13181 cx.set_state(
13182 &r#"
13183 fn main() {
13184 sampleˇ
13185 }
13186 "#
13187 .unindent(),
13188 );
13189
13190 cx.update_editor(|editor, window, cx| {
13191 editor.handle_input("(", window, cx);
13192 });
13193 cx.assert_editor_state(
13194 &"
13195 fn main() {
13196 sample(ˇ)
13197 }
13198 "
13199 .unindent(),
13200 );
13201
13202 let mocked_response = lsp::SignatureHelp {
13203 signatures: vec![lsp::SignatureInformation {
13204 label: "fn sample(param1: u8, param2: u8)".to_string(),
13205 documentation: None,
13206 parameters: Some(vec![
13207 lsp::ParameterInformation {
13208 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13209 documentation: None,
13210 },
13211 lsp::ParameterInformation {
13212 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13213 documentation: None,
13214 },
13215 ]),
13216 active_parameter: None,
13217 }],
13218 active_signature: Some(0),
13219 active_parameter: Some(0),
13220 };
13221 handle_signature_help_request(&mut cx, mocked_response).await;
13222
13223 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13224 .await;
13225
13226 cx.editor(|editor, _, _| {
13227 let signature_help_state = editor.signature_help_state.popover().cloned();
13228 let signature = signature_help_state.unwrap();
13229 assert_eq!(
13230 signature.signatures[signature.current_signature].label,
13231 "fn sample(param1: u8, param2: u8)"
13232 );
13233 });
13234}
13235
13236#[gpui::test]
13237async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13238 init_test(cx, |_| {});
13239
13240 cx.update(|cx| {
13241 cx.update_global::<SettingsStore, _>(|settings, cx| {
13242 settings.update_user_settings(cx, |settings| {
13243 settings.editor.auto_signature_help = Some(false);
13244 settings.editor.show_signature_help_after_edits = Some(false);
13245 });
13246 });
13247 });
13248
13249 let mut cx = EditorLspTestContext::new_rust(
13250 lsp::ServerCapabilities {
13251 signature_help_provider: Some(lsp::SignatureHelpOptions {
13252 ..Default::default()
13253 }),
13254 ..Default::default()
13255 },
13256 cx,
13257 )
13258 .await;
13259
13260 let language = Language::new(
13261 LanguageConfig {
13262 name: "Rust".into(),
13263 brackets: BracketPairConfig {
13264 pairs: vec![
13265 BracketPair {
13266 start: "{".to_string(),
13267 end: "}".to_string(),
13268 close: true,
13269 surround: true,
13270 newline: true,
13271 },
13272 BracketPair {
13273 start: "(".to_string(),
13274 end: ")".to_string(),
13275 close: true,
13276 surround: true,
13277 newline: true,
13278 },
13279 BracketPair {
13280 start: "/*".to_string(),
13281 end: " */".to_string(),
13282 close: true,
13283 surround: true,
13284 newline: true,
13285 },
13286 BracketPair {
13287 start: "[".to_string(),
13288 end: "]".to_string(),
13289 close: false,
13290 surround: false,
13291 newline: true,
13292 },
13293 BracketPair {
13294 start: "\"".to_string(),
13295 end: "\"".to_string(),
13296 close: true,
13297 surround: true,
13298 newline: false,
13299 },
13300 BracketPair {
13301 start: "<".to_string(),
13302 end: ">".to_string(),
13303 close: false,
13304 surround: true,
13305 newline: true,
13306 },
13307 ],
13308 ..Default::default()
13309 },
13310 autoclose_before: "})]".to_string(),
13311 ..Default::default()
13312 },
13313 Some(tree_sitter_rust::LANGUAGE.into()),
13314 );
13315 let language = Arc::new(language);
13316
13317 cx.language_registry().add(language.clone());
13318 cx.update_buffer(|buffer, cx| {
13319 buffer.set_language(Some(language), cx);
13320 });
13321
13322 // Ensure that signature_help is not called when no signature help is enabled.
13323 cx.set_state(
13324 &r#"
13325 fn main() {
13326 sampleˇ
13327 }
13328 "#
13329 .unindent(),
13330 );
13331 cx.update_editor(|editor, window, cx| {
13332 editor.handle_input("(", window, cx);
13333 });
13334 cx.assert_editor_state(
13335 &"
13336 fn main() {
13337 sample(ˇ)
13338 }
13339 "
13340 .unindent(),
13341 );
13342 cx.editor(|editor, _, _| {
13343 assert!(editor.signature_help_state.task().is_none());
13344 });
13345
13346 let mocked_response = lsp::SignatureHelp {
13347 signatures: vec![lsp::SignatureInformation {
13348 label: "fn sample(param1: u8, param2: u8)".to_string(),
13349 documentation: None,
13350 parameters: Some(vec![
13351 lsp::ParameterInformation {
13352 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13353 documentation: None,
13354 },
13355 lsp::ParameterInformation {
13356 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13357 documentation: None,
13358 },
13359 ]),
13360 active_parameter: None,
13361 }],
13362 active_signature: Some(0),
13363 active_parameter: Some(0),
13364 };
13365
13366 // Ensure that signature_help is called when enabled afte edits
13367 cx.update(|_, cx| {
13368 cx.update_global::<SettingsStore, _>(|settings, cx| {
13369 settings.update_user_settings(cx, |settings| {
13370 settings.editor.auto_signature_help = Some(false);
13371 settings.editor.show_signature_help_after_edits = Some(true);
13372 });
13373 });
13374 });
13375 cx.set_state(
13376 &r#"
13377 fn main() {
13378 sampleˇ
13379 }
13380 "#
13381 .unindent(),
13382 );
13383 cx.update_editor(|editor, window, cx| {
13384 editor.handle_input("(", window, cx);
13385 });
13386 cx.assert_editor_state(
13387 &"
13388 fn main() {
13389 sample(ˇ)
13390 }
13391 "
13392 .unindent(),
13393 );
13394 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13395 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13396 .await;
13397 cx.update_editor(|editor, _, _| {
13398 let signature_help_state = editor.signature_help_state.popover().cloned();
13399 assert!(signature_help_state.is_some());
13400 let signature = signature_help_state.unwrap();
13401 assert_eq!(
13402 signature.signatures[signature.current_signature].label,
13403 "fn sample(param1: u8, param2: u8)"
13404 );
13405 editor.signature_help_state = SignatureHelpState::default();
13406 });
13407
13408 // Ensure that signature_help is called when auto signature help override is enabled
13409 cx.update(|_, cx| {
13410 cx.update_global::<SettingsStore, _>(|settings, cx| {
13411 settings.update_user_settings(cx, |settings| {
13412 settings.editor.auto_signature_help = Some(true);
13413 settings.editor.show_signature_help_after_edits = Some(false);
13414 });
13415 });
13416 });
13417 cx.set_state(
13418 &r#"
13419 fn main() {
13420 sampleˇ
13421 }
13422 "#
13423 .unindent(),
13424 );
13425 cx.update_editor(|editor, window, cx| {
13426 editor.handle_input("(", window, cx);
13427 });
13428 cx.assert_editor_state(
13429 &"
13430 fn main() {
13431 sample(ˇ)
13432 }
13433 "
13434 .unindent(),
13435 );
13436 handle_signature_help_request(&mut cx, mocked_response).await;
13437 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13438 .await;
13439 cx.editor(|editor, _, _| {
13440 let signature_help_state = editor.signature_help_state.popover().cloned();
13441 assert!(signature_help_state.is_some());
13442 let signature = signature_help_state.unwrap();
13443 assert_eq!(
13444 signature.signatures[signature.current_signature].label,
13445 "fn sample(param1: u8, param2: u8)"
13446 );
13447 });
13448}
13449
13450#[gpui::test]
13451async fn test_signature_help(cx: &mut TestAppContext) {
13452 init_test(cx, |_| {});
13453 cx.update(|cx| {
13454 cx.update_global::<SettingsStore, _>(|settings, cx| {
13455 settings.update_user_settings(cx, |settings| {
13456 settings.editor.auto_signature_help = Some(true);
13457 });
13458 });
13459 });
13460
13461 let mut cx = EditorLspTestContext::new_rust(
13462 lsp::ServerCapabilities {
13463 signature_help_provider: Some(lsp::SignatureHelpOptions {
13464 ..Default::default()
13465 }),
13466 ..Default::default()
13467 },
13468 cx,
13469 )
13470 .await;
13471
13472 // A test that directly calls `show_signature_help`
13473 cx.update_editor(|editor, window, cx| {
13474 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13475 });
13476
13477 let mocked_response = lsp::SignatureHelp {
13478 signatures: vec![lsp::SignatureInformation {
13479 label: "fn sample(param1: u8, param2: u8)".to_string(),
13480 documentation: None,
13481 parameters: Some(vec![
13482 lsp::ParameterInformation {
13483 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13484 documentation: None,
13485 },
13486 lsp::ParameterInformation {
13487 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13488 documentation: None,
13489 },
13490 ]),
13491 active_parameter: None,
13492 }],
13493 active_signature: Some(0),
13494 active_parameter: Some(0),
13495 };
13496 handle_signature_help_request(&mut cx, mocked_response).await;
13497
13498 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13499 .await;
13500
13501 cx.editor(|editor, _, _| {
13502 let signature_help_state = editor.signature_help_state.popover().cloned();
13503 assert!(signature_help_state.is_some());
13504 let signature = signature_help_state.unwrap();
13505 assert_eq!(
13506 signature.signatures[signature.current_signature].label,
13507 "fn sample(param1: u8, param2: u8)"
13508 );
13509 });
13510
13511 // When exiting outside from inside the brackets, `signature_help` is closed.
13512 cx.set_state(indoc! {"
13513 fn main() {
13514 sample(ˇ);
13515 }
13516
13517 fn sample(param1: u8, param2: u8) {}
13518 "});
13519
13520 cx.update_editor(|editor, window, cx| {
13521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13522 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13523 });
13524 });
13525
13526 let mocked_response = lsp::SignatureHelp {
13527 signatures: Vec::new(),
13528 active_signature: None,
13529 active_parameter: None,
13530 };
13531 handle_signature_help_request(&mut cx, mocked_response).await;
13532
13533 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13534 .await;
13535
13536 cx.editor(|editor, _, _| {
13537 assert!(!editor.signature_help_state.is_shown());
13538 });
13539
13540 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13541 cx.set_state(indoc! {"
13542 fn main() {
13543 sample(ˇ);
13544 }
13545
13546 fn sample(param1: u8, param2: u8) {}
13547 "});
13548
13549 let mocked_response = lsp::SignatureHelp {
13550 signatures: vec![lsp::SignatureInformation {
13551 label: "fn sample(param1: u8, param2: u8)".to_string(),
13552 documentation: None,
13553 parameters: Some(vec![
13554 lsp::ParameterInformation {
13555 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13556 documentation: None,
13557 },
13558 lsp::ParameterInformation {
13559 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13560 documentation: None,
13561 },
13562 ]),
13563 active_parameter: None,
13564 }],
13565 active_signature: Some(0),
13566 active_parameter: Some(0),
13567 };
13568 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13569 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13570 .await;
13571 cx.editor(|editor, _, _| {
13572 assert!(editor.signature_help_state.is_shown());
13573 });
13574
13575 // Restore the popover with more parameter input
13576 cx.set_state(indoc! {"
13577 fn main() {
13578 sample(param1, param2ˇ);
13579 }
13580
13581 fn sample(param1: u8, param2: u8) {}
13582 "});
13583
13584 let mocked_response = lsp::SignatureHelp {
13585 signatures: vec![lsp::SignatureInformation {
13586 label: "fn sample(param1: u8, param2: u8)".to_string(),
13587 documentation: None,
13588 parameters: Some(vec![
13589 lsp::ParameterInformation {
13590 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13591 documentation: None,
13592 },
13593 lsp::ParameterInformation {
13594 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13595 documentation: None,
13596 },
13597 ]),
13598 active_parameter: None,
13599 }],
13600 active_signature: Some(0),
13601 active_parameter: Some(1),
13602 };
13603 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13604 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13605 .await;
13606
13607 // When selecting a range, the popover is gone.
13608 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13609 cx.update_editor(|editor, window, cx| {
13610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13611 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13612 })
13613 });
13614 cx.assert_editor_state(indoc! {"
13615 fn main() {
13616 sample(param1, «ˇparam2»);
13617 }
13618
13619 fn sample(param1: u8, param2: u8) {}
13620 "});
13621 cx.editor(|editor, _, _| {
13622 assert!(!editor.signature_help_state.is_shown());
13623 });
13624
13625 // When unselecting again, the popover is back if within the brackets.
13626 cx.update_editor(|editor, window, cx| {
13627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13628 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13629 })
13630 });
13631 cx.assert_editor_state(indoc! {"
13632 fn main() {
13633 sample(param1, ˇparam2);
13634 }
13635
13636 fn sample(param1: u8, param2: u8) {}
13637 "});
13638 handle_signature_help_request(&mut cx, mocked_response).await;
13639 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13640 .await;
13641 cx.editor(|editor, _, _| {
13642 assert!(editor.signature_help_state.is_shown());
13643 });
13644
13645 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13646 cx.update_editor(|editor, window, cx| {
13647 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13648 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13649 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13650 })
13651 });
13652 cx.assert_editor_state(indoc! {"
13653 fn main() {
13654 sample(param1, ˇparam2);
13655 }
13656
13657 fn sample(param1: u8, param2: u8) {}
13658 "});
13659
13660 let mocked_response = lsp::SignatureHelp {
13661 signatures: vec![lsp::SignatureInformation {
13662 label: "fn sample(param1: u8, param2: u8)".to_string(),
13663 documentation: None,
13664 parameters: Some(vec![
13665 lsp::ParameterInformation {
13666 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13667 documentation: None,
13668 },
13669 lsp::ParameterInformation {
13670 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13671 documentation: None,
13672 },
13673 ]),
13674 active_parameter: None,
13675 }],
13676 active_signature: Some(0),
13677 active_parameter: Some(1),
13678 };
13679 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13680 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13681 .await;
13682 cx.update_editor(|editor, _, cx| {
13683 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13684 });
13685 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13686 .await;
13687 cx.update_editor(|editor, window, cx| {
13688 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13689 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13690 })
13691 });
13692 cx.assert_editor_state(indoc! {"
13693 fn main() {
13694 sample(param1, «ˇparam2»);
13695 }
13696
13697 fn sample(param1: u8, param2: u8) {}
13698 "});
13699 cx.update_editor(|editor, window, cx| {
13700 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13701 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13702 })
13703 });
13704 cx.assert_editor_state(indoc! {"
13705 fn main() {
13706 sample(param1, ˇparam2);
13707 }
13708
13709 fn sample(param1: u8, param2: u8) {}
13710 "});
13711 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13712 .await;
13713}
13714
13715#[gpui::test]
13716async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13717 init_test(cx, |_| {});
13718
13719 let mut cx = EditorLspTestContext::new_rust(
13720 lsp::ServerCapabilities {
13721 signature_help_provider: Some(lsp::SignatureHelpOptions {
13722 ..Default::default()
13723 }),
13724 ..Default::default()
13725 },
13726 cx,
13727 )
13728 .await;
13729
13730 cx.set_state(indoc! {"
13731 fn main() {
13732 overloadedˇ
13733 }
13734 "});
13735
13736 cx.update_editor(|editor, window, cx| {
13737 editor.handle_input("(", window, cx);
13738 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13739 });
13740
13741 // Mock response with 3 signatures
13742 let mocked_response = lsp::SignatureHelp {
13743 signatures: vec![
13744 lsp::SignatureInformation {
13745 label: "fn overloaded(x: i32)".to_string(),
13746 documentation: None,
13747 parameters: Some(vec![lsp::ParameterInformation {
13748 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13749 documentation: None,
13750 }]),
13751 active_parameter: None,
13752 },
13753 lsp::SignatureInformation {
13754 label: "fn overloaded(x: i32, y: i32)".to_string(),
13755 documentation: None,
13756 parameters: Some(vec![
13757 lsp::ParameterInformation {
13758 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13759 documentation: None,
13760 },
13761 lsp::ParameterInformation {
13762 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13763 documentation: None,
13764 },
13765 ]),
13766 active_parameter: None,
13767 },
13768 lsp::SignatureInformation {
13769 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13770 documentation: None,
13771 parameters: Some(vec![
13772 lsp::ParameterInformation {
13773 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13774 documentation: None,
13775 },
13776 lsp::ParameterInformation {
13777 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13778 documentation: None,
13779 },
13780 lsp::ParameterInformation {
13781 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13782 documentation: None,
13783 },
13784 ]),
13785 active_parameter: None,
13786 },
13787 ],
13788 active_signature: Some(1),
13789 active_parameter: Some(0),
13790 };
13791 handle_signature_help_request(&mut cx, mocked_response).await;
13792
13793 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13794 .await;
13795
13796 // Verify we have multiple signatures and the right one is selected
13797 cx.editor(|editor, _, _| {
13798 let popover = editor.signature_help_state.popover().cloned().unwrap();
13799 assert_eq!(popover.signatures.len(), 3);
13800 // active_signature was 1, so that should be the current
13801 assert_eq!(popover.current_signature, 1);
13802 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13803 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13804 assert_eq!(
13805 popover.signatures[2].label,
13806 "fn overloaded(x: i32, y: i32, z: i32)"
13807 );
13808 });
13809
13810 // Test navigation functionality
13811 cx.update_editor(|editor, window, cx| {
13812 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13813 });
13814
13815 cx.editor(|editor, _, _| {
13816 let popover = editor.signature_help_state.popover().cloned().unwrap();
13817 assert_eq!(popover.current_signature, 2);
13818 });
13819
13820 // Test wrap around
13821 cx.update_editor(|editor, window, cx| {
13822 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13823 });
13824
13825 cx.editor(|editor, _, _| {
13826 let popover = editor.signature_help_state.popover().cloned().unwrap();
13827 assert_eq!(popover.current_signature, 0);
13828 });
13829
13830 // Test previous navigation
13831 cx.update_editor(|editor, window, cx| {
13832 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13833 });
13834
13835 cx.editor(|editor, _, _| {
13836 let popover = editor.signature_help_state.popover().cloned().unwrap();
13837 assert_eq!(popover.current_signature, 2);
13838 });
13839}
13840
13841#[gpui::test]
13842async fn test_completion_mode(cx: &mut TestAppContext) {
13843 init_test(cx, |_| {});
13844 let mut cx = EditorLspTestContext::new_rust(
13845 lsp::ServerCapabilities {
13846 completion_provider: Some(lsp::CompletionOptions {
13847 resolve_provider: Some(true),
13848 ..Default::default()
13849 }),
13850 ..Default::default()
13851 },
13852 cx,
13853 )
13854 .await;
13855
13856 struct Run {
13857 run_description: &'static str,
13858 initial_state: String,
13859 buffer_marked_text: String,
13860 completion_label: &'static str,
13861 completion_text: &'static str,
13862 expected_with_insert_mode: String,
13863 expected_with_replace_mode: String,
13864 expected_with_replace_subsequence_mode: String,
13865 expected_with_replace_suffix_mode: String,
13866 }
13867
13868 let runs = [
13869 Run {
13870 run_description: "Start of word matches completion text",
13871 initial_state: "before ediˇ after".into(),
13872 buffer_marked_text: "before <edi|> after".into(),
13873 completion_label: "editor",
13874 completion_text: "editor",
13875 expected_with_insert_mode: "before editorˇ after".into(),
13876 expected_with_replace_mode: "before editorˇ after".into(),
13877 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13878 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13879 },
13880 Run {
13881 run_description: "Accept same text at the middle of the word",
13882 initial_state: "before ediˇtor after".into(),
13883 buffer_marked_text: "before <edi|tor> after".into(),
13884 completion_label: "editor",
13885 completion_text: "editor",
13886 expected_with_insert_mode: "before editorˇtor after".into(),
13887 expected_with_replace_mode: "before editorˇ after".into(),
13888 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13889 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13890 },
13891 Run {
13892 run_description: "End of word matches completion text -- cursor at end",
13893 initial_state: "before torˇ after".into(),
13894 buffer_marked_text: "before <tor|> after".into(),
13895 completion_label: "editor",
13896 completion_text: "editor",
13897 expected_with_insert_mode: "before editorˇ after".into(),
13898 expected_with_replace_mode: "before editorˇ after".into(),
13899 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13900 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13901 },
13902 Run {
13903 run_description: "End of word matches completion text -- cursor at start",
13904 initial_state: "before ˇtor after".into(),
13905 buffer_marked_text: "before <|tor> after".into(),
13906 completion_label: "editor",
13907 completion_text: "editor",
13908 expected_with_insert_mode: "before editorˇtor after".into(),
13909 expected_with_replace_mode: "before editorˇ after".into(),
13910 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13911 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13912 },
13913 Run {
13914 run_description: "Prepend text containing whitespace",
13915 initial_state: "pˇfield: bool".into(),
13916 buffer_marked_text: "<p|field>: bool".into(),
13917 completion_label: "pub ",
13918 completion_text: "pub ",
13919 expected_with_insert_mode: "pub ˇfield: bool".into(),
13920 expected_with_replace_mode: "pub ˇ: bool".into(),
13921 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13922 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13923 },
13924 Run {
13925 run_description: "Add element to start of list",
13926 initial_state: "[element_ˇelement_2]".into(),
13927 buffer_marked_text: "[<element_|element_2>]".into(),
13928 completion_label: "element_1",
13929 completion_text: "element_1",
13930 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13931 expected_with_replace_mode: "[element_1ˇ]".into(),
13932 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13933 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13934 },
13935 Run {
13936 run_description: "Add element to start of list -- first and second elements are equal",
13937 initial_state: "[elˇelement]".into(),
13938 buffer_marked_text: "[<el|element>]".into(),
13939 completion_label: "element",
13940 completion_text: "element",
13941 expected_with_insert_mode: "[elementˇelement]".into(),
13942 expected_with_replace_mode: "[elementˇ]".into(),
13943 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13944 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13945 },
13946 Run {
13947 run_description: "Ends with matching suffix",
13948 initial_state: "SubˇError".into(),
13949 buffer_marked_text: "<Sub|Error>".into(),
13950 completion_label: "SubscriptionError",
13951 completion_text: "SubscriptionError",
13952 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13953 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13954 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13955 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13956 },
13957 Run {
13958 run_description: "Suffix is a subsequence -- contiguous",
13959 initial_state: "SubˇErr".into(),
13960 buffer_marked_text: "<Sub|Err>".into(),
13961 completion_label: "SubscriptionError",
13962 completion_text: "SubscriptionError",
13963 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13964 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13965 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13966 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13967 },
13968 Run {
13969 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13970 initial_state: "Suˇscrirr".into(),
13971 buffer_marked_text: "<Su|scrirr>".into(),
13972 completion_label: "SubscriptionError",
13973 completion_text: "SubscriptionError",
13974 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13975 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13976 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13977 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13978 },
13979 Run {
13980 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13981 initial_state: "foo(indˇix)".into(),
13982 buffer_marked_text: "foo(<ind|ix>)".into(),
13983 completion_label: "node_index",
13984 completion_text: "node_index",
13985 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13986 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13987 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13988 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13989 },
13990 Run {
13991 run_description: "Replace range ends before cursor - should extend to cursor",
13992 initial_state: "before editˇo after".into(),
13993 buffer_marked_text: "before <{ed}>it|o after".into(),
13994 completion_label: "editor",
13995 completion_text: "editor",
13996 expected_with_insert_mode: "before editorˇo after".into(),
13997 expected_with_replace_mode: "before editorˇo after".into(),
13998 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13999 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14000 },
14001 Run {
14002 run_description: "Uses label for suffix matching",
14003 initial_state: "before ediˇtor after".into(),
14004 buffer_marked_text: "before <edi|tor> after".into(),
14005 completion_label: "editor",
14006 completion_text: "editor()",
14007 expected_with_insert_mode: "before editor()ˇtor after".into(),
14008 expected_with_replace_mode: "before editor()ˇ after".into(),
14009 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14010 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14011 },
14012 Run {
14013 run_description: "Case insensitive subsequence and suffix matching",
14014 initial_state: "before EDiˇtoR after".into(),
14015 buffer_marked_text: "before <EDi|toR> after".into(),
14016 completion_label: "editor",
14017 completion_text: "editor",
14018 expected_with_insert_mode: "before editorˇtoR after".into(),
14019 expected_with_replace_mode: "before editorˇ after".into(),
14020 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14021 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14022 },
14023 ];
14024
14025 for run in runs {
14026 let run_variations = [
14027 (LspInsertMode::Insert, run.expected_with_insert_mode),
14028 (LspInsertMode::Replace, run.expected_with_replace_mode),
14029 (
14030 LspInsertMode::ReplaceSubsequence,
14031 run.expected_with_replace_subsequence_mode,
14032 ),
14033 (
14034 LspInsertMode::ReplaceSuffix,
14035 run.expected_with_replace_suffix_mode,
14036 ),
14037 ];
14038
14039 for (lsp_insert_mode, expected_text) in run_variations {
14040 eprintln!(
14041 "run = {:?}, mode = {lsp_insert_mode:.?}",
14042 run.run_description,
14043 );
14044
14045 update_test_language_settings(&mut cx, |settings| {
14046 settings.defaults.completions = Some(CompletionSettingsContent {
14047 lsp_insert_mode: Some(lsp_insert_mode),
14048 words: Some(WordsCompletionMode::Disabled),
14049 words_min_length: Some(0),
14050 ..Default::default()
14051 });
14052 });
14053
14054 cx.set_state(&run.initial_state);
14055 cx.update_editor(|editor, window, cx| {
14056 editor.show_completions(&ShowCompletions, window, cx);
14057 });
14058
14059 let counter = Arc::new(AtomicUsize::new(0));
14060 handle_completion_request_with_insert_and_replace(
14061 &mut cx,
14062 &run.buffer_marked_text,
14063 vec![(run.completion_label, run.completion_text)],
14064 counter.clone(),
14065 )
14066 .await;
14067 cx.condition(|editor, _| editor.context_menu_visible())
14068 .await;
14069 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14070
14071 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14072 editor
14073 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14074 .unwrap()
14075 });
14076 cx.assert_editor_state(&expected_text);
14077 handle_resolve_completion_request(&mut cx, None).await;
14078 apply_additional_edits.await.unwrap();
14079 }
14080 }
14081}
14082
14083#[gpui::test]
14084async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14085 init_test(cx, |_| {});
14086 let mut cx = EditorLspTestContext::new_rust(
14087 lsp::ServerCapabilities {
14088 completion_provider: Some(lsp::CompletionOptions {
14089 resolve_provider: Some(true),
14090 ..Default::default()
14091 }),
14092 ..Default::default()
14093 },
14094 cx,
14095 )
14096 .await;
14097
14098 let initial_state = "SubˇError";
14099 let buffer_marked_text = "<Sub|Error>";
14100 let completion_text = "SubscriptionError";
14101 let expected_with_insert_mode = "SubscriptionErrorˇError";
14102 let expected_with_replace_mode = "SubscriptionErrorˇ";
14103
14104 update_test_language_settings(&mut cx, |settings| {
14105 settings.defaults.completions = Some(CompletionSettingsContent {
14106 words: Some(WordsCompletionMode::Disabled),
14107 words_min_length: Some(0),
14108 // set the opposite here to ensure that the action is overriding the default behavior
14109 lsp_insert_mode: Some(LspInsertMode::Insert),
14110 ..Default::default()
14111 });
14112 });
14113
14114 cx.set_state(initial_state);
14115 cx.update_editor(|editor, window, cx| {
14116 editor.show_completions(&ShowCompletions, window, cx);
14117 });
14118
14119 let counter = Arc::new(AtomicUsize::new(0));
14120 handle_completion_request_with_insert_and_replace(
14121 &mut cx,
14122 buffer_marked_text,
14123 vec![(completion_text, completion_text)],
14124 counter.clone(),
14125 )
14126 .await;
14127 cx.condition(|editor, _| editor.context_menu_visible())
14128 .await;
14129 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14130
14131 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14132 editor
14133 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14134 .unwrap()
14135 });
14136 cx.assert_editor_state(expected_with_replace_mode);
14137 handle_resolve_completion_request(&mut cx, None).await;
14138 apply_additional_edits.await.unwrap();
14139
14140 update_test_language_settings(&mut cx, |settings| {
14141 settings.defaults.completions = Some(CompletionSettingsContent {
14142 words: Some(WordsCompletionMode::Disabled),
14143 words_min_length: Some(0),
14144 // set the opposite here to ensure that the action is overriding the default behavior
14145 lsp_insert_mode: Some(LspInsertMode::Replace),
14146 ..Default::default()
14147 });
14148 });
14149
14150 cx.set_state(initial_state);
14151 cx.update_editor(|editor, window, cx| {
14152 editor.show_completions(&ShowCompletions, window, cx);
14153 });
14154 handle_completion_request_with_insert_and_replace(
14155 &mut cx,
14156 buffer_marked_text,
14157 vec![(completion_text, completion_text)],
14158 counter.clone(),
14159 )
14160 .await;
14161 cx.condition(|editor, _| editor.context_menu_visible())
14162 .await;
14163 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14164
14165 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14166 editor
14167 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14168 .unwrap()
14169 });
14170 cx.assert_editor_state(expected_with_insert_mode);
14171 handle_resolve_completion_request(&mut cx, None).await;
14172 apply_additional_edits.await.unwrap();
14173}
14174
14175#[gpui::test]
14176async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14177 init_test(cx, |_| {});
14178 let mut cx = EditorLspTestContext::new_rust(
14179 lsp::ServerCapabilities {
14180 completion_provider: Some(lsp::CompletionOptions {
14181 resolve_provider: Some(true),
14182 ..Default::default()
14183 }),
14184 ..Default::default()
14185 },
14186 cx,
14187 )
14188 .await;
14189
14190 // scenario: surrounding text matches completion text
14191 let completion_text = "to_offset";
14192 let initial_state = indoc! {"
14193 1. buf.to_offˇsuffix
14194 2. buf.to_offˇsuf
14195 3. buf.to_offˇfix
14196 4. buf.to_offˇ
14197 5. into_offˇensive
14198 6. ˇsuffix
14199 7. let ˇ //
14200 8. aaˇzz
14201 9. buf.to_off«zzzzzˇ»suffix
14202 10. buf.«ˇzzzzz»suffix
14203 11. to_off«ˇzzzzz»
14204
14205 buf.to_offˇsuffix // newest cursor
14206 "};
14207 let completion_marked_buffer = indoc! {"
14208 1. buf.to_offsuffix
14209 2. buf.to_offsuf
14210 3. buf.to_offfix
14211 4. buf.to_off
14212 5. into_offensive
14213 6. suffix
14214 7. let //
14215 8. aazz
14216 9. buf.to_offzzzzzsuffix
14217 10. buf.zzzzzsuffix
14218 11. to_offzzzzz
14219
14220 buf.<to_off|suffix> // newest cursor
14221 "};
14222 let expected = indoc! {"
14223 1. buf.to_offsetˇ
14224 2. buf.to_offsetˇsuf
14225 3. buf.to_offsetˇfix
14226 4. buf.to_offsetˇ
14227 5. into_offsetˇensive
14228 6. to_offsetˇsuffix
14229 7. let to_offsetˇ //
14230 8. aato_offsetˇzz
14231 9. buf.to_offsetˇ
14232 10. buf.to_offsetˇsuffix
14233 11. to_offsetˇ
14234
14235 buf.to_offsetˇ // newest cursor
14236 "};
14237 cx.set_state(initial_state);
14238 cx.update_editor(|editor, window, cx| {
14239 editor.show_completions(&ShowCompletions, window, cx);
14240 });
14241 handle_completion_request_with_insert_and_replace(
14242 &mut cx,
14243 completion_marked_buffer,
14244 vec![(completion_text, completion_text)],
14245 Arc::new(AtomicUsize::new(0)),
14246 )
14247 .await;
14248 cx.condition(|editor, _| editor.context_menu_visible())
14249 .await;
14250 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14251 editor
14252 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14253 .unwrap()
14254 });
14255 cx.assert_editor_state(expected);
14256 handle_resolve_completion_request(&mut cx, None).await;
14257 apply_additional_edits.await.unwrap();
14258
14259 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14260 let completion_text = "foo_and_bar";
14261 let initial_state = indoc! {"
14262 1. ooanbˇ
14263 2. zooanbˇ
14264 3. ooanbˇz
14265 4. zooanbˇz
14266 5. ooanˇ
14267 6. oanbˇ
14268
14269 ooanbˇ
14270 "};
14271 let completion_marked_buffer = indoc! {"
14272 1. ooanb
14273 2. zooanb
14274 3. ooanbz
14275 4. zooanbz
14276 5. ooan
14277 6. oanb
14278
14279 <ooanb|>
14280 "};
14281 let expected = indoc! {"
14282 1. foo_and_barˇ
14283 2. zfoo_and_barˇ
14284 3. foo_and_barˇz
14285 4. zfoo_and_barˇz
14286 5. ooanfoo_and_barˇ
14287 6. oanbfoo_and_barˇ
14288
14289 foo_and_barˇ
14290 "};
14291 cx.set_state(initial_state);
14292 cx.update_editor(|editor, window, cx| {
14293 editor.show_completions(&ShowCompletions, window, cx);
14294 });
14295 handle_completion_request_with_insert_and_replace(
14296 &mut cx,
14297 completion_marked_buffer,
14298 vec![(completion_text, completion_text)],
14299 Arc::new(AtomicUsize::new(0)),
14300 )
14301 .await;
14302 cx.condition(|editor, _| editor.context_menu_visible())
14303 .await;
14304 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14305 editor
14306 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14307 .unwrap()
14308 });
14309 cx.assert_editor_state(expected);
14310 handle_resolve_completion_request(&mut cx, None).await;
14311 apply_additional_edits.await.unwrap();
14312
14313 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14314 // (expects the same as if it was inserted at the end)
14315 let completion_text = "foo_and_bar";
14316 let initial_state = indoc! {"
14317 1. ooˇanb
14318 2. zooˇanb
14319 3. ooˇanbz
14320 4. zooˇanbz
14321
14322 ooˇanb
14323 "};
14324 let completion_marked_buffer = indoc! {"
14325 1. ooanb
14326 2. zooanb
14327 3. ooanbz
14328 4. zooanbz
14329
14330 <oo|anb>
14331 "};
14332 let expected = indoc! {"
14333 1. foo_and_barˇ
14334 2. zfoo_and_barˇ
14335 3. foo_and_barˇz
14336 4. zfoo_and_barˇz
14337
14338 foo_and_barˇ
14339 "};
14340 cx.set_state(initial_state);
14341 cx.update_editor(|editor, window, cx| {
14342 editor.show_completions(&ShowCompletions, window, cx);
14343 });
14344 handle_completion_request_with_insert_and_replace(
14345 &mut cx,
14346 completion_marked_buffer,
14347 vec![(completion_text, completion_text)],
14348 Arc::new(AtomicUsize::new(0)),
14349 )
14350 .await;
14351 cx.condition(|editor, _| editor.context_menu_visible())
14352 .await;
14353 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14354 editor
14355 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14356 .unwrap()
14357 });
14358 cx.assert_editor_state(expected);
14359 handle_resolve_completion_request(&mut cx, None).await;
14360 apply_additional_edits.await.unwrap();
14361}
14362
14363// This used to crash
14364#[gpui::test]
14365async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14366 init_test(cx, |_| {});
14367
14368 let buffer_text = indoc! {"
14369 fn main() {
14370 10.satu;
14371
14372 //
14373 // separate cursors so they open in different excerpts (manually reproducible)
14374 //
14375
14376 10.satu20;
14377 }
14378 "};
14379 let multibuffer_text_with_selections = indoc! {"
14380 fn main() {
14381 10.satuˇ;
14382
14383 //
14384
14385 //
14386
14387 10.satuˇ20;
14388 }
14389 "};
14390 let expected_multibuffer = indoc! {"
14391 fn main() {
14392 10.saturating_sub()ˇ;
14393
14394 //
14395
14396 //
14397
14398 10.saturating_sub()ˇ;
14399 }
14400 "};
14401
14402 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14403 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14404
14405 let fs = FakeFs::new(cx.executor());
14406 fs.insert_tree(
14407 path!("/a"),
14408 json!({
14409 "main.rs": buffer_text,
14410 }),
14411 )
14412 .await;
14413
14414 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14415 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14416 language_registry.add(rust_lang());
14417 let mut fake_servers = language_registry.register_fake_lsp(
14418 "Rust",
14419 FakeLspAdapter {
14420 capabilities: lsp::ServerCapabilities {
14421 completion_provider: Some(lsp::CompletionOptions {
14422 resolve_provider: None,
14423 ..lsp::CompletionOptions::default()
14424 }),
14425 ..lsp::ServerCapabilities::default()
14426 },
14427 ..FakeLspAdapter::default()
14428 },
14429 );
14430 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14431 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14432 let buffer = project
14433 .update(cx, |project, cx| {
14434 project.open_local_buffer(path!("/a/main.rs"), cx)
14435 })
14436 .await
14437 .unwrap();
14438
14439 let multi_buffer = cx.new(|cx| {
14440 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14441 multi_buffer.push_excerpts(
14442 buffer.clone(),
14443 [ExcerptRange::new(0..first_excerpt_end)],
14444 cx,
14445 );
14446 multi_buffer.push_excerpts(
14447 buffer.clone(),
14448 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14449 cx,
14450 );
14451 multi_buffer
14452 });
14453
14454 let editor = workspace
14455 .update(cx, |_, window, cx| {
14456 cx.new(|cx| {
14457 Editor::new(
14458 EditorMode::Full {
14459 scale_ui_elements_with_buffer_font_size: false,
14460 show_active_line_background: false,
14461 sizing_behavior: SizingBehavior::Default,
14462 },
14463 multi_buffer.clone(),
14464 Some(project.clone()),
14465 window,
14466 cx,
14467 )
14468 })
14469 })
14470 .unwrap();
14471
14472 let pane = workspace
14473 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14474 .unwrap();
14475 pane.update_in(cx, |pane, window, cx| {
14476 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14477 });
14478
14479 let fake_server = fake_servers.next().await.unwrap();
14480
14481 editor.update_in(cx, |editor, window, cx| {
14482 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14483 s.select_ranges([
14484 Point::new(1, 11)..Point::new(1, 11),
14485 Point::new(7, 11)..Point::new(7, 11),
14486 ])
14487 });
14488
14489 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14490 });
14491
14492 editor.update_in(cx, |editor, window, cx| {
14493 editor.show_completions(&ShowCompletions, window, cx);
14494 });
14495
14496 fake_server
14497 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14498 let completion_item = lsp::CompletionItem {
14499 label: "saturating_sub()".into(),
14500 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14501 lsp::InsertReplaceEdit {
14502 new_text: "saturating_sub()".to_owned(),
14503 insert: lsp::Range::new(
14504 lsp::Position::new(7, 7),
14505 lsp::Position::new(7, 11),
14506 ),
14507 replace: lsp::Range::new(
14508 lsp::Position::new(7, 7),
14509 lsp::Position::new(7, 13),
14510 ),
14511 },
14512 )),
14513 ..lsp::CompletionItem::default()
14514 };
14515
14516 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14517 })
14518 .next()
14519 .await
14520 .unwrap();
14521
14522 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14523 .await;
14524
14525 editor
14526 .update_in(cx, |editor, window, cx| {
14527 editor
14528 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14529 .unwrap()
14530 })
14531 .await
14532 .unwrap();
14533
14534 editor.update(cx, |editor, cx| {
14535 assert_text_with_selections(editor, expected_multibuffer, cx);
14536 })
14537}
14538
14539#[gpui::test]
14540async fn test_completion(cx: &mut TestAppContext) {
14541 init_test(cx, |_| {});
14542
14543 let mut cx = EditorLspTestContext::new_rust(
14544 lsp::ServerCapabilities {
14545 completion_provider: Some(lsp::CompletionOptions {
14546 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14547 resolve_provider: Some(true),
14548 ..Default::default()
14549 }),
14550 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14551 ..Default::default()
14552 },
14553 cx,
14554 )
14555 .await;
14556 let counter = Arc::new(AtomicUsize::new(0));
14557
14558 cx.set_state(indoc! {"
14559 oneˇ
14560 two
14561 three
14562 "});
14563 cx.simulate_keystroke(".");
14564 handle_completion_request(
14565 indoc! {"
14566 one.|<>
14567 two
14568 three
14569 "},
14570 vec!["first_completion", "second_completion"],
14571 true,
14572 counter.clone(),
14573 &mut cx,
14574 )
14575 .await;
14576 cx.condition(|editor, _| editor.context_menu_visible())
14577 .await;
14578 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14579
14580 let _handler = handle_signature_help_request(
14581 &mut cx,
14582 lsp::SignatureHelp {
14583 signatures: vec![lsp::SignatureInformation {
14584 label: "test signature".to_string(),
14585 documentation: None,
14586 parameters: Some(vec![lsp::ParameterInformation {
14587 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14588 documentation: None,
14589 }]),
14590 active_parameter: None,
14591 }],
14592 active_signature: None,
14593 active_parameter: None,
14594 },
14595 );
14596 cx.update_editor(|editor, window, cx| {
14597 assert!(
14598 !editor.signature_help_state.is_shown(),
14599 "No signature help was called for"
14600 );
14601 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14602 });
14603 cx.run_until_parked();
14604 cx.update_editor(|editor, _, _| {
14605 assert!(
14606 !editor.signature_help_state.is_shown(),
14607 "No signature help should be shown when completions menu is open"
14608 );
14609 });
14610
14611 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14612 editor.context_menu_next(&Default::default(), window, cx);
14613 editor
14614 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14615 .unwrap()
14616 });
14617 cx.assert_editor_state(indoc! {"
14618 one.second_completionˇ
14619 two
14620 three
14621 "});
14622
14623 handle_resolve_completion_request(
14624 &mut cx,
14625 Some(vec![
14626 (
14627 //This overlaps with the primary completion edit which is
14628 //misbehavior from the LSP spec, test that we filter it out
14629 indoc! {"
14630 one.second_ˇcompletion
14631 two
14632 threeˇ
14633 "},
14634 "overlapping additional edit",
14635 ),
14636 (
14637 indoc! {"
14638 one.second_completion
14639 two
14640 threeˇ
14641 "},
14642 "\nadditional edit",
14643 ),
14644 ]),
14645 )
14646 .await;
14647 apply_additional_edits.await.unwrap();
14648 cx.assert_editor_state(indoc! {"
14649 one.second_completionˇ
14650 two
14651 three
14652 additional edit
14653 "});
14654
14655 cx.set_state(indoc! {"
14656 one.second_completion
14657 twoˇ
14658 threeˇ
14659 additional edit
14660 "});
14661 cx.simulate_keystroke(" ");
14662 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14663 cx.simulate_keystroke("s");
14664 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14665
14666 cx.assert_editor_state(indoc! {"
14667 one.second_completion
14668 two sˇ
14669 three sˇ
14670 additional edit
14671 "});
14672 handle_completion_request(
14673 indoc! {"
14674 one.second_completion
14675 two s
14676 three <s|>
14677 additional edit
14678 "},
14679 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14680 true,
14681 counter.clone(),
14682 &mut cx,
14683 )
14684 .await;
14685 cx.condition(|editor, _| editor.context_menu_visible())
14686 .await;
14687 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14688
14689 cx.simulate_keystroke("i");
14690
14691 handle_completion_request(
14692 indoc! {"
14693 one.second_completion
14694 two si
14695 three <si|>
14696 additional edit
14697 "},
14698 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14699 true,
14700 counter.clone(),
14701 &mut cx,
14702 )
14703 .await;
14704 cx.condition(|editor, _| editor.context_menu_visible())
14705 .await;
14706 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14707
14708 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14709 editor
14710 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14711 .unwrap()
14712 });
14713 cx.assert_editor_state(indoc! {"
14714 one.second_completion
14715 two sixth_completionˇ
14716 three sixth_completionˇ
14717 additional edit
14718 "});
14719
14720 apply_additional_edits.await.unwrap();
14721
14722 update_test_language_settings(&mut cx, |settings| {
14723 settings.defaults.show_completions_on_input = Some(false);
14724 });
14725 cx.set_state("editorˇ");
14726 cx.simulate_keystroke(".");
14727 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14728 cx.simulate_keystrokes("c l o");
14729 cx.assert_editor_state("editor.cloˇ");
14730 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14731 cx.update_editor(|editor, window, cx| {
14732 editor.show_completions(&ShowCompletions, window, cx);
14733 });
14734 handle_completion_request(
14735 "editor.<clo|>",
14736 vec!["close", "clobber"],
14737 true,
14738 counter.clone(),
14739 &mut cx,
14740 )
14741 .await;
14742 cx.condition(|editor, _| editor.context_menu_visible())
14743 .await;
14744 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14745
14746 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14747 editor
14748 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14749 .unwrap()
14750 });
14751 cx.assert_editor_state("editor.clobberˇ");
14752 handle_resolve_completion_request(&mut cx, None).await;
14753 apply_additional_edits.await.unwrap();
14754}
14755
14756#[gpui::test]
14757async fn test_completion_reuse(cx: &mut TestAppContext) {
14758 init_test(cx, |_| {});
14759
14760 let mut cx = EditorLspTestContext::new_rust(
14761 lsp::ServerCapabilities {
14762 completion_provider: Some(lsp::CompletionOptions {
14763 trigger_characters: Some(vec![".".to_string()]),
14764 ..Default::default()
14765 }),
14766 ..Default::default()
14767 },
14768 cx,
14769 )
14770 .await;
14771
14772 let counter = Arc::new(AtomicUsize::new(0));
14773 cx.set_state("objˇ");
14774 cx.simulate_keystroke(".");
14775
14776 // Initial completion request returns complete results
14777 let is_incomplete = false;
14778 handle_completion_request(
14779 "obj.|<>",
14780 vec!["a", "ab", "abc"],
14781 is_incomplete,
14782 counter.clone(),
14783 &mut cx,
14784 )
14785 .await;
14786 cx.run_until_parked();
14787 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14788 cx.assert_editor_state("obj.ˇ");
14789 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14790
14791 // Type "a" - filters existing completions
14792 cx.simulate_keystroke("a");
14793 cx.run_until_parked();
14794 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14795 cx.assert_editor_state("obj.aˇ");
14796 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14797
14798 // Type "b" - filters existing completions
14799 cx.simulate_keystroke("b");
14800 cx.run_until_parked();
14801 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14802 cx.assert_editor_state("obj.abˇ");
14803 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14804
14805 // Type "c" - filters existing completions
14806 cx.simulate_keystroke("c");
14807 cx.run_until_parked();
14808 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14809 cx.assert_editor_state("obj.abcˇ");
14810 check_displayed_completions(vec!["abc"], &mut cx);
14811
14812 // Backspace to delete "c" - filters existing completions
14813 cx.update_editor(|editor, window, cx| {
14814 editor.backspace(&Backspace, window, cx);
14815 });
14816 cx.run_until_parked();
14817 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14818 cx.assert_editor_state("obj.abˇ");
14819 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14820
14821 // Moving cursor to the left dismisses menu.
14822 cx.update_editor(|editor, window, cx| {
14823 editor.move_left(&MoveLeft, window, cx);
14824 });
14825 cx.run_until_parked();
14826 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14827 cx.assert_editor_state("obj.aˇb");
14828 cx.update_editor(|editor, _, _| {
14829 assert_eq!(editor.context_menu_visible(), false);
14830 });
14831
14832 // Type "b" - new request
14833 cx.simulate_keystroke("b");
14834 let is_incomplete = false;
14835 handle_completion_request(
14836 "obj.<ab|>a",
14837 vec!["ab", "abc"],
14838 is_incomplete,
14839 counter.clone(),
14840 &mut cx,
14841 )
14842 .await;
14843 cx.run_until_parked();
14844 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14845 cx.assert_editor_state("obj.abˇb");
14846 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14847
14848 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14849 cx.update_editor(|editor, window, cx| {
14850 editor.backspace(&Backspace, window, cx);
14851 });
14852 let is_incomplete = false;
14853 handle_completion_request(
14854 "obj.<a|>b",
14855 vec!["a", "ab", "abc"],
14856 is_incomplete,
14857 counter.clone(),
14858 &mut cx,
14859 )
14860 .await;
14861 cx.run_until_parked();
14862 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14863 cx.assert_editor_state("obj.aˇb");
14864 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14865
14866 // Backspace to delete "a" - dismisses menu.
14867 cx.update_editor(|editor, window, cx| {
14868 editor.backspace(&Backspace, window, cx);
14869 });
14870 cx.run_until_parked();
14871 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14872 cx.assert_editor_state("obj.ˇb");
14873 cx.update_editor(|editor, _, _| {
14874 assert_eq!(editor.context_menu_visible(), false);
14875 });
14876}
14877
14878#[gpui::test]
14879async fn test_word_completion(cx: &mut TestAppContext) {
14880 let lsp_fetch_timeout_ms = 10;
14881 init_test(cx, |language_settings| {
14882 language_settings.defaults.completions = Some(CompletionSettingsContent {
14883 words_min_length: Some(0),
14884 lsp_fetch_timeout_ms: Some(10),
14885 lsp_insert_mode: Some(LspInsertMode::Insert),
14886 ..Default::default()
14887 });
14888 });
14889
14890 let mut cx = EditorLspTestContext::new_rust(
14891 lsp::ServerCapabilities {
14892 completion_provider: Some(lsp::CompletionOptions {
14893 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14894 ..lsp::CompletionOptions::default()
14895 }),
14896 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14897 ..lsp::ServerCapabilities::default()
14898 },
14899 cx,
14900 )
14901 .await;
14902
14903 let throttle_completions = Arc::new(AtomicBool::new(false));
14904
14905 let lsp_throttle_completions = throttle_completions.clone();
14906 let _completion_requests_handler =
14907 cx.lsp
14908 .server
14909 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14910 let lsp_throttle_completions = lsp_throttle_completions.clone();
14911 let cx = cx.clone();
14912 async move {
14913 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14914 cx.background_executor()
14915 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14916 .await;
14917 }
14918 Ok(Some(lsp::CompletionResponse::Array(vec![
14919 lsp::CompletionItem {
14920 label: "first".into(),
14921 ..lsp::CompletionItem::default()
14922 },
14923 lsp::CompletionItem {
14924 label: "last".into(),
14925 ..lsp::CompletionItem::default()
14926 },
14927 ])))
14928 }
14929 });
14930
14931 cx.set_state(indoc! {"
14932 oneˇ
14933 two
14934 three
14935 "});
14936 cx.simulate_keystroke(".");
14937 cx.executor().run_until_parked();
14938 cx.condition(|editor, _| editor.context_menu_visible())
14939 .await;
14940 cx.update_editor(|editor, window, cx| {
14941 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14942 {
14943 assert_eq!(
14944 completion_menu_entries(menu),
14945 &["first", "last"],
14946 "When LSP server is fast to reply, no fallback word completions are used"
14947 );
14948 } else {
14949 panic!("expected completion menu to be open");
14950 }
14951 editor.cancel(&Cancel, window, cx);
14952 });
14953 cx.executor().run_until_parked();
14954 cx.condition(|editor, _| !editor.context_menu_visible())
14955 .await;
14956
14957 throttle_completions.store(true, atomic::Ordering::Release);
14958 cx.simulate_keystroke(".");
14959 cx.executor()
14960 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14961 cx.executor().run_until_parked();
14962 cx.condition(|editor, _| editor.context_menu_visible())
14963 .await;
14964 cx.update_editor(|editor, _, _| {
14965 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14966 {
14967 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14968 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14969 } else {
14970 panic!("expected completion menu to be open");
14971 }
14972 });
14973}
14974
14975#[gpui::test]
14976async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14977 init_test(cx, |language_settings| {
14978 language_settings.defaults.completions = Some(CompletionSettingsContent {
14979 words: Some(WordsCompletionMode::Enabled),
14980 words_min_length: Some(0),
14981 lsp_insert_mode: Some(LspInsertMode::Insert),
14982 ..Default::default()
14983 });
14984 });
14985
14986 let mut cx = EditorLspTestContext::new_rust(
14987 lsp::ServerCapabilities {
14988 completion_provider: Some(lsp::CompletionOptions {
14989 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14990 ..lsp::CompletionOptions::default()
14991 }),
14992 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14993 ..lsp::ServerCapabilities::default()
14994 },
14995 cx,
14996 )
14997 .await;
14998
14999 let _completion_requests_handler =
15000 cx.lsp
15001 .server
15002 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15003 Ok(Some(lsp::CompletionResponse::Array(vec![
15004 lsp::CompletionItem {
15005 label: "first".into(),
15006 ..lsp::CompletionItem::default()
15007 },
15008 lsp::CompletionItem {
15009 label: "last".into(),
15010 ..lsp::CompletionItem::default()
15011 },
15012 ])))
15013 });
15014
15015 cx.set_state(indoc! {"ˇ
15016 first
15017 last
15018 second
15019 "});
15020 cx.simulate_keystroke(".");
15021 cx.executor().run_until_parked();
15022 cx.condition(|editor, _| editor.context_menu_visible())
15023 .await;
15024 cx.update_editor(|editor, _, _| {
15025 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15026 {
15027 assert_eq!(
15028 completion_menu_entries(menu),
15029 &["first", "last", "second"],
15030 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15031 );
15032 } else {
15033 panic!("expected completion menu to be open");
15034 }
15035 });
15036}
15037
15038#[gpui::test]
15039async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15040 init_test(cx, |language_settings| {
15041 language_settings.defaults.completions = Some(CompletionSettingsContent {
15042 words: Some(WordsCompletionMode::Disabled),
15043 words_min_length: Some(0),
15044 lsp_insert_mode: Some(LspInsertMode::Insert),
15045 ..Default::default()
15046 });
15047 });
15048
15049 let mut cx = EditorLspTestContext::new_rust(
15050 lsp::ServerCapabilities {
15051 completion_provider: Some(lsp::CompletionOptions {
15052 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15053 ..lsp::CompletionOptions::default()
15054 }),
15055 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15056 ..lsp::ServerCapabilities::default()
15057 },
15058 cx,
15059 )
15060 .await;
15061
15062 let _completion_requests_handler =
15063 cx.lsp
15064 .server
15065 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15066 panic!("LSP completions should not be queried when dealing with word completions")
15067 });
15068
15069 cx.set_state(indoc! {"ˇ
15070 first
15071 last
15072 second
15073 "});
15074 cx.update_editor(|editor, window, cx| {
15075 editor.show_word_completions(&ShowWordCompletions, window, cx);
15076 });
15077 cx.executor().run_until_parked();
15078 cx.condition(|editor, _| editor.context_menu_visible())
15079 .await;
15080 cx.update_editor(|editor, _, _| {
15081 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15082 {
15083 assert_eq!(
15084 completion_menu_entries(menu),
15085 &["first", "last", "second"],
15086 "`ShowWordCompletions` action should show word completions"
15087 );
15088 } else {
15089 panic!("expected completion menu to be open");
15090 }
15091 });
15092
15093 cx.simulate_keystroke("l");
15094 cx.executor().run_until_parked();
15095 cx.condition(|editor, _| editor.context_menu_visible())
15096 .await;
15097 cx.update_editor(|editor, _, _| {
15098 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15099 {
15100 assert_eq!(
15101 completion_menu_entries(menu),
15102 &["last"],
15103 "After showing word completions, further editing should filter them and not query the LSP"
15104 );
15105 } else {
15106 panic!("expected completion menu to be open");
15107 }
15108 });
15109}
15110
15111#[gpui::test]
15112async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15113 init_test(cx, |language_settings| {
15114 language_settings.defaults.completions = Some(CompletionSettingsContent {
15115 words_min_length: Some(0),
15116 lsp: Some(false),
15117 lsp_insert_mode: Some(LspInsertMode::Insert),
15118 ..Default::default()
15119 });
15120 });
15121
15122 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15123
15124 cx.set_state(indoc! {"ˇ
15125 0_usize
15126 let
15127 33
15128 4.5f32
15129 "});
15130 cx.update_editor(|editor, window, cx| {
15131 editor.show_completions(&ShowCompletions, window, cx);
15132 });
15133 cx.executor().run_until_parked();
15134 cx.condition(|editor, _| editor.context_menu_visible())
15135 .await;
15136 cx.update_editor(|editor, window, cx| {
15137 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15138 {
15139 assert_eq!(
15140 completion_menu_entries(menu),
15141 &["let"],
15142 "With no digits in the completion query, no digits should be in the word completions"
15143 );
15144 } else {
15145 panic!("expected completion menu to be open");
15146 }
15147 editor.cancel(&Cancel, window, cx);
15148 });
15149
15150 cx.set_state(indoc! {"3ˇ
15151 0_usize
15152 let
15153 3
15154 33.35f32
15155 "});
15156 cx.update_editor(|editor, window, cx| {
15157 editor.show_completions(&ShowCompletions, window, cx);
15158 });
15159 cx.executor().run_until_parked();
15160 cx.condition(|editor, _| editor.context_menu_visible())
15161 .await;
15162 cx.update_editor(|editor, _, _| {
15163 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15164 {
15165 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15166 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15167 } else {
15168 panic!("expected completion menu to be open");
15169 }
15170 });
15171}
15172
15173#[gpui::test]
15174async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15175 init_test(cx, |language_settings| {
15176 language_settings.defaults.completions = Some(CompletionSettingsContent {
15177 words: Some(WordsCompletionMode::Enabled),
15178 words_min_length: Some(3),
15179 lsp_insert_mode: Some(LspInsertMode::Insert),
15180 ..Default::default()
15181 });
15182 });
15183
15184 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15185 cx.set_state(indoc! {"ˇ
15186 wow
15187 wowen
15188 wowser
15189 "});
15190 cx.simulate_keystroke("w");
15191 cx.executor().run_until_parked();
15192 cx.update_editor(|editor, _, _| {
15193 if editor.context_menu.borrow_mut().is_some() {
15194 panic!(
15195 "expected completion menu to be hidden, as words completion threshold is not met"
15196 );
15197 }
15198 });
15199
15200 cx.update_editor(|editor, window, cx| {
15201 editor.show_word_completions(&ShowWordCompletions, window, cx);
15202 });
15203 cx.executor().run_until_parked();
15204 cx.update_editor(|editor, window, cx| {
15205 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15206 {
15207 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");
15208 } else {
15209 panic!("expected completion menu to be open after the word completions are called with an action");
15210 }
15211
15212 editor.cancel(&Cancel, window, cx);
15213 });
15214 cx.update_editor(|editor, _, _| {
15215 if editor.context_menu.borrow_mut().is_some() {
15216 panic!("expected completion menu to be hidden after canceling");
15217 }
15218 });
15219
15220 cx.simulate_keystroke("o");
15221 cx.executor().run_until_parked();
15222 cx.update_editor(|editor, _, _| {
15223 if editor.context_menu.borrow_mut().is_some() {
15224 panic!(
15225 "expected completion menu to be hidden, as words completion threshold is not met still"
15226 );
15227 }
15228 });
15229
15230 cx.simulate_keystroke("w");
15231 cx.executor().run_until_parked();
15232 cx.update_editor(|editor, _, _| {
15233 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15234 {
15235 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15236 } else {
15237 panic!("expected completion menu to be open after the word completions threshold is met");
15238 }
15239 });
15240}
15241
15242#[gpui::test]
15243async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15244 init_test(cx, |language_settings| {
15245 language_settings.defaults.completions = Some(CompletionSettingsContent {
15246 words: Some(WordsCompletionMode::Enabled),
15247 words_min_length: Some(0),
15248 lsp_insert_mode: Some(LspInsertMode::Insert),
15249 ..Default::default()
15250 });
15251 });
15252
15253 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15254 cx.update_editor(|editor, _, _| {
15255 editor.disable_word_completions();
15256 });
15257 cx.set_state(indoc! {"ˇ
15258 wow
15259 wowen
15260 wowser
15261 "});
15262 cx.simulate_keystroke("w");
15263 cx.executor().run_until_parked();
15264 cx.update_editor(|editor, _, _| {
15265 if editor.context_menu.borrow_mut().is_some() {
15266 panic!(
15267 "expected completion menu to be hidden, as words completion are disabled for this editor"
15268 );
15269 }
15270 });
15271
15272 cx.update_editor(|editor, window, cx| {
15273 editor.show_word_completions(&ShowWordCompletions, window, cx);
15274 });
15275 cx.executor().run_until_parked();
15276 cx.update_editor(|editor, _, _| {
15277 if editor.context_menu.borrow_mut().is_some() {
15278 panic!(
15279 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15280 );
15281 }
15282 });
15283}
15284
15285#[gpui::test]
15286async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15287 init_test(cx, |language_settings| {
15288 language_settings.defaults.completions = Some(CompletionSettingsContent {
15289 words: Some(WordsCompletionMode::Disabled),
15290 words_min_length: Some(0),
15291 lsp_insert_mode: Some(LspInsertMode::Insert),
15292 ..Default::default()
15293 });
15294 });
15295
15296 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15297 cx.update_editor(|editor, _, _| {
15298 editor.set_completion_provider(None);
15299 });
15300 cx.set_state(indoc! {"ˇ
15301 wow
15302 wowen
15303 wowser
15304 "});
15305 cx.simulate_keystroke("w");
15306 cx.executor().run_until_parked();
15307 cx.update_editor(|editor, _, _| {
15308 if editor.context_menu.borrow_mut().is_some() {
15309 panic!("expected completion menu to be hidden, as disabled in settings");
15310 }
15311 });
15312}
15313
15314fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15315 let position = || lsp::Position {
15316 line: params.text_document_position.position.line,
15317 character: params.text_document_position.position.character,
15318 };
15319 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15320 range: lsp::Range {
15321 start: position(),
15322 end: position(),
15323 },
15324 new_text: text.to_string(),
15325 }))
15326}
15327
15328#[gpui::test]
15329async fn test_multiline_completion(cx: &mut TestAppContext) {
15330 init_test(cx, |_| {});
15331
15332 let fs = FakeFs::new(cx.executor());
15333 fs.insert_tree(
15334 path!("/a"),
15335 json!({
15336 "main.ts": "a",
15337 }),
15338 )
15339 .await;
15340
15341 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15342 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15343 let typescript_language = Arc::new(Language::new(
15344 LanguageConfig {
15345 name: "TypeScript".into(),
15346 matcher: LanguageMatcher {
15347 path_suffixes: vec!["ts".to_string()],
15348 ..LanguageMatcher::default()
15349 },
15350 line_comments: vec!["// ".into()],
15351 ..LanguageConfig::default()
15352 },
15353 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15354 ));
15355 language_registry.add(typescript_language.clone());
15356 let mut fake_servers = language_registry.register_fake_lsp(
15357 "TypeScript",
15358 FakeLspAdapter {
15359 capabilities: lsp::ServerCapabilities {
15360 completion_provider: Some(lsp::CompletionOptions {
15361 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15362 ..lsp::CompletionOptions::default()
15363 }),
15364 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15365 ..lsp::ServerCapabilities::default()
15366 },
15367 // Emulate vtsls label generation
15368 label_for_completion: Some(Box::new(|item, _| {
15369 let text = if let Some(description) = item
15370 .label_details
15371 .as_ref()
15372 .and_then(|label_details| label_details.description.as_ref())
15373 {
15374 format!("{} {}", item.label, description)
15375 } else if let Some(detail) = &item.detail {
15376 format!("{} {}", item.label, detail)
15377 } else {
15378 item.label.clone()
15379 };
15380 Some(language::CodeLabel::plain(text, None))
15381 })),
15382 ..FakeLspAdapter::default()
15383 },
15384 );
15385 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15386 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15387 let worktree_id = workspace
15388 .update(cx, |workspace, _window, cx| {
15389 workspace.project().update(cx, |project, cx| {
15390 project.worktrees(cx).next().unwrap().read(cx).id()
15391 })
15392 })
15393 .unwrap();
15394 let _buffer = project
15395 .update(cx, |project, cx| {
15396 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15397 })
15398 .await
15399 .unwrap();
15400 let editor = workspace
15401 .update(cx, |workspace, window, cx| {
15402 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15403 })
15404 .unwrap()
15405 .await
15406 .unwrap()
15407 .downcast::<Editor>()
15408 .unwrap();
15409 let fake_server = fake_servers.next().await.unwrap();
15410
15411 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15412 let multiline_label_2 = "a\nb\nc\n";
15413 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15414 let multiline_description = "d\ne\nf\n";
15415 let multiline_detail_2 = "g\nh\ni\n";
15416
15417 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15418 move |params, _| async move {
15419 Ok(Some(lsp::CompletionResponse::Array(vec![
15420 lsp::CompletionItem {
15421 label: multiline_label.to_string(),
15422 text_edit: gen_text_edit(¶ms, "new_text_1"),
15423 ..lsp::CompletionItem::default()
15424 },
15425 lsp::CompletionItem {
15426 label: "single line label 1".to_string(),
15427 detail: Some(multiline_detail.to_string()),
15428 text_edit: gen_text_edit(¶ms, "new_text_2"),
15429 ..lsp::CompletionItem::default()
15430 },
15431 lsp::CompletionItem {
15432 label: "single line label 2".to_string(),
15433 label_details: Some(lsp::CompletionItemLabelDetails {
15434 description: Some(multiline_description.to_string()),
15435 detail: None,
15436 }),
15437 text_edit: gen_text_edit(¶ms, "new_text_2"),
15438 ..lsp::CompletionItem::default()
15439 },
15440 lsp::CompletionItem {
15441 label: multiline_label_2.to_string(),
15442 detail: Some(multiline_detail_2.to_string()),
15443 text_edit: gen_text_edit(¶ms, "new_text_3"),
15444 ..lsp::CompletionItem::default()
15445 },
15446 lsp::CompletionItem {
15447 label: "Label with many spaces and \t but without newlines".to_string(),
15448 detail: Some(
15449 "Details with many spaces and \t but without newlines".to_string(),
15450 ),
15451 text_edit: gen_text_edit(¶ms, "new_text_4"),
15452 ..lsp::CompletionItem::default()
15453 },
15454 ])))
15455 },
15456 );
15457
15458 editor.update_in(cx, |editor, window, cx| {
15459 cx.focus_self(window);
15460 editor.move_to_end(&MoveToEnd, window, cx);
15461 editor.handle_input(".", window, cx);
15462 });
15463 cx.run_until_parked();
15464 completion_handle.next().await.unwrap();
15465
15466 editor.update(cx, |editor, _| {
15467 assert!(editor.context_menu_visible());
15468 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15469 {
15470 let completion_labels = menu
15471 .completions
15472 .borrow()
15473 .iter()
15474 .map(|c| c.label.text.clone())
15475 .collect::<Vec<_>>();
15476 assert_eq!(
15477 completion_labels,
15478 &[
15479 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15480 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15481 "single line label 2 d e f ",
15482 "a b c g h i ",
15483 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15484 ],
15485 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15486 );
15487
15488 for completion in menu
15489 .completions
15490 .borrow()
15491 .iter() {
15492 assert_eq!(
15493 completion.label.filter_range,
15494 0..completion.label.text.len(),
15495 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15496 );
15497 }
15498 } else {
15499 panic!("expected completion menu to be open");
15500 }
15501 });
15502}
15503
15504#[gpui::test]
15505async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15506 init_test(cx, |_| {});
15507 let mut cx = EditorLspTestContext::new_rust(
15508 lsp::ServerCapabilities {
15509 completion_provider: Some(lsp::CompletionOptions {
15510 trigger_characters: Some(vec![".".to_string()]),
15511 ..Default::default()
15512 }),
15513 ..Default::default()
15514 },
15515 cx,
15516 )
15517 .await;
15518 cx.lsp
15519 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15520 Ok(Some(lsp::CompletionResponse::Array(vec![
15521 lsp::CompletionItem {
15522 label: "first".into(),
15523 ..Default::default()
15524 },
15525 lsp::CompletionItem {
15526 label: "last".into(),
15527 ..Default::default()
15528 },
15529 ])))
15530 });
15531 cx.set_state("variableˇ");
15532 cx.simulate_keystroke(".");
15533 cx.executor().run_until_parked();
15534
15535 cx.update_editor(|editor, _, _| {
15536 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15537 {
15538 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15539 } else {
15540 panic!("expected completion menu to be open");
15541 }
15542 });
15543
15544 cx.update_editor(|editor, window, cx| {
15545 editor.move_page_down(&MovePageDown::default(), window, cx);
15546 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15547 {
15548 assert!(
15549 menu.selected_item == 1,
15550 "expected PageDown to select the last item from the context menu"
15551 );
15552 } else {
15553 panic!("expected completion menu to stay open after PageDown");
15554 }
15555 });
15556
15557 cx.update_editor(|editor, window, cx| {
15558 editor.move_page_up(&MovePageUp::default(), window, cx);
15559 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15560 {
15561 assert!(
15562 menu.selected_item == 0,
15563 "expected PageUp to select the first item from the context menu"
15564 );
15565 } else {
15566 panic!("expected completion menu to stay open after PageUp");
15567 }
15568 });
15569}
15570
15571#[gpui::test]
15572async fn test_as_is_completions(cx: &mut TestAppContext) {
15573 init_test(cx, |_| {});
15574 let mut cx = EditorLspTestContext::new_rust(
15575 lsp::ServerCapabilities {
15576 completion_provider: Some(lsp::CompletionOptions {
15577 ..Default::default()
15578 }),
15579 ..Default::default()
15580 },
15581 cx,
15582 )
15583 .await;
15584 cx.lsp
15585 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15586 Ok(Some(lsp::CompletionResponse::Array(vec![
15587 lsp::CompletionItem {
15588 label: "unsafe".into(),
15589 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15590 range: lsp::Range {
15591 start: lsp::Position {
15592 line: 1,
15593 character: 2,
15594 },
15595 end: lsp::Position {
15596 line: 1,
15597 character: 3,
15598 },
15599 },
15600 new_text: "unsafe".to_string(),
15601 })),
15602 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15603 ..Default::default()
15604 },
15605 ])))
15606 });
15607 cx.set_state("fn a() {}\n nˇ");
15608 cx.executor().run_until_parked();
15609 cx.update_editor(|editor, window, cx| {
15610 editor.trigger_completion_on_input("n", true, window, cx)
15611 });
15612 cx.executor().run_until_parked();
15613
15614 cx.update_editor(|editor, window, cx| {
15615 editor.confirm_completion(&Default::default(), window, cx)
15616 });
15617 cx.executor().run_until_parked();
15618 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15619}
15620
15621#[gpui::test]
15622async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15623 init_test(cx, |_| {});
15624 let language =
15625 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15626 let mut cx = EditorLspTestContext::new(
15627 language,
15628 lsp::ServerCapabilities {
15629 completion_provider: Some(lsp::CompletionOptions {
15630 ..lsp::CompletionOptions::default()
15631 }),
15632 ..lsp::ServerCapabilities::default()
15633 },
15634 cx,
15635 )
15636 .await;
15637
15638 cx.set_state(
15639 "#ifndef BAR_H
15640#define BAR_H
15641
15642#include <stdbool.h>
15643
15644int fn_branch(bool do_branch1, bool do_branch2);
15645
15646#endif // BAR_H
15647ˇ",
15648 );
15649 cx.executor().run_until_parked();
15650 cx.update_editor(|editor, window, cx| {
15651 editor.handle_input("#", window, cx);
15652 });
15653 cx.executor().run_until_parked();
15654 cx.update_editor(|editor, window, cx| {
15655 editor.handle_input("i", window, cx);
15656 });
15657 cx.executor().run_until_parked();
15658 cx.update_editor(|editor, window, cx| {
15659 editor.handle_input("n", window, cx);
15660 });
15661 cx.executor().run_until_parked();
15662 cx.assert_editor_state(
15663 "#ifndef BAR_H
15664#define BAR_H
15665
15666#include <stdbool.h>
15667
15668int fn_branch(bool do_branch1, bool do_branch2);
15669
15670#endif // BAR_H
15671#inˇ",
15672 );
15673
15674 cx.lsp
15675 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15676 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15677 is_incomplete: false,
15678 item_defaults: None,
15679 items: vec![lsp::CompletionItem {
15680 kind: Some(lsp::CompletionItemKind::SNIPPET),
15681 label_details: Some(lsp::CompletionItemLabelDetails {
15682 detail: Some("header".to_string()),
15683 description: None,
15684 }),
15685 label: " include".to_string(),
15686 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15687 range: lsp::Range {
15688 start: lsp::Position {
15689 line: 8,
15690 character: 1,
15691 },
15692 end: lsp::Position {
15693 line: 8,
15694 character: 1,
15695 },
15696 },
15697 new_text: "include \"$0\"".to_string(),
15698 })),
15699 sort_text: Some("40b67681include".to_string()),
15700 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15701 filter_text: Some("include".to_string()),
15702 insert_text: Some("include \"$0\"".to_string()),
15703 ..lsp::CompletionItem::default()
15704 }],
15705 })))
15706 });
15707 cx.update_editor(|editor, window, cx| {
15708 editor.show_completions(&ShowCompletions, window, cx);
15709 });
15710 cx.executor().run_until_parked();
15711 cx.update_editor(|editor, window, cx| {
15712 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15713 });
15714 cx.executor().run_until_parked();
15715 cx.assert_editor_state(
15716 "#ifndef BAR_H
15717#define BAR_H
15718
15719#include <stdbool.h>
15720
15721int fn_branch(bool do_branch1, bool do_branch2);
15722
15723#endif // BAR_H
15724#include \"ˇ\"",
15725 );
15726
15727 cx.lsp
15728 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15729 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15730 is_incomplete: true,
15731 item_defaults: None,
15732 items: vec![lsp::CompletionItem {
15733 kind: Some(lsp::CompletionItemKind::FILE),
15734 label: "AGL/".to_string(),
15735 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15736 range: lsp::Range {
15737 start: lsp::Position {
15738 line: 8,
15739 character: 10,
15740 },
15741 end: lsp::Position {
15742 line: 8,
15743 character: 11,
15744 },
15745 },
15746 new_text: "AGL/".to_string(),
15747 })),
15748 sort_text: Some("40b67681AGL/".to_string()),
15749 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15750 filter_text: Some("AGL/".to_string()),
15751 insert_text: Some("AGL/".to_string()),
15752 ..lsp::CompletionItem::default()
15753 }],
15754 })))
15755 });
15756 cx.update_editor(|editor, window, cx| {
15757 editor.show_completions(&ShowCompletions, window, cx);
15758 });
15759 cx.executor().run_until_parked();
15760 cx.update_editor(|editor, window, cx| {
15761 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15762 });
15763 cx.executor().run_until_parked();
15764 cx.assert_editor_state(
15765 r##"#ifndef BAR_H
15766#define BAR_H
15767
15768#include <stdbool.h>
15769
15770int fn_branch(bool do_branch1, bool do_branch2);
15771
15772#endif // BAR_H
15773#include "AGL/ˇ"##,
15774 );
15775
15776 cx.update_editor(|editor, window, cx| {
15777 editor.handle_input("\"", window, cx);
15778 });
15779 cx.executor().run_until_parked();
15780 cx.assert_editor_state(
15781 r##"#ifndef BAR_H
15782#define BAR_H
15783
15784#include <stdbool.h>
15785
15786int fn_branch(bool do_branch1, bool do_branch2);
15787
15788#endif // BAR_H
15789#include "AGL/"ˇ"##,
15790 );
15791}
15792
15793#[gpui::test]
15794async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15795 init_test(cx, |_| {});
15796
15797 let mut cx = EditorLspTestContext::new_rust(
15798 lsp::ServerCapabilities {
15799 completion_provider: Some(lsp::CompletionOptions {
15800 trigger_characters: Some(vec![".".to_string()]),
15801 resolve_provider: Some(true),
15802 ..Default::default()
15803 }),
15804 ..Default::default()
15805 },
15806 cx,
15807 )
15808 .await;
15809
15810 cx.set_state("fn main() { let a = 2ˇ; }");
15811 cx.simulate_keystroke(".");
15812 let completion_item = lsp::CompletionItem {
15813 label: "Some".into(),
15814 kind: Some(lsp::CompletionItemKind::SNIPPET),
15815 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15816 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15817 kind: lsp::MarkupKind::Markdown,
15818 value: "```rust\nSome(2)\n```".to_string(),
15819 })),
15820 deprecated: Some(false),
15821 sort_text: Some("Some".to_string()),
15822 filter_text: Some("Some".to_string()),
15823 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15824 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15825 range: lsp::Range {
15826 start: lsp::Position {
15827 line: 0,
15828 character: 22,
15829 },
15830 end: lsp::Position {
15831 line: 0,
15832 character: 22,
15833 },
15834 },
15835 new_text: "Some(2)".to_string(),
15836 })),
15837 additional_text_edits: Some(vec![lsp::TextEdit {
15838 range: lsp::Range {
15839 start: lsp::Position {
15840 line: 0,
15841 character: 20,
15842 },
15843 end: lsp::Position {
15844 line: 0,
15845 character: 22,
15846 },
15847 },
15848 new_text: "".to_string(),
15849 }]),
15850 ..Default::default()
15851 };
15852
15853 let closure_completion_item = completion_item.clone();
15854 let counter = Arc::new(AtomicUsize::new(0));
15855 let counter_clone = counter.clone();
15856 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15857 let task_completion_item = closure_completion_item.clone();
15858 counter_clone.fetch_add(1, atomic::Ordering::Release);
15859 async move {
15860 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15861 is_incomplete: true,
15862 item_defaults: None,
15863 items: vec![task_completion_item],
15864 })))
15865 }
15866 });
15867
15868 cx.condition(|editor, _| editor.context_menu_visible())
15869 .await;
15870 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15871 assert!(request.next().await.is_some());
15872 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15873
15874 cx.simulate_keystrokes("S o m");
15875 cx.condition(|editor, _| editor.context_menu_visible())
15876 .await;
15877 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15878 assert!(request.next().await.is_some());
15879 assert!(request.next().await.is_some());
15880 assert!(request.next().await.is_some());
15881 request.close();
15882 assert!(request.next().await.is_none());
15883 assert_eq!(
15884 counter.load(atomic::Ordering::Acquire),
15885 4,
15886 "With the completions menu open, only one LSP request should happen per input"
15887 );
15888}
15889
15890#[gpui::test]
15891async fn test_toggle_comment(cx: &mut TestAppContext) {
15892 init_test(cx, |_| {});
15893 let mut cx = EditorTestContext::new(cx).await;
15894 let language = Arc::new(Language::new(
15895 LanguageConfig {
15896 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15897 ..Default::default()
15898 },
15899 Some(tree_sitter_rust::LANGUAGE.into()),
15900 ));
15901 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15902
15903 // If multiple selections intersect a line, the line is only toggled once.
15904 cx.set_state(indoc! {"
15905 fn a() {
15906 «//b();
15907 ˇ»// «c();
15908 //ˇ» d();
15909 }
15910 "});
15911
15912 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15913
15914 cx.assert_editor_state(indoc! {"
15915 fn a() {
15916 «b();
15917 c();
15918 ˇ» d();
15919 }
15920 "});
15921
15922 // The comment prefix is inserted at the same column for every line in a
15923 // selection.
15924 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15925
15926 cx.assert_editor_state(indoc! {"
15927 fn a() {
15928 // «b();
15929 // c();
15930 ˇ»// d();
15931 }
15932 "});
15933
15934 // If a selection ends at the beginning of a line, that line is not toggled.
15935 cx.set_selections_state(indoc! {"
15936 fn a() {
15937 // b();
15938 «// c();
15939 ˇ» // d();
15940 }
15941 "});
15942
15943 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15944
15945 cx.assert_editor_state(indoc! {"
15946 fn a() {
15947 // b();
15948 «c();
15949 ˇ» // d();
15950 }
15951 "});
15952
15953 // If a selection span a single line and is empty, the line is toggled.
15954 cx.set_state(indoc! {"
15955 fn a() {
15956 a();
15957 b();
15958 ˇ
15959 }
15960 "});
15961
15962 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15963
15964 cx.assert_editor_state(indoc! {"
15965 fn a() {
15966 a();
15967 b();
15968 //•ˇ
15969 }
15970 "});
15971
15972 // If a selection span multiple lines, empty lines are not toggled.
15973 cx.set_state(indoc! {"
15974 fn a() {
15975 «a();
15976
15977 c();ˇ»
15978 }
15979 "});
15980
15981 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15982
15983 cx.assert_editor_state(indoc! {"
15984 fn a() {
15985 // «a();
15986
15987 // c();ˇ»
15988 }
15989 "});
15990
15991 // If a selection includes multiple comment prefixes, all lines are uncommented.
15992 cx.set_state(indoc! {"
15993 fn a() {
15994 «// a();
15995 /// b();
15996 //! c();ˇ»
15997 }
15998 "});
15999
16000 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16001
16002 cx.assert_editor_state(indoc! {"
16003 fn a() {
16004 «a();
16005 b();
16006 c();ˇ»
16007 }
16008 "});
16009}
16010
16011#[gpui::test]
16012async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16013 init_test(cx, |_| {});
16014 let mut cx = EditorTestContext::new(cx).await;
16015 let language = Arc::new(Language::new(
16016 LanguageConfig {
16017 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16018 ..Default::default()
16019 },
16020 Some(tree_sitter_rust::LANGUAGE.into()),
16021 ));
16022 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16023
16024 let toggle_comments = &ToggleComments {
16025 advance_downwards: false,
16026 ignore_indent: true,
16027 };
16028
16029 // If multiple selections intersect a line, the line is only toggled once.
16030 cx.set_state(indoc! {"
16031 fn a() {
16032 // «b();
16033 // c();
16034 // ˇ» d();
16035 }
16036 "});
16037
16038 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16039
16040 cx.assert_editor_state(indoc! {"
16041 fn a() {
16042 «b();
16043 c();
16044 ˇ» d();
16045 }
16046 "});
16047
16048 // The comment prefix is inserted at the beginning of each line
16049 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16050
16051 cx.assert_editor_state(indoc! {"
16052 fn a() {
16053 // «b();
16054 // c();
16055 // ˇ» d();
16056 }
16057 "});
16058
16059 // If a selection ends at the beginning of a line, that line is not toggled.
16060 cx.set_selections_state(indoc! {"
16061 fn a() {
16062 // b();
16063 // «c();
16064 ˇ»// d();
16065 }
16066 "});
16067
16068 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16069
16070 cx.assert_editor_state(indoc! {"
16071 fn a() {
16072 // b();
16073 «c();
16074 ˇ»// d();
16075 }
16076 "});
16077
16078 // If a selection span a single line and is empty, the line is toggled.
16079 cx.set_state(indoc! {"
16080 fn a() {
16081 a();
16082 b();
16083 ˇ
16084 }
16085 "});
16086
16087 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16088
16089 cx.assert_editor_state(indoc! {"
16090 fn a() {
16091 a();
16092 b();
16093 //ˇ
16094 }
16095 "});
16096
16097 // If a selection span multiple lines, empty lines are not toggled.
16098 cx.set_state(indoc! {"
16099 fn a() {
16100 «a();
16101
16102 c();ˇ»
16103 }
16104 "});
16105
16106 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16107
16108 cx.assert_editor_state(indoc! {"
16109 fn a() {
16110 // «a();
16111
16112 // c();ˇ»
16113 }
16114 "});
16115
16116 // If a selection includes multiple comment prefixes, all lines are uncommented.
16117 cx.set_state(indoc! {"
16118 fn a() {
16119 // «a();
16120 /// b();
16121 //! c();ˇ»
16122 }
16123 "});
16124
16125 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16126
16127 cx.assert_editor_state(indoc! {"
16128 fn a() {
16129 «a();
16130 b();
16131 c();ˇ»
16132 }
16133 "});
16134}
16135
16136#[gpui::test]
16137async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16138 init_test(cx, |_| {});
16139
16140 let language = Arc::new(Language::new(
16141 LanguageConfig {
16142 line_comments: vec!["// ".into()],
16143 ..Default::default()
16144 },
16145 Some(tree_sitter_rust::LANGUAGE.into()),
16146 ));
16147
16148 let mut cx = EditorTestContext::new(cx).await;
16149
16150 cx.language_registry().add(language.clone());
16151 cx.update_buffer(|buffer, cx| {
16152 buffer.set_language(Some(language), cx);
16153 });
16154
16155 let toggle_comments = &ToggleComments {
16156 advance_downwards: true,
16157 ignore_indent: false,
16158 };
16159
16160 // Single cursor on one line -> advance
16161 // Cursor moves horizontally 3 characters as well on non-blank line
16162 cx.set_state(indoc!(
16163 "fn a() {
16164 ˇdog();
16165 cat();
16166 }"
16167 ));
16168 cx.update_editor(|editor, window, cx| {
16169 editor.toggle_comments(toggle_comments, window, cx);
16170 });
16171 cx.assert_editor_state(indoc!(
16172 "fn a() {
16173 // dog();
16174 catˇ();
16175 }"
16176 ));
16177
16178 // Single selection on one line -> don't advance
16179 cx.set_state(indoc!(
16180 "fn a() {
16181 «dog()ˇ»;
16182 cat();
16183 }"
16184 ));
16185 cx.update_editor(|editor, window, cx| {
16186 editor.toggle_comments(toggle_comments, window, cx);
16187 });
16188 cx.assert_editor_state(indoc!(
16189 "fn a() {
16190 // «dog()ˇ»;
16191 cat();
16192 }"
16193 ));
16194
16195 // Multiple cursors on one line -> advance
16196 cx.set_state(indoc!(
16197 "fn a() {
16198 ˇdˇog();
16199 cat();
16200 }"
16201 ));
16202 cx.update_editor(|editor, window, cx| {
16203 editor.toggle_comments(toggle_comments, window, cx);
16204 });
16205 cx.assert_editor_state(indoc!(
16206 "fn a() {
16207 // dog();
16208 catˇ(ˇ);
16209 }"
16210 ));
16211
16212 // Multiple cursors on one line, with selection -> don't advance
16213 cx.set_state(indoc!(
16214 "fn a() {
16215 ˇdˇog«()ˇ»;
16216 cat();
16217 }"
16218 ));
16219 cx.update_editor(|editor, window, cx| {
16220 editor.toggle_comments(toggle_comments, window, cx);
16221 });
16222 cx.assert_editor_state(indoc!(
16223 "fn a() {
16224 // ˇdˇog«()ˇ»;
16225 cat();
16226 }"
16227 ));
16228
16229 // Single cursor on one line -> advance
16230 // Cursor moves to column 0 on blank line
16231 cx.set_state(indoc!(
16232 "fn a() {
16233 ˇdog();
16234
16235 cat();
16236 }"
16237 ));
16238 cx.update_editor(|editor, window, cx| {
16239 editor.toggle_comments(toggle_comments, window, cx);
16240 });
16241 cx.assert_editor_state(indoc!(
16242 "fn a() {
16243 // dog();
16244 ˇ
16245 cat();
16246 }"
16247 ));
16248
16249 // Single cursor on one line -> advance
16250 // Cursor starts and ends at column 0
16251 cx.set_state(indoc!(
16252 "fn a() {
16253 ˇ dog();
16254 cat();
16255 }"
16256 ));
16257 cx.update_editor(|editor, window, cx| {
16258 editor.toggle_comments(toggle_comments, window, cx);
16259 });
16260 cx.assert_editor_state(indoc!(
16261 "fn a() {
16262 // dog();
16263 ˇ cat();
16264 }"
16265 ));
16266}
16267
16268#[gpui::test]
16269async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16270 init_test(cx, |_| {});
16271
16272 let mut cx = EditorTestContext::new(cx).await;
16273
16274 let html_language = Arc::new(
16275 Language::new(
16276 LanguageConfig {
16277 name: "HTML".into(),
16278 block_comment: Some(BlockCommentConfig {
16279 start: "<!-- ".into(),
16280 prefix: "".into(),
16281 end: " -->".into(),
16282 tab_size: 0,
16283 }),
16284 ..Default::default()
16285 },
16286 Some(tree_sitter_html::LANGUAGE.into()),
16287 )
16288 .with_injection_query(
16289 r#"
16290 (script_element
16291 (raw_text) @injection.content
16292 (#set! injection.language "javascript"))
16293 "#,
16294 )
16295 .unwrap(),
16296 );
16297
16298 let javascript_language = Arc::new(Language::new(
16299 LanguageConfig {
16300 name: "JavaScript".into(),
16301 line_comments: vec!["// ".into()],
16302 ..Default::default()
16303 },
16304 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16305 ));
16306
16307 cx.language_registry().add(html_language.clone());
16308 cx.language_registry().add(javascript_language);
16309 cx.update_buffer(|buffer, cx| {
16310 buffer.set_language(Some(html_language), cx);
16311 });
16312
16313 // Toggle comments for empty selections
16314 cx.set_state(
16315 &r#"
16316 <p>A</p>ˇ
16317 <p>B</p>ˇ
16318 <p>C</p>ˇ
16319 "#
16320 .unindent(),
16321 );
16322 cx.update_editor(|editor, window, cx| {
16323 editor.toggle_comments(&ToggleComments::default(), window, cx)
16324 });
16325 cx.assert_editor_state(
16326 &r#"
16327 <!-- <p>A</p>ˇ -->
16328 <!-- <p>B</p>ˇ -->
16329 <!-- <p>C</p>ˇ -->
16330 "#
16331 .unindent(),
16332 );
16333 cx.update_editor(|editor, window, cx| {
16334 editor.toggle_comments(&ToggleComments::default(), window, cx)
16335 });
16336 cx.assert_editor_state(
16337 &r#"
16338 <p>A</p>ˇ
16339 <p>B</p>ˇ
16340 <p>C</p>ˇ
16341 "#
16342 .unindent(),
16343 );
16344
16345 // Toggle comments for mixture of empty and non-empty selections, where
16346 // multiple selections occupy a given line.
16347 cx.set_state(
16348 &r#"
16349 <p>A«</p>
16350 <p>ˇ»B</p>ˇ
16351 <p>C«</p>
16352 <p>ˇ»D</p>ˇ
16353 "#
16354 .unindent(),
16355 );
16356
16357 cx.update_editor(|editor, window, cx| {
16358 editor.toggle_comments(&ToggleComments::default(), window, cx)
16359 });
16360 cx.assert_editor_state(
16361 &r#"
16362 <!-- <p>A«</p>
16363 <p>ˇ»B</p>ˇ -->
16364 <!-- <p>C«</p>
16365 <p>ˇ»D</p>ˇ -->
16366 "#
16367 .unindent(),
16368 );
16369 cx.update_editor(|editor, window, cx| {
16370 editor.toggle_comments(&ToggleComments::default(), window, cx)
16371 });
16372 cx.assert_editor_state(
16373 &r#"
16374 <p>A«</p>
16375 <p>ˇ»B</p>ˇ
16376 <p>C«</p>
16377 <p>ˇ»D</p>ˇ
16378 "#
16379 .unindent(),
16380 );
16381
16382 // Toggle comments when different languages are active for different
16383 // selections.
16384 cx.set_state(
16385 &r#"
16386 ˇ<script>
16387 ˇvar x = new Y();
16388 ˇ</script>
16389 "#
16390 .unindent(),
16391 );
16392 cx.executor().run_until_parked();
16393 cx.update_editor(|editor, window, cx| {
16394 editor.toggle_comments(&ToggleComments::default(), window, cx)
16395 });
16396 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16397 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16398 cx.assert_editor_state(
16399 &r#"
16400 <!-- ˇ<script> -->
16401 // ˇvar x = new Y();
16402 <!-- ˇ</script> -->
16403 "#
16404 .unindent(),
16405 );
16406}
16407
16408#[gpui::test]
16409fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16410 init_test(cx, |_| {});
16411
16412 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16413 let multibuffer = cx.new(|cx| {
16414 let mut multibuffer = MultiBuffer::new(ReadWrite);
16415 multibuffer.push_excerpts(
16416 buffer.clone(),
16417 [
16418 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16419 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16420 ],
16421 cx,
16422 );
16423 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16424 multibuffer
16425 });
16426
16427 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16428 editor.update_in(cx, |editor, window, cx| {
16429 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16430 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16431 s.select_ranges([
16432 Point::new(0, 0)..Point::new(0, 0),
16433 Point::new(1, 0)..Point::new(1, 0),
16434 ])
16435 });
16436
16437 editor.handle_input("X", window, cx);
16438 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16439 assert_eq!(
16440 editor.selections.ranges(&editor.display_snapshot(cx)),
16441 [
16442 Point::new(0, 1)..Point::new(0, 1),
16443 Point::new(1, 1)..Point::new(1, 1),
16444 ]
16445 );
16446
16447 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16448 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16449 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16450 });
16451 editor.backspace(&Default::default(), window, cx);
16452 assert_eq!(editor.text(cx), "Xa\nbbb");
16453 assert_eq!(
16454 editor.selections.ranges(&editor.display_snapshot(cx)),
16455 [Point::new(1, 0)..Point::new(1, 0)]
16456 );
16457
16458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16459 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16460 });
16461 editor.backspace(&Default::default(), window, cx);
16462 assert_eq!(editor.text(cx), "X\nbb");
16463 assert_eq!(
16464 editor.selections.ranges(&editor.display_snapshot(cx)),
16465 [Point::new(0, 1)..Point::new(0, 1)]
16466 );
16467 });
16468}
16469
16470#[gpui::test]
16471fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16472 init_test(cx, |_| {});
16473
16474 let markers = vec![('[', ']').into(), ('(', ')').into()];
16475 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16476 indoc! {"
16477 [aaaa
16478 (bbbb]
16479 cccc)",
16480 },
16481 markers.clone(),
16482 );
16483 let excerpt_ranges = markers.into_iter().map(|marker| {
16484 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16485 ExcerptRange::new(context)
16486 });
16487 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16488 let multibuffer = cx.new(|cx| {
16489 let mut multibuffer = MultiBuffer::new(ReadWrite);
16490 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16491 multibuffer
16492 });
16493
16494 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16495 editor.update_in(cx, |editor, window, cx| {
16496 let (expected_text, selection_ranges) = marked_text_ranges(
16497 indoc! {"
16498 aaaa
16499 bˇbbb
16500 bˇbbˇb
16501 cccc"
16502 },
16503 true,
16504 );
16505 assert_eq!(editor.text(cx), expected_text);
16506 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16507 s.select_ranges(
16508 selection_ranges
16509 .iter()
16510 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16511 )
16512 });
16513
16514 editor.handle_input("X", window, cx);
16515
16516 let (expected_text, expected_selections) = marked_text_ranges(
16517 indoc! {"
16518 aaaa
16519 bXˇbbXb
16520 bXˇbbXˇb
16521 cccc"
16522 },
16523 false,
16524 );
16525 assert_eq!(editor.text(cx), expected_text);
16526 assert_eq!(
16527 editor.selections.ranges(&editor.display_snapshot(cx)),
16528 expected_selections
16529 .iter()
16530 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16531 .collect::<Vec<_>>()
16532 );
16533
16534 editor.newline(&Newline, window, cx);
16535 let (expected_text, expected_selections) = marked_text_ranges(
16536 indoc! {"
16537 aaaa
16538 bX
16539 ˇbbX
16540 b
16541 bX
16542 ˇbbX
16543 ˇb
16544 cccc"
16545 },
16546 false,
16547 );
16548 assert_eq!(editor.text(cx), expected_text);
16549 assert_eq!(
16550 editor.selections.ranges(&editor.display_snapshot(cx)),
16551 expected_selections
16552 .iter()
16553 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16554 .collect::<Vec<_>>()
16555 );
16556 });
16557}
16558
16559#[gpui::test]
16560fn test_refresh_selections(cx: &mut TestAppContext) {
16561 init_test(cx, |_| {});
16562
16563 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16564 let mut excerpt1_id = None;
16565 let multibuffer = cx.new(|cx| {
16566 let mut multibuffer = MultiBuffer::new(ReadWrite);
16567 excerpt1_id = multibuffer
16568 .push_excerpts(
16569 buffer.clone(),
16570 [
16571 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16572 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16573 ],
16574 cx,
16575 )
16576 .into_iter()
16577 .next();
16578 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16579 multibuffer
16580 });
16581
16582 let editor = cx.add_window(|window, cx| {
16583 let mut editor = build_editor(multibuffer.clone(), window, cx);
16584 let snapshot = editor.snapshot(window, cx);
16585 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16586 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16587 });
16588 editor.begin_selection(
16589 Point::new(2, 1).to_display_point(&snapshot),
16590 true,
16591 1,
16592 window,
16593 cx,
16594 );
16595 assert_eq!(
16596 editor.selections.ranges(&editor.display_snapshot(cx)),
16597 [
16598 Point::new(1, 3)..Point::new(1, 3),
16599 Point::new(2, 1)..Point::new(2, 1),
16600 ]
16601 );
16602 editor
16603 });
16604
16605 // Refreshing selections is a no-op when excerpts haven't changed.
16606 _ = editor.update(cx, |editor, window, cx| {
16607 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16608 assert_eq!(
16609 editor.selections.ranges(&editor.display_snapshot(cx)),
16610 [
16611 Point::new(1, 3)..Point::new(1, 3),
16612 Point::new(2, 1)..Point::new(2, 1),
16613 ]
16614 );
16615 });
16616
16617 multibuffer.update(cx, |multibuffer, cx| {
16618 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16619 });
16620 _ = editor.update(cx, |editor, window, cx| {
16621 // Removing an excerpt causes the first selection to become degenerate.
16622 assert_eq!(
16623 editor.selections.ranges(&editor.display_snapshot(cx)),
16624 [
16625 Point::new(0, 0)..Point::new(0, 0),
16626 Point::new(0, 1)..Point::new(0, 1)
16627 ]
16628 );
16629
16630 // Refreshing selections will relocate the first selection to the original buffer
16631 // location.
16632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16633 assert_eq!(
16634 editor.selections.ranges(&editor.display_snapshot(cx)),
16635 [
16636 Point::new(0, 1)..Point::new(0, 1),
16637 Point::new(0, 3)..Point::new(0, 3)
16638 ]
16639 );
16640 assert!(editor.selections.pending_anchor().is_some());
16641 });
16642}
16643
16644#[gpui::test]
16645fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16646 init_test(cx, |_| {});
16647
16648 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16649 let mut excerpt1_id = None;
16650 let multibuffer = cx.new(|cx| {
16651 let mut multibuffer = MultiBuffer::new(ReadWrite);
16652 excerpt1_id = multibuffer
16653 .push_excerpts(
16654 buffer.clone(),
16655 [
16656 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16657 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16658 ],
16659 cx,
16660 )
16661 .into_iter()
16662 .next();
16663 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16664 multibuffer
16665 });
16666
16667 let editor = cx.add_window(|window, cx| {
16668 let mut editor = build_editor(multibuffer.clone(), window, cx);
16669 let snapshot = editor.snapshot(window, cx);
16670 editor.begin_selection(
16671 Point::new(1, 3).to_display_point(&snapshot),
16672 false,
16673 1,
16674 window,
16675 cx,
16676 );
16677 assert_eq!(
16678 editor.selections.ranges(&editor.display_snapshot(cx)),
16679 [Point::new(1, 3)..Point::new(1, 3)]
16680 );
16681 editor
16682 });
16683
16684 multibuffer.update(cx, |multibuffer, cx| {
16685 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16686 });
16687 _ = editor.update(cx, |editor, window, cx| {
16688 assert_eq!(
16689 editor.selections.ranges(&editor.display_snapshot(cx)),
16690 [Point::new(0, 0)..Point::new(0, 0)]
16691 );
16692
16693 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16695 assert_eq!(
16696 editor.selections.ranges(&editor.display_snapshot(cx)),
16697 [Point::new(0, 3)..Point::new(0, 3)]
16698 );
16699 assert!(editor.selections.pending_anchor().is_some());
16700 });
16701}
16702
16703#[gpui::test]
16704async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16705 init_test(cx, |_| {});
16706
16707 let language = Arc::new(
16708 Language::new(
16709 LanguageConfig {
16710 brackets: BracketPairConfig {
16711 pairs: vec![
16712 BracketPair {
16713 start: "{".to_string(),
16714 end: "}".to_string(),
16715 close: true,
16716 surround: true,
16717 newline: true,
16718 },
16719 BracketPair {
16720 start: "/* ".to_string(),
16721 end: " */".to_string(),
16722 close: true,
16723 surround: true,
16724 newline: true,
16725 },
16726 ],
16727 ..Default::default()
16728 },
16729 ..Default::default()
16730 },
16731 Some(tree_sitter_rust::LANGUAGE.into()),
16732 )
16733 .with_indents_query("")
16734 .unwrap(),
16735 );
16736
16737 let text = concat!(
16738 "{ }\n", //
16739 " x\n", //
16740 " /* */\n", //
16741 "x\n", //
16742 "{{} }\n", //
16743 );
16744
16745 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16746 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16747 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16748 editor
16749 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16750 .await;
16751
16752 editor.update_in(cx, |editor, window, cx| {
16753 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16754 s.select_display_ranges([
16755 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16756 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16757 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16758 ])
16759 });
16760 editor.newline(&Newline, window, cx);
16761
16762 assert_eq!(
16763 editor.buffer().read(cx).read(cx).text(),
16764 concat!(
16765 "{ \n", // Suppress rustfmt
16766 "\n", //
16767 "}\n", //
16768 " x\n", //
16769 " /* \n", //
16770 " \n", //
16771 " */\n", //
16772 "x\n", //
16773 "{{} \n", //
16774 "}\n", //
16775 )
16776 );
16777 });
16778}
16779
16780#[gpui::test]
16781fn test_highlighted_ranges(cx: &mut TestAppContext) {
16782 init_test(cx, |_| {});
16783
16784 let editor = cx.add_window(|window, cx| {
16785 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16786 build_editor(buffer, window, cx)
16787 });
16788
16789 _ = editor.update(cx, |editor, window, cx| {
16790 struct Type1;
16791 struct Type2;
16792
16793 let buffer = editor.buffer.read(cx).snapshot(cx);
16794
16795 let anchor_range =
16796 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16797
16798 editor.highlight_background::<Type1>(
16799 &[
16800 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16801 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16802 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16803 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16804 ],
16805 |_| Hsla::red(),
16806 cx,
16807 );
16808 editor.highlight_background::<Type2>(
16809 &[
16810 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16811 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16812 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16813 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16814 ],
16815 |_| Hsla::green(),
16816 cx,
16817 );
16818
16819 let snapshot = editor.snapshot(window, cx);
16820 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16821 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16822 &snapshot,
16823 cx.theme(),
16824 );
16825 assert_eq!(
16826 highlighted_ranges,
16827 &[
16828 (
16829 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16830 Hsla::green(),
16831 ),
16832 (
16833 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16834 Hsla::red(),
16835 ),
16836 (
16837 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16838 Hsla::green(),
16839 ),
16840 (
16841 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16842 Hsla::red(),
16843 ),
16844 ]
16845 );
16846 assert_eq!(
16847 editor.sorted_background_highlights_in_range(
16848 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16849 &snapshot,
16850 cx.theme(),
16851 ),
16852 &[(
16853 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16854 Hsla::red(),
16855 )]
16856 );
16857 });
16858}
16859
16860#[gpui::test]
16861async fn test_following(cx: &mut TestAppContext) {
16862 init_test(cx, |_| {});
16863
16864 let fs = FakeFs::new(cx.executor());
16865 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16866
16867 let buffer = project.update(cx, |project, cx| {
16868 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16869 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16870 });
16871 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16872 let follower = cx.update(|cx| {
16873 cx.open_window(
16874 WindowOptions {
16875 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16876 gpui::Point::new(px(0.), px(0.)),
16877 gpui::Point::new(px(10.), px(80.)),
16878 ))),
16879 ..Default::default()
16880 },
16881 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16882 )
16883 .unwrap()
16884 });
16885
16886 let is_still_following = Rc::new(RefCell::new(true));
16887 let follower_edit_event_count = Rc::new(RefCell::new(0));
16888 let pending_update = Rc::new(RefCell::new(None));
16889 let leader_entity = leader.root(cx).unwrap();
16890 let follower_entity = follower.root(cx).unwrap();
16891 _ = follower.update(cx, {
16892 let update = pending_update.clone();
16893 let is_still_following = is_still_following.clone();
16894 let follower_edit_event_count = follower_edit_event_count.clone();
16895 |_, window, cx| {
16896 cx.subscribe_in(
16897 &leader_entity,
16898 window,
16899 move |_, leader, event, window, cx| {
16900 leader.read(cx).add_event_to_update_proto(
16901 event,
16902 &mut update.borrow_mut(),
16903 window,
16904 cx,
16905 );
16906 },
16907 )
16908 .detach();
16909
16910 cx.subscribe_in(
16911 &follower_entity,
16912 window,
16913 move |_, _, event: &EditorEvent, _window, _cx| {
16914 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16915 *is_still_following.borrow_mut() = false;
16916 }
16917
16918 if let EditorEvent::BufferEdited = event {
16919 *follower_edit_event_count.borrow_mut() += 1;
16920 }
16921 },
16922 )
16923 .detach();
16924 }
16925 });
16926
16927 // Update the selections only
16928 _ = leader.update(cx, |leader, window, cx| {
16929 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16930 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
16931 });
16932 });
16933 follower
16934 .update(cx, |follower, window, cx| {
16935 follower.apply_update_proto(
16936 &project,
16937 pending_update.borrow_mut().take().unwrap(),
16938 window,
16939 cx,
16940 )
16941 })
16942 .unwrap()
16943 .await
16944 .unwrap();
16945 _ = follower.update(cx, |follower, _, cx| {
16946 assert_eq!(
16947 follower.selections.ranges(&follower.display_snapshot(cx)),
16948 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
16949 );
16950 });
16951 assert!(*is_still_following.borrow());
16952 assert_eq!(*follower_edit_event_count.borrow(), 0);
16953
16954 // Update the scroll position only
16955 _ = leader.update(cx, |leader, window, cx| {
16956 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16957 });
16958 follower
16959 .update(cx, |follower, window, cx| {
16960 follower.apply_update_proto(
16961 &project,
16962 pending_update.borrow_mut().take().unwrap(),
16963 window,
16964 cx,
16965 )
16966 })
16967 .unwrap()
16968 .await
16969 .unwrap();
16970 assert_eq!(
16971 follower
16972 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16973 .unwrap(),
16974 gpui::Point::new(1.5, 3.5)
16975 );
16976 assert!(*is_still_following.borrow());
16977 assert_eq!(*follower_edit_event_count.borrow(), 0);
16978
16979 // Update the selections and scroll position. The follower's scroll position is updated
16980 // via autoscroll, not via the leader's exact scroll position.
16981 _ = leader.update(cx, |leader, window, cx| {
16982 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16983 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
16984 });
16985 leader.request_autoscroll(Autoscroll::newest(), cx);
16986 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16987 });
16988 follower
16989 .update(cx, |follower, window, cx| {
16990 follower.apply_update_proto(
16991 &project,
16992 pending_update.borrow_mut().take().unwrap(),
16993 window,
16994 cx,
16995 )
16996 })
16997 .unwrap()
16998 .await
16999 .unwrap();
17000 _ = follower.update(cx, |follower, _, cx| {
17001 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17002 assert_eq!(
17003 follower.selections.ranges(&follower.display_snapshot(cx)),
17004 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17005 );
17006 });
17007 assert!(*is_still_following.borrow());
17008
17009 // Creating a pending selection that precedes another selection
17010 _ = leader.update(cx, |leader, window, cx| {
17011 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17012 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17013 });
17014 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17015 });
17016 follower
17017 .update(cx, |follower, window, cx| {
17018 follower.apply_update_proto(
17019 &project,
17020 pending_update.borrow_mut().take().unwrap(),
17021 window,
17022 cx,
17023 )
17024 })
17025 .unwrap()
17026 .await
17027 .unwrap();
17028 _ = follower.update(cx, |follower, _, cx| {
17029 assert_eq!(
17030 follower.selections.ranges(&follower.display_snapshot(cx)),
17031 vec![
17032 MultiBufferOffset(0)..MultiBufferOffset(0),
17033 MultiBufferOffset(1)..MultiBufferOffset(1)
17034 ]
17035 );
17036 });
17037 assert!(*is_still_following.borrow());
17038
17039 // Extend the pending selection so that it surrounds another selection
17040 _ = leader.update(cx, |leader, window, cx| {
17041 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17042 });
17043 follower
17044 .update(cx, |follower, window, cx| {
17045 follower.apply_update_proto(
17046 &project,
17047 pending_update.borrow_mut().take().unwrap(),
17048 window,
17049 cx,
17050 )
17051 })
17052 .unwrap()
17053 .await
17054 .unwrap();
17055 _ = follower.update(cx, |follower, _, cx| {
17056 assert_eq!(
17057 follower.selections.ranges(&follower.display_snapshot(cx)),
17058 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17059 );
17060 });
17061
17062 // Scrolling locally breaks the follow
17063 _ = follower.update(cx, |follower, window, cx| {
17064 let top_anchor = follower
17065 .buffer()
17066 .read(cx)
17067 .read(cx)
17068 .anchor_after(MultiBufferOffset(0));
17069 follower.set_scroll_anchor(
17070 ScrollAnchor {
17071 anchor: top_anchor,
17072 offset: gpui::Point::new(0.0, 0.5),
17073 },
17074 window,
17075 cx,
17076 );
17077 });
17078 assert!(!(*is_still_following.borrow()));
17079}
17080
17081#[gpui::test]
17082async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17083 init_test(cx, |_| {});
17084
17085 let fs = FakeFs::new(cx.executor());
17086 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17087 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17088 let pane = workspace
17089 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17090 .unwrap();
17091
17092 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17093
17094 let leader = pane.update_in(cx, |_, window, cx| {
17095 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17096 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17097 });
17098
17099 // Start following the editor when it has no excerpts.
17100 let mut state_message =
17101 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17102 let workspace_entity = workspace.root(cx).unwrap();
17103 let follower_1 = cx
17104 .update_window(*workspace.deref(), |_, window, cx| {
17105 Editor::from_state_proto(
17106 workspace_entity,
17107 ViewId {
17108 creator: CollaboratorId::PeerId(PeerId::default()),
17109 id: 0,
17110 },
17111 &mut state_message,
17112 window,
17113 cx,
17114 )
17115 })
17116 .unwrap()
17117 .unwrap()
17118 .await
17119 .unwrap();
17120
17121 let update_message = Rc::new(RefCell::new(None));
17122 follower_1.update_in(cx, {
17123 let update = update_message.clone();
17124 |_, window, cx| {
17125 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17126 leader.read(cx).add_event_to_update_proto(
17127 event,
17128 &mut update.borrow_mut(),
17129 window,
17130 cx,
17131 );
17132 })
17133 .detach();
17134 }
17135 });
17136
17137 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17138 (
17139 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17140 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17141 )
17142 });
17143
17144 // Insert some excerpts.
17145 leader.update(cx, |leader, cx| {
17146 leader.buffer.update(cx, |multibuffer, cx| {
17147 multibuffer.set_excerpts_for_path(
17148 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17149 buffer_1.clone(),
17150 vec![
17151 Point::row_range(0..3),
17152 Point::row_range(1..6),
17153 Point::row_range(12..15),
17154 ],
17155 0,
17156 cx,
17157 );
17158 multibuffer.set_excerpts_for_path(
17159 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17160 buffer_2.clone(),
17161 vec![Point::row_range(0..6), Point::row_range(8..12)],
17162 0,
17163 cx,
17164 );
17165 });
17166 });
17167
17168 // Apply the update of adding the excerpts.
17169 follower_1
17170 .update_in(cx, |follower, window, cx| {
17171 follower.apply_update_proto(
17172 &project,
17173 update_message.borrow().clone().unwrap(),
17174 window,
17175 cx,
17176 )
17177 })
17178 .await
17179 .unwrap();
17180 assert_eq!(
17181 follower_1.update(cx, |editor, cx| editor.text(cx)),
17182 leader.update(cx, |editor, cx| editor.text(cx))
17183 );
17184 update_message.borrow_mut().take();
17185
17186 // Start following separately after it already has excerpts.
17187 let mut state_message =
17188 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17189 let workspace_entity = workspace.root(cx).unwrap();
17190 let follower_2 = cx
17191 .update_window(*workspace.deref(), |_, window, cx| {
17192 Editor::from_state_proto(
17193 workspace_entity,
17194 ViewId {
17195 creator: CollaboratorId::PeerId(PeerId::default()),
17196 id: 0,
17197 },
17198 &mut state_message,
17199 window,
17200 cx,
17201 )
17202 })
17203 .unwrap()
17204 .unwrap()
17205 .await
17206 .unwrap();
17207 assert_eq!(
17208 follower_2.update(cx, |editor, cx| editor.text(cx)),
17209 leader.update(cx, |editor, cx| editor.text(cx))
17210 );
17211
17212 // Remove some excerpts.
17213 leader.update(cx, |leader, cx| {
17214 leader.buffer.update(cx, |multibuffer, cx| {
17215 let excerpt_ids = multibuffer.excerpt_ids();
17216 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17217 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17218 });
17219 });
17220
17221 // Apply the update of removing the excerpts.
17222 follower_1
17223 .update_in(cx, |follower, window, cx| {
17224 follower.apply_update_proto(
17225 &project,
17226 update_message.borrow().clone().unwrap(),
17227 window,
17228 cx,
17229 )
17230 })
17231 .await
17232 .unwrap();
17233 follower_2
17234 .update_in(cx, |follower, window, cx| {
17235 follower.apply_update_proto(
17236 &project,
17237 update_message.borrow().clone().unwrap(),
17238 window,
17239 cx,
17240 )
17241 })
17242 .await
17243 .unwrap();
17244 update_message.borrow_mut().take();
17245 assert_eq!(
17246 follower_1.update(cx, |editor, cx| editor.text(cx)),
17247 leader.update(cx, |editor, cx| editor.text(cx))
17248 );
17249}
17250
17251#[gpui::test]
17252async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17253 init_test(cx, |_| {});
17254
17255 let mut cx = EditorTestContext::new(cx).await;
17256 let lsp_store =
17257 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17258
17259 cx.set_state(indoc! {"
17260 ˇfn func(abc def: i32) -> u32 {
17261 }
17262 "});
17263
17264 cx.update(|_, cx| {
17265 lsp_store.update(cx, |lsp_store, cx| {
17266 lsp_store
17267 .update_diagnostics(
17268 LanguageServerId(0),
17269 lsp::PublishDiagnosticsParams {
17270 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17271 version: None,
17272 diagnostics: vec![
17273 lsp::Diagnostic {
17274 range: lsp::Range::new(
17275 lsp::Position::new(0, 11),
17276 lsp::Position::new(0, 12),
17277 ),
17278 severity: Some(lsp::DiagnosticSeverity::ERROR),
17279 ..Default::default()
17280 },
17281 lsp::Diagnostic {
17282 range: lsp::Range::new(
17283 lsp::Position::new(0, 12),
17284 lsp::Position::new(0, 15),
17285 ),
17286 severity: Some(lsp::DiagnosticSeverity::ERROR),
17287 ..Default::default()
17288 },
17289 lsp::Diagnostic {
17290 range: lsp::Range::new(
17291 lsp::Position::new(0, 25),
17292 lsp::Position::new(0, 28),
17293 ),
17294 severity: Some(lsp::DiagnosticSeverity::ERROR),
17295 ..Default::default()
17296 },
17297 ],
17298 },
17299 None,
17300 DiagnosticSourceKind::Pushed,
17301 &[],
17302 cx,
17303 )
17304 .unwrap()
17305 });
17306 });
17307
17308 executor.run_until_parked();
17309
17310 cx.update_editor(|editor, window, cx| {
17311 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17312 });
17313
17314 cx.assert_editor_state(indoc! {"
17315 fn func(abc def: i32) -> ˇu32 {
17316 }
17317 "});
17318
17319 cx.update_editor(|editor, window, cx| {
17320 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17321 });
17322
17323 cx.assert_editor_state(indoc! {"
17324 fn func(abc ˇdef: i32) -> u32 {
17325 }
17326 "});
17327
17328 cx.update_editor(|editor, window, cx| {
17329 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17330 });
17331
17332 cx.assert_editor_state(indoc! {"
17333 fn func(abcˇ def: i32) -> u32 {
17334 }
17335 "});
17336
17337 cx.update_editor(|editor, window, cx| {
17338 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17339 });
17340
17341 cx.assert_editor_state(indoc! {"
17342 fn func(abc def: i32) -> ˇu32 {
17343 }
17344 "});
17345}
17346
17347#[gpui::test]
17348async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17349 init_test(cx, |_| {});
17350
17351 let mut cx = EditorTestContext::new(cx).await;
17352
17353 let diff_base = r#"
17354 use some::mod;
17355
17356 const A: u32 = 42;
17357
17358 fn main() {
17359 println!("hello");
17360
17361 println!("world");
17362 }
17363 "#
17364 .unindent();
17365
17366 // Edits are modified, removed, modified, added
17367 cx.set_state(
17368 &r#"
17369 use some::modified;
17370
17371 ˇ
17372 fn main() {
17373 println!("hello there");
17374
17375 println!("around the");
17376 println!("world");
17377 }
17378 "#
17379 .unindent(),
17380 );
17381
17382 cx.set_head_text(&diff_base);
17383 executor.run_until_parked();
17384
17385 cx.update_editor(|editor, window, cx| {
17386 //Wrap around the bottom of the buffer
17387 for _ in 0..3 {
17388 editor.go_to_next_hunk(&GoToHunk, window, cx);
17389 }
17390 });
17391
17392 cx.assert_editor_state(
17393 &r#"
17394 ˇuse some::modified;
17395
17396
17397 fn main() {
17398 println!("hello there");
17399
17400 println!("around the");
17401 println!("world");
17402 }
17403 "#
17404 .unindent(),
17405 );
17406
17407 cx.update_editor(|editor, window, cx| {
17408 //Wrap around the top of the buffer
17409 for _ in 0..2 {
17410 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17411 }
17412 });
17413
17414 cx.assert_editor_state(
17415 &r#"
17416 use some::modified;
17417
17418
17419 fn main() {
17420 ˇ println!("hello there");
17421
17422 println!("around the");
17423 println!("world");
17424 }
17425 "#
17426 .unindent(),
17427 );
17428
17429 cx.update_editor(|editor, window, cx| {
17430 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17431 });
17432
17433 cx.assert_editor_state(
17434 &r#"
17435 use some::modified;
17436
17437 ˇ
17438 fn main() {
17439 println!("hello there");
17440
17441 println!("around the");
17442 println!("world");
17443 }
17444 "#
17445 .unindent(),
17446 );
17447
17448 cx.update_editor(|editor, window, cx| {
17449 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17450 });
17451
17452 cx.assert_editor_state(
17453 &r#"
17454 ˇuse some::modified;
17455
17456
17457 fn main() {
17458 println!("hello there");
17459
17460 println!("around the");
17461 println!("world");
17462 }
17463 "#
17464 .unindent(),
17465 );
17466
17467 cx.update_editor(|editor, window, cx| {
17468 for _ in 0..2 {
17469 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17470 }
17471 });
17472
17473 cx.assert_editor_state(
17474 &r#"
17475 use some::modified;
17476
17477
17478 fn main() {
17479 ˇ println!("hello there");
17480
17481 println!("around the");
17482 println!("world");
17483 }
17484 "#
17485 .unindent(),
17486 );
17487
17488 cx.update_editor(|editor, window, cx| {
17489 editor.fold(&Fold, window, cx);
17490 });
17491
17492 cx.update_editor(|editor, window, cx| {
17493 editor.go_to_next_hunk(&GoToHunk, window, cx);
17494 });
17495
17496 cx.assert_editor_state(
17497 &r#"
17498 ˇuse some::modified;
17499
17500
17501 fn main() {
17502 println!("hello there");
17503
17504 println!("around the");
17505 println!("world");
17506 }
17507 "#
17508 .unindent(),
17509 );
17510}
17511
17512#[test]
17513fn test_split_words() {
17514 fn split(text: &str) -> Vec<&str> {
17515 split_words(text).collect()
17516 }
17517
17518 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17519 assert_eq!(split("hello_world"), &["hello_", "world"]);
17520 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17521 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17522 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17523 assert_eq!(split("helloworld"), &["helloworld"]);
17524
17525 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17526}
17527
17528#[test]
17529fn test_split_words_for_snippet_prefix() {
17530 fn split(text: &str) -> Vec<&str> {
17531 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17532 }
17533
17534 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17535 assert_eq!(split("hello_world"), &["hello_world"]);
17536 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17537 assert_eq!(split("Hello_World"), &["Hello_World"]);
17538 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17539 assert_eq!(split("helloworld"), &["helloworld"]);
17540 assert_eq!(
17541 split("this@is!@#$^many . symbols"),
17542 &[
17543 "symbols",
17544 " symbols",
17545 ". symbols",
17546 " . symbols",
17547 " . symbols",
17548 " . symbols",
17549 "many . symbols",
17550 "^many . symbols",
17551 "$^many . symbols",
17552 "#$^many . symbols",
17553 "@#$^many . symbols",
17554 "!@#$^many . symbols",
17555 "is!@#$^many . symbols",
17556 "@is!@#$^many . symbols",
17557 "this@is!@#$^many . symbols",
17558 ],
17559 );
17560 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17561}
17562
17563#[gpui::test]
17564async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17565 init_test(cx, |_| {});
17566
17567 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17568
17569 #[track_caller]
17570 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17571 let _state_context = cx.set_state(before);
17572 cx.run_until_parked();
17573 cx.update_editor(|editor, window, cx| {
17574 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17575 });
17576 cx.run_until_parked();
17577 cx.assert_editor_state(after);
17578 }
17579
17580 // Outside bracket jumps to outside of matching bracket
17581 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17582 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17583
17584 // Inside bracket jumps to inside of matching bracket
17585 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17586 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17587
17588 // When outside a bracket and inside, favor jumping to the inside bracket
17589 assert(
17590 "console.log('foo', [1, 2, 3]ˇ);",
17591 "console.log('foo', ˇ[1, 2, 3]);",
17592 &mut cx,
17593 );
17594 assert(
17595 "console.log(ˇ'foo', [1, 2, 3]);",
17596 "console.log('foo'ˇ, [1, 2, 3]);",
17597 &mut cx,
17598 );
17599
17600 // Bias forward if two options are equally likely
17601 assert(
17602 "let result = curried_fun()ˇ();",
17603 "let result = curried_fun()()ˇ;",
17604 &mut cx,
17605 );
17606
17607 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17608 assert(
17609 indoc! {"
17610 function test() {
17611 console.log('test')ˇ
17612 }"},
17613 indoc! {"
17614 function test() {
17615 console.logˇ('test')
17616 }"},
17617 &mut cx,
17618 );
17619}
17620
17621#[gpui::test]
17622async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
17623 init_test(cx, |_| {});
17624 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
17625 language_registry.add(markdown_lang());
17626 language_registry.add(rust_lang());
17627 let buffer = cx.new(|cx| {
17628 let mut buffer = language::Buffer::local(
17629 indoc! {"
17630 ```rs
17631 impl Worktree {
17632 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17633 }
17634 }
17635 ```
17636 "},
17637 cx,
17638 );
17639 buffer.set_language_registry(language_registry.clone());
17640 buffer.set_language(Some(markdown_lang()), cx);
17641 buffer
17642 });
17643 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17644 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17645 cx.executor().run_until_parked();
17646 _ = editor.update(cx, |editor, window, cx| {
17647 // Case 1: Test outer enclosing brackets
17648 select_ranges(
17649 editor,
17650 &indoc! {"
17651 ```rs
17652 impl Worktree {
17653 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17654 }
17655 }ˇ
17656 ```
17657 "},
17658 window,
17659 cx,
17660 );
17661 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17662 assert_text_with_selections(
17663 editor,
17664 &indoc! {"
17665 ```rs
17666 impl Worktree ˇ{
17667 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17668 }
17669 }
17670 ```
17671 "},
17672 cx,
17673 );
17674 // Case 2: Test inner enclosing brackets
17675 select_ranges(
17676 editor,
17677 &indoc! {"
17678 ```rs
17679 impl Worktree {
17680 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17681 }ˇ
17682 }
17683 ```
17684 "},
17685 window,
17686 cx,
17687 );
17688 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17689 assert_text_with_selections(
17690 editor,
17691 &indoc! {"
17692 ```rs
17693 impl Worktree {
17694 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
17695 }
17696 }
17697 ```
17698 "},
17699 cx,
17700 );
17701 });
17702}
17703
17704#[gpui::test]
17705async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17706 init_test(cx, |_| {});
17707
17708 let fs = FakeFs::new(cx.executor());
17709 fs.insert_tree(
17710 path!("/a"),
17711 json!({
17712 "main.rs": "fn main() { let a = 5; }",
17713 "other.rs": "// Test file",
17714 }),
17715 )
17716 .await;
17717 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17718
17719 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17720 language_registry.add(Arc::new(Language::new(
17721 LanguageConfig {
17722 name: "Rust".into(),
17723 matcher: LanguageMatcher {
17724 path_suffixes: vec!["rs".to_string()],
17725 ..Default::default()
17726 },
17727 brackets: BracketPairConfig {
17728 pairs: vec![BracketPair {
17729 start: "{".to_string(),
17730 end: "}".to_string(),
17731 close: true,
17732 surround: true,
17733 newline: true,
17734 }],
17735 disabled_scopes_by_bracket_ix: Vec::new(),
17736 },
17737 ..Default::default()
17738 },
17739 Some(tree_sitter_rust::LANGUAGE.into()),
17740 )));
17741 let mut fake_servers = language_registry.register_fake_lsp(
17742 "Rust",
17743 FakeLspAdapter {
17744 capabilities: lsp::ServerCapabilities {
17745 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17746 first_trigger_character: "{".to_string(),
17747 more_trigger_character: None,
17748 }),
17749 ..Default::default()
17750 },
17751 ..Default::default()
17752 },
17753 );
17754
17755 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17756
17757 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17758
17759 let worktree_id = workspace
17760 .update(cx, |workspace, _, cx| {
17761 workspace.project().update(cx, |project, cx| {
17762 project.worktrees(cx).next().unwrap().read(cx).id()
17763 })
17764 })
17765 .unwrap();
17766
17767 let buffer = project
17768 .update(cx, |project, cx| {
17769 project.open_local_buffer(path!("/a/main.rs"), cx)
17770 })
17771 .await
17772 .unwrap();
17773 let editor_handle = workspace
17774 .update(cx, |workspace, window, cx| {
17775 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17776 })
17777 .unwrap()
17778 .await
17779 .unwrap()
17780 .downcast::<Editor>()
17781 .unwrap();
17782
17783 cx.executor().start_waiting();
17784 let fake_server = fake_servers.next().await.unwrap();
17785
17786 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17787 |params, _| async move {
17788 assert_eq!(
17789 params.text_document_position.text_document.uri,
17790 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17791 );
17792 assert_eq!(
17793 params.text_document_position.position,
17794 lsp::Position::new(0, 21),
17795 );
17796
17797 Ok(Some(vec![lsp::TextEdit {
17798 new_text: "]".to_string(),
17799 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17800 }]))
17801 },
17802 );
17803
17804 editor_handle.update_in(cx, |editor, window, cx| {
17805 window.focus(&editor.focus_handle(cx));
17806 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17807 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17808 });
17809 editor.handle_input("{", window, cx);
17810 });
17811
17812 cx.executor().run_until_parked();
17813
17814 buffer.update(cx, |buffer, _| {
17815 assert_eq!(
17816 buffer.text(),
17817 "fn main() { let a = {5}; }",
17818 "No extra braces from on type formatting should appear in the buffer"
17819 )
17820 });
17821}
17822
17823#[gpui::test(iterations = 20, seeds(31))]
17824async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17825 init_test(cx, |_| {});
17826
17827 let mut cx = EditorLspTestContext::new_rust(
17828 lsp::ServerCapabilities {
17829 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17830 first_trigger_character: ".".to_string(),
17831 more_trigger_character: None,
17832 }),
17833 ..Default::default()
17834 },
17835 cx,
17836 )
17837 .await;
17838
17839 cx.update_buffer(|buffer, _| {
17840 // This causes autoindent to be async.
17841 buffer.set_sync_parse_timeout(Duration::ZERO)
17842 });
17843
17844 cx.set_state("fn c() {\n d()ˇ\n}\n");
17845 cx.simulate_keystroke("\n");
17846 cx.run_until_parked();
17847
17848 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17849 let mut request =
17850 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17851 let buffer_cloned = buffer_cloned.clone();
17852 async move {
17853 buffer_cloned.update(&mut cx, |buffer, _| {
17854 assert_eq!(
17855 buffer.text(),
17856 "fn c() {\n d()\n .\n}\n",
17857 "OnTypeFormatting should triggered after autoindent applied"
17858 )
17859 })?;
17860
17861 Ok(Some(vec![]))
17862 }
17863 });
17864
17865 cx.simulate_keystroke(".");
17866 cx.run_until_parked();
17867
17868 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17869 assert!(request.next().await.is_some());
17870 request.close();
17871 assert!(request.next().await.is_none());
17872}
17873
17874#[gpui::test]
17875async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17876 init_test(cx, |_| {});
17877
17878 let fs = FakeFs::new(cx.executor());
17879 fs.insert_tree(
17880 path!("/a"),
17881 json!({
17882 "main.rs": "fn main() { let a = 5; }",
17883 "other.rs": "// Test file",
17884 }),
17885 )
17886 .await;
17887
17888 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17889
17890 let server_restarts = Arc::new(AtomicUsize::new(0));
17891 let closure_restarts = Arc::clone(&server_restarts);
17892 let language_server_name = "test language server";
17893 let language_name: LanguageName = "Rust".into();
17894
17895 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17896 language_registry.add(Arc::new(Language::new(
17897 LanguageConfig {
17898 name: language_name.clone(),
17899 matcher: LanguageMatcher {
17900 path_suffixes: vec!["rs".to_string()],
17901 ..Default::default()
17902 },
17903 ..Default::default()
17904 },
17905 Some(tree_sitter_rust::LANGUAGE.into()),
17906 )));
17907 let mut fake_servers = language_registry.register_fake_lsp(
17908 "Rust",
17909 FakeLspAdapter {
17910 name: language_server_name,
17911 initialization_options: Some(json!({
17912 "testOptionValue": true
17913 })),
17914 initializer: Some(Box::new(move |fake_server| {
17915 let task_restarts = Arc::clone(&closure_restarts);
17916 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17917 task_restarts.fetch_add(1, atomic::Ordering::Release);
17918 futures::future::ready(Ok(()))
17919 });
17920 })),
17921 ..Default::default()
17922 },
17923 );
17924
17925 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17926 let _buffer = project
17927 .update(cx, |project, cx| {
17928 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17929 })
17930 .await
17931 .unwrap();
17932 let _fake_server = fake_servers.next().await.unwrap();
17933 update_test_language_settings(cx, |language_settings| {
17934 language_settings.languages.0.insert(
17935 language_name.clone().0,
17936 LanguageSettingsContent {
17937 tab_size: NonZeroU32::new(8),
17938 ..Default::default()
17939 },
17940 );
17941 });
17942 cx.executor().run_until_parked();
17943 assert_eq!(
17944 server_restarts.load(atomic::Ordering::Acquire),
17945 0,
17946 "Should not restart LSP server on an unrelated change"
17947 );
17948
17949 update_test_project_settings(cx, |project_settings| {
17950 project_settings.lsp.insert(
17951 "Some other server name".into(),
17952 LspSettings {
17953 binary: None,
17954 settings: None,
17955 initialization_options: Some(json!({
17956 "some other init value": false
17957 })),
17958 enable_lsp_tasks: false,
17959 fetch: None,
17960 },
17961 );
17962 });
17963 cx.executor().run_until_parked();
17964 assert_eq!(
17965 server_restarts.load(atomic::Ordering::Acquire),
17966 0,
17967 "Should not restart LSP server on an unrelated LSP settings change"
17968 );
17969
17970 update_test_project_settings(cx, |project_settings| {
17971 project_settings.lsp.insert(
17972 language_server_name.into(),
17973 LspSettings {
17974 binary: None,
17975 settings: None,
17976 initialization_options: Some(json!({
17977 "anotherInitValue": false
17978 })),
17979 enable_lsp_tasks: false,
17980 fetch: None,
17981 },
17982 );
17983 });
17984 cx.executor().run_until_parked();
17985 assert_eq!(
17986 server_restarts.load(atomic::Ordering::Acquire),
17987 1,
17988 "Should restart LSP server on a related LSP settings change"
17989 );
17990
17991 update_test_project_settings(cx, |project_settings| {
17992 project_settings.lsp.insert(
17993 language_server_name.into(),
17994 LspSettings {
17995 binary: None,
17996 settings: None,
17997 initialization_options: Some(json!({
17998 "anotherInitValue": false
17999 })),
18000 enable_lsp_tasks: false,
18001 fetch: None,
18002 },
18003 );
18004 });
18005 cx.executor().run_until_parked();
18006 assert_eq!(
18007 server_restarts.load(atomic::Ordering::Acquire),
18008 1,
18009 "Should not restart LSP server on a related LSP settings change that is the same"
18010 );
18011
18012 update_test_project_settings(cx, |project_settings| {
18013 project_settings.lsp.insert(
18014 language_server_name.into(),
18015 LspSettings {
18016 binary: None,
18017 settings: None,
18018 initialization_options: None,
18019 enable_lsp_tasks: false,
18020 fetch: None,
18021 },
18022 );
18023 });
18024 cx.executor().run_until_parked();
18025 assert_eq!(
18026 server_restarts.load(atomic::Ordering::Acquire),
18027 2,
18028 "Should restart LSP server on another related LSP settings change"
18029 );
18030}
18031
18032#[gpui::test]
18033async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18034 init_test(cx, |_| {});
18035
18036 let mut cx = EditorLspTestContext::new_rust(
18037 lsp::ServerCapabilities {
18038 completion_provider: Some(lsp::CompletionOptions {
18039 trigger_characters: Some(vec![".".to_string()]),
18040 resolve_provider: Some(true),
18041 ..Default::default()
18042 }),
18043 ..Default::default()
18044 },
18045 cx,
18046 )
18047 .await;
18048
18049 cx.set_state("fn main() { let a = 2ˇ; }");
18050 cx.simulate_keystroke(".");
18051 let completion_item = lsp::CompletionItem {
18052 label: "some".into(),
18053 kind: Some(lsp::CompletionItemKind::SNIPPET),
18054 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18055 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18056 kind: lsp::MarkupKind::Markdown,
18057 value: "```rust\nSome(2)\n```".to_string(),
18058 })),
18059 deprecated: Some(false),
18060 sort_text: Some("fffffff2".to_string()),
18061 filter_text: Some("some".to_string()),
18062 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18063 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18064 range: lsp::Range {
18065 start: lsp::Position {
18066 line: 0,
18067 character: 22,
18068 },
18069 end: lsp::Position {
18070 line: 0,
18071 character: 22,
18072 },
18073 },
18074 new_text: "Some(2)".to_string(),
18075 })),
18076 additional_text_edits: Some(vec![lsp::TextEdit {
18077 range: lsp::Range {
18078 start: lsp::Position {
18079 line: 0,
18080 character: 20,
18081 },
18082 end: lsp::Position {
18083 line: 0,
18084 character: 22,
18085 },
18086 },
18087 new_text: "".to_string(),
18088 }]),
18089 ..Default::default()
18090 };
18091
18092 let closure_completion_item = completion_item.clone();
18093 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18094 let task_completion_item = closure_completion_item.clone();
18095 async move {
18096 Ok(Some(lsp::CompletionResponse::Array(vec![
18097 task_completion_item,
18098 ])))
18099 }
18100 });
18101
18102 request.next().await;
18103
18104 cx.condition(|editor, _| editor.context_menu_visible())
18105 .await;
18106 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18107 editor
18108 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18109 .unwrap()
18110 });
18111 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18112
18113 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18114 let task_completion_item = completion_item.clone();
18115 async move { Ok(task_completion_item) }
18116 })
18117 .next()
18118 .await
18119 .unwrap();
18120 apply_additional_edits.await.unwrap();
18121 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18122}
18123
18124#[gpui::test]
18125async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18126 init_test(cx, |_| {});
18127
18128 let mut cx = EditorLspTestContext::new_rust(
18129 lsp::ServerCapabilities {
18130 completion_provider: Some(lsp::CompletionOptions {
18131 trigger_characters: Some(vec![".".to_string()]),
18132 resolve_provider: Some(true),
18133 ..Default::default()
18134 }),
18135 ..Default::default()
18136 },
18137 cx,
18138 )
18139 .await;
18140
18141 cx.set_state("fn main() { let a = 2ˇ; }");
18142 cx.simulate_keystroke(".");
18143
18144 let item1 = lsp::CompletionItem {
18145 label: "method id()".to_string(),
18146 filter_text: Some("id".to_string()),
18147 detail: None,
18148 documentation: None,
18149 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18150 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18151 new_text: ".id".to_string(),
18152 })),
18153 ..lsp::CompletionItem::default()
18154 };
18155
18156 let item2 = lsp::CompletionItem {
18157 label: "other".to_string(),
18158 filter_text: Some("other".to_string()),
18159 detail: None,
18160 documentation: None,
18161 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18162 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18163 new_text: ".other".to_string(),
18164 })),
18165 ..lsp::CompletionItem::default()
18166 };
18167
18168 let item1 = item1.clone();
18169 cx.set_request_handler::<lsp::request::Completion, _, _>({
18170 let item1 = item1.clone();
18171 move |_, _, _| {
18172 let item1 = item1.clone();
18173 let item2 = item2.clone();
18174 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18175 }
18176 })
18177 .next()
18178 .await;
18179
18180 cx.condition(|editor, _| editor.context_menu_visible())
18181 .await;
18182 cx.update_editor(|editor, _, _| {
18183 let context_menu = editor.context_menu.borrow_mut();
18184 let context_menu = context_menu
18185 .as_ref()
18186 .expect("Should have the context menu deployed");
18187 match context_menu {
18188 CodeContextMenu::Completions(completions_menu) => {
18189 let completions = completions_menu.completions.borrow_mut();
18190 assert_eq!(
18191 completions
18192 .iter()
18193 .map(|completion| &completion.label.text)
18194 .collect::<Vec<_>>(),
18195 vec!["method id()", "other"]
18196 )
18197 }
18198 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18199 }
18200 });
18201
18202 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18203 let item1 = item1.clone();
18204 move |_, item_to_resolve, _| {
18205 let item1 = item1.clone();
18206 async move {
18207 if item1 == item_to_resolve {
18208 Ok(lsp::CompletionItem {
18209 label: "method id()".to_string(),
18210 filter_text: Some("id".to_string()),
18211 detail: Some("Now resolved!".to_string()),
18212 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18213 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18214 range: lsp::Range::new(
18215 lsp::Position::new(0, 22),
18216 lsp::Position::new(0, 22),
18217 ),
18218 new_text: ".id".to_string(),
18219 })),
18220 ..lsp::CompletionItem::default()
18221 })
18222 } else {
18223 Ok(item_to_resolve)
18224 }
18225 }
18226 }
18227 })
18228 .next()
18229 .await
18230 .unwrap();
18231 cx.run_until_parked();
18232
18233 cx.update_editor(|editor, window, cx| {
18234 editor.context_menu_next(&Default::default(), window, cx);
18235 });
18236
18237 cx.update_editor(|editor, _, _| {
18238 let context_menu = editor.context_menu.borrow_mut();
18239 let context_menu = context_menu
18240 .as_ref()
18241 .expect("Should have the context menu deployed");
18242 match context_menu {
18243 CodeContextMenu::Completions(completions_menu) => {
18244 let completions = completions_menu.completions.borrow_mut();
18245 assert_eq!(
18246 completions
18247 .iter()
18248 .map(|completion| &completion.label.text)
18249 .collect::<Vec<_>>(),
18250 vec!["method id() Now resolved!", "other"],
18251 "Should update first completion label, but not second as the filter text did not match."
18252 );
18253 }
18254 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18255 }
18256 });
18257}
18258
18259#[gpui::test]
18260async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18261 init_test(cx, |_| {});
18262 let mut cx = EditorLspTestContext::new_rust(
18263 lsp::ServerCapabilities {
18264 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18265 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18266 completion_provider: Some(lsp::CompletionOptions {
18267 resolve_provider: Some(true),
18268 ..Default::default()
18269 }),
18270 ..Default::default()
18271 },
18272 cx,
18273 )
18274 .await;
18275 cx.set_state(indoc! {"
18276 struct TestStruct {
18277 field: i32
18278 }
18279
18280 fn mainˇ() {
18281 let unused_var = 42;
18282 let test_struct = TestStruct { field: 42 };
18283 }
18284 "});
18285 let symbol_range = cx.lsp_range(indoc! {"
18286 struct TestStruct {
18287 field: i32
18288 }
18289
18290 «fn main»() {
18291 let unused_var = 42;
18292 let test_struct = TestStruct { field: 42 };
18293 }
18294 "});
18295 let mut hover_requests =
18296 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18297 Ok(Some(lsp::Hover {
18298 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18299 kind: lsp::MarkupKind::Markdown,
18300 value: "Function documentation".to_string(),
18301 }),
18302 range: Some(symbol_range),
18303 }))
18304 });
18305
18306 // Case 1: Test that code action menu hide hover popover
18307 cx.dispatch_action(Hover);
18308 hover_requests.next().await;
18309 cx.condition(|editor, _| editor.hover_state.visible()).await;
18310 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18311 move |_, _, _| async move {
18312 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18313 lsp::CodeAction {
18314 title: "Remove unused variable".to_string(),
18315 kind: Some(CodeActionKind::QUICKFIX),
18316 edit: Some(lsp::WorkspaceEdit {
18317 changes: Some(
18318 [(
18319 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18320 vec![lsp::TextEdit {
18321 range: lsp::Range::new(
18322 lsp::Position::new(5, 4),
18323 lsp::Position::new(5, 27),
18324 ),
18325 new_text: "".to_string(),
18326 }],
18327 )]
18328 .into_iter()
18329 .collect(),
18330 ),
18331 ..Default::default()
18332 }),
18333 ..Default::default()
18334 },
18335 )]))
18336 },
18337 );
18338 cx.update_editor(|editor, window, cx| {
18339 editor.toggle_code_actions(
18340 &ToggleCodeActions {
18341 deployed_from: None,
18342 quick_launch: false,
18343 },
18344 window,
18345 cx,
18346 );
18347 });
18348 code_action_requests.next().await;
18349 cx.run_until_parked();
18350 cx.condition(|editor, _| editor.context_menu_visible())
18351 .await;
18352 cx.update_editor(|editor, _, _| {
18353 assert!(
18354 !editor.hover_state.visible(),
18355 "Hover popover should be hidden when code action menu is shown"
18356 );
18357 // Hide code actions
18358 editor.context_menu.take();
18359 });
18360
18361 // Case 2: Test that code completions hide hover popover
18362 cx.dispatch_action(Hover);
18363 hover_requests.next().await;
18364 cx.condition(|editor, _| editor.hover_state.visible()).await;
18365 let counter = Arc::new(AtomicUsize::new(0));
18366 let mut completion_requests =
18367 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18368 let counter = counter.clone();
18369 async move {
18370 counter.fetch_add(1, atomic::Ordering::Release);
18371 Ok(Some(lsp::CompletionResponse::Array(vec![
18372 lsp::CompletionItem {
18373 label: "main".into(),
18374 kind: Some(lsp::CompletionItemKind::FUNCTION),
18375 detail: Some("() -> ()".to_string()),
18376 ..Default::default()
18377 },
18378 lsp::CompletionItem {
18379 label: "TestStruct".into(),
18380 kind: Some(lsp::CompletionItemKind::STRUCT),
18381 detail: Some("struct TestStruct".to_string()),
18382 ..Default::default()
18383 },
18384 ])))
18385 }
18386 });
18387 cx.update_editor(|editor, window, cx| {
18388 editor.show_completions(&ShowCompletions, window, cx);
18389 });
18390 completion_requests.next().await;
18391 cx.condition(|editor, _| editor.context_menu_visible())
18392 .await;
18393 cx.update_editor(|editor, _, _| {
18394 assert!(
18395 !editor.hover_state.visible(),
18396 "Hover popover should be hidden when completion menu is shown"
18397 );
18398 });
18399}
18400
18401#[gpui::test]
18402async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18403 init_test(cx, |_| {});
18404
18405 let mut cx = EditorLspTestContext::new_rust(
18406 lsp::ServerCapabilities {
18407 completion_provider: Some(lsp::CompletionOptions {
18408 trigger_characters: Some(vec![".".to_string()]),
18409 resolve_provider: Some(true),
18410 ..Default::default()
18411 }),
18412 ..Default::default()
18413 },
18414 cx,
18415 )
18416 .await;
18417
18418 cx.set_state("fn main() { let a = 2ˇ; }");
18419 cx.simulate_keystroke(".");
18420
18421 let unresolved_item_1 = lsp::CompletionItem {
18422 label: "id".to_string(),
18423 filter_text: Some("id".to_string()),
18424 detail: None,
18425 documentation: None,
18426 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18427 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18428 new_text: ".id".to_string(),
18429 })),
18430 ..lsp::CompletionItem::default()
18431 };
18432 let resolved_item_1 = lsp::CompletionItem {
18433 additional_text_edits: Some(vec![lsp::TextEdit {
18434 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18435 new_text: "!!".to_string(),
18436 }]),
18437 ..unresolved_item_1.clone()
18438 };
18439 let unresolved_item_2 = lsp::CompletionItem {
18440 label: "other".to_string(),
18441 filter_text: Some("other".to_string()),
18442 detail: None,
18443 documentation: None,
18444 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18445 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18446 new_text: ".other".to_string(),
18447 })),
18448 ..lsp::CompletionItem::default()
18449 };
18450 let resolved_item_2 = lsp::CompletionItem {
18451 additional_text_edits: Some(vec![lsp::TextEdit {
18452 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18453 new_text: "??".to_string(),
18454 }]),
18455 ..unresolved_item_2.clone()
18456 };
18457
18458 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18459 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18460 cx.lsp
18461 .server
18462 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18463 let unresolved_item_1 = unresolved_item_1.clone();
18464 let resolved_item_1 = resolved_item_1.clone();
18465 let unresolved_item_2 = unresolved_item_2.clone();
18466 let resolved_item_2 = resolved_item_2.clone();
18467 let resolve_requests_1 = resolve_requests_1.clone();
18468 let resolve_requests_2 = resolve_requests_2.clone();
18469 move |unresolved_request, _| {
18470 let unresolved_item_1 = unresolved_item_1.clone();
18471 let resolved_item_1 = resolved_item_1.clone();
18472 let unresolved_item_2 = unresolved_item_2.clone();
18473 let resolved_item_2 = resolved_item_2.clone();
18474 let resolve_requests_1 = resolve_requests_1.clone();
18475 let resolve_requests_2 = resolve_requests_2.clone();
18476 async move {
18477 if unresolved_request == unresolved_item_1 {
18478 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18479 Ok(resolved_item_1.clone())
18480 } else if unresolved_request == unresolved_item_2 {
18481 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18482 Ok(resolved_item_2.clone())
18483 } else {
18484 panic!("Unexpected completion item {unresolved_request:?}")
18485 }
18486 }
18487 }
18488 })
18489 .detach();
18490
18491 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18492 let unresolved_item_1 = unresolved_item_1.clone();
18493 let unresolved_item_2 = unresolved_item_2.clone();
18494 async move {
18495 Ok(Some(lsp::CompletionResponse::Array(vec![
18496 unresolved_item_1,
18497 unresolved_item_2,
18498 ])))
18499 }
18500 })
18501 .next()
18502 .await;
18503
18504 cx.condition(|editor, _| editor.context_menu_visible())
18505 .await;
18506 cx.update_editor(|editor, _, _| {
18507 let context_menu = editor.context_menu.borrow_mut();
18508 let context_menu = context_menu
18509 .as_ref()
18510 .expect("Should have the context menu deployed");
18511 match context_menu {
18512 CodeContextMenu::Completions(completions_menu) => {
18513 let completions = completions_menu.completions.borrow_mut();
18514 assert_eq!(
18515 completions
18516 .iter()
18517 .map(|completion| &completion.label.text)
18518 .collect::<Vec<_>>(),
18519 vec!["id", "other"]
18520 )
18521 }
18522 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18523 }
18524 });
18525 cx.run_until_parked();
18526
18527 cx.update_editor(|editor, window, cx| {
18528 editor.context_menu_next(&ContextMenuNext, window, cx);
18529 });
18530 cx.run_until_parked();
18531 cx.update_editor(|editor, window, cx| {
18532 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18533 });
18534 cx.run_until_parked();
18535 cx.update_editor(|editor, window, cx| {
18536 editor.context_menu_next(&ContextMenuNext, window, cx);
18537 });
18538 cx.run_until_parked();
18539 cx.update_editor(|editor, window, cx| {
18540 editor
18541 .compose_completion(&ComposeCompletion::default(), window, cx)
18542 .expect("No task returned")
18543 })
18544 .await
18545 .expect("Completion failed");
18546 cx.run_until_parked();
18547
18548 cx.update_editor(|editor, _, cx| {
18549 assert_eq!(
18550 resolve_requests_1.load(atomic::Ordering::Acquire),
18551 1,
18552 "Should always resolve once despite multiple selections"
18553 );
18554 assert_eq!(
18555 resolve_requests_2.load(atomic::Ordering::Acquire),
18556 1,
18557 "Should always resolve once after multiple selections and applying the completion"
18558 );
18559 assert_eq!(
18560 editor.text(cx),
18561 "fn main() { let a = ??.other; }",
18562 "Should use resolved data when applying the completion"
18563 );
18564 });
18565}
18566
18567#[gpui::test]
18568async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18569 init_test(cx, |_| {});
18570
18571 let item_0 = lsp::CompletionItem {
18572 label: "abs".into(),
18573 insert_text: Some("abs".into()),
18574 data: Some(json!({ "very": "special"})),
18575 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18576 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18577 lsp::InsertReplaceEdit {
18578 new_text: "abs".to_string(),
18579 insert: lsp::Range::default(),
18580 replace: lsp::Range::default(),
18581 },
18582 )),
18583 ..lsp::CompletionItem::default()
18584 };
18585 let items = iter::once(item_0.clone())
18586 .chain((11..51).map(|i| lsp::CompletionItem {
18587 label: format!("item_{}", i),
18588 insert_text: Some(format!("item_{}", i)),
18589 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18590 ..lsp::CompletionItem::default()
18591 }))
18592 .collect::<Vec<_>>();
18593
18594 let default_commit_characters = vec!["?".to_string()];
18595 let default_data = json!({ "default": "data"});
18596 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18597 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18598 let default_edit_range = lsp::Range {
18599 start: lsp::Position {
18600 line: 0,
18601 character: 5,
18602 },
18603 end: lsp::Position {
18604 line: 0,
18605 character: 5,
18606 },
18607 };
18608
18609 let mut cx = EditorLspTestContext::new_rust(
18610 lsp::ServerCapabilities {
18611 completion_provider: Some(lsp::CompletionOptions {
18612 trigger_characters: Some(vec![".".to_string()]),
18613 resolve_provider: Some(true),
18614 ..Default::default()
18615 }),
18616 ..Default::default()
18617 },
18618 cx,
18619 )
18620 .await;
18621
18622 cx.set_state("fn main() { let a = 2ˇ; }");
18623 cx.simulate_keystroke(".");
18624
18625 let completion_data = default_data.clone();
18626 let completion_characters = default_commit_characters.clone();
18627 let completion_items = items.clone();
18628 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18629 let default_data = completion_data.clone();
18630 let default_commit_characters = completion_characters.clone();
18631 let items = completion_items.clone();
18632 async move {
18633 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18634 items,
18635 item_defaults: Some(lsp::CompletionListItemDefaults {
18636 data: Some(default_data.clone()),
18637 commit_characters: Some(default_commit_characters.clone()),
18638 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18639 default_edit_range,
18640 )),
18641 insert_text_format: Some(default_insert_text_format),
18642 insert_text_mode: Some(default_insert_text_mode),
18643 }),
18644 ..lsp::CompletionList::default()
18645 })))
18646 }
18647 })
18648 .next()
18649 .await;
18650
18651 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18652 cx.lsp
18653 .server
18654 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18655 let closure_resolved_items = resolved_items.clone();
18656 move |item_to_resolve, _| {
18657 let closure_resolved_items = closure_resolved_items.clone();
18658 async move {
18659 closure_resolved_items.lock().push(item_to_resolve.clone());
18660 Ok(item_to_resolve)
18661 }
18662 }
18663 })
18664 .detach();
18665
18666 cx.condition(|editor, _| editor.context_menu_visible())
18667 .await;
18668 cx.run_until_parked();
18669 cx.update_editor(|editor, _, _| {
18670 let menu = editor.context_menu.borrow_mut();
18671 match menu.as_ref().expect("should have the completions menu") {
18672 CodeContextMenu::Completions(completions_menu) => {
18673 assert_eq!(
18674 completions_menu
18675 .entries
18676 .borrow()
18677 .iter()
18678 .map(|mat| mat.string.clone())
18679 .collect::<Vec<String>>(),
18680 items
18681 .iter()
18682 .map(|completion| completion.label.clone())
18683 .collect::<Vec<String>>()
18684 );
18685 }
18686 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18687 }
18688 });
18689 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18690 // with 4 from the end.
18691 assert_eq!(
18692 *resolved_items.lock(),
18693 [&items[0..16], &items[items.len() - 4..items.len()]]
18694 .concat()
18695 .iter()
18696 .cloned()
18697 .map(|mut item| {
18698 if item.data.is_none() {
18699 item.data = Some(default_data.clone());
18700 }
18701 item
18702 })
18703 .collect::<Vec<lsp::CompletionItem>>(),
18704 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18705 );
18706 resolved_items.lock().clear();
18707
18708 cx.update_editor(|editor, window, cx| {
18709 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18710 });
18711 cx.run_until_parked();
18712 // Completions that have already been resolved are skipped.
18713 assert_eq!(
18714 *resolved_items.lock(),
18715 items[items.len() - 17..items.len() - 4]
18716 .iter()
18717 .cloned()
18718 .map(|mut item| {
18719 if item.data.is_none() {
18720 item.data = Some(default_data.clone());
18721 }
18722 item
18723 })
18724 .collect::<Vec<lsp::CompletionItem>>()
18725 );
18726 resolved_items.lock().clear();
18727}
18728
18729#[gpui::test]
18730async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18731 init_test(cx, |_| {});
18732
18733 let mut cx = EditorLspTestContext::new(
18734 Language::new(
18735 LanguageConfig {
18736 matcher: LanguageMatcher {
18737 path_suffixes: vec!["jsx".into()],
18738 ..Default::default()
18739 },
18740 overrides: [(
18741 "element".into(),
18742 LanguageConfigOverride {
18743 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18744 ..Default::default()
18745 },
18746 )]
18747 .into_iter()
18748 .collect(),
18749 ..Default::default()
18750 },
18751 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18752 )
18753 .with_override_query("(jsx_self_closing_element) @element")
18754 .unwrap(),
18755 lsp::ServerCapabilities {
18756 completion_provider: Some(lsp::CompletionOptions {
18757 trigger_characters: Some(vec![":".to_string()]),
18758 ..Default::default()
18759 }),
18760 ..Default::default()
18761 },
18762 cx,
18763 )
18764 .await;
18765
18766 cx.lsp
18767 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18768 Ok(Some(lsp::CompletionResponse::Array(vec![
18769 lsp::CompletionItem {
18770 label: "bg-blue".into(),
18771 ..Default::default()
18772 },
18773 lsp::CompletionItem {
18774 label: "bg-red".into(),
18775 ..Default::default()
18776 },
18777 lsp::CompletionItem {
18778 label: "bg-yellow".into(),
18779 ..Default::default()
18780 },
18781 ])))
18782 });
18783
18784 cx.set_state(r#"<p class="bgˇ" />"#);
18785
18786 // Trigger completion when typing a dash, because the dash is an extra
18787 // word character in the 'element' scope, which contains the cursor.
18788 cx.simulate_keystroke("-");
18789 cx.executor().run_until_parked();
18790 cx.update_editor(|editor, _, _| {
18791 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18792 {
18793 assert_eq!(
18794 completion_menu_entries(menu),
18795 &["bg-blue", "bg-red", "bg-yellow"]
18796 );
18797 } else {
18798 panic!("expected completion menu to be open");
18799 }
18800 });
18801
18802 cx.simulate_keystroke("l");
18803 cx.executor().run_until_parked();
18804 cx.update_editor(|editor, _, _| {
18805 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18806 {
18807 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18808 } else {
18809 panic!("expected completion menu to be open");
18810 }
18811 });
18812
18813 // When filtering completions, consider the character after the '-' to
18814 // be the start of a subword.
18815 cx.set_state(r#"<p class="yelˇ" />"#);
18816 cx.simulate_keystroke("l");
18817 cx.executor().run_until_parked();
18818 cx.update_editor(|editor, _, _| {
18819 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18820 {
18821 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18822 } else {
18823 panic!("expected completion menu to be open");
18824 }
18825 });
18826}
18827
18828fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18829 let entries = menu.entries.borrow();
18830 entries.iter().map(|mat| mat.string.clone()).collect()
18831}
18832
18833#[gpui::test]
18834async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18835 init_test(cx, |settings| {
18836 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18837 });
18838
18839 let fs = FakeFs::new(cx.executor());
18840 fs.insert_file(path!("/file.ts"), Default::default()).await;
18841
18842 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18843 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18844
18845 language_registry.add(Arc::new(Language::new(
18846 LanguageConfig {
18847 name: "TypeScript".into(),
18848 matcher: LanguageMatcher {
18849 path_suffixes: vec!["ts".to_string()],
18850 ..Default::default()
18851 },
18852 ..Default::default()
18853 },
18854 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18855 )));
18856 update_test_language_settings(cx, |settings| {
18857 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18858 });
18859
18860 let test_plugin = "test_plugin";
18861 let _ = language_registry.register_fake_lsp(
18862 "TypeScript",
18863 FakeLspAdapter {
18864 prettier_plugins: vec![test_plugin],
18865 ..Default::default()
18866 },
18867 );
18868
18869 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18870 let buffer = project
18871 .update(cx, |project, cx| {
18872 project.open_local_buffer(path!("/file.ts"), cx)
18873 })
18874 .await
18875 .unwrap();
18876
18877 let buffer_text = "one\ntwo\nthree\n";
18878 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18879 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18880 editor.update_in(cx, |editor, window, cx| {
18881 editor.set_text(buffer_text, window, cx)
18882 });
18883
18884 editor
18885 .update_in(cx, |editor, window, cx| {
18886 editor.perform_format(
18887 project.clone(),
18888 FormatTrigger::Manual,
18889 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18890 window,
18891 cx,
18892 )
18893 })
18894 .unwrap()
18895 .await;
18896 assert_eq!(
18897 editor.update(cx, |editor, cx| editor.text(cx)),
18898 buffer_text.to_string() + prettier_format_suffix,
18899 "Test prettier formatting was not applied to the original buffer text",
18900 );
18901
18902 update_test_language_settings(cx, |settings| {
18903 settings.defaults.formatter = Some(FormatterList::default())
18904 });
18905 let format = editor.update_in(cx, |editor, window, cx| {
18906 editor.perform_format(
18907 project.clone(),
18908 FormatTrigger::Manual,
18909 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18910 window,
18911 cx,
18912 )
18913 });
18914 format.await.unwrap();
18915 assert_eq!(
18916 editor.update(cx, |editor, cx| editor.text(cx)),
18917 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18918 "Autoformatting (via test prettier) was not applied to the original buffer text",
18919 );
18920}
18921
18922#[gpui::test]
18923async fn test_addition_reverts(cx: &mut TestAppContext) {
18924 init_test(cx, |_| {});
18925 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18926 let base_text = indoc! {r#"
18927 struct Row;
18928 struct Row1;
18929 struct Row2;
18930
18931 struct Row4;
18932 struct Row5;
18933 struct Row6;
18934
18935 struct Row8;
18936 struct Row9;
18937 struct Row10;"#};
18938
18939 // When addition hunks are not adjacent to carets, no hunk revert is performed
18940 assert_hunk_revert(
18941 indoc! {r#"struct Row;
18942 struct Row1;
18943 struct Row1.1;
18944 struct Row1.2;
18945 struct Row2;ˇ
18946
18947 struct Row4;
18948 struct Row5;
18949 struct Row6;
18950
18951 struct Row8;
18952 ˇstruct Row9;
18953 struct Row9.1;
18954 struct Row9.2;
18955 struct Row9.3;
18956 struct Row10;"#},
18957 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18958 indoc! {r#"struct Row;
18959 struct Row1;
18960 struct Row1.1;
18961 struct Row1.2;
18962 struct Row2;ˇ
18963
18964 struct Row4;
18965 struct Row5;
18966 struct Row6;
18967
18968 struct Row8;
18969 ˇstruct Row9;
18970 struct Row9.1;
18971 struct Row9.2;
18972 struct Row9.3;
18973 struct Row10;"#},
18974 base_text,
18975 &mut cx,
18976 );
18977 // Same for selections
18978 assert_hunk_revert(
18979 indoc! {r#"struct Row;
18980 struct Row1;
18981 struct Row2;
18982 struct Row2.1;
18983 struct Row2.2;
18984 «ˇ
18985 struct Row4;
18986 struct» Row5;
18987 «struct Row6;
18988 ˇ»
18989 struct Row9.1;
18990 struct Row9.2;
18991 struct Row9.3;
18992 struct Row8;
18993 struct Row9;
18994 struct Row10;"#},
18995 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18996 indoc! {r#"struct Row;
18997 struct Row1;
18998 struct Row2;
18999 struct Row2.1;
19000 struct Row2.2;
19001 «ˇ
19002 struct Row4;
19003 struct» Row5;
19004 «struct Row6;
19005 ˇ»
19006 struct Row9.1;
19007 struct Row9.2;
19008 struct Row9.3;
19009 struct Row8;
19010 struct Row9;
19011 struct Row10;"#},
19012 base_text,
19013 &mut cx,
19014 );
19015
19016 // When carets and selections intersect the addition hunks, those are reverted.
19017 // Adjacent carets got merged.
19018 assert_hunk_revert(
19019 indoc! {r#"struct Row;
19020 ˇ// something on the top
19021 struct Row1;
19022 struct Row2;
19023 struct Roˇw3.1;
19024 struct Row2.2;
19025 struct Row2.3;ˇ
19026
19027 struct Row4;
19028 struct ˇRow5.1;
19029 struct Row5.2;
19030 struct «Rowˇ»5.3;
19031 struct Row5;
19032 struct Row6;
19033 ˇ
19034 struct Row9.1;
19035 struct «Rowˇ»9.2;
19036 struct «ˇRow»9.3;
19037 struct Row8;
19038 struct Row9;
19039 «ˇ// something on bottom»
19040 struct Row10;"#},
19041 vec![
19042 DiffHunkStatusKind::Added,
19043 DiffHunkStatusKind::Added,
19044 DiffHunkStatusKind::Added,
19045 DiffHunkStatusKind::Added,
19046 DiffHunkStatusKind::Added,
19047 ],
19048 indoc! {r#"struct Row;
19049 ˇstruct Row1;
19050 struct Row2;
19051 ˇ
19052 struct Row4;
19053 ˇstruct Row5;
19054 struct Row6;
19055 ˇ
19056 ˇstruct Row8;
19057 struct Row9;
19058 ˇstruct Row10;"#},
19059 base_text,
19060 &mut cx,
19061 );
19062}
19063
19064#[gpui::test]
19065async fn test_modification_reverts(cx: &mut TestAppContext) {
19066 init_test(cx, |_| {});
19067 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19068 let base_text = indoc! {r#"
19069 struct Row;
19070 struct Row1;
19071 struct Row2;
19072
19073 struct Row4;
19074 struct Row5;
19075 struct Row6;
19076
19077 struct Row8;
19078 struct Row9;
19079 struct Row10;"#};
19080
19081 // Modification hunks behave the same as the addition ones.
19082 assert_hunk_revert(
19083 indoc! {r#"struct Row;
19084 struct Row1;
19085 struct Row33;
19086 ˇ
19087 struct Row4;
19088 struct Row5;
19089 struct Row6;
19090 ˇ
19091 struct Row99;
19092 struct Row9;
19093 struct Row10;"#},
19094 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19095 indoc! {r#"struct Row;
19096 struct Row1;
19097 struct Row33;
19098 ˇ
19099 struct Row4;
19100 struct Row5;
19101 struct Row6;
19102 ˇ
19103 struct Row99;
19104 struct Row9;
19105 struct Row10;"#},
19106 base_text,
19107 &mut cx,
19108 );
19109 assert_hunk_revert(
19110 indoc! {r#"struct Row;
19111 struct Row1;
19112 struct Row33;
19113 «ˇ
19114 struct Row4;
19115 struct» Row5;
19116 «struct Row6;
19117 ˇ»
19118 struct Row99;
19119 struct Row9;
19120 struct Row10;"#},
19121 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19122 indoc! {r#"struct Row;
19123 struct Row1;
19124 struct Row33;
19125 «ˇ
19126 struct Row4;
19127 struct» Row5;
19128 «struct Row6;
19129 ˇ»
19130 struct Row99;
19131 struct Row9;
19132 struct Row10;"#},
19133 base_text,
19134 &mut cx,
19135 );
19136
19137 assert_hunk_revert(
19138 indoc! {r#"ˇstruct Row1.1;
19139 struct Row1;
19140 «ˇstr»uct Row22;
19141
19142 struct ˇRow44;
19143 struct Row5;
19144 struct «Rˇ»ow66;ˇ
19145
19146 «struˇ»ct Row88;
19147 struct Row9;
19148 struct Row1011;ˇ"#},
19149 vec![
19150 DiffHunkStatusKind::Modified,
19151 DiffHunkStatusKind::Modified,
19152 DiffHunkStatusKind::Modified,
19153 DiffHunkStatusKind::Modified,
19154 DiffHunkStatusKind::Modified,
19155 DiffHunkStatusKind::Modified,
19156 ],
19157 indoc! {r#"struct Row;
19158 ˇstruct Row1;
19159 struct Row2;
19160 ˇ
19161 struct Row4;
19162 ˇstruct Row5;
19163 struct Row6;
19164 ˇ
19165 struct Row8;
19166 ˇstruct Row9;
19167 struct Row10;ˇ"#},
19168 base_text,
19169 &mut cx,
19170 );
19171}
19172
19173#[gpui::test]
19174async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19175 init_test(cx, |_| {});
19176 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19177 let base_text = indoc! {r#"
19178 one
19179
19180 two
19181 three
19182 "#};
19183
19184 cx.set_head_text(base_text);
19185 cx.set_state("\nˇ\n");
19186 cx.executor().run_until_parked();
19187 cx.update_editor(|editor, _window, cx| {
19188 editor.expand_selected_diff_hunks(cx);
19189 });
19190 cx.executor().run_until_parked();
19191 cx.update_editor(|editor, window, cx| {
19192 editor.backspace(&Default::default(), window, cx);
19193 });
19194 cx.run_until_parked();
19195 cx.assert_state_with_diff(
19196 indoc! {r#"
19197
19198 - two
19199 - threeˇ
19200 +
19201 "#}
19202 .to_string(),
19203 );
19204}
19205
19206#[gpui::test]
19207async fn test_deletion_reverts(cx: &mut TestAppContext) {
19208 init_test(cx, |_| {});
19209 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19210 let base_text = indoc! {r#"struct Row;
19211struct Row1;
19212struct Row2;
19213
19214struct Row4;
19215struct Row5;
19216struct Row6;
19217
19218struct Row8;
19219struct Row9;
19220struct Row10;"#};
19221
19222 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19223 assert_hunk_revert(
19224 indoc! {r#"struct Row;
19225 struct Row2;
19226
19227 ˇstruct Row4;
19228 struct Row5;
19229 struct Row6;
19230 ˇ
19231 struct Row8;
19232 struct Row10;"#},
19233 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19234 indoc! {r#"struct Row;
19235 struct Row2;
19236
19237 ˇstruct Row4;
19238 struct Row5;
19239 struct Row6;
19240 ˇ
19241 struct Row8;
19242 struct Row10;"#},
19243 base_text,
19244 &mut cx,
19245 );
19246 assert_hunk_revert(
19247 indoc! {r#"struct Row;
19248 struct Row2;
19249
19250 «ˇstruct Row4;
19251 struct» Row5;
19252 «struct Row6;
19253 ˇ»
19254 struct Row8;
19255 struct Row10;"#},
19256 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19257 indoc! {r#"struct Row;
19258 struct Row2;
19259
19260 «ˇstruct Row4;
19261 struct» Row5;
19262 «struct Row6;
19263 ˇ»
19264 struct Row8;
19265 struct Row10;"#},
19266 base_text,
19267 &mut cx,
19268 );
19269
19270 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19271 assert_hunk_revert(
19272 indoc! {r#"struct Row;
19273 ˇstruct Row2;
19274
19275 struct Row4;
19276 struct Row5;
19277 struct Row6;
19278
19279 struct Row8;ˇ
19280 struct Row10;"#},
19281 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19282 indoc! {r#"struct Row;
19283 struct Row1;
19284 ˇstruct Row2;
19285
19286 struct Row4;
19287 struct Row5;
19288 struct Row6;
19289
19290 struct Row8;ˇ
19291 struct Row9;
19292 struct Row10;"#},
19293 base_text,
19294 &mut cx,
19295 );
19296 assert_hunk_revert(
19297 indoc! {r#"struct Row;
19298 struct Row2«ˇ;
19299 struct Row4;
19300 struct» Row5;
19301 «struct Row6;
19302
19303 struct Row8;ˇ»
19304 struct Row10;"#},
19305 vec![
19306 DiffHunkStatusKind::Deleted,
19307 DiffHunkStatusKind::Deleted,
19308 DiffHunkStatusKind::Deleted,
19309 ],
19310 indoc! {r#"struct Row;
19311 struct Row1;
19312 struct Row2«ˇ;
19313
19314 struct Row4;
19315 struct» Row5;
19316 «struct Row6;
19317
19318 struct Row8;ˇ»
19319 struct Row9;
19320 struct Row10;"#},
19321 base_text,
19322 &mut cx,
19323 );
19324}
19325
19326#[gpui::test]
19327async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19328 init_test(cx, |_| {});
19329
19330 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19331 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19332 let base_text_3 =
19333 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19334
19335 let text_1 = edit_first_char_of_every_line(base_text_1);
19336 let text_2 = edit_first_char_of_every_line(base_text_2);
19337 let text_3 = edit_first_char_of_every_line(base_text_3);
19338
19339 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19340 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19341 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19342
19343 let multibuffer = cx.new(|cx| {
19344 let mut multibuffer = MultiBuffer::new(ReadWrite);
19345 multibuffer.push_excerpts(
19346 buffer_1.clone(),
19347 [
19348 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19349 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19350 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19351 ],
19352 cx,
19353 );
19354 multibuffer.push_excerpts(
19355 buffer_2.clone(),
19356 [
19357 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19358 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19359 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19360 ],
19361 cx,
19362 );
19363 multibuffer.push_excerpts(
19364 buffer_3.clone(),
19365 [
19366 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19367 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19368 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19369 ],
19370 cx,
19371 );
19372 multibuffer
19373 });
19374
19375 let fs = FakeFs::new(cx.executor());
19376 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19377 let (editor, cx) = cx
19378 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19379 editor.update_in(cx, |editor, _window, cx| {
19380 for (buffer, diff_base) in [
19381 (buffer_1.clone(), base_text_1),
19382 (buffer_2.clone(), base_text_2),
19383 (buffer_3.clone(), base_text_3),
19384 ] {
19385 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19386 editor
19387 .buffer
19388 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19389 }
19390 });
19391 cx.executor().run_until_parked();
19392
19393 editor.update_in(cx, |editor, window, cx| {
19394 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}");
19395 editor.select_all(&SelectAll, window, cx);
19396 editor.git_restore(&Default::default(), window, cx);
19397 });
19398 cx.executor().run_until_parked();
19399
19400 // When all ranges are selected, all buffer hunks are reverted.
19401 editor.update(cx, |editor, cx| {
19402 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");
19403 });
19404 buffer_1.update(cx, |buffer, _| {
19405 assert_eq!(buffer.text(), base_text_1);
19406 });
19407 buffer_2.update(cx, |buffer, _| {
19408 assert_eq!(buffer.text(), base_text_2);
19409 });
19410 buffer_3.update(cx, |buffer, _| {
19411 assert_eq!(buffer.text(), base_text_3);
19412 });
19413
19414 editor.update_in(cx, |editor, window, cx| {
19415 editor.undo(&Default::default(), window, cx);
19416 });
19417
19418 editor.update_in(cx, |editor, window, cx| {
19419 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19420 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19421 });
19422 editor.git_restore(&Default::default(), window, cx);
19423 });
19424
19425 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19426 // but not affect buffer_2 and its related excerpts.
19427 editor.update(cx, |editor, cx| {
19428 assert_eq!(
19429 editor.text(cx),
19430 "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}"
19431 );
19432 });
19433 buffer_1.update(cx, |buffer, _| {
19434 assert_eq!(buffer.text(), base_text_1);
19435 });
19436 buffer_2.update(cx, |buffer, _| {
19437 assert_eq!(
19438 buffer.text(),
19439 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19440 );
19441 });
19442 buffer_3.update(cx, |buffer, _| {
19443 assert_eq!(
19444 buffer.text(),
19445 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19446 );
19447 });
19448
19449 fn edit_first_char_of_every_line(text: &str) -> String {
19450 text.split('\n')
19451 .map(|line| format!("X{}", &line[1..]))
19452 .collect::<Vec<_>>()
19453 .join("\n")
19454 }
19455}
19456
19457#[gpui::test]
19458async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19459 init_test(cx, |_| {});
19460
19461 let cols = 4;
19462 let rows = 10;
19463 let sample_text_1 = sample_text(rows, cols, 'a');
19464 assert_eq!(
19465 sample_text_1,
19466 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19467 );
19468 let sample_text_2 = sample_text(rows, cols, 'l');
19469 assert_eq!(
19470 sample_text_2,
19471 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19472 );
19473 let sample_text_3 = sample_text(rows, cols, 'v');
19474 assert_eq!(
19475 sample_text_3,
19476 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19477 );
19478
19479 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19480 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19481 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19482
19483 let multi_buffer = cx.new(|cx| {
19484 let mut multibuffer = MultiBuffer::new(ReadWrite);
19485 multibuffer.push_excerpts(
19486 buffer_1.clone(),
19487 [
19488 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19489 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19490 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19491 ],
19492 cx,
19493 );
19494 multibuffer.push_excerpts(
19495 buffer_2.clone(),
19496 [
19497 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19498 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19499 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19500 ],
19501 cx,
19502 );
19503 multibuffer.push_excerpts(
19504 buffer_3.clone(),
19505 [
19506 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19507 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19508 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19509 ],
19510 cx,
19511 );
19512 multibuffer
19513 });
19514
19515 let fs = FakeFs::new(cx.executor());
19516 fs.insert_tree(
19517 "/a",
19518 json!({
19519 "main.rs": sample_text_1,
19520 "other.rs": sample_text_2,
19521 "lib.rs": sample_text_3,
19522 }),
19523 )
19524 .await;
19525 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19526 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19527 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19528 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19529 Editor::new(
19530 EditorMode::full(),
19531 multi_buffer,
19532 Some(project.clone()),
19533 window,
19534 cx,
19535 )
19536 });
19537 let multibuffer_item_id = workspace
19538 .update(cx, |workspace, window, cx| {
19539 assert!(
19540 workspace.active_item(cx).is_none(),
19541 "active item should be None before the first item is added"
19542 );
19543 workspace.add_item_to_active_pane(
19544 Box::new(multi_buffer_editor.clone()),
19545 None,
19546 true,
19547 window,
19548 cx,
19549 );
19550 let active_item = workspace
19551 .active_item(cx)
19552 .expect("should have an active item after adding the multi buffer");
19553 assert_eq!(
19554 active_item.buffer_kind(cx),
19555 ItemBufferKind::Multibuffer,
19556 "A multi buffer was expected to active after adding"
19557 );
19558 active_item.item_id()
19559 })
19560 .unwrap();
19561 cx.executor().run_until_parked();
19562
19563 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19564 editor.change_selections(
19565 SelectionEffects::scroll(Autoscroll::Next),
19566 window,
19567 cx,
19568 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
19569 );
19570 editor.open_excerpts(&OpenExcerpts, window, cx);
19571 });
19572 cx.executor().run_until_parked();
19573 let first_item_id = workspace
19574 .update(cx, |workspace, window, cx| {
19575 let active_item = workspace
19576 .active_item(cx)
19577 .expect("should have an active item after navigating into the 1st buffer");
19578 let first_item_id = active_item.item_id();
19579 assert_ne!(
19580 first_item_id, multibuffer_item_id,
19581 "Should navigate into the 1st buffer and activate it"
19582 );
19583 assert_eq!(
19584 active_item.buffer_kind(cx),
19585 ItemBufferKind::Singleton,
19586 "New active item should be a singleton buffer"
19587 );
19588 assert_eq!(
19589 active_item
19590 .act_as::<Editor>(cx)
19591 .expect("should have navigated into an editor for the 1st buffer")
19592 .read(cx)
19593 .text(cx),
19594 sample_text_1
19595 );
19596
19597 workspace
19598 .go_back(workspace.active_pane().downgrade(), window, cx)
19599 .detach_and_log_err(cx);
19600
19601 first_item_id
19602 })
19603 .unwrap();
19604 cx.executor().run_until_parked();
19605 workspace
19606 .update(cx, |workspace, _, cx| {
19607 let active_item = workspace
19608 .active_item(cx)
19609 .expect("should have an active item after navigating back");
19610 assert_eq!(
19611 active_item.item_id(),
19612 multibuffer_item_id,
19613 "Should navigate back to the multi buffer"
19614 );
19615 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19616 })
19617 .unwrap();
19618
19619 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19620 editor.change_selections(
19621 SelectionEffects::scroll(Autoscroll::Next),
19622 window,
19623 cx,
19624 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
19625 );
19626 editor.open_excerpts(&OpenExcerpts, window, cx);
19627 });
19628 cx.executor().run_until_parked();
19629 let second_item_id = workspace
19630 .update(cx, |workspace, window, cx| {
19631 let active_item = workspace
19632 .active_item(cx)
19633 .expect("should have an active item after navigating into the 2nd buffer");
19634 let second_item_id = active_item.item_id();
19635 assert_ne!(
19636 second_item_id, multibuffer_item_id,
19637 "Should navigate away from the multibuffer"
19638 );
19639 assert_ne!(
19640 second_item_id, first_item_id,
19641 "Should navigate into the 2nd buffer and activate it"
19642 );
19643 assert_eq!(
19644 active_item.buffer_kind(cx),
19645 ItemBufferKind::Singleton,
19646 "New active item should be a singleton buffer"
19647 );
19648 assert_eq!(
19649 active_item
19650 .act_as::<Editor>(cx)
19651 .expect("should have navigated into an editor")
19652 .read(cx)
19653 .text(cx),
19654 sample_text_2
19655 );
19656
19657 workspace
19658 .go_back(workspace.active_pane().downgrade(), window, cx)
19659 .detach_and_log_err(cx);
19660
19661 second_item_id
19662 })
19663 .unwrap();
19664 cx.executor().run_until_parked();
19665 workspace
19666 .update(cx, |workspace, _, cx| {
19667 let active_item = workspace
19668 .active_item(cx)
19669 .expect("should have an active item after navigating back from the 2nd buffer");
19670 assert_eq!(
19671 active_item.item_id(),
19672 multibuffer_item_id,
19673 "Should navigate back from the 2nd buffer to the multi buffer"
19674 );
19675 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19676 })
19677 .unwrap();
19678
19679 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19680 editor.change_selections(
19681 SelectionEffects::scroll(Autoscroll::Next),
19682 window,
19683 cx,
19684 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
19685 );
19686 editor.open_excerpts(&OpenExcerpts, window, cx);
19687 });
19688 cx.executor().run_until_parked();
19689 workspace
19690 .update(cx, |workspace, window, cx| {
19691 let active_item = workspace
19692 .active_item(cx)
19693 .expect("should have an active item after navigating into the 3rd buffer");
19694 let third_item_id = active_item.item_id();
19695 assert_ne!(
19696 third_item_id, multibuffer_item_id,
19697 "Should navigate into the 3rd buffer and activate it"
19698 );
19699 assert_ne!(third_item_id, first_item_id);
19700 assert_ne!(third_item_id, second_item_id);
19701 assert_eq!(
19702 active_item.buffer_kind(cx),
19703 ItemBufferKind::Singleton,
19704 "New active item should be a singleton buffer"
19705 );
19706 assert_eq!(
19707 active_item
19708 .act_as::<Editor>(cx)
19709 .expect("should have navigated into an editor")
19710 .read(cx)
19711 .text(cx),
19712 sample_text_3
19713 );
19714
19715 workspace
19716 .go_back(workspace.active_pane().downgrade(), window, cx)
19717 .detach_and_log_err(cx);
19718 })
19719 .unwrap();
19720 cx.executor().run_until_parked();
19721 workspace
19722 .update(cx, |workspace, _, cx| {
19723 let active_item = workspace
19724 .active_item(cx)
19725 .expect("should have an active item after navigating back from the 3rd buffer");
19726 assert_eq!(
19727 active_item.item_id(),
19728 multibuffer_item_id,
19729 "Should navigate back from the 3rd buffer to the multi buffer"
19730 );
19731 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19732 })
19733 .unwrap();
19734}
19735
19736#[gpui::test]
19737async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19738 init_test(cx, |_| {});
19739
19740 let mut cx = EditorTestContext::new(cx).await;
19741
19742 let diff_base = r#"
19743 use some::mod;
19744
19745 const A: u32 = 42;
19746
19747 fn main() {
19748 println!("hello");
19749
19750 println!("world");
19751 }
19752 "#
19753 .unindent();
19754
19755 cx.set_state(
19756 &r#"
19757 use some::modified;
19758
19759 ˇ
19760 fn main() {
19761 println!("hello there");
19762
19763 println!("around the");
19764 println!("world");
19765 }
19766 "#
19767 .unindent(),
19768 );
19769
19770 cx.set_head_text(&diff_base);
19771 executor.run_until_parked();
19772
19773 cx.update_editor(|editor, window, cx| {
19774 editor.go_to_next_hunk(&GoToHunk, window, cx);
19775 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19776 });
19777 executor.run_until_parked();
19778 cx.assert_state_with_diff(
19779 r#"
19780 use some::modified;
19781
19782
19783 fn main() {
19784 - println!("hello");
19785 + ˇ println!("hello there");
19786
19787 println!("around the");
19788 println!("world");
19789 }
19790 "#
19791 .unindent(),
19792 );
19793
19794 cx.update_editor(|editor, window, cx| {
19795 for _ in 0..2 {
19796 editor.go_to_next_hunk(&GoToHunk, window, cx);
19797 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19798 }
19799 });
19800 executor.run_until_parked();
19801 cx.assert_state_with_diff(
19802 r#"
19803 - use some::mod;
19804 + ˇuse some::modified;
19805
19806
19807 fn main() {
19808 - println!("hello");
19809 + println!("hello there");
19810
19811 + println!("around the");
19812 println!("world");
19813 }
19814 "#
19815 .unindent(),
19816 );
19817
19818 cx.update_editor(|editor, window, cx| {
19819 editor.go_to_next_hunk(&GoToHunk, window, cx);
19820 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19821 });
19822 executor.run_until_parked();
19823 cx.assert_state_with_diff(
19824 r#"
19825 - use some::mod;
19826 + use some::modified;
19827
19828 - const A: u32 = 42;
19829 ˇ
19830 fn main() {
19831 - println!("hello");
19832 + println!("hello there");
19833
19834 + println!("around the");
19835 println!("world");
19836 }
19837 "#
19838 .unindent(),
19839 );
19840
19841 cx.update_editor(|editor, window, cx| {
19842 editor.cancel(&Cancel, window, cx);
19843 });
19844
19845 cx.assert_state_with_diff(
19846 r#"
19847 use some::modified;
19848
19849 ˇ
19850 fn main() {
19851 println!("hello there");
19852
19853 println!("around the");
19854 println!("world");
19855 }
19856 "#
19857 .unindent(),
19858 );
19859}
19860
19861#[gpui::test]
19862async fn test_diff_base_change_with_expanded_diff_hunks(
19863 executor: BackgroundExecutor,
19864 cx: &mut TestAppContext,
19865) {
19866 init_test(cx, |_| {});
19867
19868 let mut cx = EditorTestContext::new(cx).await;
19869
19870 let diff_base = r#"
19871 use some::mod1;
19872 use some::mod2;
19873
19874 const A: u32 = 42;
19875 const B: u32 = 42;
19876 const C: u32 = 42;
19877
19878 fn main() {
19879 println!("hello");
19880
19881 println!("world");
19882 }
19883 "#
19884 .unindent();
19885
19886 cx.set_state(
19887 &r#"
19888 use some::mod2;
19889
19890 const A: u32 = 42;
19891 const C: u32 = 42;
19892
19893 fn main(ˇ) {
19894 //println!("hello");
19895
19896 println!("world");
19897 //
19898 //
19899 }
19900 "#
19901 .unindent(),
19902 );
19903
19904 cx.set_head_text(&diff_base);
19905 executor.run_until_parked();
19906
19907 cx.update_editor(|editor, window, cx| {
19908 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19909 });
19910 executor.run_until_parked();
19911 cx.assert_state_with_diff(
19912 r#"
19913 - use some::mod1;
19914 use some::mod2;
19915
19916 const A: u32 = 42;
19917 - const B: u32 = 42;
19918 const C: u32 = 42;
19919
19920 fn main(ˇ) {
19921 - println!("hello");
19922 + //println!("hello");
19923
19924 println!("world");
19925 + //
19926 + //
19927 }
19928 "#
19929 .unindent(),
19930 );
19931
19932 cx.set_head_text("new diff base!");
19933 executor.run_until_parked();
19934 cx.assert_state_with_diff(
19935 r#"
19936 - new diff base!
19937 + use some::mod2;
19938 +
19939 + const A: u32 = 42;
19940 + const C: u32 = 42;
19941 +
19942 + fn main(ˇ) {
19943 + //println!("hello");
19944 +
19945 + println!("world");
19946 + //
19947 + //
19948 + }
19949 "#
19950 .unindent(),
19951 );
19952}
19953
19954#[gpui::test]
19955async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19956 init_test(cx, |_| {});
19957
19958 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19959 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19960 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19961 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19962 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19963 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19964
19965 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19966 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19967 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19968
19969 let multi_buffer = cx.new(|cx| {
19970 let mut multibuffer = MultiBuffer::new(ReadWrite);
19971 multibuffer.push_excerpts(
19972 buffer_1.clone(),
19973 [
19974 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19975 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19976 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19977 ],
19978 cx,
19979 );
19980 multibuffer.push_excerpts(
19981 buffer_2.clone(),
19982 [
19983 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19984 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19985 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19986 ],
19987 cx,
19988 );
19989 multibuffer.push_excerpts(
19990 buffer_3.clone(),
19991 [
19992 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19993 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19994 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19995 ],
19996 cx,
19997 );
19998 multibuffer
19999 });
20000
20001 let editor =
20002 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20003 editor
20004 .update(cx, |editor, _window, cx| {
20005 for (buffer, diff_base) in [
20006 (buffer_1.clone(), file_1_old),
20007 (buffer_2.clone(), file_2_old),
20008 (buffer_3.clone(), file_3_old),
20009 ] {
20010 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20011 editor
20012 .buffer
20013 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20014 }
20015 })
20016 .unwrap();
20017
20018 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20019 cx.run_until_parked();
20020
20021 cx.assert_editor_state(
20022 &"
20023 ˇaaa
20024 ccc
20025 ddd
20026
20027 ggg
20028 hhh
20029
20030
20031 lll
20032 mmm
20033 NNN
20034
20035 qqq
20036 rrr
20037
20038 uuu
20039 111
20040 222
20041 333
20042
20043 666
20044 777
20045
20046 000
20047 !!!"
20048 .unindent(),
20049 );
20050
20051 cx.update_editor(|editor, window, cx| {
20052 editor.select_all(&SelectAll, window, cx);
20053 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20054 });
20055 cx.executor().run_until_parked();
20056
20057 cx.assert_state_with_diff(
20058 "
20059 «aaa
20060 - bbb
20061 ccc
20062 ddd
20063
20064 ggg
20065 hhh
20066
20067
20068 lll
20069 mmm
20070 - nnn
20071 + NNN
20072
20073 qqq
20074 rrr
20075
20076 uuu
20077 111
20078 222
20079 333
20080
20081 + 666
20082 777
20083
20084 000
20085 !!!ˇ»"
20086 .unindent(),
20087 );
20088}
20089
20090#[gpui::test]
20091async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20092 init_test(cx, |_| {});
20093
20094 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20095 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20096
20097 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20098 let multi_buffer = cx.new(|cx| {
20099 let mut multibuffer = MultiBuffer::new(ReadWrite);
20100 multibuffer.push_excerpts(
20101 buffer.clone(),
20102 [
20103 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20104 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20105 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20106 ],
20107 cx,
20108 );
20109 multibuffer
20110 });
20111
20112 let editor =
20113 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20114 editor
20115 .update(cx, |editor, _window, cx| {
20116 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20117 editor
20118 .buffer
20119 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20120 })
20121 .unwrap();
20122
20123 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20124 cx.run_until_parked();
20125
20126 cx.update_editor(|editor, window, cx| {
20127 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20128 });
20129 cx.executor().run_until_parked();
20130
20131 // When the start of a hunk coincides with the start of its excerpt,
20132 // the hunk is expanded. When the start of a hunk is earlier than
20133 // the start of its excerpt, the hunk is not expanded.
20134 cx.assert_state_with_diff(
20135 "
20136 ˇaaa
20137 - bbb
20138 + BBB
20139
20140 - ddd
20141 - eee
20142 + DDD
20143 + EEE
20144 fff
20145
20146 iii
20147 "
20148 .unindent(),
20149 );
20150}
20151
20152#[gpui::test]
20153async fn test_edits_around_expanded_insertion_hunks(
20154 executor: BackgroundExecutor,
20155 cx: &mut TestAppContext,
20156) {
20157 init_test(cx, |_| {});
20158
20159 let mut cx = EditorTestContext::new(cx).await;
20160
20161 let diff_base = r#"
20162 use some::mod1;
20163 use some::mod2;
20164
20165 const A: u32 = 42;
20166
20167 fn main() {
20168 println!("hello");
20169
20170 println!("world");
20171 }
20172 "#
20173 .unindent();
20174 executor.run_until_parked();
20175 cx.set_state(
20176 &r#"
20177 use some::mod1;
20178 use some::mod2;
20179
20180 const A: u32 = 42;
20181 const B: u32 = 42;
20182 const C: u32 = 42;
20183 ˇ
20184
20185 fn main() {
20186 println!("hello");
20187
20188 println!("world");
20189 }
20190 "#
20191 .unindent(),
20192 );
20193
20194 cx.set_head_text(&diff_base);
20195 executor.run_until_parked();
20196
20197 cx.update_editor(|editor, window, cx| {
20198 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20199 });
20200 executor.run_until_parked();
20201
20202 cx.assert_state_with_diff(
20203 r#"
20204 use some::mod1;
20205 use some::mod2;
20206
20207 const A: u32 = 42;
20208 + const B: u32 = 42;
20209 + const C: u32 = 42;
20210 + ˇ
20211
20212 fn main() {
20213 println!("hello");
20214
20215 println!("world");
20216 }
20217 "#
20218 .unindent(),
20219 );
20220
20221 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20222 executor.run_until_parked();
20223
20224 cx.assert_state_with_diff(
20225 r#"
20226 use some::mod1;
20227 use some::mod2;
20228
20229 const A: u32 = 42;
20230 + const B: u32 = 42;
20231 + const C: u32 = 42;
20232 + const D: u32 = 42;
20233 + ˇ
20234
20235 fn main() {
20236 println!("hello");
20237
20238 println!("world");
20239 }
20240 "#
20241 .unindent(),
20242 );
20243
20244 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20245 executor.run_until_parked();
20246
20247 cx.assert_state_with_diff(
20248 r#"
20249 use some::mod1;
20250 use some::mod2;
20251
20252 const A: u32 = 42;
20253 + const B: u32 = 42;
20254 + const C: u32 = 42;
20255 + const D: u32 = 42;
20256 + const E: u32 = 42;
20257 + ˇ
20258
20259 fn main() {
20260 println!("hello");
20261
20262 println!("world");
20263 }
20264 "#
20265 .unindent(),
20266 );
20267
20268 cx.update_editor(|editor, window, cx| {
20269 editor.delete_line(&DeleteLine, window, cx);
20270 });
20271 executor.run_until_parked();
20272
20273 cx.assert_state_with_diff(
20274 r#"
20275 use some::mod1;
20276 use some::mod2;
20277
20278 const A: u32 = 42;
20279 + const B: u32 = 42;
20280 + const C: u32 = 42;
20281 + const D: u32 = 42;
20282 + const E: u32 = 42;
20283 ˇ
20284 fn main() {
20285 println!("hello");
20286
20287 println!("world");
20288 }
20289 "#
20290 .unindent(),
20291 );
20292
20293 cx.update_editor(|editor, window, cx| {
20294 editor.move_up(&MoveUp, window, cx);
20295 editor.delete_line(&DeleteLine, window, cx);
20296 editor.move_up(&MoveUp, window, cx);
20297 editor.delete_line(&DeleteLine, window, cx);
20298 editor.move_up(&MoveUp, window, cx);
20299 editor.delete_line(&DeleteLine, window, cx);
20300 });
20301 executor.run_until_parked();
20302 cx.assert_state_with_diff(
20303 r#"
20304 use some::mod1;
20305 use some::mod2;
20306
20307 const A: u32 = 42;
20308 + const B: u32 = 42;
20309 ˇ
20310 fn main() {
20311 println!("hello");
20312
20313 println!("world");
20314 }
20315 "#
20316 .unindent(),
20317 );
20318
20319 cx.update_editor(|editor, window, cx| {
20320 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20321 editor.delete_line(&DeleteLine, window, cx);
20322 });
20323 executor.run_until_parked();
20324 cx.assert_state_with_diff(
20325 r#"
20326 ˇ
20327 fn main() {
20328 println!("hello");
20329
20330 println!("world");
20331 }
20332 "#
20333 .unindent(),
20334 );
20335}
20336
20337#[gpui::test]
20338async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20339 init_test(cx, |_| {});
20340
20341 let mut cx = EditorTestContext::new(cx).await;
20342 cx.set_head_text(indoc! { "
20343 one
20344 two
20345 three
20346 four
20347 five
20348 "
20349 });
20350 cx.set_state(indoc! { "
20351 one
20352 ˇthree
20353 five
20354 "});
20355 cx.run_until_parked();
20356 cx.update_editor(|editor, window, cx| {
20357 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20358 });
20359 cx.assert_state_with_diff(
20360 indoc! { "
20361 one
20362 - two
20363 ˇthree
20364 - four
20365 five
20366 "}
20367 .to_string(),
20368 );
20369 cx.update_editor(|editor, window, cx| {
20370 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20371 });
20372
20373 cx.assert_state_with_diff(
20374 indoc! { "
20375 one
20376 ˇthree
20377 five
20378 "}
20379 .to_string(),
20380 );
20381
20382 cx.set_state(indoc! { "
20383 one
20384 ˇTWO
20385 three
20386 four
20387 five
20388 "});
20389 cx.run_until_parked();
20390 cx.update_editor(|editor, window, cx| {
20391 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20392 });
20393
20394 cx.assert_state_with_diff(
20395 indoc! { "
20396 one
20397 - two
20398 + ˇTWO
20399 three
20400 four
20401 five
20402 "}
20403 .to_string(),
20404 );
20405 cx.update_editor(|editor, window, cx| {
20406 editor.move_up(&Default::default(), window, cx);
20407 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20408 });
20409 cx.assert_state_with_diff(
20410 indoc! { "
20411 one
20412 ˇTWO
20413 three
20414 four
20415 five
20416 "}
20417 .to_string(),
20418 );
20419}
20420
20421#[gpui::test]
20422async fn test_edits_around_expanded_deletion_hunks(
20423 executor: BackgroundExecutor,
20424 cx: &mut TestAppContext,
20425) {
20426 init_test(cx, |_| {});
20427
20428 let mut cx = EditorTestContext::new(cx).await;
20429
20430 let diff_base = r#"
20431 use some::mod1;
20432 use some::mod2;
20433
20434 const A: u32 = 42;
20435 const B: u32 = 42;
20436 const C: u32 = 42;
20437
20438
20439 fn main() {
20440 println!("hello");
20441
20442 println!("world");
20443 }
20444 "#
20445 .unindent();
20446 executor.run_until_parked();
20447 cx.set_state(
20448 &r#"
20449 use some::mod1;
20450 use some::mod2;
20451
20452 ˇconst B: u32 = 42;
20453 const C: u32 = 42;
20454
20455
20456 fn main() {
20457 println!("hello");
20458
20459 println!("world");
20460 }
20461 "#
20462 .unindent(),
20463 );
20464
20465 cx.set_head_text(&diff_base);
20466 executor.run_until_parked();
20467
20468 cx.update_editor(|editor, window, cx| {
20469 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20470 });
20471 executor.run_until_parked();
20472
20473 cx.assert_state_with_diff(
20474 r#"
20475 use some::mod1;
20476 use some::mod2;
20477
20478 - const A: u32 = 42;
20479 ˇconst B: u32 = 42;
20480 const C: u32 = 42;
20481
20482
20483 fn main() {
20484 println!("hello");
20485
20486 println!("world");
20487 }
20488 "#
20489 .unindent(),
20490 );
20491
20492 cx.update_editor(|editor, window, cx| {
20493 editor.delete_line(&DeleteLine, window, cx);
20494 });
20495 executor.run_until_parked();
20496 cx.assert_state_with_diff(
20497 r#"
20498 use some::mod1;
20499 use some::mod2;
20500
20501 - const A: u32 = 42;
20502 - const B: u32 = 42;
20503 ˇconst C: u32 = 42;
20504
20505
20506 fn main() {
20507 println!("hello");
20508
20509 println!("world");
20510 }
20511 "#
20512 .unindent(),
20513 );
20514
20515 cx.update_editor(|editor, window, cx| {
20516 editor.delete_line(&DeleteLine, window, cx);
20517 });
20518 executor.run_until_parked();
20519 cx.assert_state_with_diff(
20520 r#"
20521 use some::mod1;
20522 use some::mod2;
20523
20524 - const A: u32 = 42;
20525 - const B: u32 = 42;
20526 - const C: u32 = 42;
20527 ˇ
20528
20529 fn main() {
20530 println!("hello");
20531
20532 println!("world");
20533 }
20534 "#
20535 .unindent(),
20536 );
20537
20538 cx.update_editor(|editor, window, cx| {
20539 editor.handle_input("replacement", window, cx);
20540 });
20541 executor.run_until_parked();
20542 cx.assert_state_with_diff(
20543 r#"
20544 use some::mod1;
20545 use some::mod2;
20546
20547 - const A: u32 = 42;
20548 - const B: u32 = 42;
20549 - const C: u32 = 42;
20550 -
20551 + replacementˇ
20552
20553 fn main() {
20554 println!("hello");
20555
20556 println!("world");
20557 }
20558 "#
20559 .unindent(),
20560 );
20561}
20562
20563#[gpui::test]
20564async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20565 init_test(cx, |_| {});
20566
20567 let mut cx = EditorTestContext::new(cx).await;
20568
20569 let base_text = r#"
20570 one
20571 two
20572 three
20573 four
20574 five
20575 "#
20576 .unindent();
20577 executor.run_until_parked();
20578 cx.set_state(
20579 &r#"
20580 one
20581 two
20582 fˇour
20583 five
20584 "#
20585 .unindent(),
20586 );
20587
20588 cx.set_head_text(&base_text);
20589 executor.run_until_parked();
20590
20591 cx.update_editor(|editor, window, cx| {
20592 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20593 });
20594 executor.run_until_parked();
20595
20596 cx.assert_state_with_diff(
20597 r#"
20598 one
20599 two
20600 - three
20601 fˇour
20602 five
20603 "#
20604 .unindent(),
20605 );
20606
20607 cx.update_editor(|editor, window, cx| {
20608 editor.backspace(&Backspace, window, cx);
20609 editor.backspace(&Backspace, window, cx);
20610 });
20611 executor.run_until_parked();
20612 cx.assert_state_with_diff(
20613 r#"
20614 one
20615 two
20616 - threeˇ
20617 - four
20618 + our
20619 five
20620 "#
20621 .unindent(),
20622 );
20623}
20624
20625#[gpui::test]
20626async fn test_edit_after_expanded_modification_hunk(
20627 executor: BackgroundExecutor,
20628 cx: &mut TestAppContext,
20629) {
20630 init_test(cx, |_| {});
20631
20632 let mut cx = EditorTestContext::new(cx).await;
20633
20634 let diff_base = r#"
20635 use some::mod1;
20636 use some::mod2;
20637
20638 const A: u32 = 42;
20639 const B: u32 = 42;
20640 const C: u32 = 42;
20641 const D: u32 = 42;
20642
20643
20644 fn main() {
20645 println!("hello");
20646
20647 println!("world");
20648 }"#
20649 .unindent();
20650
20651 cx.set_state(
20652 &r#"
20653 use some::mod1;
20654 use some::mod2;
20655
20656 const A: u32 = 42;
20657 const B: u32 = 42;
20658 const C: u32 = 43ˇ
20659 const D: u32 = 42;
20660
20661
20662 fn main() {
20663 println!("hello");
20664
20665 println!("world");
20666 }"#
20667 .unindent(),
20668 );
20669
20670 cx.set_head_text(&diff_base);
20671 executor.run_until_parked();
20672 cx.update_editor(|editor, window, cx| {
20673 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20674 });
20675 executor.run_until_parked();
20676
20677 cx.assert_state_with_diff(
20678 r#"
20679 use some::mod1;
20680 use some::mod2;
20681
20682 const A: u32 = 42;
20683 const B: u32 = 42;
20684 - const C: u32 = 42;
20685 + const C: u32 = 43ˇ
20686 const D: u32 = 42;
20687
20688
20689 fn main() {
20690 println!("hello");
20691
20692 println!("world");
20693 }"#
20694 .unindent(),
20695 );
20696
20697 cx.update_editor(|editor, window, cx| {
20698 editor.handle_input("\nnew_line\n", window, cx);
20699 });
20700 executor.run_until_parked();
20701
20702 cx.assert_state_with_diff(
20703 r#"
20704 use some::mod1;
20705 use some::mod2;
20706
20707 const A: u32 = 42;
20708 const B: u32 = 42;
20709 - const C: u32 = 42;
20710 + const C: u32 = 43
20711 + new_line
20712 + ˇ
20713 const D: u32 = 42;
20714
20715
20716 fn main() {
20717 println!("hello");
20718
20719 println!("world");
20720 }"#
20721 .unindent(),
20722 );
20723}
20724
20725#[gpui::test]
20726async fn test_stage_and_unstage_added_file_hunk(
20727 executor: BackgroundExecutor,
20728 cx: &mut TestAppContext,
20729) {
20730 init_test(cx, |_| {});
20731
20732 let mut cx = EditorTestContext::new(cx).await;
20733 cx.update_editor(|editor, _, cx| {
20734 editor.set_expand_all_diff_hunks(cx);
20735 });
20736
20737 let working_copy = r#"
20738 ˇfn main() {
20739 println!("hello, world!");
20740 }
20741 "#
20742 .unindent();
20743
20744 cx.set_state(&working_copy);
20745 executor.run_until_parked();
20746
20747 cx.assert_state_with_diff(
20748 r#"
20749 + ˇfn main() {
20750 + println!("hello, world!");
20751 + }
20752 "#
20753 .unindent(),
20754 );
20755 cx.assert_index_text(None);
20756
20757 cx.update_editor(|editor, window, cx| {
20758 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20759 });
20760 executor.run_until_parked();
20761 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20762 cx.assert_state_with_diff(
20763 r#"
20764 + ˇfn main() {
20765 + println!("hello, world!");
20766 + }
20767 "#
20768 .unindent(),
20769 );
20770
20771 cx.update_editor(|editor, window, cx| {
20772 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20773 });
20774 executor.run_until_parked();
20775 cx.assert_index_text(None);
20776}
20777
20778async fn setup_indent_guides_editor(
20779 text: &str,
20780 cx: &mut TestAppContext,
20781) -> (BufferId, EditorTestContext) {
20782 init_test(cx, |_| {});
20783
20784 let mut cx = EditorTestContext::new(cx).await;
20785
20786 let buffer_id = cx.update_editor(|editor, window, cx| {
20787 editor.set_text(text, window, cx);
20788 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20789
20790 buffer_ids[0]
20791 });
20792
20793 (buffer_id, cx)
20794}
20795
20796fn assert_indent_guides(
20797 range: Range<u32>,
20798 expected: Vec<IndentGuide>,
20799 active_indices: Option<Vec<usize>>,
20800 cx: &mut EditorTestContext,
20801) {
20802 let indent_guides = cx.update_editor(|editor, window, cx| {
20803 let snapshot = editor.snapshot(window, cx).display_snapshot;
20804 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20805 editor,
20806 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20807 true,
20808 &snapshot,
20809 cx,
20810 );
20811
20812 indent_guides.sort_by(|a, b| {
20813 a.depth.cmp(&b.depth).then(
20814 a.start_row
20815 .cmp(&b.start_row)
20816 .then(a.end_row.cmp(&b.end_row)),
20817 )
20818 });
20819 indent_guides
20820 });
20821
20822 if let Some(expected) = active_indices {
20823 let active_indices = cx.update_editor(|editor, window, cx| {
20824 let snapshot = editor.snapshot(window, cx).display_snapshot;
20825 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20826 });
20827
20828 assert_eq!(
20829 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20830 expected,
20831 "Active indent guide indices do not match"
20832 );
20833 }
20834
20835 assert_eq!(indent_guides, expected, "Indent guides do not match");
20836}
20837
20838fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20839 IndentGuide {
20840 buffer_id,
20841 start_row: MultiBufferRow(start_row),
20842 end_row: MultiBufferRow(end_row),
20843 depth,
20844 tab_size: 4,
20845 settings: IndentGuideSettings {
20846 enabled: true,
20847 line_width: 1,
20848 active_line_width: 1,
20849 coloring: IndentGuideColoring::default(),
20850 background_coloring: IndentGuideBackgroundColoring::default(),
20851 },
20852 }
20853}
20854
20855#[gpui::test]
20856async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20857 let (buffer_id, mut cx) = setup_indent_guides_editor(
20858 &"
20859 fn main() {
20860 let a = 1;
20861 }"
20862 .unindent(),
20863 cx,
20864 )
20865 .await;
20866
20867 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20868}
20869
20870#[gpui::test]
20871async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20872 let (buffer_id, mut cx) = setup_indent_guides_editor(
20873 &"
20874 fn main() {
20875 let a = 1;
20876 let b = 2;
20877 }"
20878 .unindent(),
20879 cx,
20880 )
20881 .await;
20882
20883 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20884}
20885
20886#[gpui::test]
20887async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20888 let (buffer_id, mut cx) = setup_indent_guides_editor(
20889 &"
20890 fn main() {
20891 let a = 1;
20892 if a == 3 {
20893 let b = 2;
20894 } else {
20895 let c = 3;
20896 }
20897 }"
20898 .unindent(),
20899 cx,
20900 )
20901 .await;
20902
20903 assert_indent_guides(
20904 0..8,
20905 vec![
20906 indent_guide(buffer_id, 1, 6, 0),
20907 indent_guide(buffer_id, 3, 3, 1),
20908 indent_guide(buffer_id, 5, 5, 1),
20909 ],
20910 None,
20911 &mut cx,
20912 );
20913}
20914
20915#[gpui::test]
20916async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20917 let (buffer_id, mut cx) = setup_indent_guides_editor(
20918 &"
20919 fn main() {
20920 let a = 1;
20921 let b = 2;
20922 let c = 3;
20923 }"
20924 .unindent(),
20925 cx,
20926 )
20927 .await;
20928
20929 assert_indent_guides(
20930 0..5,
20931 vec![
20932 indent_guide(buffer_id, 1, 3, 0),
20933 indent_guide(buffer_id, 2, 2, 1),
20934 ],
20935 None,
20936 &mut cx,
20937 );
20938}
20939
20940#[gpui::test]
20941async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20942 let (buffer_id, mut cx) = setup_indent_guides_editor(
20943 &"
20944 fn main() {
20945 let a = 1;
20946
20947 let c = 3;
20948 }"
20949 .unindent(),
20950 cx,
20951 )
20952 .await;
20953
20954 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20955}
20956
20957#[gpui::test]
20958async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20959 let (buffer_id, mut cx) = setup_indent_guides_editor(
20960 &"
20961 fn main() {
20962 let a = 1;
20963
20964 let c = 3;
20965
20966 if a == 3 {
20967 let b = 2;
20968 } else {
20969 let c = 3;
20970 }
20971 }"
20972 .unindent(),
20973 cx,
20974 )
20975 .await;
20976
20977 assert_indent_guides(
20978 0..11,
20979 vec![
20980 indent_guide(buffer_id, 1, 9, 0),
20981 indent_guide(buffer_id, 6, 6, 1),
20982 indent_guide(buffer_id, 8, 8, 1),
20983 ],
20984 None,
20985 &mut cx,
20986 );
20987}
20988
20989#[gpui::test]
20990async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20991 let (buffer_id, mut cx) = setup_indent_guides_editor(
20992 &"
20993 fn main() {
20994 let a = 1;
20995
20996 let c = 3;
20997
20998 if a == 3 {
20999 let b = 2;
21000 } else {
21001 let c = 3;
21002 }
21003 }"
21004 .unindent(),
21005 cx,
21006 )
21007 .await;
21008
21009 assert_indent_guides(
21010 1..11,
21011 vec![
21012 indent_guide(buffer_id, 1, 9, 0),
21013 indent_guide(buffer_id, 6, 6, 1),
21014 indent_guide(buffer_id, 8, 8, 1),
21015 ],
21016 None,
21017 &mut cx,
21018 );
21019}
21020
21021#[gpui::test]
21022async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21023 let (buffer_id, mut cx) = setup_indent_guides_editor(
21024 &"
21025 fn main() {
21026 let a = 1;
21027
21028 let c = 3;
21029
21030 if a == 3 {
21031 let b = 2;
21032 } else {
21033 let c = 3;
21034 }
21035 }"
21036 .unindent(),
21037 cx,
21038 )
21039 .await;
21040
21041 assert_indent_guides(
21042 1..10,
21043 vec![
21044 indent_guide(buffer_id, 1, 9, 0),
21045 indent_guide(buffer_id, 6, 6, 1),
21046 indent_guide(buffer_id, 8, 8, 1),
21047 ],
21048 None,
21049 &mut cx,
21050 );
21051}
21052
21053#[gpui::test]
21054async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21055 let (buffer_id, mut cx) = setup_indent_guides_editor(
21056 &"
21057 fn main() {
21058 if a {
21059 b(
21060 c,
21061 d,
21062 )
21063 } else {
21064 e(
21065 f
21066 )
21067 }
21068 }"
21069 .unindent(),
21070 cx,
21071 )
21072 .await;
21073
21074 assert_indent_guides(
21075 0..11,
21076 vec![
21077 indent_guide(buffer_id, 1, 10, 0),
21078 indent_guide(buffer_id, 2, 5, 1),
21079 indent_guide(buffer_id, 7, 9, 1),
21080 indent_guide(buffer_id, 3, 4, 2),
21081 indent_guide(buffer_id, 8, 8, 2),
21082 ],
21083 None,
21084 &mut cx,
21085 );
21086
21087 cx.update_editor(|editor, window, cx| {
21088 editor.fold_at(MultiBufferRow(2), window, cx);
21089 assert_eq!(
21090 editor.display_text(cx),
21091 "
21092 fn main() {
21093 if a {
21094 b(⋯
21095 )
21096 } else {
21097 e(
21098 f
21099 )
21100 }
21101 }"
21102 .unindent()
21103 );
21104 });
21105
21106 assert_indent_guides(
21107 0..11,
21108 vec![
21109 indent_guide(buffer_id, 1, 10, 0),
21110 indent_guide(buffer_id, 2, 5, 1),
21111 indent_guide(buffer_id, 7, 9, 1),
21112 indent_guide(buffer_id, 8, 8, 2),
21113 ],
21114 None,
21115 &mut cx,
21116 );
21117}
21118
21119#[gpui::test]
21120async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21121 let (buffer_id, mut cx) = setup_indent_guides_editor(
21122 &"
21123 block1
21124 block2
21125 block3
21126 block4
21127 block2
21128 block1
21129 block1"
21130 .unindent(),
21131 cx,
21132 )
21133 .await;
21134
21135 assert_indent_guides(
21136 1..10,
21137 vec![
21138 indent_guide(buffer_id, 1, 4, 0),
21139 indent_guide(buffer_id, 2, 3, 1),
21140 indent_guide(buffer_id, 3, 3, 2),
21141 ],
21142 None,
21143 &mut cx,
21144 );
21145}
21146
21147#[gpui::test]
21148async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21149 let (buffer_id, mut cx) = setup_indent_guides_editor(
21150 &"
21151 block1
21152 block2
21153 block3
21154
21155 block1
21156 block1"
21157 .unindent(),
21158 cx,
21159 )
21160 .await;
21161
21162 assert_indent_guides(
21163 0..6,
21164 vec![
21165 indent_guide(buffer_id, 1, 2, 0),
21166 indent_guide(buffer_id, 2, 2, 1),
21167 ],
21168 None,
21169 &mut cx,
21170 );
21171}
21172
21173#[gpui::test]
21174async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21175 let (buffer_id, mut cx) = setup_indent_guides_editor(
21176 &"
21177 function component() {
21178 \treturn (
21179 \t\t\t
21180 \t\t<div>
21181 \t\t\t<abc></abc>
21182 \t\t</div>
21183 \t)
21184 }"
21185 .unindent(),
21186 cx,
21187 )
21188 .await;
21189
21190 assert_indent_guides(
21191 0..8,
21192 vec![
21193 indent_guide(buffer_id, 1, 6, 0),
21194 indent_guide(buffer_id, 2, 5, 1),
21195 indent_guide(buffer_id, 4, 4, 2),
21196 ],
21197 None,
21198 &mut cx,
21199 );
21200}
21201
21202#[gpui::test]
21203async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21204 let (buffer_id, mut cx) = setup_indent_guides_editor(
21205 &"
21206 function component() {
21207 \treturn (
21208 \t
21209 \t\t<div>
21210 \t\t\t<abc></abc>
21211 \t\t</div>
21212 \t)
21213 }"
21214 .unindent(),
21215 cx,
21216 )
21217 .await;
21218
21219 assert_indent_guides(
21220 0..8,
21221 vec![
21222 indent_guide(buffer_id, 1, 6, 0),
21223 indent_guide(buffer_id, 2, 5, 1),
21224 indent_guide(buffer_id, 4, 4, 2),
21225 ],
21226 None,
21227 &mut cx,
21228 );
21229}
21230
21231#[gpui::test]
21232async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21233 let (buffer_id, mut cx) = setup_indent_guides_editor(
21234 &"
21235 block1
21236
21237
21238
21239 block2
21240 "
21241 .unindent(),
21242 cx,
21243 )
21244 .await;
21245
21246 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21247}
21248
21249#[gpui::test]
21250async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21251 let (buffer_id, mut cx) = setup_indent_guides_editor(
21252 &"
21253 def a:
21254 \tb = 3
21255 \tif True:
21256 \t\tc = 4
21257 \t\td = 5
21258 \tprint(b)
21259 "
21260 .unindent(),
21261 cx,
21262 )
21263 .await;
21264
21265 assert_indent_guides(
21266 0..6,
21267 vec![
21268 indent_guide(buffer_id, 1, 5, 0),
21269 indent_guide(buffer_id, 3, 4, 1),
21270 ],
21271 None,
21272 &mut cx,
21273 );
21274}
21275
21276#[gpui::test]
21277async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21278 let (buffer_id, mut cx) = setup_indent_guides_editor(
21279 &"
21280 fn main() {
21281 let a = 1;
21282 }"
21283 .unindent(),
21284 cx,
21285 )
21286 .await;
21287
21288 cx.update_editor(|editor, window, cx| {
21289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21290 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21291 });
21292 });
21293
21294 assert_indent_guides(
21295 0..3,
21296 vec![indent_guide(buffer_id, 1, 1, 0)],
21297 Some(vec![0]),
21298 &mut cx,
21299 );
21300}
21301
21302#[gpui::test]
21303async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21304 let (buffer_id, mut cx) = setup_indent_guides_editor(
21305 &"
21306 fn main() {
21307 if 1 == 2 {
21308 let a = 1;
21309 }
21310 }"
21311 .unindent(),
21312 cx,
21313 )
21314 .await;
21315
21316 cx.update_editor(|editor, window, cx| {
21317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21318 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21319 });
21320 });
21321
21322 assert_indent_guides(
21323 0..4,
21324 vec![
21325 indent_guide(buffer_id, 1, 3, 0),
21326 indent_guide(buffer_id, 2, 2, 1),
21327 ],
21328 Some(vec![1]),
21329 &mut cx,
21330 );
21331
21332 cx.update_editor(|editor, window, cx| {
21333 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21334 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21335 });
21336 });
21337
21338 assert_indent_guides(
21339 0..4,
21340 vec![
21341 indent_guide(buffer_id, 1, 3, 0),
21342 indent_guide(buffer_id, 2, 2, 1),
21343 ],
21344 Some(vec![1]),
21345 &mut cx,
21346 );
21347
21348 cx.update_editor(|editor, window, cx| {
21349 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21350 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21351 });
21352 });
21353
21354 assert_indent_guides(
21355 0..4,
21356 vec![
21357 indent_guide(buffer_id, 1, 3, 0),
21358 indent_guide(buffer_id, 2, 2, 1),
21359 ],
21360 Some(vec![0]),
21361 &mut cx,
21362 );
21363}
21364
21365#[gpui::test]
21366async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21367 let (buffer_id, mut cx) = setup_indent_guides_editor(
21368 &"
21369 fn main() {
21370 let a = 1;
21371
21372 let b = 2;
21373 }"
21374 .unindent(),
21375 cx,
21376 )
21377 .await;
21378
21379 cx.update_editor(|editor, window, cx| {
21380 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21381 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21382 });
21383 });
21384
21385 assert_indent_guides(
21386 0..5,
21387 vec![indent_guide(buffer_id, 1, 3, 0)],
21388 Some(vec![0]),
21389 &mut cx,
21390 );
21391}
21392
21393#[gpui::test]
21394async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21395 let (buffer_id, mut cx) = setup_indent_guides_editor(
21396 &"
21397 def m:
21398 a = 1
21399 pass"
21400 .unindent(),
21401 cx,
21402 )
21403 .await;
21404
21405 cx.update_editor(|editor, window, cx| {
21406 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21407 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21408 });
21409 });
21410
21411 assert_indent_guides(
21412 0..3,
21413 vec![indent_guide(buffer_id, 1, 2, 0)],
21414 Some(vec![0]),
21415 &mut cx,
21416 );
21417}
21418
21419#[gpui::test]
21420async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21421 init_test(cx, |_| {});
21422 let mut cx = EditorTestContext::new(cx).await;
21423 let text = indoc! {
21424 "
21425 impl A {
21426 fn b() {
21427 0;
21428 3;
21429 5;
21430 6;
21431 7;
21432 }
21433 }
21434 "
21435 };
21436 let base_text = indoc! {
21437 "
21438 impl A {
21439 fn b() {
21440 0;
21441 1;
21442 2;
21443 3;
21444 4;
21445 }
21446 fn c() {
21447 5;
21448 6;
21449 7;
21450 }
21451 }
21452 "
21453 };
21454
21455 cx.update_editor(|editor, window, cx| {
21456 editor.set_text(text, window, cx);
21457
21458 editor.buffer().update(cx, |multibuffer, cx| {
21459 let buffer = multibuffer.as_singleton().unwrap();
21460 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21461
21462 multibuffer.set_all_diff_hunks_expanded(cx);
21463 multibuffer.add_diff(diff, cx);
21464
21465 buffer.read(cx).remote_id()
21466 })
21467 });
21468 cx.run_until_parked();
21469
21470 cx.assert_state_with_diff(
21471 indoc! { "
21472 impl A {
21473 fn b() {
21474 0;
21475 - 1;
21476 - 2;
21477 3;
21478 - 4;
21479 - }
21480 - fn c() {
21481 5;
21482 6;
21483 7;
21484 }
21485 }
21486 ˇ"
21487 }
21488 .to_string(),
21489 );
21490
21491 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21492 editor
21493 .snapshot(window, cx)
21494 .buffer_snapshot()
21495 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21496 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21497 .collect::<Vec<_>>()
21498 });
21499 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21500 assert_eq!(
21501 actual_guides,
21502 vec![
21503 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21504 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21505 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21506 ]
21507 );
21508}
21509
21510#[gpui::test]
21511async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21512 init_test(cx, |_| {});
21513 let mut cx = EditorTestContext::new(cx).await;
21514
21515 let diff_base = r#"
21516 a
21517 b
21518 c
21519 "#
21520 .unindent();
21521
21522 cx.set_state(
21523 &r#"
21524 ˇA
21525 b
21526 C
21527 "#
21528 .unindent(),
21529 );
21530 cx.set_head_text(&diff_base);
21531 cx.update_editor(|editor, window, cx| {
21532 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21533 });
21534 executor.run_until_parked();
21535
21536 let both_hunks_expanded = r#"
21537 - a
21538 + ˇA
21539 b
21540 - c
21541 + C
21542 "#
21543 .unindent();
21544
21545 cx.assert_state_with_diff(both_hunks_expanded.clone());
21546
21547 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21548 let snapshot = editor.snapshot(window, cx);
21549 let hunks = editor
21550 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21551 .collect::<Vec<_>>();
21552 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21553 hunks
21554 .into_iter()
21555 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21556 .collect::<Vec<_>>()
21557 });
21558 assert_eq!(hunk_ranges.len(), 2);
21559
21560 cx.update_editor(|editor, _, cx| {
21561 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21562 });
21563 executor.run_until_parked();
21564
21565 let second_hunk_expanded = r#"
21566 ˇA
21567 b
21568 - c
21569 + C
21570 "#
21571 .unindent();
21572
21573 cx.assert_state_with_diff(second_hunk_expanded);
21574
21575 cx.update_editor(|editor, _, cx| {
21576 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21577 });
21578 executor.run_until_parked();
21579
21580 cx.assert_state_with_diff(both_hunks_expanded.clone());
21581
21582 cx.update_editor(|editor, _, cx| {
21583 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21584 });
21585 executor.run_until_parked();
21586
21587 let first_hunk_expanded = r#"
21588 - a
21589 + ˇA
21590 b
21591 C
21592 "#
21593 .unindent();
21594
21595 cx.assert_state_with_diff(first_hunk_expanded);
21596
21597 cx.update_editor(|editor, _, cx| {
21598 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21599 });
21600 executor.run_until_parked();
21601
21602 cx.assert_state_with_diff(both_hunks_expanded);
21603
21604 cx.set_state(
21605 &r#"
21606 ˇA
21607 b
21608 "#
21609 .unindent(),
21610 );
21611 cx.run_until_parked();
21612
21613 // TODO this cursor position seems bad
21614 cx.assert_state_with_diff(
21615 r#"
21616 - ˇa
21617 + A
21618 b
21619 "#
21620 .unindent(),
21621 );
21622
21623 cx.update_editor(|editor, window, cx| {
21624 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21625 });
21626
21627 cx.assert_state_with_diff(
21628 r#"
21629 - ˇa
21630 + A
21631 b
21632 - c
21633 "#
21634 .unindent(),
21635 );
21636
21637 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21638 let snapshot = editor.snapshot(window, cx);
21639 let hunks = editor
21640 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21641 .collect::<Vec<_>>();
21642 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21643 hunks
21644 .into_iter()
21645 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21646 .collect::<Vec<_>>()
21647 });
21648 assert_eq!(hunk_ranges.len(), 2);
21649
21650 cx.update_editor(|editor, _, cx| {
21651 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21652 });
21653 executor.run_until_parked();
21654
21655 cx.assert_state_with_diff(
21656 r#"
21657 - ˇa
21658 + A
21659 b
21660 "#
21661 .unindent(),
21662 );
21663}
21664
21665#[gpui::test]
21666async fn test_toggle_deletion_hunk_at_start_of_file(
21667 executor: BackgroundExecutor,
21668 cx: &mut TestAppContext,
21669) {
21670 init_test(cx, |_| {});
21671 let mut cx = EditorTestContext::new(cx).await;
21672
21673 let diff_base = r#"
21674 a
21675 b
21676 c
21677 "#
21678 .unindent();
21679
21680 cx.set_state(
21681 &r#"
21682 ˇb
21683 c
21684 "#
21685 .unindent(),
21686 );
21687 cx.set_head_text(&diff_base);
21688 cx.update_editor(|editor, window, cx| {
21689 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21690 });
21691 executor.run_until_parked();
21692
21693 let hunk_expanded = r#"
21694 - a
21695 ˇb
21696 c
21697 "#
21698 .unindent();
21699
21700 cx.assert_state_with_diff(hunk_expanded.clone());
21701
21702 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21703 let snapshot = editor.snapshot(window, cx);
21704 let hunks = editor
21705 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21706 .collect::<Vec<_>>();
21707 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21708 hunks
21709 .into_iter()
21710 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21711 .collect::<Vec<_>>()
21712 });
21713 assert_eq!(hunk_ranges.len(), 1);
21714
21715 cx.update_editor(|editor, _, cx| {
21716 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21717 });
21718 executor.run_until_parked();
21719
21720 let hunk_collapsed = r#"
21721 ˇb
21722 c
21723 "#
21724 .unindent();
21725
21726 cx.assert_state_with_diff(hunk_collapsed);
21727
21728 cx.update_editor(|editor, _, cx| {
21729 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21730 });
21731 executor.run_until_parked();
21732
21733 cx.assert_state_with_diff(hunk_expanded);
21734}
21735
21736#[gpui::test]
21737async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21738 init_test(cx, |_| {});
21739
21740 let fs = FakeFs::new(cx.executor());
21741 fs.insert_tree(
21742 path!("/test"),
21743 json!({
21744 ".git": {},
21745 "file-1": "ONE\n",
21746 "file-2": "TWO\n",
21747 "file-3": "THREE\n",
21748 }),
21749 )
21750 .await;
21751
21752 fs.set_head_for_repo(
21753 path!("/test/.git").as_ref(),
21754 &[
21755 ("file-1", "one\n".into()),
21756 ("file-2", "two\n".into()),
21757 ("file-3", "three\n".into()),
21758 ],
21759 "deadbeef",
21760 );
21761
21762 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21763 let mut buffers = vec![];
21764 for i in 1..=3 {
21765 let buffer = project
21766 .update(cx, |project, cx| {
21767 let path = format!(path!("/test/file-{}"), i);
21768 project.open_local_buffer(path, cx)
21769 })
21770 .await
21771 .unwrap();
21772 buffers.push(buffer);
21773 }
21774
21775 let multibuffer = cx.new(|cx| {
21776 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21777 multibuffer.set_all_diff_hunks_expanded(cx);
21778 for buffer in &buffers {
21779 let snapshot = buffer.read(cx).snapshot();
21780 multibuffer.set_excerpts_for_path(
21781 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21782 buffer.clone(),
21783 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21784 2,
21785 cx,
21786 );
21787 }
21788 multibuffer
21789 });
21790
21791 let editor = cx.add_window(|window, cx| {
21792 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21793 });
21794 cx.run_until_parked();
21795
21796 let snapshot = editor
21797 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21798 .unwrap();
21799 let hunks = snapshot
21800 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21801 .map(|hunk| match hunk {
21802 DisplayDiffHunk::Unfolded {
21803 display_row_range, ..
21804 } => display_row_range,
21805 DisplayDiffHunk::Folded { .. } => unreachable!(),
21806 })
21807 .collect::<Vec<_>>();
21808 assert_eq!(
21809 hunks,
21810 [
21811 DisplayRow(2)..DisplayRow(4),
21812 DisplayRow(7)..DisplayRow(9),
21813 DisplayRow(12)..DisplayRow(14),
21814 ]
21815 );
21816}
21817
21818#[gpui::test]
21819async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21820 init_test(cx, |_| {});
21821
21822 let mut cx = EditorTestContext::new(cx).await;
21823 cx.set_head_text(indoc! { "
21824 one
21825 two
21826 three
21827 four
21828 five
21829 "
21830 });
21831 cx.set_index_text(indoc! { "
21832 one
21833 two
21834 three
21835 four
21836 five
21837 "
21838 });
21839 cx.set_state(indoc! {"
21840 one
21841 TWO
21842 ˇTHREE
21843 FOUR
21844 five
21845 "});
21846 cx.run_until_parked();
21847 cx.update_editor(|editor, window, cx| {
21848 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21849 });
21850 cx.run_until_parked();
21851 cx.assert_index_text(Some(indoc! {"
21852 one
21853 TWO
21854 THREE
21855 FOUR
21856 five
21857 "}));
21858 cx.set_state(indoc! { "
21859 one
21860 TWO
21861 ˇTHREE-HUNDRED
21862 FOUR
21863 five
21864 "});
21865 cx.run_until_parked();
21866 cx.update_editor(|editor, window, cx| {
21867 let snapshot = editor.snapshot(window, cx);
21868 let hunks = editor
21869 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21870 .collect::<Vec<_>>();
21871 assert_eq!(hunks.len(), 1);
21872 assert_eq!(
21873 hunks[0].status(),
21874 DiffHunkStatus {
21875 kind: DiffHunkStatusKind::Modified,
21876 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21877 }
21878 );
21879
21880 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21881 });
21882 cx.run_until_parked();
21883 cx.assert_index_text(Some(indoc! {"
21884 one
21885 TWO
21886 THREE-HUNDRED
21887 FOUR
21888 five
21889 "}));
21890}
21891
21892#[gpui::test]
21893fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21894 init_test(cx, |_| {});
21895
21896 let editor = cx.add_window(|window, cx| {
21897 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21898 build_editor(buffer, window, cx)
21899 });
21900
21901 let render_args = Arc::new(Mutex::new(None));
21902 let snapshot = editor
21903 .update(cx, |editor, window, cx| {
21904 let snapshot = editor.buffer().read(cx).snapshot(cx);
21905 let range =
21906 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21907
21908 struct RenderArgs {
21909 row: MultiBufferRow,
21910 folded: bool,
21911 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21912 }
21913
21914 let crease = Crease::inline(
21915 range,
21916 FoldPlaceholder::test(),
21917 {
21918 let toggle_callback = render_args.clone();
21919 move |row, folded, callback, _window, _cx| {
21920 *toggle_callback.lock() = Some(RenderArgs {
21921 row,
21922 folded,
21923 callback,
21924 });
21925 div()
21926 }
21927 },
21928 |_row, _folded, _window, _cx| div(),
21929 );
21930
21931 editor.insert_creases(Some(crease), cx);
21932 let snapshot = editor.snapshot(window, cx);
21933 let _div =
21934 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21935 snapshot
21936 })
21937 .unwrap();
21938
21939 let render_args = render_args.lock().take().unwrap();
21940 assert_eq!(render_args.row, MultiBufferRow(1));
21941 assert!(!render_args.folded);
21942 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21943
21944 cx.update_window(*editor, |_, window, cx| {
21945 (render_args.callback)(true, window, cx)
21946 })
21947 .unwrap();
21948 let snapshot = editor
21949 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21950 .unwrap();
21951 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21952
21953 cx.update_window(*editor, |_, window, cx| {
21954 (render_args.callback)(false, window, cx)
21955 })
21956 .unwrap();
21957 let snapshot = editor
21958 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21959 .unwrap();
21960 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21961}
21962
21963#[gpui::test]
21964async fn test_input_text(cx: &mut TestAppContext) {
21965 init_test(cx, |_| {});
21966 let mut cx = EditorTestContext::new(cx).await;
21967
21968 cx.set_state(
21969 &r#"ˇone
21970 two
21971
21972 three
21973 fourˇ
21974 five
21975
21976 siˇx"#
21977 .unindent(),
21978 );
21979
21980 cx.dispatch_action(HandleInput(String::new()));
21981 cx.assert_editor_state(
21982 &r#"ˇone
21983 two
21984
21985 three
21986 fourˇ
21987 five
21988
21989 siˇx"#
21990 .unindent(),
21991 );
21992
21993 cx.dispatch_action(HandleInput("AAAA".to_string()));
21994 cx.assert_editor_state(
21995 &r#"AAAAˇone
21996 two
21997
21998 three
21999 fourAAAAˇ
22000 five
22001
22002 siAAAAˇx"#
22003 .unindent(),
22004 );
22005}
22006
22007#[gpui::test]
22008async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22009 init_test(cx, |_| {});
22010
22011 let mut cx = EditorTestContext::new(cx).await;
22012 cx.set_state(
22013 r#"let foo = 1;
22014let foo = 2;
22015let foo = 3;
22016let fooˇ = 4;
22017let foo = 5;
22018let foo = 6;
22019let foo = 7;
22020let foo = 8;
22021let foo = 9;
22022let foo = 10;
22023let foo = 11;
22024let foo = 12;
22025let foo = 13;
22026let foo = 14;
22027let foo = 15;"#,
22028 );
22029
22030 cx.update_editor(|e, window, cx| {
22031 assert_eq!(
22032 e.next_scroll_position,
22033 NextScrollCursorCenterTopBottom::Center,
22034 "Default next scroll direction is center",
22035 );
22036
22037 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22038 assert_eq!(
22039 e.next_scroll_position,
22040 NextScrollCursorCenterTopBottom::Top,
22041 "After center, next scroll direction should be top",
22042 );
22043
22044 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22045 assert_eq!(
22046 e.next_scroll_position,
22047 NextScrollCursorCenterTopBottom::Bottom,
22048 "After top, next scroll direction should be bottom",
22049 );
22050
22051 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22052 assert_eq!(
22053 e.next_scroll_position,
22054 NextScrollCursorCenterTopBottom::Center,
22055 "After bottom, scrolling should start over",
22056 );
22057
22058 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22059 assert_eq!(
22060 e.next_scroll_position,
22061 NextScrollCursorCenterTopBottom::Top,
22062 "Scrolling continues if retriggered fast enough"
22063 );
22064 });
22065
22066 cx.executor()
22067 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22068 cx.executor().run_until_parked();
22069 cx.update_editor(|e, _, _| {
22070 assert_eq!(
22071 e.next_scroll_position,
22072 NextScrollCursorCenterTopBottom::Center,
22073 "If scrolling is not triggered fast enough, it should reset"
22074 );
22075 });
22076}
22077
22078#[gpui::test]
22079async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22080 init_test(cx, |_| {});
22081 let mut cx = EditorLspTestContext::new_rust(
22082 lsp::ServerCapabilities {
22083 definition_provider: Some(lsp::OneOf::Left(true)),
22084 references_provider: Some(lsp::OneOf::Left(true)),
22085 ..lsp::ServerCapabilities::default()
22086 },
22087 cx,
22088 )
22089 .await;
22090
22091 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22092 let go_to_definition = cx
22093 .lsp
22094 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22095 move |params, _| async move {
22096 if empty_go_to_definition {
22097 Ok(None)
22098 } else {
22099 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22100 uri: params.text_document_position_params.text_document.uri,
22101 range: lsp::Range::new(
22102 lsp::Position::new(4, 3),
22103 lsp::Position::new(4, 6),
22104 ),
22105 })))
22106 }
22107 },
22108 );
22109 let references = cx
22110 .lsp
22111 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22112 Ok(Some(vec![lsp::Location {
22113 uri: params.text_document_position.text_document.uri,
22114 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22115 }]))
22116 });
22117 (go_to_definition, references)
22118 };
22119
22120 cx.set_state(
22121 &r#"fn one() {
22122 let mut a = ˇtwo();
22123 }
22124
22125 fn two() {}"#
22126 .unindent(),
22127 );
22128 set_up_lsp_handlers(false, &mut cx);
22129 let navigated = cx
22130 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22131 .await
22132 .expect("Failed to navigate to definition");
22133 assert_eq!(
22134 navigated,
22135 Navigated::Yes,
22136 "Should have navigated to definition from the GetDefinition response"
22137 );
22138 cx.assert_editor_state(
22139 &r#"fn one() {
22140 let mut a = two();
22141 }
22142
22143 fn «twoˇ»() {}"#
22144 .unindent(),
22145 );
22146
22147 let editors = cx.update_workspace(|workspace, _, cx| {
22148 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22149 });
22150 cx.update_editor(|_, _, test_editor_cx| {
22151 assert_eq!(
22152 editors.len(),
22153 1,
22154 "Initially, only one, test, editor should be open in the workspace"
22155 );
22156 assert_eq!(
22157 test_editor_cx.entity(),
22158 editors.last().expect("Asserted len is 1").clone()
22159 );
22160 });
22161
22162 set_up_lsp_handlers(true, &mut cx);
22163 let navigated = cx
22164 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22165 .await
22166 .expect("Failed to navigate to lookup references");
22167 assert_eq!(
22168 navigated,
22169 Navigated::Yes,
22170 "Should have navigated to references as a fallback after empty GoToDefinition response"
22171 );
22172 // We should not change the selections in the existing file,
22173 // if opening another milti buffer with the references
22174 cx.assert_editor_state(
22175 &r#"fn one() {
22176 let mut a = two();
22177 }
22178
22179 fn «twoˇ»() {}"#
22180 .unindent(),
22181 );
22182 let editors = cx.update_workspace(|workspace, _, cx| {
22183 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22184 });
22185 cx.update_editor(|_, _, test_editor_cx| {
22186 assert_eq!(
22187 editors.len(),
22188 2,
22189 "After falling back to references search, we open a new editor with the results"
22190 );
22191 let references_fallback_text = editors
22192 .into_iter()
22193 .find(|new_editor| *new_editor != test_editor_cx.entity())
22194 .expect("Should have one non-test editor now")
22195 .read(test_editor_cx)
22196 .text(test_editor_cx);
22197 assert_eq!(
22198 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22199 "Should use the range from the references response and not the GoToDefinition one"
22200 );
22201 });
22202}
22203
22204#[gpui::test]
22205async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22206 init_test(cx, |_| {});
22207 cx.update(|cx| {
22208 let mut editor_settings = EditorSettings::get_global(cx).clone();
22209 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22210 EditorSettings::override_global(editor_settings, cx);
22211 });
22212 let mut cx = EditorLspTestContext::new_rust(
22213 lsp::ServerCapabilities {
22214 definition_provider: Some(lsp::OneOf::Left(true)),
22215 references_provider: Some(lsp::OneOf::Left(true)),
22216 ..lsp::ServerCapabilities::default()
22217 },
22218 cx,
22219 )
22220 .await;
22221 let original_state = r#"fn one() {
22222 let mut a = ˇtwo();
22223 }
22224
22225 fn two() {}"#
22226 .unindent();
22227 cx.set_state(&original_state);
22228
22229 let mut go_to_definition = cx
22230 .lsp
22231 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22232 move |_, _| async move { Ok(None) },
22233 );
22234 let _references = cx
22235 .lsp
22236 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22237 panic!("Should not call for references with no go to definition fallback")
22238 });
22239
22240 let navigated = cx
22241 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22242 .await
22243 .expect("Failed to navigate to lookup references");
22244 go_to_definition
22245 .next()
22246 .await
22247 .expect("Should have called the go_to_definition handler");
22248
22249 assert_eq!(
22250 navigated,
22251 Navigated::No,
22252 "Should have navigated to references as a fallback after empty GoToDefinition response"
22253 );
22254 cx.assert_editor_state(&original_state);
22255 let editors = cx.update_workspace(|workspace, _, cx| {
22256 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22257 });
22258 cx.update_editor(|_, _, _| {
22259 assert_eq!(
22260 editors.len(),
22261 1,
22262 "After unsuccessful fallback, no other editor should have been opened"
22263 );
22264 });
22265}
22266
22267#[gpui::test]
22268async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22269 init_test(cx, |_| {});
22270 let mut cx = EditorLspTestContext::new_rust(
22271 lsp::ServerCapabilities {
22272 references_provider: Some(lsp::OneOf::Left(true)),
22273 ..lsp::ServerCapabilities::default()
22274 },
22275 cx,
22276 )
22277 .await;
22278
22279 cx.set_state(
22280 &r#"
22281 fn one() {
22282 let mut a = two();
22283 }
22284
22285 fn ˇtwo() {}"#
22286 .unindent(),
22287 );
22288 cx.lsp
22289 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22290 Ok(Some(vec![
22291 lsp::Location {
22292 uri: params.text_document_position.text_document.uri.clone(),
22293 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22294 },
22295 lsp::Location {
22296 uri: params.text_document_position.text_document.uri,
22297 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22298 },
22299 ]))
22300 });
22301 let navigated = cx
22302 .update_editor(|editor, window, cx| {
22303 editor.find_all_references(&FindAllReferences, window, cx)
22304 })
22305 .unwrap()
22306 .await
22307 .expect("Failed to navigate to references");
22308 assert_eq!(
22309 navigated,
22310 Navigated::Yes,
22311 "Should have navigated to references from the FindAllReferences response"
22312 );
22313 cx.assert_editor_state(
22314 &r#"fn one() {
22315 let mut a = two();
22316 }
22317
22318 fn ˇtwo() {}"#
22319 .unindent(),
22320 );
22321
22322 let editors = cx.update_workspace(|workspace, _, cx| {
22323 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22324 });
22325 cx.update_editor(|_, _, _| {
22326 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22327 });
22328
22329 cx.set_state(
22330 &r#"fn one() {
22331 let mut a = ˇtwo();
22332 }
22333
22334 fn two() {}"#
22335 .unindent(),
22336 );
22337 let navigated = cx
22338 .update_editor(|editor, window, cx| {
22339 editor.find_all_references(&FindAllReferences, window, cx)
22340 })
22341 .unwrap()
22342 .await
22343 .expect("Failed to navigate to references");
22344 assert_eq!(
22345 navigated,
22346 Navigated::Yes,
22347 "Should have navigated to references from the FindAllReferences response"
22348 );
22349 cx.assert_editor_state(
22350 &r#"fn one() {
22351 let mut a = ˇtwo();
22352 }
22353
22354 fn two() {}"#
22355 .unindent(),
22356 );
22357 let editors = cx.update_workspace(|workspace, _, cx| {
22358 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22359 });
22360 cx.update_editor(|_, _, _| {
22361 assert_eq!(
22362 editors.len(),
22363 2,
22364 "should have re-used the previous multibuffer"
22365 );
22366 });
22367
22368 cx.set_state(
22369 &r#"fn one() {
22370 let mut a = ˇtwo();
22371 }
22372 fn three() {}
22373 fn two() {}"#
22374 .unindent(),
22375 );
22376 cx.lsp
22377 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22378 Ok(Some(vec![
22379 lsp::Location {
22380 uri: params.text_document_position.text_document.uri.clone(),
22381 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22382 },
22383 lsp::Location {
22384 uri: params.text_document_position.text_document.uri,
22385 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22386 },
22387 ]))
22388 });
22389 let navigated = cx
22390 .update_editor(|editor, window, cx| {
22391 editor.find_all_references(&FindAllReferences, window, cx)
22392 })
22393 .unwrap()
22394 .await
22395 .expect("Failed to navigate to references");
22396 assert_eq!(
22397 navigated,
22398 Navigated::Yes,
22399 "Should have navigated to references from the FindAllReferences response"
22400 );
22401 cx.assert_editor_state(
22402 &r#"fn one() {
22403 let mut a = ˇtwo();
22404 }
22405 fn three() {}
22406 fn two() {}"#
22407 .unindent(),
22408 );
22409 let editors = cx.update_workspace(|workspace, _, cx| {
22410 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22411 });
22412 cx.update_editor(|_, _, _| {
22413 assert_eq!(
22414 editors.len(),
22415 3,
22416 "should have used a new multibuffer as offsets changed"
22417 );
22418 });
22419}
22420#[gpui::test]
22421async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22422 init_test(cx, |_| {});
22423
22424 let language = Arc::new(Language::new(
22425 LanguageConfig::default(),
22426 Some(tree_sitter_rust::LANGUAGE.into()),
22427 ));
22428
22429 let text = r#"
22430 #[cfg(test)]
22431 mod tests() {
22432 #[test]
22433 fn runnable_1() {
22434 let a = 1;
22435 }
22436
22437 #[test]
22438 fn runnable_2() {
22439 let a = 1;
22440 let b = 2;
22441 }
22442 }
22443 "#
22444 .unindent();
22445
22446 let fs = FakeFs::new(cx.executor());
22447 fs.insert_file("/file.rs", Default::default()).await;
22448
22449 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22450 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22451 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22452 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22453 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22454
22455 let editor = cx.new_window_entity(|window, cx| {
22456 Editor::new(
22457 EditorMode::full(),
22458 multi_buffer,
22459 Some(project.clone()),
22460 window,
22461 cx,
22462 )
22463 });
22464
22465 editor.update_in(cx, |editor, window, cx| {
22466 let snapshot = editor.buffer().read(cx).snapshot(cx);
22467 editor.tasks.insert(
22468 (buffer.read(cx).remote_id(), 3),
22469 RunnableTasks {
22470 templates: vec![],
22471 offset: snapshot.anchor_before(MultiBufferOffset(43)),
22472 column: 0,
22473 extra_variables: HashMap::default(),
22474 context_range: BufferOffset(43)..BufferOffset(85),
22475 },
22476 );
22477 editor.tasks.insert(
22478 (buffer.read(cx).remote_id(), 8),
22479 RunnableTasks {
22480 templates: vec![],
22481 offset: snapshot.anchor_before(MultiBufferOffset(86)),
22482 column: 0,
22483 extra_variables: HashMap::default(),
22484 context_range: BufferOffset(86)..BufferOffset(191),
22485 },
22486 );
22487
22488 // Test finding task when cursor is inside function body
22489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22490 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22491 });
22492 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22493 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22494
22495 // Test finding task when cursor is on function name
22496 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22497 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22498 });
22499 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22500 assert_eq!(row, 8, "Should find task when cursor is on function name");
22501 });
22502}
22503
22504#[gpui::test]
22505async fn test_folding_buffers(cx: &mut TestAppContext) {
22506 init_test(cx, |_| {});
22507
22508 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22509 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22510 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22511
22512 let fs = FakeFs::new(cx.executor());
22513 fs.insert_tree(
22514 path!("/a"),
22515 json!({
22516 "first.rs": sample_text_1,
22517 "second.rs": sample_text_2,
22518 "third.rs": sample_text_3,
22519 }),
22520 )
22521 .await;
22522 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22523 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22524 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22525 let worktree = project.update(cx, |project, cx| {
22526 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22527 assert_eq!(worktrees.len(), 1);
22528 worktrees.pop().unwrap()
22529 });
22530 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22531
22532 let buffer_1 = project
22533 .update(cx, |project, cx| {
22534 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22535 })
22536 .await
22537 .unwrap();
22538 let buffer_2 = project
22539 .update(cx, |project, cx| {
22540 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22541 })
22542 .await
22543 .unwrap();
22544 let buffer_3 = project
22545 .update(cx, |project, cx| {
22546 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22547 })
22548 .await
22549 .unwrap();
22550
22551 let multi_buffer = cx.new(|cx| {
22552 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22553 multi_buffer.push_excerpts(
22554 buffer_1.clone(),
22555 [
22556 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22557 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22558 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22559 ],
22560 cx,
22561 );
22562 multi_buffer.push_excerpts(
22563 buffer_2.clone(),
22564 [
22565 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22566 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22567 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22568 ],
22569 cx,
22570 );
22571 multi_buffer.push_excerpts(
22572 buffer_3.clone(),
22573 [
22574 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22575 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22576 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22577 ],
22578 cx,
22579 );
22580 multi_buffer
22581 });
22582 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22583 Editor::new(
22584 EditorMode::full(),
22585 multi_buffer.clone(),
22586 Some(project.clone()),
22587 window,
22588 cx,
22589 )
22590 });
22591
22592 assert_eq!(
22593 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22594 "\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",
22595 );
22596
22597 multi_buffer_editor.update(cx, |editor, cx| {
22598 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22599 });
22600 assert_eq!(
22601 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22602 "\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",
22603 "After folding the first buffer, its text should not be displayed"
22604 );
22605
22606 multi_buffer_editor.update(cx, |editor, cx| {
22607 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22608 });
22609 assert_eq!(
22610 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22611 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22612 "After folding the second buffer, its text should not be displayed"
22613 );
22614
22615 multi_buffer_editor.update(cx, |editor, cx| {
22616 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22617 });
22618 assert_eq!(
22619 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22620 "\n\n\n\n\n",
22621 "After folding the third buffer, its text should not be displayed"
22622 );
22623
22624 // Emulate selection inside the fold logic, that should work
22625 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22626 editor
22627 .snapshot(window, cx)
22628 .next_line_boundary(Point::new(0, 4));
22629 });
22630
22631 multi_buffer_editor.update(cx, |editor, cx| {
22632 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22633 });
22634 assert_eq!(
22635 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22636 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22637 "After unfolding the second buffer, its text should be displayed"
22638 );
22639
22640 // Typing inside of buffer 1 causes that buffer to be unfolded.
22641 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22642 assert_eq!(
22643 multi_buffer
22644 .read(cx)
22645 .snapshot(cx)
22646 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22647 .collect::<String>(),
22648 "bbbb"
22649 );
22650 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22651 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22652 });
22653 editor.handle_input("B", window, cx);
22654 });
22655
22656 assert_eq!(
22657 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22658 "\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",
22659 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22660 );
22661
22662 multi_buffer_editor.update(cx, |editor, cx| {
22663 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22664 });
22665 assert_eq!(
22666 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22667 "\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",
22668 "After unfolding the all buffers, all original text should be displayed"
22669 );
22670}
22671
22672#[gpui::test]
22673async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22674 init_test(cx, |_| {});
22675
22676 let sample_text_1 = "1111\n2222\n3333".to_string();
22677 let sample_text_2 = "4444\n5555\n6666".to_string();
22678 let sample_text_3 = "7777\n8888\n9999".to_string();
22679
22680 let fs = FakeFs::new(cx.executor());
22681 fs.insert_tree(
22682 path!("/a"),
22683 json!({
22684 "first.rs": sample_text_1,
22685 "second.rs": sample_text_2,
22686 "third.rs": sample_text_3,
22687 }),
22688 )
22689 .await;
22690 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22691 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22692 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22693 let worktree = project.update(cx, |project, cx| {
22694 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22695 assert_eq!(worktrees.len(), 1);
22696 worktrees.pop().unwrap()
22697 });
22698 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22699
22700 let buffer_1 = project
22701 .update(cx, |project, cx| {
22702 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22703 })
22704 .await
22705 .unwrap();
22706 let buffer_2 = project
22707 .update(cx, |project, cx| {
22708 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22709 })
22710 .await
22711 .unwrap();
22712 let buffer_3 = project
22713 .update(cx, |project, cx| {
22714 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22715 })
22716 .await
22717 .unwrap();
22718
22719 let multi_buffer = cx.new(|cx| {
22720 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22721 multi_buffer.push_excerpts(
22722 buffer_1.clone(),
22723 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22724 cx,
22725 );
22726 multi_buffer.push_excerpts(
22727 buffer_2.clone(),
22728 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22729 cx,
22730 );
22731 multi_buffer.push_excerpts(
22732 buffer_3.clone(),
22733 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22734 cx,
22735 );
22736 multi_buffer
22737 });
22738
22739 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22740 Editor::new(
22741 EditorMode::full(),
22742 multi_buffer,
22743 Some(project.clone()),
22744 window,
22745 cx,
22746 )
22747 });
22748
22749 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22750 assert_eq!(
22751 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22752 full_text,
22753 );
22754
22755 multi_buffer_editor.update(cx, |editor, cx| {
22756 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22757 });
22758 assert_eq!(
22759 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22760 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22761 "After folding the first buffer, its text should not be displayed"
22762 );
22763
22764 multi_buffer_editor.update(cx, |editor, cx| {
22765 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22766 });
22767
22768 assert_eq!(
22769 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22770 "\n\n\n\n\n\n7777\n8888\n9999",
22771 "After folding the second buffer, its text should not be displayed"
22772 );
22773
22774 multi_buffer_editor.update(cx, |editor, cx| {
22775 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22776 });
22777 assert_eq!(
22778 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22779 "\n\n\n\n\n",
22780 "After folding the third buffer, its text should not be displayed"
22781 );
22782
22783 multi_buffer_editor.update(cx, |editor, cx| {
22784 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22785 });
22786 assert_eq!(
22787 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22788 "\n\n\n\n4444\n5555\n6666\n\n",
22789 "After unfolding the second buffer, its text should be displayed"
22790 );
22791
22792 multi_buffer_editor.update(cx, |editor, cx| {
22793 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22794 });
22795 assert_eq!(
22796 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22797 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22798 "After unfolding the first buffer, its text should be displayed"
22799 );
22800
22801 multi_buffer_editor.update(cx, |editor, cx| {
22802 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22803 });
22804 assert_eq!(
22805 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22806 full_text,
22807 "After unfolding all buffers, all original text should be displayed"
22808 );
22809}
22810
22811#[gpui::test]
22812async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22813 init_test(cx, |_| {});
22814
22815 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22816
22817 let fs = FakeFs::new(cx.executor());
22818 fs.insert_tree(
22819 path!("/a"),
22820 json!({
22821 "main.rs": sample_text,
22822 }),
22823 )
22824 .await;
22825 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22826 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22827 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22828 let worktree = project.update(cx, |project, cx| {
22829 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22830 assert_eq!(worktrees.len(), 1);
22831 worktrees.pop().unwrap()
22832 });
22833 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22834
22835 let buffer_1 = project
22836 .update(cx, |project, cx| {
22837 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22838 })
22839 .await
22840 .unwrap();
22841
22842 let multi_buffer = cx.new(|cx| {
22843 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22844 multi_buffer.push_excerpts(
22845 buffer_1.clone(),
22846 [ExcerptRange::new(
22847 Point::new(0, 0)
22848 ..Point::new(
22849 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22850 0,
22851 ),
22852 )],
22853 cx,
22854 );
22855 multi_buffer
22856 });
22857 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22858 Editor::new(
22859 EditorMode::full(),
22860 multi_buffer,
22861 Some(project.clone()),
22862 window,
22863 cx,
22864 )
22865 });
22866
22867 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22868 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22869 enum TestHighlight {}
22870 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22871 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22872 editor.highlight_text::<TestHighlight>(
22873 vec![highlight_range.clone()],
22874 HighlightStyle::color(Hsla::green()),
22875 cx,
22876 );
22877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22878 s.select_ranges(Some(highlight_range))
22879 });
22880 });
22881
22882 let full_text = format!("\n\n{sample_text}");
22883 assert_eq!(
22884 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22885 full_text,
22886 );
22887}
22888
22889#[gpui::test]
22890async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22891 init_test(cx, |_| {});
22892 cx.update(|cx| {
22893 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22894 "keymaps/default-linux.json",
22895 cx,
22896 )
22897 .unwrap();
22898 cx.bind_keys(default_key_bindings);
22899 });
22900
22901 let (editor, cx) = cx.add_window_view(|window, cx| {
22902 let multi_buffer = MultiBuffer::build_multi(
22903 [
22904 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22905 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22906 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22907 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22908 ],
22909 cx,
22910 );
22911 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22912
22913 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22914 // fold all but the second buffer, so that we test navigating between two
22915 // adjacent folded buffers, as well as folded buffers at the start and
22916 // end the multibuffer
22917 editor.fold_buffer(buffer_ids[0], cx);
22918 editor.fold_buffer(buffer_ids[2], cx);
22919 editor.fold_buffer(buffer_ids[3], cx);
22920
22921 editor
22922 });
22923 cx.simulate_resize(size(px(1000.), px(1000.)));
22924
22925 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22926 cx.assert_excerpts_with_selections(indoc! {"
22927 [EXCERPT]
22928 ˇ[FOLDED]
22929 [EXCERPT]
22930 a1
22931 b1
22932 [EXCERPT]
22933 [FOLDED]
22934 [EXCERPT]
22935 [FOLDED]
22936 "
22937 });
22938 cx.simulate_keystroke("down");
22939 cx.assert_excerpts_with_selections(indoc! {"
22940 [EXCERPT]
22941 [FOLDED]
22942 [EXCERPT]
22943 ˇa1
22944 b1
22945 [EXCERPT]
22946 [FOLDED]
22947 [EXCERPT]
22948 [FOLDED]
22949 "
22950 });
22951 cx.simulate_keystroke("down");
22952 cx.assert_excerpts_with_selections(indoc! {"
22953 [EXCERPT]
22954 [FOLDED]
22955 [EXCERPT]
22956 a1
22957 ˇb1
22958 [EXCERPT]
22959 [FOLDED]
22960 [EXCERPT]
22961 [FOLDED]
22962 "
22963 });
22964 cx.simulate_keystroke("down");
22965 cx.assert_excerpts_with_selections(indoc! {"
22966 [EXCERPT]
22967 [FOLDED]
22968 [EXCERPT]
22969 a1
22970 b1
22971 ˇ[EXCERPT]
22972 [FOLDED]
22973 [EXCERPT]
22974 [FOLDED]
22975 "
22976 });
22977 cx.simulate_keystroke("down");
22978 cx.assert_excerpts_with_selections(indoc! {"
22979 [EXCERPT]
22980 [FOLDED]
22981 [EXCERPT]
22982 a1
22983 b1
22984 [EXCERPT]
22985 ˇ[FOLDED]
22986 [EXCERPT]
22987 [FOLDED]
22988 "
22989 });
22990 for _ in 0..5 {
22991 cx.simulate_keystroke("down");
22992 cx.assert_excerpts_with_selections(indoc! {"
22993 [EXCERPT]
22994 [FOLDED]
22995 [EXCERPT]
22996 a1
22997 b1
22998 [EXCERPT]
22999 [FOLDED]
23000 [EXCERPT]
23001 ˇ[FOLDED]
23002 "
23003 });
23004 }
23005
23006 cx.simulate_keystroke("up");
23007 cx.assert_excerpts_with_selections(indoc! {"
23008 [EXCERPT]
23009 [FOLDED]
23010 [EXCERPT]
23011 a1
23012 b1
23013 [EXCERPT]
23014 ˇ[FOLDED]
23015 [EXCERPT]
23016 [FOLDED]
23017 "
23018 });
23019 cx.simulate_keystroke("up");
23020 cx.assert_excerpts_with_selections(indoc! {"
23021 [EXCERPT]
23022 [FOLDED]
23023 [EXCERPT]
23024 a1
23025 b1
23026 ˇ[EXCERPT]
23027 [FOLDED]
23028 [EXCERPT]
23029 [FOLDED]
23030 "
23031 });
23032 cx.simulate_keystroke("up");
23033 cx.assert_excerpts_with_selections(indoc! {"
23034 [EXCERPT]
23035 [FOLDED]
23036 [EXCERPT]
23037 a1
23038 ˇb1
23039 [EXCERPT]
23040 [FOLDED]
23041 [EXCERPT]
23042 [FOLDED]
23043 "
23044 });
23045 cx.simulate_keystroke("up");
23046 cx.assert_excerpts_with_selections(indoc! {"
23047 [EXCERPT]
23048 [FOLDED]
23049 [EXCERPT]
23050 ˇa1
23051 b1
23052 [EXCERPT]
23053 [FOLDED]
23054 [EXCERPT]
23055 [FOLDED]
23056 "
23057 });
23058 for _ in 0..5 {
23059 cx.simulate_keystroke("up");
23060 cx.assert_excerpts_with_selections(indoc! {"
23061 [EXCERPT]
23062 ˇ[FOLDED]
23063 [EXCERPT]
23064 a1
23065 b1
23066 [EXCERPT]
23067 [FOLDED]
23068 [EXCERPT]
23069 [FOLDED]
23070 "
23071 });
23072 }
23073}
23074
23075#[gpui::test]
23076async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23077 init_test(cx, |_| {});
23078
23079 // Simple insertion
23080 assert_highlighted_edits(
23081 "Hello, world!",
23082 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23083 true,
23084 cx,
23085 |highlighted_edits, cx| {
23086 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23087 assert_eq!(highlighted_edits.highlights.len(), 1);
23088 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23089 assert_eq!(
23090 highlighted_edits.highlights[0].1.background_color,
23091 Some(cx.theme().status().created_background)
23092 );
23093 },
23094 )
23095 .await;
23096
23097 // Replacement
23098 assert_highlighted_edits(
23099 "This is a test.",
23100 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23101 false,
23102 cx,
23103 |highlighted_edits, cx| {
23104 assert_eq!(highlighted_edits.text, "That is a test.");
23105 assert_eq!(highlighted_edits.highlights.len(), 1);
23106 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23107 assert_eq!(
23108 highlighted_edits.highlights[0].1.background_color,
23109 Some(cx.theme().status().created_background)
23110 );
23111 },
23112 )
23113 .await;
23114
23115 // Multiple edits
23116 assert_highlighted_edits(
23117 "Hello, world!",
23118 vec![
23119 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23120 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23121 ],
23122 false,
23123 cx,
23124 |highlighted_edits, cx| {
23125 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23126 assert_eq!(highlighted_edits.highlights.len(), 2);
23127 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23128 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23129 assert_eq!(
23130 highlighted_edits.highlights[0].1.background_color,
23131 Some(cx.theme().status().created_background)
23132 );
23133 assert_eq!(
23134 highlighted_edits.highlights[1].1.background_color,
23135 Some(cx.theme().status().created_background)
23136 );
23137 },
23138 )
23139 .await;
23140
23141 // Multiple lines with edits
23142 assert_highlighted_edits(
23143 "First line\nSecond line\nThird line\nFourth line",
23144 vec![
23145 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23146 (
23147 Point::new(2, 0)..Point::new(2, 10),
23148 "New third line".to_string(),
23149 ),
23150 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23151 ],
23152 false,
23153 cx,
23154 |highlighted_edits, cx| {
23155 assert_eq!(
23156 highlighted_edits.text,
23157 "Second modified\nNew third line\nFourth updated line"
23158 );
23159 assert_eq!(highlighted_edits.highlights.len(), 3);
23160 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23161 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23162 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23163 for highlight in &highlighted_edits.highlights {
23164 assert_eq!(
23165 highlight.1.background_color,
23166 Some(cx.theme().status().created_background)
23167 );
23168 }
23169 },
23170 )
23171 .await;
23172}
23173
23174#[gpui::test]
23175async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23176 init_test(cx, |_| {});
23177
23178 // Deletion
23179 assert_highlighted_edits(
23180 "Hello, world!",
23181 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23182 true,
23183 cx,
23184 |highlighted_edits, cx| {
23185 assert_eq!(highlighted_edits.text, "Hello, world!");
23186 assert_eq!(highlighted_edits.highlights.len(), 1);
23187 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23188 assert_eq!(
23189 highlighted_edits.highlights[0].1.background_color,
23190 Some(cx.theme().status().deleted_background)
23191 );
23192 },
23193 )
23194 .await;
23195
23196 // Insertion
23197 assert_highlighted_edits(
23198 "Hello, world!",
23199 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23200 true,
23201 cx,
23202 |highlighted_edits, cx| {
23203 assert_eq!(highlighted_edits.highlights.len(), 1);
23204 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23205 assert_eq!(
23206 highlighted_edits.highlights[0].1.background_color,
23207 Some(cx.theme().status().created_background)
23208 );
23209 },
23210 )
23211 .await;
23212}
23213
23214async fn assert_highlighted_edits(
23215 text: &str,
23216 edits: Vec<(Range<Point>, String)>,
23217 include_deletions: bool,
23218 cx: &mut TestAppContext,
23219 assertion_fn: impl Fn(HighlightedText, &App),
23220) {
23221 let window = cx.add_window(|window, cx| {
23222 let buffer = MultiBuffer::build_simple(text, cx);
23223 Editor::new(EditorMode::full(), buffer, None, window, cx)
23224 });
23225 let cx = &mut VisualTestContext::from_window(*window, cx);
23226
23227 let (buffer, snapshot) = window
23228 .update(cx, |editor, _window, cx| {
23229 (
23230 editor.buffer().clone(),
23231 editor.buffer().read(cx).snapshot(cx),
23232 )
23233 })
23234 .unwrap();
23235
23236 let edits = edits
23237 .into_iter()
23238 .map(|(range, edit)| {
23239 (
23240 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23241 edit,
23242 )
23243 })
23244 .collect::<Vec<_>>();
23245
23246 let text_anchor_edits = edits
23247 .clone()
23248 .into_iter()
23249 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23250 .collect::<Vec<_>>();
23251
23252 let edit_preview = window
23253 .update(cx, |_, _window, cx| {
23254 buffer
23255 .read(cx)
23256 .as_singleton()
23257 .unwrap()
23258 .read(cx)
23259 .preview_edits(text_anchor_edits.into(), cx)
23260 })
23261 .unwrap()
23262 .await;
23263
23264 cx.update(|_window, cx| {
23265 let highlighted_edits = edit_prediction_edit_text(
23266 snapshot.as_singleton().unwrap().2,
23267 &edits,
23268 &edit_preview,
23269 include_deletions,
23270 cx,
23271 );
23272 assertion_fn(highlighted_edits, cx)
23273 });
23274}
23275
23276#[track_caller]
23277fn assert_breakpoint(
23278 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23279 path: &Arc<Path>,
23280 expected: Vec<(u32, Breakpoint)>,
23281) {
23282 if expected.is_empty() {
23283 assert!(!breakpoints.contains_key(path), "{}", path.display());
23284 } else {
23285 let mut breakpoint = breakpoints
23286 .get(path)
23287 .unwrap()
23288 .iter()
23289 .map(|breakpoint| {
23290 (
23291 breakpoint.row,
23292 Breakpoint {
23293 message: breakpoint.message.clone(),
23294 state: breakpoint.state,
23295 condition: breakpoint.condition.clone(),
23296 hit_condition: breakpoint.hit_condition.clone(),
23297 },
23298 )
23299 })
23300 .collect::<Vec<_>>();
23301
23302 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23303
23304 assert_eq!(expected, breakpoint);
23305 }
23306}
23307
23308fn add_log_breakpoint_at_cursor(
23309 editor: &mut Editor,
23310 log_message: &str,
23311 window: &mut Window,
23312 cx: &mut Context<Editor>,
23313) {
23314 let (anchor, bp) = editor
23315 .breakpoints_at_cursors(window, cx)
23316 .first()
23317 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23318 .unwrap_or_else(|| {
23319 let snapshot = editor.snapshot(window, cx);
23320 let cursor_position: Point =
23321 editor.selections.newest(&snapshot.display_snapshot).head();
23322
23323 let breakpoint_position = snapshot
23324 .buffer_snapshot()
23325 .anchor_before(Point::new(cursor_position.row, 0));
23326
23327 (breakpoint_position, Breakpoint::new_log(log_message))
23328 });
23329
23330 editor.edit_breakpoint_at_anchor(
23331 anchor,
23332 bp,
23333 BreakpointEditAction::EditLogMessage(log_message.into()),
23334 cx,
23335 );
23336}
23337
23338#[gpui::test]
23339async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23340 init_test(cx, |_| {});
23341
23342 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23343 let fs = FakeFs::new(cx.executor());
23344 fs.insert_tree(
23345 path!("/a"),
23346 json!({
23347 "main.rs": sample_text,
23348 }),
23349 )
23350 .await;
23351 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23352 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23353 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23354
23355 let fs = FakeFs::new(cx.executor());
23356 fs.insert_tree(
23357 path!("/a"),
23358 json!({
23359 "main.rs": sample_text,
23360 }),
23361 )
23362 .await;
23363 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23364 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23365 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23366 let worktree_id = workspace
23367 .update(cx, |workspace, _window, cx| {
23368 workspace.project().update(cx, |project, cx| {
23369 project.worktrees(cx).next().unwrap().read(cx).id()
23370 })
23371 })
23372 .unwrap();
23373
23374 let buffer = project
23375 .update(cx, |project, cx| {
23376 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23377 })
23378 .await
23379 .unwrap();
23380
23381 let (editor, cx) = cx.add_window_view(|window, cx| {
23382 Editor::new(
23383 EditorMode::full(),
23384 MultiBuffer::build_from_buffer(buffer, cx),
23385 Some(project.clone()),
23386 window,
23387 cx,
23388 )
23389 });
23390
23391 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23392 let abs_path = project.read_with(cx, |project, cx| {
23393 project
23394 .absolute_path(&project_path, cx)
23395 .map(Arc::from)
23396 .unwrap()
23397 });
23398
23399 // assert we can add breakpoint on the first line
23400 editor.update_in(cx, |editor, window, cx| {
23401 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23402 editor.move_to_end(&MoveToEnd, window, cx);
23403 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23404 });
23405
23406 let breakpoints = editor.update(cx, |editor, cx| {
23407 editor
23408 .breakpoint_store()
23409 .as_ref()
23410 .unwrap()
23411 .read(cx)
23412 .all_source_breakpoints(cx)
23413 });
23414
23415 assert_eq!(1, breakpoints.len());
23416 assert_breakpoint(
23417 &breakpoints,
23418 &abs_path,
23419 vec![
23420 (0, Breakpoint::new_standard()),
23421 (3, Breakpoint::new_standard()),
23422 ],
23423 );
23424
23425 editor.update_in(cx, |editor, window, cx| {
23426 editor.move_to_beginning(&MoveToBeginning, window, cx);
23427 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23428 });
23429
23430 let breakpoints = editor.update(cx, |editor, cx| {
23431 editor
23432 .breakpoint_store()
23433 .as_ref()
23434 .unwrap()
23435 .read(cx)
23436 .all_source_breakpoints(cx)
23437 });
23438
23439 assert_eq!(1, breakpoints.len());
23440 assert_breakpoint(
23441 &breakpoints,
23442 &abs_path,
23443 vec![(3, Breakpoint::new_standard())],
23444 );
23445
23446 editor.update_in(cx, |editor, window, cx| {
23447 editor.move_to_end(&MoveToEnd, window, cx);
23448 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23449 });
23450
23451 let breakpoints = editor.update(cx, |editor, cx| {
23452 editor
23453 .breakpoint_store()
23454 .as_ref()
23455 .unwrap()
23456 .read(cx)
23457 .all_source_breakpoints(cx)
23458 });
23459
23460 assert_eq!(0, breakpoints.len());
23461 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23462}
23463
23464#[gpui::test]
23465async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23466 init_test(cx, |_| {});
23467
23468 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23469
23470 let fs = FakeFs::new(cx.executor());
23471 fs.insert_tree(
23472 path!("/a"),
23473 json!({
23474 "main.rs": sample_text,
23475 }),
23476 )
23477 .await;
23478 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23479 let (workspace, cx) =
23480 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23481
23482 let worktree_id = workspace.update(cx, |workspace, cx| {
23483 workspace.project().update(cx, |project, cx| {
23484 project.worktrees(cx).next().unwrap().read(cx).id()
23485 })
23486 });
23487
23488 let buffer = project
23489 .update(cx, |project, cx| {
23490 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23491 })
23492 .await
23493 .unwrap();
23494
23495 let (editor, cx) = cx.add_window_view(|window, cx| {
23496 Editor::new(
23497 EditorMode::full(),
23498 MultiBuffer::build_from_buffer(buffer, cx),
23499 Some(project.clone()),
23500 window,
23501 cx,
23502 )
23503 });
23504
23505 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23506 let abs_path = project.read_with(cx, |project, cx| {
23507 project
23508 .absolute_path(&project_path, cx)
23509 .map(Arc::from)
23510 .unwrap()
23511 });
23512
23513 editor.update_in(cx, |editor, window, cx| {
23514 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23515 });
23516
23517 let breakpoints = editor.update(cx, |editor, cx| {
23518 editor
23519 .breakpoint_store()
23520 .as_ref()
23521 .unwrap()
23522 .read(cx)
23523 .all_source_breakpoints(cx)
23524 });
23525
23526 assert_breakpoint(
23527 &breakpoints,
23528 &abs_path,
23529 vec![(0, Breakpoint::new_log("hello world"))],
23530 );
23531
23532 // Removing a log message from a log breakpoint should remove it
23533 editor.update_in(cx, |editor, window, cx| {
23534 add_log_breakpoint_at_cursor(editor, "", window, cx);
23535 });
23536
23537 let breakpoints = editor.update(cx, |editor, cx| {
23538 editor
23539 .breakpoint_store()
23540 .as_ref()
23541 .unwrap()
23542 .read(cx)
23543 .all_source_breakpoints(cx)
23544 });
23545
23546 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23547
23548 editor.update_in(cx, |editor, window, cx| {
23549 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23550 editor.move_to_end(&MoveToEnd, window, cx);
23551 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23552 // Not adding a log message to a standard breakpoint shouldn't remove it
23553 add_log_breakpoint_at_cursor(editor, "", window, cx);
23554 });
23555
23556 let breakpoints = editor.update(cx, |editor, cx| {
23557 editor
23558 .breakpoint_store()
23559 .as_ref()
23560 .unwrap()
23561 .read(cx)
23562 .all_source_breakpoints(cx)
23563 });
23564
23565 assert_breakpoint(
23566 &breakpoints,
23567 &abs_path,
23568 vec![
23569 (0, Breakpoint::new_standard()),
23570 (3, Breakpoint::new_standard()),
23571 ],
23572 );
23573
23574 editor.update_in(cx, |editor, window, cx| {
23575 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23576 });
23577
23578 let breakpoints = editor.update(cx, |editor, cx| {
23579 editor
23580 .breakpoint_store()
23581 .as_ref()
23582 .unwrap()
23583 .read(cx)
23584 .all_source_breakpoints(cx)
23585 });
23586
23587 assert_breakpoint(
23588 &breakpoints,
23589 &abs_path,
23590 vec![
23591 (0, Breakpoint::new_standard()),
23592 (3, Breakpoint::new_log("hello world")),
23593 ],
23594 );
23595
23596 editor.update_in(cx, |editor, window, cx| {
23597 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23598 });
23599
23600 let breakpoints = editor.update(cx, |editor, cx| {
23601 editor
23602 .breakpoint_store()
23603 .as_ref()
23604 .unwrap()
23605 .read(cx)
23606 .all_source_breakpoints(cx)
23607 });
23608
23609 assert_breakpoint(
23610 &breakpoints,
23611 &abs_path,
23612 vec![
23613 (0, Breakpoint::new_standard()),
23614 (3, Breakpoint::new_log("hello Earth!!")),
23615 ],
23616 );
23617}
23618
23619/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23620/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23621/// or when breakpoints were placed out of order. This tests for a regression too
23622#[gpui::test]
23623async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23624 init_test(cx, |_| {});
23625
23626 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23627 let fs = FakeFs::new(cx.executor());
23628 fs.insert_tree(
23629 path!("/a"),
23630 json!({
23631 "main.rs": sample_text,
23632 }),
23633 )
23634 .await;
23635 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23636 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23637 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23638
23639 let fs = FakeFs::new(cx.executor());
23640 fs.insert_tree(
23641 path!("/a"),
23642 json!({
23643 "main.rs": sample_text,
23644 }),
23645 )
23646 .await;
23647 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23648 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23649 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23650 let worktree_id = workspace
23651 .update(cx, |workspace, _window, cx| {
23652 workspace.project().update(cx, |project, cx| {
23653 project.worktrees(cx).next().unwrap().read(cx).id()
23654 })
23655 })
23656 .unwrap();
23657
23658 let buffer = project
23659 .update(cx, |project, cx| {
23660 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23661 })
23662 .await
23663 .unwrap();
23664
23665 let (editor, cx) = cx.add_window_view(|window, cx| {
23666 Editor::new(
23667 EditorMode::full(),
23668 MultiBuffer::build_from_buffer(buffer, cx),
23669 Some(project.clone()),
23670 window,
23671 cx,
23672 )
23673 });
23674
23675 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23676 let abs_path = project.read_with(cx, |project, cx| {
23677 project
23678 .absolute_path(&project_path, cx)
23679 .map(Arc::from)
23680 .unwrap()
23681 });
23682
23683 // assert we can add breakpoint on the first line
23684 editor.update_in(cx, |editor, window, cx| {
23685 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23686 editor.move_to_end(&MoveToEnd, window, cx);
23687 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23688 editor.move_up(&MoveUp, window, cx);
23689 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23690 });
23691
23692 let breakpoints = editor.update(cx, |editor, cx| {
23693 editor
23694 .breakpoint_store()
23695 .as_ref()
23696 .unwrap()
23697 .read(cx)
23698 .all_source_breakpoints(cx)
23699 });
23700
23701 assert_eq!(1, breakpoints.len());
23702 assert_breakpoint(
23703 &breakpoints,
23704 &abs_path,
23705 vec![
23706 (0, Breakpoint::new_standard()),
23707 (2, Breakpoint::new_standard()),
23708 (3, Breakpoint::new_standard()),
23709 ],
23710 );
23711
23712 editor.update_in(cx, |editor, window, cx| {
23713 editor.move_to_beginning(&MoveToBeginning, window, cx);
23714 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23715 editor.move_to_end(&MoveToEnd, window, cx);
23716 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23717 // Disabling a breakpoint that doesn't exist should do nothing
23718 editor.move_up(&MoveUp, window, cx);
23719 editor.move_up(&MoveUp, window, cx);
23720 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23721 });
23722
23723 let breakpoints = editor.update(cx, |editor, cx| {
23724 editor
23725 .breakpoint_store()
23726 .as_ref()
23727 .unwrap()
23728 .read(cx)
23729 .all_source_breakpoints(cx)
23730 });
23731
23732 let disable_breakpoint = {
23733 let mut bp = Breakpoint::new_standard();
23734 bp.state = BreakpointState::Disabled;
23735 bp
23736 };
23737
23738 assert_eq!(1, breakpoints.len());
23739 assert_breakpoint(
23740 &breakpoints,
23741 &abs_path,
23742 vec![
23743 (0, disable_breakpoint.clone()),
23744 (2, Breakpoint::new_standard()),
23745 (3, disable_breakpoint.clone()),
23746 ],
23747 );
23748
23749 editor.update_in(cx, |editor, window, cx| {
23750 editor.move_to_beginning(&MoveToBeginning, window, cx);
23751 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23752 editor.move_to_end(&MoveToEnd, window, cx);
23753 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23754 editor.move_up(&MoveUp, window, cx);
23755 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23756 });
23757
23758 let breakpoints = editor.update(cx, |editor, cx| {
23759 editor
23760 .breakpoint_store()
23761 .as_ref()
23762 .unwrap()
23763 .read(cx)
23764 .all_source_breakpoints(cx)
23765 });
23766
23767 assert_eq!(1, breakpoints.len());
23768 assert_breakpoint(
23769 &breakpoints,
23770 &abs_path,
23771 vec![
23772 (0, Breakpoint::new_standard()),
23773 (2, disable_breakpoint),
23774 (3, Breakpoint::new_standard()),
23775 ],
23776 );
23777}
23778
23779#[gpui::test]
23780async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23781 init_test(cx, |_| {});
23782 let capabilities = lsp::ServerCapabilities {
23783 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23784 prepare_provider: Some(true),
23785 work_done_progress_options: Default::default(),
23786 })),
23787 ..Default::default()
23788 };
23789 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23790
23791 cx.set_state(indoc! {"
23792 struct Fˇoo {}
23793 "});
23794
23795 cx.update_editor(|editor, _, cx| {
23796 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23797 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23798 editor.highlight_background::<DocumentHighlightRead>(
23799 &[highlight_range],
23800 |theme| theme.colors().editor_document_highlight_read_background,
23801 cx,
23802 );
23803 });
23804
23805 let mut prepare_rename_handler = cx
23806 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23807 move |_, _, _| async move {
23808 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23809 start: lsp::Position {
23810 line: 0,
23811 character: 7,
23812 },
23813 end: lsp::Position {
23814 line: 0,
23815 character: 10,
23816 },
23817 })))
23818 },
23819 );
23820 let prepare_rename_task = cx
23821 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23822 .expect("Prepare rename was not started");
23823 prepare_rename_handler.next().await.unwrap();
23824 prepare_rename_task.await.expect("Prepare rename failed");
23825
23826 let mut rename_handler =
23827 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23828 let edit = lsp::TextEdit {
23829 range: lsp::Range {
23830 start: lsp::Position {
23831 line: 0,
23832 character: 7,
23833 },
23834 end: lsp::Position {
23835 line: 0,
23836 character: 10,
23837 },
23838 },
23839 new_text: "FooRenamed".to_string(),
23840 };
23841 Ok(Some(lsp::WorkspaceEdit::new(
23842 // Specify the same edit twice
23843 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23844 )))
23845 });
23846 let rename_task = cx
23847 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23848 .expect("Confirm rename was not started");
23849 rename_handler.next().await.unwrap();
23850 rename_task.await.expect("Confirm rename failed");
23851 cx.run_until_parked();
23852
23853 // Despite two edits, only one is actually applied as those are identical
23854 cx.assert_editor_state(indoc! {"
23855 struct FooRenamedˇ {}
23856 "});
23857}
23858
23859#[gpui::test]
23860async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23861 init_test(cx, |_| {});
23862 // These capabilities indicate that the server does not support prepare rename.
23863 let capabilities = lsp::ServerCapabilities {
23864 rename_provider: Some(lsp::OneOf::Left(true)),
23865 ..Default::default()
23866 };
23867 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23868
23869 cx.set_state(indoc! {"
23870 struct Fˇoo {}
23871 "});
23872
23873 cx.update_editor(|editor, _window, cx| {
23874 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23875 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23876 editor.highlight_background::<DocumentHighlightRead>(
23877 &[highlight_range],
23878 |theme| theme.colors().editor_document_highlight_read_background,
23879 cx,
23880 );
23881 });
23882
23883 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23884 .expect("Prepare rename was not started")
23885 .await
23886 .expect("Prepare rename failed");
23887
23888 let mut rename_handler =
23889 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23890 let edit = lsp::TextEdit {
23891 range: lsp::Range {
23892 start: lsp::Position {
23893 line: 0,
23894 character: 7,
23895 },
23896 end: lsp::Position {
23897 line: 0,
23898 character: 10,
23899 },
23900 },
23901 new_text: "FooRenamed".to_string(),
23902 };
23903 Ok(Some(lsp::WorkspaceEdit::new(
23904 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23905 )))
23906 });
23907 let rename_task = cx
23908 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23909 .expect("Confirm rename was not started");
23910 rename_handler.next().await.unwrap();
23911 rename_task.await.expect("Confirm rename failed");
23912 cx.run_until_parked();
23913
23914 // Correct range is renamed, as `surrounding_word` is used to find it.
23915 cx.assert_editor_state(indoc! {"
23916 struct FooRenamedˇ {}
23917 "});
23918}
23919
23920#[gpui::test]
23921async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23922 init_test(cx, |_| {});
23923 let mut cx = EditorTestContext::new(cx).await;
23924
23925 let language = Arc::new(
23926 Language::new(
23927 LanguageConfig::default(),
23928 Some(tree_sitter_html::LANGUAGE.into()),
23929 )
23930 .with_brackets_query(
23931 r#"
23932 ("<" @open "/>" @close)
23933 ("</" @open ">" @close)
23934 ("<" @open ">" @close)
23935 ("\"" @open "\"" @close)
23936 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23937 "#,
23938 )
23939 .unwrap(),
23940 );
23941 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23942
23943 cx.set_state(indoc! {"
23944 <span>ˇ</span>
23945 "});
23946 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23947 cx.assert_editor_state(indoc! {"
23948 <span>
23949 ˇ
23950 </span>
23951 "});
23952
23953 cx.set_state(indoc! {"
23954 <span><span></span>ˇ</span>
23955 "});
23956 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23957 cx.assert_editor_state(indoc! {"
23958 <span><span></span>
23959 ˇ</span>
23960 "});
23961
23962 cx.set_state(indoc! {"
23963 <span>ˇ
23964 </span>
23965 "});
23966 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23967 cx.assert_editor_state(indoc! {"
23968 <span>
23969 ˇ
23970 </span>
23971 "});
23972}
23973
23974#[gpui::test(iterations = 10)]
23975async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23976 init_test(cx, |_| {});
23977
23978 let fs = FakeFs::new(cx.executor());
23979 fs.insert_tree(
23980 path!("/dir"),
23981 json!({
23982 "a.ts": "a",
23983 }),
23984 )
23985 .await;
23986
23987 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23988 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23989 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23990
23991 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23992 language_registry.add(Arc::new(Language::new(
23993 LanguageConfig {
23994 name: "TypeScript".into(),
23995 matcher: LanguageMatcher {
23996 path_suffixes: vec!["ts".to_string()],
23997 ..Default::default()
23998 },
23999 ..Default::default()
24000 },
24001 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24002 )));
24003 let mut fake_language_servers = language_registry.register_fake_lsp(
24004 "TypeScript",
24005 FakeLspAdapter {
24006 capabilities: lsp::ServerCapabilities {
24007 code_lens_provider: Some(lsp::CodeLensOptions {
24008 resolve_provider: Some(true),
24009 }),
24010 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24011 commands: vec!["_the/command".to_string()],
24012 ..lsp::ExecuteCommandOptions::default()
24013 }),
24014 ..lsp::ServerCapabilities::default()
24015 },
24016 ..FakeLspAdapter::default()
24017 },
24018 );
24019
24020 let editor = workspace
24021 .update(cx, |workspace, window, cx| {
24022 workspace.open_abs_path(
24023 PathBuf::from(path!("/dir/a.ts")),
24024 OpenOptions::default(),
24025 window,
24026 cx,
24027 )
24028 })
24029 .unwrap()
24030 .await
24031 .unwrap()
24032 .downcast::<Editor>()
24033 .unwrap();
24034 cx.executor().run_until_parked();
24035
24036 let fake_server = fake_language_servers.next().await.unwrap();
24037
24038 let buffer = editor.update(cx, |editor, cx| {
24039 editor
24040 .buffer()
24041 .read(cx)
24042 .as_singleton()
24043 .expect("have opened a single file by path")
24044 });
24045
24046 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24047 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24048 drop(buffer_snapshot);
24049 let actions = cx
24050 .update_window(*workspace, |_, window, cx| {
24051 project.code_actions(&buffer, anchor..anchor, window, cx)
24052 })
24053 .unwrap();
24054
24055 fake_server
24056 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24057 Ok(Some(vec![
24058 lsp::CodeLens {
24059 range: lsp::Range::default(),
24060 command: Some(lsp::Command {
24061 title: "Code lens command".to_owned(),
24062 command: "_the/command".to_owned(),
24063 arguments: None,
24064 }),
24065 data: None,
24066 },
24067 lsp::CodeLens {
24068 range: lsp::Range::default(),
24069 command: Some(lsp::Command {
24070 title: "Command not in capabilities".to_owned(),
24071 command: "not in capabilities".to_owned(),
24072 arguments: None,
24073 }),
24074 data: None,
24075 },
24076 lsp::CodeLens {
24077 range: lsp::Range {
24078 start: lsp::Position {
24079 line: 1,
24080 character: 1,
24081 },
24082 end: lsp::Position {
24083 line: 1,
24084 character: 1,
24085 },
24086 },
24087 command: Some(lsp::Command {
24088 title: "Command not in range".to_owned(),
24089 command: "_the/command".to_owned(),
24090 arguments: None,
24091 }),
24092 data: None,
24093 },
24094 ]))
24095 })
24096 .next()
24097 .await;
24098
24099 let actions = actions.await.unwrap();
24100 assert_eq!(
24101 actions.len(),
24102 1,
24103 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24104 );
24105 let action = actions[0].clone();
24106 let apply = project.update(cx, |project, cx| {
24107 project.apply_code_action(buffer.clone(), action, true, cx)
24108 });
24109
24110 // Resolving the code action does not populate its edits. In absence of
24111 // edits, we must execute the given command.
24112 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24113 |mut lens, _| async move {
24114 let lens_command = lens.command.as_mut().expect("should have a command");
24115 assert_eq!(lens_command.title, "Code lens command");
24116 lens_command.arguments = Some(vec![json!("the-argument")]);
24117 Ok(lens)
24118 },
24119 );
24120
24121 // While executing the command, the language server sends the editor
24122 // a `workspaceEdit` request.
24123 fake_server
24124 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24125 let fake = fake_server.clone();
24126 move |params, _| {
24127 assert_eq!(params.command, "_the/command");
24128 let fake = fake.clone();
24129 async move {
24130 fake.server
24131 .request::<lsp::request::ApplyWorkspaceEdit>(
24132 lsp::ApplyWorkspaceEditParams {
24133 label: None,
24134 edit: lsp::WorkspaceEdit {
24135 changes: Some(
24136 [(
24137 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24138 vec![lsp::TextEdit {
24139 range: lsp::Range::new(
24140 lsp::Position::new(0, 0),
24141 lsp::Position::new(0, 0),
24142 ),
24143 new_text: "X".into(),
24144 }],
24145 )]
24146 .into_iter()
24147 .collect(),
24148 ),
24149 ..lsp::WorkspaceEdit::default()
24150 },
24151 },
24152 )
24153 .await
24154 .into_response()
24155 .unwrap();
24156 Ok(Some(json!(null)))
24157 }
24158 }
24159 })
24160 .next()
24161 .await;
24162
24163 // Applying the code lens command returns a project transaction containing the edits
24164 // sent by the language server in its `workspaceEdit` request.
24165 let transaction = apply.await.unwrap();
24166 assert!(transaction.0.contains_key(&buffer));
24167 buffer.update(cx, |buffer, cx| {
24168 assert_eq!(buffer.text(), "Xa");
24169 buffer.undo(cx);
24170 assert_eq!(buffer.text(), "a");
24171 });
24172
24173 let actions_after_edits = cx
24174 .update_window(*workspace, |_, window, cx| {
24175 project.code_actions(&buffer, anchor..anchor, window, cx)
24176 })
24177 .unwrap()
24178 .await
24179 .unwrap();
24180 assert_eq!(
24181 actions, actions_after_edits,
24182 "For the same selection, same code lens actions should be returned"
24183 );
24184
24185 let _responses =
24186 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24187 panic!("No more code lens requests are expected");
24188 });
24189 editor.update_in(cx, |editor, window, cx| {
24190 editor.select_all(&SelectAll, window, cx);
24191 });
24192 cx.executor().run_until_parked();
24193 let new_actions = cx
24194 .update_window(*workspace, |_, window, cx| {
24195 project.code_actions(&buffer, anchor..anchor, window, cx)
24196 })
24197 .unwrap()
24198 .await
24199 .unwrap();
24200 assert_eq!(
24201 actions, new_actions,
24202 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24203 );
24204}
24205
24206#[gpui::test]
24207async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24208 init_test(cx, |_| {});
24209
24210 let fs = FakeFs::new(cx.executor());
24211 let main_text = r#"fn main() {
24212println!("1");
24213println!("2");
24214println!("3");
24215println!("4");
24216println!("5");
24217}"#;
24218 let lib_text = "mod foo {}";
24219 fs.insert_tree(
24220 path!("/a"),
24221 json!({
24222 "lib.rs": lib_text,
24223 "main.rs": main_text,
24224 }),
24225 )
24226 .await;
24227
24228 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24229 let (workspace, cx) =
24230 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24231 let worktree_id = workspace.update(cx, |workspace, cx| {
24232 workspace.project().update(cx, |project, cx| {
24233 project.worktrees(cx).next().unwrap().read(cx).id()
24234 })
24235 });
24236
24237 let expected_ranges = vec![
24238 Point::new(0, 0)..Point::new(0, 0),
24239 Point::new(1, 0)..Point::new(1, 1),
24240 Point::new(2, 0)..Point::new(2, 2),
24241 Point::new(3, 0)..Point::new(3, 3),
24242 ];
24243
24244 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24245 let editor_1 = workspace
24246 .update_in(cx, |workspace, window, cx| {
24247 workspace.open_path(
24248 (worktree_id, rel_path("main.rs")),
24249 Some(pane_1.downgrade()),
24250 true,
24251 window,
24252 cx,
24253 )
24254 })
24255 .unwrap()
24256 .await
24257 .downcast::<Editor>()
24258 .unwrap();
24259 pane_1.update(cx, |pane, cx| {
24260 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24261 open_editor.update(cx, |editor, cx| {
24262 assert_eq!(
24263 editor.display_text(cx),
24264 main_text,
24265 "Original main.rs text on initial open",
24266 );
24267 assert_eq!(
24268 editor
24269 .selections
24270 .all::<Point>(&editor.display_snapshot(cx))
24271 .into_iter()
24272 .map(|s| s.range())
24273 .collect::<Vec<_>>(),
24274 vec![Point::zero()..Point::zero()],
24275 "Default selections on initial open",
24276 );
24277 })
24278 });
24279 editor_1.update_in(cx, |editor, window, cx| {
24280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24281 s.select_ranges(expected_ranges.clone());
24282 });
24283 });
24284
24285 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24286 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24287 });
24288 let editor_2 = workspace
24289 .update_in(cx, |workspace, window, cx| {
24290 workspace.open_path(
24291 (worktree_id, rel_path("main.rs")),
24292 Some(pane_2.downgrade()),
24293 true,
24294 window,
24295 cx,
24296 )
24297 })
24298 .unwrap()
24299 .await
24300 .downcast::<Editor>()
24301 .unwrap();
24302 pane_2.update(cx, |pane, cx| {
24303 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24304 open_editor.update(cx, |editor, cx| {
24305 assert_eq!(
24306 editor.display_text(cx),
24307 main_text,
24308 "Original main.rs text on initial open in another panel",
24309 );
24310 assert_eq!(
24311 editor
24312 .selections
24313 .all::<Point>(&editor.display_snapshot(cx))
24314 .into_iter()
24315 .map(|s| s.range())
24316 .collect::<Vec<_>>(),
24317 vec![Point::zero()..Point::zero()],
24318 "Default selections on initial open in another panel",
24319 );
24320 })
24321 });
24322
24323 editor_2.update_in(cx, |editor, window, cx| {
24324 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24325 });
24326
24327 let _other_editor_1 = workspace
24328 .update_in(cx, |workspace, window, cx| {
24329 workspace.open_path(
24330 (worktree_id, rel_path("lib.rs")),
24331 Some(pane_1.downgrade()),
24332 true,
24333 window,
24334 cx,
24335 )
24336 })
24337 .unwrap()
24338 .await
24339 .downcast::<Editor>()
24340 .unwrap();
24341 pane_1
24342 .update_in(cx, |pane, window, cx| {
24343 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24344 })
24345 .await
24346 .unwrap();
24347 drop(editor_1);
24348 pane_1.update(cx, |pane, cx| {
24349 pane.active_item()
24350 .unwrap()
24351 .downcast::<Editor>()
24352 .unwrap()
24353 .update(cx, |editor, cx| {
24354 assert_eq!(
24355 editor.display_text(cx),
24356 lib_text,
24357 "Other file should be open and active",
24358 );
24359 });
24360 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24361 });
24362
24363 let _other_editor_2 = workspace
24364 .update_in(cx, |workspace, window, cx| {
24365 workspace.open_path(
24366 (worktree_id, rel_path("lib.rs")),
24367 Some(pane_2.downgrade()),
24368 true,
24369 window,
24370 cx,
24371 )
24372 })
24373 .unwrap()
24374 .await
24375 .downcast::<Editor>()
24376 .unwrap();
24377 pane_2
24378 .update_in(cx, |pane, window, cx| {
24379 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24380 })
24381 .await
24382 .unwrap();
24383 drop(editor_2);
24384 pane_2.update(cx, |pane, cx| {
24385 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24386 open_editor.update(cx, |editor, cx| {
24387 assert_eq!(
24388 editor.display_text(cx),
24389 lib_text,
24390 "Other file should be open and active in another panel too",
24391 );
24392 });
24393 assert_eq!(
24394 pane.items().count(),
24395 1,
24396 "No other editors should be open in another pane",
24397 );
24398 });
24399
24400 let _editor_1_reopened = workspace
24401 .update_in(cx, |workspace, window, cx| {
24402 workspace.open_path(
24403 (worktree_id, rel_path("main.rs")),
24404 Some(pane_1.downgrade()),
24405 true,
24406 window,
24407 cx,
24408 )
24409 })
24410 .unwrap()
24411 .await
24412 .downcast::<Editor>()
24413 .unwrap();
24414 let _editor_2_reopened = workspace
24415 .update_in(cx, |workspace, window, cx| {
24416 workspace.open_path(
24417 (worktree_id, rel_path("main.rs")),
24418 Some(pane_2.downgrade()),
24419 true,
24420 window,
24421 cx,
24422 )
24423 })
24424 .unwrap()
24425 .await
24426 .downcast::<Editor>()
24427 .unwrap();
24428 pane_1.update(cx, |pane, cx| {
24429 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24430 open_editor.update(cx, |editor, cx| {
24431 assert_eq!(
24432 editor.display_text(cx),
24433 main_text,
24434 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24435 );
24436 assert_eq!(
24437 editor
24438 .selections
24439 .all::<Point>(&editor.display_snapshot(cx))
24440 .into_iter()
24441 .map(|s| s.range())
24442 .collect::<Vec<_>>(),
24443 expected_ranges,
24444 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24445 );
24446 })
24447 });
24448 pane_2.update(cx, |pane, cx| {
24449 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24450 open_editor.update(cx, |editor, cx| {
24451 assert_eq!(
24452 editor.display_text(cx),
24453 r#"fn main() {
24454⋯rintln!("1");
24455⋯intln!("2");
24456⋯ntln!("3");
24457println!("4");
24458println!("5");
24459}"#,
24460 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24461 );
24462 assert_eq!(
24463 editor
24464 .selections
24465 .all::<Point>(&editor.display_snapshot(cx))
24466 .into_iter()
24467 .map(|s| s.range())
24468 .collect::<Vec<_>>(),
24469 vec![Point::zero()..Point::zero()],
24470 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24471 );
24472 })
24473 });
24474}
24475
24476#[gpui::test]
24477async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24478 init_test(cx, |_| {});
24479
24480 let fs = FakeFs::new(cx.executor());
24481 let main_text = r#"fn main() {
24482println!("1");
24483println!("2");
24484println!("3");
24485println!("4");
24486println!("5");
24487}"#;
24488 let lib_text = "mod foo {}";
24489 fs.insert_tree(
24490 path!("/a"),
24491 json!({
24492 "lib.rs": lib_text,
24493 "main.rs": main_text,
24494 }),
24495 )
24496 .await;
24497
24498 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24499 let (workspace, cx) =
24500 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24501 let worktree_id = workspace.update(cx, |workspace, cx| {
24502 workspace.project().update(cx, |project, cx| {
24503 project.worktrees(cx).next().unwrap().read(cx).id()
24504 })
24505 });
24506
24507 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24508 let editor = workspace
24509 .update_in(cx, |workspace, window, cx| {
24510 workspace.open_path(
24511 (worktree_id, rel_path("main.rs")),
24512 Some(pane.downgrade()),
24513 true,
24514 window,
24515 cx,
24516 )
24517 })
24518 .unwrap()
24519 .await
24520 .downcast::<Editor>()
24521 .unwrap();
24522 pane.update(cx, |pane, cx| {
24523 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24524 open_editor.update(cx, |editor, cx| {
24525 assert_eq!(
24526 editor.display_text(cx),
24527 main_text,
24528 "Original main.rs text on initial open",
24529 );
24530 })
24531 });
24532 editor.update_in(cx, |editor, window, cx| {
24533 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24534 });
24535
24536 cx.update_global(|store: &mut SettingsStore, cx| {
24537 store.update_user_settings(cx, |s| {
24538 s.workspace.restore_on_file_reopen = Some(false);
24539 });
24540 });
24541 editor.update_in(cx, |editor, window, cx| {
24542 editor.fold_ranges(
24543 vec![
24544 Point::new(1, 0)..Point::new(1, 1),
24545 Point::new(2, 0)..Point::new(2, 2),
24546 Point::new(3, 0)..Point::new(3, 3),
24547 ],
24548 false,
24549 window,
24550 cx,
24551 );
24552 });
24553 pane.update_in(cx, |pane, window, cx| {
24554 pane.close_all_items(&CloseAllItems::default(), window, cx)
24555 })
24556 .await
24557 .unwrap();
24558 pane.update(cx, |pane, _| {
24559 assert!(pane.active_item().is_none());
24560 });
24561 cx.update_global(|store: &mut SettingsStore, cx| {
24562 store.update_user_settings(cx, |s| {
24563 s.workspace.restore_on_file_reopen = Some(true);
24564 });
24565 });
24566
24567 let _editor_reopened = workspace
24568 .update_in(cx, |workspace, window, cx| {
24569 workspace.open_path(
24570 (worktree_id, rel_path("main.rs")),
24571 Some(pane.downgrade()),
24572 true,
24573 window,
24574 cx,
24575 )
24576 })
24577 .unwrap()
24578 .await
24579 .downcast::<Editor>()
24580 .unwrap();
24581 pane.update(cx, |pane, cx| {
24582 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24583 open_editor.update(cx, |editor, cx| {
24584 assert_eq!(
24585 editor.display_text(cx),
24586 main_text,
24587 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24588 );
24589 })
24590 });
24591}
24592
24593#[gpui::test]
24594async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24595 struct EmptyModalView {
24596 focus_handle: gpui::FocusHandle,
24597 }
24598 impl EventEmitter<DismissEvent> for EmptyModalView {}
24599 impl Render for EmptyModalView {
24600 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24601 div()
24602 }
24603 }
24604 impl Focusable for EmptyModalView {
24605 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24606 self.focus_handle.clone()
24607 }
24608 }
24609 impl workspace::ModalView for EmptyModalView {}
24610 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24611 EmptyModalView {
24612 focus_handle: cx.focus_handle(),
24613 }
24614 }
24615
24616 init_test(cx, |_| {});
24617
24618 let fs = FakeFs::new(cx.executor());
24619 let project = Project::test(fs, [], cx).await;
24620 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24621 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24622 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24623 let editor = cx.new_window_entity(|window, cx| {
24624 Editor::new(
24625 EditorMode::full(),
24626 buffer,
24627 Some(project.clone()),
24628 window,
24629 cx,
24630 )
24631 });
24632 workspace
24633 .update(cx, |workspace, window, cx| {
24634 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24635 })
24636 .unwrap();
24637 editor.update_in(cx, |editor, window, cx| {
24638 editor.open_context_menu(&OpenContextMenu, window, cx);
24639 assert!(editor.mouse_context_menu.is_some());
24640 });
24641 workspace
24642 .update(cx, |workspace, window, cx| {
24643 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24644 })
24645 .unwrap();
24646 cx.read(|cx| {
24647 assert!(editor.read(cx).mouse_context_menu.is_none());
24648 });
24649}
24650
24651fn set_linked_edit_ranges(
24652 opening: (Point, Point),
24653 closing: (Point, Point),
24654 editor: &mut Editor,
24655 cx: &mut Context<Editor>,
24656) {
24657 let Some((buffer, _)) = editor
24658 .buffer
24659 .read(cx)
24660 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24661 else {
24662 panic!("Failed to get buffer for selection position");
24663 };
24664 let buffer = buffer.read(cx);
24665 let buffer_id = buffer.remote_id();
24666 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24667 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24668 let mut linked_ranges = HashMap::default();
24669 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24670 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24671}
24672
24673#[gpui::test]
24674async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24675 init_test(cx, |_| {});
24676
24677 let fs = FakeFs::new(cx.executor());
24678 fs.insert_file(path!("/file.html"), Default::default())
24679 .await;
24680
24681 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24682
24683 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24684 let html_language = Arc::new(Language::new(
24685 LanguageConfig {
24686 name: "HTML".into(),
24687 matcher: LanguageMatcher {
24688 path_suffixes: vec!["html".to_string()],
24689 ..LanguageMatcher::default()
24690 },
24691 brackets: BracketPairConfig {
24692 pairs: vec![BracketPair {
24693 start: "<".into(),
24694 end: ">".into(),
24695 close: true,
24696 ..Default::default()
24697 }],
24698 ..Default::default()
24699 },
24700 ..Default::default()
24701 },
24702 Some(tree_sitter_html::LANGUAGE.into()),
24703 ));
24704 language_registry.add(html_language);
24705 let mut fake_servers = language_registry.register_fake_lsp(
24706 "HTML",
24707 FakeLspAdapter {
24708 capabilities: lsp::ServerCapabilities {
24709 completion_provider: Some(lsp::CompletionOptions {
24710 resolve_provider: Some(true),
24711 ..Default::default()
24712 }),
24713 ..Default::default()
24714 },
24715 ..Default::default()
24716 },
24717 );
24718
24719 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24720 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24721
24722 let worktree_id = workspace
24723 .update(cx, |workspace, _window, cx| {
24724 workspace.project().update(cx, |project, cx| {
24725 project.worktrees(cx).next().unwrap().read(cx).id()
24726 })
24727 })
24728 .unwrap();
24729 project
24730 .update(cx, |project, cx| {
24731 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24732 })
24733 .await
24734 .unwrap();
24735 let editor = workspace
24736 .update(cx, |workspace, window, cx| {
24737 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24738 })
24739 .unwrap()
24740 .await
24741 .unwrap()
24742 .downcast::<Editor>()
24743 .unwrap();
24744
24745 let fake_server = fake_servers.next().await.unwrap();
24746 editor.update_in(cx, |editor, window, cx| {
24747 editor.set_text("<ad></ad>", window, cx);
24748 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24749 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24750 });
24751 set_linked_edit_ranges(
24752 (Point::new(0, 1), Point::new(0, 3)),
24753 (Point::new(0, 6), Point::new(0, 8)),
24754 editor,
24755 cx,
24756 );
24757 });
24758 let mut completion_handle =
24759 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24760 Ok(Some(lsp::CompletionResponse::Array(vec![
24761 lsp::CompletionItem {
24762 label: "head".to_string(),
24763 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24764 lsp::InsertReplaceEdit {
24765 new_text: "head".to_string(),
24766 insert: lsp::Range::new(
24767 lsp::Position::new(0, 1),
24768 lsp::Position::new(0, 3),
24769 ),
24770 replace: lsp::Range::new(
24771 lsp::Position::new(0, 1),
24772 lsp::Position::new(0, 3),
24773 ),
24774 },
24775 )),
24776 ..Default::default()
24777 },
24778 ])))
24779 });
24780 editor.update_in(cx, |editor, window, cx| {
24781 editor.show_completions(&ShowCompletions, window, cx);
24782 });
24783 cx.run_until_parked();
24784 completion_handle.next().await.unwrap();
24785 editor.update(cx, |editor, _| {
24786 assert!(
24787 editor.context_menu_visible(),
24788 "Completion menu should be visible"
24789 );
24790 });
24791 editor.update_in(cx, |editor, window, cx| {
24792 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24793 });
24794 cx.executor().run_until_parked();
24795 editor.update(cx, |editor, cx| {
24796 assert_eq!(editor.text(cx), "<head></head>");
24797 });
24798}
24799
24800#[gpui::test]
24801async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24802 init_test(cx, |_| {});
24803
24804 let mut cx = EditorTestContext::new(cx).await;
24805 let language = Arc::new(Language::new(
24806 LanguageConfig {
24807 name: "TSX".into(),
24808 matcher: LanguageMatcher {
24809 path_suffixes: vec!["tsx".to_string()],
24810 ..LanguageMatcher::default()
24811 },
24812 brackets: BracketPairConfig {
24813 pairs: vec![BracketPair {
24814 start: "<".into(),
24815 end: ">".into(),
24816 close: true,
24817 ..Default::default()
24818 }],
24819 ..Default::default()
24820 },
24821 linked_edit_characters: HashSet::from_iter(['.']),
24822 ..Default::default()
24823 },
24824 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24825 ));
24826 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24827
24828 // Test typing > does not extend linked pair
24829 cx.set_state("<divˇ<div></div>");
24830 cx.update_editor(|editor, _, cx| {
24831 set_linked_edit_ranges(
24832 (Point::new(0, 1), Point::new(0, 4)),
24833 (Point::new(0, 11), Point::new(0, 14)),
24834 editor,
24835 cx,
24836 );
24837 });
24838 cx.update_editor(|editor, window, cx| {
24839 editor.handle_input(">", window, cx);
24840 });
24841 cx.assert_editor_state("<div>ˇ<div></div>");
24842
24843 // Test typing . do extend linked pair
24844 cx.set_state("<Animatedˇ></Animated>");
24845 cx.update_editor(|editor, _, cx| {
24846 set_linked_edit_ranges(
24847 (Point::new(0, 1), Point::new(0, 9)),
24848 (Point::new(0, 12), Point::new(0, 20)),
24849 editor,
24850 cx,
24851 );
24852 });
24853 cx.update_editor(|editor, window, cx| {
24854 editor.handle_input(".", window, cx);
24855 });
24856 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24857 cx.update_editor(|editor, _, cx| {
24858 set_linked_edit_ranges(
24859 (Point::new(0, 1), Point::new(0, 10)),
24860 (Point::new(0, 13), Point::new(0, 21)),
24861 editor,
24862 cx,
24863 );
24864 });
24865 cx.update_editor(|editor, window, cx| {
24866 editor.handle_input("V", window, cx);
24867 });
24868 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24869}
24870
24871#[gpui::test]
24872async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24873 init_test(cx, |_| {});
24874
24875 let fs = FakeFs::new(cx.executor());
24876 fs.insert_tree(
24877 path!("/root"),
24878 json!({
24879 "a": {
24880 "main.rs": "fn main() {}",
24881 },
24882 "foo": {
24883 "bar": {
24884 "external_file.rs": "pub mod external {}",
24885 }
24886 }
24887 }),
24888 )
24889 .await;
24890
24891 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24892 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24893 language_registry.add(rust_lang());
24894 let _fake_servers = language_registry.register_fake_lsp(
24895 "Rust",
24896 FakeLspAdapter {
24897 ..FakeLspAdapter::default()
24898 },
24899 );
24900 let (workspace, cx) =
24901 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24902 let worktree_id = workspace.update(cx, |workspace, cx| {
24903 workspace.project().update(cx, |project, cx| {
24904 project.worktrees(cx).next().unwrap().read(cx).id()
24905 })
24906 });
24907
24908 let assert_language_servers_count =
24909 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24910 project.update(cx, |project, cx| {
24911 let current = project
24912 .lsp_store()
24913 .read(cx)
24914 .as_local()
24915 .unwrap()
24916 .language_servers
24917 .len();
24918 assert_eq!(expected, current, "{context}");
24919 });
24920 };
24921
24922 assert_language_servers_count(
24923 0,
24924 "No servers should be running before any file is open",
24925 cx,
24926 );
24927 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24928 let main_editor = workspace
24929 .update_in(cx, |workspace, window, cx| {
24930 workspace.open_path(
24931 (worktree_id, rel_path("main.rs")),
24932 Some(pane.downgrade()),
24933 true,
24934 window,
24935 cx,
24936 )
24937 })
24938 .unwrap()
24939 .await
24940 .downcast::<Editor>()
24941 .unwrap();
24942 pane.update(cx, |pane, cx| {
24943 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24944 open_editor.update(cx, |editor, cx| {
24945 assert_eq!(
24946 editor.display_text(cx),
24947 "fn main() {}",
24948 "Original main.rs text on initial open",
24949 );
24950 });
24951 assert_eq!(open_editor, main_editor);
24952 });
24953 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24954
24955 let external_editor = workspace
24956 .update_in(cx, |workspace, window, cx| {
24957 workspace.open_abs_path(
24958 PathBuf::from("/root/foo/bar/external_file.rs"),
24959 OpenOptions::default(),
24960 window,
24961 cx,
24962 )
24963 })
24964 .await
24965 .expect("opening external file")
24966 .downcast::<Editor>()
24967 .expect("downcasted external file's open element to editor");
24968 pane.update(cx, |pane, cx| {
24969 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24970 open_editor.update(cx, |editor, cx| {
24971 assert_eq!(
24972 editor.display_text(cx),
24973 "pub mod external {}",
24974 "External file is open now",
24975 );
24976 });
24977 assert_eq!(open_editor, external_editor);
24978 });
24979 assert_language_servers_count(
24980 1,
24981 "Second, external, *.rs file should join the existing server",
24982 cx,
24983 );
24984
24985 pane.update_in(cx, |pane, window, cx| {
24986 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24987 })
24988 .await
24989 .unwrap();
24990 pane.update_in(cx, |pane, window, cx| {
24991 pane.navigate_backward(&Default::default(), window, cx);
24992 });
24993 cx.run_until_parked();
24994 pane.update(cx, |pane, cx| {
24995 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24996 open_editor.update(cx, |editor, cx| {
24997 assert_eq!(
24998 editor.display_text(cx),
24999 "pub mod external {}",
25000 "External file is open now",
25001 );
25002 });
25003 });
25004 assert_language_servers_count(
25005 1,
25006 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25007 cx,
25008 );
25009
25010 cx.update(|_, cx| {
25011 workspace::reload(cx);
25012 });
25013 assert_language_servers_count(
25014 1,
25015 "After reloading the worktree with local and external files opened, only one project should be started",
25016 cx,
25017 );
25018}
25019
25020#[gpui::test]
25021async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25022 init_test(cx, |_| {});
25023
25024 let mut cx = EditorTestContext::new(cx).await;
25025 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25026 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25027
25028 // test cursor move to start of each line on tab
25029 // for `if`, `elif`, `else`, `while`, `with` and `for`
25030 cx.set_state(indoc! {"
25031 def main():
25032 ˇ for item in items:
25033 ˇ while item.active:
25034 ˇ if item.value > 10:
25035 ˇ continue
25036 ˇ elif item.value < 0:
25037 ˇ break
25038 ˇ else:
25039 ˇ with item.context() as ctx:
25040 ˇ yield count
25041 ˇ else:
25042 ˇ log('while else')
25043 ˇ else:
25044 ˇ log('for else')
25045 "});
25046 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25047 cx.assert_editor_state(indoc! {"
25048 def main():
25049 ˇfor item in items:
25050 ˇwhile item.active:
25051 ˇif item.value > 10:
25052 ˇcontinue
25053 ˇelif item.value < 0:
25054 ˇbreak
25055 ˇelse:
25056 ˇwith item.context() as ctx:
25057 ˇyield count
25058 ˇelse:
25059 ˇlog('while else')
25060 ˇelse:
25061 ˇlog('for else')
25062 "});
25063 // test relative indent is preserved when tab
25064 // for `if`, `elif`, `else`, `while`, `with` and `for`
25065 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25066 cx.assert_editor_state(indoc! {"
25067 def main():
25068 ˇfor item in items:
25069 ˇwhile item.active:
25070 ˇif item.value > 10:
25071 ˇcontinue
25072 ˇelif item.value < 0:
25073 ˇbreak
25074 ˇelse:
25075 ˇwith item.context() as ctx:
25076 ˇyield count
25077 ˇelse:
25078 ˇlog('while else')
25079 ˇelse:
25080 ˇlog('for else')
25081 "});
25082
25083 // test cursor move to start of each line on tab
25084 // for `try`, `except`, `else`, `finally`, `match` and `def`
25085 cx.set_state(indoc! {"
25086 def main():
25087 ˇ try:
25088 ˇ fetch()
25089 ˇ except ValueError:
25090 ˇ handle_error()
25091 ˇ else:
25092 ˇ match value:
25093 ˇ case _:
25094 ˇ finally:
25095 ˇ def status():
25096 ˇ return 0
25097 "});
25098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25099 cx.assert_editor_state(indoc! {"
25100 def main():
25101 ˇtry:
25102 ˇfetch()
25103 ˇexcept ValueError:
25104 ˇhandle_error()
25105 ˇelse:
25106 ˇmatch value:
25107 ˇcase _:
25108 ˇfinally:
25109 ˇdef status():
25110 ˇreturn 0
25111 "});
25112 // test relative indent is preserved when tab
25113 // for `try`, `except`, `else`, `finally`, `match` and `def`
25114 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25115 cx.assert_editor_state(indoc! {"
25116 def main():
25117 ˇtry:
25118 ˇfetch()
25119 ˇexcept ValueError:
25120 ˇhandle_error()
25121 ˇelse:
25122 ˇmatch value:
25123 ˇcase _:
25124 ˇfinally:
25125 ˇdef status():
25126 ˇreturn 0
25127 "});
25128}
25129
25130#[gpui::test]
25131async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25132 init_test(cx, |_| {});
25133
25134 let mut cx = EditorTestContext::new(cx).await;
25135 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25136 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25137
25138 // test `else` auto outdents when typed inside `if` block
25139 cx.set_state(indoc! {"
25140 def main():
25141 if i == 2:
25142 return
25143 ˇ
25144 "});
25145 cx.update_editor(|editor, window, cx| {
25146 editor.handle_input("else:", window, cx);
25147 });
25148 cx.assert_editor_state(indoc! {"
25149 def main():
25150 if i == 2:
25151 return
25152 else:ˇ
25153 "});
25154
25155 // test `except` auto outdents when typed inside `try` block
25156 cx.set_state(indoc! {"
25157 def main():
25158 try:
25159 i = 2
25160 ˇ
25161 "});
25162 cx.update_editor(|editor, window, cx| {
25163 editor.handle_input("except:", window, cx);
25164 });
25165 cx.assert_editor_state(indoc! {"
25166 def main():
25167 try:
25168 i = 2
25169 except:ˇ
25170 "});
25171
25172 // test `else` auto outdents when typed inside `except` block
25173 cx.set_state(indoc! {"
25174 def main():
25175 try:
25176 i = 2
25177 except:
25178 j = 2
25179 ˇ
25180 "});
25181 cx.update_editor(|editor, window, cx| {
25182 editor.handle_input("else:", window, cx);
25183 });
25184 cx.assert_editor_state(indoc! {"
25185 def main():
25186 try:
25187 i = 2
25188 except:
25189 j = 2
25190 else:ˇ
25191 "});
25192
25193 // test `finally` auto outdents when typed inside `else` block
25194 cx.set_state(indoc! {"
25195 def main():
25196 try:
25197 i = 2
25198 except:
25199 j = 2
25200 else:
25201 k = 2
25202 ˇ
25203 "});
25204 cx.update_editor(|editor, window, cx| {
25205 editor.handle_input("finally:", window, cx);
25206 });
25207 cx.assert_editor_state(indoc! {"
25208 def main():
25209 try:
25210 i = 2
25211 except:
25212 j = 2
25213 else:
25214 k = 2
25215 finally:ˇ
25216 "});
25217
25218 // test `else` does not outdents when typed inside `except` block right after for block
25219 cx.set_state(indoc! {"
25220 def main():
25221 try:
25222 i = 2
25223 except:
25224 for i in range(n):
25225 pass
25226 ˇ
25227 "});
25228 cx.update_editor(|editor, window, cx| {
25229 editor.handle_input("else:", window, cx);
25230 });
25231 cx.assert_editor_state(indoc! {"
25232 def main():
25233 try:
25234 i = 2
25235 except:
25236 for i in range(n):
25237 pass
25238 else:ˇ
25239 "});
25240
25241 // test `finally` auto outdents when typed inside `else` block right after for block
25242 cx.set_state(indoc! {"
25243 def main():
25244 try:
25245 i = 2
25246 except:
25247 j = 2
25248 else:
25249 for i in range(n):
25250 pass
25251 ˇ
25252 "});
25253 cx.update_editor(|editor, window, cx| {
25254 editor.handle_input("finally:", window, cx);
25255 });
25256 cx.assert_editor_state(indoc! {"
25257 def main():
25258 try:
25259 i = 2
25260 except:
25261 j = 2
25262 else:
25263 for i in range(n):
25264 pass
25265 finally:ˇ
25266 "});
25267
25268 // test `except` outdents to inner "try" block
25269 cx.set_state(indoc! {"
25270 def main():
25271 try:
25272 i = 2
25273 if i == 2:
25274 try:
25275 i = 3
25276 ˇ
25277 "});
25278 cx.update_editor(|editor, window, cx| {
25279 editor.handle_input("except:", window, cx);
25280 });
25281 cx.assert_editor_state(indoc! {"
25282 def main():
25283 try:
25284 i = 2
25285 if i == 2:
25286 try:
25287 i = 3
25288 except:ˇ
25289 "});
25290
25291 // test `except` outdents to outer "try" block
25292 cx.set_state(indoc! {"
25293 def main():
25294 try:
25295 i = 2
25296 if i == 2:
25297 try:
25298 i = 3
25299 ˇ
25300 "});
25301 cx.update_editor(|editor, window, cx| {
25302 editor.handle_input("except:", window, cx);
25303 });
25304 cx.assert_editor_state(indoc! {"
25305 def main():
25306 try:
25307 i = 2
25308 if i == 2:
25309 try:
25310 i = 3
25311 except:ˇ
25312 "});
25313
25314 // test `else` stays at correct indent when typed after `for` block
25315 cx.set_state(indoc! {"
25316 def main():
25317 for i in range(10):
25318 if i == 3:
25319 break
25320 ˇ
25321 "});
25322 cx.update_editor(|editor, window, cx| {
25323 editor.handle_input("else:", window, cx);
25324 });
25325 cx.assert_editor_state(indoc! {"
25326 def main():
25327 for i in range(10):
25328 if i == 3:
25329 break
25330 else:ˇ
25331 "});
25332
25333 // test does not outdent on typing after line with square brackets
25334 cx.set_state(indoc! {"
25335 def f() -> list[str]:
25336 ˇ
25337 "});
25338 cx.update_editor(|editor, window, cx| {
25339 editor.handle_input("a", window, cx);
25340 });
25341 cx.assert_editor_state(indoc! {"
25342 def f() -> list[str]:
25343 aˇ
25344 "});
25345
25346 // test does not outdent on typing : after case keyword
25347 cx.set_state(indoc! {"
25348 match 1:
25349 caseˇ
25350 "});
25351 cx.update_editor(|editor, window, cx| {
25352 editor.handle_input(":", window, cx);
25353 });
25354 cx.assert_editor_state(indoc! {"
25355 match 1:
25356 case:ˇ
25357 "});
25358}
25359
25360#[gpui::test]
25361async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25362 init_test(cx, |_| {});
25363 update_test_language_settings(cx, |settings| {
25364 settings.defaults.extend_comment_on_newline = Some(false);
25365 });
25366 let mut cx = EditorTestContext::new(cx).await;
25367 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25368 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25369
25370 // test correct indent after newline on comment
25371 cx.set_state(indoc! {"
25372 # COMMENT:ˇ
25373 "});
25374 cx.update_editor(|editor, window, cx| {
25375 editor.newline(&Newline, window, cx);
25376 });
25377 cx.assert_editor_state(indoc! {"
25378 # COMMENT:
25379 ˇ
25380 "});
25381
25382 // test correct indent after newline in brackets
25383 cx.set_state(indoc! {"
25384 {ˇ}
25385 "});
25386 cx.update_editor(|editor, window, cx| {
25387 editor.newline(&Newline, window, cx);
25388 });
25389 cx.run_until_parked();
25390 cx.assert_editor_state(indoc! {"
25391 {
25392 ˇ
25393 }
25394 "});
25395
25396 cx.set_state(indoc! {"
25397 (ˇ)
25398 "});
25399 cx.update_editor(|editor, window, cx| {
25400 editor.newline(&Newline, window, cx);
25401 });
25402 cx.run_until_parked();
25403 cx.assert_editor_state(indoc! {"
25404 (
25405 ˇ
25406 )
25407 "});
25408
25409 // do not indent after empty lists or dictionaries
25410 cx.set_state(indoc! {"
25411 a = []ˇ
25412 "});
25413 cx.update_editor(|editor, window, cx| {
25414 editor.newline(&Newline, window, cx);
25415 });
25416 cx.run_until_parked();
25417 cx.assert_editor_state(indoc! {"
25418 a = []
25419 ˇ
25420 "});
25421}
25422
25423#[gpui::test]
25424async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25425 init_test(cx, |_| {});
25426
25427 let mut cx = EditorTestContext::new(cx).await;
25428 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25429 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25430
25431 // test cursor move to start of each line on tab
25432 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25433 cx.set_state(indoc! {"
25434 function main() {
25435 ˇ for item in $items; do
25436 ˇ while [ -n \"$item\" ]; do
25437 ˇ if [ \"$value\" -gt 10 ]; then
25438 ˇ continue
25439 ˇ elif [ \"$value\" -lt 0 ]; then
25440 ˇ break
25441 ˇ else
25442 ˇ echo \"$item\"
25443 ˇ fi
25444 ˇ done
25445 ˇ done
25446 ˇ}
25447 "});
25448 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25449 cx.assert_editor_state(indoc! {"
25450 function main() {
25451 ˇfor item in $items; do
25452 ˇwhile [ -n \"$item\" ]; do
25453 ˇif [ \"$value\" -gt 10 ]; then
25454 ˇcontinue
25455 ˇelif [ \"$value\" -lt 0 ]; then
25456 ˇbreak
25457 ˇelse
25458 ˇecho \"$item\"
25459 ˇfi
25460 ˇdone
25461 ˇdone
25462 ˇ}
25463 "});
25464 // test relative indent is preserved when tab
25465 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25466 cx.assert_editor_state(indoc! {"
25467 function main() {
25468 ˇfor item in $items; do
25469 ˇwhile [ -n \"$item\" ]; do
25470 ˇif [ \"$value\" -gt 10 ]; then
25471 ˇcontinue
25472 ˇelif [ \"$value\" -lt 0 ]; then
25473 ˇbreak
25474 ˇelse
25475 ˇecho \"$item\"
25476 ˇfi
25477 ˇdone
25478 ˇdone
25479 ˇ}
25480 "});
25481
25482 // test cursor move to start of each line on tab
25483 // for `case` statement with patterns
25484 cx.set_state(indoc! {"
25485 function handle() {
25486 ˇ case \"$1\" in
25487 ˇ start)
25488 ˇ echo \"a\"
25489 ˇ ;;
25490 ˇ stop)
25491 ˇ echo \"b\"
25492 ˇ ;;
25493 ˇ *)
25494 ˇ echo \"c\"
25495 ˇ ;;
25496 ˇ esac
25497 ˇ}
25498 "});
25499 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25500 cx.assert_editor_state(indoc! {"
25501 function handle() {
25502 ˇcase \"$1\" in
25503 ˇstart)
25504 ˇecho \"a\"
25505 ˇ;;
25506 ˇstop)
25507 ˇecho \"b\"
25508 ˇ;;
25509 ˇ*)
25510 ˇecho \"c\"
25511 ˇ;;
25512 ˇesac
25513 ˇ}
25514 "});
25515}
25516
25517#[gpui::test]
25518async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25519 init_test(cx, |_| {});
25520
25521 let mut cx = EditorTestContext::new(cx).await;
25522 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25523 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25524
25525 // test indents on comment insert
25526 cx.set_state(indoc! {"
25527 function main() {
25528 ˇ for item in $items; do
25529 ˇ while [ -n \"$item\" ]; do
25530 ˇ if [ \"$value\" -gt 10 ]; then
25531 ˇ continue
25532 ˇ elif [ \"$value\" -lt 0 ]; then
25533 ˇ break
25534 ˇ else
25535 ˇ echo \"$item\"
25536 ˇ fi
25537 ˇ done
25538 ˇ done
25539 ˇ}
25540 "});
25541 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25542 cx.assert_editor_state(indoc! {"
25543 function main() {
25544 #ˇ for item in $items; do
25545 #ˇ while [ -n \"$item\" ]; do
25546 #ˇ if [ \"$value\" -gt 10 ]; then
25547 #ˇ continue
25548 #ˇ elif [ \"$value\" -lt 0 ]; then
25549 #ˇ break
25550 #ˇ else
25551 #ˇ echo \"$item\"
25552 #ˇ fi
25553 #ˇ done
25554 #ˇ done
25555 #ˇ}
25556 "});
25557}
25558
25559#[gpui::test]
25560async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25561 init_test(cx, |_| {});
25562
25563 let mut cx = EditorTestContext::new(cx).await;
25564 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25565 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25566
25567 // test `else` auto outdents when typed inside `if` block
25568 cx.set_state(indoc! {"
25569 if [ \"$1\" = \"test\" ]; then
25570 echo \"foo bar\"
25571 ˇ
25572 "});
25573 cx.update_editor(|editor, window, cx| {
25574 editor.handle_input("else", window, cx);
25575 });
25576 cx.assert_editor_state(indoc! {"
25577 if [ \"$1\" = \"test\" ]; then
25578 echo \"foo bar\"
25579 elseˇ
25580 "});
25581
25582 // test `elif` auto outdents when typed inside `if` block
25583 cx.set_state(indoc! {"
25584 if [ \"$1\" = \"test\" ]; then
25585 echo \"foo bar\"
25586 ˇ
25587 "});
25588 cx.update_editor(|editor, window, cx| {
25589 editor.handle_input("elif", window, cx);
25590 });
25591 cx.assert_editor_state(indoc! {"
25592 if [ \"$1\" = \"test\" ]; then
25593 echo \"foo bar\"
25594 elifˇ
25595 "});
25596
25597 // test `fi` auto outdents when typed inside `else` block
25598 cx.set_state(indoc! {"
25599 if [ \"$1\" = \"test\" ]; then
25600 echo \"foo bar\"
25601 else
25602 echo \"bar baz\"
25603 ˇ
25604 "});
25605 cx.update_editor(|editor, window, cx| {
25606 editor.handle_input("fi", window, cx);
25607 });
25608 cx.assert_editor_state(indoc! {"
25609 if [ \"$1\" = \"test\" ]; then
25610 echo \"foo bar\"
25611 else
25612 echo \"bar baz\"
25613 fiˇ
25614 "});
25615
25616 // test `done` auto outdents when typed inside `while` block
25617 cx.set_state(indoc! {"
25618 while read line; do
25619 echo \"$line\"
25620 ˇ
25621 "});
25622 cx.update_editor(|editor, window, cx| {
25623 editor.handle_input("done", window, cx);
25624 });
25625 cx.assert_editor_state(indoc! {"
25626 while read line; do
25627 echo \"$line\"
25628 doneˇ
25629 "});
25630
25631 // test `done` auto outdents when typed inside `for` block
25632 cx.set_state(indoc! {"
25633 for file in *.txt; do
25634 cat \"$file\"
25635 ˇ
25636 "});
25637 cx.update_editor(|editor, window, cx| {
25638 editor.handle_input("done", window, cx);
25639 });
25640 cx.assert_editor_state(indoc! {"
25641 for file in *.txt; do
25642 cat \"$file\"
25643 doneˇ
25644 "});
25645
25646 // test `esac` auto outdents when typed inside `case` block
25647 cx.set_state(indoc! {"
25648 case \"$1\" in
25649 start)
25650 echo \"foo bar\"
25651 ;;
25652 stop)
25653 echo \"bar baz\"
25654 ;;
25655 ˇ
25656 "});
25657 cx.update_editor(|editor, window, cx| {
25658 editor.handle_input("esac", window, cx);
25659 });
25660 cx.assert_editor_state(indoc! {"
25661 case \"$1\" in
25662 start)
25663 echo \"foo bar\"
25664 ;;
25665 stop)
25666 echo \"bar baz\"
25667 ;;
25668 esacˇ
25669 "});
25670
25671 // test `*)` auto outdents when typed inside `case` block
25672 cx.set_state(indoc! {"
25673 case \"$1\" in
25674 start)
25675 echo \"foo bar\"
25676 ;;
25677 ˇ
25678 "});
25679 cx.update_editor(|editor, window, cx| {
25680 editor.handle_input("*)", window, cx);
25681 });
25682 cx.assert_editor_state(indoc! {"
25683 case \"$1\" in
25684 start)
25685 echo \"foo bar\"
25686 ;;
25687 *)ˇ
25688 "});
25689
25690 // test `fi` outdents to correct level with nested if blocks
25691 cx.set_state(indoc! {"
25692 if [ \"$1\" = \"test\" ]; then
25693 echo \"outer if\"
25694 if [ \"$2\" = \"debug\" ]; then
25695 echo \"inner if\"
25696 ˇ
25697 "});
25698 cx.update_editor(|editor, window, cx| {
25699 editor.handle_input("fi", window, cx);
25700 });
25701 cx.assert_editor_state(indoc! {"
25702 if [ \"$1\" = \"test\" ]; then
25703 echo \"outer if\"
25704 if [ \"$2\" = \"debug\" ]; then
25705 echo \"inner if\"
25706 fiˇ
25707 "});
25708}
25709
25710#[gpui::test]
25711async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25712 init_test(cx, |_| {});
25713 update_test_language_settings(cx, |settings| {
25714 settings.defaults.extend_comment_on_newline = Some(false);
25715 });
25716 let mut cx = EditorTestContext::new(cx).await;
25717 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25718 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25719
25720 // test correct indent after newline on comment
25721 cx.set_state(indoc! {"
25722 # COMMENT:ˇ
25723 "});
25724 cx.update_editor(|editor, window, cx| {
25725 editor.newline(&Newline, window, cx);
25726 });
25727 cx.assert_editor_state(indoc! {"
25728 # COMMENT:
25729 ˇ
25730 "});
25731
25732 // test correct indent after newline after `then`
25733 cx.set_state(indoc! {"
25734
25735 if [ \"$1\" = \"test\" ]; thenˇ
25736 "});
25737 cx.update_editor(|editor, window, cx| {
25738 editor.newline(&Newline, window, cx);
25739 });
25740 cx.run_until_parked();
25741 cx.assert_editor_state(indoc! {"
25742
25743 if [ \"$1\" = \"test\" ]; then
25744 ˇ
25745 "});
25746
25747 // test correct indent after newline after `else`
25748 cx.set_state(indoc! {"
25749 if [ \"$1\" = \"test\" ]; then
25750 elseˇ
25751 "});
25752 cx.update_editor(|editor, window, cx| {
25753 editor.newline(&Newline, window, cx);
25754 });
25755 cx.run_until_parked();
25756 cx.assert_editor_state(indoc! {"
25757 if [ \"$1\" = \"test\" ]; then
25758 else
25759 ˇ
25760 "});
25761
25762 // test correct indent after newline after `elif`
25763 cx.set_state(indoc! {"
25764 if [ \"$1\" = \"test\" ]; then
25765 elifˇ
25766 "});
25767 cx.update_editor(|editor, window, cx| {
25768 editor.newline(&Newline, window, cx);
25769 });
25770 cx.run_until_parked();
25771 cx.assert_editor_state(indoc! {"
25772 if [ \"$1\" = \"test\" ]; then
25773 elif
25774 ˇ
25775 "});
25776
25777 // test correct indent after newline after `do`
25778 cx.set_state(indoc! {"
25779 for file in *.txt; doˇ
25780 "});
25781 cx.update_editor(|editor, window, cx| {
25782 editor.newline(&Newline, window, cx);
25783 });
25784 cx.run_until_parked();
25785 cx.assert_editor_state(indoc! {"
25786 for file in *.txt; do
25787 ˇ
25788 "});
25789
25790 // test correct indent after newline after case pattern
25791 cx.set_state(indoc! {"
25792 case \"$1\" in
25793 start)ˇ
25794 "});
25795 cx.update_editor(|editor, window, cx| {
25796 editor.newline(&Newline, window, cx);
25797 });
25798 cx.run_until_parked();
25799 cx.assert_editor_state(indoc! {"
25800 case \"$1\" in
25801 start)
25802 ˇ
25803 "});
25804
25805 // test correct indent after newline after case pattern
25806 cx.set_state(indoc! {"
25807 case \"$1\" in
25808 start)
25809 ;;
25810 *)ˇ
25811 "});
25812 cx.update_editor(|editor, window, cx| {
25813 editor.newline(&Newline, window, cx);
25814 });
25815 cx.run_until_parked();
25816 cx.assert_editor_state(indoc! {"
25817 case \"$1\" in
25818 start)
25819 ;;
25820 *)
25821 ˇ
25822 "});
25823
25824 // test correct indent after newline after function opening brace
25825 cx.set_state(indoc! {"
25826 function test() {ˇ}
25827 "});
25828 cx.update_editor(|editor, window, cx| {
25829 editor.newline(&Newline, window, cx);
25830 });
25831 cx.run_until_parked();
25832 cx.assert_editor_state(indoc! {"
25833 function test() {
25834 ˇ
25835 }
25836 "});
25837
25838 // test no extra indent after semicolon on same line
25839 cx.set_state(indoc! {"
25840 echo \"test\";ˇ
25841 "});
25842 cx.update_editor(|editor, window, cx| {
25843 editor.newline(&Newline, window, cx);
25844 });
25845 cx.run_until_parked();
25846 cx.assert_editor_state(indoc! {"
25847 echo \"test\";
25848 ˇ
25849 "});
25850}
25851
25852fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25853 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25854 point..point
25855}
25856
25857#[track_caller]
25858fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25859 let (text, ranges) = marked_text_ranges(marked_text, true);
25860 assert_eq!(editor.text(cx), text);
25861 assert_eq!(
25862 editor.selections.ranges(&editor.display_snapshot(cx)),
25863 ranges
25864 .iter()
25865 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
25866 .collect::<Vec<_>>(),
25867 "Assert selections are {}",
25868 marked_text
25869 );
25870}
25871
25872pub fn handle_signature_help_request(
25873 cx: &mut EditorLspTestContext,
25874 mocked_response: lsp::SignatureHelp,
25875) -> impl Future<Output = ()> + use<> {
25876 let mut request =
25877 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25878 let mocked_response = mocked_response.clone();
25879 async move { Ok(Some(mocked_response)) }
25880 });
25881
25882 async move {
25883 request.next().await;
25884 }
25885}
25886
25887#[track_caller]
25888pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25889 cx.update_editor(|editor, _, _| {
25890 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25891 let entries = menu.entries.borrow();
25892 let entries = entries
25893 .iter()
25894 .map(|entry| entry.string.as_str())
25895 .collect::<Vec<_>>();
25896 assert_eq!(entries, expected);
25897 } else {
25898 panic!("Expected completions menu");
25899 }
25900 });
25901}
25902
25903#[gpui::test]
25904async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
25905 init_test(cx, |_| {});
25906 let mut cx = EditorLspTestContext::new_rust(
25907 lsp::ServerCapabilities {
25908 completion_provider: Some(lsp::CompletionOptions {
25909 ..Default::default()
25910 }),
25911 ..Default::default()
25912 },
25913 cx,
25914 )
25915 .await;
25916 cx.lsp
25917 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25918 Ok(Some(lsp::CompletionResponse::Array(vec![
25919 lsp::CompletionItem {
25920 label: "unsafe".into(),
25921 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25922 range: lsp::Range {
25923 start: lsp::Position {
25924 line: 0,
25925 character: 9,
25926 },
25927 end: lsp::Position {
25928 line: 0,
25929 character: 11,
25930 },
25931 },
25932 new_text: "unsafe".to_string(),
25933 })),
25934 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
25935 ..Default::default()
25936 },
25937 ])))
25938 });
25939
25940 cx.update_editor(|editor, _, cx| {
25941 editor.project().unwrap().update(cx, |project, cx| {
25942 project.snippets().update(cx, |snippets, _cx| {
25943 snippets.add_snippet_for_test(
25944 None,
25945 PathBuf::from("test_snippets.json"),
25946 vec![
25947 Arc::new(project::snippet_provider::Snippet {
25948 prefix: vec![
25949 "unlimited word count".to_string(),
25950 "unlimit word count".to_string(),
25951 "unlimited unknown".to_string(),
25952 ],
25953 body: "this is many words".to_string(),
25954 description: Some("description".to_string()),
25955 name: "multi-word snippet test".to_string(),
25956 }),
25957 Arc::new(project::snippet_provider::Snippet {
25958 prefix: vec!["unsnip".to_string(), "@few".to_string()],
25959 body: "fewer words".to_string(),
25960 description: Some("alt description".to_string()),
25961 name: "other name".to_string(),
25962 }),
25963 Arc::new(project::snippet_provider::Snippet {
25964 prefix: vec!["ab aa".to_string()],
25965 body: "abcd".to_string(),
25966 description: None,
25967 name: "alphabet".to_string(),
25968 }),
25969 ],
25970 );
25971 });
25972 })
25973 });
25974
25975 let get_completions = |cx: &mut EditorLspTestContext| {
25976 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
25977 Some(CodeContextMenu::Completions(context_menu)) => {
25978 let entries = context_menu.entries.borrow();
25979 entries
25980 .iter()
25981 .map(|entry| entry.string.clone())
25982 .collect_vec()
25983 }
25984 _ => vec![],
25985 })
25986 };
25987
25988 // snippets:
25989 // @foo
25990 // foo bar
25991 //
25992 // when typing:
25993 //
25994 // when typing:
25995 // - if I type a symbol "open the completions with snippets only"
25996 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
25997 //
25998 // stuff we need:
25999 // - filtering logic change?
26000 // - remember how far back the completion started.
26001
26002 let test_cases: &[(&str, &[&str])] = &[
26003 (
26004 "un",
26005 &[
26006 "unsafe",
26007 "unlimit word count",
26008 "unlimited unknown",
26009 "unlimited word count",
26010 "unsnip",
26011 ],
26012 ),
26013 (
26014 "u ",
26015 &[
26016 "unlimit word count",
26017 "unlimited unknown",
26018 "unlimited word count",
26019 ],
26020 ),
26021 ("u a", &["ab aa", "unsafe"]), // unsAfe
26022 (
26023 "u u",
26024 &[
26025 "unsafe",
26026 "unlimit word count",
26027 "unlimited unknown", // ranked highest among snippets
26028 "unlimited word count",
26029 "unsnip",
26030 ],
26031 ),
26032 ("uw c", &["unlimit word count", "unlimited word count"]),
26033 (
26034 "u w",
26035 &[
26036 "unlimit word count",
26037 "unlimited word count",
26038 "unlimited unknown",
26039 ],
26040 ),
26041 ("u w ", &["unlimit word count", "unlimited word count"]),
26042 (
26043 "u ",
26044 &[
26045 "unlimit word count",
26046 "unlimited unknown",
26047 "unlimited word count",
26048 ],
26049 ),
26050 ("wor", &[]),
26051 ("uf", &["unsafe"]),
26052 ("af", &["unsafe"]),
26053 ("afu", &[]),
26054 (
26055 "ue",
26056 &["unsafe", "unlimited unknown", "unlimited word count"],
26057 ),
26058 ("@", &["@few"]),
26059 ("@few", &["@few"]),
26060 ("@ ", &[]),
26061 ("a@", &["@few"]),
26062 ("a@f", &["@few", "unsafe"]),
26063 ("a@fw", &["@few"]),
26064 ("a", &["ab aa", "unsafe"]),
26065 ("aa", &["ab aa"]),
26066 ("aaa", &["ab aa"]),
26067 ("ab", &["ab aa"]),
26068 ("ab ", &["ab aa"]),
26069 ("ab a", &["ab aa", "unsafe"]),
26070 ("ab ab", &["ab aa"]),
26071 ("ab ab aa", &["ab aa"]),
26072 ];
26073
26074 for &(input_to_simulate, expected_completions) in test_cases {
26075 cx.set_state("fn a() { ˇ }\n");
26076 for c in input_to_simulate.split("") {
26077 cx.simulate_input(c);
26078 cx.run_until_parked();
26079 }
26080 let expected_completions = expected_completions
26081 .iter()
26082 .map(|s| s.to_string())
26083 .collect_vec();
26084 assert_eq!(
26085 get_completions(&mut cx),
26086 expected_completions,
26087 "< actual / expected >, input = {input_to_simulate:?}",
26088 );
26089 }
26090}
26091
26092/// Handle completion request passing a marked string specifying where the completion
26093/// should be triggered from using '|' character, what range should be replaced, and what completions
26094/// should be returned using '<' and '>' to delimit the range.
26095///
26096/// Also see `handle_completion_request_with_insert_and_replace`.
26097#[track_caller]
26098pub fn handle_completion_request(
26099 marked_string: &str,
26100 completions: Vec<&'static str>,
26101 is_incomplete: bool,
26102 counter: Arc<AtomicUsize>,
26103 cx: &mut EditorLspTestContext,
26104) -> impl Future<Output = ()> {
26105 let complete_from_marker: TextRangeMarker = '|'.into();
26106 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26107 let (_, mut marked_ranges) = marked_text_ranges_by(
26108 marked_string,
26109 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26110 );
26111
26112 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26113 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26114 ));
26115 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26116 let replace_range =
26117 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26118
26119 let mut request =
26120 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26121 let completions = completions.clone();
26122 counter.fetch_add(1, atomic::Ordering::Release);
26123 async move {
26124 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26125 assert_eq!(
26126 params.text_document_position.position,
26127 complete_from_position
26128 );
26129 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26130 is_incomplete,
26131 item_defaults: None,
26132 items: completions
26133 .iter()
26134 .map(|completion_text| lsp::CompletionItem {
26135 label: completion_text.to_string(),
26136 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26137 range: replace_range,
26138 new_text: completion_text.to_string(),
26139 })),
26140 ..Default::default()
26141 })
26142 .collect(),
26143 })))
26144 }
26145 });
26146
26147 async move {
26148 request.next().await;
26149 }
26150}
26151
26152/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26153/// given instead, which also contains an `insert` range.
26154///
26155/// This function uses markers to define ranges:
26156/// - `|` marks the cursor position
26157/// - `<>` marks the replace range
26158/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26159pub fn handle_completion_request_with_insert_and_replace(
26160 cx: &mut EditorLspTestContext,
26161 marked_string: &str,
26162 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26163 counter: Arc<AtomicUsize>,
26164) -> impl Future<Output = ()> {
26165 let complete_from_marker: TextRangeMarker = '|'.into();
26166 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26167 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26168
26169 let (_, mut marked_ranges) = marked_text_ranges_by(
26170 marked_string,
26171 vec![
26172 complete_from_marker.clone(),
26173 replace_range_marker.clone(),
26174 insert_range_marker.clone(),
26175 ],
26176 );
26177
26178 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26179 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26180 ));
26181 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26182 let replace_range =
26183 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26184
26185 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26186 Some(ranges) if !ranges.is_empty() => {
26187 let range1 = ranges[0].clone();
26188 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26189 }
26190 _ => lsp::Range {
26191 start: replace_range.start,
26192 end: complete_from_position,
26193 },
26194 };
26195
26196 let mut request =
26197 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26198 let completions = completions.clone();
26199 counter.fetch_add(1, atomic::Ordering::Release);
26200 async move {
26201 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26202 assert_eq!(
26203 params.text_document_position.position, complete_from_position,
26204 "marker `|` position doesn't match",
26205 );
26206 Ok(Some(lsp::CompletionResponse::Array(
26207 completions
26208 .iter()
26209 .map(|(label, new_text)| lsp::CompletionItem {
26210 label: label.to_string(),
26211 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26212 lsp::InsertReplaceEdit {
26213 insert: insert_range,
26214 replace: replace_range,
26215 new_text: new_text.to_string(),
26216 },
26217 )),
26218 ..Default::default()
26219 })
26220 .collect(),
26221 )))
26222 }
26223 });
26224
26225 async move {
26226 request.next().await;
26227 }
26228}
26229
26230fn handle_resolve_completion_request(
26231 cx: &mut EditorLspTestContext,
26232 edits: Option<Vec<(&'static str, &'static str)>>,
26233) -> impl Future<Output = ()> {
26234 let edits = edits.map(|edits| {
26235 edits
26236 .iter()
26237 .map(|(marked_string, new_text)| {
26238 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26239 let replace_range = cx.to_lsp_range(
26240 MultiBufferOffset(marked_ranges[0].start)
26241 ..MultiBufferOffset(marked_ranges[0].end),
26242 );
26243 lsp::TextEdit::new(replace_range, new_text.to_string())
26244 })
26245 .collect::<Vec<_>>()
26246 });
26247
26248 let mut request =
26249 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26250 let edits = edits.clone();
26251 async move {
26252 Ok(lsp::CompletionItem {
26253 additional_text_edits: edits,
26254 ..Default::default()
26255 })
26256 }
26257 });
26258
26259 async move {
26260 request.next().await;
26261 }
26262}
26263
26264pub(crate) fn update_test_language_settings(
26265 cx: &mut TestAppContext,
26266 f: impl Fn(&mut AllLanguageSettingsContent),
26267) {
26268 cx.update(|cx| {
26269 SettingsStore::update_global(cx, |store, cx| {
26270 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26271 });
26272 });
26273}
26274
26275pub(crate) fn update_test_project_settings(
26276 cx: &mut TestAppContext,
26277 f: impl Fn(&mut ProjectSettingsContent),
26278) {
26279 cx.update(|cx| {
26280 SettingsStore::update_global(cx, |store, cx| {
26281 store.update_user_settings(cx, |settings| f(&mut settings.project));
26282 });
26283 });
26284}
26285
26286pub(crate) fn update_test_editor_settings(
26287 cx: &mut TestAppContext,
26288 f: impl Fn(&mut EditorSettingsContent),
26289) {
26290 cx.update(|cx| {
26291 SettingsStore::update_global(cx, |store, cx| {
26292 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26293 })
26294 })
26295}
26296
26297pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26298 cx.update(|cx| {
26299 assets::Assets.load_test_fonts(cx);
26300 let store = SettingsStore::test(cx);
26301 cx.set_global(store);
26302 theme::init(theme::LoadThemes::JustBase, cx);
26303 release_channel::init(semver::Version::new(0, 0, 0), cx);
26304 crate::init(cx);
26305 });
26306 zlog::init_test();
26307 update_test_language_settings(cx, f);
26308}
26309
26310#[track_caller]
26311fn assert_hunk_revert(
26312 not_reverted_text_with_selections: &str,
26313 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26314 expected_reverted_text_with_selections: &str,
26315 base_text: &str,
26316 cx: &mut EditorLspTestContext,
26317) {
26318 cx.set_state(not_reverted_text_with_selections);
26319 cx.set_head_text(base_text);
26320 cx.executor().run_until_parked();
26321
26322 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26323 let snapshot = editor.snapshot(window, cx);
26324 let reverted_hunk_statuses = snapshot
26325 .buffer_snapshot()
26326 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26327 .map(|hunk| hunk.status().kind)
26328 .collect::<Vec<_>>();
26329
26330 editor.git_restore(&Default::default(), window, cx);
26331 reverted_hunk_statuses
26332 });
26333 cx.executor().run_until_parked();
26334 cx.assert_editor_state(expected_reverted_text_with_selections);
26335 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26336}
26337
26338#[gpui::test(iterations = 10)]
26339async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26340 init_test(cx, |_| {});
26341
26342 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26343 let counter = diagnostic_requests.clone();
26344
26345 let fs = FakeFs::new(cx.executor());
26346 fs.insert_tree(
26347 path!("/a"),
26348 json!({
26349 "first.rs": "fn main() { let a = 5; }",
26350 "second.rs": "// Test file",
26351 }),
26352 )
26353 .await;
26354
26355 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26356 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26357 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26358
26359 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26360 language_registry.add(rust_lang());
26361 let mut fake_servers = language_registry.register_fake_lsp(
26362 "Rust",
26363 FakeLspAdapter {
26364 capabilities: lsp::ServerCapabilities {
26365 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26366 lsp::DiagnosticOptions {
26367 identifier: None,
26368 inter_file_dependencies: true,
26369 workspace_diagnostics: true,
26370 work_done_progress_options: Default::default(),
26371 },
26372 )),
26373 ..Default::default()
26374 },
26375 ..Default::default()
26376 },
26377 );
26378
26379 let editor = workspace
26380 .update(cx, |workspace, window, cx| {
26381 workspace.open_abs_path(
26382 PathBuf::from(path!("/a/first.rs")),
26383 OpenOptions::default(),
26384 window,
26385 cx,
26386 )
26387 })
26388 .unwrap()
26389 .await
26390 .unwrap()
26391 .downcast::<Editor>()
26392 .unwrap();
26393 let fake_server = fake_servers.next().await.unwrap();
26394 let server_id = fake_server.server.server_id();
26395 let mut first_request = fake_server
26396 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26397 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26398 let result_id = Some(new_result_id.to_string());
26399 assert_eq!(
26400 params.text_document.uri,
26401 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26402 );
26403 async move {
26404 Ok(lsp::DocumentDiagnosticReportResult::Report(
26405 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26406 related_documents: None,
26407 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26408 items: Vec::new(),
26409 result_id,
26410 },
26411 }),
26412 ))
26413 }
26414 });
26415
26416 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
26417 project.update(cx, |project, cx| {
26418 let buffer_id = editor
26419 .read(cx)
26420 .buffer()
26421 .read(cx)
26422 .as_singleton()
26423 .expect("created a singleton buffer")
26424 .read(cx)
26425 .remote_id();
26426 let buffer_result_id = project
26427 .lsp_store()
26428 .read(cx)
26429 .result_id(server_id, buffer_id, cx);
26430 assert_eq!(expected, buffer_result_id);
26431 });
26432 };
26433
26434 ensure_result_id(None, cx);
26435 cx.executor().advance_clock(Duration::from_millis(60));
26436 cx.executor().run_until_parked();
26437 assert_eq!(
26438 diagnostic_requests.load(atomic::Ordering::Acquire),
26439 1,
26440 "Opening file should trigger diagnostic request"
26441 );
26442 first_request
26443 .next()
26444 .await
26445 .expect("should have sent the first diagnostics pull request");
26446 ensure_result_id(Some("1".to_string()), cx);
26447
26448 // Editing should trigger diagnostics
26449 editor.update_in(cx, |editor, window, cx| {
26450 editor.handle_input("2", window, cx)
26451 });
26452 cx.executor().advance_clock(Duration::from_millis(60));
26453 cx.executor().run_until_parked();
26454 assert_eq!(
26455 diagnostic_requests.load(atomic::Ordering::Acquire),
26456 2,
26457 "Editing should trigger diagnostic request"
26458 );
26459 ensure_result_id(Some("2".to_string()), cx);
26460
26461 // Moving cursor should not trigger diagnostic request
26462 editor.update_in(cx, |editor, window, cx| {
26463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26464 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26465 });
26466 });
26467 cx.executor().advance_clock(Duration::from_millis(60));
26468 cx.executor().run_until_parked();
26469 assert_eq!(
26470 diagnostic_requests.load(atomic::Ordering::Acquire),
26471 2,
26472 "Cursor movement should not trigger diagnostic request"
26473 );
26474 ensure_result_id(Some("2".to_string()), cx);
26475 // Multiple rapid edits should be debounced
26476 for _ in 0..5 {
26477 editor.update_in(cx, |editor, window, cx| {
26478 editor.handle_input("x", window, cx)
26479 });
26480 }
26481 cx.executor().advance_clock(Duration::from_millis(60));
26482 cx.executor().run_until_parked();
26483
26484 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26485 assert!(
26486 final_requests <= 4,
26487 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26488 );
26489 ensure_result_id(Some(final_requests.to_string()), cx);
26490}
26491
26492#[gpui::test]
26493async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26494 // Regression test for issue #11671
26495 // Previously, adding a cursor after moving multiple cursors would reset
26496 // the cursor count instead of adding to the existing cursors.
26497 init_test(cx, |_| {});
26498 let mut cx = EditorTestContext::new(cx).await;
26499
26500 // Create a simple buffer with cursor at start
26501 cx.set_state(indoc! {"
26502 ˇaaaa
26503 bbbb
26504 cccc
26505 dddd
26506 eeee
26507 ffff
26508 gggg
26509 hhhh"});
26510
26511 // Add 2 cursors below (so we have 3 total)
26512 cx.update_editor(|editor, window, cx| {
26513 editor.add_selection_below(&Default::default(), window, cx);
26514 editor.add_selection_below(&Default::default(), window, cx);
26515 });
26516
26517 // Verify we have 3 cursors
26518 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26519 assert_eq!(
26520 initial_count, 3,
26521 "Should have 3 cursors after adding 2 below"
26522 );
26523
26524 // Move down one line
26525 cx.update_editor(|editor, window, cx| {
26526 editor.move_down(&MoveDown, window, cx);
26527 });
26528
26529 // Add another cursor below
26530 cx.update_editor(|editor, window, cx| {
26531 editor.add_selection_below(&Default::default(), window, cx);
26532 });
26533
26534 // Should now have 4 cursors (3 original + 1 new)
26535 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26536 assert_eq!(
26537 final_count, 4,
26538 "Should have 4 cursors after moving and adding another"
26539 );
26540}
26541
26542#[gpui::test]
26543async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26544 init_test(cx, |_| {});
26545
26546 let mut cx = EditorTestContext::new(cx).await;
26547
26548 cx.set_state(indoc!(
26549 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26550 Second line here"#
26551 ));
26552
26553 cx.update_editor(|editor, window, cx| {
26554 // Enable soft wrapping with a narrow width to force soft wrapping and
26555 // confirm that more than 2 rows are being displayed.
26556 editor.set_wrap_width(Some(100.0.into()), cx);
26557 assert!(editor.display_text(cx).lines().count() > 2);
26558
26559 editor.add_selection_below(
26560 &AddSelectionBelow {
26561 skip_soft_wrap: true,
26562 },
26563 window,
26564 cx,
26565 );
26566
26567 assert_eq!(
26568 display_ranges(editor, cx),
26569 &[
26570 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26571 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26572 ]
26573 );
26574
26575 editor.add_selection_above(
26576 &AddSelectionAbove {
26577 skip_soft_wrap: true,
26578 },
26579 window,
26580 cx,
26581 );
26582
26583 assert_eq!(
26584 display_ranges(editor, cx),
26585 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26586 );
26587
26588 editor.add_selection_below(
26589 &AddSelectionBelow {
26590 skip_soft_wrap: false,
26591 },
26592 window,
26593 cx,
26594 );
26595
26596 assert_eq!(
26597 display_ranges(editor, cx),
26598 &[
26599 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26600 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26601 ]
26602 );
26603
26604 editor.add_selection_above(
26605 &AddSelectionAbove {
26606 skip_soft_wrap: false,
26607 },
26608 window,
26609 cx,
26610 );
26611
26612 assert_eq!(
26613 display_ranges(editor, cx),
26614 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26615 );
26616 });
26617}
26618
26619#[gpui::test(iterations = 10)]
26620async fn test_document_colors(cx: &mut TestAppContext) {
26621 let expected_color = Rgba {
26622 r: 0.33,
26623 g: 0.33,
26624 b: 0.33,
26625 a: 0.33,
26626 };
26627
26628 init_test(cx, |_| {});
26629
26630 let fs = FakeFs::new(cx.executor());
26631 fs.insert_tree(
26632 path!("/a"),
26633 json!({
26634 "first.rs": "fn main() { let a = 5; }",
26635 }),
26636 )
26637 .await;
26638
26639 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26640 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26641 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26642
26643 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26644 language_registry.add(rust_lang());
26645 let mut fake_servers = language_registry.register_fake_lsp(
26646 "Rust",
26647 FakeLspAdapter {
26648 capabilities: lsp::ServerCapabilities {
26649 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26650 ..lsp::ServerCapabilities::default()
26651 },
26652 name: "rust-analyzer",
26653 ..FakeLspAdapter::default()
26654 },
26655 );
26656 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26657 "Rust",
26658 FakeLspAdapter {
26659 capabilities: lsp::ServerCapabilities {
26660 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26661 ..lsp::ServerCapabilities::default()
26662 },
26663 name: "not-rust-analyzer",
26664 ..FakeLspAdapter::default()
26665 },
26666 );
26667
26668 let editor = workspace
26669 .update(cx, |workspace, window, cx| {
26670 workspace.open_abs_path(
26671 PathBuf::from(path!("/a/first.rs")),
26672 OpenOptions::default(),
26673 window,
26674 cx,
26675 )
26676 })
26677 .unwrap()
26678 .await
26679 .unwrap()
26680 .downcast::<Editor>()
26681 .unwrap();
26682 let fake_language_server = fake_servers.next().await.unwrap();
26683 let fake_language_server_without_capabilities =
26684 fake_servers_without_capabilities.next().await.unwrap();
26685 let requests_made = Arc::new(AtomicUsize::new(0));
26686 let closure_requests_made = Arc::clone(&requests_made);
26687 let mut color_request_handle = fake_language_server
26688 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26689 let requests_made = Arc::clone(&closure_requests_made);
26690 async move {
26691 assert_eq!(
26692 params.text_document.uri,
26693 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26694 );
26695 requests_made.fetch_add(1, atomic::Ordering::Release);
26696 Ok(vec![
26697 lsp::ColorInformation {
26698 range: lsp::Range {
26699 start: lsp::Position {
26700 line: 0,
26701 character: 0,
26702 },
26703 end: lsp::Position {
26704 line: 0,
26705 character: 1,
26706 },
26707 },
26708 color: lsp::Color {
26709 red: 0.33,
26710 green: 0.33,
26711 blue: 0.33,
26712 alpha: 0.33,
26713 },
26714 },
26715 lsp::ColorInformation {
26716 range: lsp::Range {
26717 start: lsp::Position {
26718 line: 0,
26719 character: 0,
26720 },
26721 end: lsp::Position {
26722 line: 0,
26723 character: 1,
26724 },
26725 },
26726 color: lsp::Color {
26727 red: 0.33,
26728 green: 0.33,
26729 blue: 0.33,
26730 alpha: 0.33,
26731 },
26732 },
26733 ])
26734 }
26735 });
26736
26737 let _handle = fake_language_server_without_capabilities
26738 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26739 panic!("Should not be called");
26740 });
26741 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26742 color_request_handle.next().await.unwrap();
26743 cx.run_until_parked();
26744 assert_eq!(
26745 1,
26746 requests_made.load(atomic::Ordering::Acquire),
26747 "Should query for colors once per editor open"
26748 );
26749 editor.update_in(cx, |editor, _, cx| {
26750 assert_eq!(
26751 vec![expected_color],
26752 extract_color_inlays(editor, cx),
26753 "Should have an initial inlay"
26754 );
26755 });
26756
26757 // opening another file in a split should not influence the LSP query counter
26758 workspace
26759 .update(cx, |workspace, window, cx| {
26760 assert_eq!(
26761 workspace.panes().len(),
26762 1,
26763 "Should have one pane with one editor"
26764 );
26765 workspace.move_item_to_pane_in_direction(
26766 &MoveItemToPaneInDirection {
26767 direction: SplitDirection::Right,
26768 focus: false,
26769 clone: true,
26770 },
26771 window,
26772 cx,
26773 );
26774 })
26775 .unwrap();
26776 cx.run_until_parked();
26777 workspace
26778 .update(cx, |workspace, _, cx| {
26779 let panes = workspace.panes();
26780 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26781 for pane in panes {
26782 let editor = pane
26783 .read(cx)
26784 .active_item()
26785 .and_then(|item| item.downcast::<Editor>())
26786 .expect("Should have opened an editor in each split");
26787 let editor_file = editor
26788 .read(cx)
26789 .buffer()
26790 .read(cx)
26791 .as_singleton()
26792 .expect("test deals with singleton buffers")
26793 .read(cx)
26794 .file()
26795 .expect("test buffese should have a file")
26796 .path();
26797 assert_eq!(
26798 editor_file.as_ref(),
26799 rel_path("first.rs"),
26800 "Both editors should be opened for the same file"
26801 )
26802 }
26803 })
26804 .unwrap();
26805
26806 cx.executor().advance_clock(Duration::from_millis(500));
26807 let save = editor.update_in(cx, |editor, window, cx| {
26808 editor.move_to_end(&MoveToEnd, window, cx);
26809 editor.handle_input("dirty", window, cx);
26810 editor.save(
26811 SaveOptions {
26812 format: true,
26813 autosave: true,
26814 },
26815 project.clone(),
26816 window,
26817 cx,
26818 )
26819 });
26820 save.await.unwrap();
26821
26822 color_request_handle.next().await.unwrap();
26823 cx.run_until_parked();
26824 assert_eq!(
26825 2,
26826 requests_made.load(atomic::Ordering::Acquire),
26827 "Should query for colors once per save (deduplicated) and once per formatting after save"
26828 );
26829
26830 drop(editor);
26831 let close = workspace
26832 .update(cx, |workspace, window, cx| {
26833 workspace.active_pane().update(cx, |pane, cx| {
26834 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26835 })
26836 })
26837 .unwrap();
26838 close.await.unwrap();
26839 let close = workspace
26840 .update(cx, |workspace, window, cx| {
26841 workspace.active_pane().update(cx, |pane, cx| {
26842 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26843 })
26844 })
26845 .unwrap();
26846 close.await.unwrap();
26847 assert_eq!(
26848 2,
26849 requests_made.load(atomic::Ordering::Acquire),
26850 "After saving and closing all editors, no extra requests should be made"
26851 );
26852 workspace
26853 .update(cx, |workspace, _, cx| {
26854 assert!(
26855 workspace.active_item(cx).is_none(),
26856 "Should close all editors"
26857 )
26858 })
26859 .unwrap();
26860
26861 workspace
26862 .update(cx, |workspace, window, cx| {
26863 workspace.active_pane().update(cx, |pane, cx| {
26864 pane.navigate_backward(&workspace::GoBack, window, cx);
26865 })
26866 })
26867 .unwrap();
26868 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26869 cx.run_until_parked();
26870 let editor = workspace
26871 .update(cx, |workspace, _, cx| {
26872 workspace
26873 .active_item(cx)
26874 .expect("Should have reopened the editor again after navigating back")
26875 .downcast::<Editor>()
26876 .expect("Should be an editor")
26877 })
26878 .unwrap();
26879
26880 assert_eq!(
26881 2,
26882 requests_made.load(atomic::Ordering::Acquire),
26883 "Cache should be reused on buffer close and reopen"
26884 );
26885 editor.update(cx, |editor, cx| {
26886 assert_eq!(
26887 vec![expected_color],
26888 extract_color_inlays(editor, cx),
26889 "Should have an initial inlay"
26890 );
26891 });
26892
26893 drop(color_request_handle);
26894 let closure_requests_made = Arc::clone(&requests_made);
26895 let mut empty_color_request_handle = fake_language_server
26896 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26897 let requests_made = Arc::clone(&closure_requests_made);
26898 async move {
26899 assert_eq!(
26900 params.text_document.uri,
26901 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26902 );
26903 requests_made.fetch_add(1, atomic::Ordering::Release);
26904 Ok(Vec::new())
26905 }
26906 });
26907 let save = editor.update_in(cx, |editor, window, cx| {
26908 editor.move_to_end(&MoveToEnd, window, cx);
26909 editor.handle_input("dirty_again", window, cx);
26910 editor.save(
26911 SaveOptions {
26912 format: false,
26913 autosave: true,
26914 },
26915 project.clone(),
26916 window,
26917 cx,
26918 )
26919 });
26920 save.await.unwrap();
26921
26922 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26923 empty_color_request_handle.next().await.unwrap();
26924 cx.run_until_parked();
26925 assert_eq!(
26926 3,
26927 requests_made.load(atomic::Ordering::Acquire),
26928 "Should query for colors once per save only, as formatting was not requested"
26929 );
26930 editor.update(cx, |editor, cx| {
26931 assert_eq!(
26932 Vec::<Rgba>::new(),
26933 extract_color_inlays(editor, cx),
26934 "Should clear all colors when the server returns an empty response"
26935 );
26936 });
26937}
26938
26939#[gpui::test]
26940async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26941 init_test(cx, |_| {});
26942 let (editor, cx) = cx.add_window_view(Editor::single_line);
26943 editor.update_in(cx, |editor, window, cx| {
26944 editor.set_text("oops\n\nwow\n", window, cx)
26945 });
26946 cx.run_until_parked();
26947 editor.update(cx, |editor, cx| {
26948 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26949 });
26950 editor.update(cx, |editor, cx| {
26951 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
26952 });
26953 cx.run_until_parked();
26954 editor.update(cx, |editor, cx| {
26955 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26956 });
26957}
26958
26959#[gpui::test]
26960async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26961 init_test(cx, |_| {});
26962
26963 cx.update(|cx| {
26964 register_project_item::<Editor>(cx);
26965 });
26966
26967 let fs = FakeFs::new(cx.executor());
26968 fs.insert_tree("/root1", json!({})).await;
26969 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26970 .await;
26971
26972 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26973 let (workspace, cx) =
26974 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26975
26976 let worktree_id = project.update(cx, |project, cx| {
26977 project.worktrees(cx).next().unwrap().read(cx).id()
26978 });
26979
26980 let handle = workspace
26981 .update_in(cx, |workspace, window, cx| {
26982 let project_path = (worktree_id, rel_path("one.pdf"));
26983 workspace.open_path(project_path, None, true, window, cx)
26984 })
26985 .await
26986 .unwrap();
26987
26988 assert_eq!(
26989 handle.to_any_view().entity_type(),
26990 TypeId::of::<InvalidItemView>()
26991 );
26992}
26993
26994#[gpui::test]
26995async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26996 init_test(cx, |_| {});
26997
26998 let language = Arc::new(Language::new(
26999 LanguageConfig::default(),
27000 Some(tree_sitter_rust::LANGUAGE.into()),
27001 ));
27002
27003 // Test hierarchical sibling navigation
27004 let text = r#"
27005 fn outer() {
27006 if condition {
27007 let a = 1;
27008 }
27009 let b = 2;
27010 }
27011
27012 fn another() {
27013 let c = 3;
27014 }
27015 "#;
27016
27017 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27018 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27019 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27020
27021 // Wait for parsing to complete
27022 editor
27023 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27024 .await;
27025
27026 editor.update_in(cx, |editor, window, cx| {
27027 // Start by selecting "let a = 1;" inside the if block
27028 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27029 s.select_display_ranges([
27030 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27031 ]);
27032 });
27033
27034 let initial_selection = editor
27035 .selections
27036 .display_ranges(&editor.display_snapshot(cx));
27037 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27038
27039 // Test select next sibling - should move up levels to find the next sibling
27040 // Since "let a = 1;" has no siblings in the if block, it should move up
27041 // to find "let b = 2;" which is a sibling of the if block
27042 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27043 let next_selection = editor
27044 .selections
27045 .display_ranges(&editor.display_snapshot(cx));
27046
27047 // Should have a selection and it should be different from the initial
27048 assert_eq!(
27049 next_selection.len(),
27050 1,
27051 "Should have one selection after next"
27052 );
27053 assert_ne!(
27054 next_selection[0], initial_selection[0],
27055 "Next sibling selection should be different"
27056 );
27057
27058 // Test hierarchical navigation by going to the end of the current function
27059 // and trying to navigate to the next function
27060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27061 s.select_display_ranges([
27062 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27063 ]);
27064 });
27065
27066 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27067 let function_next_selection = editor
27068 .selections
27069 .display_ranges(&editor.display_snapshot(cx));
27070
27071 // Should move to the next function
27072 assert_eq!(
27073 function_next_selection.len(),
27074 1,
27075 "Should have one selection after function next"
27076 );
27077
27078 // Test select previous sibling navigation
27079 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27080 let prev_selection = editor
27081 .selections
27082 .display_ranges(&editor.display_snapshot(cx));
27083
27084 // Should have a selection and it should be different
27085 assert_eq!(
27086 prev_selection.len(),
27087 1,
27088 "Should have one selection after prev"
27089 );
27090 assert_ne!(
27091 prev_selection[0], function_next_selection[0],
27092 "Previous sibling selection should be different from next"
27093 );
27094 });
27095}
27096
27097#[gpui::test]
27098async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27099 init_test(cx, |_| {});
27100
27101 let mut cx = EditorTestContext::new(cx).await;
27102 cx.set_state(
27103 "let ˇvariable = 42;
27104let another = variable + 1;
27105let result = variable * 2;",
27106 );
27107
27108 // Set up document highlights manually (simulating LSP response)
27109 cx.update_editor(|editor, _window, cx| {
27110 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27111
27112 // Create highlights for "variable" occurrences
27113 let highlight_ranges = [
27114 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27115 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27116 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27117 ];
27118
27119 let anchor_ranges: Vec<_> = highlight_ranges
27120 .iter()
27121 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27122 .collect();
27123
27124 editor.highlight_background::<DocumentHighlightRead>(
27125 &anchor_ranges,
27126 |theme| theme.colors().editor_document_highlight_read_background,
27127 cx,
27128 );
27129 });
27130
27131 // Go to next highlight - should move to second "variable"
27132 cx.update_editor(|editor, window, cx| {
27133 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27134 });
27135 cx.assert_editor_state(
27136 "let variable = 42;
27137let another = ˇvariable + 1;
27138let result = variable * 2;",
27139 );
27140
27141 // Go to next highlight - should move to third "variable"
27142 cx.update_editor(|editor, window, cx| {
27143 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27144 });
27145 cx.assert_editor_state(
27146 "let variable = 42;
27147let another = variable + 1;
27148let result = ˇvariable * 2;",
27149 );
27150
27151 // Go to next highlight - should stay at third "variable" (no wrap-around)
27152 cx.update_editor(|editor, window, cx| {
27153 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27154 });
27155 cx.assert_editor_state(
27156 "let variable = 42;
27157let another = variable + 1;
27158let result = ˇvariable * 2;",
27159 );
27160
27161 // Now test going backwards from third position
27162 cx.update_editor(|editor, window, cx| {
27163 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27164 });
27165 cx.assert_editor_state(
27166 "let variable = 42;
27167let another = ˇvariable + 1;
27168let result = variable * 2;",
27169 );
27170
27171 // Go to previous highlight - should move to first "variable"
27172 cx.update_editor(|editor, window, cx| {
27173 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27174 });
27175 cx.assert_editor_state(
27176 "let ˇvariable = 42;
27177let another = variable + 1;
27178let result = variable * 2;",
27179 );
27180
27181 // Go to previous highlight - should stay on first "variable"
27182 cx.update_editor(|editor, window, cx| {
27183 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27184 });
27185 cx.assert_editor_state(
27186 "let ˇvariable = 42;
27187let another = variable + 1;
27188let result = variable * 2;",
27189 );
27190}
27191
27192#[gpui::test]
27193async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27194 cx: &mut gpui::TestAppContext,
27195) {
27196 init_test(cx, |_| {});
27197
27198 let url = "https://zed.dev";
27199
27200 let markdown_language = Arc::new(Language::new(
27201 LanguageConfig {
27202 name: "Markdown".into(),
27203 ..LanguageConfig::default()
27204 },
27205 None,
27206 ));
27207
27208 let mut cx = EditorTestContext::new(cx).await;
27209 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27210 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27211
27212 cx.update_editor(|editor, window, cx| {
27213 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27214 editor.paste(&Paste, window, cx);
27215 });
27216
27217 cx.assert_editor_state(&format!(
27218 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27219 ));
27220}
27221
27222#[gpui::test]
27223async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
27224 cx: &mut gpui::TestAppContext,
27225) {
27226 init_test(cx, |_| {});
27227
27228 let url = "https://zed.dev";
27229
27230 let markdown_language = Arc::new(Language::new(
27231 LanguageConfig {
27232 name: "Markdown".into(),
27233 ..LanguageConfig::default()
27234 },
27235 None,
27236 ));
27237
27238 let mut cx = EditorTestContext::new(cx).await;
27239 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27240 cx.set_state(&format!(
27241 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
27242 ));
27243
27244 cx.update_editor(|editor, window, cx| {
27245 editor.copy(&Copy, window, cx);
27246 });
27247
27248 cx.set_state(&format!(
27249 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
27250 ));
27251
27252 cx.update_editor(|editor, window, cx| {
27253 editor.paste(&Paste, window, cx);
27254 });
27255
27256 cx.assert_editor_state(&format!(
27257 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
27258 ));
27259}
27260
27261#[gpui::test]
27262async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
27263 cx: &mut gpui::TestAppContext,
27264) {
27265 init_test(cx, |_| {});
27266
27267 let url = "https://zed.dev";
27268
27269 let markdown_language = Arc::new(Language::new(
27270 LanguageConfig {
27271 name: "Markdown".into(),
27272 ..LanguageConfig::default()
27273 },
27274 None,
27275 ));
27276
27277 let mut cx = EditorTestContext::new(cx).await;
27278 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27279 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
27280
27281 cx.update_editor(|editor, window, cx| {
27282 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27283 editor.paste(&Paste, window, cx);
27284 });
27285
27286 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
27287}
27288
27289#[gpui::test]
27290async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
27291 cx: &mut gpui::TestAppContext,
27292) {
27293 init_test(cx, |_| {});
27294
27295 let text = "Awesome";
27296
27297 let markdown_language = Arc::new(Language::new(
27298 LanguageConfig {
27299 name: "Markdown".into(),
27300 ..LanguageConfig::default()
27301 },
27302 None,
27303 ));
27304
27305 let mut cx = EditorTestContext::new(cx).await;
27306 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27307 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
27308
27309 cx.update_editor(|editor, window, cx| {
27310 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
27311 editor.paste(&Paste, window, cx);
27312 });
27313
27314 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
27315}
27316
27317#[gpui::test]
27318async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
27319 cx: &mut gpui::TestAppContext,
27320) {
27321 init_test(cx, |_| {});
27322
27323 let url = "https://zed.dev";
27324
27325 let markdown_language = Arc::new(Language::new(
27326 LanguageConfig {
27327 name: "Rust".into(),
27328 ..LanguageConfig::default()
27329 },
27330 None,
27331 ));
27332
27333 let mut cx = EditorTestContext::new(cx).await;
27334 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27335 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
27336
27337 cx.update_editor(|editor, window, cx| {
27338 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27339 editor.paste(&Paste, window, cx);
27340 });
27341
27342 cx.assert_editor_state(&format!(
27343 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
27344 ));
27345}
27346
27347#[gpui::test]
27348async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
27349 cx: &mut TestAppContext,
27350) {
27351 init_test(cx, |_| {});
27352
27353 let url = "https://zed.dev";
27354
27355 let markdown_language = Arc::new(Language::new(
27356 LanguageConfig {
27357 name: "Markdown".into(),
27358 ..LanguageConfig::default()
27359 },
27360 None,
27361 ));
27362
27363 let (editor, cx) = cx.add_window_view(|window, cx| {
27364 let multi_buffer = MultiBuffer::build_multi(
27365 [
27366 ("this will embed -> link", vec![Point::row_range(0..1)]),
27367 ("this will replace -> link", vec![Point::row_range(0..1)]),
27368 ],
27369 cx,
27370 );
27371 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
27372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27373 s.select_ranges(vec![
27374 Point::new(0, 19)..Point::new(0, 23),
27375 Point::new(1, 21)..Point::new(1, 25),
27376 ])
27377 });
27378 let first_buffer_id = multi_buffer
27379 .read(cx)
27380 .excerpt_buffer_ids()
27381 .into_iter()
27382 .next()
27383 .unwrap();
27384 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
27385 first_buffer.update(cx, |buffer, cx| {
27386 buffer.set_language(Some(markdown_language.clone()), cx);
27387 });
27388
27389 editor
27390 });
27391 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27392
27393 cx.update_editor(|editor, window, cx| {
27394 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27395 editor.paste(&Paste, window, cx);
27396 });
27397
27398 cx.assert_editor_state(&format!(
27399 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
27400 ));
27401}
27402
27403#[gpui::test]
27404async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
27405 init_test(cx, |_| {});
27406
27407 let fs = FakeFs::new(cx.executor());
27408 fs.insert_tree(
27409 path!("/project"),
27410 json!({
27411 "first.rs": "# First Document\nSome content here.",
27412 "second.rs": "Plain text content for second file.",
27413 }),
27414 )
27415 .await;
27416
27417 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
27418 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27419 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27420
27421 let language = rust_lang();
27422 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27423 language_registry.add(language.clone());
27424 let mut fake_servers = language_registry.register_fake_lsp(
27425 "Rust",
27426 FakeLspAdapter {
27427 ..FakeLspAdapter::default()
27428 },
27429 );
27430
27431 let buffer1 = project
27432 .update(cx, |project, cx| {
27433 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
27434 })
27435 .await
27436 .unwrap();
27437 let buffer2 = project
27438 .update(cx, |project, cx| {
27439 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
27440 })
27441 .await
27442 .unwrap();
27443
27444 let multi_buffer = cx.new(|cx| {
27445 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
27446 multi_buffer.set_excerpts_for_path(
27447 PathKey::for_buffer(&buffer1, cx),
27448 buffer1.clone(),
27449 [Point::zero()..buffer1.read(cx).max_point()],
27450 3,
27451 cx,
27452 );
27453 multi_buffer.set_excerpts_for_path(
27454 PathKey::for_buffer(&buffer2, cx),
27455 buffer2.clone(),
27456 [Point::zero()..buffer1.read(cx).max_point()],
27457 3,
27458 cx,
27459 );
27460 multi_buffer
27461 });
27462
27463 let (editor, cx) = cx.add_window_view(|window, cx| {
27464 Editor::new(
27465 EditorMode::full(),
27466 multi_buffer,
27467 Some(project.clone()),
27468 window,
27469 cx,
27470 )
27471 });
27472
27473 let fake_language_server = fake_servers.next().await.unwrap();
27474
27475 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
27476
27477 let save = editor.update_in(cx, |editor, window, cx| {
27478 assert!(editor.is_dirty(cx));
27479
27480 editor.save(
27481 SaveOptions {
27482 format: true,
27483 autosave: true,
27484 },
27485 project,
27486 window,
27487 cx,
27488 )
27489 });
27490 let (start_edit_tx, start_edit_rx) = oneshot::channel();
27491 let (done_edit_tx, done_edit_rx) = oneshot::channel();
27492 let mut done_edit_rx = Some(done_edit_rx);
27493 let mut start_edit_tx = Some(start_edit_tx);
27494
27495 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
27496 start_edit_tx.take().unwrap().send(()).unwrap();
27497 let done_edit_rx = done_edit_rx.take().unwrap();
27498 async move {
27499 done_edit_rx.await.unwrap();
27500 Ok(None)
27501 }
27502 });
27503
27504 start_edit_rx.await.unwrap();
27505 buffer2
27506 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
27507 .unwrap();
27508
27509 done_edit_tx.send(()).unwrap();
27510
27511 save.await.unwrap();
27512 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
27513}
27514
27515#[track_caller]
27516fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
27517 editor
27518 .all_inlays(cx)
27519 .into_iter()
27520 .filter_map(|inlay| inlay.get_color())
27521 .map(Rgba::from)
27522 .collect()
27523}
27524
27525#[gpui::test]
27526fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
27527 init_test(cx, |_| {});
27528
27529 let editor = cx.add_window(|window, cx| {
27530 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
27531 build_editor(buffer, window, cx)
27532 });
27533
27534 editor
27535 .update(cx, |editor, window, cx| {
27536 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27537 s.select_display_ranges([
27538 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
27539 ])
27540 });
27541
27542 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
27543
27544 assert_eq!(
27545 editor.display_text(cx),
27546 "line1\nline2\nline2",
27547 "Duplicating last line upward should create duplicate above, not on same line"
27548 );
27549
27550 assert_eq!(
27551 editor
27552 .selections
27553 .display_ranges(&editor.display_snapshot(cx)),
27554 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27555 "Selection should move to the duplicated line"
27556 );
27557 })
27558 .unwrap();
27559}
27560
27561#[gpui::test]
27562async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27563 init_test(cx, |_| {});
27564
27565 let mut cx = EditorTestContext::new(cx).await;
27566
27567 cx.set_state("line1\nline2ˇ");
27568
27569 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27570
27571 let clipboard_text = cx
27572 .read_from_clipboard()
27573 .and_then(|item| item.text().as_deref().map(str::to_string));
27574
27575 assert_eq!(
27576 clipboard_text,
27577 Some("line2\n".to_string()),
27578 "Copying a line without trailing newline should include a newline"
27579 );
27580
27581 cx.set_state("line1\nˇ");
27582
27583 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27584
27585 cx.assert_editor_state("line1\nline2\nˇ");
27586}
27587
27588#[gpui::test]
27589async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27590 init_test(cx, |_| {});
27591
27592 let mut cx = EditorTestContext::new(cx).await;
27593
27594 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27595
27596 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27597
27598 let clipboard_text = cx
27599 .read_from_clipboard()
27600 .and_then(|item| item.text().as_deref().map(str::to_string));
27601
27602 assert_eq!(
27603 clipboard_text,
27604 Some("line1\nline2\nline3\n".to_string()),
27605 "Copying multiple lines should include a single newline between lines"
27606 );
27607
27608 cx.set_state("lineA\nˇ");
27609
27610 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27611
27612 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27613}
27614
27615#[gpui::test]
27616async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27617 init_test(cx, |_| {});
27618
27619 let mut cx = EditorTestContext::new(cx).await;
27620
27621 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27622
27623 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
27624
27625 let clipboard_text = cx
27626 .read_from_clipboard()
27627 .and_then(|item| item.text().as_deref().map(str::to_string));
27628
27629 assert_eq!(
27630 clipboard_text,
27631 Some("line1\nline2\nline3\n".to_string()),
27632 "Copying multiple lines should include a single newline between lines"
27633 );
27634
27635 cx.set_state("lineA\nˇ");
27636
27637 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27638
27639 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27640}
27641
27642#[gpui::test]
27643async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27644 init_test(cx, |_| {});
27645
27646 let mut cx = EditorTestContext::new(cx).await;
27647
27648 cx.set_state("line1\nline2ˇ");
27649 cx.update_editor(|e, window, cx| {
27650 e.set_mode(EditorMode::SingleLine);
27651 assert!(e.key_context(window, cx).contains("end_of_input"));
27652 });
27653 cx.set_state("ˇline1\nline2");
27654 cx.update_editor(|e, window, cx| {
27655 assert!(!e.key_context(window, cx).contains("end_of_input"));
27656 });
27657 cx.set_state("line1ˇ\nline2");
27658 cx.update_editor(|e, window, cx| {
27659 assert!(!e.key_context(window, cx).contains("end_of_input"));
27660 });
27661}
27662
27663#[gpui::test]
27664async fn test_sticky_scroll(cx: &mut TestAppContext) {
27665 init_test(cx, |_| {});
27666 let mut cx = EditorTestContext::new(cx).await;
27667
27668 let buffer = indoc! {"
27669 ˇfn foo() {
27670 let abc = 123;
27671 }
27672 struct Bar;
27673 impl Bar {
27674 fn new() -> Self {
27675 Self
27676 }
27677 }
27678 fn baz() {
27679 }
27680 "};
27681 cx.set_state(&buffer);
27682
27683 cx.update_editor(|e, _, cx| {
27684 e.buffer()
27685 .read(cx)
27686 .as_singleton()
27687 .unwrap()
27688 .update(cx, |buffer, cx| {
27689 buffer.set_language(Some(rust_lang()), cx);
27690 })
27691 });
27692
27693 let mut sticky_headers = |offset: ScrollOffset| {
27694 cx.update_editor(|e, window, cx| {
27695 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27696 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27697 .into_iter()
27698 .map(
27699 |StickyHeader {
27700 start_point,
27701 offset,
27702 ..
27703 }| { (start_point, offset) },
27704 )
27705 .collect::<Vec<_>>()
27706 })
27707 };
27708
27709 let fn_foo = Point { row: 0, column: 0 };
27710 let impl_bar = Point { row: 4, column: 0 };
27711 let fn_new = Point { row: 5, column: 4 };
27712
27713 assert_eq!(sticky_headers(0.0), vec![]);
27714 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27715 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27716 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27717 assert_eq!(sticky_headers(2.0), vec![]);
27718 assert_eq!(sticky_headers(2.5), vec![]);
27719 assert_eq!(sticky_headers(3.0), vec![]);
27720 assert_eq!(sticky_headers(3.5), vec![]);
27721 assert_eq!(sticky_headers(4.0), vec![]);
27722 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27723 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27724 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27725 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27726 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27727 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27728 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27729 assert_eq!(sticky_headers(8.0), vec![]);
27730 assert_eq!(sticky_headers(8.5), vec![]);
27731 assert_eq!(sticky_headers(9.0), vec![]);
27732 assert_eq!(sticky_headers(9.5), vec![]);
27733 assert_eq!(sticky_headers(10.0), vec![]);
27734}
27735
27736#[gpui::test]
27737async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27738 init_test(cx, |_| {});
27739 cx.update(|cx| {
27740 SettingsStore::update_global(cx, |store, cx| {
27741 store.update_user_settings(cx, |settings| {
27742 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27743 enabled: Some(true),
27744 })
27745 });
27746 });
27747 });
27748 let mut cx = EditorTestContext::new(cx).await;
27749
27750 let line_height = cx.editor(|editor, window, _cx| {
27751 editor
27752 .style()
27753 .unwrap()
27754 .text
27755 .line_height_in_pixels(window.rem_size())
27756 });
27757
27758 let buffer = indoc! {"
27759 ˇfn foo() {
27760 let abc = 123;
27761 }
27762 struct Bar;
27763 impl Bar {
27764 fn new() -> Self {
27765 Self
27766 }
27767 }
27768 fn baz() {
27769 }
27770 "};
27771 cx.set_state(&buffer);
27772
27773 cx.update_editor(|e, _, cx| {
27774 e.buffer()
27775 .read(cx)
27776 .as_singleton()
27777 .unwrap()
27778 .update(cx, |buffer, cx| {
27779 buffer.set_language(Some(rust_lang()), cx);
27780 })
27781 });
27782
27783 let fn_foo = || empty_range(0, 0);
27784 let impl_bar = || empty_range(4, 0);
27785 let fn_new = || empty_range(5, 4);
27786
27787 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27788 cx.update_editor(|e, window, cx| {
27789 e.scroll(
27790 gpui::Point {
27791 x: 0.,
27792 y: scroll_offset,
27793 },
27794 None,
27795 window,
27796 cx,
27797 );
27798 });
27799 cx.simulate_click(
27800 gpui::Point {
27801 x: px(0.),
27802 y: click_offset as f32 * line_height,
27803 },
27804 Modifiers::none(),
27805 );
27806 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27807 };
27808
27809 assert_eq!(
27810 scroll_and_click(
27811 4.5, // impl Bar is halfway off the screen
27812 0.0 // click top of screen
27813 ),
27814 // scrolled to impl Bar
27815 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27816 );
27817
27818 assert_eq!(
27819 scroll_and_click(
27820 4.5, // impl Bar is halfway off the screen
27821 0.25 // click middle of impl Bar
27822 ),
27823 // scrolled to impl Bar
27824 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27825 );
27826
27827 assert_eq!(
27828 scroll_and_click(
27829 4.5, // impl Bar is halfway off the screen
27830 1.5 // click below impl Bar (e.g. fn new())
27831 ),
27832 // scrolled to fn new() - this is below the impl Bar header which has persisted
27833 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27834 );
27835
27836 assert_eq!(
27837 scroll_and_click(
27838 5.5, // fn new is halfway underneath impl Bar
27839 0.75 // click on the overlap of impl Bar and fn new()
27840 ),
27841 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27842 );
27843
27844 assert_eq!(
27845 scroll_and_click(
27846 5.5, // fn new is halfway underneath impl Bar
27847 1.25 // click on the visible part of fn new()
27848 ),
27849 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27850 );
27851
27852 assert_eq!(
27853 scroll_and_click(
27854 1.5, // fn foo is halfway off the screen
27855 0.0 // click top of screen
27856 ),
27857 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
27858 );
27859
27860 assert_eq!(
27861 scroll_and_click(
27862 1.5, // fn foo is halfway off the screen
27863 0.75 // click visible part of let abc...
27864 )
27865 .0,
27866 // no change in scroll
27867 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
27868 (gpui::Point { x: 0., y: 1.5 })
27869 );
27870}
27871
27872#[gpui::test]
27873async fn test_next_prev_reference(cx: &mut TestAppContext) {
27874 const CYCLE_POSITIONS: &[&'static str] = &[
27875 indoc! {"
27876 fn foo() {
27877 let ˇabc = 123;
27878 let x = abc + 1;
27879 let y = abc + 2;
27880 let z = abc + 2;
27881 }
27882 "},
27883 indoc! {"
27884 fn foo() {
27885 let abc = 123;
27886 let x = ˇabc + 1;
27887 let y = abc + 2;
27888 let z = abc + 2;
27889 }
27890 "},
27891 indoc! {"
27892 fn foo() {
27893 let abc = 123;
27894 let x = abc + 1;
27895 let y = ˇabc + 2;
27896 let z = abc + 2;
27897 }
27898 "},
27899 indoc! {"
27900 fn foo() {
27901 let abc = 123;
27902 let x = abc + 1;
27903 let y = abc + 2;
27904 let z = ˇabc + 2;
27905 }
27906 "},
27907 ];
27908
27909 init_test(cx, |_| {});
27910
27911 let mut cx = EditorLspTestContext::new_rust(
27912 lsp::ServerCapabilities {
27913 references_provider: Some(lsp::OneOf::Left(true)),
27914 ..Default::default()
27915 },
27916 cx,
27917 )
27918 .await;
27919
27920 // importantly, the cursor is in the middle
27921 cx.set_state(indoc! {"
27922 fn foo() {
27923 let aˇbc = 123;
27924 let x = abc + 1;
27925 let y = abc + 2;
27926 let z = abc + 2;
27927 }
27928 "});
27929
27930 let reference_ranges = [
27931 lsp::Position::new(1, 8),
27932 lsp::Position::new(2, 12),
27933 lsp::Position::new(3, 12),
27934 lsp::Position::new(4, 12),
27935 ]
27936 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27937
27938 cx.lsp
27939 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27940 Ok(Some(
27941 reference_ranges
27942 .map(|range| lsp::Location {
27943 uri: params.text_document_position.text_document.uri.clone(),
27944 range,
27945 })
27946 .to_vec(),
27947 ))
27948 });
27949
27950 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27951 cx.update_editor(|editor, window, cx| {
27952 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27953 })
27954 .unwrap()
27955 .await
27956 .unwrap()
27957 };
27958
27959 _move(Direction::Next, 1, &mut cx).await;
27960 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27961
27962 _move(Direction::Next, 1, &mut cx).await;
27963 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27964
27965 _move(Direction::Next, 1, &mut cx).await;
27966 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27967
27968 // loops back to the start
27969 _move(Direction::Next, 1, &mut cx).await;
27970 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27971
27972 // loops back to the end
27973 _move(Direction::Prev, 1, &mut cx).await;
27974 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27975
27976 _move(Direction::Prev, 1, &mut cx).await;
27977 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27978
27979 _move(Direction::Prev, 1, &mut cx).await;
27980 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27981
27982 _move(Direction::Prev, 1, &mut cx).await;
27983 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27984
27985 _move(Direction::Next, 3, &mut cx).await;
27986 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27987
27988 _move(Direction::Prev, 2, &mut cx).await;
27989 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27990}
27991
27992#[gpui::test]
27993async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
27994 init_test(cx, |_| {});
27995
27996 let (editor, cx) = cx.add_window_view(|window, cx| {
27997 let multi_buffer = MultiBuffer::build_multi(
27998 [
27999 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28000 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28001 ],
28002 cx,
28003 );
28004 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28005 });
28006
28007 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28008 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28009
28010 cx.assert_excerpts_with_selections(indoc! {"
28011 [EXCERPT]
28012 ˇ1
28013 2
28014 3
28015 [EXCERPT]
28016 1
28017 2
28018 3
28019 "});
28020
28021 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28022 cx.update_editor(|editor, window, cx| {
28023 editor.change_selections(None.into(), window, cx, |s| {
28024 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28025 });
28026 });
28027 cx.assert_excerpts_with_selections(indoc! {"
28028 [EXCERPT]
28029 1
28030 2ˇ
28031 3
28032 [EXCERPT]
28033 1
28034 2
28035 3
28036 "});
28037
28038 cx.update_editor(|editor, window, cx| {
28039 editor
28040 .select_all_matches(&SelectAllMatches, window, cx)
28041 .unwrap();
28042 });
28043 cx.assert_excerpts_with_selections(indoc! {"
28044 [EXCERPT]
28045 1
28046 2ˇ
28047 3
28048 [EXCERPT]
28049 1
28050 2ˇ
28051 3
28052 "});
28053
28054 cx.update_editor(|editor, window, cx| {
28055 editor.handle_input("X", window, cx);
28056 });
28057 cx.assert_excerpts_with_selections(indoc! {"
28058 [EXCERPT]
28059 1
28060 Xˇ
28061 3
28062 [EXCERPT]
28063 1
28064 Xˇ
28065 3
28066 "});
28067
28068 // Scenario 2: Select "2", then fold second buffer before insertion
28069 cx.update_multibuffer(|mb, cx| {
28070 for buffer_id in buffer_ids.iter() {
28071 let buffer = mb.buffer(*buffer_id).unwrap();
28072 buffer.update(cx, |buffer, cx| {
28073 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28074 });
28075 }
28076 });
28077
28078 // Select "2" and select all matches
28079 cx.update_editor(|editor, window, cx| {
28080 editor.change_selections(None.into(), window, cx, |s| {
28081 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28082 });
28083 editor
28084 .select_all_matches(&SelectAllMatches, window, cx)
28085 .unwrap();
28086 });
28087
28088 // Fold second buffer - should remove selections from folded buffer
28089 cx.update_editor(|editor, _, cx| {
28090 editor.fold_buffer(buffer_ids[1], cx);
28091 });
28092 cx.assert_excerpts_with_selections(indoc! {"
28093 [EXCERPT]
28094 1
28095 2ˇ
28096 3
28097 [EXCERPT]
28098 [FOLDED]
28099 "});
28100
28101 // Insert text - should only affect first buffer
28102 cx.update_editor(|editor, window, cx| {
28103 editor.handle_input("Y", window, cx);
28104 });
28105 cx.update_editor(|editor, _, cx| {
28106 editor.unfold_buffer(buffer_ids[1], cx);
28107 });
28108 cx.assert_excerpts_with_selections(indoc! {"
28109 [EXCERPT]
28110 1
28111 Yˇ
28112 3
28113 [EXCERPT]
28114 1
28115 2
28116 3
28117 "});
28118
28119 // Scenario 3: Select "2", then fold first buffer before insertion
28120 cx.update_multibuffer(|mb, cx| {
28121 for buffer_id in buffer_ids.iter() {
28122 let buffer = mb.buffer(*buffer_id).unwrap();
28123 buffer.update(cx, |buffer, cx| {
28124 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28125 });
28126 }
28127 });
28128
28129 // Select "2" and select all matches
28130 cx.update_editor(|editor, window, cx| {
28131 editor.change_selections(None.into(), window, cx, |s| {
28132 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28133 });
28134 editor
28135 .select_all_matches(&SelectAllMatches, window, cx)
28136 .unwrap();
28137 });
28138
28139 // Fold first buffer - should remove selections from folded buffer
28140 cx.update_editor(|editor, _, cx| {
28141 editor.fold_buffer(buffer_ids[0], cx);
28142 });
28143 cx.assert_excerpts_with_selections(indoc! {"
28144 [EXCERPT]
28145 [FOLDED]
28146 [EXCERPT]
28147 1
28148 2ˇ
28149 3
28150 "});
28151
28152 // Insert text - should only affect second buffer
28153 cx.update_editor(|editor, window, cx| {
28154 editor.handle_input("Z", window, cx);
28155 });
28156 cx.update_editor(|editor, _, cx| {
28157 editor.unfold_buffer(buffer_ids[0], cx);
28158 });
28159 cx.assert_excerpts_with_selections(indoc! {"
28160 [EXCERPT]
28161 1
28162 2
28163 3
28164 [EXCERPT]
28165 1
28166 Zˇ
28167 3
28168 "});
28169
28170 // Edge case scenario: fold all buffers, then try to insert
28171 cx.update_editor(|editor, _, cx| {
28172 editor.fold_buffer(buffer_ids[0], cx);
28173 editor.fold_buffer(buffer_ids[1], cx);
28174 });
28175 cx.assert_excerpts_with_selections(indoc! {"
28176 [EXCERPT]
28177 ˇ[FOLDED]
28178 [EXCERPT]
28179 [FOLDED]
28180 "});
28181
28182 // Insert should work via default selection
28183 cx.update_editor(|editor, window, cx| {
28184 editor.handle_input("W", window, cx);
28185 });
28186 cx.update_editor(|editor, _, cx| {
28187 editor.unfold_buffer(buffer_ids[0], cx);
28188 editor.unfold_buffer(buffer_ids[1], cx);
28189 });
28190 cx.assert_excerpts_with_selections(indoc! {"
28191 [EXCERPT]
28192 Wˇ1
28193 2
28194 3
28195 [EXCERPT]
28196 1
28197 Z
28198 3
28199 "});
28200}
28201
28202#[gpui::test]
28203async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
28204 init_test(cx, |_| {});
28205
28206 let (editor, cx) = cx.add_window_view(|window, cx| {
28207 let multi_buffer = MultiBuffer::build_multi(
28208 [
28209 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28210 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
28211 ],
28212 cx,
28213 );
28214 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28215 });
28216
28217 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28218
28219 cx.assert_excerpts_with_selections(indoc! {"
28220 [EXCERPT]
28221 ˇ1
28222 2
28223 3
28224 [EXCERPT]
28225 1
28226 2
28227 3
28228 4
28229 5
28230 6
28231 7
28232 8
28233 9
28234 "});
28235
28236 cx.update_editor(|editor, window, cx| {
28237 editor.change_selections(None.into(), window, cx, |s| {
28238 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
28239 });
28240 });
28241
28242 cx.assert_excerpts_with_selections(indoc! {"
28243 [EXCERPT]
28244 1
28245 2
28246 3
28247 [EXCERPT]
28248 1
28249 2
28250 3
28251 4
28252 5
28253 6
28254 ˇ7
28255 8
28256 9
28257 "});
28258
28259 cx.update_editor(|editor, _window, cx| {
28260 editor.set_vertical_scroll_margin(0, cx);
28261 });
28262
28263 cx.update_editor(|editor, window, cx| {
28264 assert_eq!(editor.vertical_scroll_margin(), 0);
28265 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28266 assert_eq!(
28267 editor.snapshot(window, cx).scroll_position(),
28268 gpui::Point::new(0., 12.0)
28269 );
28270 });
28271
28272 cx.update_editor(|editor, _window, cx| {
28273 editor.set_vertical_scroll_margin(3, cx);
28274 });
28275
28276 cx.update_editor(|editor, window, cx| {
28277 assert_eq!(editor.vertical_scroll_margin(), 3);
28278 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28279 assert_eq!(
28280 editor.snapshot(window, cx).scroll_position(),
28281 gpui::Point::new(0., 9.0)
28282 );
28283 });
28284}