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 let buffer_id = hunks[0].buffer_id;
21554 hunks
21555 .into_iter()
21556 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21557 .collect::<Vec<_>>()
21558 });
21559 assert_eq!(hunk_ranges.len(), 2);
21560
21561 cx.update_editor(|editor, _, cx| {
21562 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21563 });
21564 executor.run_until_parked();
21565
21566 let second_hunk_expanded = r#"
21567 ˇA
21568 b
21569 - c
21570 + C
21571 "#
21572 .unindent();
21573
21574 cx.assert_state_with_diff(second_hunk_expanded);
21575
21576 cx.update_editor(|editor, _, cx| {
21577 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21578 });
21579 executor.run_until_parked();
21580
21581 cx.assert_state_with_diff(both_hunks_expanded.clone());
21582
21583 cx.update_editor(|editor, _, cx| {
21584 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21585 });
21586 executor.run_until_parked();
21587
21588 let first_hunk_expanded = r#"
21589 - a
21590 + ˇA
21591 b
21592 C
21593 "#
21594 .unindent();
21595
21596 cx.assert_state_with_diff(first_hunk_expanded);
21597
21598 cx.update_editor(|editor, _, cx| {
21599 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21600 });
21601 executor.run_until_parked();
21602
21603 cx.assert_state_with_diff(both_hunks_expanded);
21604
21605 cx.set_state(
21606 &r#"
21607 ˇA
21608 b
21609 "#
21610 .unindent(),
21611 );
21612 cx.run_until_parked();
21613
21614 // TODO this cursor position seems bad
21615 cx.assert_state_with_diff(
21616 r#"
21617 - ˇa
21618 + A
21619 b
21620 "#
21621 .unindent(),
21622 );
21623
21624 cx.update_editor(|editor, window, cx| {
21625 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21626 });
21627
21628 cx.assert_state_with_diff(
21629 r#"
21630 - ˇa
21631 + A
21632 b
21633 - c
21634 "#
21635 .unindent(),
21636 );
21637
21638 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21639 let snapshot = editor.snapshot(window, cx);
21640 let hunks = editor
21641 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21642 .collect::<Vec<_>>();
21643 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21644 let buffer_id = hunks[0].buffer_id;
21645 hunks
21646 .into_iter()
21647 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21648 .collect::<Vec<_>>()
21649 });
21650 assert_eq!(hunk_ranges.len(), 2);
21651
21652 cx.update_editor(|editor, _, cx| {
21653 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21654 });
21655 executor.run_until_parked();
21656
21657 cx.assert_state_with_diff(
21658 r#"
21659 - ˇa
21660 + A
21661 b
21662 "#
21663 .unindent(),
21664 );
21665}
21666
21667#[gpui::test]
21668async fn test_toggle_deletion_hunk_at_start_of_file(
21669 executor: BackgroundExecutor,
21670 cx: &mut TestAppContext,
21671) {
21672 init_test(cx, |_| {});
21673 let mut cx = EditorTestContext::new(cx).await;
21674
21675 let diff_base = r#"
21676 a
21677 b
21678 c
21679 "#
21680 .unindent();
21681
21682 cx.set_state(
21683 &r#"
21684 ˇb
21685 c
21686 "#
21687 .unindent(),
21688 );
21689 cx.set_head_text(&diff_base);
21690 cx.update_editor(|editor, window, cx| {
21691 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21692 });
21693 executor.run_until_parked();
21694
21695 let hunk_expanded = r#"
21696 - a
21697 ˇb
21698 c
21699 "#
21700 .unindent();
21701
21702 cx.assert_state_with_diff(hunk_expanded.clone());
21703
21704 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21705 let snapshot = editor.snapshot(window, cx);
21706 let hunks = editor
21707 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21708 .collect::<Vec<_>>();
21709 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21710 let buffer_id = hunks[0].buffer_id;
21711 hunks
21712 .into_iter()
21713 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21714 .collect::<Vec<_>>()
21715 });
21716 assert_eq!(hunk_ranges.len(), 1);
21717
21718 cx.update_editor(|editor, _, cx| {
21719 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21720 });
21721 executor.run_until_parked();
21722
21723 let hunk_collapsed = r#"
21724 ˇb
21725 c
21726 "#
21727 .unindent();
21728
21729 cx.assert_state_with_diff(hunk_collapsed);
21730
21731 cx.update_editor(|editor, _, cx| {
21732 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21733 });
21734 executor.run_until_parked();
21735
21736 cx.assert_state_with_diff(hunk_expanded);
21737}
21738
21739#[gpui::test]
21740async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21741 init_test(cx, |_| {});
21742
21743 let fs = FakeFs::new(cx.executor());
21744 fs.insert_tree(
21745 path!("/test"),
21746 json!({
21747 ".git": {},
21748 "file-1": "ONE\n",
21749 "file-2": "TWO\n",
21750 "file-3": "THREE\n",
21751 }),
21752 )
21753 .await;
21754
21755 fs.set_head_for_repo(
21756 path!("/test/.git").as_ref(),
21757 &[
21758 ("file-1", "one\n".into()),
21759 ("file-2", "two\n".into()),
21760 ("file-3", "three\n".into()),
21761 ],
21762 "deadbeef",
21763 );
21764
21765 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21766 let mut buffers = vec![];
21767 for i in 1..=3 {
21768 let buffer = project
21769 .update(cx, |project, cx| {
21770 let path = format!(path!("/test/file-{}"), i);
21771 project.open_local_buffer(path, cx)
21772 })
21773 .await
21774 .unwrap();
21775 buffers.push(buffer);
21776 }
21777
21778 let multibuffer = cx.new(|cx| {
21779 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21780 multibuffer.set_all_diff_hunks_expanded(cx);
21781 for buffer in &buffers {
21782 let snapshot = buffer.read(cx).snapshot();
21783 multibuffer.set_excerpts_for_path(
21784 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21785 buffer.clone(),
21786 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21787 2,
21788 cx,
21789 );
21790 }
21791 multibuffer
21792 });
21793
21794 let editor = cx.add_window(|window, cx| {
21795 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21796 });
21797 cx.run_until_parked();
21798
21799 let snapshot = editor
21800 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21801 .unwrap();
21802 let hunks = snapshot
21803 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21804 .map(|hunk| match hunk {
21805 DisplayDiffHunk::Unfolded {
21806 display_row_range, ..
21807 } => display_row_range,
21808 DisplayDiffHunk::Folded { .. } => unreachable!(),
21809 })
21810 .collect::<Vec<_>>();
21811 assert_eq!(
21812 hunks,
21813 [
21814 DisplayRow(2)..DisplayRow(4),
21815 DisplayRow(7)..DisplayRow(9),
21816 DisplayRow(12)..DisplayRow(14),
21817 ]
21818 );
21819}
21820
21821#[gpui::test]
21822async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21823 init_test(cx, |_| {});
21824
21825 let mut cx = EditorTestContext::new(cx).await;
21826 cx.set_head_text(indoc! { "
21827 one
21828 two
21829 three
21830 four
21831 five
21832 "
21833 });
21834 cx.set_index_text(indoc! { "
21835 one
21836 two
21837 three
21838 four
21839 five
21840 "
21841 });
21842 cx.set_state(indoc! {"
21843 one
21844 TWO
21845 ˇTHREE
21846 FOUR
21847 five
21848 "});
21849 cx.run_until_parked();
21850 cx.update_editor(|editor, window, cx| {
21851 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21852 });
21853 cx.run_until_parked();
21854 cx.assert_index_text(Some(indoc! {"
21855 one
21856 TWO
21857 THREE
21858 FOUR
21859 five
21860 "}));
21861 cx.set_state(indoc! { "
21862 one
21863 TWO
21864 ˇTHREE-HUNDRED
21865 FOUR
21866 five
21867 "});
21868 cx.run_until_parked();
21869 cx.update_editor(|editor, window, cx| {
21870 let snapshot = editor.snapshot(window, cx);
21871 let hunks = editor
21872 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21873 .collect::<Vec<_>>();
21874 assert_eq!(hunks.len(), 1);
21875 assert_eq!(
21876 hunks[0].status(),
21877 DiffHunkStatus {
21878 kind: DiffHunkStatusKind::Modified,
21879 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21880 }
21881 );
21882
21883 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21884 });
21885 cx.run_until_parked();
21886 cx.assert_index_text(Some(indoc! {"
21887 one
21888 TWO
21889 THREE-HUNDRED
21890 FOUR
21891 five
21892 "}));
21893}
21894
21895#[gpui::test]
21896fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21897 init_test(cx, |_| {});
21898
21899 let editor = cx.add_window(|window, cx| {
21900 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21901 build_editor(buffer, window, cx)
21902 });
21903
21904 let render_args = Arc::new(Mutex::new(None));
21905 let snapshot = editor
21906 .update(cx, |editor, window, cx| {
21907 let snapshot = editor.buffer().read(cx).snapshot(cx);
21908 let range =
21909 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21910
21911 struct RenderArgs {
21912 row: MultiBufferRow,
21913 folded: bool,
21914 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21915 }
21916
21917 let crease = Crease::inline(
21918 range,
21919 FoldPlaceholder::test(),
21920 {
21921 let toggle_callback = render_args.clone();
21922 move |row, folded, callback, _window, _cx| {
21923 *toggle_callback.lock() = Some(RenderArgs {
21924 row,
21925 folded,
21926 callback,
21927 });
21928 div()
21929 }
21930 },
21931 |_row, _folded, _window, _cx| div(),
21932 );
21933
21934 editor.insert_creases(Some(crease), cx);
21935 let snapshot = editor.snapshot(window, cx);
21936 let _div =
21937 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21938 snapshot
21939 })
21940 .unwrap();
21941
21942 let render_args = render_args.lock().take().unwrap();
21943 assert_eq!(render_args.row, MultiBufferRow(1));
21944 assert!(!render_args.folded);
21945 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21946
21947 cx.update_window(*editor, |_, window, cx| {
21948 (render_args.callback)(true, window, cx)
21949 })
21950 .unwrap();
21951 let snapshot = editor
21952 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21953 .unwrap();
21954 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21955
21956 cx.update_window(*editor, |_, window, cx| {
21957 (render_args.callback)(false, window, cx)
21958 })
21959 .unwrap();
21960 let snapshot = editor
21961 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21962 .unwrap();
21963 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21964}
21965
21966#[gpui::test]
21967async fn test_input_text(cx: &mut TestAppContext) {
21968 init_test(cx, |_| {});
21969 let mut cx = EditorTestContext::new(cx).await;
21970
21971 cx.set_state(
21972 &r#"ˇone
21973 two
21974
21975 three
21976 fourˇ
21977 five
21978
21979 siˇx"#
21980 .unindent(),
21981 );
21982
21983 cx.dispatch_action(HandleInput(String::new()));
21984 cx.assert_editor_state(
21985 &r#"ˇone
21986 two
21987
21988 three
21989 fourˇ
21990 five
21991
21992 siˇx"#
21993 .unindent(),
21994 );
21995
21996 cx.dispatch_action(HandleInput("AAAA".to_string()));
21997 cx.assert_editor_state(
21998 &r#"AAAAˇone
21999 two
22000
22001 three
22002 fourAAAAˇ
22003 five
22004
22005 siAAAAˇx"#
22006 .unindent(),
22007 );
22008}
22009
22010#[gpui::test]
22011async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22012 init_test(cx, |_| {});
22013
22014 let mut cx = EditorTestContext::new(cx).await;
22015 cx.set_state(
22016 r#"let foo = 1;
22017let foo = 2;
22018let foo = 3;
22019let fooˇ = 4;
22020let foo = 5;
22021let foo = 6;
22022let foo = 7;
22023let foo = 8;
22024let foo = 9;
22025let foo = 10;
22026let foo = 11;
22027let foo = 12;
22028let foo = 13;
22029let foo = 14;
22030let foo = 15;"#,
22031 );
22032
22033 cx.update_editor(|e, window, cx| {
22034 assert_eq!(
22035 e.next_scroll_position,
22036 NextScrollCursorCenterTopBottom::Center,
22037 "Default next scroll direction is center",
22038 );
22039
22040 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22041 assert_eq!(
22042 e.next_scroll_position,
22043 NextScrollCursorCenterTopBottom::Top,
22044 "After center, next scroll direction should be top",
22045 );
22046
22047 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22048 assert_eq!(
22049 e.next_scroll_position,
22050 NextScrollCursorCenterTopBottom::Bottom,
22051 "After top, next scroll direction should be bottom",
22052 );
22053
22054 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22055 assert_eq!(
22056 e.next_scroll_position,
22057 NextScrollCursorCenterTopBottom::Center,
22058 "After bottom, scrolling should start over",
22059 );
22060
22061 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22062 assert_eq!(
22063 e.next_scroll_position,
22064 NextScrollCursorCenterTopBottom::Top,
22065 "Scrolling continues if retriggered fast enough"
22066 );
22067 });
22068
22069 cx.executor()
22070 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22071 cx.executor().run_until_parked();
22072 cx.update_editor(|e, _, _| {
22073 assert_eq!(
22074 e.next_scroll_position,
22075 NextScrollCursorCenterTopBottom::Center,
22076 "If scrolling is not triggered fast enough, it should reset"
22077 );
22078 });
22079}
22080
22081#[gpui::test]
22082async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22083 init_test(cx, |_| {});
22084 let mut cx = EditorLspTestContext::new_rust(
22085 lsp::ServerCapabilities {
22086 definition_provider: Some(lsp::OneOf::Left(true)),
22087 references_provider: Some(lsp::OneOf::Left(true)),
22088 ..lsp::ServerCapabilities::default()
22089 },
22090 cx,
22091 )
22092 .await;
22093
22094 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22095 let go_to_definition = cx
22096 .lsp
22097 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22098 move |params, _| async move {
22099 if empty_go_to_definition {
22100 Ok(None)
22101 } else {
22102 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22103 uri: params.text_document_position_params.text_document.uri,
22104 range: lsp::Range::new(
22105 lsp::Position::new(4, 3),
22106 lsp::Position::new(4, 6),
22107 ),
22108 })))
22109 }
22110 },
22111 );
22112 let references = cx
22113 .lsp
22114 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22115 Ok(Some(vec![lsp::Location {
22116 uri: params.text_document_position.text_document.uri,
22117 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22118 }]))
22119 });
22120 (go_to_definition, references)
22121 };
22122
22123 cx.set_state(
22124 &r#"fn one() {
22125 let mut a = ˇtwo();
22126 }
22127
22128 fn two() {}"#
22129 .unindent(),
22130 );
22131 set_up_lsp_handlers(false, &mut cx);
22132 let navigated = cx
22133 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22134 .await
22135 .expect("Failed to navigate to definition");
22136 assert_eq!(
22137 navigated,
22138 Navigated::Yes,
22139 "Should have navigated to definition from the GetDefinition response"
22140 );
22141 cx.assert_editor_state(
22142 &r#"fn one() {
22143 let mut a = two();
22144 }
22145
22146 fn «twoˇ»() {}"#
22147 .unindent(),
22148 );
22149
22150 let editors = cx.update_workspace(|workspace, _, cx| {
22151 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22152 });
22153 cx.update_editor(|_, _, test_editor_cx| {
22154 assert_eq!(
22155 editors.len(),
22156 1,
22157 "Initially, only one, test, editor should be open in the workspace"
22158 );
22159 assert_eq!(
22160 test_editor_cx.entity(),
22161 editors.last().expect("Asserted len is 1").clone()
22162 );
22163 });
22164
22165 set_up_lsp_handlers(true, &mut cx);
22166 let navigated = cx
22167 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22168 .await
22169 .expect("Failed to navigate to lookup references");
22170 assert_eq!(
22171 navigated,
22172 Navigated::Yes,
22173 "Should have navigated to references as a fallback after empty GoToDefinition response"
22174 );
22175 // We should not change the selections in the existing file,
22176 // if opening another milti buffer with the references
22177 cx.assert_editor_state(
22178 &r#"fn one() {
22179 let mut a = two();
22180 }
22181
22182 fn «twoˇ»() {}"#
22183 .unindent(),
22184 );
22185 let editors = cx.update_workspace(|workspace, _, cx| {
22186 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22187 });
22188 cx.update_editor(|_, _, test_editor_cx| {
22189 assert_eq!(
22190 editors.len(),
22191 2,
22192 "After falling back to references search, we open a new editor with the results"
22193 );
22194 let references_fallback_text = editors
22195 .into_iter()
22196 .find(|new_editor| *new_editor != test_editor_cx.entity())
22197 .expect("Should have one non-test editor now")
22198 .read(test_editor_cx)
22199 .text(test_editor_cx);
22200 assert_eq!(
22201 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22202 "Should use the range from the references response and not the GoToDefinition one"
22203 );
22204 });
22205}
22206
22207#[gpui::test]
22208async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22209 init_test(cx, |_| {});
22210 cx.update(|cx| {
22211 let mut editor_settings = EditorSettings::get_global(cx).clone();
22212 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22213 EditorSettings::override_global(editor_settings, cx);
22214 });
22215 let mut cx = EditorLspTestContext::new_rust(
22216 lsp::ServerCapabilities {
22217 definition_provider: Some(lsp::OneOf::Left(true)),
22218 references_provider: Some(lsp::OneOf::Left(true)),
22219 ..lsp::ServerCapabilities::default()
22220 },
22221 cx,
22222 )
22223 .await;
22224 let original_state = r#"fn one() {
22225 let mut a = ˇtwo();
22226 }
22227
22228 fn two() {}"#
22229 .unindent();
22230 cx.set_state(&original_state);
22231
22232 let mut go_to_definition = cx
22233 .lsp
22234 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22235 move |_, _| async move { Ok(None) },
22236 );
22237 let _references = cx
22238 .lsp
22239 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22240 panic!("Should not call for references with no go to definition fallback")
22241 });
22242
22243 let navigated = cx
22244 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22245 .await
22246 .expect("Failed to navigate to lookup references");
22247 go_to_definition
22248 .next()
22249 .await
22250 .expect("Should have called the go_to_definition handler");
22251
22252 assert_eq!(
22253 navigated,
22254 Navigated::No,
22255 "Should have navigated to references as a fallback after empty GoToDefinition response"
22256 );
22257 cx.assert_editor_state(&original_state);
22258 let editors = cx.update_workspace(|workspace, _, cx| {
22259 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22260 });
22261 cx.update_editor(|_, _, _| {
22262 assert_eq!(
22263 editors.len(),
22264 1,
22265 "After unsuccessful fallback, no other editor should have been opened"
22266 );
22267 });
22268}
22269
22270#[gpui::test]
22271async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22272 init_test(cx, |_| {});
22273 let mut cx = EditorLspTestContext::new_rust(
22274 lsp::ServerCapabilities {
22275 references_provider: Some(lsp::OneOf::Left(true)),
22276 ..lsp::ServerCapabilities::default()
22277 },
22278 cx,
22279 )
22280 .await;
22281
22282 cx.set_state(
22283 &r#"
22284 fn one() {
22285 let mut a = two();
22286 }
22287
22288 fn ˇtwo() {}"#
22289 .unindent(),
22290 );
22291 cx.lsp
22292 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22293 Ok(Some(vec![
22294 lsp::Location {
22295 uri: params.text_document_position.text_document.uri.clone(),
22296 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22297 },
22298 lsp::Location {
22299 uri: params.text_document_position.text_document.uri,
22300 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22301 },
22302 ]))
22303 });
22304 let navigated = cx
22305 .update_editor(|editor, window, cx| {
22306 editor.find_all_references(&FindAllReferences, window, cx)
22307 })
22308 .unwrap()
22309 .await
22310 .expect("Failed to navigate to references");
22311 assert_eq!(
22312 navigated,
22313 Navigated::Yes,
22314 "Should have navigated to references from the FindAllReferences response"
22315 );
22316 cx.assert_editor_state(
22317 &r#"fn one() {
22318 let mut a = two();
22319 }
22320
22321 fn ˇtwo() {}"#
22322 .unindent(),
22323 );
22324
22325 let editors = cx.update_workspace(|workspace, _, cx| {
22326 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22327 });
22328 cx.update_editor(|_, _, _| {
22329 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22330 });
22331
22332 cx.set_state(
22333 &r#"fn one() {
22334 let mut a = ˇtwo();
22335 }
22336
22337 fn two() {}"#
22338 .unindent(),
22339 );
22340 let navigated = cx
22341 .update_editor(|editor, window, cx| {
22342 editor.find_all_references(&FindAllReferences, window, cx)
22343 })
22344 .unwrap()
22345 .await
22346 .expect("Failed to navigate to references");
22347 assert_eq!(
22348 navigated,
22349 Navigated::Yes,
22350 "Should have navigated to references from the FindAllReferences response"
22351 );
22352 cx.assert_editor_state(
22353 &r#"fn one() {
22354 let mut a = ˇtwo();
22355 }
22356
22357 fn two() {}"#
22358 .unindent(),
22359 );
22360 let editors = cx.update_workspace(|workspace, _, cx| {
22361 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22362 });
22363 cx.update_editor(|_, _, _| {
22364 assert_eq!(
22365 editors.len(),
22366 2,
22367 "should have re-used the previous multibuffer"
22368 );
22369 });
22370
22371 cx.set_state(
22372 &r#"fn one() {
22373 let mut a = ˇtwo();
22374 }
22375 fn three() {}
22376 fn two() {}"#
22377 .unindent(),
22378 );
22379 cx.lsp
22380 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22381 Ok(Some(vec![
22382 lsp::Location {
22383 uri: params.text_document_position.text_document.uri.clone(),
22384 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22385 },
22386 lsp::Location {
22387 uri: params.text_document_position.text_document.uri,
22388 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22389 },
22390 ]))
22391 });
22392 let navigated = cx
22393 .update_editor(|editor, window, cx| {
22394 editor.find_all_references(&FindAllReferences, window, cx)
22395 })
22396 .unwrap()
22397 .await
22398 .expect("Failed to navigate to references");
22399 assert_eq!(
22400 navigated,
22401 Navigated::Yes,
22402 "Should have navigated to references from the FindAllReferences response"
22403 );
22404 cx.assert_editor_state(
22405 &r#"fn one() {
22406 let mut a = ˇtwo();
22407 }
22408 fn three() {}
22409 fn two() {}"#
22410 .unindent(),
22411 );
22412 let editors = cx.update_workspace(|workspace, _, cx| {
22413 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22414 });
22415 cx.update_editor(|_, _, _| {
22416 assert_eq!(
22417 editors.len(),
22418 3,
22419 "should have used a new multibuffer as offsets changed"
22420 );
22421 });
22422}
22423#[gpui::test]
22424async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22425 init_test(cx, |_| {});
22426
22427 let language = Arc::new(Language::new(
22428 LanguageConfig::default(),
22429 Some(tree_sitter_rust::LANGUAGE.into()),
22430 ));
22431
22432 let text = r#"
22433 #[cfg(test)]
22434 mod tests() {
22435 #[test]
22436 fn runnable_1() {
22437 let a = 1;
22438 }
22439
22440 #[test]
22441 fn runnable_2() {
22442 let a = 1;
22443 let b = 2;
22444 }
22445 }
22446 "#
22447 .unindent();
22448
22449 let fs = FakeFs::new(cx.executor());
22450 fs.insert_file("/file.rs", Default::default()).await;
22451
22452 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22453 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22454 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22455 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22456 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22457
22458 let editor = cx.new_window_entity(|window, cx| {
22459 Editor::new(
22460 EditorMode::full(),
22461 multi_buffer,
22462 Some(project.clone()),
22463 window,
22464 cx,
22465 )
22466 });
22467
22468 editor.update_in(cx, |editor, window, cx| {
22469 let snapshot = editor.buffer().read(cx).snapshot(cx);
22470 editor.tasks.insert(
22471 (buffer.read(cx).remote_id(), 3),
22472 RunnableTasks {
22473 templates: vec![],
22474 offset: snapshot.anchor_before(MultiBufferOffset(43)),
22475 column: 0,
22476 extra_variables: HashMap::default(),
22477 context_range: BufferOffset(43)..BufferOffset(85),
22478 },
22479 );
22480 editor.tasks.insert(
22481 (buffer.read(cx).remote_id(), 8),
22482 RunnableTasks {
22483 templates: vec![],
22484 offset: snapshot.anchor_before(MultiBufferOffset(86)),
22485 column: 0,
22486 extra_variables: HashMap::default(),
22487 context_range: BufferOffset(86)..BufferOffset(191),
22488 },
22489 );
22490
22491 // Test finding task when cursor is inside function body
22492 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22493 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22494 });
22495 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22496 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22497
22498 // Test finding task when cursor is on function name
22499 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22500 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22501 });
22502 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22503 assert_eq!(row, 8, "Should find task when cursor is on function name");
22504 });
22505}
22506
22507#[gpui::test]
22508async fn test_folding_buffers(cx: &mut TestAppContext) {
22509 init_test(cx, |_| {});
22510
22511 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22512 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22513 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22514
22515 let fs = FakeFs::new(cx.executor());
22516 fs.insert_tree(
22517 path!("/a"),
22518 json!({
22519 "first.rs": sample_text_1,
22520 "second.rs": sample_text_2,
22521 "third.rs": sample_text_3,
22522 }),
22523 )
22524 .await;
22525 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22526 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22527 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22528 let worktree = project.update(cx, |project, cx| {
22529 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22530 assert_eq!(worktrees.len(), 1);
22531 worktrees.pop().unwrap()
22532 });
22533 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22534
22535 let buffer_1 = project
22536 .update(cx, |project, cx| {
22537 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22538 })
22539 .await
22540 .unwrap();
22541 let buffer_2 = project
22542 .update(cx, |project, cx| {
22543 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22544 })
22545 .await
22546 .unwrap();
22547 let buffer_3 = project
22548 .update(cx, |project, cx| {
22549 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22550 })
22551 .await
22552 .unwrap();
22553
22554 let multi_buffer = cx.new(|cx| {
22555 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22556 multi_buffer.push_excerpts(
22557 buffer_1.clone(),
22558 [
22559 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22560 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22561 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22562 ],
22563 cx,
22564 );
22565 multi_buffer.push_excerpts(
22566 buffer_2.clone(),
22567 [
22568 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22569 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22570 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22571 ],
22572 cx,
22573 );
22574 multi_buffer.push_excerpts(
22575 buffer_3.clone(),
22576 [
22577 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22578 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22579 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22580 ],
22581 cx,
22582 );
22583 multi_buffer
22584 });
22585 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22586 Editor::new(
22587 EditorMode::full(),
22588 multi_buffer.clone(),
22589 Some(project.clone()),
22590 window,
22591 cx,
22592 )
22593 });
22594
22595 assert_eq!(
22596 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22597 "\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",
22598 );
22599
22600 multi_buffer_editor.update(cx, |editor, cx| {
22601 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22602 });
22603 assert_eq!(
22604 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22605 "\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",
22606 "After folding the first buffer, its text should not be displayed"
22607 );
22608
22609 multi_buffer_editor.update(cx, |editor, cx| {
22610 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22611 });
22612 assert_eq!(
22613 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22614 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22615 "After folding the second buffer, its text should not be displayed"
22616 );
22617
22618 multi_buffer_editor.update(cx, |editor, cx| {
22619 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22620 });
22621 assert_eq!(
22622 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22623 "\n\n\n\n\n",
22624 "After folding the third buffer, its text should not be displayed"
22625 );
22626
22627 // Emulate selection inside the fold logic, that should work
22628 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22629 editor
22630 .snapshot(window, cx)
22631 .next_line_boundary(Point::new(0, 4));
22632 });
22633
22634 multi_buffer_editor.update(cx, |editor, cx| {
22635 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22636 });
22637 assert_eq!(
22638 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22639 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22640 "After unfolding the second buffer, its text should be displayed"
22641 );
22642
22643 // Typing inside of buffer 1 causes that buffer to be unfolded.
22644 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22645 assert_eq!(
22646 multi_buffer
22647 .read(cx)
22648 .snapshot(cx)
22649 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22650 .collect::<String>(),
22651 "bbbb"
22652 );
22653 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22654 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22655 });
22656 editor.handle_input("B", window, cx);
22657 });
22658
22659 assert_eq!(
22660 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22661 "\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",
22662 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22663 );
22664
22665 multi_buffer_editor.update(cx, |editor, cx| {
22666 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22667 });
22668 assert_eq!(
22669 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22670 "\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",
22671 "After unfolding the all buffers, all original text should be displayed"
22672 );
22673}
22674
22675#[gpui::test]
22676async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22677 init_test(cx, |_| {});
22678
22679 let sample_text_1 = "1111\n2222\n3333".to_string();
22680 let sample_text_2 = "4444\n5555\n6666".to_string();
22681 let sample_text_3 = "7777\n8888\n9999".to_string();
22682
22683 let fs = FakeFs::new(cx.executor());
22684 fs.insert_tree(
22685 path!("/a"),
22686 json!({
22687 "first.rs": sample_text_1,
22688 "second.rs": sample_text_2,
22689 "third.rs": sample_text_3,
22690 }),
22691 )
22692 .await;
22693 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22694 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22695 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22696 let worktree = project.update(cx, |project, cx| {
22697 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22698 assert_eq!(worktrees.len(), 1);
22699 worktrees.pop().unwrap()
22700 });
22701 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22702
22703 let buffer_1 = project
22704 .update(cx, |project, cx| {
22705 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22706 })
22707 .await
22708 .unwrap();
22709 let buffer_2 = project
22710 .update(cx, |project, cx| {
22711 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22712 })
22713 .await
22714 .unwrap();
22715 let buffer_3 = project
22716 .update(cx, |project, cx| {
22717 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22718 })
22719 .await
22720 .unwrap();
22721
22722 let multi_buffer = cx.new(|cx| {
22723 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22724 multi_buffer.push_excerpts(
22725 buffer_1.clone(),
22726 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22727 cx,
22728 );
22729 multi_buffer.push_excerpts(
22730 buffer_2.clone(),
22731 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22732 cx,
22733 );
22734 multi_buffer.push_excerpts(
22735 buffer_3.clone(),
22736 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22737 cx,
22738 );
22739 multi_buffer
22740 });
22741
22742 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22743 Editor::new(
22744 EditorMode::full(),
22745 multi_buffer,
22746 Some(project.clone()),
22747 window,
22748 cx,
22749 )
22750 });
22751
22752 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22753 assert_eq!(
22754 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22755 full_text,
22756 );
22757
22758 multi_buffer_editor.update(cx, |editor, cx| {
22759 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22760 });
22761 assert_eq!(
22762 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22763 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22764 "After folding the first buffer, its text should not be displayed"
22765 );
22766
22767 multi_buffer_editor.update(cx, |editor, cx| {
22768 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22769 });
22770
22771 assert_eq!(
22772 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22773 "\n\n\n\n\n\n7777\n8888\n9999",
22774 "After folding the second buffer, its text should not be displayed"
22775 );
22776
22777 multi_buffer_editor.update(cx, |editor, cx| {
22778 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22779 });
22780 assert_eq!(
22781 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22782 "\n\n\n\n\n",
22783 "After folding the third buffer, its text should not be displayed"
22784 );
22785
22786 multi_buffer_editor.update(cx, |editor, cx| {
22787 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22788 });
22789 assert_eq!(
22790 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22791 "\n\n\n\n4444\n5555\n6666\n\n",
22792 "After unfolding the second buffer, its text should be displayed"
22793 );
22794
22795 multi_buffer_editor.update(cx, |editor, cx| {
22796 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22797 });
22798 assert_eq!(
22799 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22800 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22801 "After unfolding the first buffer, its text should be displayed"
22802 );
22803
22804 multi_buffer_editor.update(cx, |editor, cx| {
22805 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22806 });
22807 assert_eq!(
22808 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22809 full_text,
22810 "After unfolding all buffers, all original text should be displayed"
22811 );
22812}
22813
22814#[gpui::test]
22815async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22816 init_test(cx, |_| {});
22817
22818 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22819
22820 let fs = FakeFs::new(cx.executor());
22821 fs.insert_tree(
22822 path!("/a"),
22823 json!({
22824 "main.rs": sample_text,
22825 }),
22826 )
22827 .await;
22828 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22830 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22831 let worktree = project.update(cx, |project, cx| {
22832 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22833 assert_eq!(worktrees.len(), 1);
22834 worktrees.pop().unwrap()
22835 });
22836 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22837
22838 let buffer_1 = project
22839 .update(cx, |project, cx| {
22840 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22841 })
22842 .await
22843 .unwrap();
22844
22845 let multi_buffer = cx.new(|cx| {
22846 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22847 multi_buffer.push_excerpts(
22848 buffer_1.clone(),
22849 [ExcerptRange::new(
22850 Point::new(0, 0)
22851 ..Point::new(
22852 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22853 0,
22854 ),
22855 )],
22856 cx,
22857 );
22858 multi_buffer
22859 });
22860 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22861 Editor::new(
22862 EditorMode::full(),
22863 multi_buffer,
22864 Some(project.clone()),
22865 window,
22866 cx,
22867 )
22868 });
22869
22870 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22871 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22872 enum TestHighlight {}
22873 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22874 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22875 editor.highlight_text::<TestHighlight>(
22876 vec![highlight_range.clone()],
22877 HighlightStyle::color(Hsla::green()),
22878 cx,
22879 );
22880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22881 s.select_ranges(Some(highlight_range))
22882 });
22883 });
22884
22885 let full_text = format!("\n\n{sample_text}");
22886 assert_eq!(
22887 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22888 full_text,
22889 );
22890}
22891
22892#[gpui::test]
22893async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22894 init_test(cx, |_| {});
22895 cx.update(|cx| {
22896 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22897 "keymaps/default-linux.json",
22898 cx,
22899 )
22900 .unwrap();
22901 cx.bind_keys(default_key_bindings);
22902 });
22903
22904 let (editor, cx) = cx.add_window_view(|window, cx| {
22905 let multi_buffer = MultiBuffer::build_multi(
22906 [
22907 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22908 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22909 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22910 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22911 ],
22912 cx,
22913 );
22914 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22915
22916 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22917 // fold all but the second buffer, so that we test navigating between two
22918 // adjacent folded buffers, as well as folded buffers at the start and
22919 // end the multibuffer
22920 editor.fold_buffer(buffer_ids[0], cx);
22921 editor.fold_buffer(buffer_ids[2], cx);
22922 editor.fold_buffer(buffer_ids[3], cx);
22923
22924 editor
22925 });
22926 cx.simulate_resize(size(px(1000.), px(1000.)));
22927
22928 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22929 cx.assert_excerpts_with_selections(indoc! {"
22930 [EXCERPT]
22931 ˇ[FOLDED]
22932 [EXCERPT]
22933 a1
22934 b1
22935 [EXCERPT]
22936 [FOLDED]
22937 [EXCERPT]
22938 [FOLDED]
22939 "
22940 });
22941 cx.simulate_keystroke("down");
22942 cx.assert_excerpts_with_selections(indoc! {"
22943 [EXCERPT]
22944 [FOLDED]
22945 [EXCERPT]
22946 ˇa1
22947 b1
22948 [EXCERPT]
22949 [FOLDED]
22950 [EXCERPT]
22951 [FOLDED]
22952 "
22953 });
22954 cx.simulate_keystroke("down");
22955 cx.assert_excerpts_with_selections(indoc! {"
22956 [EXCERPT]
22957 [FOLDED]
22958 [EXCERPT]
22959 a1
22960 ˇb1
22961 [EXCERPT]
22962 [FOLDED]
22963 [EXCERPT]
22964 [FOLDED]
22965 "
22966 });
22967 cx.simulate_keystroke("down");
22968 cx.assert_excerpts_with_selections(indoc! {"
22969 [EXCERPT]
22970 [FOLDED]
22971 [EXCERPT]
22972 a1
22973 b1
22974 ˇ[EXCERPT]
22975 [FOLDED]
22976 [EXCERPT]
22977 [FOLDED]
22978 "
22979 });
22980 cx.simulate_keystroke("down");
22981 cx.assert_excerpts_with_selections(indoc! {"
22982 [EXCERPT]
22983 [FOLDED]
22984 [EXCERPT]
22985 a1
22986 b1
22987 [EXCERPT]
22988 ˇ[FOLDED]
22989 [EXCERPT]
22990 [FOLDED]
22991 "
22992 });
22993 for _ in 0..5 {
22994 cx.simulate_keystroke("down");
22995 cx.assert_excerpts_with_selections(indoc! {"
22996 [EXCERPT]
22997 [FOLDED]
22998 [EXCERPT]
22999 a1
23000 b1
23001 [EXCERPT]
23002 [FOLDED]
23003 [EXCERPT]
23004 ˇ[FOLDED]
23005 "
23006 });
23007 }
23008
23009 cx.simulate_keystroke("up");
23010 cx.assert_excerpts_with_selections(indoc! {"
23011 [EXCERPT]
23012 [FOLDED]
23013 [EXCERPT]
23014 a1
23015 b1
23016 [EXCERPT]
23017 ˇ[FOLDED]
23018 [EXCERPT]
23019 [FOLDED]
23020 "
23021 });
23022 cx.simulate_keystroke("up");
23023 cx.assert_excerpts_with_selections(indoc! {"
23024 [EXCERPT]
23025 [FOLDED]
23026 [EXCERPT]
23027 a1
23028 b1
23029 ˇ[EXCERPT]
23030 [FOLDED]
23031 [EXCERPT]
23032 [FOLDED]
23033 "
23034 });
23035 cx.simulate_keystroke("up");
23036 cx.assert_excerpts_with_selections(indoc! {"
23037 [EXCERPT]
23038 [FOLDED]
23039 [EXCERPT]
23040 a1
23041 ˇb1
23042 [EXCERPT]
23043 [FOLDED]
23044 [EXCERPT]
23045 [FOLDED]
23046 "
23047 });
23048 cx.simulate_keystroke("up");
23049 cx.assert_excerpts_with_selections(indoc! {"
23050 [EXCERPT]
23051 [FOLDED]
23052 [EXCERPT]
23053 ˇa1
23054 b1
23055 [EXCERPT]
23056 [FOLDED]
23057 [EXCERPT]
23058 [FOLDED]
23059 "
23060 });
23061 for _ in 0..5 {
23062 cx.simulate_keystroke("up");
23063 cx.assert_excerpts_with_selections(indoc! {"
23064 [EXCERPT]
23065 ˇ[FOLDED]
23066 [EXCERPT]
23067 a1
23068 b1
23069 [EXCERPT]
23070 [FOLDED]
23071 [EXCERPT]
23072 [FOLDED]
23073 "
23074 });
23075 }
23076}
23077
23078#[gpui::test]
23079async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23080 init_test(cx, |_| {});
23081
23082 // Simple insertion
23083 assert_highlighted_edits(
23084 "Hello, world!",
23085 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23086 true,
23087 cx,
23088 |highlighted_edits, cx| {
23089 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23090 assert_eq!(highlighted_edits.highlights.len(), 1);
23091 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23092 assert_eq!(
23093 highlighted_edits.highlights[0].1.background_color,
23094 Some(cx.theme().status().created_background)
23095 );
23096 },
23097 )
23098 .await;
23099
23100 // Replacement
23101 assert_highlighted_edits(
23102 "This is a test.",
23103 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23104 false,
23105 cx,
23106 |highlighted_edits, cx| {
23107 assert_eq!(highlighted_edits.text, "That is a test.");
23108 assert_eq!(highlighted_edits.highlights.len(), 1);
23109 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23110 assert_eq!(
23111 highlighted_edits.highlights[0].1.background_color,
23112 Some(cx.theme().status().created_background)
23113 );
23114 },
23115 )
23116 .await;
23117
23118 // Multiple edits
23119 assert_highlighted_edits(
23120 "Hello, world!",
23121 vec![
23122 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23123 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23124 ],
23125 false,
23126 cx,
23127 |highlighted_edits, cx| {
23128 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23129 assert_eq!(highlighted_edits.highlights.len(), 2);
23130 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23131 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23132 assert_eq!(
23133 highlighted_edits.highlights[0].1.background_color,
23134 Some(cx.theme().status().created_background)
23135 );
23136 assert_eq!(
23137 highlighted_edits.highlights[1].1.background_color,
23138 Some(cx.theme().status().created_background)
23139 );
23140 },
23141 )
23142 .await;
23143
23144 // Multiple lines with edits
23145 assert_highlighted_edits(
23146 "First line\nSecond line\nThird line\nFourth line",
23147 vec![
23148 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23149 (
23150 Point::new(2, 0)..Point::new(2, 10),
23151 "New third line".to_string(),
23152 ),
23153 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23154 ],
23155 false,
23156 cx,
23157 |highlighted_edits, cx| {
23158 assert_eq!(
23159 highlighted_edits.text,
23160 "Second modified\nNew third line\nFourth updated line"
23161 );
23162 assert_eq!(highlighted_edits.highlights.len(), 3);
23163 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23164 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23165 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23166 for highlight in &highlighted_edits.highlights {
23167 assert_eq!(
23168 highlight.1.background_color,
23169 Some(cx.theme().status().created_background)
23170 );
23171 }
23172 },
23173 )
23174 .await;
23175}
23176
23177#[gpui::test]
23178async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23179 init_test(cx, |_| {});
23180
23181 // Deletion
23182 assert_highlighted_edits(
23183 "Hello, world!",
23184 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23185 true,
23186 cx,
23187 |highlighted_edits, cx| {
23188 assert_eq!(highlighted_edits.text, "Hello, world!");
23189 assert_eq!(highlighted_edits.highlights.len(), 1);
23190 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23191 assert_eq!(
23192 highlighted_edits.highlights[0].1.background_color,
23193 Some(cx.theme().status().deleted_background)
23194 );
23195 },
23196 )
23197 .await;
23198
23199 // Insertion
23200 assert_highlighted_edits(
23201 "Hello, world!",
23202 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23203 true,
23204 cx,
23205 |highlighted_edits, cx| {
23206 assert_eq!(highlighted_edits.highlights.len(), 1);
23207 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23208 assert_eq!(
23209 highlighted_edits.highlights[0].1.background_color,
23210 Some(cx.theme().status().created_background)
23211 );
23212 },
23213 )
23214 .await;
23215}
23216
23217async fn assert_highlighted_edits(
23218 text: &str,
23219 edits: Vec<(Range<Point>, String)>,
23220 include_deletions: bool,
23221 cx: &mut TestAppContext,
23222 assertion_fn: impl Fn(HighlightedText, &App),
23223) {
23224 let window = cx.add_window(|window, cx| {
23225 let buffer = MultiBuffer::build_simple(text, cx);
23226 Editor::new(EditorMode::full(), buffer, None, window, cx)
23227 });
23228 let cx = &mut VisualTestContext::from_window(*window, cx);
23229
23230 let (buffer, snapshot) = window
23231 .update(cx, |editor, _window, cx| {
23232 (
23233 editor.buffer().clone(),
23234 editor.buffer().read(cx).snapshot(cx),
23235 )
23236 })
23237 .unwrap();
23238
23239 let edits = edits
23240 .into_iter()
23241 .map(|(range, edit)| {
23242 (
23243 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23244 edit,
23245 )
23246 })
23247 .collect::<Vec<_>>();
23248
23249 let text_anchor_edits = edits
23250 .clone()
23251 .into_iter()
23252 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23253 .collect::<Vec<_>>();
23254
23255 let edit_preview = window
23256 .update(cx, |_, _window, cx| {
23257 buffer
23258 .read(cx)
23259 .as_singleton()
23260 .unwrap()
23261 .read(cx)
23262 .preview_edits(text_anchor_edits.into(), cx)
23263 })
23264 .unwrap()
23265 .await;
23266
23267 cx.update(|_window, cx| {
23268 let highlighted_edits = edit_prediction_edit_text(
23269 snapshot.as_singleton().unwrap().2,
23270 &edits,
23271 &edit_preview,
23272 include_deletions,
23273 cx,
23274 );
23275 assertion_fn(highlighted_edits, cx)
23276 });
23277}
23278
23279#[track_caller]
23280fn assert_breakpoint(
23281 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23282 path: &Arc<Path>,
23283 expected: Vec<(u32, Breakpoint)>,
23284) {
23285 if expected.is_empty() {
23286 assert!(!breakpoints.contains_key(path), "{}", path.display());
23287 } else {
23288 let mut breakpoint = breakpoints
23289 .get(path)
23290 .unwrap()
23291 .iter()
23292 .map(|breakpoint| {
23293 (
23294 breakpoint.row,
23295 Breakpoint {
23296 message: breakpoint.message.clone(),
23297 state: breakpoint.state,
23298 condition: breakpoint.condition.clone(),
23299 hit_condition: breakpoint.hit_condition.clone(),
23300 },
23301 )
23302 })
23303 .collect::<Vec<_>>();
23304
23305 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23306
23307 assert_eq!(expected, breakpoint);
23308 }
23309}
23310
23311fn add_log_breakpoint_at_cursor(
23312 editor: &mut Editor,
23313 log_message: &str,
23314 window: &mut Window,
23315 cx: &mut Context<Editor>,
23316) {
23317 let (anchor, bp) = editor
23318 .breakpoints_at_cursors(window, cx)
23319 .first()
23320 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23321 .unwrap_or_else(|| {
23322 let snapshot = editor.snapshot(window, cx);
23323 let cursor_position: Point =
23324 editor.selections.newest(&snapshot.display_snapshot).head();
23325
23326 let breakpoint_position = snapshot
23327 .buffer_snapshot()
23328 .anchor_before(Point::new(cursor_position.row, 0));
23329
23330 (breakpoint_position, Breakpoint::new_log(log_message))
23331 });
23332
23333 editor.edit_breakpoint_at_anchor(
23334 anchor,
23335 bp,
23336 BreakpointEditAction::EditLogMessage(log_message.into()),
23337 cx,
23338 );
23339}
23340
23341#[gpui::test]
23342async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23343 init_test(cx, |_| {});
23344
23345 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23346 let fs = FakeFs::new(cx.executor());
23347 fs.insert_tree(
23348 path!("/a"),
23349 json!({
23350 "main.rs": sample_text,
23351 }),
23352 )
23353 .await;
23354 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23355 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23356 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23357
23358 let fs = FakeFs::new(cx.executor());
23359 fs.insert_tree(
23360 path!("/a"),
23361 json!({
23362 "main.rs": sample_text,
23363 }),
23364 )
23365 .await;
23366 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23367 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23368 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23369 let worktree_id = workspace
23370 .update(cx, |workspace, _window, cx| {
23371 workspace.project().update(cx, |project, cx| {
23372 project.worktrees(cx).next().unwrap().read(cx).id()
23373 })
23374 })
23375 .unwrap();
23376
23377 let buffer = project
23378 .update(cx, |project, cx| {
23379 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23380 })
23381 .await
23382 .unwrap();
23383
23384 let (editor, cx) = cx.add_window_view(|window, cx| {
23385 Editor::new(
23386 EditorMode::full(),
23387 MultiBuffer::build_from_buffer(buffer, cx),
23388 Some(project.clone()),
23389 window,
23390 cx,
23391 )
23392 });
23393
23394 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23395 let abs_path = project.read_with(cx, |project, cx| {
23396 project
23397 .absolute_path(&project_path, cx)
23398 .map(Arc::from)
23399 .unwrap()
23400 });
23401
23402 // assert we can add breakpoint on the first line
23403 editor.update_in(cx, |editor, window, cx| {
23404 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23405 editor.move_to_end(&MoveToEnd, window, cx);
23406 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23407 });
23408
23409 let breakpoints = editor.update(cx, |editor, cx| {
23410 editor
23411 .breakpoint_store()
23412 .as_ref()
23413 .unwrap()
23414 .read(cx)
23415 .all_source_breakpoints(cx)
23416 });
23417
23418 assert_eq!(1, breakpoints.len());
23419 assert_breakpoint(
23420 &breakpoints,
23421 &abs_path,
23422 vec![
23423 (0, Breakpoint::new_standard()),
23424 (3, Breakpoint::new_standard()),
23425 ],
23426 );
23427
23428 editor.update_in(cx, |editor, window, cx| {
23429 editor.move_to_beginning(&MoveToBeginning, window, cx);
23430 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23431 });
23432
23433 let breakpoints = editor.update(cx, |editor, cx| {
23434 editor
23435 .breakpoint_store()
23436 .as_ref()
23437 .unwrap()
23438 .read(cx)
23439 .all_source_breakpoints(cx)
23440 });
23441
23442 assert_eq!(1, breakpoints.len());
23443 assert_breakpoint(
23444 &breakpoints,
23445 &abs_path,
23446 vec![(3, Breakpoint::new_standard())],
23447 );
23448
23449 editor.update_in(cx, |editor, window, cx| {
23450 editor.move_to_end(&MoveToEnd, window, cx);
23451 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23452 });
23453
23454 let breakpoints = editor.update(cx, |editor, cx| {
23455 editor
23456 .breakpoint_store()
23457 .as_ref()
23458 .unwrap()
23459 .read(cx)
23460 .all_source_breakpoints(cx)
23461 });
23462
23463 assert_eq!(0, breakpoints.len());
23464 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23465}
23466
23467#[gpui::test]
23468async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23469 init_test(cx, |_| {});
23470
23471 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23472
23473 let fs = FakeFs::new(cx.executor());
23474 fs.insert_tree(
23475 path!("/a"),
23476 json!({
23477 "main.rs": sample_text,
23478 }),
23479 )
23480 .await;
23481 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23482 let (workspace, cx) =
23483 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23484
23485 let worktree_id = workspace.update(cx, |workspace, cx| {
23486 workspace.project().update(cx, |project, cx| {
23487 project.worktrees(cx).next().unwrap().read(cx).id()
23488 })
23489 });
23490
23491 let buffer = project
23492 .update(cx, |project, cx| {
23493 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23494 })
23495 .await
23496 .unwrap();
23497
23498 let (editor, cx) = cx.add_window_view(|window, cx| {
23499 Editor::new(
23500 EditorMode::full(),
23501 MultiBuffer::build_from_buffer(buffer, cx),
23502 Some(project.clone()),
23503 window,
23504 cx,
23505 )
23506 });
23507
23508 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23509 let abs_path = project.read_with(cx, |project, cx| {
23510 project
23511 .absolute_path(&project_path, cx)
23512 .map(Arc::from)
23513 .unwrap()
23514 });
23515
23516 editor.update_in(cx, |editor, window, cx| {
23517 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23518 });
23519
23520 let breakpoints = editor.update(cx, |editor, cx| {
23521 editor
23522 .breakpoint_store()
23523 .as_ref()
23524 .unwrap()
23525 .read(cx)
23526 .all_source_breakpoints(cx)
23527 });
23528
23529 assert_breakpoint(
23530 &breakpoints,
23531 &abs_path,
23532 vec![(0, Breakpoint::new_log("hello world"))],
23533 );
23534
23535 // Removing a log message from a log breakpoint should remove it
23536 editor.update_in(cx, |editor, window, cx| {
23537 add_log_breakpoint_at_cursor(editor, "", window, cx);
23538 });
23539
23540 let breakpoints = editor.update(cx, |editor, cx| {
23541 editor
23542 .breakpoint_store()
23543 .as_ref()
23544 .unwrap()
23545 .read(cx)
23546 .all_source_breakpoints(cx)
23547 });
23548
23549 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23550
23551 editor.update_in(cx, |editor, window, cx| {
23552 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23553 editor.move_to_end(&MoveToEnd, window, cx);
23554 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23555 // Not adding a log message to a standard breakpoint shouldn't remove it
23556 add_log_breakpoint_at_cursor(editor, "", window, cx);
23557 });
23558
23559 let breakpoints = editor.update(cx, |editor, cx| {
23560 editor
23561 .breakpoint_store()
23562 .as_ref()
23563 .unwrap()
23564 .read(cx)
23565 .all_source_breakpoints(cx)
23566 });
23567
23568 assert_breakpoint(
23569 &breakpoints,
23570 &abs_path,
23571 vec![
23572 (0, Breakpoint::new_standard()),
23573 (3, Breakpoint::new_standard()),
23574 ],
23575 );
23576
23577 editor.update_in(cx, |editor, window, cx| {
23578 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23579 });
23580
23581 let breakpoints = editor.update(cx, |editor, cx| {
23582 editor
23583 .breakpoint_store()
23584 .as_ref()
23585 .unwrap()
23586 .read(cx)
23587 .all_source_breakpoints(cx)
23588 });
23589
23590 assert_breakpoint(
23591 &breakpoints,
23592 &abs_path,
23593 vec![
23594 (0, Breakpoint::new_standard()),
23595 (3, Breakpoint::new_log("hello world")),
23596 ],
23597 );
23598
23599 editor.update_in(cx, |editor, window, cx| {
23600 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23601 });
23602
23603 let breakpoints = editor.update(cx, |editor, cx| {
23604 editor
23605 .breakpoint_store()
23606 .as_ref()
23607 .unwrap()
23608 .read(cx)
23609 .all_source_breakpoints(cx)
23610 });
23611
23612 assert_breakpoint(
23613 &breakpoints,
23614 &abs_path,
23615 vec![
23616 (0, Breakpoint::new_standard()),
23617 (3, Breakpoint::new_log("hello Earth!!")),
23618 ],
23619 );
23620}
23621
23622/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23623/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23624/// or when breakpoints were placed out of order. This tests for a regression too
23625#[gpui::test]
23626async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23627 init_test(cx, |_| {});
23628
23629 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23630 let fs = FakeFs::new(cx.executor());
23631 fs.insert_tree(
23632 path!("/a"),
23633 json!({
23634 "main.rs": sample_text,
23635 }),
23636 )
23637 .await;
23638 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23639 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23640 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23641
23642 let fs = FakeFs::new(cx.executor());
23643 fs.insert_tree(
23644 path!("/a"),
23645 json!({
23646 "main.rs": sample_text,
23647 }),
23648 )
23649 .await;
23650 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23651 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23652 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23653 let worktree_id = workspace
23654 .update(cx, |workspace, _window, cx| {
23655 workspace.project().update(cx, |project, cx| {
23656 project.worktrees(cx).next().unwrap().read(cx).id()
23657 })
23658 })
23659 .unwrap();
23660
23661 let buffer = project
23662 .update(cx, |project, cx| {
23663 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23664 })
23665 .await
23666 .unwrap();
23667
23668 let (editor, cx) = cx.add_window_view(|window, cx| {
23669 Editor::new(
23670 EditorMode::full(),
23671 MultiBuffer::build_from_buffer(buffer, cx),
23672 Some(project.clone()),
23673 window,
23674 cx,
23675 )
23676 });
23677
23678 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23679 let abs_path = project.read_with(cx, |project, cx| {
23680 project
23681 .absolute_path(&project_path, cx)
23682 .map(Arc::from)
23683 .unwrap()
23684 });
23685
23686 // assert we can add breakpoint on the first line
23687 editor.update_in(cx, |editor, window, cx| {
23688 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23689 editor.move_to_end(&MoveToEnd, window, cx);
23690 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23691 editor.move_up(&MoveUp, window, cx);
23692 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23693 });
23694
23695 let breakpoints = editor.update(cx, |editor, cx| {
23696 editor
23697 .breakpoint_store()
23698 .as_ref()
23699 .unwrap()
23700 .read(cx)
23701 .all_source_breakpoints(cx)
23702 });
23703
23704 assert_eq!(1, breakpoints.len());
23705 assert_breakpoint(
23706 &breakpoints,
23707 &abs_path,
23708 vec![
23709 (0, Breakpoint::new_standard()),
23710 (2, Breakpoint::new_standard()),
23711 (3, Breakpoint::new_standard()),
23712 ],
23713 );
23714
23715 editor.update_in(cx, |editor, window, cx| {
23716 editor.move_to_beginning(&MoveToBeginning, window, cx);
23717 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23718 editor.move_to_end(&MoveToEnd, window, cx);
23719 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23720 // Disabling a breakpoint that doesn't exist should do nothing
23721 editor.move_up(&MoveUp, window, cx);
23722 editor.move_up(&MoveUp, window, cx);
23723 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23724 });
23725
23726 let breakpoints = editor.update(cx, |editor, cx| {
23727 editor
23728 .breakpoint_store()
23729 .as_ref()
23730 .unwrap()
23731 .read(cx)
23732 .all_source_breakpoints(cx)
23733 });
23734
23735 let disable_breakpoint = {
23736 let mut bp = Breakpoint::new_standard();
23737 bp.state = BreakpointState::Disabled;
23738 bp
23739 };
23740
23741 assert_eq!(1, breakpoints.len());
23742 assert_breakpoint(
23743 &breakpoints,
23744 &abs_path,
23745 vec![
23746 (0, disable_breakpoint.clone()),
23747 (2, Breakpoint::new_standard()),
23748 (3, disable_breakpoint.clone()),
23749 ],
23750 );
23751
23752 editor.update_in(cx, |editor, window, cx| {
23753 editor.move_to_beginning(&MoveToBeginning, window, cx);
23754 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23755 editor.move_to_end(&MoveToEnd, window, cx);
23756 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23757 editor.move_up(&MoveUp, window, cx);
23758 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23759 });
23760
23761 let breakpoints = editor.update(cx, |editor, cx| {
23762 editor
23763 .breakpoint_store()
23764 .as_ref()
23765 .unwrap()
23766 .read(cx)
23767 .all_source_breakpoints(cx)
23768 });
23769
23770 assert_eq!(1, breakpoints.len());
23771 assert_breakpoint(
23772 &breakpoints,
23773 &abs_path,
23774 vec![
23775 (0, Breakpoint::new_standard()),
23776 (2, disable_breakpoint),
23777 (3, Breakpoint::new_standard()),
23778 ],
23779 );
23780}
23781
23782#[gpui::test]
23783async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23784 init_test(cx, |_| {});
23785 let capabilities = lsp::ServerCapabilities {
23786 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23787 prepare_provider: Some(true),
23788 work_done_progress_options: Default::default(),
23789 })),
23790 ..Default::default()
23791 };
23792 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23793
23794 cx.set_state(indoc! {"
23795 struct Fˇoo {}
23796 "});
23797
23798 cx.update_editor(|editor, _, cx| {
23799 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23800 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23801 editor.highlight_background::<DocumentHighlightRead>(
23802 &[highlight_range],
23803 |theme| theme.colors().editor_document_highlight_read_background,
23804 cx,
23805 );
23806 });
23807
23808 let mut prepare_rename_handler = cx
23809 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23810 move |_, _, _| async move {
23811 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23812 start: lsp::Position {
23813 line: 0,
23814 character: 7,
23815 },
23816 end: lsp::Position {
23817 line: 0,
23818 character: 10,
23819 },
23820 })))
23821 },
23822 );
23823 let prepare_rename_task = cx
23824 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23825 .expect("Prepare rename was not started");
23826 prepare_rename_handler.next().await.unwrap();
23827 prepare_rename_task.await.expect("Prepare rename failed");
23828
23829 let mut rename_handler =
23830 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23831 let edit = lsp::TextEdit {
23832 range: lsp::Range {
23833 start: lsp::Position {
23834 line: 0,
23835 character: 7,
23836 },
23837 end: lsp::Position {
23838 line: 0,
23839 character: 10,
23840 },
23841 },
23842 new_text: "FooRenamed".to_string(),
23843 };
23844 Ok(Some(lsp::WorkspaceEdit::new(
23845 // Specify the same edit twice
23846 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23847 )))
23848 });
23849 let rename_task = cx
23850 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23851 .expect("Confirm rename was not started");
23852 rename_handler.next().await.unwrap();
23853 rename_task.await.expect("Confirm rename failed");
23854 cx.run_until_parked();
23855
23856 // Despite two edits, only one is actually applied as those are identical
23857 cx.assert_editor_state(indoc! {"
23858 struct FooRenamedˇ {}
23859 "});
23860}
23861
23862#[gpui::test]
23863async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23864 init_test(cx, |_| {});
23865 // These capabilities indicate that the server does not support prepare rename.
23866 let capabilities = lsp::ServerCapabilities {
23867 rename_provider: Some(lsp::OneOf::Left(true)),
23868 ..Default::default()
23869 };
23870 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23871
23872 cx.set_state(indoc! {"
23873 struct Fˇoo {}
23874 "});
23875
23876 cx.update_editor(|editor, _window, cx| {
23877 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23878 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23879 editor.highlight_background::<DocumentHighlightRead>(
23880 &[highlight_range],
23881 |theme| theme.colors().editor_document_highlight_read_background,
23882 cx,
23883 );
23884 });
23885
23886 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23887 .expect("Prepare rename was not started")
23888 .await
23889 .expect("Prepare rename failed");
23890
23891 let mut rename_handler =
23892 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23893 let edit = lsp::TextEdit {
23894 range: lsp::Range {
23895 start: lsp::Position {
23896 line: 0,
23897 character: 7,
23898 },
23899 end: lsp::Position {
23900 line: 0,
23901 character: 10,
23902 },
23903 },
23904 new_text: "FooRenamed".to_string(),
23905 };
23906 Ok(Some(lsp::WorkspaceEdit::new(
23907 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23908 )))
23909 });
23910 let rename_task = cx
23911 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23912 .expect("Confirm rename was not started");
23913 rename_handler.next().await.unwrap();
23914 rename_task.await.expect("Confirm rename failed");
23915 cx.run_until_parked();
23916
23917 // Correct range is renamed, as `surrounding_word` is used to find it.
23918 cx.assert_editor_state(indoc! {"
23919 struct FooRenamedˇ {}
23920 "});
23921}
23922
23923#[gpui::test]
23924async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23925 init_test(cx, |_| {});
23926 let mut cx = EditorTestContext::new(cx).await;
23927
23928 let language = Arc::new(
23929 Language::new(
23930 LanguageConfig::default(),
23931 Some(tree_sitter_html::LANGUAGE.into()),
23932 )
23933 .with_brackets_query(
23934 r#"
23935 ("<" @open "/>" @close)
23936 ("</" @open ">" @close)
23937 ("<" @open ">" @close)
23938 ("\"" @open "\"" @close)
23939 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23940 "#,
23941 )
23942 .unwrap(),
23943 );
23944 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23945
23946 cx.set_state(indoc! {"
23947 <span>ˇ</span>
23948 "});
23949 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23950 cx.assert_editor_state(indoc! {"
23951 <span>
23952 ˇ
23953 </span>
23954 "});
23955
23956 cx.set_state(indoc! {"
23957 <span><span></span>ˇ</span>
23958 "});
23959 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23960 cx.assert_editor_state(indoc! {"
23961 <span><span></span>
23962 ˇ</span>
23963 "});
23964
23965 cx.set_state(indoc! {"
23966 <span>ˇ
23967 </span>
23968 "});
23969 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23970 cx.assert_editor_state(indoc! {"
23971 <span>
23972 ˇ
23973 </span>
23974 "});
23975}
23976
23977#[gpui::test(iterations = 10)]
23978async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23979 init_test(cx, |_| {});
23980
23981 let fs = FakeFs::new(cx.executor());
23982 fs.insert_tree(
23983 path!("/dir"),
23984 json!({
23985 "a.ts": "a",
23986 }),
23987 )
23988 .await;
23989
23990 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23991 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23992 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23993
23994 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23995 language_registry.add(Arc::new(Language::new(
23996 LanguageConfig {
23997 name: "TypeScript".into(),
23998 matcher: LanguageMatcher {
23999 path_suffixes: vec!["ts".to_string()],
24000 ..Default::default()
24001 },
24002 ..Default::default()
24003 },
24004 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24005 )));
24006 let mut fake_language_servers = language_registry.register_fake_lsp(
24007 "TypeScript",
24008 FakeLspAdapter {
24009 capabilities: lsp::ServerCapabilities {
24010 code_lens_provider: Some(lsp::CodeLensOptions {
24011 resolve_provider: Some(true),
24012 }),
24013 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24014 commands: vec!["_the/command".to_string()],
24015 ..lsp::ExecuteCommandOptions::default()
24016 }),
24017 ..lsp::ServerCapabilities::default()
24018 },
24019 ..FakeLspAdapter::default()
24020 },
24021 );
24022
24023 let editor = workspace
24024 .update(cx, |workspace, window, cx| {
24025 workspace.open_abs_path(
24026 PathBuf::from(path!("/dir/a.ts")),
24027 OpenOptions::default(),
24028 window,
24029 cx,
24030 )
24031 })
24032 .unwrap()
24033 .await
24034 .unwrap()
24035 .downcast::<Editor>()
24036 .unwrap();
24037 cx.executor().run_until_parked();
24038
24039 let fake_server = fake_language_servers.next().await.unwrap();
24040
24041 let buffer = editor.update(cx, |editor, cx| {
24042 editor
24043 .buffer()
24044 .read(cx)
24045 .as_singleton()
24046 .expect("have opened a single file by path")
24047 });
24048
24049 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24050 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24051 drop(buffer_snapshot);
24052 let actions = cx
24053 .update_window(*workspace, |_, window, cx| {
24054 project.code_actions(&buffer, anchor..anchor, window, cx)
24055 })
24056 .unwrap();
24057
24058 fake_server
24059 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24060 Ok(Some(vec![
24061 lsp::CodeLens {
24062 range: lsp::Range::default(),
24063 command: Some(lsp::Command {
24064 title: "Code lens command".to_owned(),
24065 command: "_the/command".to_owned(),
24066 arguments: None,
24067 }),
24068 data: None,
24069 },
24070 lsp::CodeLens {
24071 range: lsp::Range::default(),
24072 command: Some(lsp::Command {
24073 title: "Command not in capabilities".to_owned(),
24074 command: "not in capabilities".to_owned(),
24075 arguments: None,
24076 }),
24077 data: None,
24078 },
24079 lsp::CodeLens {
24080 range: lsp::Range {
24081 start: lsp::Position {
24082 line: 1,
24083 character: 1,
24084 },
24085 end: lsp::Position {
24086 line: 1,
24087 character: 1,
24088 },
24089 },
24090 command: Some(lsp::Command {
24091 title: "Command not in range".to_owned(),
24092 command: "_the/command".to_owned(),
24093 arguments: None,
24094 }),
24095 data: None,
24096 },
24097 ]))
24098 })
24099 .next()
24100 .await;
24101
24102 let actions = actions.await.unwrap();
24103 assert_eq!(
24104 actions.len(),
24105 1,
24106 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24107 );
24108 let action = actions[0].clone();
24109 let apply = project.update(cx, |project, cx| {
24110 project.apply_code_action(buffer.clone(), action, true, cx)
24111 });
24112
24113 // Resolving the code action does not populate its edits. In absence of
24114 // edits, we must execute the given command.
24115 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24116 |mut lens, _| async move {
24117 let lens_command = lens.command.as_mut().expect("should have a command");
24118 assert_eq!(lens_command.title, "Code lens command");
24119 lens_command.arguments = Some(vec![json!("the-argument")]);
24120 Ok(lens)
24121 },
24122 );
24123
24124 // While executing the command, the language server sends the editor
24125 // a `workspaceEdit` request.
24126 fake_server
24127 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24128 let fake = fake_server.clone();
24129 move |params, _| {
24130 assert_eq!(params.command, "_the/command");
24131 let fake = fake.clone();
24132 async move {
24133 fake.server
24134 .request::<lsp::request::ApplyWorkspaceEdit>(
24135 lsp::ApplyWorkspaceEditParams {
24136 label: None,
24137 edit: lsp::WorkspaceEdit {
24138 changes: Some(
24139 [(
24140 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24141 vec![lsp::TextEdit {
24142 range: lsp::Range::new(
24143 lsp::Position::new(0, 0),
24144 lsp::Position::new(0, 0),
24145 ),
24146 new_text: "X".into(),
24147 }],
24148 )]
24149 .into_iter()
24150 .collect(),
24151 ),
24152 ..lsp::WorkspaceEdit::default()
24153 },
24154 },
24155 )
24156 .await
24157 .into_response()
24158 .unwrap();
24159 Ok(Some(json!(null)))
24160 }
24161 }
24162 })
24163 .next()
24164 .await;
24165
24166 // Applying the code lens command returns a project transaction containing the edits
24167 // sent by the language server in its `workspaceEdit` request.
24168 let transaction = apply.await.unwrap();
24169 assert!(transaction.0.contains_key(&buffer));
24170 buffer.update(cx, |buffer, cx| {
24171 assert_eq!(buffer.text(), "Xa");
24172 buffer.undo(cx);
24173 assert_eq!(buffer.text(), "a");
24174 });
24175
24176 let actions_after_edits = cx
24177 .update_window(*workspace, |_, window, cx| {
24178 project.code_actions(&buffer, anchor..anchor, window, cx)
24179 })
24180 .unwrap()
24181 .await
24182 .unwrap();
24183 assert_eq!(
24184 actions, actions_after_edits,
24185 "For the same selection, same code lens actions should be returned"
24186 );
24187
24188 let _responses =
24189 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24190 panic!("No more code lens requests are expected");
24191 });
24192 editor.update_in(cx, |editor, window, cx| {
24193 editor.select_all(&SelectAll, window, cx);
24194 });
24195 cx.executor().run_until_parked();
24196 let new_actions = cx
24197 .update_window(*workspace, |_, window, cx| {
24198 project.code_actions(&buffer, anchor..anchor, window, cx)
24199 })
24200 .unwrap()
24201 .await
24202 .unwrap();
24203 assert_eq!(
24204 actions, new_actions,
24205 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24206 );
24207}
24208
24209#[gpui::test]
24210async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24211 init_test(cx, |_| {});
24212
24213 let fs = FakeFs::new(cx.executor());
24214 let main_text = r#"fn main() {
24215println!("1");
24216println!("2");
24217println!("3");
24218println!("4");
24219println!("5");
24220}"#;
24221 let lib_text = "mod foo {}";
24222 fs.insert_tree(
24223 path!("/a"),
24224 json!({
24225 "lib.rs": lib_text,
24226 "main.rs": main_text,
24227 }),
24228 )
24229 .await;
24230
24231 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24232 let (workspace, cx) =
24233 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24234 let worktree_id = workspace.update(cx, |workspace, cx| {
24235 workspace.project().update(cx, |project, cx| {
24236 project.worktrees(cx).next().unwrap().read(cx).id()
24237 })
24238 });
24239
24240 let expected_ranges = vec![
24241 Point::new(0, 0)..Point::new(0, 0),
24242 Point::new(1, 0)..Point::new(1, 1),
24243 Point::new(2, 0)..Point::new(2, 2),
24244 Point::new(3, 0)..Point::new(3, 3),
24245 ];
24246
24247 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24248 let editor_1 = workspace
24249 .update_in(cx, |workspace, window, cx| {
24250 workspace.open_path(
24251 (worktree_id, rel_path("main.rs")),
24252 Some(pane_1.downgrade()),
24253 true,
24254 window,
24255 cx,
24256 )
24257 })
24258 .unwrap()
24259 .await
24260 .downcast::<Editor>()
24261 .unwrap();
24262 pane_1.update(cx, |pane, cx| {
24263 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24264 open_editor.update(cx, |editor, cx| {
24265 assert_eq!(
24266 editor.display_text(cx),
24267 main_text,
24268 "Original main.rs text on initial open",
24269 );
24270 assert_eq!(
24271 editor
24272 .selections
24273 .all::<Point>(&editor.display_snapshot(cx))
24274 .into_iter()
24275 .map(|s| s.range())
24276 .collect::<Vec<_>>(),
24277 vec![Point::zero()..Point::zero()],
24278 "Default selections on initial open",
24279 );
24280 })
24281 });
24282 editor_1.update_in(cx, |editor, window, cx| {
24283 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24284 s.select_ranges(expected_ranges.clone());
24285 });
24286 });
24287
24288 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24289 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24290 });
24291 let editor_2 = workspace
24292 .update_in(cx, |workspace, window, cx| {
24293 workspace.open_path(
24294 (worktree_id, rel_path("main.rs")),
24295 Some(pane_2.downgrade()),
24296 true,
24297 window,
24298 cx,
24299 )
24300 })
24301 .unwrap()
24302 .await
24303 .downcast::<Editor>()
24304 .unwrap();
24305 pane_2.update(cx, |pane, cx| {
24306 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24307 open_editor.update(cx, |editor, cx| {
24308 assert_eq!(
24309 editor.display_text(cx),
24310 main_text,
24311 "Original main.rs text on initial open in another panel",
24312 );
24313 assert_eq!(
24314 editor
24315 .selections
24316 .all::<Point>(&editor.display_snapshot(cx))
24317 .into_iter()
24318 .map(|s| s.range())
24319 .collect::<Vec<_>>(),
24320 vec![Point::zero()..Point::zero()],
24321 "Default selections on initial open in another panel",
24322 );
24323 })
24324 });
24325
24326 editor_2.update_in(cx, |editor, window, cx| {
24327 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24328 });
24329
24330 let _other_editor_1 = workspace
24331 .update_in(cx, |workspace, window, cx| {
24332 workspace.open_path(
24333 (worktree_id, rel_path("lib.rs")),
24334 Some(pane_1.downgrade()),
24335 true,
24336 window,
24337 cx,
24338 )
24339 })
24340 .unwrap()
24341 .await
24342 .downcast::<Editor>()
24343 .unwrap();
24344 pane_1
24345 .update_in(cx, |pane, window, cx| {
24346 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24347 })
24348 .await
24349 .unwrap();
24350 drop(editor_1);
24351 pane_1.update(cx, |pane, cx| {
24352 pane.active_item()
24353 .unwrap()
24354 .downcast::<Editor>()
24355 .unwrap()
24356 .update(cx, |editor, cx| {
24357 assert_eq!(
24358 editor.display_text(cx),
24359 lib_text,
24360 "Other file should be open and active",
24361 );
24362 });
24363 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24364 });
24365
24366 let _other_editor_2 = workspace
24367 .update_in(cx, |workspace, window, cx| {
24368 workspace.open_path(
24369 (worktree_id, rel_path("lib.rs")),
24370 Some(pane_2.downgrade()),
24371 true,
24372 window,
24373 cx,
24374 )
24375 })
24376 .unwrap()
24377 .await
24378 .downcast::<Editor>()
24379 .unwrap();
24380 pane_2
24381 .update_in(cx, |pane, window, cx| {
24382 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24383 })
24384 .await
24385 .unwrap();
24386 drop(editor_2);
24387 pane_2.update(cx, |pane, cx| {
24388 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24389 open_editor.update(cx, |editor, cx| {
24390 assert_eq!(
24391 editor.display_text(cx),
24392 lib_text,
24393 "Other file should be open and active in another panel too",
24394 );
24395 });
24396 assert_eq!(
24397 pane.items().count(),
24398 1,
24399 "No other editors should be open in another pane",
24400 );
24401 });
24402
24403 let _editor_1_reopened = workspace
24404 .update_in(cx, |workspace, window, cx| {
24405 workspace.open_path(
24406 (worktree_id, rel_path("main.rs")),
24407 Some(pane_1.downgrade()),
24408 true,
24409 window,
24410 cx,
24411 )
24412 })
24413 .unwrap()
24414 .await
24415 .downcast::<Editor>()
24416 .unwrap();
24417 let _editor_2_reopened = workspace
24418 .update_in(cx, |workspace, window, cx| {
24419 workspace.open_path(
24420 (worktree_id, rel_path("main.rs")),
24421 Some(pane_2.downgrade()),
24422 true,
24423 window,
24424 cx,
24425 )
24426 })
24427 .unwrap()
24428 .await
24429 .downcast::<Editor>()
24430 .unwrap();
24431 pane_1.update(cx, |pane, cx| {
24432 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24433 open_editor.update(cx, |editor, cx| {
24434 assert_eq!(
24435 editor.display_text(cx),
24436 main_text,
24437 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24438 );
24439 assert_eq!(
24440 editor
24441 .selections
24442 .all::<Point>(&editor.display_snapshot(cx))
24443 .into_iter()
24444 .map(|s| s.range())
24445 .collect::<Vec<_>>(),
24446 expected_ranges,
24447 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24448 );
24449 })
24450 });
24451 pane_2.update(cx, |pane, cx| {
24452 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24453 open_editor.update(cx, |editor, cx| {
24454 assert_eq!(
24455 editor.display_text(cx),
24456 r#"fn main() {
24457⋯rintln!("1");
24458⋯intln!("2");
24459⋯ntln!("3");
24460println!("4");
24461println!("5");
24462}"#,
24463 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24464 );
24465 assert_eq!(
24466 editor
24467 .selections
24468 .all::<Point>(&editor.display_snapshot(cx))
24469 .into_iter()
24470 .map(|s| s.range())
24471 .collect::<Vec<_>>(),
24472 vec![Point::zero()..Point::zero()],
24473 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24474 );
24475 })
24476 });
24477}
24478
24479#[gpui::test]
24480async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24481 init_test(cx, |_| {});
24482
24483 let fs = FakeFs::new(cx.executor());
24484 let main_text = r#"fn main() {
24485println!("1");
24486println!("2");
24487println!("3");
24488println!("4");
24489println!("5");
24490}"#;
24491 let lib_text = "mod foo {}";
24492 fs.insert_tree(
24493 path!("/a"),
24494 json!({
24495 "lib.rs": lib_text,
24496 "main.rs": main_text,
24497 }),
24498 )
24499 .await;
24500
24501 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24502 let (workspace, cx) =
24503 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24504 let worktree_id = workspace.update(cx, |workspace, cx| {
24505 workspace.project().update(cx, |project, cx| {
24506 project.worktrees(cx).next().unwrap().read(cx).id()
24507 })
24508 });
24509
24510 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24511 let editor = workspace
24512 .update_in(cx, |workspace, window, cx| {
24513 workspace.open_path(
24514 (worktree_id, rel_path("main.rs")),
24515 Some(pane.downgrade()),
24516 true,
24517 window,
24518 cx,
24519 )
24520 })
24521 .unwrap()
24522 .await
24523 .downcast::<Editor>()
24524 .unwrap();
24525 pane.update(cx, |pane, cx| {
24526 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24527 open_editor.update(cx, |editor, cx| {
24528 assert_eq!(
24529 editor.display_text(cx),
24530 main_text,
24531 "Original main.rs text on initial open",
24532 );
24533 })
24534 });
24535 editor.update_in(cx, |editor, window, cx| {
24536 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24537 });
24538
24539 cx.update_global(|store: &mut SettingsStore, cx| {
24540 store.update_user_settings(cx, |s| {
24541 s.workspace.restore_on_file_reopen = Some(false);
24542 });
24543 });
24544 editor.update_in(cx, |editor, window, cx| {
24545 editor.fold_ranges(
24546 vec![
24547 Point::new(1, 0)..Point::new(1, 1),
24548 Point::new(2, 0)..Point::new(2, 2),
24549 Point::new(3, 0)..Point::new(3, 3),
24550 ],
24551 false,
24552 window,
24553 cx,
24554 );
24555 });
24556 pane.update_in(cx, |pane, window, cx| {
24557 pane.close_all_items(&CloseAllItems::default(), window, cx)
24558 })
24559 .await
24560 .unwrap();
24561 pane.update(cx, |pane, _| {
24562 assert!(pane.active_item().is_none());
24563 });
24564 cx.update_global(|store: &mut SettingsStore, cx| {
24565 store.update_user_settings(cx, |s| {
24566 s.workspace.restore_on_file_reopen = Some(true);
24567 });
24568 });
24569
24570 let _editor_reopened = workspace
24571 .update_in(cx, |workspace, window, cx| {
24572 workspace.open_path(
24573 (worktree_id, rel_path("main.rs")),
24574 Some(pane.downgrade()),
24575 true,
24576 window,
24577 cx,
24578 )
24579 })
24580 .unwrap()
24581 .await
24582 .downcast::<Editor>()
24583 .unwrap();
24584 pane.update(cx, |pane, cx| {
24585 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24586 open_editor.update(cx, |editor, cx| {
24587 assert_eq!(
24588 editor.display_text(cx),
24589 main_text,
24590 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24591 );
24592 })
24593 });
24594}
24595
24596#[gpui::test]
24597async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24598 struct EmptyModalView {
24599 focus_handle: gpui::FocusHandle,
24600 }
24601 impl EventEmitter<DismissEvent> for EmptyModalView {}
24602 impl Render for EmptyModalView {
24603 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24604 div()
24605 }
24606 }
24607 impl Focusable for EmptyModalView {
24608 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24609 self.focus_handle.clone()
24610 }
24611 }
24612 impl workspace::ModalView for EmptyModalView {}
24613 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24614 EmptyModalView {
24615 focus_handle: cx.focus_handle(),
24616 }
24617 }
24618
24619 init_test(cx, |_| {});
24620
24621 let fs = FakeFs::new(cx.executor());
24622 let project = Project::test(fs, [], cx).await;
24623 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24624 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24625 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24626 let editor = cx.new_window_entity(|window, cx| {
24627 Editor::new(
24628 EditorMode::full(),
24629 buffer,
24630 Some(project.clone()),
24631 window,
24632 cx,
24633 )
24634 });
24635 workspace
24636 .update(cx, |workspace, window, cx| {
24637 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24638 })
24639 .unwrap();
24640 editor.update_in(cx, |editor, window, cx| {
24641 editor.open_context_menu(&OpenContextMenu, window, cx);
24642 assert!(editor.mouse_context_menu.is_some());
24643 });
24644 workspace
24645 .update(cx, |workspace, window, cx| {
24646 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24647 })
24648 .unwrap();
24649 cx.read(|cx| {
24650 assert!(editor.read(cx).mouse_context_menu.is_none());
24651 });
24652}
24653
24654fn set_linked_edit_ranges(
24655 opening: (Point, Point),
24656 closing: (Point, Point),
24657 editor: &mut Editor,
24658 cx: &mut Context<Editor>,
24659) {
24660 let Some((buffer, _)) = editor
24661 .buffer
24662 .read(cx)
24663 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24664 else {
24665 panic!("Failed to get buffer for selection position");
24666 };
24667 let buffer = buffer.read(cx);
24668 let buffer_id = buffer.remote_id();
24669 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24670 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24671 let mut linked_ranges = HashMap::default();
24672 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24673 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24674}
24675
24676#[gpui::test]
24677async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24678 init_test(cx, |_| {});
24679
24680 let fs = FakeFs::new(cx.executor());
24681 fs.insert_file(path!("/file.html"), Default::default())
24682 .await;
24683
24684 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24685
24686 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24687 let html_language = Arc::new(Language::new(
24688 LanguageConfig {
24689 name: "HTML".into(),
24690 matcher: LanguageMatcher {
24691 path_suffixes: vec!["html".to_string()],
24692 ..LanguageMatcher::default()
24693 },
24694 brackets: BracketPairConfig {
24695 pairs: vec![BracketPair {
24696 start: "<".into(),
24697 end: ">".into(),
24698 close: true,
24699 ..Default::default()
24700 }],
24701 ..Default::default()
24702 },
24703 ..Default::default()
24704 },
24705 Some(tree_sitter_html::LANGUAGE.into()),
24706 ));
24707 language_registry.add(html_language);
24708 let mut fake_servers = language_registry.register_fake_lsp(
24709 "HTML",
24710 FakeLspAdapter {
24711 capabilities: lsp::ServerCapabilities {
24712 completion_provider: Some(lsp::CompletionOptions {
24713 resolve_provider: Some(true),
24714 ..Default::default()
24715 }),
24716 ..Default::default()
24717 },
24718 ..Default::default()
24719 },
24720 );
24721
24722 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24723 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24724
24725 let worktree_id = workspace
24726 .update(cx, |workspace, _window, cx| {
24727 workspace.project().update(cx, |project, cx| {
24728 project.worktrees(cx).next().unwrap().read(cx).id()
24729 })
24730 })
24731 .unwrap();
24732 project
24733 .update(cx, |project, cx| {
24734 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24735 })
24736 .await
24737 .unwrap();
24738 let editor = workspace
24739 .update(cx, |workspace, window, cx| {
24740 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24741 })
24742 .unwrap()
24743 .await
24744 .unwrap()
24745 .downcast::<Editor>()
24746 .unwrap();
24747
24748 let fake_server = fake_servers.next().await.unwrap();
24749 editor.update_in(cx, |editor, window, cx| {
24750 editor.set_text("<ad></ad>", window, cx);
24751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24752 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24753 });
24754 set_linked_edit_ranges(
24755 (Point::new(0, 1), Point::new(0, 3)),
24756 (Point::new(0, 6), Point::new(0, 8)),
24757 editor,
24758 cx,
24759 );
24760 });
24761 let mut completion_handle =
24762 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24763 Ok(Some(lsp::CompletionResponse::Array(vec![
24764 lsp::CompletionItem {
24765 label: "head".to_string(),
24766 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24767 lsp::InsertReplaceEdit {
24768 new_text: "head".to_string(),
24769 insert: lsp::Range::new(
24770 lsp::Position::new(0, 1),
24771 lsp::Position::new(0, 3),
24772 ),
24773 replace: lsp::Range::new(
24774 lsp::Position::new(0, 1),
24775 lsp::Position::new(0, 3),
24776 ),
24777 },
24778 )),
24779 ..Default::default()
24780 },
24781 ])))
24782 });
24783 editor.update_in(cx, |editor, window, cx| {
24784 editor.show_completions(&ShowCompletions, window, cx);
24785 });
24786 cx.run_until_parked();
24787 completion_handle.next().await.unwrap();
24788 editor.update(cx, |editor, _| {
24789 assert!(
24790 editor.context_menu_visible(),
24791 "Completion menu should be visible"
24792 );
24793 });
24794 editor.update_in(cx, |editor, window, cx| {
24795 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24796 });
24797 cx.executor().run_until_parked();
24798 editor.update(cx, |editor, cx| {
24799 assert_eq!(editor.text(cx), "<head></head>");
24800 });
24801}
24802
24803#[gpui::test]
24804async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24805 init_test(cx, |_| {});
24806
24807 let mut cx = EditorTestContext::new(cx).await;
24808 let language = Arc::new(Language::new(
24809 LanguageConfig {
24810 name: "TSX".into(),
24811 matcher: LanguageMatcher {
24812 path_suffixes: vec!["tsx".to_string()],
24813 ..LanguageMatcher::default()
24814 },
24815 brackets: BracketPairConfig {
24816 pairs: vec![BracketPair {
24817 start: "<".into(),
24818 end: ">".into(),
24819 close: true,
24820 ..Default::default()
24821 }],
24822 ..Default::default()
24823 },
24824 linked_edit_characters: HashSet::from_iter(['.']),
24825 ..Default::default()
24826 },
24827 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24828 ));
24829 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24830
24831 // Test typing > does not extend linked pair
24832 cx.set_state("<divˇ<div></div>");
24833 cx.update_editor(|editor, _, cx| {
24834 set_linked_edit_ranges(
24835 (Point::new(0, 1), Point::new(0, 4)),
24836 (Point::new(0, 11), Point::new(0, 14)),
24837 editor,
24838 cx,
24839 );
24840 });
24841 cx.update_editor(|editor, window, cx| {
24842 editor.handle_input(">", window, cx);
24843 });
24844 cx.assert_editor_state("<div>ˇ<div></div>");
24845
24846 // Test typing . do extend linked pair
24847 cx.set_state("<Animatedˇ></Animated>");
24848 cx.update_editor(|editor, _, cx| {
24849 set_linked_edit_ranges(
24850 (Point::new(0, 1), Point::new(0, 9)),
24851 (Point::new(0, 12), Point::new(0, 20)),
24852 editor,
24853 cx,
24854 );
24855 });
24856 cx.update_editor(|editor, window, cx| {
24857 editor.handle_input(".", window, cx);
24858 });
24859 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24860 cx.update_editor(|editor, _, cx| {
24861 set_linked_edit_ranges(
24862 (Point::new(0, 1), Point::new(0, 10)),
24863 (Point::new(0, 13), Point::new(0, 21)),
24864 editor,
24865 cx,
24866 );
24867 });
24868 cx.update_editor(|editor, window, cx| {
24869 editor.handle_input("V", window, cx);
24870 });
24871 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24872}
24873
24874#[gpui::test]
24875async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24876 init_test(cx, |_| {});
24877
24878 let fs = FakeFs::new(cx.executor());
24879 fs.insert_tree(
24880 path!("/root"),
24881 json!({
24882 "a": {
24883 "main.rs": "fn main() {}",
24884 },
24885 "foo": {
24886 "bar": {
24887 "external_file.rs": "pub mod external {}",
24888 }
24889 }
24890 }),
24891 )
24892 .await;
24893
24894 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24895 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24896 language_registry.add(rust_lang());
24897 let _fake_servers = language_registry.register_fake_lsp(
24898 "Rust",
24899 FakeLspAdapter {
24900 ..FakeLspAdapter::default()
24901 },
24902 );
24903 let (workspace, cx) =
24904 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24905 let worktree_id = workspace.update(cx, |workspace, cx| {
24906 workspace.project().update(cx, |project, cx| {
24907 project.worktrees(cx).next().unwrap().read(cx).id()
24908 })
24909 });
24910
24911 let assert_language_servers_count =
24912 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24913 project.update(cx, |project, cx| {
24914 let current = project
24915 .lsp_store()
24916 .read(cx)
24917 .as_local()
24918 .unwrap()
24919 .language_servers
24920 .len();
24921 assert_eq!(expected, current, "{context}");
24922 });
24923 };
24924
24925 assert_language_servers_count(
24926 0,
24927 "No servers should be running before any file is open",
24928 cx,
24929 );
24930 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24931 let main_editor = workspace
24932 .update_in(cx, |workspace, window, cx| {
24933 workspace.open_path(
24934 (worktree_id, rel_path("main.rs")),
24935 Some(pane.downgrade()),
24936 true,
24937 window,
24938 cx,
24939 )
24940 })
24941 .unwrap()
24942 .await
24943 .downcast::<Editor>()
24944 .unwrap();
24945 pane.update(cx, |pane, cx| {
24946 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24947 open_editor.update(cx, |editor, cx| {
24948 assert_eq!(
24949 editor.display_text(cx),
24950 "fn main() {}",
24951 "Original main.rs text on initial open",
24952 );
24953 });
24954 assert_eq!(open_editor, main_editor);
24955 });
24956 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24957
24958 let external_editor = workspace
24959 .update_in(cx, |workspace, window, cx| {
24960 workspace.open_abs_path(
24961 PathBuf::from("/root/foo/bar/external_file.rs"),
24962 OpenOptions::default(),
24963 window,
24964 cx,
24965 )
24966 })
24967 .await
24968 .expect("opening external file")
24969 .downcast::<Editor>()
24970 .expect("downcasted external file's open element to editor");
24971 pane.update(cx, |pane, cx| {
24972 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24973 open_editor.update(cx, |editor, cx| {
24974 assert_eq!(
24975 editor.display_text(cx),
24976 "pub mod external {}",
24977 "External file is open now",
24978 );
24979 });
24980 assert_eq!(open_editor, external_editor);
24981 });
24982 assert_language_servers_count(
24983 1,
24984 "Second, external, *.rs file should join the existing server",
24985 cx,
24986 );
24987
24988 pane.update_in(cx, |pane, window, cx| {
24989 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24990 })
24991 .await
24992 .unwrap();
24993 pane.update_in(cx, |pane, window, cx| {
24994 pane.navigate_backward(&Default::default(), window, cx);
24995 });
24996 cx.run_until_parked();
24997 pane.update(cx, |pane, cx| {
24998 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24999 open_editor.update(cx, |editor, cx| {
25000 assert_eq!(
25001 editor.display_text(cx),
25002 "pub mod external {}",
25003 "External file is open now",
25004 );
25005 });
25006 });
25007 assert_language_servers_count(
25008 1,
25009 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25010 cx,
25011 );
25012
25013 cx.update(|_, cx| {
25014 workspace::reload(cx);
25015 });
25016 assert_language_servers_count(
25017 1,
25018 "After reloading the worktree with local and external files opened, only one project should be started",
25019 cx,
25020 );
25021}
25022
25023#[gpui::test]
25024async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25025 init_test(cx, |_| {});
25026
25027 let mut cx = EditorTestContext::new(cx).await;
25028 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25029 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25030
25031 // test cursor move to start of each line on tab
25032 // for `if`, `elif`, `else`, `while`, `with` and `for`
25033 cx.set_state(indoc! {"
25034 def main():
25035 ˇ for item in items:
25036 ˇ while item.active:
25037 ˇ if item.value > 10:
25038 ˇ continue
25039 ˇ elif item.value < 0:
25040 ˇ break
25041 ˇ else:
25042 ˇ with item.context() as ctx:
25043 ˇ yield count
25044 ˇ else:
25045 ˇ log('while else')
25046 ˇ else:
25047 ˇ log('for else')
25048 "});
25049 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25050 cx.assert_editor_state(indoc! {"
25051 def main():
25052 ˇfor item in items:
25053 ˇwhile item.active:
25054 ˇif item.value > 10:
25055 ˇcontinue
25056 ˇelif item.value < 0:
25057 ˇbreak
25058 ˇelse:
25059 ˇwith item.context() as ctx:
25060 ˇyield count
25061 ˇelse:
25062 ˇlog('while else')
25063 ˇelse:
25064 ˇlog('for else')
25065 "});
25066 // test relative indent is preserved when tab
25067 // for `if`, `elif`, `else`, `while`, `with` and `for`
25068 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25069 cx.assert_editor_state(indoc! {"
25070 def main():
25071 ˇfor item in items:
25072 ˇwhile item.active:
25073 ˇif item.value > 10:
25074 ˇcontinue
25075 ˇelif item.value < 0:
25076 ˇbreak
25077 ˇelse:
25078 ˇwith item.context() as ctx:
25079 ˇyield count
25080 ˇelse:
25081 ˇlog('while else')
25082 ˇelse:
25083 ˇlog('for else')
25084 "});
25085
25086 // test cursor move to start of each line on tab
25087 // for `try`, `except`, `else`, `finally`, `match` and `def`
25088 cx.set_state(indoc! {"
25089 def main():
25090 ˇ try:
25091 ˇ fetch()
25092 ˇ except ValueError:
25093 ˇ handle_error()
25094 ˇ else:
25095 ˇ match value:
25096 ˇ case _:
25097 ˇ finally:
25098 ˇ def status():
25099 ˇ return 0
25100 "});
25101 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25102 cx.assert_editor_state(indoc! {"
25103 def main():
25104 ˇtry:
25105 ˇfetch()
25106 ˇexcept ValueError:
25107 ˇhandle_error()
25108 ˇelse:
25109 ˇmatch value:
25110 ˇcase _:
25111 ˇfinally:
25112 ˇdef status():
25113 ˇreturn 0
25114 "});
25115 // test relative indent is preserved when tab
25116 // for `try`, `except`, `else`, `finally`, `match` and `def`
25117 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25118 cx.assert_editor_state(indoc! {"
25119 def main():
25120 ˇtry:
25121 ˇfetch()
25122 ˇexcept ValueError:
25123 ˇhandle_error()
25124 ˇelse:
25125 ˇmatch value:
25126 ˇcase _:
25127 ˇfinally:
25128 ˇdef status():
25129 ˇreturn 0
25130 "});
25131}
25132
25133#[gpui::test]
25134async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25135 init_test(cx, |_| {});
25136
25137 let mut cx = EditorTestContext::new(cx).await;
25138 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25139 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25140
25141 // test `else` auto outdents when typed inside `if` block
25142 cx.set_state(indoc! {"
25143 def main():
25144 if i == 2:
25145 return
25146 ˇ
25147 "});
25148 cx.update_editor(|editor, window, cx| {
25149 editor.handle_input("else:", window, cx);
25150 });
25151 cx.assert_editor_state(indoc! {"
25152 def main():
25153 if i == 2:
25154 return
25155 else:ˇ
25156 "});
25157
25158 // test `except` auto outdents when typed inside `try` block
25159 cx.set_state(indoc! {"
25160 def main():
25161 try:
25162 i = 2
25163 ˇ
25164 "});
25165 cx.update_editor(|editor, window, cx| {
25166 editor.handle_input("except:", window, cx);
25167 });
25168 cx.assert_editor_state(indoc! {"
25169 def main():
25170 try:
25171 i = 2
25172 except:ˇ
25173 "});
25174
25175 // test `else` auto outdents when typed inside `except` block
25176 cx.set_state(indoc! {"
25177 def main():
25178 try:
25179 i = 2
25180 except:
25181 j = 2
25182 ˇ
25183 "});
25184 cx.update_editor(|editor, window, cx| {
25185 editor.handle_input("else:", window, cx);
25186 });
25187 cx.assert_editor_state(indoc! {"
25188 def main():
25189 try:
25190 i = 2
25191 except:
25192 j = 2
25193 else:ˇ
25194 "});
25195
25196 // test `finally` auto outdents when typed inside `else` block
25197 cx.set_state(indoc! {"
25198 def main():
25199 try:
25200 i = 2
25201 except:
25202 j = 2
25203 else:
25204 k = 2
25205 ˇ
25206 "});
25207 cx.update_editor(|editor, window, cx| {
25208 editor.handle_input("finally:", window, cx);
25209 });
25210 cx.assert_editor_state(indoc! {"
25211 def main():
25212 try:
25213 i = 2
25214 except:
25215 j = 2
25216 else:
25217 k = 2
25218 finally:ˇ
25219 "});
25220
25221 // test `else` does not outdents when typed inside `except` block right after for block
25222 cx.set_state(indoc! {"
25223 def main():
25224 try:
25225 i = 2
25226 except:
25227 for i in range(n):
25228 pass
25229 ˇ
25230 "});
25231 cx.update_editor(|editor, window, cx| {
25232 editor.handle_input("else:", window, cx);
25233 });
25234 cx.assert_editor_state(indoc! {"
25235 def main():
25236 try:
25237 i = 2
25238 except:
25239 for i in range(n):
25240 pass
25241 else:ˇ
25242 "});
25243
25244 // test `finally` auto outdents when typed inside `else` block right after for block
25245 cx.set_state(indoc! {"
25246 def main():
25247 try:
25248 i = 2
25249 except:
25250 j = 2
25251 else:
25252 for i in range(n):
25253 pass
25254 ˇ
25255 "});
25256 cx.update_editor(|editor, window, cx| {
25257 editor.handle_input("finally:", window, cx);
25258 });
25259 cx.assert_editor_state(indoc! {"
25260 def main():
25261 try:
25262 i = 2
25263 except:
25264 j = 2
25265 else:
25266 for i in range(n):
25267 pass
25268 finally:ˇ
25269 "});
25270
25271 // test `except` outdents to inner "try" block
25272 cx.set_state(indoc! {"
25273 def main():
25274 try:
25275 i = 2
25276 if i == 2:
25277 try:
25278 i = 3
25279 ˇ
25280 "});
25281 cx.update_editor(|editor, window, cx| {
25282 editor.handle_input("except:", window, cx);
25283 });
25284 cx.assert_editor_state(indoc! {"
25285 def main():
25286 try:
25287 i = 2
25288 if i == 2:
25289 try:
25290 i = 3
25291 except:ˇ
25292 "});
25293
25294 // test `except` outdents to outer "try" block
25295 cx.set_state(indoc! {"
25296 def main():
25297 try:
25298 i = 2
25299 if i == 2:
25300 try:
25301 i = 3
25302 ˇ
25303 "});
25304 cx.update_editor(|editor, window, cx| {
25305 editor.handle_input("except:", window, cx);
25306 });
25307 cx.assert_editor_state(indoc! {"
25308 def main():
25309 try:
25310 i = 2
25311 if i == 2:
25312 try:
25313 i = 3
25314 except:ˇ
25315 "});
25316
25317 // test `else` stays at correct indent when typed after `for` block
25318 cx.set_state(indoc! {"
25319 def main():
25320 for i in range(10):
25321 if i == 3:
25322 break
25323 ˇ
25324 "});
25325 cx.update_editor(|editor, window, cx| {
25326 editor.handle_input("else:", window, cx);
25327 });
25328 cx.assert_editor_state(indoc! {"
25329 def main():
25330 for i in range(10):
25331 if i == 3:
25332 break
25333 else:ˇ
25334 "});
25335
25336 // test does not outdent on typing after line with square brackets
25337 cx.set_state(indoc! {"
25338 def f() -> list[str]:
25339 ˇ
25340 "});
25341 cx.update_editor(|editor, window, cx| {
25342 editor.handle_input("a", window, cx);
25343 });
25344 cx.assert_editor_state(indoc! {"
25345 def f() -> list[str]:
25346 aˇ
25347 "});
25348
25349 // test does not outdent on typing : after case keyword
25350 cx.set_state(indoc! {"
25351 match 1:
25352 caseˇ
25353 "});
25354 cx.update_editor(|editor, window, cx| {
25355 editor.handle_input(":", window, cx);
25356 });
25357 cx.assert_editor_state(indoc! {"
25358 match 1:
25359 case:ˇ
25360 "});
25361}
25362
25363#[gpui::test]
25364async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25365 init_test(cx, |_| {});
25366 update_test_language_settings(cx, |settings| {
25367 settings.defaults.extend_comment_on_newline = Some(false);
25368 });
25369 let mut cx = EditorTestContext::new(cx).await;
25370 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25371 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25372
25373 // test correct indent after newline on comment
25374 cx.set_state(indoc! {"
25375 # COMMENT:ˇ
25376 "});
25377 cx.update_editor(|editor, window, cx| {
25378 editor.newline(&Newline, window, cx);
25379 });
25380 cx.assert_editor_state(indoc! {"
25381 # COMMENT:
25382 ˇ
25383 "});
25384
25385 // test correct indent after newline in brackets
25386 cx.set_state(indoc! {"
25387 {ˇ}
25388 "});
25389 cx.update_editor(|editor, window, cx| {
25390 editor.newline(&Newline, window, cx);
25391 });
25392 cx.run_until_parked();
25393 cx.assert_editor_state(indoc! {"
25394 {
25395 ˇ
25396 }
25397 "});
25398
25399 cx.set_state(indoc! {"
25400 (ˇ)
25401 "});
25402 cx.update_editor(|editor, window, cx| {
25403 editor.newline(&Newline, window, cx);
25404 });
25405 cx.run_until_parked();
25406 cx.assert_editor_state(indoc! {"
25407 (
25408 ˇ
25409 )
25410 "});
25411
25412 // do not indent after empty lists or dictionaries
25413 cx.set_state(indoc! {"
25414 a = []ˇ
25415 "});
25416 cx.update_editor(|editor, window, cx| {
25417 editor.newline(&Newline, window, cx);
25418 });
25419 cx.run_until_parked();
25420 cx.assert_editor_state(indoc! {"
25421 a = []
25422 ˇ
25423 "});
25424}
25425
25426#[gpui::test]
25427async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25428 init_test(cx, |_| {});
25429
25430 let mut cx = EditorTestContext::new(cx).await;
25431 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25432 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25433
25434 // test cursor move to start of each line on tab
25435 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25436 cx.set_state(indoc! {"
25437 function main() {
25438 ˇ for item in $items; do
25439 ˇ while [ -n \"$item\" ]; do
25440 ˇ if [ \"$value\" -gt 10 ]; then
25441 ˇ continue
25442 ˇ elif [ \"$value\" -lt 0 ]; then
25443 ˇ break
25444 ˇ else
25445 ˇ echo \"$item\"
25446 ˇ fi
25447 ˇ done
25448 ˇ done
25449 ˇ}
25450 "});
25451 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25452 cx.assert_editor_state(indoc! {"
25453 function main() {
25454 ˇfor item in $items; do
25455 ˇwhile [ -n \"$item\" ]; do
25456 ˇif [ \"$value\" -gt 10 ]; then
25457 ˇcontinue
25458 ˇelif [ \"$value\" -lt 0 ]; then
25459 ˇbreak
25460 ˇelse
25461 ˇecho \"$item\"
25462 ˇfi
25463 ˇdone
25464 ˇdone
25465 ˇ}
25466 "});
25467 // test relative indent is preserved when tab
25468 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25469 cx.assert_editor_state(indoc! {"
25470 function main() {
25471 ˇfor item in $items; do
25472 ˇwhile [ -n \"$item\" ]; do
25473 ˇif [ \"$value\" -gt 10 ]; then
25474 ˇcontinue
25475 ˇelif [ \"$value\" -lt 0 ]; then
25476 ˇbreak
25477 ˇelse
25478 ˇecho \"$item\"
25479 ˇfi
25480 ˇdone
25481 ˇdone
25482 ˇ}
25483 "});
25484
25485 // test cursor move to start of each line on tab
25486 // for `case` statement with patterns
25487 cx.set_state(indoc! {"
25488 function handle() {
25489 ˇ case \"$1\" in
25490 ˇ start)
25491 ˇ echo \"a\"
25492 ˇ ;;
25493 ˇ stop)
25494 ˇ echo \"b\"
25495 ˇ ;;
25496 ˇ *)
25497 ˇ echo \"c\"
25498 ˇ ;;
25499 ˇ esac
25500 ˇ}
25501 "});
25502 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25503 cx.assert_editor_state(indoc! {"
25504 function handle() {
25505 ˇcase \"$1\" in
25506 ˇstart)
25507 ˇecho \"a\"
25508 ˇ;;
25509 ˇstop)
25510 ˇecho \"b\"
25511 ˇ;;
25512 ˇ*)
25513 ˇecho \"c\"
25514 ˇ;;
25515 ˇesac
25516 ˇ}
25517 "});
25518}
25519
25520#[gpui::test]
25521async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25522 init_test(cx, |_| {});
25523
25524 let mut cx = EditorTestContext::new(cx).await;
25525 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25526 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25527
25528 // test indents on comment insert
25529 cx.set_state(indoc! {"
25530 function main() {
25531 ˇ for item in $items; do
25532 ˇ while [ -n \"$item\" ]; do
25533 ˇ if [ \"$value\" -gt 10 ]; then
25534 ˇ continue
25535 ˇ elif [ \"$value\" -lt 0 ]; then
25536 ˇ break
25537 ˇ else
25538 ˇ echo \"$item\"
25539 ˇ fi
25540 ˇ done
25541 ˇ done
25542 ˇ}
25543 "});
25544 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25545 cx.assert_editor_state(indoc! {"
25546 function main() {
25547 #ˇ for item in $items; do
25548 #ˇ while [ -n \"$item\" ]; do
25549 #ˇ if [ \"$value\" -gt 10 ]; then
25550 #ˇ continue
25551 #ˇ elif [ \"$value\" -lt 0 ]; then
25552 #ˇ break
25553 #ˇ else
25554 #ˇ echo \"$item\"
25555 #ˇ fi
25556 #ˇ done
25557 #ˇ done
25558 #ˇ}
25559 "});
25560}
25561
25562#[gpui::test]
25563async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25564 init_test(cx, |_| {});
25565
25566 let mut cx = EditorTestContext::new(cx).await;
25567 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25568 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25569
25570 // test `else` auto outdents when typed inside `if` block
25571 cx.set_state(indoc! {"
25572 if [ \"$1\" = \"test\" ]; then
25573 echo \"foo bar\"
25574 ˇ
25575 "});
25576 cx.update_editor(|editor, window, cx| {
25577 editor.handle_input("else", window, cx);
25578 });
25579 cx.assert_editor_state(indoc! {"
25580 if [ \"$1\" = \"test\" ]; then
25581 echo \"foo bar\"
25582 elseˇ
25583 "});
25584
25585 // test `elif` auto outdents when typed inside `if` block
25586 cx.set_state(indoc! {"
25587 if [ \"$1\" = \"test\" ]; then
25588 echo \"foo bar\"
25589 ˇ
25590 "});
25591 cx.update_editor(|editor, window, cx| {
25592 editor.handle_input("elif", window, cx);
25593 });
25594 cx.assert_editor_state(indoc! {"
25595 if [ \"$1\" = \"test\" ]; then
25596 echo \"foo bar\"
25597 elifˇ
25598 "});
25599
25600 // test `fi` auto outdents when typed inside `else` block
25601 cx.set_state(indoc! {"
25602 if [ \"$1\" = \"test\" ]; then
25603 echo \"foo bar\"
25604 else
25605 echo \"bar baz\"
25606 ˇ
25607 "});
25608 cx.update_editor(|editor, window, cx| {
25609 editor.handle_input("fi", window, cx);
25610 });
25611 cx.assert_editor_state(indoc! {"
25612 if [ \"$1\" = \"test\" ]; then
25613 echo \"foo bar\"
25614 else
25615 echo \"bar baz\"
25616 fiˇ
25617 "});
25618
25619 // test `done` auto outdents when typed inside `while` block
25620 cx.set_state(indoc! {"
25621 while read line; do
25622 echo \"$line\"
25623 ˇ
25624 "});
25625 cx.update_editor(|editor, window, cx| {
25626 editor.handle_input("done", window, cx);
25627 });
25628 cx.assert_editor_state(indoc! {"
25629 while read line; do
25630 echo \"$line\"
25631 doneˇ
25632 "});
25633
25634 // test `done` auto outdents when typed inside `for` block
25635 cx.set_state(indoc! {"
25636 for file in *.txt; do
25637 cat \"$file\"
25638 ˇ
25639 "});
25640 cx.update_editor(|editor, window, cx| {
25641 editor.handle_input("done", window, cx);
25642 });
25643 cx.assert_editor_state(indoc! {"
25644 for file in *.txt; do
25645 cat \"$file\"
25646 doneˇ
25647 "});
25648
25649 // test `esac` auto outdents when typed inside `case` block
25650 cx.set_state(indoc! {"
25651 case \"$1\" in
25652 start)
25653 echo \"foo bar\"
25654 ;;
25655 stop)
25656 echo \"bar baz\"
25657 ;;
25658 ˇ
25659 "});
25660 cx.update_editor(|editor, window, cx| {
25661 editor.handle_input("esac", window, cx);
25662 });
25663 cx.assert_editor_state(indoc! {"
25664 case \"$1\" in
25665 start)
25666 echo \"foo bar\"
25667 ;;
25668 stop)
25669 echo \"bar baz\"
25670 ;;
25671 esacˇ
25672 "});
25673
25674 // test `*)` auto outdents when typed inside `case` block
25675 cx.set_state(indoc! {"
25676 case \"$1\" in
25677 start)
25678 echo \"foo bar\"
25679 ;;
25680 ˇ
25681 "});
25682 cx.update_editor(|editor, window, cx| {
25683 editor.handle_input("*)", window, cx);
25684 });
25685 cx.assert_editor_state(indoc! {"
25686 case \"$1\" in
25687 start)
25688 echo \"foo bar\"
25689 ;;
25690 *)ˇ
25691 "});
25692
25693 // test `fi` outdents to correct level with nested if blocks
25694 cx.set_state(indoc! {"
25695 if [ \"$1\" = \"test\" ]; then
25696 echo \"outer if\"
25697 if [ \"$2\" = \"debug\" ]; then
25698 echo \"inner if\"
25699 ˇ
25700 "});
25701 cx.update_editor(|editor, window, cx| {
25702 editor.handle_input("fi", window, cx);
25703 });
25704 cx.assert_editor_state(indoc! {"
25705 if [ \"$1\" = \"test\" ]; then
25706 echo \"outer if\"
25707 if [ \"$2\" = \"debug\" ]; then
25708 echo \"inner if\"
25709 fiˇ
25710 "});
25711}
25712
25713#[gpui::test]
25714async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25715 init_test(cx, |_| {});
25716 update_test_language_settings(cx, |settings| {
25717 settings.defaults.extend_comment_on_newline = Some(false);
25718 });
25719 let mut cx = EditorTestContext::new(cx).await;
25720 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25721 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25722
25723 // test correct indent after newline on comment
25724 cx.set_state(indoc! {"
25725 # COMMENT:ˇ
25726 "});
25727 cx.update_editor(|editor, window, cx| {
25728 editor.newline(&Newline, window, cx);
25729 });
25730 cx.assert_editor_state(indoc! {"
25731 # COMMENT:
25732 ˇ
25733 "});
25734
25735 // test correct indent after newline after `then`
25736 cx.set_state(indoc! {"
25737
25738 if [ \"$1\" = \"test\" ]; thenˇ
25739 "});
25740 cx.update_editor(|editor, window, cx| {
25741 editor.newline(&Newline, window, cx);
25742 });
25743 cx.run_until_parked();
25744 cx.assert_editor_state(indoc! {"
25745
25746 if [ \"$1\" = \"test\" ]; then
25747 ˇ
25748 "});
25749
25750 // test correct indent after newline after `else`
25751 cx.set_state(indoc! {"
25752 if [ \"$1\" = \"test\" ]; then
25753 elseˇ
25754 "});
25755 cx.update_editor(|editor, window, cx| {
25756 editor.newline(&Newline, window, cx);
25757 });
25758 cx.run_until_parked();
25759 cx.assert_editor_state(indoc! {"
25760 if [ \"$1\" = \"test\" ]; then
25761 else
25762 ˇ
25763 "});
25764
25765 // test correct indent after newline after `elif`
25766 cx.set_state(indoc! {"
25767 if [ \"$1\" = \"test\" ]; then
25768 elifˇ
25769 "});
25770 cx.update_editor(|editor, window, cx| {
25771 editor.newline(&Newline, window, cx);
25772 });
25773 cx.run_until_parked();
25774 cx.assert_editor_state(indoc! {"
25775 if [ \"$1\" = \"test\" ]; then
25776 elif
25777 ˇ
25778 "});
25779
25780 // test correct indent after newline after `do`
25781 cx.set_state(indoc! {"
25782 for file in *.txt; doˇ
25783 "});
25784 cx.update_editor(|editor, window, cx| {
25785 editor.newline(&Newline, window, cx);
25786 });
25787 cx.run_until_parked();
25788 cx.assert_editor_state(indoc! {"
25789 for file in *.txt; do
25790 ˇ
25791 "});
25792
25793 // test correct indent after newline after case pattern
25794 cx.set_state(indoc! {"
25795 case \"$1\" in
25796 start)ˇ
25797 "});
25798 cx.update_editor(|editor, window, cx| {
25799 editor.newline(&Newline, window, cx);
25800 });
25801 cx.run_until_parked();
25802 cx.assert_editor_state(indoc! {"
25803 case \"$1\" in
25804 start)
25805 ˇ
25806 "});
25807
25808 // test correct indent after newline after case pattern
25809 cx.set_state(indoc! {"
25810 case \"$1\" in
25811 start)
25812 ;;
25813 *)ˇ
25814 "});
25815 cx.update_editor(|editor, window, cx| {
25816 editor.newline(&Newline, window, cx);
25817 });
25818 cx.run_until_parked();
25819 cx.assert_editor_state(indoc! {"
25820 case \"$1\" in
25821 start)
25822 ;;
25823 *)
25824 ˇ
25825 "});
25826
25827 // test correct indent after newline after function opening brace
25828 cx.set_state(indoc! {"
25829 function test() {ˇ}
25830 "});
25831 cx.update_editor(|editor, window, cx| {
25832 editor.newline(&Newline, window, cx);
25833 });
25834 cx.run_until_parked();
25835 cx.assert_editor_state(indoc! {"
25836 function test() {
25837 ˇ
25838 }
25839 "});
25840
25841 // test no extra indent after semicolon on same line
25842 cx.set_state(indoc! {"
25843 echo \"test\";ˇ
25844 "});
25845 cx.update_editor(|editor, window, cx| {
25846 editor.newline(&Newline, window, cx);
25847 });
25848 cx.run_until_parked();
25849 cx.assert_editor_state(indoc! {"
25850 echo \"test\";
25851 ˇ
25852 "});
25853}
25854
25855fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25856 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25857 point..point
25858}
25859
25860#[track_caller]
25861fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25862 let (text, ranges) = marked_text_ranges(marked_text, true);
25863 assert_eq!(editor.text(cx), text);
25864 assert_eq!(
25865 editor.selections.ranges(&editor.display_snapshot(cx)),
25866 ranges
25867 .iter()
25868 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
25869 .collect::<Vec<_>>(),
25870 "Assert selections are {}",
25871 marked_text
25872 );
25873}
25874
25875pub fn handle_signature_help_request(
25876 cx: &mut EditorLspTestContext,
25877 mocked_response: lsp::SignatureHelp,
25878) -> impl Future<Output = ()> + use<> {
25879 let mut request =
25880 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25881 let mocked_response = mocked_response.clone();
25882 async move { Ok(Some(mocked_response)) }
25883 });
25884
25885 async move {
25886 request.next().await;
25887 }
25888}
25889
25890#[track_caller]
25891pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25892 cx.update_editor(|editor, _, _| {
25893 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25894 let entries = menu.entries.borrow();
25895 let entries = entries
25896 .iter()
25897 .map(|entry| entry.string.as_str())
25898 .collect::<Vec<_>>();
25899 assert_eq!(entries, expected);
25900 } else {
25901 panic!("Expected completions menu");
25902 }
25903 });
25904}
25905
25906#[gpui::test]
25907async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
25908 init_test(cx, |_| {});
25909 let mut cx = EditorLspTestContext::new_rust(
25910 lsp::ServerCapabilities {
25911 completion_provider: Some(lsp::CompletionOptions {
25912 ..Default::default()
25913 }),
25914 ..Default::default()
25915 },
25916 cx,
25917 )
25918 .await;
25919 cx.lsp
25920 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25921 Ok(Some(lsp::CompletionResponse::Array(vec![
25922 lsp::CompletionItem {
25923 label: "unsafe".into(),
25924 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25925 range: lsp::Range {
25926 start: lsp::Position {
25927 line: 0,
25928 character: 9,
25929 },
25930 end: lsp::Position {
25931 line: 0,
25932 character: 11,
25933 },
25934 },
25935 new_text: "unsafe".to_string(),
25936 })),
25937 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
25938 ..Default::default()
25939 },
25940 ])))
25941 });
25942
25943 cx.update_editor(|editor, _, cx| {
25944 editor.project().unwrap().update(cx, |project, cx| {
25945 project.snippets().update(cx, |snippets, _cx| {
25946 snippets.add_snippet_for_test(
25947 None,
25948 PathBuf::from("test_snippets.json"),
25949 vec![
25950 Arc::new(project::snippet_provider::Snippet {
25951 prefix: vec![
25952 "unlimited word count".to_string(),
25953 "unlimit word count".to_string(),
25954 "unlimited unknown".to_string(),
25955 ],
25956 body: "this is many words".to_string(),
25957 description: Some("description".to_string()),
25958 name: "multi-word snippet test".to_string(),
25959 }),
25960 Arc::new(project::snippet_provider::Snippet {
25961 prefix: vec!["unsnip".to_string(), "@few".to_string()],
25962 body: "fewer words".to_string(),
25963 description: Some("alt description".to_string()),
25964 name: "other name".to_string(),
25965 }),
25966 Arc::new(project::snippet_provider::Snippet {
25967 prefix: vec!["ab aa".to_string()],
25968 body: "abcd".to_string(),
25969 description: None,
25970 name: "alphabet".to_string(),
25971 }),
25972 ],
25973 );
25974 });
25975 })
25976 });
25977
25978 let get_completions = |cx: &mut EditorLspTestContext| {
25979 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
25980 Some(CodeContextMenu::Completions(context_menu)) => {
25981 let entries = context_menu.entries.borrow();
25982 entries
25983 .iter()
25984 .map(|entry| entry.string.clone())
25985 .collect_vec()
25986 }
25987 _ => vec![],
25988 })
25989 };
25990
25991 // snippets:
25992 // @foo
25993 // foo bar
25994 //
25995 // when typing:
25996 //
25997 // when typing:
25998 // - if I type a symbol "open the completions with snippets only"
25999 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26000 //
26001 // stuff we need:
26002 // - filtering logic change?
26003 // - remember how far back the completion started.
26004
26005 let test_cases: &[(&str, &[&str])] = &[
26006 (
26007 "un",
26008 &[
26009 "unsafe",
26010 "unlimit word count",
26011 "unlimited unknown",
26012 "unlimited word count",
26013 "unsnip",
26014 ],
26015 ),
26016 (
26017 "u ",
26018 &[
26019 "unlimit word count",
26020 "unlimited unknown",
26021 "unlimited word count",
26022 ],
26023 ),
26024 ("u a", &["ab aa", "unsafe"]), // unsAfe
26025 (
26026 "u u",
26027 &[
26028 "unsafe",
26029 "unlimit word count",
26030 "unlimited unknown", // ranked highest among snippets
26031 "unlimited word count",
26032 "unsnip",
26033 ],
26034 ),
26035 ("uw c", &["unlimit word count", "unlimited word count"]),
26036 (
26037 "u w",
26038 &[
26039 "unlimit word count",
26040 "unlimited word count",
26041 "unlimited unknown",
26042 ],
26043 ),
26044 ("u w ", &["unlimit word count", "unlimited word count"]),
26045 (
26046 "u ",
26047 &[
26048 "unlimit word count",
26049 "unlimited unknown",
26050 "unlimited word count",
26051 ],
26052 ),
26053 ("wor", &[]),
26054 ("uf", &["unsafe"]),
26055 ("af", &["unsafe"]),
26056 ("afu", &[]),
26057 (
26058 "ue",
26059 &["unsafe", "unlimited unknown", "unlimited word count"],
26060 ),
26061 ("@", &["@few"]),
26062 ("@few", &["@few"]),
26063 ("@ ", &[]),
26064 ("a@", &["@few"]),
26065 ("a@f", &["@few", "unsafe"]),
26066 ("a@fw", &["@few"]),
26067 ("a", &["ab aa", "unsafe"]),
26068 ("aa", &["ab aa"]),
26069 ("aaa", &["ab aa"]),
26070 ("ab", &["ab aa"]),
26071 ("ab ", &["ab aa"]),
26072 ("ab a", &["ab aa", "unsafe"]),
26073 ("ab ab", &["ab aa"]),
26074 ("ab ab aa", &["ab aa"]),
26075 ];
26076
26077 for &(input_to_simulate, expected_completions) in test_cases {
26078 cx.set_state("fn a() { ˇ }\n");
26079 for c in input_to_simulate.split("") {
26080 cx.simulate_input(c);
26081 cx.run_until_parked();
26082 }
26083 let expected_completions = expected_completions
26084 .iter()
26085 .map(|s| s.to_string())
26086 .collect_vec();
26087 assert_eq!(
26088 get_completions(&mut cx),
26089 expected_completions,
26090 "< actual / expected >, input = {input_to_simulate:?}",
26091 );
26092 }
26093}
26094
26095/// Handle completion request passing a marked string specifying where the completion
26096/// should be triggered from using '|' character, what range should be replaced, and what completions
26097/// should be returned using '<' and '>' to delimit the range.
26098///
26099/// Also see `handle_completion_request_with_insert_and_replace`.
26100#[track_caller]
26101pub fn handle_completion_request(
26102 marked_string: &str,
26103 completions: Vec<&'static str>,
26104 is_incomplete: bool,
26105 counter: Arc<AtomicUsize>,
26106 cx: &mut EditorLspTestContext,
26107) -> impl Future<Output = ()> {
26108 let complete_from_marker: TextRangeMarker = '|'.into();
26109 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26110 let (_, mut marked_ranges) = marked_text_ranges_by(
26111 marked_string,
26112 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26113 );
26114
26115 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26116 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26117 ));
26118 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26119 let replace_range =
26120 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26121
26122 let mut request =
26123 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26124 let completions = completions.clone();
26125 counter.fetch_add(1, atomic::Ordering::Release);
26126 async move {
26127 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26128 assert_eq!(
26129 params.text_document_position.position,
26130 complete_from_position
26131 );
26132 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26133 is_incomplete,
26134 item_defaults: None,
26135 items: completions
26136 .iter()
26137 .map(|completion_text| lsp::CompletionItem {
26138 label: completion_text.to_string(),
26139 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26140 range: replace_range,
26141 new_text: completion_text.to_string(),
26142 })),
26143 ..Default::default()
26144 })
26145 .collect(),
26146 })))
26147 }
26148 });
26149
26150 async move {
26151 request.next().await;
26152 }
26153}
26154
26155/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26156/// given instead, which also contains an `insert` range.
26157///
26158/// This function uses markers to define ranges:
26159/// - `|` marks the cursor position
26160/// - `<>` marks the replace range
26161/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26162pub fn handle_completion_request_with_insert_and_replace(
26163 cx: &mut EditorLspTestContext,
26164 marked_string: &str,
26165 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26166 counter: Arc<AtomicUsize>,
26167) -> impl Future<Output = ()> {
26168 let complete_from_marker: TextRangeMarker = '|'.into();
26169 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26170 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26171
26172 let (_, mut marked_ranges) = marked_text_ranges_by(
26173 marked_string,
26174 vec![
26175 complete_from_marker.clone(),
26176 replace_range_marker.clone(),
26177 insert_range_marker.clone(),
26178 ],
26179 );
26180
26181 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26182 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26183 ));
26184 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26185 let replace_range =
26186 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26187
26188 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26189 Some(ranges) if !ranges.is_empty() => {
26190 let range1 = ranges[0].clone();
26191 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26192 }
26193 _ => lsp::Range {
26194 start: replace_range.start,
26195 end: complete_from_position,
26196 },
26197 };
26198
26199 let mut request =
26200 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26201 let completions = completions.clone();
26202 counter.fetch_add(1, atomic::Ordering::Release);
26203 async move {
26204 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26205 assert_eq!(
26206 params.text_document_position.position, complete_from_position,
26207 "marker `|` position doesn't match",
26208 );
26209 Ok(Some(lsp::CompletionResponse::Array(
26210 completions
26211 .iter()
26212 .map(|(label, new_text)| lsp::CompletionItem {
26213 label: label.to_string(),
26214 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26215 lsp::InsertReplaceEdit {
26216 insert: insert_range,
26217 replace: replace_range,
26218 new_text: new_text.to_string(),
26219 },
26220 )),
26221 ..Default::default()
26222 })
26223 .collect(),
26224 )))
26225 }
26226 });
26227
26228 async move {
26229 request.next().await;
26230 }
26231}
26232
26233fn handle_resolve_completion_request(
26234 cx: &mut EditorLspTestContext,
26235 edits: Option<Vec<(&'static str, &'static str)>>,
26236) -> impl Future<Output = ()> {
26237 let edits = edits.map(|edits| {
26238 edits
26239 .iter()
26240 .map(|(marked_string, new_text)| {
26241 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26242 let replace_range = cx.to_lsp_range(
26243 MultiBufferOffset(marked_ranges[0].start)
26244 ..MultiBufferOffset(marked_ranges[0].end),
26245 );
26246 lsp::TextEdit::new(replace_range, new_text.to_string())
26247 })
26248 .collect::<Vec<_>>()
26249 });
26250
26251 let mut request =
26252 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26253 let edits = edits.clone();
26254 async move {
26255 Ok(lsp::CompletionItem {
26256 additional_text_edits: edits,
26257 ..Default::default()
26258 })
26259 }
26260 });
26261
26262 async move {
26263 request.next().await;
26264 }
26265}
26266
26267pub(crate) fn update_test_language_settings(
26268 cx: &mut TestAppContext,
26269 f: impl Fn(&mut AllLanguageSettingsContent),
26270) {
26271 cx.update(|cx| {
26272 SettingsStore::update_global(cx, |store, cx| {
26273 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26274 });
26275 });
26276}
26277
26278pub(crate) fn update_test_project_settings(
26279 cx: &mut TestAppContext,
26280 f: impl Fn(&mut ProjectSettingsContent),
26281) {
26282 cx.update(|cx| {
26283 SettingsStore::update_global(cx, |store, cx| {
26284 store.update_user_settings(cx, |settings| f(&mut settings.project));
26285 });
26286 });
26287}
26288
26289pub(crate) fn update_test_editor_settings(
26290 cx: &mut TestAppContext,
26291 f: impl Fn(&mut EditorSettingsContent),
26292) {
26293 cx.update(|cx| {
26294 SettingsStore::update_global(cx, |store, cx| {
26295 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26296 })
26297 })
26298}
26299
26300pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26301 cx.update(|cx| {
26302 assets::Assets.load_test_fonts(cx);
26303 let store = SettingsStore::test(cx);
26304 cx.set_global(store);
26305 theme::init(theme::LoadThemes::JustBase, cx);
26306 release_channel::init(semver::Version::new(0, 0, 0), cx);
26307 crate::init(cx);
26308 });
26309 zlog::init_test();
26310 update_test_language_settings(cx, f);
26311}
26312
26313#[track_caller]
26314fn assert_hunk_revert(
26315 not_reverted_text_with_selections: &str,
26316 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26317 expected_reverted_text_with_selections: &str,
26318 base_text: &str,
26319 cx: &mut EditorLspTestContext,
26320) {
26321 cx.set_state(not_reverted_text_with_selections);
26322 cx.set_head_text(base_text);
26323 cx.executor().run_until_parked();
26324
26325 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26326 let snapshot = editor.snapshot(window, cx);
26327 let reverted_hunk_statuses = snapshot
26328 .buffer_snapshot()
26329 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26330 .map(|hunk| hunk.status().kind)
26331 .collect::<Vec<_>>();
26332
26333 editor.git_restore(&Default::default(), window, cx);
26334 reverted_hunk_statuses
26335 });
26336 cx.executor().run_until_parked();
26337 cx.assert_editor_state(expected_reverted_text_with_selections);
26338 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26339}
26340
26341#[gpui::test(iterations = 10)]
26342async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26343 init_test(cx, |_| {});
26344
26345 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26346 let counter = diagnostic_requests.clone();
26347
26348 let fs = FakeFs::new(cx.executor());
26349 fs.insert_tree(
26350 path!("/a"),
26351 json!({
26352 "first.rs": "fn main() { let a = 5; }",
26353 "second.rs": "// Test file",
26354 }),
26355 )
26356 .await;
26357
26358 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26359 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26360 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26361
26362 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26363 language_registry.add(rust_lang());
26364 let mut fake_servers = language_registry.register_fake_lsp(
26365 "Rust",
26366 FakeLspAdapter {
26367 capabilities: lsp::ServerCapabilities {
26368 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26369 lsp::DiagnosticOptions {
26370 identifier: None,
26371 inter_file_dependencies: true,
26372 workspace_diagnostics: true,
26373 work_done_progress_options: Default::default(),
26374 },
26375 )),
26376 ..Default::default()
26377 },
26378 ..Default::default()
26379 },
26380 );
26381
26382 let editor = workspace
26383 .update(cx, |workspace, window, cx| {
26384 workspace.open_abs_path(
26385 PathBuf::from(path!("/a/first.rs")),
26386 OpenOptions::default(),
26387 window,
26388 cx,
26389 )
26390 })
26391 .unwrap()
26392 .await
26393 .unwrap()
26394 .downcast::<Editor>()
26395 .unwrap();
26396 let fake_server = fake_servers.next().await.unwrap();
26397 let server_id = fake_server.server.server_id();
26398 let mut first_request = fake_server
26399 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26400 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26401 let result_id = Some(new_result_id.to_string());
26402 assert_eq!(
26403 params.text_document.uri,
26404 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26405 );
26406 async move {
26407 Ok(lsp::DocumentDiagnosticReportResult::Report(
26408 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26409 related_documents: None,
26410 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26411 items: Vec::new(),
26412 result_id,
26413 },
26414 }),
26415 ))
26416 }
26417 });
26418
26419 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
26420 project.update(cx, |project, cx| {
26421 let buffer_id = editor
26422 .read(cx)
26423 .buffer()
26424 .read(cx)
26425 .as_singleton()
26426 .expect("created a singleton buffer")
26427 .read(cx)
26428 .remote_id();
26429 let buffer_result_id = project
26430 .lsp_store()
26431 .read(cx)
26432 .result_id(server_id, buffer_id, cx);
26433 assert_eq!(expected, buffer_result_id);
26434 });
26435 };
26436
26437 ensure_result_id(None, cx);
26438 cx.executor().advance_clock(Duration::from_millis(60));
26439 cx.executor().run_until_parked();
26440 assert_eq!(
26441 diagnostic_requests.load(atomic::Ordering::Acquire),
26442 1,
26443 "Opening file should trigger diagnostic request"
26444 );
26445 first_request
26446 .next()
26447 .await
26448 .expect("should have sent the first diagnostics pull request");
26449 ensure_result_id(Some("1".to_string()), cx);
26450
26451 // Editing should trigger diagnostics
26452 editor.update_in(cx, |editor, window, cx| {
26453 editor.handle_input("2", window, cx)
26454 });
26455 cx.executor().advance_clock(Duration::from_millis(60));
26456 cx.executor().run_until_parked();
26457 assert_eq!(
26458 diagnostic_requests.load(atomic::Ordering::Acquire),
26459 2,
26460 "Editing should trigger diagnostic request"
26461 );
26462 ensure_result_id(Some("2".to_string()), cx);
26463
26464 // Moving cursor should not trigger diagnostic request
26465 editor.update_in(cx, |editor, window, cx| {
26466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26467 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26468 });
26469 });
26470 cx.executor().advance_clock(Duration::from_millis(60));
26471 cx.executor().run_until_parked();
26472 assert_eq!(
26473 diagnostic_requests.load(atomic::Ordering::Acquire),
26474 2,
26475 "Cursor movement should not trigger diagnostic request"
26476 );
26477 ensure_result_id(Some("2".to_string()), cx);
26478 // Multiple rapid edits should be debounced
26479 for _ in 0..5 {
26480 editor.update_in(cx, |editor, window, cx| {
26481 editor.handle_input("x", window, cx)
26482 });
26483 }
26484 cx.executor().advance_clock(Duration::from_millis(60));
26485 cx.executor().run_until_parked();
26486
26487 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26488 assert!(
26489 final_requests <= 4,
26490 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26491 );
26492 ensure_result_id(Some(final_requests.to_string()), cx);
26493}
26494
26495#[gpui::test]
26496async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26497 // Regression test for issue #11671
26498 // Previously, adding a cursor after moving multiple cursors would reset
26499 // the cursor count instead of adding to the existing cursors.
26500 init_test(cx, |_| {});
26501 let mut cx = EditorTestContext::new(cx).await;
26502
26503 // Create a simple buffer with cursor at start
26504 cx.set_state(indoc! {"
26505 ˇaaaa
26506 bbbb
26507 cccc
26508 dddd
26509 eeee
26510 ffff
26511 gggg
26512 hhhh"});
26513
26514 // Add 2 cursors below (so we have 3 total)
26515 cx.update_editor(|editor, window, cx| {
26516 editor.add_selection_below(&Default::default(), window, cx);
26517 editor.add_selection_below(&Default::default(), window, cx);
26518 });
26519
26520 // Verify we have 3 cursors
26521 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26522 assert_eq!(
26523 initial_count, 3,
26524 "Should have 3 cursors after adding 2 below"
26525 );
26526
26527 // Move down one line
26528 cx.update_editor(|editor, window, cx| {
26529 editor.move_down(&MoveDown, window, cx);
26530 });
26531
26532 // Add another cursor below
26533 cx.update_editor(|editor, window, cx| {
26534 editor.add_selection_below(&Default::default(), window, cx);
26535 });
26536
26537 // Should now have 4 cursors (3 original + 1 new)
26538 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26539 assert_eq!(
26540 final_count, 4,
26541 "Should have 4 cursors after moving and adding another"
26542 );
26543}
26544
26545#[gpui::test]
26546async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26547 init_test(cx, |_| {});
26548
26549 let mut cx = EditorTestContext::new(cx).await;
26550
26551 cx.set_state(indoc!(
26552 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26553 Second line here"#
26554 ));
26555
26556 cx.update_editor(|editor, window, cx| {
26557 // Enable soft wrapping with a narrow width to force soft wrapping and
26558 // confirm that more than 2 rows are being displayed.
26559 editor.set_wrap_width(Some(100.0.into()), cx);
26560 assert!(editor.display_text(cx).lines().count() > 2);
26561
26562 editor.add_selection_below(
26563 &AddSelectionBelow {
26564 skip_soft_wrap: true,
26565 },
26566 window,
26567 cx,
26568 );
26569
26570 assert_eq!(
26571 display_ranges(editor, cx),
26572 &[
26573 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26574 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26575 ]
26576 );
26577
26578 editor.add_selection_above(
26579 &AddSelectionAbove {
26580 skip_soft_wrap: true,
26581 },
26582 window,
26583 cx,
26584 );
26585
26586 assert_eq!(
26587 display_ranges(editor, cx),
26588 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26589 );
26590
26591 editor.add_selection_below(
26592 &AddSelectionBelow {
26593 skip_soft_wrap: false,
26594 },
26595 window,
26596 cx,
26597 );
26598
26599 assert_eq!(
26600 display_ranges(editor, cx),
26601 &[
26602 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26603 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26604 ]
26605 );
26606
26607 editor.add_selection_above(
26608 &AddSelectionAbove {
26609 skip_soft_wrap: false,
26610 },
26611 window,
26612 cx,
26613 );
26614
26615 assert_eq!(
26616 display_ranges(editor, cx),
26617 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26618 );
26619 });
26620}
26621
26622#[gpui::test(iterations = 10)]
26623async fn test_document_colors(cx: &mut TestAppContext) {
26624 let expected_color = Rgba {
26625 r: 0.33,
26626 g: 0.33,
26627 b: 0.33,
26628 a: 0.33,
26629 };
26630
26631 init_test(cx, |_| {});
26632
26633 let fs = FakeFs::new(cx.executor());
26634 fs.insert_tree(
26635 path!("/a"),
26636 json!({
26637 "first.rs": "fn main() { let a = 5; }",
26638 }),
26639 )
26640 .await;
26641
26642 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26643 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26644 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26645
26646 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26647 language_registry.add(rust_lang());
26648 let mut fake_servers = language_registry.register_fake_lsp(
26649 "Rust",
26650 FakeLspAdapter {
26651 capabilities: lsp::ServerCapabilities {
26652 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26653 ..lsp::ServerCapabilities::default()
26654 },
26655 name: "rust-analyzer",
26656 ..FakeLspAdapter::default()
26657 },
26658 );
26659 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26660 "Rust",
26661 FakeLspAdapter {
26662 capabilities: lsp::ServerCapabilities {
26663 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26664 ..lsp::ServerCapabilities::default()
26665 },
26666 name: "not-rust-analyzer",
26667 ..FakeLspAdapter::default()
26668 },
26669 );
26670
26671 let editor = workspace
26672 .update(cx, |workspace, window, cx| {
26673 workspace.open_abs_path(
26674 PathBuf::from(path!("/a/first.rs")),
26675 OpenOptions::default(),
26676 window,
26677 cx,
26678 )
26679 })
26680 .unwrap()
26681 .await
26682 .unwrap()
26683 .downcast::<Editor>()
26684 .unwrap();
26685 let fake_language_server = fake_servers.next().await.unwrap();
26686 let fake_language_server_without_capabilities =
26687 fake_servers_without_capabilities.next().await.unwrap();
26688 let requests_made = Arc::new(AtomicUsize::new(0));
26689 let closure_requests_made = Arc::clone(&requests_made);
26690 let mut color_request_handle = fake_language_server
26691 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26692 let requests_made = Arc::clone(&closure_requests_made);
26693 async move {
26694 assert_eq!(
26695 params.text_document.uri,
26696 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26697 );
26698 requests_made.fetch_add(1, atomic::Ordering::Release);
26699 Ok(vec![
26700 lsp::ColorInformation {
26701 range: lsp::Range {
26702 start: lsp::Position {
26703 line: 0,
26704 character: 0,
26705 },
26706 end: lsp::Position {
26707 line: 0,
26708 character: 1,
26709 },
26710 },
26711 color: lsp::Color {
26712 red: 0.33,
26713 green: 0.33,
26714 blue: 0.33,
26715 alpha: 0.33,
26716 },
26717 },
26718 lsp::ColorInformation {
26719 range: lsp::Range {
26720 start: lsp::Position {
26721 line: 0,
26722 character: 0,
26723 },
26724 end: lsp::Position {
26725 line: 0,
26726 character: 1,
26727 },
26728 },
26729 color: lsp::Color {
26730 red: 0.33,
26731 green: 0.33,
26732 blue: 0.33,
26733 alpha: 0.33,
26734 },
26735 },
26736 ])
26737 }
26738 });
26739
26740 let _handle = fake_language_server_without_capabilities
26741 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26742 panic!("Should not be called");
26743 });
26744 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26745 color_request_handle.next().await.unwrap();
26746 cx.run_until_parked();
26747 assert_eq!(
26748 1,
26749 requests_made.load(atomic::Ordering::Acquire),
26750 "Should query for colors once per editor open"
26751 );
26752 editor.update_in(cx, |editor, _, cx| {
26753 assert_eq!(
26754 vec![expected_color],
26755 extract_color_inlays(editor, cx),
26756 "Should have an initial inlay"
26757 );
26758 });
26759
26760 // opening another file in a split should not influence the LSP query counter
26761 workspace
26762 .update(cx, |workspace, window, cx| {
26763 assert_eq!(
26764 workspace.panes().len(),
26765 1,
26766 "Should have one pane with one editor"
26767 );
26768 workspace.move_item_to_pane_in_direction(
26769 &MoveItemToPaneInDirection {
26770 direction: SplitDirection::Right,
26771 focus: false,
26772 clone: true,
26773 },
26774 window,
26775 cx,
26776 );
26777 })
26778 .unwrap();
26779 cx.run_until_parked();
26780 workspace
26781 .update(cx, |workspace, _, cx| {
26782 let panes = workspace.panes();
26783 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26784 for pane in panes {
26785 let editor = pane
26786 .read(cx)
26787 .active_item()
26788 .and_then(|item| item.downcast::<Editor>())
26789 .expect("Should have opened an editor in each split");
26790 let editor_file = editor
26791 .read(cx)
26792 .buffer()
26793 .read(cx)
26794 .as_singleton()
26795 .expect("test deals with singleton buffers")
26796 .read(cx)
26797 .file()
26798 .expect("test buffese should have a file")
26799 .path();
26800 assert_eq!(
26801 editor_file.as_ref(),
26802 rel_path("first.rs"),
26803 "Both editors should be opened for the same file"
26804 )
26805 }
26806 })
26807 .unwrap();
26808
26809 cx.executor().advance_clock(Duration::from_millis(500));
26810 let save = editor.update_in(cx, |editor, window, cx| {
26811 editor.move_to_end(&MoveToEnd, window, cx);
26812 editor.handle_input("dirty", window, cx);
26813 editor.save(
26814 SaveOptions {
26815 format: true,
26816 autosave: true,
26817 },
26818 project.clone(),
26819 window,
26820 cx,
26821 )
26822 });
26823 save.await.unwrap();
26824
26825 color_request_handle.next().await.unwrap();
26826 cx.run_until_parked();
26827 assert_eq!(
26828 2,
26829 requests_made.load(atomic::Ordering::Acquire),
26830 "Should query for colors once per save (deduplicated) and once per formatting after save"
26831 );
26832
26833 drop(editor);
26834 let close = workspace
26835 .update(cx, |workspace, window, cx| {
26836 workspace.active_pane().update(cx, |pane, cx| {
26837 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26838 })
26839 })
26840 .unwrap();
26841 close.await.unwrap();
26842 let close = workspace
26843 .update(cx, |workspace, window, cx| {
26844 workspace.active_pane().update(cx, |pane, cx| {
26845 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26846 })
26847 })
26848 .unwrap();
26849 close.await.unwrap();
26850 assert_eq!(
26851 2,
26852 requests_made.load(atomic::Ordering::Acquire),
26853 "After saving and closing all editors, no extra requests should be made"
26854 );
26855 workspace
26856 .update(cx, |workspace, _, cx| {
26857 assert!(
26858 workspace.active_item(cx).is_none(),
26859 "Should close all editors"
26860 )
26861 })
26862 .unwrap();
26863
26864 workspace
26865 .update(cx, |workspace, window, cx| {
26866 workspace.active_pane().update(cx, |pane, cx| {
26867 pane.navigate_backward(&workspace::GoBack, window, cx);
26868 })
26869 })
26870 .unwrap();
26871 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26872 cx.run_until_parked();
26873 let editor = workspace
26874 .update(cx, |workspace, _, cx| {
26875 workspace
26876 .active_item(cx)
26877 .expect("Should have reopened the editor again after navigating back")
26878 .downcast::<Editor>()
26879 .expect("Should be an editor")
26880 })
26881 .unwrap();
26882
26883 assert_eq!(
26884 2,
26885 requests_made.load(atomic::Ordering::Acquire),
26886 "Cache should be reused on buffer close and reopen"
26887 );
26888 editor.update(cx, |editor, cx| {
26889 assert_eq!(
26890 vec![expected_color],
26891 extract_color_inlays(editor, cx),
26892 "Should have an initial inlay"
26893 );
26894 });
26895
26896 drop(color_request_handle);
26897 let closure_requests_made = Arc::clone(&requests_made);
26898 let mut empty_color_request_handle = fake_language_server
26899 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26900 let requests_made = Arc::clone(&closure_requests_made);
26901 async move {
26902 assert_eq!(
26903 params.text_document.uri,
26904 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26905 );
26906 requests_made.fetch_add(1, atomic::Ordering::Release);
26907 Ok(Vec::new())
26908 }
26909 });
26910 let save = editor.update_in(cx, |editor, window, cx| {
26911 editor.move_to_end(&MoveToEnd, window, cx);
26912 editor.handle_input("dirty_again", window, cx);
26913 editor.save(
26914 SaveOptions {
26915 format: false,
26916 autosave: true,
26917 },
26918 project.clone(),
26919 window,
26920 cx,
26921 )
26922 });
26923 save.await.unwrap();
26924
26925 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26926 empty_color_request_handle.next().await.unwrap();
26927 cx.run_until_parked();
26928 assert_eq!(
26929 3,
26930 requests_made.load(atomic::Ordering::Acquire),
26931 "Should query for colors once per save only, as formatting was not requested"
26932 );
26933 editor.update(cx, |editor, cx| {
26934 assert_eq!(
26935 Vec::<Rgba>::new(),
26936 extract_color_inlays(editor, cx),
26937 "Should clear all colors when the server returns an empty response"
26938 );
26939 });
26940}
26941
26942#[gpui::test]
26943async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26944 init_test(cx, |_| {});
26945 let (editor, cx) = cx.add_window_view(Editor::single_line);
26946 editor.update_in(cx, |editor, window, cx| {
26947 editor.set_text("oops\n\nwow\n", window, cx)
26948 });
26949 cx.run_until_parked();
26950 editor.update(cx, |editor, cx| {
26951 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26952 });
26953 editor.update(cx, |editor, cx| {
26954 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
26955 });
26956 cx.run_until_parked();
26957 editor.update(cx, |editor, cx| {
26958 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26959 });
26960}
26961
26962#[gpui::test]
26963async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26964 init_test(cx, |_| {});
26965
26966 cx.update(|cx| {
26967 register_project_item::<Editor>(cx);
26968 });
26969
26970 let fs = FakeFs::new(cx.executor());
26971 fs.insert_tree("/root1", json!({})).await;
26972 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26973 .await;
26974
26975 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26976 let (workspace, cx) =
26977 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26978
26979 let worktree_id = project.update(cx, |project, cx| {
26980 project.worktrees(cx).next().unwrap().read(cx).id()
26981 });
26982
26983 let handle = workspace
26984 .update_in(cx, |workspace, window, cx| {
26985 let project_path = (worktree_id, rel_path("one.pdf"));
26986 workspace.open_path(project_path, None, true, window, cx)
26987 })
26988 .await
26989 .unwrap();
26990
26991 assert_eq!(
26992 handle.to_any_view().entity_type(),
26993 TypeId::of::<InvalidItemView>()
26994 );
26995}
26996
26997#[gpui::test]
26998async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26999 init_test(cx, |_| {});
27000
27001 let language = Arc::new(Language::new(
27002 LanguageConfig::default(),
27003 Some(tree_sitter_rust::LANGUAGE.into()),
27004 ));
27005
27006 // Test hierarchical sibling navigation
27007 let text = r#"
27008 fn outer() {
27009 if condition {
27010 let a = 1;
27011 }
27012 let b = 2;
27013 }
27014
27015 fn another() {
27016 let c = 3;
27017 }
27018 "#;
27019
27020 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27021 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27022 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27023
27024 // Wait for parsing to complete
27025 editor
27026 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27027 .await;
27028
27029 editor.update_in(cx, |editor, window, cx| {
27030 // Start by selecting "let a = 1;" inside the if block
27031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27032 s.select_display_ranges([
27033 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27034 ]);
27035 });
27036
27037 let initial_selection = editor
27038 .selections
27039 .display_ranges(&editor.display_snapshot(cx));
27040 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27041
27042 // Test select next sibling - should move up levels to find the next sibling
27043 // Since "let a = 1;" has no siblings in the if block, it should move up
27044 // to find "let b = 2;" which is a sibling of the if block
27045 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27046 let next_selection = editor
27047 .selections
27048 .display_ranges(&editor.display_snapshot(cx));
27049
27050 // Should have a selection and it should be different from the initial
27051 assert_eq!(
27052 next_selection.len(),
27053 1,
27054 "Should have one selection after next"
27055 );
27056 assert_ne!(
27057 next_selection[0], initial_selection[0],
27058 "Next sibling selection should be different"
27059 );
27060
27061 // Test hierarchical navigation by going to the end of the current function
27062 // and trying to navigate to the next function
27063 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27064 s.select_display_ranges([
27065 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27066 ]);
27067 });
27068
27069 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27070 let function_next_selection = editor
27071 .selections
27072 .display_ranges(&editor.display_snapshot(cx));
27073
27074 // Should move to the next function
27075 assert_eq!(
27076 function_next_selection.len(),
27077 1,
27078 "Should have one selection after function next"
27079 );
27080
27081 // Test select previous sibling navigation
27082 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27083 let prev_selection = editor
27084 .selections
27085 .display_ranges(&editor.display_snapshot(cx));
27086
27087 // Should have a selection and it should be different
27088 assert_eq!(
27089 prev_selection.len(),
27090 1,
27091 "Should have one selection after prev"
27092 );
27093 assert_ne!(
27094 prev_selection[0], function_next_selection[0],
27095 "Previous sibling selection should be different from next"
27096 );
27097 });
27098}
27099
27100#[gpui::test]
27101async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27102 init_test(cx, |_| {});
27103
27104 let mut cx = EditorTestContext::new(cx).await;
27105 cx.set_state(
27106 "let ˇvariable = 42;
27107let another = variable + 1;
27108let result = variable * 2;",
27109 );
27110
27111 // Set up document highlights manually (simulating LSP response)
27112 cx.update_editor(|editor, _window, cx| {
27113 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27114
27115 // Create highlights for "variable" occurrences
27116 let highlight_ranges = [
27117 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27118 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27119 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27120 ];
27121
27122 let anchor_ranges: Vec<_> = highlight_ranges
27123 .iter()
27124 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27125 .collect();
27126
27127 editor.highlight_background::<DocumentHighlightRead>(
27128 &anchor_ranges,
27129 |theme| theme.colors().editor_document_highlight_read_background,
27130 cx,
27131 );
27132 });
27133
27134 // Go to next highlight - should move to second "variable"
27135 cx.update_editor(|editor, window, cx| {
27136 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27137 });
27138 cx.assert_editor_state(
27139 "let variable = 42;
27140let another = ˇvariable + 1;
27141let result = variable * 2;",
27142 );
27143
27144 // Go to next highlight - should move to third "variable"
27145 cx.update_editor(|editor, window, cx| {
27146 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27147 });
27148 cx.assert_editor_state(
27149 "let variable = 42;
27150let another = variable + 1;
27151let result = ˇvariable * 2;",
27152 );
27153
27154 // Go to next highlight - should stay at third "variable" (no wrap-around)
27155 cx.update_editor(|editor, window, cx| {
27156 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27157 });
27158 cx.assert_editor_state(
27159 "let variable = 42;
27160let another = variable + 1;
27161let result = ˇvariable * 2;",
27162 );
27163
27164 // Now test going backwards from third position
27165 cx.update_editor(|editor, window, cx| {
27166 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27167 });
27168 cx.assert_editor_state(
27169 "let variable = 42;
27170let another = ˇvariable + 1;
27171let result = variable * 2;",
27172 );
27173
27174 // Go to previous highlight - should move to first "variable"
27175 cx.update_editor(|editor, window, cx| {
27176 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27177 });
27178 cx.assert_editor_state(
27179 "let ˇvariable = 42;
27180let another = variable + 1;
27181let result = variable * 2;",
27182 );
27183
27184 // Go to previous highlight - should stay on first "variable"
27185 cx.update_editor(|editor, window, cx| {
27186 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27187 });
27188 cx.assert_editor_state(
27189 "let ˇvariable = 42;
27190let another = variable + 1;
27191let result = variable * 2;",
27192 );
27193}
27194
27195#[gpui::test]
27196async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27197 cx: &mut gpui::TestAppContext,
27198) {
27199 init_test(cx, |_| {});
27200
27201 let url = "https://zed.dev";
27202
27203 let markdown_language = Arc::new(Language::new(
27204 LanguageConfig {
27205 name: "Markdown".into(),
27206 ..LanguageConfig::default()
27207 },
27208 None,
27209 ));
27210
27211 let mut cx = EditorTestContext::new(cx).await;
27212 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27213 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27214
27215 cx.update_editor(|editor, window, cx| {
27216 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27217 editor.paste(&Paste, window, cx);
27218 });
27219
27220 cx.assert_editor_state(&format!(
27221 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27222 ));
27223}
27224
27225#[gpui::test]
27226async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
27227 cx: &mut gpui::TestAppContext,
27228) {
27229 init_test(cx, |_| {});
27230
27231 let url = "https://zed.dev";
27232
27233 let markdown_language = Arc::new(Language::new(
27234 LanguageConfig {
27235 name: "Markdown".into(),
27236 ..LanguageConfig::default()
27237 },
27238 None,
27239 ));
27240
27241 let mut cx = EditorTestContext::new(cx).await;
27242 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27243 cx.set_state(&format!(
27244 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
27245 ));
27246
27247 cx.update_editor(|editor, window, cx| {
27248 editor.copy(&Copy, window, cx);
27249 });
27250
27251 cx.set_state(&format!(
27252 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
27253 ));
27254
27255 cx.update_editor(|editor, window, cx| {
27256 editor.paste(&Paste, window, cx);
27257 });
27258
27259 cx.assert_editor_state(&format!(
27260 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
27261 ));
27262}
27263
27264#[gpui::test]
27265async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
27266 cx: &mut gpui::TestAppContext,
27267) {
27268 init_test(cx, |_| {});
27269
27270 let url = "https://zed.dev";
27271
27272 let markdown_language = Arc::new(Language::new(
27273 LanguageConfig {
27274 name: "Markdown".into(),
27275 ..LanguageConfig::default()
27276 },
27277 None,
27278 ));
27279
27280 let mut cx = EditorTestContext::new(cx).await;
27281 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27282 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
27283
27284 cx.update_editor(|editor, window, cx| {
27285 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27286 editor.paste(&Paste, window, cx);
27287 });
27288
27289 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
27290}
27291
27292#[gpui::test]
27293async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
27294 cx: &mut gpui::TestAppContext,
27295) {
27296 init_test(cx, |_| {});
27297
27298 let text = "Awesome";
27299
27300 let markdown_language = Arc::new(Language::new(
27301 LanguageConfig {
27302 name: "Markdown".into(),
27303 ..LanguageConfig::default()
27304 },
27305 None,
27306 ));
27307
27308 let mut cx = EditorTestContext::new(cx).await;
27309 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27310 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
27311
27312 cx.update_editor(|editor, window, cx| {
27313 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
27314 editor.paste(&Paste, window, cx);
27315 });
27316
27317 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
27318}
27319
27320#[gpui::test]
27321async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
27322 cx: &mut gpui::TestAppContext,
27323) {
27324 init_test(cx, |_| {});
27325
27326 let url = "https://zed.dev";
27327
27328 let markdown_language = Arc::new(Language::new(
27329 LanguageConfig {
27330 name: "Rust".into(),
27331 ..LanguageConfig::default()
27332 },
27333 None,
27334 ));
27335
27336 let mut cx = EditorTestContext::new(cx).await;
27337 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27338 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
27339
27340 cx.update_editor(|editor, window, cx| {
27341 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27342 editor.paste(&Paste, window, cx);
27343 });
27344
27345 cx.assert_editor_state(&format!(
27346 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
27347 ));
27348}
27349
27350#[gpui::test]
27351async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
27352 cx: &mut TestAppContext,
27353) {
27354 init_test(cx, |_| {});
27355
27356 let url = "https://zed.dev";
27357
27358 let markdown_language = Arc::new(Language::new(
27359 LanguageConfig {
27360 name: "Markdown".into(),
27361 ..LanguageConfig::default()
27362 },
27363 None,
27364 ));
27365
27366 let (editor, cx) = cx.add_window_view(|window, cx| {
27367 let multi_buffer = MultiBuffer::build_multi(
27368 [
27369 ("this will embed -> link", vec![Point::row_range(0..1)]),
27370 ("this will replace -> link", vec![Point::row_range(0..1)]),
27371 ],
27372 cx,
27373 );
27374 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
27375 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27376 s.select_ranges(vec![
27377 Point::new(0, 19)..Point::new(0, 23),
27378 Point::new(1, 21)..Point::new(1, 25),
27379 ])
27380 });
27381 let first_buffer_id = multi_buffer
27382 .read(cx)
27383 .excerpt_buffer_ids()
27384 .into_iter()
27385 .next()
27386 .unwrap();
27387 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
27388 first_buffer.update(cx, |buffer, cx| {
27389 buffer.set_language(Some(markdown_language.clone()), cx);
27390 });
27391
27392 editor
27393 });
27394 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27395
27396 cx.update_editor(|editor, window, cx| {
27397 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27398 editor.paste(&Paste, window, cx);
27399 });
27400
27401 cx.assert_editor_state(&format!(
27402 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
27403 ));
27404}
27405
27406#[gpui::test]
27407async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
27408 init_test(cx, |_| {});
27409
27410 let fs = FakeFs::new(cx.executor());
27411 fs.insert_tree(
27412 path!("/project"),
27413 json!({
27414 "first.rs": "# First Document\nSome content here.",
27415 "second.rs": "Plain text content for second file.",
27416 }),
27417 )
27418 .await;
27419
27420 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
27421 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27422 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27423
27424 let language = rust_lang();
27425 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27426 language_registry.add(language.clone());
27427 let mut fake_servers = language_registry.register_fake_lsp(
27428 "Rust",
27429 FakeLspAdapter {
27430 ..FakeLspAdapter::default()
27431 },
27432 );
27433
27434 let buffer1 = project
27435 .update(cx, |project, cx| {
27436 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
27437 })
27438 .await
27439 .unwrap();
27440 let buffer2 = project
27441 .update(cx, |project, cx| {
27442 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
27443 })
27444 .await
27445 .unwrap();
27446
27447 let multi_buffer = cx.new(|cx| {
27448 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
27449 multi_buffer.set_excerpts_for_path(
27450 PathKey::for_buffer(&buffer1, cx),
27451 buffer1.clone(),
27452 [Point::zero()..buffer1.read(cx).max_point()],
27453 3,
27454 cx,
27455 );
27456 multi_buffer.set_excerpts_for_path(
27457 PathKey::for_buffer(&buffer2, cx),
27458 buffer2.clone(),
27459 [Point::zero()..buffer1.read(cx).max_point()],
27460 3,
27461 cx,
27462 );
27463 multi_buffer
27464 });
27465
27466 let (editor, cx) = cx.add_window_view(|window, cx| {
27467 Editor::new(
27468 EditorMode::full(),
27469 multi_buffer,
27470 Some(project.clone()),
27471 window,
27472 cx,
27473 )
27474 });
27475
27476 let fake_language_server = fake_servers.next().await.unwrap();
27477
27478 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
27479
27480 let save = editor.update_in(cx, |editor, window, cx| {
27481 assert!(editor.is_dirty(cx));
27482
27483 editor.save(
27484 SaveOptions {
27485 format: true,
27486 autosave: true,
27487 },
27488 project,
27489 window,
27490 cx,
27491 )
27492 });
27493 let (start_edit_tx, start_edit_rx) = oneshot::channel();
27494 let (done_edit_tx, done_edit_rx) = oneshot::channel();
27495 let mut done_edit_rx = Some(done_edit_rx);
27496 let mut start_edit_tx = Some(start_edit_tx);
27497
27498 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
27499 start_edit_tx.take().unwrap().send(()).unwrap();
27500 let done_edit_rx = done_edit_rx.take().unwrap();
27501 async move {
27502 done_edit_rx.await.unwrap();
27503 Ok(None)
27504 }
27505 });
27506
27507 start_edit_rx.await.unwrap();
27508 buffer2
27509 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
27510 .unwrap();
27511
27512 done_edit_tx.send(()).unwrap();
27513
27514 save.await.unwrap();
27515 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
27516}
27517
27518#[track_caller]
27519fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
27520 editor
27521 .all_inlays(cx)
27522 .into_iter()
27523 .filter_map(|inlay| inlay.get_color())
27524 .map(Rgba::from)
27525 .collect()
27526}
27527
27528#[gpui::test]
27529fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
27530 init_test(cx, |_| {});
27531
27532 let editor = cx.add_window(|window, cx| {
27533 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
27534 build_editor(buffer, window, cx)
27535 });
27536
27537 editor
27538 .update(cx, |editor, window, cx| {
27539 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27540 s.select_display_ranges([
27541 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
27542 ])
27543 });
27544
27545 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
27546
27547 assert_eq!(
27548 editor.display_text(cx),
27549 "line1\nline2\nline2",
27550 "Duplicating last line upward should create duplicate above, not on same line"
27551 );
27552
27553 assert_eq!(
27554 editor
27555 .selections
27556 .display_ranges(&editor.display_snapshot(cx)),
27557 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27558 "Selection should move to the duplicated line"
27559 );
27560 })
27561 .unwrap();
27562}
27563
27564#[gpui::test]
27565async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27566 init_test(cx, |_| {});
27567
27568 let mut cx = EditorTestContext::new(cx).await;
27569
27570 cx.set_state("line1\nline2ˇ");
27571
27572 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27573
27574 let clipboard_text = cx
27575 .read_from_clipboard()
27576 .and_then(|item| item.text().as_deref().map(str::to_string));
27577
27578 assert_eq!(
27579 clipboard_text,
27580 Some("line2\n".to_string()),
27581 "Copying a line without trailing newline should include a newline"
27582 );
27583
27584 cx.set_state("line1\nˇ");
27585
27586 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27587
27588 cx.assert_editor_state("line1\nline2\nˇ");
27589}
27590
27591#[gpui::test]
27592async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27593 init_test(cx, |_| {});
27594
27595 let mut cx = EditorTestContext::new(cx).await;
27596
27597 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27598
27599 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27600
27601 let clipboard_text = cx
27602 .read_from_clipboard()
27603 .and_then(|item| item.text().as_deref().map(str::to_string));
27604
27605 assert_eq!(
27606 clipboard_text,
27607 Some("line1\nline2\nline3\n".to_string()),
27608 "Copying multiple lines should include a single newline between lines"
27609 );
27610
27611 cx.set_state("lineA\nˇ");
27612
27613 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27614
27615 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27616}
27617
27618#[gpui::test]
27619async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27620 init_test(cx, |_| {});
27621
27622 let mut cx = EditorTestContext::new(cx).await;
27623
27624 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27625
27626 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
27627
27628 let clipboard_text = cx
27629 .read_from_clipboard()
27630 .and_then(|item| item.text().as_deref().map(str::to_string));
27631
27632 assert_eq!(
27633 clipboard_text,
27634 Some("line1\nline2\nline3\n".to_string()),
27635 "Copying multiple lines should include a single newline between lines"
27636 );
27637
27638 cx.set_state("lineA\nˇ");
27639
27640 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27641
27642 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27643}
27644
27645#[gpui::test]
27646async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27647 init_test(cx, |_| {});
27648
27649 let mut cx = EditorTestContext::new(cx).await;
27650
27651 cx.set_state("line1\nline2ˇ");
27652 cx.update_editor(|e, window, cx| {
27653 e.set_mode(EditorMode::SingleLine);
27654 assert!(e.key_context(window, cx).contains("end_of_input"));
27655 });
27656 cx.set_state("ˇline1\nline2");
27657 cx.update_editor(|e, window, cx| {
27658 assert!(!e.key_context(window, cx).contains("end_of_input"));
27659 });
27660 cx.set_state("line1ˇ\nline2");
27661 cx.update_editor(|e, window, cx| {
27662 assert!(!e.key_context(window, cx).contains("end_of_input"));
27663 });
27664}
27665
27666#[gpui::test]
27667async fn test_sticky_scroll(cx: &mut TestAppContext) {
27668 init_test(cx, |_| {});
27669 let mut cx = EditorTestContext::new(cx).await;
27670
27671 let buffer = indoc! {"
27672 ˇfn foo() {
27673 let abc = 123;
27674 }
27675 struct Bar;
27676 impl Bar {
27677 fn new() -> Self {
27678 Self
27679 }
27680 }
27681 fn baz() {
27682 }
27683 "};
27684 cx.set_state(&buffer);
27685
27686 cx.update_editor(|e, _, cx| {
27687 e.buffer()
27688 .read(cx)
27689 .as_singleton()
27690 .unwrap()
27691 .update(cx, |buffer, cx| {
27692 buffer.set_language(Some(rust_lang()), cx);
27693 })
27694 });
27695
27696 let mut sticky_headers = |offset: ScrollOffset| {
27697 cx.update_editor(|e, window, cx| {
27698 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27699 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27700 .into_iter()
27701 .map(
27702 |StickyHeader {
27703 start_point,
27704 offset,
27705 ..
27706 }| { (start_point, offset) },
27707 )
27708 .collect::<Vec<_>>()
27709 })
27710 };
27711
27712 let fn_foo = Point { row: 0, column: 0 };
27713 let impl_bar = Point { row: 4, column: 0 };
27714 let fn_new = Point { row: 5, column: 4 };
27715
27716 assert_eq!(sticky_headers(0.0), vec![]);
27717 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27718 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27719 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27720 assert_eq!(sticky_headers(2.0), vec![]);
27721 assert_eq!(sticky_headers(2.5), vec![]);
27722 assert_eq!(sticky_headers(3.0), vec![]);
27723 assert_eq!(sticky_headers(3.5), vec![]);
27724 assert_eq!(sticky_headers(4.0), vec![]);
27725 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27726 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27727 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27728 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27729 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27730 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27731 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27732 assert_eq!(sticky_headers(8.0), vec![]);
27733 assert_eq!(sticky_headers(8.5), vec![]);
27734 assert_eq!(sticky_headers(9.0), vec![]);
27735 assert_eq!(sticky_headers(9.5), vec![]);
27736 assert_eq!(sticky_headers(10.0), vec![]);
27737}
27738
27739#[gpui::test]
27740async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27741 init_test(cx, |_| {});
27742 cx.update(|cx| {
27743 SettingsStore::update_global(cx, |store, cx| {
27744 store.update_user_settings(cx, |settings| {
27745 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27746 enabled: Some(true),
27747 })
27748 });
27749 });
27750 });
27751 let mut cx = EditorTestContext::new(cx).await;
27752
27753 let line_height = cx.editor(|editor, window, _cx| {
27754 editor
27755 .style()
27756 .unwrap()
27757 .text
27758 .line_height_in_pixels(window.rem_size())
27759 });
27760
27761 let buffer = indoc! {"
27762 ˇfn foo() {
27763 let abc = 123;
27764 }
27765 struct Bar;
27766 impl Bar {
27767 fn new() -> Self {
27768 Self
27769 }
27770 }
27771 fn baz() {
27772 }
27773 "};
27774 cx.set_state(&buffer);
27775
27776 cx.update_editor(|e, _, cx| {
27777 e.buffer()
27778 .read(cx)
27779 .as_singleton()
27780 .unwrap()
27781 .update(cx, |buffer, cx| {
27782 buffer.set_language(Some(rust_lang()), cx);
27783 })
27784 });
27785
27786 let fn_foo = || empty_range(0, 0);
27787 let impl_bar = || empty_range(4, 0);
27788 let fn_new = || empty_range(5, 4);
27789
27790 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27791 cx.update_editor(|e, window, cx| {
27792 e.scroll(
27793 gpui::Point {
27794 x: 0.,
27795 y: scroll_offset,
27796 },
27797 None,
27798 window,
27799 cx,
27800 );
27801 });
27802 cx.simulate_click(
27803 gpui::Point {
27804 x: px(0.),
27805 y: click_offset as f32 * line_height,
27806 },
27807 Modifiers::none(),
27808 );
27809 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27810 };
27811
27812 assert_eq!(
27813 scroll_and_click(
27814 4.5, // impl Bar is halfway off the screen
27815 0.0 // click top of screen
27816 ),
27817 // scrolled to impl Bar
27818 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27819 );
27820
27821 assert_eq!(
27822 scroll_and_click(
27823 4.5, // impl Bar is halfway off the screen
27824 0.25 // click middle of impl Bar
27825 ),
27826 // scrolled to impl Bar
27827 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27828 );
27829
27830 assert_eq!(
27831 scroll_and_click(
27832 4.5, // impl Bar is halfway off the screen
27833 1.5 // click below impl Bar (e.g. fn new())
27834 ),
27835 // scrolled to fn new() - this is below the impl Bar header which has persisted
27836 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27837 );
27838
27839 assert_eq!(
27840 scroll_and_click(
27841 5.5, // fn new is halfway underneath impl Bar
27842 0.75 // click on the overlap of impl Bar and fn new()
27843 ),
27844 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27845 );
27846
27847 assert_eq!(
27848 scroll_and_click(
27849 5.5, // fn new is halfway underneath impl Bar
27850 1.25 // click on the visible part of fn new()
27851 ),
27852 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27853 );
27854
27855 assert_eq!(
27856 scroll_and_click(
27857 1.5, // fn foo is halfway off the screen
27858 0.0 // click top of screen
27859 ),
27860 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
27861 );
27862
27863 assert_eq!(
27864 scroll_and_click(
27865 1.5, // fn foo is halfway off the screen
27866 0.75 // click visible part of let abc...
27867 )
27868 .0,
27869 // no change in scroll
27870 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
27871 (gpui::Point { x: 0., y: 1.5 })
27872 );
27873}
27874
27875#[gpui::test]
27876async fn test_next_prev_reference(cx: &mut TestAppContext) {
27877 const CYCLE_POSITIONS: &[&'static str] = &[
27878 indoc! {"
27879 fn foo() {
27880 let ˇabc = 123;
27881 let x = abc + 1;
27882 let y = abc + 2;
27883 let z = abc + 2;
27884 }
27885 "},
27886 indoc! {"
27887 fn foo() {
27888 let abc = 123;
27889 let x = ˇabc + 1;
27890 let y = abc + 2;
27891 let z = abc + 2;
27892 }
27893 "},
27894 indoc! {"
27895 fn foo() {
27896 let abc = 123;
27897 let x = abc + 1;
27898 let y = ˇabc + 2;
27899 let z = abc + 2;
27900 }
27901 "},
27902 indoc! {"
27903 fn foo() {
27904 let abc = 123;
27905 let x = abc + 1;
27906 let y = abc + 2;
27907 let z = ˇabc + 2;
27908 }
27909 "},
27910 ];
27911
27912 init_test(cx, |_| {});
27913
27914 let mut cx = EditorLspTestContext::new_rust(
27915 lsp::ServerCapabilities {
27916 references_provider: Some(lsp::OneOf::Left(true)),
27917 ..Default::default()
27918 },
27919 cx,
27920 )
27921 .await;
27922
27923 // importantly, the cursor is in the middle
27924 cx.set_state(indoc! {"
27925 fn foo() {
27926 let aˇbc = 123;
27927 let x = abc + 1;
27928 let y = abc + 2;
27929 let z = abc + 2;
27930 }
27931 "});
27932
27933 let reference_ranges = [
27934 lsp::Position::new(1, 8),
27935 lsp::Position::new(2, 12),
27936 lsp::Position::new(3, 12),
27937 lsp::Position::new(4, 12),
27938 ]
27939 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27940
27941 cx.lsp
27942 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27943 Ok(Some(
27944 reference_ranges
27945 .map(|range| lsp::Location {
27946 uri: params.text_document_position.text_document.uri.clone(),
27947 range,
27948 })
27949 .to_vec(),
27950 ))
27951 });
27952
27953 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27954 cx.update_editor(|editor, window, cx| {
27955 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27956 })
27957 .unwrap()
27958 .await
27959 .unwrap()
27960 };
27961
27962 _move(Direction::Next, 1, &mut cx).await;
27963 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27964
27965 _move(Direction::Next, 1, &mut cx).await;
27966 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27967
27968 _move(Direction::Next, 1, &mut cx).await;
27969 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27970
27971 // loops back to the start
27972 _move(Direction::Next, 1, &mut cx).await;
27973 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27974
27975 // loops back to the end
27976 _move(Direction::Prev, 1, &mut cx).await;
27977 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27978
27979 _move(Direction::Prev, 1, &mut cx).await;
27980 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27981
27982 _move(Direction::Prev, 1, &mut cx).await;
27983 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27984
27985 _move(Direction::Prev, 1, &mut cx).await;
27986 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27987
27988 _move(Direction::Next, 3, &mut cx).await;
27989 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27990
27991 _move(Direction::Prev, 2, &mut cx).await;
27992 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27993}
27994
27995#[gpui::test]
27996async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
27997 init_test(cx, |_| {});
27998
27999 let (editor, cx) = cx.add_window_view(|window, cx| {
28000 let multi_buffer = MultiBuffer::build_multi(
28001 [
28002 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28003 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28004 ],
28005 cx,
28006 );
28007 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28008 });
28009
28010 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28011 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28012
28013 cx.assert_excerpts_with_selections(indoc! {"
28014 [EXCERPT]
28015 ˇ1
28016 2
28017 3
28018 [EXCERPT]
28019 1
28020 2
28021 3
28022 "});
28023
28024 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28025 cx.update_editor(|editor, window, cx| {
28026 editor.change_selections(None.into(), window, cx, |s| {
28027 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28028 });
28029 });
28030 cx.assert_excerpts_with_selections(indoc! {"
28031 [EXCERPT]
28032 1
28033 2ˇ
28034 3
28035 [EXCERPT]
28036 1
28037 2
28038 3
28039 "});
28040
28041 cx.update_editor(|editor, window, cx| {
28042 editor
28043 .select_all_matches(&SelectAllMatches, window, cx)
28044 .unwrap();
28045 });
28046 cx.assert_excerpts_with_selections(indoc! {"
28047 [EXCERPT]
28048 1
28049 2ˇ
28050 3
28051 [EXCERPT]
28052 1
28053 2ˇ
28054 3
28055 "});
28056
28057 cx.update_editor(|editor, window, cx| {
28058 editor.handle_input("X", window, cx);
28059 });
28060 cx.assert_excerpts_with_selections(indoc! {"
28061 [EXCERPT]
28062 1
28063 Xˇ
28064 3
28065 [EXCERPT]
28066 1
28067 Xˇ
28068 3
28069 "});
28070
28071 // Scenario 2: Select "2", then fold second buffer before insertion
28072 cx.update_multibuffer(|mb, cx| {
28073 for buffer_id in buffer_ids.iter() {
28074 let buffer = mb.buffer(*buffer_id).unwrap();
28075 buffer.update(cx, |buffer, cx| {
28076 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28077 });
28078 }
28079 });
28080
28081 // Select "2" and select all matches
28082 cx.update_editor(|editor, window, cx| {
28083 editor.change_selections(None.into(), window, cx, |s| {
28084 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28085 });
28086 editor
28087 .select_all_matches(&SelectAllMatches, window, cx)
28088 .unwrap();
28089 });
28090
28091 // Fold second buffer - should remove selections from folded buffer
28092 cx.update_editor(|editor, _, cx| {
28093 editor.fold_buffer(buffer_ids[1], cx);
28094 });
28095 cx.assert_excerpts_with_selections(indoc! {"
28096 [EXCERPT]
28097 1
28098 2ˇ
28099 3
28100 [EXCERPT]
28101 [FOLDED]
28102 "});
28103
28104 // Insert text - should only affect first buffer
28105 cx.update_editor(|editor, window, cx| {
28106 editor.handle_input("Y", window, cx);
28107 });
28108 cx.update_editor(|editor, _, cx| {
28109 editor.unfold_buffer(buffer_ids[1], cx);
28110 });
28111 cx.assert_excerpts_with_selections(indoc! {"
28112 [EXCERPT]
28113 1
28114 Yˇ
28115 3
28116 [EXCERPT]
28117 1
28118 2
28119 3
28120 "});
28121
28122 // Scenario 3: Select "2", then fold first buffer before insertion
28123 cx.update_multibuffer(|mb, cx| {
28124 for buffer_id in buffer_ids.iter() {
28125 let buffer = mb.buffer(*buffer_id).unwrap();
28126 buffer.update(cx, |buffer, cx| {
28127 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28128 });
28129 }
28130 });
28131
28132 // Select "2" and select all matches
28133 cx.update_editor(|editor, window, cx| {
28134 editor.change_selections(None.into(), window, cx, |s| {
28135 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28136 });
28137 editor
28138 .select_all_matches(&SelectAllMatches, window, cx)
28139 .unwrap();
28140 });
28141
28142 // Fold first buffer - should remove selections from folded buffer
28143 cx.update_editor(|editor, _, cx| {
28144 editor.fold_buffer(buffer_ids[0], cx);
28145 });
28146 cx.assert_excerpts_with_selections(indoc! {"
28147 [EXCERPT]
28148 [FOLDED]
28149 [EXCERPT]
28150 1
28151 2ˇ
28152 3
28153 "});
28154
28155 // Insert text - should only affect second buffer
28156 cx.update_editor(|editor, window, cx| {
28157 editor.handle_input("Z", window, cx);
28158 });
28159 cx.update_editor(|editor, _, cx| {
28160 editor.unfold_buffer(buffer_ids[0], cx);
28161 });
28162 cx.assert_excerpts_with_selections(indoc! {"
28163 [EXCERPT]
28164 1
28165 2
28166 3
28167 [EXCERPT]
28168 1
28169 Zˇ
28170 3
28171 "});
28172
28173 // Edge case scenario: fold all buffers, then try to insert
28174 cx.update_editor(|editor, _, cx| {
28175 editor.fold_buffer(buffer_ids[0], cx);
28176 editor.fold_buffer(buffer_ids[1], cx);
28177 });
28178 cx.assert_excerpts_with_selections(indoc! {"
28179 [EXCERPT]
28180 ˇ[FOLDED]
28181 [EXCERPT]
28182 [FOLDED]
28183 "});
28184
28185 // Insert should work via default selection
28186 cx.update_editor(|editor, window, cx| {
28187 editor.handle_input("W", window, cx);
28188 });
28189 cx.update_editor(|editor, _, cx| {
28190 editor.unfold_buffer(buffer_ids[0], cx);
28191 editor.unfold_buffer(buffer_ids[1], cx);
28192 });
28193 cx.assert_excerpts_with_selections(indoc! {"
28194 [EXCERPT]
28195 Wˇ1
28196 2
28197 3
28198 [EXCERPT]
28199 1
28200 Z
28201 3
28202 "});
28203}