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::{
39 IndentGuide, MultiBufferFilterMode, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
40};
41use parking_lot::Mutex;
42use pretty_assertions::{assert_eq, assert_ne};
43use project::{
44 FakeFs,
45 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
46 project_settings::LspSettings,
47};
48use serde_json::{self, json};
49use settings::{
50 AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
51 IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
52};
53use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
54use std::{
55 iter,
56 sync::atomic::{self, AtomicUsize},
57};
58use test::build_editor_with_project;
59use text::ToPoint as _;
60use unindent::Unindent;
61use util::{
62 assert_set_eq, path,
63 rel_path::rel_path,
64 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
65 uri,
66};
67use workspace::{
68 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
69 OpenOptions, ViewId,
70 invalid_item_view::InvalidItemView,
71 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
72 register_project_item,
73};
74
75fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
76 editor
77 .selections
78 .display_ranges(&editor.display_snapshot(cx))
79}
80
81#[gpui::test]
82fn test_edit_events(cx: &mut TestAppContext) {
83 init_test(cx, |_| {});
84
85 let buffer = cx.new(|cx| {
86 let mut buffer = language::Buffer::local("123456", cx);
87 buffer.set_group_interval(Duration::from_secs(1));
88 buffer
89 });
90
91 let events = Rc::new(RefCell::new(Vec::new()));
92 let editor1 = cx.add_window({
93 let events = events.clone();
94 |window, cx| {
95 let entity = cx.entity();
96 cx.subscribe_in(
97 &entity,
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor1", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 let editor2 = cx.add_window({
113 let events = events.clone();
114 |window, cx| {
115 cx.subscribe_in(
116 &cx.entity(),
117 window,
118 move |_, _, event: &EditorEvent, _, _| match event {
119 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
120 EditorEvent::BufferEdited => {
121 events.borrow_mut().push(("editor2", "buffer edited"))
122 }
123 _ => {}
124 },
125 )
126 .detach();
127 Editor::for_buffer(buffer.clone(), None, window, cx)
128 }
129 });
130
131 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
132
133 // Mutating editor 1 will emit an `Edited` event only for that editor.
134 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor1", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Mutating editor 2 will emit an `Edited` event only for that editor.
145 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor2", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Undoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Redoing on editor 1 will emit an `Edited` event only for that editor.
167 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor1", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Undoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // Redoing on editor 2 will emit an `Edited` event only for that editor.
189 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
190 assert_eq!(
191 mem::take(&mut *events.borrow_mut()),
192 [
193 ("editor2", "edited"),
194 ("editor1", "buffer edited"),
195 ("editor2", "buffer edited"),
196 ]
197 );
198
199 // No event is emitted when the mutation is a no-op.
200 _ = editor2.update(cx, |editor, window, cx| {
201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
202 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
203 });
204
205 editor.backspace(&Backspace, window, cx);
206 });
207 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
208}
209
210#[gpui::test]
211fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
212 init_test(cx, |_| {});
213
214 let mut now = Instant::now();
215 let group_interval = Duration::from_millis(1);
216 let buffer = cx.new(|cx| {
217 let mut buf = language::Buffer::local("123456", cx);
218 buf.set_group_interval(group_interval);
219 buf
220 });
221 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
222 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
223
224 _ = editor.update(cx, |editor, window, cx| {
225 editor.start_transaction_at(now, window, cx);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
228 });
229
230 editor.insert("cd", window, cx);
231 editor.end_transaction_at(now, cx);
232 assert_eq!(editor.text(cx), "12cd56");
233 assert_eq!(
234 editor.selections.ranges(&editor.display_snapshot(cx)),
235 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
236 );
237
238 editor.start_transaction_at(now, window, cx);
239 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
240 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
241 });
242 editor.insert("e", window, cx);
243 editor.end_transaction_at(now, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(
246 editor.selections.ranges(&editor.display_snapshot(cx)),
247 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
248 );
249
250 now += group_interval + Duration::from_millis(1);
251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
252 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
253 });
254
255 // Simulate an edit in another editor
256 buffer.update(cx, |buffer, cx| {
257 buffer.start_transaction_at(now, cx);
258 buffer.edit(
259 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
260 None,
261 cx,
262 );
263 buffer.edit(
264 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
265 None,
266 cx,
267 );
268 buffer.end_transaction_at(now, cx);
269 });
270
271 assert_eq!(editor.text(cx), "ab2cde6");
272 assert_eq!(
273 editor.selections.ranges(&editor.display_snapshot(cx)),
274 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
275 );
276
277 // Last transaction happened past the group interval in a different editor.
278 // Undo it individually and don't restore selections.
279 editor.undo(&Undo, window, cx);
280 assert_eq!(editor.text(cx), "12cde6");
281 assert_eq!(
282 editor.selections.ranges(&editor.display_snapshot(cx)),
283 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
284 );
285
286 // First two transactions happened within the group interval in this editor.
287 // Undo them together and restore selections.
288 editor.undo(&Undo, window, cx);
289 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
290 assert_eq!(editor.text(cx), "123456");
291 assert_eq!(
292 editor.selections.ranges(&editor.display_snapshot(cx)),
293 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
294 );
295
296 // Redo the first two transactions together.
297 editor.redo(&Redo, window, cx);
298 assert_eq!(editor.text(cx), "12cde6");
299 assert_eq!(
300 editor.selections.ranges(&editor.display_snapshot(cx)),
301 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
302 );
303
304 // Redo the last transaction on its own.
305 editor.redo(&Redo, window, cx);
306 assert_eq!(editor.text(cx), "ab2cde6");
307 assert_eq!(
308 editor.selections.ranges(&editor.display_snapshot(cx)),
309 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
310 );
311
312 // Test empty transactions.
313 editor.start_transaction_at(now, window, cx);
314 editor.end_transaction_at(now, cx);
315 editor.undo(&Undo, window, cx);
316 assert_eq!(editor.text(cx), "12cde6");
317 });
318}
319
320#[gpui::test]
321fn test_ime_composition(cx: &mut TestAppContext) {
322 init_test(cx, |_| {});
323
324 let buffer = cx.new(|cx| {
325 let mut buffer = language::Buffer::local("abcde", cx);
326 // Ensure automatic grouping doesn't occur.
327 buffer.set_group_interval(Duration::ZERO);
328 buffer
329 });
330
331 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
332 cx.add_window(|window, cx| {
333 let mut editor = build_editor(buffer.clone(), window, cx);
334
335 // Start a new IME composition.
336 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
337 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
338 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
339 assert_eq!(editor.text(cx), "äbcde");
340 assert_eq!(
341 editor.marked_text_ranges(cx),
342 Some(vec![
343 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
344 ])
345 );
346
347 // Finalize IME composition.
348 editor.replace_text_in_range(None, "ā", window, cx);
349 assert_eq!(editor.text(cx), "ābcde");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 // IME composition edits are grouped and are undone/redone at once.
353 editor.undo(&Default::default(), window, cx);
354 assert_eq!(editor.text(cx), "abcde");
355 assert_eq!(editor.marked_text_ranges(cx), None);
356 editor.redo(&Default::default(), window, cx);
357 assert_eq!(editor.text(cx), "ābcde");
358 assert_eq!(editor.marked_text_ranges(cx), None);
359
360 // Start a new IME composition.
361 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
362 assert_eq!(
363 editor.marked_text_ranges(cx),
364 Some(vec![
365 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
366 ])
367 );
368
369 // Undoing during an IME composition cancels it.
370 editor.undo(&Default::default(), window, cx);
371 assert_eq!(editor.text(cx), "ābcde");
372 assert_eq!(editor.marked_text_ranges(cx), None);
373
374 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
375 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
376 assert_eq!(editor.text(cx), "ābcdè");
377 assert_eq!(
378 editor.marked_text_ranges(cx),
379 Some(vec![
380 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
381 ])
382 );
383
384 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
385 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
386 assert_eq!(editor.text(cx), "ābcdę");
387 assert_eq!(editor.marked_text_ranges(cx), None);
388
389 // Start a new IME composition with multiple cursors.
390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
391 s.select_ranges([
392 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
393 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
394 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
395 ])
396 });
397 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
398 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
399 assert_eq!(
400 editor.marked_text_ranges(cx),
401 Some(vec![
402 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
403 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
404 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
405 ])
406 );
407
408 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
409 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
410 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
411 assert_eq!(
412 editor.marked_text_ranges(cx),
413 Some(vec![
414 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
415 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
416 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
417 ])
418 );
419
420 // Finalize IME composition with multiple cursors.
421 editor.replace_text_in_range(Some(9..10), "2", window, cx);
422 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
423 assert_eq!(editor.marked_text_ranges(cx), None);
424
425 editor
426 });
427}
428
429#[gpui::test]
430fn test_selection_with_mouse(cx: &mut TestAppContext) {
431 init_test(cx, |_| {});
432
433 let editor = cx.add_window(|window, cx| {
434 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
435 build_editor(buffer, window, cx)
436 });
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
440 });
441 assert_eq!(
442 editor
443 .update(cx, |editor, _, cx| display_ranges(editor, cx))
444 .unwrap(),
445 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(3), 3),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| display_ranges(editor, cx))
461 .unwrap(),
462 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
463 );
464
465 _ = editor.update(cx, |editor, window, cx| {
466 editor.update_selection(
467 DisplayPoint::new(DisplayRow(1), 1),
468 0,
469 gpui::Point::<f32>::default(),
470 window,
471 cx,
472 );
473 });
474
475 assert_eq!(
476 editor
477 .update(cx, |editor, _, cx| display_ranges(editor, cx))
478 .unwrap(),
479 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
480 );
481
482 _ = editor.update(cx, |editor, window, cx| {
483 editor.end_selection(window, cx);
484 editor.update_selection(
485 DisplayPoint::new(DisplayRow(3), 3),
486 0,
487 gpui::Point::<f32>::default(),
488 window,
489 cx,
490 );
491 });
492
493 assert_eq!(
494 editor
495 .update(cx, |editor, _, cx| display_ranges(editor, cx))
496 .unwrap(),
497 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
498 );
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
502 editor.update_selection(
503 DisplayPoint::new(DisplayRow(0), 0),
504 0,
505 gpui::Point::<f32>::default(),
506 window,
507 cx,
508 );
509 });
510
511 assert_eq!(
512 editor
513 .update(cx, |editor, _, cx| display_ranges(editor, cx))
514 .unwrap(),
515 [
516 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
517 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
518 ]
519 );
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.end_selection(window, cx);
523 });
524
525 assert_eq!(
526 editor
527 .update(cx, |editor, _, cx| display_ranges(editor, cx))
528 .unwrap(),
529 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
530 );
531}
532
533#[gpui::test]
534fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
535 init_test(cx, |_| {});
536
537 let editor = cx.add_window(|window, cx| {
538 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
539 build_editor(buffer, window, cx)
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
544 });
545
546 _ = editor.update(cx, |editor, window, cx| {
547 editor.end_selection(window, cx);
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
552 });
553
554 _ = editor.update(cx, |editor, window, cx| {
555 editor.end_selection(window, cx);
556 });
557
558 assert_eq!(
559 editor
560 .update(cx, |editor, _, cx| display_ranges(editor, cx))
561 .unwrap(),
562 [
563 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
564 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
565 ]
566 );
567
568 _ = editor.update(cx, |editor, window, cx| {
569 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
570 });
571
572 _ = editor.update(cx, |editor, window, cx| {
573 editor.end_selection(window, cx);
574 });
575
576 assert_eq!(
577 editor
578 .update(cx, |editor, _, cx| display_ranges(editor, cx))
579 .unwrap(),
580 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
581 );
582}
583
584#[gpui::test]
585fn test_canceling_pending_selection(cx: &mut TestAppContext) {
586 init_test(cx, |_| {});
587
588 let editor = cx.add_window(|window, cx| {
589 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
590 build_editor(buffer, window, cx)
591 });
592
593 _ = editor.update(cx, |editor, window, cx| {
594 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
595 assert_eq!(
596 display_ranges(editor, cx),
597 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
598 );
599 });
600
601 _ = editor.update(cx, |editor, window, cx| {
602 editor.update_selection(
603 DisplayPoint::new(DisplayRow(3), 3),
604 0,
605 gpui::Point::<f32>::default(),
606 window,
607 cx,
608 );
609 assert_eq!(
610 display_ranges(editor, cx),
611 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
612 );
613 });
614
615 _ = editor.update(cx, |editor, window, cx| {
616 editor.cancel(&Cancel, window, cx);
617 editor.update_selection(
618 DisplayPoint::new(DisplayRow(1), 1),
619 0,
620 gpui::Point::<f32>::default(),
621 window,
622 cx,
623 );
624 assert_eq!(
625 display_ranges(editor, cx),
626 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
627 );
628 });
629}
630
631#[gpui::test]
632fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
633 init_test(cx, |_| {});
634
635 let editor = cx.add_window(|window, cx| {
636 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
637 build_editor(buffer, window, cx)
638 });
639
640 _ = editor.update(cx, |editor, window, cx| {
641 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
642 assert_eq!(
643 display_ranges(editor, cx),
644 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
645 );
646
647 editor.move_down(&Default::default(), window, cx);
648 assert_eq!(
649 display_ranges(editor, cx),
650 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
651 );
652
653 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
654 assert_eq!(
655 display_ranges(editor, cx),
656 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
657 );
658
659 editor.move_up(&Default::default(), window, cx);
660 assert_eq!(
661 display_ranges(editor, cx),
662 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
663 );
664 });
665}
666
667#[gpui::test]
668fn test_extending_selection(cx: &mut TestAppContext) {
669 init_test(cx, |_| {});
670
671 let editor = cx.add_window(|window, cx| {
672 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
673 build_editor(buffer, window, cx)
674 });
675
676 _ = editor.update(cx, |editor, window, cx| {
677 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
678 editor.end_selection(window, cx);
679 assert_eq!(
680 display_ranges(editor, cx),
681 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
682 );
683
684 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
685 editor.end_selection(window, cx);
686 assert_eq!(
687 display_ranges(editor, cx),
688 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
689 );
690
691 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
692 editor.end_selection(window, cx);
693 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
694 assert_eq!(
695 display_ranges(editor, cx),
696 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
697 );
698
699 editor.update_selection(
700 DisplayPoint::new(DisplayRow(0), 1),
701 0,
702 gpui::Point::<f32>::default(),
703 window,
704 cx,
705 );
706 editor.end_selection(window, cx);
707 assert_eq!(
708 display_ranges(editor, cx),
709 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
710 );
711
712 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
713 editor.end_selection(window, cx);
714 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
715 editor.end_selection(window, cx);
716 assert_eq!(
717 display_ranges(editor, cx),
718 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
719 );
720
721 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
722 assert_eq!(
723 display_ranges(editor, cx),
724 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
725 );
726
727 editor.update_selection(
728 DisplayPoint::new(DisplayRow(0), 6),
729 0,
730 gpui::Point::<f32>::default(),
731 window,
732 cx,
733 );
734 assert_eq!(
735 display_ranges(editor, cx),
736 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
737 );
738
739 editor.update_selection(
740 DisplayPoint::new(DisplayRow(0), 1),
741 0,
742 gpui::Point::<f32>::default(),
743 window,
744 cx,
745 );
746 editor.end_selection(window, cx);
747 assert_eq!(
748 display_ranges(editor, cx),
749 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
750 );
751 });
752}
753
754#[gpui::test]
755fn test_clone(cx: &mut TestAppContext) {
756 init_test(cx, |_| {});
757
758 let (text, selection_ranges) = marked_text_ranges(
759 indoc! {"
760 one
761 two
762 threeˇ
763 four
764 fiveˇ
765 "},
766 true,
767 );
768
769 let editor = cx.add_window(|window, cx| {
770 let buffer = MultiBuffer::build_simple(&text, cx);
771 build_editor(buffer, window, cx)
772 });
773
774 _ = editor.update(cx, |editor, window, cx| {
775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
776 s.select_ranges(
777 selection_ranges
778 .iter()
779 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
780 )
781 });
782 editor.fold_creases(
783 vec![
784 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
785 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
786 ],
787 true,
788 window,
789 cx,
790 );
791 });
792
793 let cloned_editor = editor
794 .update(cx, |editor, _, cx| {
795 cx.open_window(Default::default(), |window, cx| {
796 cx.new(|cx| editor.clone(window, cx))
797 })
798 })
799 .unwrap()
800 .unwrap();
801
802 let snapshot = editor
803 .update(cx, |e, window, cx| e.snapshot(window, cx))
804 .unwrap();
805 let cloned_snapshot = cloned_editor
806 .update(cx, |e, window, cx| e.snapshot(window, cx))
807 .unwrap();
808
809 assert_eq!(
810 cloned_editor
811 .update(cx, |e, _, cx| e.display_text(cx))
812 .unwrap(),
813 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
814 );
815 assert_eq!(
816 cloned_snapshot
817 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
818 .collect::<Vec<_>>(),
819 snapshot
820 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
821 .collect::<Vec<_>>(),
822 );
823 assert_set_eq!(
824 cloned_editor
825 .update(cx, |editor, _, cx| editor
826 .selections
827 .ranges::<Point>(&editor.display_snapshot(cx)))
828 .unwrap(),
829 editor
830 .update(cx, |editor, _, cx| editor
831 .selections
832 .ranges(&editor.display_snapshot(cx)))
833 .unwrap()
834 );
835 assert_set_eq!(
836 cloned_editor
837 .update(cx, |e, _window, cx| e
838 .selections
839 .display_ranges(&e.display_snapshot(cx)))
840 .unwrap(),
841 editor
842 .update(cx, |e, _, cx| e
843 .selections
844 .display_ranges(&e.display_snapshot(cx)))
845 .unwrap()
846 );
847}
848
849#[gpui::test]
850async fn test_navigation_history(cx: &mut TestAppContext) {
851 init_test(cx, |_| {});
852
853 use workspace::item::Item;
854
855 let fs = FakeFs::new(cx.executor());
856 let project = Project::test(fs, [], cx).await;
857 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
858 let pane = workspace
859 .update(cx, |workspace, _, _| workspace.active_pane().clone())
860 .unwrap();
861
862 _ = workspace.update(cx, |_v, window, cx| {
863 cx.new(|cx| {
864 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
865 let mut editor = build_editor(buffer, window, cx);
866 let handle = cx.entity();
867 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
868
869 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
870 editor.nav_history.as_mut().unwrap().pop_backward(cx)
871 }
872
873 // Move the cursor a small distance.
874 // Nothing is added to the navigation history.
875 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
876 s.select_display_ranges([
877 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
878 ])
879 });
880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
881 s.select_display_ranges([
882 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
883 ])
884 });
885 assert!(pop_history(&mut editor, cx).is_none());
886
887 // Move the cursor a large distance.
888 // The history can jump back to the previous position.
889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
890 s.select_display_ranges([
891 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
892 ])
893 });
894 let nav_entry = pop_history(&mut editor, cx).unwrap();
895 editor.navigate(nav_entry.data.unwrap(), window, cx);
896 assert_eq!(nav_entry.item.id(), cx.entity_id());
897 assert_eq!(
898 editor
899 .selections
900 .display_ranges(&editor.display_snapshot(cx)),
901 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
902 );
903 assert!(pop_history(&mut editor, cx).is_none());
904
905 // Move the cursor a small distance via the mouse.
906 // Nothing is added to the navigation history.
907 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
908 editor.end_selection(window, cx);
909 assert_eq!(
910 editor
911 .selections
912 .display_ranges(&editor.display_snapshot(cx)),
913 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
914 );
915 assert!(pop_history(&mut editor, cx).is_none());
916
917 // Move the cursor a large distance via the mouse.
918 // The history can jump back to the previous position.
919 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
920 editor.end_selection(window, cx);
921 assert_eq!(
922 editor
923 .selections
924 .display_ranges(&editor.display_snapshot(cx)),
925 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
926 );
927 let nav_entry = pop_history(&mut editor, cx).unwrap();
928 editor.navigate(nav_entry.data.unwrap(), window, cx);
929 assert_eq!(nav_entry.item.id(), cx.entity_id());
930 assert_eq!(
931 editor
932 .selections
933 .display_ranges(&editor.display_snapshot(cx)),
934 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
935 );
936 assert!(pop_history(&mut editor, cx).is_none());
937
938 // Set scroll position to check later
939 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
940 let original_scroll_position = editor.scroll_manager.anchor();
941
942 // Jump to the end of the document and adjust scroll
943 editor.move_to_end(&MoveToEnd, window, cx);
944 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
945 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
946
947 let nav_entry = pop_history(&mut editor, cx).unwrap();
948 editor.navigate(nav_entry.data.unwrap(), window, cx);
949 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
950
951 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
952 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
953 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
954 let invalid_point = Point::new(9999, 0);
955 editor.navigate(
956 Box::new(NavigationData {
957 cursor_anchor: invalid_anchor,
958 cursor_position: invalid_point,
959 scroll_anchor: ScrollAnchor {
960 anchor: invalid_anchor,
961 offset: Default::default(),
962 },
963 scroll_top_row: invalid_point.row,
964 }),
965 window,
966 cx,
967 );
968 assert_eq!(
969 editor
970 .selections
971 .display_ranges(&editor.display_snapshot(cx)),
972 &[editor.max_point(cx)..editor.max_point(cx)]
973 );
974 assert_eq!(
975 editor.scroll_position(cx),
976 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
977 );
978
979 editor
980 })
981 });
982}
983
984#[gpui::test]
985fn test_cancel(cx: &mut TestAppContext) {
986 init_test(cx, |_| {});
987
988 let editor = cx.add_window(|window, cx| {
989 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
990 build_editor(buffer, window, cx)
991 });
992
993 _ = editor.update(cx, |editor, window, cx| {
994 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
995 editor.update_selection(
996 DisplayPoint::new(DisplayRow(1), 1),
997 0,
998 gpui::Point::<f32>::default(),
999 window,
1000 cx,
1001 );
1002 editor.end_selection(window, cx);
1003
1004 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1005 editor.update_selection(
1006 DisplayPoint::new(DisplayRow(0), 3),
1007 0,
1008 gpui::Point::<f32>::default(),
1009 window,
1010 cx,
1011 );
1012 editor.end_selection(window, cx);
1013 assert_eq!(
1014 display_ranges(editor, cx),
1015 [
1016 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1017 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1018 ]
1019 );
1020 });
1021
1022 _ = editor.update(cx, |editor, window, cx| {
1023 editor.cancel(&Cancel, window, cx);
1024 assert_eq!(
1025 display_ranges(editor, cx),
1026 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1027 );
1028 });
1029
1030 _ = editor.update(cx, |editor, window, cx| {
1031 editor.cancel(&Cancel, window, cx);
1032 assert_eq!(
1033 display_ranges(editor, cx),
1034 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 impl Foo {
1047 // Hello!
1048
1049 fn a() {
1050 1
1051 }
1052
1053 fn b() {
1054 2
1055 }
1056
1057 fn c() {
1058 3
1059 }
1060 }
1061 "
1062 .unindent(),
1063 cx,
1064 );
1065 build_editor(buffer, window, cx)
1066 });
1067
1068 _ = editor.update(cx, |editor, window, cx| {
1069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1070 s.select_display_ranges([
1071 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1072 ]);
1073 });
1074 editor.fold(&Fold, window, cx);
1075 assert_eq!(
1076 editor.display_text(cx),
1077 "
1078 impl Foo {
1079 // Hello!
1080
1081 fn a() {
1082 1
1083 }
1084
1085 fn b() {⋯
1086 }
1087
1088 fn c() {⋯
1089 }
1090 }
1091 "
1092 .unindent(),
1093 );
1094
1095 editor.fold(&Fold, window, cx);
1096 assert_eq!(
1097 editor.display_text(cx),
1098 "
1099 impl Foo {⋯
1100 }
1101 "
1102 .unindent(),
1103 );
1104
1105 editor.unfold_lines(&UnfoldLines, window, cx);
1106 assert_eq!(
1107 editor.display_text(cx),
1108 "
1109 impl Foo {
1110 // Hello!
1111
1112 fn a() {
1113 1
1114 }
1115
1116 fn b() {⋯
1117 }
1118
1119 fn c() {⋯
1120 }
1121 }
1122 "
1123 .unindent(),
1124 );
1125
1126 editor.unfold_lines(&UnfoldLines, window, cx);
1127 assert_eq!(
1128 editor.display_text(cx),
1129 editor.buffer.read(cx).read(cx).text()
1130 );
1131 });
1132}
1133
1134#[gpui::test]
1135fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1136 init_test(cx, |_| {});
1137
1138 let editor = cx.add_window(|window, cx| {
1139 let buffer = MultiBuffer::build_simple(
1140 &"
1141 class Foo:
1142 # Hello!
1143
1144 def a():
1145 print(1)
1146
1147 def b():
1148 print(2)
1149
1150 def c():
1151 print(3)
1152 "
1153 .unindent(),
1154 cx,
1155 );
1156 build_editor(buffer, window, cx)
1157 });
1158
1159 _ = editor.update(cx, |editor, window, cx| {
1160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1161 s.select_display_ranges([
1162 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1163 ]);
1164 });
1165 editor.fold(&Fold, window, cx);
1166 assert_eq!(
1167 editor.display_text(cx),
1168 "
1169 class Foo:
1170 # Hello!
1171
1172 def a():
1173 print(1)
1174
1175 def b():⋯
1176
1177 def c():⋯
1178 "
1179 .unindent(),
1180 );
1181
1182 editor.fold(&Fold, window, cx);
1183 assert_eq!(
1184 editor.display_text(cx),
1185 "
1186 class Foo:⋯
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.unfold_lines(&UnfoldLines, window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:
1196 # Hello!
1197
1198 def a():
1199 print(1)
1200
1201 def b():⋯
1202
1203 def c():⋯
1204 "
1205 .unindent(),
1206 );
1207
1208 editor.unfold_lines(&UnfoldLines, window, cx);
1209 assert_eq!(
1210 editor.display_text(cx),
1211 editor.buffer.read(cx).read(cx).text()
1212 );
1213 });
1214}
1215
1216#[gpui::test]
1217fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1218 init_test(cx, |_| {});
1219
1220 let editor = cx.add_window(|window, cx| {
1221 let buffer = MultiBuffer::build_simple(
1222 &"
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 def c():
1234 print(3)
1235
1236
1237 "
1238 .unindent(),
1239 cx,
1240 );
1241 build_editor(buffer, window, cx)
1242 });
1243
1244 _ = editor.update(cx, |editor, window, cx| {
1245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1246 s.select_display_ranges([
1247 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1248 ]);
1249 });
1250 editor.fold(&Fold, window, cx);
1251 assert_eq!(
1252 editor.display_text(cx),
1253 "
1254 class Foo:
1255 # Hello!
1256
1257 def a():
1258 print(1)
1259
1260 def b():⋯
1261
1262
1263 def c():⋯
1264
1265
1266 "
1267 .unindent(),
1268 );
1269
1270 editor.fold(&Fold, window, cx);
1271 assert_eq!(
1272 editor.display_text(cx),
1273 "
1274 class Foo:⋯
1275
1276
1277 "
1278 .unindent(),
1279 );
1280
1281 editor.unfold_lines(&UnfoldLines, window, cx);
1282 assert_eq!(
1283 editor.display_text(cx),
1284 "
1285 class Foo:
1286 # Hello!
1287
1288 def a():
1289 print(1)
1290
1291 def b():⋯
1292
1293
1294 def c():⋯
1295
1296
1297 "
1298 .unindent(),
1299 );
1300
1301 editor.unfold_lines(&UnfoldLines, window, cx);
1302 assert_eq!(
1303 editor.display_text(cx),
1304 editor.buffer.read(cx).read(cx).text()
1305 );
1306 });
1307}
1308
1309#[gpui::test]
1310fn test_fold_at_level(cx: &mut TestAppContext) {
1311 init_test(cx, |_| {});
1312
1313 let editor = cx.add_window(|window, cx| {
1314 let buffer = MultiBuffer::build_simple(
1315 &"
1316 class Foo:
1317 # Hello!
1318
1319 def a():
1320 print(1)
1321
1322 def b():
1323 print(2)
1324
1325
1326 class Bar:
1327 # World!
1328
1329 def a():
1330 print(1)
1331
1332 def b():
1333 print(2)
1334
1335
1336 "
1337 .unindent(),
1338 cx,
1339 );
1340 build_editor(buffer, window, cx)
1341 });
1342
1343 _ = editor.update(cx, |editor, window, cx| {
1344 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1345 assert_eq!(
1346 editor.display_text(cx),
1347 "
1348 class Foo:
1349 # Hello!
1350
1351 def a():⋯
1352
1353 def b():⋯
1354
1355
1356 class Bar:
1357 # World!
1358
1359 def a():⋯
1360
1361 def b():⋯
1362
1363
1364 "
1365 .unindent(),
1366 );
1367
1368 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1369 assert_eq!(
1370 editor.display_text(cx),
1371 "
1372 class Foo:⋯
1373
1374
1375 class Bar:⋯
1376
1377
1378 "
1379 .unindent(),
1380 );
1381
1382 editor.unfold_all(&UnfoldAll, window, cx);
1383 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1384 assert_eq!(
1385 editor.display_text(cx),
1386 "
1387 class Foo:
1388 # Hello!
1389
1390 def a():
1391 print(1)
1392
1393 def b():
1394 print(2)
1395
1396
1397 class Bar:
1398 # World!
1399
1400 def a():
1401 print(1)
1402
1403 def b():
1404 print(2)
1405
1406
1407 "
1408 .unindent(),
1409 );
1410
1411 assert_eq!(
1412 editor.display_text(cx),
1413 editor.buffer.read(cx).read(cx).text()
1414 );
1415 let (_, positions) = marked_text_ranges(
1416 &"
1417 class Foo:
1418 # Hello!
1419
1420 def a():
1421 print(1)
1422
1423 def b():
1424 p«riˇ»nt(2)
1425
1426
1427 class Bar:
1428 # World!
1429
1430 def a():
1431 «ˇprint(1)
1432
1433 def b():
1434 print(2)»
1435
1436
1437 "
1438 .unindent(),
1439 true,
1440 );
1441
1442 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1443 s.select_ranges(
1444 positions
1445 .iter()
1446 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1447 )
1448 });
1449
1450 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1451 assert_eq!(
1452 editor.display_text(cx),
1453 "
1454 class Foo:
1455 # Hello!
1456
1457 def a():⋯
1458
1459 def b():
1460 print(2)
1461
1462
1463 class Bar:
1464 # World!
1465
1466 def a():
1467 print(1)
1468
1469 def b():
1470 print(2)
1471
1472
1473 "
1474 .unindent(),
1475 );
1476 });
1477}
1478
1479#[gpui::test]
1480fn test_move_cursor(cx: &mut TestAppContext) {
1481 init_test(cx, |_| {});
1482
1483 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1484 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1485
1486 buffer.update(cx, |buffer, cx| {
1487 buffer.edit(
1488 vec![
1489 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1490 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1491 ],
1492 None,
1493 cx,
1494 );
1495 });
1496 _ = editor.update(cx, |editor, window, cx| {
1497 assert_eq!(
1498 display_ranges(editor, cx),
1499 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1500 );
1501
1502 editor.move_down(&MoveDown, window, cx);
1503 assert_eq!(
1504 display_ranges(editor, cx),
1505 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1506 );
1507
1508 editor.move_right(&MoveRight, window, cx);
1509 assert_eq!(
1510 display_ranges(editor, cx),
1511 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1512 );
1513
1514 editor.move_left(&MoveLeft, window, cx);
1515 assert_eq!(
1516 display_ranges(editor, cx),
1517 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1518 );
1519
1520 editor.move_up(&MoveUp, window, cx);
1521 assert_eq!(
1522 display_ranges(editor, cx),
1523 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1524 );
1525
1526 editor.move_to_end(&MoveToEnd, window, cx);
1527 assert_eq!(
1528 display_ranges(editor, cx),
1529 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1530 );
1531
1532 editor.move_to_beginning(&MoveToBeginning, window, cx);
1533 assert_eq!(
1534 display_ranges(editor, cx),
1535 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1536 );
1537
1538 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1539 s.select_display_ranges([
1540 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1541 ]);
1542 });
1543 editor.select_to_beginning(&SelectToBeginning, window, cx);
1544 assert_eq!(
1545 display_ranges(editor, cx),
1546 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1547 );
1548
1549 editor.select_to_end(&SelectToEnd, window, cx);
1550 assert_eq!(
1551 display_ranges(editor, cx),
1552 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1553 );
1554 });
1555}
1556
1557#[gpui::test]
1558fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1559 init_test(cx, |_| {});
1560
1561 let editor = cx.add_window(|window, cx| {
1562 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1563 build_editor(buffer, window, cx)
1564 });
1565
1566 assert_eq!('🟥'.len_utf8(), 4);
1567 assert_eq!('α'.len_utf8(), 2);
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.fold_creases(
1571 vec![
1572 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1573 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1574 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1575 ],
1576 true,
1577 window,
1578 cx,
1579 );
1580 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1581
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 editor.move_right(&MoveRight, window, cx);
1587 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1588
1589 editor.move_down(&MoveDown, window, cx);
1590 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".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, "ab".len())]);
1595 editor.move_left(&MoveLeft, window, cx);
1596 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1597
1598 editor.move_down(&MoveDown, 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 editor.move_right(&MoveRight, window, cx);
1605 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1606
1607 editor.move_up(&MoveUp, window, cx);
1608 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1609 editor.move_down(&MoveDown, window, cx);
1610 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1611 editor.move_up(&MoveUp, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1613
1614 editor.move_up(&MoveUp, 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 editor.move_left(&MoveLeft, window, cx);
1619 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1620 });
1621}
1622
1623#[gpui::test]
1624fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1625 init_test(cx, |_| {});
1626
1627 let editor = cx.add_window(|window, cx| {
1628 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1629 build_editor(buffer, window, cx)
1630 });
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1633 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1634 });
1635
1636 // moving above start of document should move selection to start of document,
1637 // but the next move down should still be at the original goal_x
1638 editor.move_up(&MoveUp, window, cx);
1639 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1640
1641 editor.move_down(&MoveDown, window, cx);
1642 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1643
1644 editor.move_down(&MoveDown, window, cx);
1645 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1646
1647 editor.move_down(&MoveDown, window, cx);
1648 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1649
1650 editor.move_down(&MoveDown, window, cx);
1651 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1652
1653 // moving past end of document should not change goal_x
1654 editor.move_down(&MoveDown, window, cx);
1655 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1656
1657 editor.move_down(&MoveDown, window, cx);
1658 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1659
1660 editor.move_up(&MoveUp, window, cx);
1661 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1662
1663 editor.move_up(&MoveUp, window, cx);
1664 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1665
1666 editor.move_up(&MoveUp, window, cx);
1667 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1668 });
1669}
1670
1671#[gpui::test]
1672fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1673 init_test(cx, |_| {});
1674 let move_to_beg = MoveToBeginningOfLine {
1675 stop_at_soft_wraps: true,
1676 stop_at_indent: true,
1677 };
1678
1679 let delete_to_beg = DeleteToBeginningOfLine {
1680 stop_at_indent: false,
1681 };
1682
1683 let move_to_end = MoveToEndOfLine {
1684 stop_at_soft_wraps: true,
1685 };
1686
1687 let editor = cx.add_window(|window, cx| {
1688 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1689 build_editor(buffer, window, cx)
1690 });
1691 _ = editor.update(cx, |editor, window, cx| {
1692 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1693 s.select_display_ranges([
1694 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1695 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1696 ]);
1697 });
1698 });
1699
1700 _ = editor.update(cx, |editor, window, cx| {
1701 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1702 assert_eq!(
1703 display_ranges(editor, cx),
1704 &[
1705 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1706 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1707 ]
1708 );
1709 });
1710
1711 _ = editor.update(cx, |editor, window, cx| {
1712 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1713 assert_eq!(
1714 display_ranges(editor, cx),
1715 &[
1716 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1717 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1718 ]
1719 );
1720 });
1721
1722 _ = editor.update(cx, |editor, window, cx| {
1723 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1724 assert_eq!(
1725 display_ranges(editor, cx),
1726 &[
1727 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1728 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1729 ]
1730 );
1731 });
1732
1733 _ = editor.update(cx, |editor, window, cx| {
1734 editor.move_to_end_of_line(&move_to_end, window, cx);
1735 assert_eq!(
1736 display_ranges(editor, cx),
1737 &[
1738 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1739 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1740 ]
1741 );
1742 });
1743
1744 // Moving to the end of line again is a no-op.
1745 _ = editor.update(cx, |editor, window, cx| {
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 display_ranges(editor, cx),
1749 &[
1750 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1751 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1752 ]
1753 );
1754 });
1755
1756 _ = editor.update(cx, |editor, window, cx| {
1757 editor.move_left(&MoveLeft, window, cx);
1758 editor.select_to_beginning_of_line(
1759 &SelectToBeginningOfLine {
1760 stop_at_soft_wraps: true,
1761 stop_at_indent: true,
1762 },
1763 window,
1764 cx,
1765 );
1766 assert_eq!(
1767 display_ranges(editor, cx),
1768 &[
1769 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1770 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1771 ]
1772 );
1773 });
1774
1775 _ = editor.update(cx, |editor, window, cx| {
1776 editor.select_to_beginning_of_line(
1777 &SelectToBeginningOfLine {
1778 stop_at_soft_wraps: true,
1779 stop_at_indent: true,
1780 },
1781 window,
1782 cx,
1783 );
1784 assert_eq!(
1785 display_ranges(editor, cx),
1786 &[
1787 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1788 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1789 ]
1790 );
1791 });
1792
1793 _ = editor.update(cx, |editor, window, cx| {
1794 editor.select_to_beginning_of_line(
1795 &SelectToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 },
1799 window,
1800 cx,
1801 );
1802 assert_eq!(
1803 display_ranges(editor, cx),
1804 &[
1805 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1806 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1807 ]
1808 );
1809 });
1810
1811 _ = editor.update(cx, |editor, window, cx| {
1812 editor.select_to_end_of_line(
1813 &SelectToEndOfLine {
1814 stop_at_soft_wraps: true,
1815 },
1816 window,
1817 cx,
1818 );
1819 assert_eq!(
1820 display_ranges(editor, cx),
1821 &[
1822 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1824 ]
1825 );
1826 });
1827
1828 _ = editor.update(cx, |editor, window, cx| {
1829 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1830 assert_eq!(editor.display_text(cx), "ab\n de");
1831 assert_eq!(
1832 display_ranges(editor, cx),
1833 &[
1834 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1835 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1836 ]
1837 );
1838 });
1839
1840 _ = editor.update(cx, |editor, window, cx| {
1841 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1842 assert_eq!(editor.display_text(cx), "\n");
1843 assert_eq!(
1844 display_ranges(editor, cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1848 ]
1849 );
1850 });
1851}
1852
1853#[gpui::test]
1854fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1855 init_test(cx, |_| {});
1856 let move_to_beg = MoveToBeginningOfLine {
1857 stop_at_soft_wraps: false,
1858 stop_at_indent: false,
1859 };
1860
1861 let move_to_end = MoveToEndOfLine {
1862 stop_at_soft_wraps: false,
1863 };
1864
1865 let editor = cx.add_window(|window, cx| {
1866 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1867 build_editor(buffer, window, cx)
1868 });
1869
1870 _ = editor.update(cx, |editor, window, cx| {
1871 editor.set_wrap_width(Some(140.0.into()), cx);
1872
1873 // We expect the following lines after wrapping
1874 // ```
1875 // thequickbrownfox
1876 // jumpedoverthelazydo
1877 // gs
1878 // ```
1879 // The final `gs` was soft-wrapped onto a new line.
1880 assert_eq!(
1881 "thequickbrownfox\njumpedoverthelaz\nydogs",
1882 editor.display_text(cx),
1883 );
1884
1885 // First, let's assert behavior on the first line, that was not soft-wrapped.
1886 // Start the cursor at the `k` on the first line
1887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1888 s.select_display_ranges([
1889 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1890 ]);
1891 });
1892
1893 // Moving to the beginning of the line should put us at the beginning of the line.
1894 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1895 assert_eq!(
1896 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1897 display_ranges(editor, cx)
1898 );
1899
1900 // Moving to the end of the line should put us at the end of the line.
1901 editor.move_to_end_of_line(&move_to_end, window, cx);
1902 assert_eq!(
1903 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1904 display_ranges(editor, cx)
1905 );
1906
1907 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1908 // Start the cursor at the last line (`y` that was wrapped to a new line)
1909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1910 s.select_display_ranges([
1911 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1912 ]);
1913 });
1914
1915 // Moving to the beginning of the line should put us at the start of the second line of
1916 // display text, i.e., the `j`.
1917 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1918 assert_eq!(
1919 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1920 display_ranges(editor, cx)
1921 );
1922
1923 // Moving to the beginning of the line again should be a no-op.
1924 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1925 assert_eq!(
1926 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1927 display_ranges(editor, cx)
1928 );
1929
1930 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1931 // next display line.
1932 editor.move_to_end_of_line(&move_to_end, window, cx);
1933 assert_eq!(
1934 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1935 display_ranges(editor, cx)
1936 );
1937
1938 // Moving to the end of the line again should be a no-op.
1939 editor.move_to_end_of_line(&move_to_end, window, cx);
1940 assert_eq!(
1941 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1942 display_ranges(editor, cx)
1943 );
1944 });
1945}
1946
1947#[gpui::test]
1948fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1949 init_test(cx, |_| {});
1950
1951 let move_to_beg = MoveToBeginningOfLine {
1952 stop_at_soft_wraps: true,
1953 stop_at_indent: true,
1954 };
1955
1956 let select_to_beg = SelectToBeginningOfLine {
1957 stop_at_soft_wraps: true,
1958 stop_at_indent: true,
1959 };
1960
1961 let delete_to_beg = DeleteToBeginningOfLine {
1962 stop_at_indent: true,
1963 };
1964
1965 let move_to_end = MoveToEndOfLine {
1966 stop_at_soft_wraps: false,
1967 };
1968
1969 let editor = cx.add_window(|window, cx| {
1970 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1971 build_editor(buffer, window, cx)
1972 });
1973
1974 _ = editor.update(cx, |editor, window, cx| {
1975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1976 s.select_display_ranges([
1977 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1978 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1979 ]);
1980 });
1981
1982 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1983 // and the second cursor at the first non-whitespace character in the line.
1984 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1985 assert_eq!(
1986 display_ranges(editor, cx),
1987 &[
1988 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1989 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1990 ]
1991 );
1992
1993 // Moving to the beginning of the line again should be a no-op for the first cursor,
1994 // and should move the second cursor to the beginning of the line.
1995 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1996 assert_eq!(
1997 display_ranges(editor, cx),
1998 &[
1999 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2000 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2001 ]
2002 );
2003
2004 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2005 // and should move the second cursor back to the first non-whitespace character in the line.
2006 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2007 assert_eq!(
2008 display_ranges(editor, cx),
2009 &[
2010 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2011 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2012 ]
2013 );
2014
2015 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2016 // and to the first non-whitespace character in the line for the second cursor.
2017 editor.move_to_end_of_line(&move_to_end, window, cx);
2018 editor.move_left(&MoveLeft, window, cx);
2019 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2020 assert_eq!(
2021 display_ranges(editor, cx),
2022 &[
2023 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2024 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2025 ]
2026 );
2027
2028 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2029 // and should select to the beginning of the line for the second cursor.
2030 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2031 assert_eq!(
2032 display_ranges(editor, cx),
2033 &[
2034 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2035 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2036 ]
2037 );
2038
2039 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2040 // and should delete to the first non-whitespace character in the line for the second cursor.
2041 editor.move_to_end_of_line(&move_to_end, window, cx);
2042 editor.move_left(&MoveLeft, window, cx);
2043 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2044 assert_eq!(editor.text(cx), "c\n f");
2045 });
2046}
2047
2048#[gpui::test]
2049fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2050 init_test(cx, |_| {});
2051
2052 let move_to_beg = MoveToBeginningOfLine {
2053 stop_at_soft_wraps: true,
2054 stop_at_indent: true,
2055 };
2056
2057 let editor = cx.add_window(|window, cx| {
2058 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2059 build_editor(buffer, window, cx)
2060 });
2061
2062 _ = editor.update(cx, |editor, window, cx| {
2063 // test cursor between line_start and indent_start
2064 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2065 s.select_display_ranges([
2066 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2067 ]);
2068 });
2069
2070 // cursor should move to line_start
2071 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2072 assert_eq!(
2073 display_ranges(editor, cx),
2074 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2075 );
2076
2077 // cursor should move to indent_start
2078 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2079 assert_eq!(
2080 display_ranges(editor, cx),
2081 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2082 );
2083
2084 // cursor should move to back to line_start
2085 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2086 assert_eq!(
2087 display_ranges(editor, cx),
2088 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2089 );
2090 });
2091}
2092
2093#[gpui::test]
2094fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2095 init_test(cx, |_| {});
2096
2097 let editor = cx.add_window(|window, cx| {
2098 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2099 build_editor(buffer, window, cx)
2100 });
2101 _ = editor.update(cx, |editor, window, cx| {
2102 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2103 s.select_display_ranges([
2104 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2105 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2106 ])
2107 });
2108 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2109 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2110
2111 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2112 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2113
2114 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2115 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2116
2117 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2118 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2119
2120 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2121 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2122
2123 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2124 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2125
2126 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2127 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2128
2129 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2130 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2131
2132 editor.move_right(&MoveRight, window, cx);
2133 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2134 assert_selection_ranges(
2135 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2136 editor,
2137 cx,
2138 );
2139
2140 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2141 assert_selection_ranges(
2142 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2143 editor,
2144 cx,
2145 );
2146
2147 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2148 assert_selection_ranges(
2149 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2150 editor,
2151 cx,
2152 );
2153 });
2154}
2155
2156#[gpui::test]
2157fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2158 init_test(cx, |_| {});
2159
2160 let editor = cx.add_window(|window, cx| {
2161 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2162 build_editor(buffer, window, cx)
2163 });
2164
2165 _ = editor.update(cx, |editor, window, cx| {
2166 editor.set_wrap_width(Some(140.0.into()), cx);
2167 assert_eq!(
2168 editor.display_text(cx),
2169 "use one::{\n two::three::\n four::five\n};"
2170 );
2171
2172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2173 s.select_display_ranges([
2174 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2175 ]);
2176 });
2177
2178 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2179 assert_eq!(
2180 display_ranges(editor, cx),
2181 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2182 );
2183
2184 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2185 assert_eq!(
2186 display_ranges(editor, cx),
2187 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2188 );
2189
2190 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2191 assert_eq!(
2192 display_ranges(editor, cx),
2193 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2194 );
2195
2196 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2197 assert_eq!(
2198 display_ranges(editor, cx),
2199 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2200 );
2201
2202 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2203 assert_eq!(
2204 display_ranges(editor, cx),
2205 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2206 );
2207
2208 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2209 assert_eq!(
2210 display_ranges(editor, cx),
2211 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2212 );
2213 });
2214}
2215
2216#[gpui::test]
2217async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2218 init_test(cx, |_| {});
2219 let mut cx = EditorTestContext::new(cx).await;
2220
2221 let line_height = cx.editor(|editor, window, _| {
2222 editor
2223 .style()
2224 .unwrap()
2225 .text
2226 .line_height_in_pixels(window.rem_size())
2227 });
2228 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2229
2230 cx.set_state(
2231 &r#"ˇone
2232 two
2233
2234 three
2235 fourˇ
2236 five
2237
2238 six"#
2239 .unindent(),
2240 );
2241
2242 cx.update_editor(|editor, window, cx| {
2243 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2244 });
2245 cx.assert_editor_state(
2246 &r#"one
2247 two
2248 ˇ
2249 three
2250 four
2251 five
2252 ˇ
2253 six"#
2254 .unindent(),
2255 );
2256
2257 cx.update_editor(|editor, window, cx| {
2258 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2259 });
2260 cx.assert_editor_state(
2261 &r#"one
2262 two
2263
2264 three
2265 four
2266 five
2267 ˇ
2268 sixˇ"#
2269 .unindent(),
2270 );
2271
2272 cx.update_editor(|editor, window, cx| {
2273 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2274 });
2275 cx.assert_editor_state(
2276 &r#"one
2277 two
2278
2279 three
2280 four
2281 five
2282
2283 sixˇ"#
2284 .unindent(),
2285 );
2286
2287 cx.update_editor(|editor, window, cx| {
2288 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2289 });
2290 cx.assert_editor_state(
2291 &r#"one
2292 two
2293
2294 three
2295 four
2296 five
2297 ˇ
2298 six"#
2299 .unindent(),
2300 );
2301
2302 cx.update_editor(|editor, window, cx| {
2303 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2304 });
2305 cx.assert_editor_state(
2306 &r#"one
2307 two
2308 ˇ
2309 three
2310 four
2311 five
2312
2313 six"#
2314 .unindent(),
2315 );
2316
2317 cx.update_editor(|editor, window, cx| {
2318 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2319 });
2320 cx.assert_editor_state(
2321 &r#"ˇone
2322 two
2323
2324 three
2325 four
2326 five
2327
2328 six"#
2329 .unindent(),
2330 );
2331}
2332
2333#[gpui::test]
2334async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2335 init_test(cx, |_| {});
2336 let mut cx = EditorTestContext::new(cx).await;
2337 let line_height = cx.editor(|editor, window, _| {
2338 editor
2339 .style()
2340 .unwrap()
2341 .text
2342 .line_height_in_pixels(window.rem_size())
2343 });
2344 let window = cx.window;
2345 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2346
2347 cx.set_state(
2348 r#"ˇone
2349 two
2350 three
2351 four
2352 five
2353 six
2354 seven
2355 eight
2356 nine
2357 ten
2358 "#,
2359 );
2360
2361 cx.update_editor(|editor, window, cx| {
2362 assert_eq!(
2363 editor.snapshot(window, cx).scroll_position(),
2364 gpui::Point::new(0., 0.)
2365 );
2366 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2367 assert_eq!(
2368 editor.snapshot(window, cx).scroll_position(),
2369 gpui::Point::new(0., 3.)
2370 );
2371 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2372 assert_eq!(
2373 editor.snapshot(window, cx).scroll_position(),
2374 gpui::Point::new(0., 6.)
2375 );
2376 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2377 assert_eq!(
2378 editor.snapshot(window, cx).scroll_position(),
2379 gpui::Point::new(0., 3.)
2380 );
2381
2382 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2383 assert_eq!(
2384 editor.snapshot(window, cx).scroll_position(),
2385 gpui::Point::new(0., 1.)
2386 );
2387 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2388 assert_eq!(
2389 editor.snapshot(window, cx).scroll_position(),
2390 gpui::Point::new(0., 3.)
2391 );
2392 });
2393}
2394
2395#[gpui::test]
2396async fn test_autoscroll(cx: &mut TestAppContext) {
2397 init_test(cx, |_| {});
2398 let mut cx = EditorTestContext::new(cx).await;
2399
2400 let line_height = cx.update_editor(|editor, window, cx| {
2401 editor.set_vertical_scroll_margin(2, cx);
2402 editor
2403 .style()
2404 .unwrap()
2405 .text
2406 .line_height_in_pixels(window.rem_size())
2407 });
2408 let window = cx.window;
2409 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2410
2411 cx.set_state(
2412 r#"ˇone
2413 two
2414 three
2415 four
2416 five
2417 six
2418 seven
2419 eight
2420 nine
2421 ten
2422 "#,
2423 );
2424 cx.update_editor(|editor, window, cx| {
2425 assert_eq!(
2426 editor.snapshot(window, cx).scroll_position(),
2427 gpui::Point::new(0., 0.0)
2428 );
2429 });
2430
2431 // Add a cursor below the visible area. Since both cursors cannot fit
2432 // on screen, the editor autoscrolls to reveal the newest cursor, and
2433 // allows the vertical scroll margin below that cursor.
2434 cx.update_editor(|editor, window, cx| {
2435 editor.change_selections(Default::default(), window, cx, |selections| {
2436 selections.select_ranges([
2437 Point::new(0, 0)..Point::new(0, 0),
2438 Point::new(6, 0)..Point::new(6, 0),
2439 ]);
2440 })
2441 });
2442 cx.update_editor(|editor, window, cx| {
2443 assert_eq!(
2444 editor.snapshot(window, cx).scroll_position(),
2445 gpui::Point::new(0., 3.0)
2446 );
2447 });
2448
2449 // Move down. The editor cursor scrolls down to track the newest cursor.
2450 cx.update_editor(|editor, window, cx| {
2451 editor.move_down(&Default::default(), window, cx);
2452 });
2453 cx.update_editor(|editor, window, cx| {
2454 assert_eq!(
2455 editor.snapshot(window, cx).scroll_position(),
2456 gpui::Point::new(0., 4.0)
2457 );
2458 });
2459
2460 // Add a cursor above the visible area. Since both cursors fit on screen,
2461 // the editor scrolls to show both.
2462 cx.update_editor(|editor, window, cx| {
2463 editor.change_selections(Default::default(), window, cx, |selections| {
2464 selections.select_ranges([
2465 Point::new(1, 0)..Point::new(1, 0),
2466 Point::new(6, 0)..Point::new(6, 0),
2467 ]);
2468 })
2469 });
2470 cx.update_editor(|editor, window, cx| {
2471 assert_eq!(
2472 editor.snapshot(window, cx).scroll_position(),
2473 gpui::Point::new(0., 1.0)
2474 );
2475 });
2476}
2477
2478#[gpui::test]
2479async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481 let mut cx = EditorTestContext::new(cx).await;
2482
2483 let line_height = cx.editor(|editor, window, _cx| {
2484 editor
2485 .style()
2486 .unwrap()
2487 .text
2488 .line_height_in_pixels(window.rem_size())
2489 });
2490 let window = cx.window;
2491 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2492 cx.set_state(
2493 &r#"
2494 ˇone
2495 two
2496 threeˇ
2497 four
2498 five
2499 six
2500 seven
2501 eight
2502 nine
2503 ten
2504 "#
2505 .unindent(),
2506 );
2507
2508 cx.update_editor(|editor, window, cx| {
2509 editor.move_page_down(&MovePageDown::default(), window, cx)
2510 });
2511 cx.assert_editor_state(
2512 &r#"
2513 one
2514 two
2515 three
2516 ˇfour
2517 five
2518 sixˇ
2519 seven
2520 eight
2521 nine
2522 ten
2523 "#
2524 .unindent(),
2525 );
2526
2527 cx.update_editor(|editor, window, cx| {
2528 editor.move_page_down(&MovePageDown::default(), window, cx)
2529 });
2530 cx.assert_editor_state(
2531 &r#"
2532 one
2533 two
2534 three
2535 four
2536 five
2537 six
2538 ˇseven
2539 eight
2540 nineˇ
2541 ten
2542 "#
2543 .unindent(),
2544 );
2545
2546 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2547 cx.assert_editor_state(
2548 &r#"
2549 one
2550 two
2551 three
2552 ˇfour
2553 five
2554 sixˇ
2555 seven
2556 eight
2557 nine
2558 ten
2559 "#
2560 .unindent(),
2561 );
2562
2563 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2564 cx.assert_editor_state(
2565 &r#"
2566 ˇone
2567 two
2568 threeˇ
2569 four
2570 five
2571 six
2572 seven
2573 eight
2574 nine
2575 ten
2576 "#
2577 .unindent(),
2578 );
2579
2580 // Test select collapsing
2581 cx.update_editor(|editor, window, cx| {
2582 editor.move_page_down(&MovePageDown::default(), window, cx);
2583 editor.move_page_down(&MovePageDown::default(), window, cx);
2584 editor.move_page_down(&MovePageDown::default(), window, cx);
2585 });
2586 cx.assert_editor_state(
2587 &r#"
2588 one
2589 two
2590 three
2591 four
2592 five
2593 six
2594 seven
2595 eight
2596 nine
2597 ˇten
2598 ˇ"#
2599 .unindent(),
2600 );
2601}
2602
2603#[gpui::test]
2604async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2605 init_test(cx, |_| {});
2606 let mut cx = EditorTestContext::new(cx).await;
2607 cx.set_state("one «two threeˇ» four");
2608 cx.update_editor(|editor, window, cx| {
2609 editor.delete_to_beginning_of_line(
2610 &DeleteToBeginningOfLine {
2611 stop_at_indent: false,
2612 },
2613 window,
2614 cx,
2615 );
2616 assert_eq!(editor.text(cx), " four");
2617 });
2618}
2619
2620#[gpui::test]
2621async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2622 init_test(cx, |_| {});
2623
2624 let mut cx = EditorTestContext::new(cx).await;
2625
2626 // For an empty selection, the preceding word fragment is deleted.
2627 // For non-empty selections, only selected characters are deleted.
2628 cx.set_state("onˇe two t«hreˇ»e four");
2629 cx.update_editor(|editor, window, cx| {
2630 editor.delete_to_previous_word_start(
2631 &DeleteToPreviousWordStart {
2632 ignore_newlines: false,
2633 ignore_brackets: false,
2634 },
2635 window,
2636 cx,
2637 );
2638 });
2639 cx.assert_editor_state("ˇe two tˇe four");
2640
2641 cx.set_state("e tˇwo te «fˇ»our");
2642 cx.update_editor(|editor, window, cx| {
2643 editor.delete_to_next_word_end(
2644 &DeleteToNextWordEnd {
2645 ignore_newlines: false,
2646 ignore_brackets: false,
2647 },
2648 window,
2649 cx,
2650 );
2651 });
2652 cx.assert_editor_state("e tˇ te ˇour");
2653}
2654
2655#[gpui::test]
2656async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2657 init_test(cx, |_| {});
2658
2659 let mut cx = EditorTestContext::new(cx).await;
2660
2661 cx.set_state("here is some text ˇwith a space");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_previous_word_start(
2664 &DeleteToPreviousWordStart {
2665 ignore_newlines: false,
2666 ignore_brackets: true,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2673 cx.assert_editor_state("here is some textˇwith a space");
2674
2675 cx.set_state("here is some text ˇwith a space");
2676 cx.update_editor(|editor, window, cx| {
2677 editor.delete_to_previous_word_start(
2678 &DeleteToPreviousWordStart {
2679 ignore_newlines: false,
2680 ignore_brackets: false,
2681 },
2682 window,
2683 cx,
2684 );
2685 });
2686 cx.assert_editor_state("here is some textˇwith a space");
2687
2688 cx.set_state("here is some textˇ with a space");
2689 cx.update_editor(|editor, window, cx| {
2690 editor.delete_to_next_word_end(
2691 &DeleteToNextWordEnd {
2692 ignore_newlines: false,
2693 ignore_brackets: true,
2694 },
2695 window,
2696 cx,
2697 );
2698 });
2699 // Same happens in the other direction.
2700 cx.assert_editor_state("here is some textˇwith a space");
2701
2702 cx.set_state("here is some textˇ with a space");
2703 cx.update_editor(|editor, window, cx| {
2704 editor.delete_to_next_word_end(
2705 &DeleteToNextWordEnd {
2706 ignore_newlines: false,
2707 ignore_brackets: false,
2708 },
2709 window,
2710 cx,
2711 );
2712 });
2713 cx.assert_editor_state("here is some textˇwith a space");
2714
2715 cx.set_state("here is some textˇ with a space");
2716 cx.update_editor(|editor, window, cx| {
2717 editor.delete_to_next_word_end(
2718 &DeleteToNextWordEnd {
2719 ignore_newlines: true,
2720 ignore_brackets: false,
2721 },
2722 window,
2723 cx,
2724 );
2725 });
2726 cx.assert_editor_state("here is some textˇwith a space");
2727 cx.update_editor(|editor, window, cx| {
2728 editor.delete_to_previous_word_start(
2729 &DeleteToPreviousWordStart {
2730 ignore_newlines: true,
2731 ignore_brackets: false,
2732 },
2733 window,
2734 cx,
2735 );
2736 });
2737 cx.assert_editor_state("here is some ˇwith a space");
2738 cx.update_editor(|editor, window, cx| {
2739 editor.delete_to_previous_word_start(
2740 &DeleteToPreviousWordStart {
2741 ignore_newlines: true,
2742 ignore_brackets: false,
2743 },
2744 window,
2745 cx,
2746 );
2747 });
2748 // Single whitespaces are removed with the word behind them.
2749 cx.assert_editor_state("here is ˇwith a space");
2750 cx.update_editor(|editor, window, cx| {
2751 editor.delete_to_previous_word_start(
2752 &DeleteToPreviousWordStart {
2753 ignore_newlines: true,
2754 ignore_brackets: false,
2755 },
2756 window,
2757 cx,
2758 );
2759 });
2760 cx.assert_editor_state("here ˇwith a space");
2761 cx.update_editor(|editor, window, cx| {
2762 editor.delete_to_previous_word_start(
2763 &DeleteToPreviousWordStart {
2764 ignore_newlines: true,
2765 ignore_brackets: false,
2766 },
2767 window,
2768 cx,
2769 );
2770 });
2771 cx.assert_editor_state("ˇwith a space");
2772 cx.update_editor(|editor, window, cx| {
2773 editor.delete_to_previous_word_start(
2774 &DeleteToPreviousWordStart {
2775 ignore_newlines: true,
2776 ignore_brackets: false,
2777 },
2778 window,
2779 cx,
2780 );
2781 });
2782 cx.assert_editor_state("ˇwith a space");
2783 cx.update_editor(|editor, window, cx| {
2784 editor.delete_to_next_word_end(
2785 &DeleteToNextWordEnd {
2786 ignore_newlines: true,
2787 ignore_brackets: false,
2788 },
2789 window,
2790 cx,
2791 );
2792 });
2793 // Same happens in the other direction.
2794 cx.assert_editor_state("ˇ a space");
2795 cx.update_editor(|editor, window, cx| {
2796 editor.delete_to_next_word_end(
2797 &DeleteToNextWordEnd {
2798 ignore_newlines: true,
2799 ignore_brackets: false,
2800 },
2801 window,
2802 cx,
2803 );
2804 });
2805 cx.assert_editor_state("ˇ space");
2806 cx.update_editor(|editor, window, cx| {
2807 editor.delete_to_next_word_end(
2808 &DeleteToNextWordEnd {
2809 ignore_newlines: true,
2810 ignore_brackets: false,
2811 },
2812 window,
2813 cx,
2814 );
2815 });
2816 cx.assert_editor_state("ˇ");
2817 cx.update_editor(|editor, window, cx| {
2818 editor.delete_to_next_word_end(
2819 &DeleteToNextWordEnd {
2820 ignore_newlines: true,
2821 ignore_brackets: false,
2822 },
2823 window,
2824 cx,
2825 );
2826 });
2827 cx.assert_editor_state("ˇ");
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_previous_word_start(
2830 &DeleteToPreviousWordStart {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state("ˇ");
2839}
2840
2841#[gpui::test]
2842async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2843 init_test(cx, |_| {});
2844
2845 let language = Arc::new(
2846 Language::new(
2847 LanguageConfig {
2848 brackets: BracketPairConfig {
2849 pairs: vec![
2850 BracketPair {
2851 start: "\"".to_string(),
2852 end: "\"".to_string(),
2853 close: true,
2854 surround: true,
2855 newline: false,
2856 },
2857 BracketPair {
2858 start: "(".to_string(),
2859 end: ")".to_string(),
2860 close: true,
2861 surround: true,
2862 newline: true,
2863 },
2864 ],
2865 ..BracketPairConfig::default()
2866 },
2867 ..LanguageConfig::default()
2868 },
2869 Some(tree_sitter_rust::LANGUAGE.into()),
2870 )
2871 .with_brackets_query(
2872 r#"
2873 ("(" @open ")" @close)
2874 ("\"" @open "\"" @close)
2875 "#,
2876 )
2877 .unwrap(),
2878 );
2879
2880 let mut cx = EditorTestContext::new(cx).await;
2881 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2882
2883 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2884 cx.update_editor(|editor, window, cx| {
2885 editor.delete_to_previous_word_start(
2886 &DeleteToPreviousWordStart {
2887 ignore_newlines: true,
2888 ignore_brackets: false,
2889 },
2890 window,
2891 cx,
2892 );
2893 });
2894 // Deletion stops before brackets if asked to not ignore them.
2895 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2896 cx.update_editor(|editor, window, cx| {
2897 editor.delete_to_previous_word_start(
2898 &DeleteToPreviousWordStart {
2899 ignore_newlines: true,
2900 ignore_brackets: false,
2901 },
2902 window,
2903 cx,
2904 );
2905 });
2906 // Deletion has to remove a single bracket and then stop again.
2907 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2908
2909 cx.update_editor(|editor, window, cx| {
2910 editor.delete_to_previous_word_start(
2911 &DeleteToPreviousWordStart {
2912 ignore_newlines: true,
2913 ignore_brackets: false,
2914 },
2915 window,
2916 cx,
2917 );
2918 });
2919 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2920
2921 cx.update_editor(|editor, window, cx| {
2922 editor.delete_to_previous_word_start(
2923 &DeleteToPreviousWordStart {
2924 ignore_newlines: true,
2925 ignore_brackets: false,
2926 },
2927 window,
2928 cx,
2929 );
2930 });
2931 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2932
2933 cx.update_editor(|editor, window, cx| {
2934 editor.delete_to_previous_word_start(
2935 &DeleteToPreviousWordStart {
2936 ignore_newlines: true,
2937 ignore_brackets: false,
2938 },
2939 window,
2940 cx,
2941 );
2942 });
2943 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2944
2945 cx.update_editor(|editor, window, cx| {
2946 editor.delete_to_next_word_end(
2947 &DeleteToNextWordEnd {
2948 ignore_newlines: true,
2949 ignore_brackets: false,
2950 },
2951 window,
2952 cx,
2953 );
2954 });
2955 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2956 cx.assert_editor_state(r#"ˇ");"#);
2957
2958 cx.update_editor(|editor, window, cx| {
2959 editor.delete_to_next_word_end(
2960 &DeleteToNextWordEnd {
2961 ignore_newlines: true,
2962 ignore_brackets: false,
2963 },
2964 window,
2965 cx,
2966 );
2967 });
2968 cx.assert_editor_state(r#"ˇ"#);
2969
2970 cx.update_editor(|editor, window, cx| {
2971 editor.delete_to_next_word_end(
2972 &DeleteToNextWordEnd {
2973 ignore_newlines: true,
2974 ignore_brackets: false,
2975 },
2976 window,
2977 cx,
2978 );
2979 });
2980 cx.assert_editor_state(r#"ˇ"#);
2981
2982 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2983 cx.update_editor(|editor, window, cx| {
2984 editor.delete_to_previous_word_start(
2985 &DeleteToPreviousWordStart {
2986 ignore_newlines: true,
2987 ignore_brackets: true,
2988 },
2989 window,
2990 cx,
2991 );
2992 });
2993 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2994}
2995
2996#[gpui::test]
2997fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2998 init_test(cx, |_| {});
2999
3000 let editor = cx.add_window(|window, cx| {
3001 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3002 build_editor(buffer, window, cx)
3003 });
3004 let del_to_prev_word_start = DeleteToPreviousWordStart {
3005 ignore_newlines: false,
3006 ignore_brackets: false,
3007 };
3008 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3009 ignore_newlines: true,
3010 ignore_brackets: false,
3011 };
3012
3013 _ = editor.update(cx, |editor, window, cx| {
3014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3015 s.select_display_ranges([
3016 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3017 ])
3018 });
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\n");
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\nthree");
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\n");
3025 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3026 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
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(), "one\n");
3029 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3030 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3031 });
3032}
3033
3034#[gpui::test]
3035fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3036 init_test(cx, |_| {});
3037
3038 let editor = cx.add_window(|window, cx| {
3039 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3040 build_editor(buffer, window, cx)
3041 });
3042 let del_to_next_word_end = DeleteToNextWordEnd {
3043 ignore_newlines: false,
3044 ignore_brackets: false,
3045 };
3046 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3047 ignore_newlines: true,
3048 ignore_brackets: false,
3049 };
3050
3051 _ = editor.update(cx, |editor, window, cx| {
3052 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3053 s.select_display_ranges([
3054 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3055 ])
3056 });
3057 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3058 assert_eq!(
3059 editor.buffer.read(cx).read(cx).text(),
3060 "one\n two\nthree\n four"
3061 );
3062 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3063 assert_eq!(
3064 editor.buffer.read(cx).read(cx).text(),
3065 "\n two\nthree\n four"
3066 );
3067 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3068 assert_eq!(
3069 editor.buffer.read(cx).read(cx).text(),
3070 "two\nthree\n four"
3071 );
3072 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3073 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\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(), "\n 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(), "four");
3078 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3079 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3080 });
3081}
3082
3083#[gpui::test]
3084fn test_newline(cx: &mut TestAppContext) {
3085 init_test(cx, |_| {});
3086
3087 let editor = cx.add_window(|window, cx| {
3088 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3089 build_editor(buffer, window, cx)
3090 });
3091
3092 _ = editor.update(cx, |editor, window, cx| {
3093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3094 s.select_display_ranges([
3095 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3096 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3097 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3098 ])
3099 });
3100
3101 editor.newline(&Newline, window, cx);
3102 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3103 });
3104}
3105
3106#[gpui::test]
3107async fn test_newline_yaml(cx: &mut TestAppContext) {
3108 init_test(cx, |_| {});
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3112 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3113
3114 // Object (between 2 fields)
3115 cx.set_state(indoc! {"
3116 test:ˇ
3117 hello: bye"});
3118 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3119 cx.assert_editor_state(indoc! {"
3120 test:
3121 ˇ
3122 hello: bye"});
3123
3124 // Object (first and single line)
3125 cx.set_state(indoc! {"
3126 test:ˇ"});
3127 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 test:
3130 ˇ"});
3131
3132 // Array with objects (after first element)
3133 cx.set_state(indoc! {"
3134 test:
3135 - foo: barˇ"});
3136 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3137 cx.assert_editor_state(indoc! {"
3138 test:
3139 - foo: bar
3140 ˇ"});
3141
3142 // Array with objects and comment
3143 cx.set_state(indoc! {"
3144 test:
3145 - foo: bar
3146 - bar: # testˇ"});
3147 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3148 cx.assert_editor_state(indoc! {"
3149 test:
3150 - foo: bar
3151 - bar: # test
3152 ˇ"});
3153
3154 // Array with objects (after second element)
3155 cx.set_state(indoc! {"
3156 test:
3157 - foo: bar
3158 - bar: fooˇ"});
3159 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3160 cx.assert_editor_state(indoc! {"
3161 test:
3162 - foo: bar
3163 - bar: foo
3164 ˇ"});
3165
3166 // Array with strings (after first element)
3167 cx.set_state(indoc! {"
3168 test:
3169 - fooˇ"});
3170 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3171 cx.assert_editor_state(indoc! {"
3172 test:
3173 - foo
3174 ˇ"});
3175}
3176
3177#[gpui::test]
3178fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3179 init_test(cx, |_| {});
3180
3181 let editor = cx.add_window(|window, cx| {
3182 let buffer = MultiBuffer::build_simple(
3183 "
3184 a
3185 b(
3186 X
3187 )
3188 c(
3189 X
3190 )
3191 "
3192 .unindent()
3193 .as_str(),
3194 cx,
3195 );
3196 let mut editor = build_editor(buffer, window, cx);
3197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3198 s.select_ranges([
3199 Point::new(2, 4)..Point::new(2, 5),
3200 Point::new(5, 4)..Point::new(5, 5),
3201 ])
3202 });
3203 editor
3204 });
3205
3206 _ = editor.update(cx, |editor, window, cx| {
3207 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3208 editor.buffer.update(cx, |buffer, cx| {
3209 buffer.edit(
3210 [
3211 (Point::new(1, 2)..Point::new(3, 0), ""),
3212 (Point::new(4, 2)..Point::new(6, 0), ""),
3213 ],
3214 None,
3215 cx,
3216 );
3217 assert_eq!(
3218 buffer.read(cx).text(),
3219 "
3220 a
3221 b()
3222 c()
3223 "
3224 .unindent()
3225 );
3226 });
3227 assert_eq!(
3228 editor.selections.ranges(&editor.display_snapshot(cx)),
3229 &[
3230 Point::new(1, 2)..Point::new(1, 2),
3231 Point::new(2, 2)..Point::new(2, 2),
3232 ],
3233 );
3234
3235 editor.newline(&Newline, window, cx);
3236 assert_eq!(
3237 editor.text(cx),
3238 "
3239 a
3240 b(
3241 )
3242 c(
3243 )
3244 "
3245 .unindent()
3246 );
3247
3248 // The selections are moved after the inserted newlines
3249 assert_eq!(
3250 editor.selections.ranges(&editor.display_snapshot(cx)),
3251 &[
3252 Point::new(2, 0)..Point::new(2, 0),
3253 Point::new(4, 0)..Point::new(4, 0),
3254 ],
3255 );
3256 });
3257}
3258
3259#[gpui::test]
3260async fn test_newline_above(cx: &mut TestAppContext) {
3261 init_test(cx, |settings| {
3262 settings.defaults.tab_size = NonZeroU32::new(4)
3263 });
3264
3265 let language = Arc::new(
3266 Language::new(
3267 LanguageConfig::default(),
3268 Some(tree_sitter_rust::LANGUAGE.into()),
3269 )
3270 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3271 .unwrap(),
3272 );
3273
3274 let mut cx = EditorTestContext::new(cx).await;
3275 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3276 cx.set_state(indoc! {"
3277 const a: ˇA = (
3278 (ˇ
3279 «const_functionˇ»(ˇ),
3280 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3281 )ˇ
3282 ˇ);ˇ
3283 "});
3284
3285 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 ˇ
3288 const a: A = (
3289 ˇ
3290 (
3291 ˇ
3292 ˇ
3293 const_function(),
3294 ˇ
3295 ˇ
3296 ˇ
3297 ˇ
3298 something_else,
3299 ˇ
3300 )
3301 ˇ
3302 ˇ
3303 );
3304 "});
3305}
3306
3307#[gpui::test]
3308async fn test_newline_below(cx: &mut TestAppContext) {
3309 init_test(cx, |settings| {
3310 settings.defaults.tab_size = NonZeroU32::new(4)
3311 });
3312
3313 let language = Arc::new(
3314 Language::new(
3315 LanguageConfig::default(),
3316 Some(tree_sitter_rust::LANGUAGE.into()),
3317 )
3318 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3319 .unwrap(),
3320 );
3321
3322 let mut cx = EditorTestContext::new(cx).await;
3323 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3324 cx.set_state(indoc! {"
3325 const a: ˇA = (
3326 (ˇ
3327 «const_functionˇ»(ˇ),
3328 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3329 )ˇ
3330 ˇ);ˇ
3331 "});
3332
3333 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3334 cx.assert_editor_state(indoc! {"
3335 const a: A = (
3336 ˇ
3337 (
3338 ˇ
3339 const_function(),
3340 ˇ
3341 ˇ
3342 something_else,
3343 ˇ
3344 ˇ
3345 ˇ
3346 ˇ
3347 )
3348 ˇ
3349 );
3350 ˇ
3351 ˇ
3352 "});
3353}
3354
3355#[gpui::test]
3356async fn test_newline_comments(cx: &mut TestAppContext) {
3357 init_test(cx, |settings| {
3358 settings.defaults.tab_size = NonZeroU32::new(4)
3359 });
3360
3361 let language = Arc::new(Language::new(
3362 LanguageConfig {
3363 line_comments: vec!["// ".into()],
3364 ..LanguageConfig::default()
3365 },
3366 None,
3367 ));
3368 {
3369 let mut cx = EditorTestContext::new(cx).await;
3370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3371 cx.set_state(indoc! {"
3372 // Fooˇ
3373 "});
3374
3375 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3376 cx.assert_editor_state(indoc! {"
3377 // Foo
3378 // ˇ
3379 "});
3380 // Ensure that we add comment prefix when existing line contains space
3381 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3382 cx.assert_editor_state(
3383 indoc! {"
3384 // Foo
3385 //s
3386 // ˇ
3387 "}
3388 .replace("s", " ") // s is used as space placeholder to prevent format on save
3389 .as_str(),
3390 );
3391 // Ensure that we add comment prefix when existing line does not contain space
3392 cx.set_state(indoc! {"
3393 // Foo
3394 //ˇ
3395 "});
3396 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3397 cx.assert_editor_state(indoc! {"
3398 // Foo
3399 //
3400 // ˇ
3401 "});
3402 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3403 cx.set_state(indoc! {"
3404 ˇ// Foo
3405 "});
3406 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3407 cx.assert_editor_state(indoc! {"
3408
3409 ˇ// Foo
3410 "});
3411 }
3412 // Ensure that comment continuations can be disabled.
3413 update_test_language_settings(cx, |settings| {
3414 settings.defaults.extend_comment_on_newline = Some(false);
3415 });
3416 let mut cx = EditorTestContext::new(cx).await;
3417 cx.set_state(indoc! {"
3418 // Fooˇ
3419 "});
3420 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3421 cx.assert_editor_state(indoc! {"
3422 // Foo
3423 ˇ
3424 "});
3425}
3426
3427#[gpui::test]
3428async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3429 init_test(cx, |settings| {
3430 settings.defaults.tab_size = NonZeroU32::new(4)
3431 });
3432
3433 let language = Arc::new(Language::new(
3434 LanguageConfig {
3435 line_comments: vec!["// ".into(), "/// ".into()],
3436 ..LanguageConfig::default()
3437 },
3438 None,
3439 ));
3440 {
3441 let mut cx = EditorTestContext::new(cx).await;
3442 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3443 cx.set_state(indoc! {"
3444 //ˇ
3445 "});
3446 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3447 cx.assert_editor_state(indoc! {"
3448 //
3449 // ˇ
3450 "});
3451
3452 cx.set_state(indoc! {"
3453 ///ˇ
3454 "});
3455 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 ///
3458 /// ˇ
3459 "});
3460 }
3461}
3462
3463#[gpui::test]
3464async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3465 init_test(cx, |settings| {
3466 settings.defaults.tab_size = NonZeroU32::new(4)
3467 });
3468
3469 let language = Arc::new(
3470 Language::new(
3471 LanguageConfig {
3472 documentation_comment: Some(language::BlockCommentConfig {
3473 start: "/**".into(),
3474 end: "*/".into(),
3475 prefix: "* ".into(),
3476 tab_size: 1,
3477 }),
3478
3479 ..LanguageConfig::default()
3480 },
3481 Some(tree_sitter_rust::LANGUAGE.into()),
3482 )
3483 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3484 .unwrap(),
3485 );
3486
3487 {
3488 let mut cx = EditorTestContext::new(cx).await;
3489 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3490 cx.set_state(indoc! {"
3491 /**ˇ
3492 "});
3493
3494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 /**
3497 * ˇ
3498 "});
3499 // Ensure that if cursor is before the comment start,
3500 // we do not actually insert a comment prefix.
3501 cx.set_state(indoc! {"
3502 ˇ/**
3503 "});
3504 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3505 cx.assert_editor_state(indoc! {"
3506
3507 ˇ/**
3508 "});
3509 // Ensure that if cursor is between it doesn't add comment prefix.
3510 cx.set_state(indoc! {"
3511 /*ˇ*
3512 "});
3513 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3514 cx.assert_editor_state(indoc! {"
3515 /*
3516 ˇ*
3517 "});
3518 // Ensure that if suffix exists on same line after cursor it adds new line.
3519 cx.set_state(indoc! {"
3520 /**ˇ*/
3521 "});
3522 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 /**
3525 * ˇ
3526 */
3527 "});
3528 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3529 cx.set_state(indoc! {"
3530 /**ˇ */
3531 "});
3532 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3533 cx.assert_editor_state(indoc! {"
3534 /**
3535 * ˇ
3536 */
3537 "});
3538 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3539 cx.set_state(indoc! {"
3540 /** ˇ*/
3541 "});
3542 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3543 cx.assert_editor_state(
3544 indoc! {"
3545 /**s
3546 * ˇ
3547 */
3548 "}
3549 .replace("s", " ") // s is used as space placeholder to prevent format on save
3550 .as_str(),
3551 );
3552 // Ensure that delimiter space is preserved when newline on already
3553 // spaced delimiter.
3554 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3555 cx.assert_editor_state(
3556 indoc! {"
3557 /**s
3558 *s
3559 * ˇ
3560 */
3561 "}
3562 .replace("s", " ") // s is used as space placeholder to prevent format on save
3563 .as_str(),
3564 );
3565 // Ensure that delimiter space is preserved when space is not
3566 // on existing delimiter.
3567 cx.set_state(indoc! {"
3568 /**
3569 *ˇ
3570 */
3571 "});
3572 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3573 cx.assert_editor_state(indoc! {"
3574 /**
3575 *
3576 * ˇ
3577 */
3578 "});
3579 // Ensure that if suffix exists on same line after cursor it
3580 // doesn't add extra new line if prefix is not on same line.
3581 cx.set_state(indoc! {"
3582 /**
3583 ˇ*/
3584 "});
3585 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3586 cx.assert_editor_state(indoc! {"
3587 /**
3588
3589 ˇ*/
3590 "});
3591 // Ensure that it detects suffix after existing prefix.
3592 cx.set_state(indoc! {"
3593 /**ˇ/
3594 "});
3595 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3596 cx.assert_editor_state(indoc! {"
3597 /**
3598 ˇ/
3599 "});
3600 // Ensure that if suffix exists on same line before
3601 // cursor it does not add comment prefix.
3602 cx.set_state(indoc! {"
3603 /** */ˇ
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /** */
3608 ˇ
3609 "});
3610 // Ensure that if suffix exists on same line before
3611 // cursor it does not add comment prefix.
3612 cx.set_state(indoc! {"
3613 /**
3614 *
3615 */ˇ
3616 "});
3617 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3618 cx.assert_editor_state(indoc! {"
3619 /**
3620 *
3621 */
3622 ˇ
3623 "});
3624
3625 // Ensure that inline comment followed by code
3626 // doesn't add comment prefix on newline
3627 cx.set_state(indoc! {"
3628 /** */ textˇ
3629 "});
3630 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 /** */ text
3633 ˇ
3634 "});
3635
3636 // Ensure that text after comment end tag
3637 // doesn't add comment prefix on newline
3638 cx.set_state(indoc! {"
3639 /**
3640 *
3641 */ˇtext
3642 "});
3643 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3644 cx.assert_editor_state(indoc! {"
3645 /**
3646 *
3647 */
3648 ˇtext
3649 "});
3650
3651 // Ensure if not comment block it doesn't
3652 // add comment prefix on newline
3653 cx.set_state(indoc! {"
3654 * textˇ
3655 "});
3656 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 * text
3659 ˇ
3660 "});
3661 }
3662 // Ensure that comment continuations can be disabled.
3663 update_test_language_settings(cx, |settings| {
3664 settings.defaults.extend_comment_on_newline = Some(false);
3665 });
3666 let mut cx = EditorTestContext::new(cx).await;
3667 cx.set_state(indoc! {"
3668 /**ˇ
3669 "});
3670 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3671 cx.assert_editor_state(indoc! {"
3672 /**
3673 ˇ
3674 "});
3675}
3676
3677#[gpui::test]
3678async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3679 init_test(cx, |settings| {
3680 settings.defaults.tab_size = NonZeroU32::new(4)
3681 });
3682
3683 let lua_language = Arc::new(Language::new(
3684 LanguageConfig {
3685 line_comments: vec!["--".into()],
3686 block_comment: Some(language::BlockCommentConfig {
3687 start: "--[[".into(),
3688 prefix: "".into(),
3689 end: "]]".into(),
3690 tab_size: 0,
3691 }),
3692 ..LanguageConfig::default()
3693 },
3694 None,
3695 ));
3696
3697 let mut cx = EditorTestContext::new(cx).await;
3698 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3699
3700 // Line with line comment should extend
3701 cx.set_state(indoc! {"
3702 --ˇ
3703 "});
3704 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3705 cx.assert_editor_state(indoc! {"
3706 --
3707 --ˇ
3708 "});
3709
3710 // Line with block comment that matches line comment should not extend
3711 cx.set_state(indoc! {"
3712 --[[ˇ
3713 "});
3714 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 --[[
3717 ˇ
3718 "});
3719}
3720
3721#[gpui::test]
3722fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3723 init_test(cx, |_| {});
3724
3725 let editor = cx.add_window(|window, cx| {
3726 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3727 let mut editor = build_editor(buffer, window, cx);
3728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3729 s.select_ranges([
3730 MultiBufferOffset(3)..MultiBufferOffset(4),
3731 MultiBufferOffset(11)..MultiBufferOffset(12),
3732 MultiBufferOffset(19)..MultiBufferOffset(20),
3733 ])
3734 });
3735 editor
3736 });
3737
3738 _ = editor.update(cx, |editor, window, cx| {
3739 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3740 editor.buffer.update(cx, |buffer, cx| {
3741 buffer.edit(
3742 [
3743 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3744 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3745 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3746 ],
3747 None,
3748 cx,
3749 );
3750 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3751 });
3752 assert_eq!(
3753 editor.selections.ranges(&editor.display_snapshot(cx)),
3754 &[
3755 MultiBufferOffset(2)..MultiBufferOffset(2),
3756 MultiBufferOffset(7)..MultiBufferOffset(7),
3757 MultiBufferOffset(12)..MultiBufferOffset(12)
3758 ],
3759 );
3760
3761 editor.insert("Z", window, cx);
3762 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3763
3764 // The selections are moved after the inserted characters
3765 assert_eq!(
3766 editor.selections.ranges(&editor.display_snapshot(cx)),
3767 &[
3768 MultiBufferOffset(3)..MultiBufferOffset(3),
3769 MultiBufferOffset(9)..MultiBufferOffset(9),
3770 MultiBufferOffset(15)..MultiBufferOffset(15)
3771 ],
3772 );
3773 });
3774}
3775
3776#[gpui::test]
3777async fn test_tab(cx: &mut TestAppContext) {
3778 init_test(cx, |settings| {
3779 settings.defaults.tab_size = NonZeroU32::new(3)
3780 });
3781
3782 let mut cx = EditorTestContext::new(cx).await;
3783 cx.set_state(indoc! {"
3784 ˇabˇc
3785 ˇ🏀ˇ🏀ˇefg
3786 dˇ
3787 "});
3788 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3789 cx.assert_editor_state(indoc! {"
3790 ˇab ˇc
3791 ˇ🏀 ˇ🏀 ˇefg
3792 d ˇ
3793 "});
3794
3795 cx.set_state(indoc! {"
3796 a
3797 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3798 "});
3799 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3800 cx.assert_editor_state(indoc! {"
3801 a
3802 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3803 "});
3804}
3805
3806#[gpui::test]
3807async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3808 init_test(cx, |_| {});
3809
3810 let mut cx = EditorTestContext::new(cx).await;
3811 let language = Arc::new(
3812 Language::new(
3813 LanguageConfig::default(),
3814 Some(tree_sitter_rust::LANGUAGE.into()),
3815 )
3816 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3817 .unwrap(),
3818 );
3819 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3820
3821 // test when all cursors are not at suggested indent
3822 // then simply move to their suggested indent location
3823 cx.set_state(indoc! {"
3824 const a: B = (
3825 c(
3826 ˇ
3827 ˇ )
3828 );
3829 "});
3830 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3831 cx.assert_editor_state(indoc! {"
3832 const a: B = (
3833 c(
3834 ˇ
3835 ˇ)
3836 );
3837 "});
3838
3839 // test cursor already at suggested indent not moving when
3840 // other cursors are yet to reach their suggested indents
3841 cx.set_state(indoc! {"
3842 ˇ
3843 const a: B = (
3844 c(
3845 d(
3846 ˇ
3847 )
3848 ˇ
3849 ˇ )
3850 );
3851 "});
3852 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3853 cx.assert_editor_state(indoc! {"
3854 ˇ
3855 const a: B = (
3856 c(
3857 d(
3858 ˇ
3859 )
3860 ˇ
3861 ˇ)
3862 );
3863 "});
3864 // test when all cursors are at suggested indent then tab is inserted
3865 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3866 cx.assert_editor_state(indoc! {"
3867 ˇ
3868 const a: B = (
3869 c(
3870 d(
3871 ˇ
3872 )
3873 ˇ
3874 ˇ)
3875 );
3876 "});
3877
3878 // test when current indent is less than suggested indent,
3879 // we adjust line to match suggested indent and move cursor to it
3880 //
3881 // when no other cursor is at word boundary, all of them should move
3882 cx.set_state(indoc! {"
3883 const a: B = (
3884 c(
3885 d(
3886 ˇ
3887 ˇ )
3888 ˇ )
3889 );
3890 "});
3891 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3892 cx.assert_editor_state(indoc! {"
3893 const a: B = (
3894 c(
3895 d(
3896 ˇ
3897 ˇ)
3898 ˇ)
3899 );
3900 "});
3901
3902 // test when current indent is less than suggested indent,
3903 // we adjust line to match suggested indent and move cursor to it
3904 //
3905 // when some other cursor is at word boundary, it should not move
3906 cx.set_state(indoc! {"
3907 const a: B = (
3908 c(
3909 d(
3910 ˇ
3911 ˇ )
3912 ˇ)
3913 );
3914 "});
3915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3916 cx.assert_editor_state(indoc! {"
3917 const a: B = (
3918 c(
3919 d(
3920 ˇ
3921 ˇ)
3922 ˇ)
3923 );
3924 "});
3925
3926 // test when current indent is more than suggested indent,
3927 // we just move cursor to current indent instead of suggested indent
3928 //
3929 // when no other cursor is at word boundary, all of them should move
3930 cx.set_state(indoc! {"
3931 const a: B = (
3932 c(
3933 d(
3934 ˇ
3935 ˇ )
3936 ˇ )
3937 );
3938 "});
3939 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3940 cx.assert_editor_state(indoc! {"
3941 const a: B = (
3942 c(
3943 d(
3944 ˇ
3945 ˇ)
3946 ˇ)
3947 );
3948 "});
3949 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3950 cx.assert_editor_state(indoc! {"
3951 const a: B = (
3952 c(
3953 d(
3954 ˇ
3955 ˇ)
3956 ˇ)
3957 );
3958 "});
3959
3960 // test when current indent is more than suggested indent,
3961 // we just move cursor to current indent instead of suggested indent
3962 //
3963 // when some other cursor is at word boundary, it doesn't move
3964 cx.set_state(indoc! {"
3965 const a: B = (
3966 c(
3967 d(
3968 ˇ
3969 ˇ )
3970 ˇ)
3971 );
3972 "});
3973 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3974 cx.assert_editor_state(indoc! {"
3975 const a: B = (
3976 c(
3977 d(
3978 ˇ
3979 ˇ)
3980 ˇ)
3981 );
3982 "});
3983
3984 // handle auto-indent when there are multiple cursors on the same line
3985 cx.set_state(indoc! {"
3986 const a: B = (
3987 c(
3988 ˇ ˇ
3989 ˇ )
3990 );
3991 "});
3992 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 const a: B = (
3995 c(
3996 ˇ
3997 ˇ)
3998 );
3999 "});
4000}
4001
4002#[gpui::test]
4003async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4004 init_test(cx, |settings| {
4005 settings.defaults.tab_size = NonZeroU32::new(3)
4006 });
4007
4008 let mut cx = EditorTestContext::new(cx).await;
4009 cx.set_state(indoc! {"
4010 ˇ
4011 \t ˇ
4012 \t ˇ
4013 \t ˇ
4014 \t \t\t \t \t\t \t\t \t \t ˇ
4015 "});
4016
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 ˇ
4020 \t ˇ
4021 \t ˇ
4022 \t ˇ
4023 \t \t\t \t \t\t \t\t \t \t ˇ
4024 "});
4025}
4026
4027#[gpui::test]
4028async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4029 init_test(cx, |settings| {
4030 settings.defaults.tab_size = NonZeroU32::new(4)
4031 });
4032
4033 let language = Arc::new(
4034 Language::new(
4035 LanguageConfig::default(),
4036 Some(tree_sitter_rust::LANGUAGE.into()),
4037 )
4038 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4039 .unwrap(),
4040 );
4041
4042 let mut cx = EditorTestContext::new(cx).await;
4043 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4044 cx.set_state(indoc! {"
4045 fn a() {
4046 if b {
4047 \t ˇc
4048 }
4049 }
4050 "});
4051
4052 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4053 cx.assert_editor_state(indoc! {"
4054 fn a() {
4055 if b {
4056 ˇc
4057 }
4058 }
4059 "});
4060}
4061
4062#[gpui::test]
4063async fn test_indent_outdent(cx: &mut TestAppContext) {
4064 init_test(cx, |settings| {
4065 settings.defaults.tab_size = NonZeroU32::new(4);
4066 });
4067
4068 let mut cx = EditorTestContext::new(cx).await;
4069
4070 cx.set_state(indoc! {"
4071 «oneˇ» «twoˇ»
4072 three
4073 four
4074 "});
4075 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4076 cx.assert_editor_state(indoc! {"
4077 «oneˇ» «twoˇ»
4078 three
4079 four
4080 "});
4081
4082 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4083 cx.assert_editor_state(indoc! {"
4084 «oneˇ» «twoˇ»
4085 three
4086 four
4087 "});
4088
4089 // select across line ending
4090 cx.set_state(indoc! {"
4091 one two
4092 t«hree
4093 ˇ» four
4094 "});
4095 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4096 cx.assert_editor_state(indoc! {"
4097 one two
4098 t«hree
4099 ˇ» four
4100 "});
4101
4102 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4103 cx.assert_editor_state(indoc! {"
4104 one two
4105 t«hree
4106 ˇ» four
4107 "});
4108
4109 // Ensure that indenting/outdenting works when the cursor is at column 0.
4110 cx.set_state(indoc! {"
4111 one two
4112 ˇthree
4113 four
4114 "});
4115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4116 cx.assert_editor_state(indoc! {"
4117 one two
4118 ˇthree
4119 four
4120 "});
4121
4122 cx.set_state(indoc! {"
4123 one two
4124 ˇ three
4125 four
4126 "});
4127 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4128 cx.assert_editor_state(indoc! {"
4129 one two
4130 ˇthree
4131 four
4132 "});
4133}
4134
4135#[gpui::test]
4136async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4137 // This is a regression test for issue #33761
4138 init_test(cx, |_| {});
4139
4140 let mut cx = EditorTestContext::new(cx).await;
4141 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4142 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4143
4144 cx.set_state(
4145 r#"ˇ# ingress:
4146ˇ# api:
4147ˇ# enabled: false
4148ˇ# pathType: Prefix
4149ˇ# console:
4150ˇ# enabled: false
4151ˇ# pathType: Prefix
4152"#,
4153 );
4154
4155 // Press tab to indent all lines
4156 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4157
4158 cx.assert_editor_state(
4159 r#" ˇ# ingress:
4160 ˇ# api:
4161 ˇ# enabled: false
4162 ˇ# pathType: Prefix
4163 ˇ# console:
4164 ˇ# enabled: false
4165 ˇ# pathType: Prefix
4166"#,
4167 );
4168}
4169
4170#[gpui::test]
4171async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4172 // This is a test to make sure our fix for issue #33761 didn't break anything
4173 init_test(cx, |_| {});
4174
4175 let mut cx = EditorTestContext::new(cx).await;
4176 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4177 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4178
4179 cx.set_state(
4180 r#"ˇingress:
4181ˇ api:
4182ˇ enabled: false
4183ˇ pathType: Prefix
4184"#,
4185 );
4186
4187 // Press tab to indent all lines
4188 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4189
4190 cx.assert_editor_state(
4191 r#"ˇingress:
4192 ˇapi:
4193 ˇenabled: false
4194 ˇpathType: Prefix
4195"#,
4196 );
4197}
4198
4199#[gpui::test]
4200async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4201 init_test(cx, |settings| {
4202 settings.defaults.hard_tabs = Some(true);
4203 });
4204
4205 let mut cx = EditorTestContext::new(cx).await;
4206
4207 // select two ranges on one line
4208 cx.set_state(indoc! {"
4209 «oneˇ» «twoˇ»
4210 three
4211 four
4212 "});
4213 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4214 cx.assert_editor_state(indoc! {"
4215 \t«oneˇ» «twoˇ»
4216 three
4217 four
4218 "});
4219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4220 cx.assert_editor_state(indoc! {"
4221 \t\t«oneˇ» «twoˇ»
4222 three
4223 four
4224 "});
4225 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4226 cx.assert_editor_state(indoc! {"
4227 \t«oneˇ» «twoˇ»
4228 three
4229 four
4230 "});
4231 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4232 cx.assert_editor_state(indoc! {"
4233 «oneˇ» «twoˇ»
4234 three
4235 four
4236 "});
4237
4238 // select across a line ending
4239 cx.set_state(indoc! {"
4240 one two
4241 t«hree
4242 ˇ»four
4243 "});
4244 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4245 cx.assert_editor_state(indoc! {"
4246 one two
4247 \tt«hree
4248 ˇ»four
4249 "});
4250 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4251 cx.assert_editor_state(indoc! {"
4252 one two
4253 \t\tt«hree
4254 ˇ»four
4255 "});
4256 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4257 cx.assert_editor_state(indoc! {"
4258 one two
4259 \tt«hree
4260 ˇ»four
4261 "});
4262 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4263 cx.assert_editor_state(indoc! {"
4264 one two
4265 t«hree
4266 ˇ»four
4267 "});
4268
4269 // Ensure that indenting/outdenting works when the cursor is at column 0.
4270 cx.set_state(indoc! {"
4271 one two
4272 ˇthree
4273 four
4274 "});
4275 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4276 cx.assert_editor_state(indoc! {"
4277 one two
4278 ˇthree
4279 four
4280 "});
4281 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4282 cx.assert_editor_state(indoc! {"
4283 one two
4284 \tˇthree
4285 four
4286 "});
4287 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4288 cx.assert_editor_state(indoc! {"
4289 one two
4290 ˇthree
4291 four
4292 "});
4293}
4294
4295#[gpui::test]
4296fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4297 init_test(cx, |settings| {
4298 settings.languages.0.extend([
4299 (
4300 "TOML".into(),
4301 LanguageSettingsContent {
4302 tab_size: NonZeroU32::new(2),
4303 ..Default::default()
4304 },
4305 ),
4306 (
4307 "Rust".into(),
4308 LanguageSettingsContent {
4309 tab_size: NonZeroU32::new(4),
4310 ..Default::default()
4311 },
4312 ),
4313 ]);
4314 });
4315
4316 let toml_language = Arc::new(Language::new(
4317 LanguageConfig {
4318 name: "TOML".into(),
4319 ..Default::default()
4320 },
4321 None,
4322 ));
4323 let rust_language = Arc::new(Language::new(
4324 LanguageConfig {
4325 name: "Rust".into(),
4326 ..Default::default()
4327 },
4328 None,
4329 ));
4330
4331 let toml_buffer =
4332 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4333 let rust_buffer =
4334 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4335 let multibuffer = cx.new(|cx| {
4336 let mut multibuffer = MultiBuffer::new(ReadWrite);
4337 multibuffer.push_excerpts(
4338 toml_buffer.clone(),
4339 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4340 cx,
4341 );
4342 multibuffer.push_excerpts(
4343 rust_buffer.clone(),
4344 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4345 cx,
4346 );
4347 multibuffer
4348 });
4349
4350 cx.add_window(|window, cx| {
4351 let mut editor = build_editor(multibuffer, window, cx);
4352
4353 assert_eq!(
4354 editor.text(cx),
4355 indoc! {"
4356 a = 1
4357 b = 2
4358
4359 const c: usize = 3;
4360 "}
4361 );
4362
4363 select_ranges(
4364 &mut editor,
4365 indoc! {"
4366 «aˇ» = 1
4367 b = 2
4368
4369 «const c:ˇ» usize = 3;
4370 "},
4371 window,
4372 cx,
4373 );
4374
4375 editor.tab(&Tab, window, cx);
4376 assert_text_with_selections(
4377 &mut editor,
4378 indoc! {"
4379 «aˇ» = 1
4380 b = 2
4381
4382 «const c:ˇ» usize = 3;
4383 "},
4384 cx,
4385 );
4386 editor.backtab(&Backtab, window, cx);
4387 assert_text_with_selections(
4388 &mut editor,
4389 indoc! {"
4390 «aˇ» = 1
4391 b = 2
4392
4393 «const c:ˇ» usize = 3;
4394 "},
4395 cx,
4396 );
4397
4398 editor
4399 });
4400}
4401
4402#[gpui::test]
4403async fn test_backspace(cx: &mut TestAppContext) {
4404 init_test(cx, |_| {});
4405
4406 let mut cx = EditorTestContext::new(cx).await;
4407
4408 // Basic backspace
4409 cx.set_state(indoc! {"
4410 onˇe two three
4411 fou«rˇ» five six
4412 seven «ˇeight nine
4413 »ten
4414 "});
4415 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4416 cx.assert_editor_state(indoc! {"
4417 oˇe two three
4418 fouˇ five six
4419 seven ˇten
4420 "});
4421
4422 // Test backspace inside and around indents
4423 cx.set_state(indoc! {"
4424 zero
4425 ˇone
4426 ˇtwo
4427 ˇ ˇ ˇ three
4428 ˇ ˇ four
4429 "});
4430 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4431 cx.assert_editor_state(indoc! {"
4432 zero
4433 ˇone
4434 ˇtwo
4435 ˇ threeˇ four
4436 "});
4437}
4438
4439#[gpui::test]
4440async fn test_delete(cx: &mut TestAppContext) {
4441 init_test(cx, |_| {});
4442
4443 let mut cx = EditorTestContext::new(cx).await;
4444 cx.set_state(indoc! {"
4445 onˇe two three
4446 fou«rˇ» five six
4447 seven «ˇeight nine
4448 »ten
4449 "});
4450 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4451 cx.assert_editor_state(indoc! {"
4452 onˇ two three
4453 fouˇ five six
4454 seven ˇten
4455 "});
4456}
4457
4458#[gpui::test]
4459fn test_delete_line(cx: &mut TestAppContext) {
4460 init_test(cx, |_| {});
4461
4462 let editor = cx.add_window(|window, cx| {
4463 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4464 build_editor(buffer, window, cx)
4465 });
4466 _ = editor.update(cx, |editor, window, cx| {
4467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4468 s.select_display_ranges([
4469 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4470 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4471 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4472 ])
4473 });
4474 editor.delete_line(&DeleteLine, window, cx);
4475 assert_eq!(editor.display_text(cx), "ghi");
4476 assert_eq!(
4477 display_ranges(editor, cx),
4478 vec![
4479 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4480 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4481 ]
4482 );
4483 });
4484
4485 let editor = cx.add_window(|window, cx| {
4486 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4487 build_editor(buffer, window, cx)
4488 });
4489 _ = editor.update(cx, |editor, window, cx| {
4490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4491 s.select_display_ranges([
4492 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4493 ])
4494 });
4495 editor.delete_line(&DeleteLine, window, cx);
4496 assert_eq!(editor.display_text(cx), "ghi\n");
4497 assert_eq!(
4498 display_ranges(editor, cx),
4499 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4500 );
4501 });
4502
4503 let editor = cx.add_window(|window, cx| {
4504 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4505 build_editor(buffer, window, cx)
4506 });
4507 _ = editor.update(cx, |editor, window, cx| {
4508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4509 s.select_display_ranges([
4510 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4511 ])
4512 });
4513 editor.delete_line(&DeleteLine, window, cx);
4514 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4515 assert_eq!(
4516 display_ranges(editor, cx),
4517 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4518 );
4519 });
4520}
4521
4522#[gpui::test]
4523fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4524 init_test(cx, |_| {});
4525
4526 cx.add_window(|window, cx| {
4527 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4528 let mut editor = build_editor(buffer.clone(), window, cx);
4529 let buffer = buffer.read(cx).as_singleton().unwrap();
4530
4531 assert_eq!(
4532 editor
4533 .selections
4534 .ranges::<Point>(&editor.display_snapshot(cx)),
4535 &[Point::new(0, 0)..Point::new(0, 0)]
4536 );
4537
4538 // When on single line, replace newline at end by space
4539 editor.join_lines(&JoinLines, window, cx);
4540 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4541 assert_eq!(
4542 editor
4543 .selections
4544 .ranges::<Point>(&editor.display_snapshot(cx)),
4545 &[Point::new(0, 3)..Point::new(0, 3)]
4546 );
4547
4548 // When multiple lines are selected, remove newlines that are spanned by the selection
4549 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4550 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4551 });
4552 editor.join_lines(&JoinLines, window, cx);
4553 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4554 assert_eq!(
4555 editor
4556 .selections
4557 .ranges::<Point>(&editor.display_snapshot(cx)),
4558 &[Point::new(0, 11)..Point::new(0, 11)]
4559 );
4560
4561 // Undo should be transactional
4562 editor.undo(&Undo, window, cx);
4563 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4564 assert_eq!(
4565 editor
4566 .selections
4567 .ranges::<Point>(&editor.display_snapshot(cx)),
4568 &[Point::new(0, 5)..Point::new(2, 2)]
4569 );
4570
4571 // When joining an empty line don't insert a space
4572 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4573 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4574 });
4575 editor.join_lines(&JoinLines, window, cx);
4576 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4577 assert_eq!(
4578 editor
4579 .selections
4580 .ranges::<Point>(&editor.display_snapshot(cx)),
4581 [Point::new(2, 3)..Point::new(2, 3)]
4582 );
4583
4584 // We can remove trailing newlines
4585 editor.join_lines(&JoinLines, window, cx);
4586 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4587 assert_eq!(
4588 editor
4589 .selections
4590 .ranges::<Point>(&editor.display_snapshot(cx)),
4591 [Point::new(2, 3)..Point::new(2, 3)]
4592 );
4593
4594 // We don't blow up on the last line
4595 editor.join_lines(&JoinLines, window, cx);
4596 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4597 assert_eq!(
4598 editor
4599 .selections
4600 .ranges::<Point>(&editor.display_snapshot(cx)),
4601 [Point::new(2, 3)..Point::new(2, 3)]
4602 );
4603
4604 // reset to test indentation
4605 editor.buffer.update(cx, |buffer, cx| {
4606 buffer.edit(
4607 [
4608 (Point::new(1, 0)..Point::new(1, 2), " "),
4609 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4610 ],
4611 None,
4612 cx,
4613 )
4614 });
4615
4616 // We remove any leading spaces
4617 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4618 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4619 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4620 });
4621 editor.join_lines(&JoinLines, window, cx);
4622 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4623
4624 // We don't insert a space for a line containing only spaces
4625 editor.join_lines(&JoinLines, window, cx);
4626 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4627
4628 // We ignore any leading tabs
4629 editor.join_lines(&JoinLines, window, cx);
4630 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4631
4632 editor
4633 });
4634}
4635
4636#[gpui::test]
4637fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4638 init_test(cx, |_| {});
4639
4640 cx.add_window(|window, cx| {
4641 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4642 let mut editor = build_editor(buffer.clone(), window, cx);
4643 let buffer = buffer.read(cx).as_singleton().unwrap();
4644
4645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4646 s.select_ranges([
4647 Point::new(0, 2)..Point::new(1, 1),
4648 Point::new(1, 2)..Point::new(1, 2),
4649 Point::new(3, 1)..Point::new(3, 2),
4650 ])
4651 });
4652
4653 editor.join_lines(&JoinLines, window, cx);
4654 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4655
4656 assert_eq!(
4657 editor
4658 .selections
4659 .ranges::<Point>(&editor.display_snapshot(cx)),
4660 [
4661 Point::new(0, 7)..Point::new(0, 7),
4662 Point::new(1, 3)..Point::new(1, 3)
4663 ]
4664 );
4665 editor
4666 });
4667}
4668
4669#[gpui::test]
4670async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4671 init_test(cx, |_| {});
4672
4673 let mut cx = EditorTestContext::new(cx).await;
4674
4675 let diff_base = r#"
4676 Line 0
4677 Line 1
4678 Line 2
4679 Line 3
4680 "#
4681 .unindent();
4682
4683 cx.set_state(
4684 &r#"
4685 ˇLine 0
4686 Line 1
4687 Line 2
4688 Line 3
4689 "#
4690 .unindent(),
4691 );
4692
4693 cx.set_head_text(&diff_base);
4694 executor.run_until_parked();
4695
4696 // Join lines
4697 cx.update_editor(|editor, window, cx| {
4698 editor.join_lines(&JoinLines, window, cx);
4699 });
4700 executor.run_until_parked();
4701
4702 cx.assert_editor_state(
4703 &r#"
4704 Line 0ˇ Line 1
4705 Line 2
4706 Line 3
4707 "#
4708 .unindent(),
4709 );
4710 // Join again
4711 cx.update_editor(|editor, window, cx| {
4712 editor.join_lines(&JoinLines, window, cx);
4713 });
4714 executor.run_until_parked();
4715
4716 cx.assert_editor_state(
4717 &r#"
4718 Line 0 Line 1ˇ Line 2
4719 Line 3
4720 "#
4721 .unindent(),
4722 );
4723}
4724
4725#[gpui::test]
4726async fn test_custom_newlines_cause_no_false_positive_diffs(
4727 executor: BackgroundExecutor,
4728 cx: &mut TestAppContext,
4729) {
4730 init_test(cx, |_| {});
4731 let mut cx = EditorTestContext::new(cx).await;
4732 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4733 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4734 executor.run_until_parked();
4735
4736 cx.update_editor(|editor, window, cx| {
4737 let snapshot = editor.snapshot(window, cx);
4738 assert_eq!(
4739 snapshot
4740 .buffer_snapshot()
4741 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4742 .collect::<Vec<_>>(),
4743 Vec::new(),
4744 "Should not have any diffs for files with custom newlines"
4745 );
4746 });
4747}
4748
4749#[gpui::test]
4750async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4751 init_test(cx, |_| {});
4752
4753 let mut cx = EditorTestContext::new(cx).await;
4754
4755 // Test sort_lines_case_insensitive()
4756 cx.set_state(indoc! {"
4757 «z
4758 y
4759 x
4760 Z
4761 Y
4762 Xˇ»
4763 "});
4764 cx.update_editor(|e, window, cx| {
4765 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4766 });
4767 cx.assert_editor_state(indoc! {"
4768 «x
4769 X
4770 y
4771 Y
4772 z
4773 Zˇ»
4774 "});
4775
4776 // Test sort_lines_by_length()
4777 //
4778 // Demonstrates:
4779 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4780 // - sort is stable
4781 cx.set_state(indoc! {"
4782 «123
4783 æ
4784 12
4785 ∞
4786 1
4787 æˇ»
4788 "});
4789 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4790 cx.assert_editor_state(indoc! {"
4791 «æ
4792 ∞
4793 1
4794 æ
4795 12
4796 123ˇ»
4797 "});
4798
4799 // Test reverse_lines()
4800 cx.set_state(indoc! {"
4801 «5
4802 4
4803 3
4804 2
4805 1ˇ»
4806 "});
4807 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4808 cx.assert_editor_state(indoc! {"
4809 «1
4810 2
4811 3
4812 4
4813 5ˇ»
4814 "});
4815
4816 // Skip testing shuffle_line()
4817
4818 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4819 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4820
4821 // Don't manipulate when cursor is on single line, but expand the selection
4822 cx.set_state(indoc! {"
4823 ddˇdd
4824 ccc
4825 bb
4826 a
4827 "});
4828 cx.update_editor(|e, window, cx| {
4829 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4830 });
4831 cx.assert_editor_state(indoc! {"
4832 «ddddˇ»
4833 ccc
4834 bb
4835 a
4836 "});
4837
4838 // Basic manipulate case
4839 // Start selection moves to column 0
4840 // End of selection shrinks to fit shorter line
4841 cx.set_state(indoc! {"
4842 dd«d
4843 ccc
4844 bb
4845 aaaaaˇ»
4846 "});
4847 cx.update_editor(|e, window, cx| {
4848 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4849 });
4850 cx.assert_editor_state(indoc! {"
4851 «aaaaa
4852 bb
4853 ccc
4854 dddˇ»
4855 "});
4856
4857 // Manipulate case with newlines
4858 cx.set_state(indoc! {"
4859 dd«d
4860 ccc
4861
4862 bb
4863 aaaaa
4864
4865 ˇ»
4866 "});
4867 cx.update_editor(|e, window, cx| {
4868 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4869 });
4870 cx.assert_editor_state(indoc! {"
4871 «
4872
4873 aaaaa
4874 bb
4875 ccc
4876 dddˇ»
4877
4878 "});
4879
4880 // Adding new line
4881 cx.set_state(indoc! {"
4882 aa«a
4883 bbˇ»b
4884 "});
4885 cx.update_editor(|e, window, cx| {
4886 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4887 });
4888 cx.assert_editor_state(indoc! {"
4889 «aaa
4890 bbb
4891 added_lineˇ»
4892 "});
4893
4894 // Removing line
4895 cx.set_state(indoc! {"
4896 aa«a
4897 bbbˇ»
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.manipulate_immutable_lines(window, cx, |lines| {
4901 lines.pop();
4902 })
4903 });
4904 cx.assert_editor_state(indoc! {"
4905 «aaaˇ»
4906 "});
4907
4908 // Removing all lines
4909 cx.set_state(indoc! {"
4910 aa«a
4911 bbbˇ»
4912 "});
4913 cx.update_editor(|e, window, cx| {
4914 e.manipulate_immutable_lines(window, cx, |lines| {
4915 lines.drain(..);
4916 })
4917 });
4918 cx.assert_editor_state(indoc! {"
4919 ˇ
4920 "});
4921}
4922
4923#[gpui::test]
4924async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4925 init_test(cx, |_| {});
4926
4927 let mut cx = EditorTestContext::new(cx).await;
4928
4929 // Consider continuous selection as single selection
4930 cx.set_state(indoc! {"
4931 Aaa«aa
4932 cˇ»c«c
4933 bb
4934 aaaˇ»aa
4935 "});
4936 cx.update_editor(|e, window, cx| {
4937 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4938 });
4939 cx.assert_editor_state(indoc! {"
4940 «Aaaaa
4941 ccc
4942 bb
4943 aaaaaˇ»
4944 "});
4945
4946 cx.set_state(indoc! {"
4947 Aaa«aa
4948 cˇ»c«c
4949 bb
4950 aaaˇ»aa
4951 "});
4952 cx.update_editor(|e, window, cx| {
4953 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4954 });
4955 cx.assert_editor_state(indoc! {"
4956 «Aaaaa
4957 ccc
4958 bbˇ»
4959 "});
4960
4961 // Consider non continuous selection as distinct dedup operations
4962 cx.set_state(indoc! {"
4963 «aaaaa
4964 bb
4965 aaaaa
4966 aaaaaˇ»
4967
4968 aaa«aaˇ»
4969 "});
4970 cx.update_editor(|e, window, cx| {
4971 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4972 });
4973 cx.assert_editor_state(indoc! {"
4974 «aaaaa
4975 bbˇ»
4976
4977 «aaaaaˇ»
4978 "});
4979}
4980
4981#[gpui::test]
4982async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4983 init_test(cx, |_| {});
4984
4985 let mut cx = EditorTestContext::new(cx).await;
4986
4987 cx.set_state(indoc! {"
4988 «Aaa
4989 aAa
4990 Aaaˇ»
4991 "});
4992 cx.update_editor(|e, window, cx| {
4993 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4994 });
4995 cx.assert_editor_state(indoc! {"
4996 «Aaa
4997 aAaˇ»
4998 "});
4999
5000 cx.set_state(indoc! {"
5001 «Aaa
5002 aAa
5003 aaAˇ»
5004 "});
5005 cx.update_editor(|e, window, cx| {
5006 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5007 });
5008 cx.assert_editor_state(indoc! {"
5009 «Aaaˇ»
5010 "});
5011}
5012
5013#[gpui::test]
5014async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5015 init_test(cx, |_| {});
5016
5017 let mut cx = EditorTestContext::new(cx).await;
5018
5019 let js_language = Arc::new(Language::new(
5020 LanguageConfig {
5021 name: "JavaScript".into(),
5022 wrap_characters: Some(language::WrapCharactersConfig {
5023 start_prefix: "<".into(),
5024 start_suffix: ">".into(),
5025 end_prefix: "</".into(),
5026 end_suffix: ">".into(),
5027 }),
5028 ..LanguageConfig::default()
5029 },
5030 None,
5031 ));
5032
5033 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5034
5035 cx.set_state(indoc! {"
5036 «testˇ»
5037 "});
5038 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5039 cx.assert_editor_state(indoc! {"
5040 <«ˇ»>test</«ˇ»>
5041 "});
5042
5043 cx.set_state(indoc! {"
5044 «test
5045 testˇ»
5046 "});
5047 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5048 cx.assert_editor_state(indoc! {"
5049 <«ˇ»>test
5050 test</«ˇ»>
5051 "});
5052
5053 cx.set_state(indoc! {"
5054 teˇst
5055 "});
5056 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5057 cx.assert_editor_state(indoc! {"
5058 te<«ˇ»></«ˇ»>st
5059 "});
5060}
5061
5062#[gpui::test]
5063async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5064 init_test(cx, |_| {});
5065
5066 let mut cx = EditorTestContext::new(cx).await;
5067
5068 let js_language = Arc::new(Language::new(
5069 LanguageConfig {
5070 name: "JavaScript".into(),
5071 wrap_characters: Some(language::WrapCharactersConfig {
5072 start_prefix: "<".into(),
5073 start_suffix: ">".into(),
5074 end_prefix: "</".into(),
5075 end_suffix: ">".into(),
5076 }),
5077 ..LanguageConfig::default()
5078 },
5079 None,
5080 ));
5081
5082 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5083
5084 cx.set_state(indoc! {"
5085 «testˇ»
5086 «testˇ» «testˇ»
5087 «testˇ»
5088 "});
5089 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5090 cx.assert_editor_state(indoc! {"
5091 <«ˇ»>test</«ˇ»>
5092 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5093 <«ˇ»>test</«ˇ»>
5094 "});
5095
5096 cx.set_state(indoc! {"
5097 «test
5098 testˇ»
5099 «test
5100 testˇ»
5101 "});
5102 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5103 cx.assert_editor_state(indoc! {"
5104 <«ˇ»>test
5105 test</«ˇ»>
5106 <«ˇ»>test
5107 test</«ˇ»>
5108 "});
5109}
5110
5111#[gpui::test]
5112async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5113 init_test(cx, |_| {});
5114
5115 let mut cx = EditorTestContext::new(cx).await;
5116
5117 let plaintext_language = Arc::new(Language::new(
5118 LanguageConfig {
5119 name: "Plain Text".into(),
5120 ..LanguageConfig::default()
5121 },
5122 None,
5123 ));
5124
5125 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5126
5127 cx.set_state(indoc! {"
5128 «testˇ»
5129 "});
5130 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5131 cx.assert_editor_state(indoc! {"
5132 «testˇ»
5133 "});
5134}
5135
5136#[gpui::test]
5137async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5138 init_test(cx, |_| {});
5139
5140 let mut cx = EditorTestContext::new(cx).await;
5141
5142 // Manipulate with multiple selections on a single line
5143 cx.set_state(indoc! {"
5144 dd«dd
5145 cˇ»c«c
5146 bb
5147 aaaˇ»aa
5148 "});
5149 cx.update_editor(|e, window, cx| {
5150 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5151 });
5152 cx.assert_editor_state(indoc! {"
5153 «aaaaa
5154 bb
5155 ccc
5156 ddddˇ»
5157 "});
5158
5159 // Manipulate with multiple disjoin selections
5160 cx.set_state(indoc! {"
5161 5«
5162 4
5163 3
5164 2
5165 1ˇ»
5166
5167 dd«dd
5168 ccc
5169 bb
5170 aaaˇ»aa
5171 "});
5172 cx.update_editor(|e, window, cx| {
5173 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5174 });
5175 cx.assert_editor_state(indoc! {"
5176 «1
5177 2
5178 3
5179 4
5180 5ˇ»
5181
5182 «aaaaa
5183 bb
5184 ccc
5185 ddddˇ»
5186 "});
5187
5188 // Adding lines on each selection
5189 cx.set_state(indoc! {"
5190 2«
5191 1ˇ»
5192
5193 bb«bb
5194 aaaˇ»aa
5195 "});
5196 cx.update_editor(|e, window, cx| {
5197 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5198 });
5199 cx.assert_editor_state(indoc! {"
5200 «2
5201 1
5202 added lineˇ»
5203
5204 «bbbb
5205 aaaaa
5206 added lineˇ»
5207 "});
5208
5209 // Removing lines on each selection
5210 cx.set_state(indoc! {"
5211 2«
5212 1ˇ»
5213
5214 bb«bb
5215 aaaˇ»aa
5216 "});
5217 cx.update_editor(|e, window, cx| {
5218 e.manipulate_immutable_lines(window, cx, |lines| {
5219 lines.pop();
5220 })
5221 });
5222 cx.assert_editor_state(indoc! {"
5223 «2ˇ»
5224
5225 «bbbbˇ»
5226 "});
5227}
5228
5229#[gpui::test]
5230async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5231 init_test(cx, |settings| {
5232 settings.defaults.tab_size = NonZeroU32::new(3)
5233 });
5234
5235 let mut cx = EditorTestContext::new(cx).await;
5236
5237 // MULTI SELECTION
5238 // Ln.1 "«" tests empty lines
5239 // Ln.9 tests just leading whitespace
5240 cx.set_state(indoc! {"
5241 «
5242 abc // No indentationˇ»
5243 «\tabc // 1 tabˇ»
5244 \t\tabc « ˇ» // 2 tabs
5245 \t ab«c // Tab followed by space
5246 \tabc // Space followed by tab (3 spaces should be the result)
5247 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5248 abˇ»ˇc ˇ ˇ // Already space indented«
5249 \t
5250 \tabc\tdef // Only the leading tab is manipulatedˇ»
5251 "});
5252 cx.update_editor(|e, window, cx| {
5253 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5254 });
5255 cx.assert_editor_state(
5256 indoc! {"
5257 «
5258 abc // No indentation
5259 abc // 1 tab
5260 abc // 2 tabs
5261 abc // Tab followed by space
5262 abc // Space followed by tab (3 spaces should be the result)
5263 abc // Mixed indentation (tab conversion depends on the column)
5264 abc // Already space indented
5265 ·
5266 abc\tdef // Only the leading tab is manipulatedˇ»
5267 "}
5268 .replace("·", "")
5269 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5270 );
5271
5272 // Test on just a few lines, the others should remain unchanged
5273 // Only lines (3, 5, 10, 11) should change
5274 cx.set_state(
5275 indoc! {"
5276 ·
5277 abc // No indentation
5278 \tabcˇ // 1 tab
5279 \t\tabc // 2 tabs
5280 \t abcˇ // Tab followed by space
5281 \tabc // Space followed by tab (3 spaces should be the result)
5282 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5283 abc // Already space indented
5284 «\t
5285 \tabc\tdef // Only the leading tab is manipulatedˇ»
5286 "}
5287 .replace("·", "")
5288 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5289 );
5290 cx.update_editor(|e, window, cx| {
5291 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5292 });
5293 cx.assert_editor_state(
5294 indoc! {"
5295 ·
5296 abc // No indentation
5297 « abc // 1 tabˇ»
5298 \t\tabc // 2 tabs
5299 « abc // Tab followed by spaceˇ»
5300 \tabc // Space followed by tab (3 spaces should be the result)
5301 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5302 abc // Already space indented
5303 « ·
5304 abc\tdef // Only the leading tab is manipulatedˇ»
5305 "}
5306 .replace("·", "")
5307 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5308 );
5309
5310 // SINGLE SELECTION
5311 // Ln.1 "«" tests empty lines
5312 // Ln.9 tests just leading whitespace
5313 cx.set_state(indoc! {"
5314 «
5315 abc // No indentation
5316 \tabc // 1 tab
5317 \t\tabc // 2 tabs
5318 \t abc // Tab followed by space
5319 \tabc // Space followed by tab (3 spaces should be the result)
5320 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5321 abc // Already space indented
5322 \t
5323 \tabc\tdef // Only the leading tab is manipulatedˇ»
5324 "});
5325 cx.update_editor(|e, window, cx| {
5326 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5327 });
5328 cx.assert_editor_state(
5329 indoc! {"
5330 «
5331 abc // No indentation
5332 abc // 1 tab
5333 abc // 2 tabs
5334 abc // Tab followed by space
5335 abc // Space followed by tab (3 spaces should be the result)
5336 abc // Mixed indentation (tab conversion depends on the column)
5337 abc // Already space indented
5338 ·
5339 abc\tdef // Only the leading tab is manipulatedˇ»
5340 "}
5341 .replace("·", "")
5342 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5343 );
5344}
5345
5346#[gpui::test]
5347async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5348 init_test(cx, |settings| {
5349 settings.defaults.tab_size = NonZeroU32::new(3)
5350 });
5351
5352 let mut cx = EditorTestContext::new(cx).await;
5353
5354 // MULTI SELECTION
5355 // Ln.1 "«" tests empty lines
5356 // Ln.11 tests just leading whitespace
5357 cx.set_state(indoc! {"
5358 «
5359 abˇ»ˇc // No indentation
5360 abc ˇ ˇ // 1 space (< 3 so dont convert)
5361 abc « // 2 spaces (< 3 so dont convert)
5362 abc // 3 spaces (convert)
5363 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5364 «\tˇ»\t«\tˇ»abc // Already tab indented
5365 «\t abc // Tab followed by space
5366 \tabc // Space followed by tab (should be consumed due to tab)
5367 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5368 \tˇ» «\t
5369 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5370 "});
5371 cx.update_editor(|e, window, cx| {
5372 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5373 });
5374 cx.assert_editor_state(indoc! {"
5375 «
5376 abc // No indentation
5377 abc // 1 space (< 3 so dont convert)
5378 abc // 2 spaces (< 3 so dont convert)
5379 \tabc // 3 spaces (convert)
5380 \t abc // 5 spaces (1 tab + 2 spaces)
5381 \t\t\tabc // Already tab indented
5382 \t abc // Tab followed by space
5383 \tabc // Space followed by tab (should be consumed due to tab)
5384 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5385 \t\t\t
5386 \tabc \t // Only the leading spaces should be convertedˇ»
5387 "});
5388
5389 // Test on just a few lines, the other should remain unchanged
5390 // Only lines (4, 8, 11, 12) should change
5391 cx.set_state(
5392 indoc! {"
5393 ·
5394 abc // No indentation
5395 abc // 1 space (< 3 so dont convert)
5396 abc // 2 spaces (< 3 so dont convert)
5397 « abc // 3 spaces (convert)ˇ»
5398 abc // 5 spaces (1 tab + 2 spaces)
5399 \t\t\tabc // Already tab indented
5400 \t abc // Tab followed by space
5401 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5402 \t\t \tabc // Mixed indentation
5403 \t \t \t \tabc // Mixed indentation
5404 \t \tˇ
5405 « abc \t // Only the leading spaces should be convertedˇ»
5406 "}
5407 .replace("·", "")
5408 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5409 );
5410 cx.update_editor(|e, window, cx| {
5411 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5412 });
5413 cx.assert_editor_state(
5414 indoc! {"
5415 ·
5416 abc // No indentation
5417 abc // 1 space (< 3 so dont convert)
5418 abc // 2 spaces (< 3 so dont convert)
5419 «\tabc // 3 spaces (convert)ˇ»
5420 abc // 5 spaces (1 tab + 2 spaces)
5421 \t\t\tabc // Already tab indented
5422 \t abc // Tab followed by space
5423 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5424 \t\t \tabc // Mixed indentation
5425 \t \t \t \tabc // Mixed indentation
5426 «\t\t\t
5427 \tabc \t // Only the leading spaces should be convertedˇ»
5428 "}
5429 .replace("·", "")
5430 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5431 );
5432
5433 // SINGLE SELECTION
5434 // Ln.1 "«" tests empty lines
5435 // Ln.11 tests just leading whitespace
5436 cx.set_state(indoc! {"
5437 «
5438 abc // No indentation
5439 abc // 1 space (< 3 so dont convert)
5440 abc // 2 spaces (< 3 so dont convert)
5441 abc // 3 spaces (convert)
5442 abc // 5 spaces (1 tab + 2 spaces)
5443 \t\t\tabc // Already tab indented
5444 \t abc // Tab followed by space
5445 \tabc // Space followed by tab (should be consumed due to tab)
5446 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5447 \t \t
5448 abc \t // Only the leading spaces should be convertedˇ»
5449 "});
5450 cx.update_editor(|e, window, cx| {
5451 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5452 });
5453 cx.assert_editor_state(indoc! {"
5454 «
5455 abc // No indentation
5456 abc // 1 space (< 3 so dont convert)
5457 abc // 2 spaces (< 3 so dont convert)
5458 \tabc // 3 spaces (convert)
5459 \t abc // 5 spaces (1 tab + 2 spaces)
5460 \t\t\tabc // Already tab indented
5461 \t abc // Tab followed by space
5462 \tabc // Space followed by tab (should be consumed due to tab)
5463 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5464 \t\t\t
5465 \tabc \t // Only the leading spaces should be convertedˇ»
5466 "});
5467}
5468
5469#[gpui::test]
5470async fn test_toggle_case(cx: &mut TestAppContext) {
5471 init_test(cx, |_| {});
5472
5473 let mut cx = EditorTestContext::new(cx).await;
5474
5475 // If all lower case -> upper case
5476 cx.set_state(indoc! {"
5477 «hello worldˇ»
5478 "});
5479 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5480 cx.assert_editor_state(indoc! {"
5481 «HELLO WORLDˇ»
5482 "});
5483
5484 // If all upper case -> lower case
5485 cx.set_state(indoc! {"
5486 «HELLO WORLDˇ»
5487 "});
5488 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5489 cx.assert_editor_state(indoc! {"
5490 «hello worldˇ»
5491 "});
5492
5493 // If any upper case characters are identified -> lower case
5494 // This matches JetBrains IDEs
5495 cx.set_state(indoc! {"
5496 «hEllo worldˇ»
5497 "});
5498 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5499 cx.assert_editor_state(indoc! {"
5500 «hello worldˇ»
5501 "});
5502}
5503
5504#[gpui::test]
5505async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5506 init_test(cx, |_| {});
5507
5508 let mut cx = EditorTestContext::new(cx).await;
5509
5510 cx.set_state(indoc! {"
5511 «implement-windows-supportˇ»
5512 "});
5513 cx.update_editor(|e, window, cx| {
5514 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5515 });
5516 cx.assert_editor_state(indoc! {"
5517 «Implement windows supportˇ»
5518 "});
5519}
5520
5521#[gpui::test]
5522async fn test_manipulate_text(cx: &mut TestAppContext) {
5523 init_test(cx, |_| {});
5524
5525 let mut cx = EditorTestContext::new(cx).await;
5526
5527 // Test convert_to_upper_case()
5528 cx.set_state(indoc! {"
5529 «hello worldˇ»
5530 "});
5531 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5532 cx.assert_editor_state(indoc! {"
5533 «HELLO WORLDˇ»
5534 "});
5535
5536 // Test convert_to_lower_case()
5537 cx.set_state(indoc! {"
5538 «HELLO WORLDˇ»
5539 "});
5540 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5541 cx.assert_editor_state(indoc! {"
5542 «hello worldˇ»
5543 "});
5544
5545 // Test multiple line, single selection case
5546 cx.set_state(indoc! {"
5547 «The quick brown
5548 fox jumps over
5549 the lazy dogˇ»
5550 "});
5551 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5552 cx.assert_editor_state(indoc! {"
5553 «The Quick Brown
5554 Fox Jumps Over
5555 The Lazy Dogˇ»
5556 "});
5557
5558 // Test multiple line, single selection case
5559 cx.set_state(indoc! {"
5560 «The quick brown
5561 fox jumps over
5562 the lazy dogˇ»
5563 "});
5564 cx.update_editor(|e, window, cx| {
5565 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5566 });
5567 cx.assert_editor_state(indoc! {"
5568 «TheQuickBrown
5569 FoxJumpsOver
5570 TheLazyDogˇ»
5571 "});
5572
5573 // From here on out, test more complex cases of manipulate_text()
5574
5575 // Test no selection case - should affect words cursors are in
5576 // Cursor at beginning, middle, and end of word
5577 cx.set_state(indoc! {"
5578 ˇhello big beauˇtiful worldˇ
5579 "});
5580 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5581 cx.assert_editor_state(indoc! {"
5582 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5583 "});
5584
5585 // Test multiple selections on a single line and across multiple lines
5586 cx.set_state(indoc! {"
5587 «Theˇ» quick «brown
5588 foxˇ» jumps «overˇ»
5589 the «lazyˇ» dog
5590 "});
5591 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5592 cx.assert_editor_state(indoc! {"
5593 «THEˇ» quick «BROWN
5594 FOXˇ» jumps «OVERˇ»
5595 the «LAZYˇ» dog
5596 "});
5597
5598 // Test case where text length grows
5599 cx.set_state(indoc! {"
5600 «tschüߡ»
5601 "});
5602 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5603 cx.assert_editor_state(indoc! {"
5604 «TSCHÜSSˇ»
5605 "});
5606
5607 // Test to make sure we don't crash when text shrinks
5608 cx.set_state(indoc! {"
5609 aaa_bbbˇ
5610 "});
5611 cx.update_editor(|e, window, cx| {
5612 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5613 });
5614 cx.assert_editor_state(indoc! {"
5615 «aaaBbbˇ»
5616 "});
5617
5618 // Test to make sure we all aware of the fact that each word can grow and shrink
5619 // Final selections should be aware of this fact
5620 cx.set_state(indoc! {"
5621 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5622 "});
5623 cx.update_editor(|e, window, cx| {
5624 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5625 });
5626 cx.assert_editor_state(indoc! {"
5627 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5628 "});
5629
5630 cx.set_state(indoc! {"
5631 «hElLo, WoRld!ˇ»
5632 "});
5633 cx.update_editor(|e, window, cx| {
5634 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5635 });
5636 cx.assert_editor_state(indoc! {"
5637 «HeLlO, wOrLD!ˇ»
5638 "});
5639
5640 // Test selections with `line_mode() = true`.
5641 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5642 cx.set_state(indoc! {"
5643 «The quick brown
5644 fox jumps over
5645 tˇ»he lazy dog
5646 "});
5647 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5648 cx.assert_editor_state(indoc! {"
5649 «THE QUICK BROWN
5650 FOX JUMPS OVER
5651 THE LAZY DOGˇ»
5652 "});
5653}
5654
5655#[gpui::test]
5656fn test_duplicate_line(cx: &mut TestAppContext) {
5657 init_test(cx, |_| {});
5658
5659 let editor = cx.add_window(|window, cx| {
5660 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5661 build_editor(buffer, window, cx)
5662 });
5663 _ = editor.update(cx, |editor, window, cx| {
5664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5665 s.select_display_ranges([
5666 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5668 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5669 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5670 ])
5671 });
5672 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5673 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5674 assert_eq!(
5675 display_ranges(editor, cx),
5676 vec![
5677 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5678 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5679 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5680 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5681 ]
5682 );
5683 });
5684
5685 let editor = cx.add_window(|window, cx| {
5686 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5687 build_editor(buffer, window, cx)
5688 });
5689 _ = editor.update(cx, |editor, window, cx| {
5690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5691 s.select_display_ranges([
5692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5693 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5694 ])
5695 });
5696 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5697 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5698 assert_eq!(
5699 display_ranges(editor, cx),
5700 vec![
5701 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5702 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5703 ]
5704 );
5705 });
5706
5707 // With `duplicate_line_up` the selections move to the duplicated lines,
5708 // which are inserted above the original lines
5709 let editor = cx.add_window(|window, cx| {
5710 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5711 build_editor(buffer, window, cx)
5712 });
5713 _ = editor.update(cx, |editor, window, cx| {
5714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5715 s.select_display_ranges([
5716 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5717 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5718 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5719 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5720 ])
5721 });
5722 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5723 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5724 assert_eq!(
5725 display_ranges(editor, cx),
5726 vec![
5727 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5728 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5729 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5730 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5731 ]
5732 );
5733 });
5734
5735 let editor = cx.add_window(|window, cx| {
5736 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5737 build_editor(buffer, window, cx)
5738 });
5739 _ = editor.update(cx, |editor, window, cx| {
5740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5741 s.select_display_ranges([
5742 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5743 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5744 ])
5745 });
5746 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5747 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5748 assert_eq!(
5749 display_ranges(editor, cx),
5750 vec![
5751 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5752 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5753 ]
5754 );
5755 });
5756
5757 let editor = cx.add_window(|window, cx| {
5758 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5759 build_editor(buffer, window, cx)
5760 });
5761 _ = editor.update(cx, |editor, window, cx| {
5762 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5763 s.select_display_ranges([
5764 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5765 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5766 ])
5767 });
5768 editor.duplicate_selection(&DuplicateSelection, window, cx);
5769 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5770 assert_eq!(
5771 display_ranges(editor, cx),
5772 vec![
5773 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5774 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5775 ]
5776 );
5777 });
5778}
5779
5780#[gpui::test]
5781fn test_move_line_up_down(cx: &mut TestAppContext) {
5782 init_test(cx, |_| {});
5783
5784 let editor = cx.add_window(|window, cx| {
5785 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5786 build_editor(buffer, window, cx)
5787 });
5788 _ = editor.update(cx, |editor, window, cx| {
5789 editor.fold_creases(
5790 vec![
5791 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5792 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5793 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5794 ],
5795 true,
5796 window,
5797 cx,
5798 );
5799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5800 s.select_display_ranges([
5801 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5802 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5803 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5804 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5805 ])
5806 });
5807 assert_eq!(
5808 editor.display_text(cx),
5809 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5810 );
5811
5812 editor.move_line_up(&MoveLineUp, window, cx);
5813 assert_eq!(
5814 editor.display_text(cx),
5815 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5816 );
5817 assert_eq!(
5818 display_ranges(editor, cx),
5819 vec![
5820 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5821 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5822 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5823 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5824 ]
5825 );
5826 });
5827
5828 _ = editor.update(cx, |editor, window, cx| {
5829 editor.move_line_down(&MoveLineDown, window, cx);
5830 assert_eq!(
5831 editor.display_text(cx),
5832 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5833 );
5834 assert_eq!(
5835 display_ranges(editor, cx),
5836 vec![
5837 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5838 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5839 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5840 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5841 ]
5842 );
5843 });
5844
5845 _ = editor.update(cx, |editor, window, cx| {
5846 editor.move_line_down(&MoveLineDown, window, cx);
5847 assert_eq!(
5848 editor.display_text(cx),
5849 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5850 );
5851 assert_eq!(
5852 display_ranges(editor, cx),
5853 vec![
5854 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5855 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5856 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5857 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5858 ]
5859 );
5860 });
5861
5862 _ = editor.update(cx, |editor, window, cx| {
5863 editor.move_line_up(&MoveLineUp, window, cx);
5864 assert_eq!(
5865 editor.display_text(cx),
5866 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5867 );
5868 assert_eq!(
5869 display_ranges(editor, cx),
5870 vec![
5871 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5872 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5873 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5874 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5875 ]
5876 );
5877 });
5878}
5879
5880#[gpui::test]
5881fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5882 init_test(cx, |_| {});
5883 let editor = cx.add_window(|window, cx| {
5884 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5885 build_editor(buffer, window, cx)
5886 });
5887 _ = editor.update(cx, |editor, window, cx| {
5888 editor.fold_creases(
5889 vec![Crease::simple(
5890 Point::new(6, 4)..Point::new(7, 4),
5891 FoldPlaceholder::test(),
5892 )],
5893 true,
5894 window,
5895 cx,
5896 );
5897 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5898 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5899 });
5900 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5901 editor.move_line_up(&MoveLineUp, window, cx);
5902 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5903 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5904 });
5905}
5906
5907#[gpui::test]
5908fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5909 init_test(cx, |_| {});
5910
5911 let editor = cx.add_window(|window, cx| {
5912 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5913 build_editor(buffer, window, cx)
5914 });
5915 _ = editor.update(cx, |editor, window, cx| {
5916 let snapshot = editor.buffer.read(cx).snapshot(cx);
5917 editor.insert_blocks(
5918 [BlockProperties {
5919 style: BlockStyle::Fixed,
5920 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5921 height: Some(1),
5922 render: Arc::new(|_| div().into_any()),
5923 priority: 0,
5924 }],
5925 Some(Autoscroll::fit()),
5926 cx,
5927 );
5928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5929 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5930 });
5931 editor.move_line_down(&MoveLineDown, window, cx);
5932 });
5933}
5934
5935#[gpui::test]
5936async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5937 init_test(cx, |_| {});
5938
5939 let mut cx = EditorTestContext::new(cx).await;
5940 cx.set_state(
5941 &"
5942 ˇzero
5943 one
5944 two
5945 three
5946 four
5947 five
5948 "
5949 .unindent(),
5950 );
5951
5952 // Create a four-line block that replaces three lines of text.
5953 cx.update_editor(|editor, window, cx| {
5954 let snapshot = editor.snapshot(window, cx);
5955 let snapshot = &snapshot.buffer_snapshot();
5956 let placement = BlockPlacement::Replace(
5957 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5958 );
5959 editor.insert_blocks(
5960 [BlockProperties {
5961 placement,
5962 height: Some(4),
5963 style: BlockStyle::Sticky,
5964 render: Arc::new(|_| gpui::div().into_any_element()),
5965 priority: 0,
5966 }],
5967 None,
5968 cx,
5969 );
5970 });
5971
5972 // Move down so that the cursor touches the block.
5973 cx.update_editor(|editor, window, cx| {
5974 editor.move_down(&Default::default(), window, cx);
5975 });
5976 cx.assert_editor_state(
5977 &"
5978 zero
5979 «one
5980 two
5981 threeˇ»
5982 four
5983 five
5984 "
5985 .unindent(),
5986 );
5987
5988 // Move down past the block.
5989 cx.update_editor(|editor, window, cx| {
5990 editor.move_down(&Default::default(), window, cx);
5991 });
5992 cx.assert_editor_state(
5993 &"
5994 zero
5995 one
5996 two
5997 three
5998 ˇfour
5999 five
6000 "
6001 .unindent(),
6002 );
6003}
6004
6005#[gpui::test]
6006fn test_transpose(cx: &mut TestAppContext) {
6007 init_test(cx, |_| {});
6008
6009 _ = cx.add_window(|window, cx| {
6010 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6011 editor.set_style(EditorStyle::default(), window, cx);
6012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6013 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6014 });
6015 editor.transpose(&Default::default(), window, cx);
6016 assert_eq!(editor.text(cx), "bac");
6017 assert_eq!(
6018 editor.selections.ranges(&editor.display_snapshot(cx)),
6019 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6020 );
6021
6022 editor.transpose(&Default::default(), window, cx);
6023 assert_eq!(editor.text(cx), "bca");
6024 assert_eq!(
6025 editor.selections.ranges(&editor.display_snapshot(cx)),
6026 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6027 );
6028
6029 editor.transpose(&Default::default(), window, cx);
6030 assert_eq!(editor.text(cx), "bac");
6031 assert_eq!(
6032 editor.selections.ranges(&editor.display_snapshot(cx)),
6033 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6034 );
6035
6036 editor
6037 });
6038
6039 _ = cx.add_window(|window, cx| {
6040 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6041 editor.set_style(EditorStyle::default(), window, cx);
6042 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6043 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6044 });
6045 editor.transpose(&Default::default(), window, cx);
6046 assert_eq!(editor.text(cx), "acb\nde");
6047 assert_eq!(
6048 editor.selections.ranges(&editor.display_snapshot(cx)),
6049 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6050 );
6051
6052 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6053 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6054 });
6055 editor.transpose(&Default::default(), window, cx);
6056 assert_eq!(editor.text(cx), "acbd\ne");
6057 assert_eq!(
6058 editor.selections.ranges(&editor.display_snapshot(cx)),
6059 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6060 );
6061
6062 editor.transpose(&Default::default(), window, cx);
6063 assert_eq!(editor.text(cx), "acbde\n");
6064 assert_eq!(
6065 editor.selections.ranges(&editor.display_snapshot(cx)),
6066 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6067 );
6068
6069 editor.transpose(&Default::default(), window, cx);
6070 assert_eq!(editor.text(cx), "acbd\ne");
6071 assert_eq!(
6072 editor.selections.ranges(&editor.display_snapshot(cx)),
6073 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6074 );
6075
6076 editor
6077 });
6078
6079 _ = cx.add_window(|window, cx| {
6080 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6081 editor.set_style(EditorStyle::default(), window, cx);
6082 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6083 s.select_ranges([
6084 MultiBufferOffset(1)..MultiBufferOffset(1),
6085 MultiBufferOffset(2)..MultiBufferOffset(2),
6086 MultiBufferOffset(4)..MultiBufferOffset(4),
6087 ])
6088 });
6089 editor.transpose(&Default::default(), window, cx);
6090 assert_eq!(editor.text(cx), "bacd\ne");
6091 assert_eq!(
6092 editor.selections.ranges(&editor.display_snapshot(cx)),
6093 [
6094 MultiBufferOffset(2)..MultiBufferOffset(2),
6095 MultiBufferOffset(3)..MultiBufferOffset(3),
6096 MultiBufferOffset(5)..MultiBufferOffset(5)
6097 ]
6098 );
6099
6100 editor.transpose(&Default::default(), window, cx);
6101 assert_eq!(editor.text(cx), "bcade\n");
6102 assert_eq!(
6103 editor.selections.ranges(&editor.display_snapshot(cx)),
6104 [
6105 MultiBufferOffset(3)..MultiBufferOffset(3),
6106 MultiBufferOffset(4)..MultiBufferOffset(4),
6107 MultiBufferOffset(6)..MultiBufferOffset(6)
6108 ]
6109 );
6110
6111 editor.transpose(&Default::default(), window, cx);
6112 assert_eq!(editor.text(cx), "bcda\ne");
6113 assert_eq!(
6114 editor.selections.ranges(&editor.display_snapshot(cx)),
6115 [
6116 MultiBufferOffset(4)..MultiBufferOffset(4),
6117 MultiBufferOffset(6)..MultiBufferOffset(6)
6118 ]
6119 );
6120
6121 editor.transpose(&Default::default(), window, cx);
6122 assert_eq!(editor.text(cx), "bcade\n");
6123 assert_eq!(
6124 editor.selections.ranges(&editor.display_snapshot(cx)),
6125 [
6126 MultiBufferOffset(4)..MultiBufferOffset(4),
6127 MultiBufferOffset(6)..MultiBufferOffset(6)
6128 ]
6129 );
6130
6131 editor.transpose(&Default::default(), window, cx);
6132 assert_eq!(editor.text(cx), "bcaed\n");
6133 assert_eq!(
6134 editor.selections.ranges(&editor.display_snapshot(cx)),
6135 [
6136 MultiBufferOffset(5)..MultiBufferOffset(5),
6137 MultiBufferOffset(6)..MultiBufferOffset(6)
6138 ]
6139 );
6140
6141 editor
6142 });
6143
6144 _ = cx.add_window(|window, cx| {
6145 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6146 editor.set_style(EditorStyle::default(), window, cx);
6147 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6148 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6149 });
6150 editor.transpose(&Default::default(), window, cx);
6151 assert_eq!(editor.text(cx), "🏀🍐✋");
6152 assert_eq!(
6153 editor.selections.ranges(&editor.display_snapshot(cx)),
6154 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6155 );
6156
6157 editor.transpose(&Default::default(), window, cx);
6158 assert_eq!(editor.text(cx), "🏀✋🍐");
6159 assert_eq!(
6160 editor.selections.ranges(&editor.display_snapshot(cx)),
6161 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6162 );
6163
6164 editor.transpose(&Default::default(), window, cx);
6165 assert_eq!(editor.text(cx), "🏀🍐✋");
6166 assert_eq!(
6167 editor.selections.ranges(&editor.display_snapshot(cx)),
6168 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6169 );
6170
6171 editor
6172 });
6173}
6174
6175#[gpui::test]
6176async fn test_rewrap(cx: &mut TestAppContext) {
6177 init_test(cx, |settings| {
6178 settings.languages.0.extend([
6179 (
6180 "Markdown".into(),
6181 LanguageSettingsContent {
6182 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6183 preferred_line_length: Some(40),
6184 ..Default::default()
6185 },
6186 ),
6187 (
6188 "Plain Text".into(),
6189 LanguageSettingsContent {
6190 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6191 preferred_line_length: Some(40),
6192 ..Default::default()
6193 },
6194 ),
6195 (
6196 "C++".into(),
6197 LanguageSettingsContent {
6198 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6199 preferred_line_length: Some(40),
6200 ..Default::default()
6201 },
6202 ),
6203 (
6204 "Python".into(),
6205 LanguageSettingsContent {
6206 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6207 preferred_line_length: Some(40),
6208 ..Default::default()
6209 },
6210 ),
6211 (
6212 "Rust".into(),
6213 LanguageSettingsContent {
6214 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6215 preferred_line_length: Some(40),
6216 ..Default::default()
6217 },
6218 ),
6219 ])
6220 });
6221
6222 let mut cx = EditorTestContext::new(cx).await;
6223
6224 let cpp_language = Arc::new(Language::new(
6225 LanguageConfig {
6226 name: "C++".into(),
6227 line_comments: vec!["// ".into()],
6228 ..LanguageConfig::default()
6229 },
6230 None,
6231 ));
6232 let python_language = Arc::new(Language::new(
6233 LanguageConfig {
6234 name: "Python".into(),
6235 line_comments: vec!["# ".into()],
6236 ..LanguageConfig::default()
6237 },
6238 None,
6239 ));
6240 let markdown_language = Arc::new(Language::new(
6241 LanguageConfig {
6242 name: "Markdown".into(),
6243 rewrap_prefixes: vec![
6244 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6245 regex::Regex::new("[-*+]\\s+").unwrap(),
6246 ],
6247 ..LanguageConfig::default()
6248 },
6249 None,
6250 ));
6251 let rust_language = Arc::new(
6252 Language::new(
6253 LanguageConfig {
6254 name: "Rust".into(),
6255 line_comments: vec!["// ".into(), "/// ".into()],
6256 ..LanguageConfig::default()
6257 },
6258 Some(tree_sitter_rust::LANGUAGE.into()),
6259 )
6260 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6261 .unwrap(),
6262 );
6263
6264 let plaintext_language = Arc::new(Language::new(
6265 LanguageConfig {
6266 name: "Plain Text".into(),
6267 ..LanguageConfig::default()
6268 },
6269 None,
6270 ));
6271
6272 // Test basic rewrapping of a long line with a cursor
6273 assert_rewrap(
6274 indoc! {"
6275 // ˇThis is a long comment that needs to be wrapped.
6276 "},
6277 indoc! {"
6278 // ˇThis is a long comment that needs to
6279 // be wrapped.
6280 "},
6281 cpp_language.clone(),
6282 &mut cx,
6283 );
6284
6285 // Test rewrapping a full selection
6286 assert_rewrap(
6287 indoc! {"
6288 «// This selected long comment needs to be wrapped.ˇ»"
6289 },
6290 indoc! {"
6291 «// This selected long comment needs to
6292 // be wrapped.ˇ»"
6293 },
6294 cpp_language.clone(),
6295 &mut cx,
6296 );
6297
6298 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6299 assert_rewrap(
6300 indoc! {"
6301 // ˇThis is the first line.
6302 // Thisˇ is the second line.
6303 // This is the thirdˇ line, all part of one paragraph.
6304 "},
6305 indoc! {"
6306 // ˇThis is the first line. Thisˇ is the
6307 // second line. This is the thirdˇ line,
6308 // all part of one paragraph.
6309 "},
6310 cpp_language.clone(),
6311 &mut cx,
6312 );
6313
6314 // Test multiple cursors in different paragraphs trigger separate rewraps
6315 assert_rewrap(
6316 indoc! {"
6317 // ˇThis is the first paragraph, first line.
6318 // ˇThis is the first paragraph, second line.
6319
6320 // ˇThis is the second paragraph, first line.
6321 // ˇThis is the second paragraph, second line.
6322 "},
6323 indoc! {"
6324 // ˇThis is the first paragraph, first
6325 // line. ˇThis is the first paragraph,
6326 // second line.
6327
6328 // ˇThis is the second paragraph, first
6329 // line. ˇThis is the second paragraph,
6330 // second line.
6331 "},
6332 cpp_language.clone(),
6333 &mut cx,
6334 );
6335
6336 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6337 assert_rewrap(
6338 indoc! {"
6339 «// A regular long long comment to be wrapped.
6340 /// A documentation long comment to be wrapped.ˇ»
6341 "},
6342 indoc! {"
6343 «// A regular long long comment to be
6344 // wrapped.
6345 /// A documentation long comment to be
6346 /// wrapped.ˇ»
6347 "},
6348 rust_language.clone(),
6349 &mut cx,
6350 );
6351
6352 // Test that change in indentation level trigger seperate rewraps
6353 assert_rewrap(
6354 indoc! {"
6355 fn foo() {
6356 «// This is a long comment at the base indent.
6357 // This is a long comment at the next indent.ˇ»
6358 }
6359 "},
6360 indoc! {"
6361 fn foo() {
6362 «// This is a long comment at the
6363 // base indent.
6364 // This is a long comment at the
6365 // next indent.ˇ»
6366 }
6367 "},
6368 rust_language.clone(),
6369 &mut cx,
6370 );
6371
6372 // Test that different comment prefix characters (e.g., '#') are handled correctly
6373 assert_rewrap(
6374 indoc! {"
6375 # ˇThis is a long comment using a pound sign.
6376 "},
6377 indoc! {"
6378 # ˇThis is a long comment using a pound
6379 # sign.
6380 "},
6381 python_language,
6382 &mut cx,
6383 );
6384
6385 // Test rewrapping only affects comments, not code even when selected
6386 assert_rewrap(
6387 indoc! {"
6388 «/// This doc comment is long and should be wrapped.
6389 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6390 "},
6391 indoc! {"
6392 «/// This doc comment is long and should
6393 /// be wrapped.
6394 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6395 "},
6396 rust_language.clone(),
6397 &mut cx,
6398 );
6399
6400 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6401 assert_rewrap(
6402 indoc! {"
6403 # Header
6404
6405 A long long long line of markdown text to wrap.ˇ
6406 "},
6407 indoc! {"
6408 # Header
6409
6410 A long long long line of markdown text
6411 to wrap.ˇ
6412 "},
6413 markdown_language.clone(),
6414 &mut cx,
6415 );
6416
6417 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6418 assert_rewrap(
6419 indoc! {"
6420 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6421 2. This is a numbered list item that is very long and needs to be wrapped properly.
6422 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6423 "},
6424 indoc! {"
6425 «1. This is a numbered list item that is
6426 very long and needs to be wrapped
6427 properly.
6428 2. This is a numbered list item that is
6429 very long and needs to be wrapped
6430 properly.
6431 - This is an unordered list item that is
6432 also very long and should not merge
6433 with the numbered item.ˇ»
6434 "},
6435 markdown_language.clone(),
6436 &mut cx,
6437 );
6438
6439 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6440 assert_rewrap(
6441 indoc! {"
6442 «1. This is a numbered list item that is
6443 very long and needs to be wrapped
6444 properly.
6445 2. This is a numbered list item that is
6446 very long and needs to be wrapped
6447 properly.
6448 - This is an unordered list item that is
6449 also very long and should not merge with
6450 the numbered item.ˇ»
6451 "},
6452 indoc! {"
6453 «1. This is a numbered list item that is
6454 very long and needs to be wrapped
6455 properly.
6456 2. This is a numbered list item that is
6457 very long and needs to be wrapped
6458 properly.
6459 - This is an unordered list item that is
6460 also very long and should not merge
6461 with the numbered item.ˇ»
6462 "},
6463 markdown_language.clone(),
6464 &mut cx,
6465 );
6466
6467 // Test that rewrapping maintain indents even when they already exists.
6468 assert_rewrap(
6469 indoc! {"
6470 «1. This is a numbered list
6471 item that is very long and needs to be wrapped properly.
6472 2. This is a numbered list
6473 item that is very long and needs to be wrapped properly.
6474 - This is an unordered list item that is also very long and
6475 should not merge with the numbered item.ˇ»
6476 "},
6477 indoc! {"
6478 «1. This is a numbered list item that is
6479 very long and needs to be wrapped
6480 properly.
6481 2. This is a numbered list item that is
6482 very long and needs to be wrapped
6483 properly.
6484 - This is an unordered list item that is
6485 also very long and should not merge
6486 with the numbered item.ˇ»
6487 "},
6488 markdown_language,
6489 &mut cx,
6490 );
6491
6492 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6493 assert_rewrap(
6494 indoc! {"
6495 ˇThis is a very long line of plain text that will be wrapped.
6496 "},
6497 indoc! {"
6498 ˇThis is a very long line of plain text
6499 that will be wrapped.
6500 "},
6501 plaintext_language.clone(),
6502 &mut cx,
6503 );
6504
6505 // Test that non-commented code acts as a paragraph boundary within a selection
6506 assert_rewrap(
6507 indoc! {"
6508 «// This is the first long comment block to be wrapped.
6509 fn my_func(a: u32);
6510 // This is the second long comment block to be wrapped.ˇ»
6511 "},
6512 indoc! {"
6513 «// This is the first long comment block
6514 // to be wrapped.
6515 fn my_func(a: u32);
6516 // This is the second long comment block
6517 // to be wrapped.ˇ»
6518 "},
6519 rust_language,
6520 &mut cx,
6521 );
6522
6523 // Test rewrapping multiple selections, including ones with blank lines or tabs
6524 assert_rewrap(
6525 indoc! {"
6526 «ˇThis is a very long line that will be wrapped.
6527
6528 This is another paragraph in the same selection.»
6529
6530 «\tThis is a very long indented line that will be wrapped.ˇ»
6531 "},
6532 indoc! {"
6533 «ˇThis is a very long line that will be
6534 wrapped.
6535
6536 This is another paragraph in the same
6537 selection.»
6538
6539 «\tThis is a very long indented line
6540 \tthat will be wrapped.ˇ»
6541 "},
6542 plaintext_language,
6543 &mut cx,
6544 );
6545
6546 // Test that an empty comment line acts as a paragraph boundary
6547 assert_rewrap(
6548 indoc! {"
6549 // ˇThis is a long comment that will be wrapped.
6550 //
6551 // And this is another long comment that will also be wrapped.ˇ
6552 "},
6553 indoc! {"
6554 // ˇThis is a long comment that will be
6555 // wrapped.
6556 //
6557 // And this is another long comment that
6558 // will also be wrapped.ˇ
6559 "},
6560 cpp_language,
6561 &mut cx,
6562 );
6563
6564 #[track_caller]
6565 fn assert_rewrap(
6566 unwrapped_text: &str,
6567 wrapped_text: &str,
6568 language: Arc<Language>,
6569 cx: &mut EditorTestContext,
6570 ) {
6571 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6572 cx.set_state(unwrapped_text);
6573 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6574 cx.assert_editor_state(wrapped_text);
6575 }
6576}
6577
6578#[gpui::test]
6579async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6580 init_test(cx, |settings| {
6581 settings.languages.0.extend([(
6582 "Rust".into(),
6583 LanguageSettingsContent {
6584 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6585 preferred_line_length: Some(40),
6586 ..Default::default()
6587 },
6588 )])
6589 });
6590
6591 let mut cx = EditorTestContext::new(cx).await;
6592
6593 let rust_lang = Arc::new(
6594 Language::new(
6595 LanguageConfig {
6596 name: "Rust".into(),
6597 line_comments: vec!["// ".into()],
6598 block_comment: Some(BlockCommentConfig {
6599 start: "/*".into(),
6600 end: "*/".into(),
6601 prefix: "* ".into(),
6602 tab_size: 1,
6603 }),
6604 documentation_comment: Some(BlockCommentConfig {
6605 start: "/**".into(),
6606 end: "*/".into(),
6607 prefix: "* ".into(),
6608 tab_size: 1,
6609 }),
6610
6611 ..LanguageConfig::default()
6612 },
6613 Some(tree_sitter_rust::LANGUAGE.into()),
6614 )
6615 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6616 .unwrap(),
6617 );
6618
6619 // regular block comment
6620 assert_rewrap(
6621 indoc! {"
6622 /*
6623 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6624 */
6625 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6626 "},
6627 indoc! {"
6628 /*
6629 *ˇ Lorem ipsum dolor sit amet,
6630 * consectetur adipiscing elit.
6631 */
6632 /*
6633 *ˇ Lorem ipsum dolor sit amet,
6634 * consectetur adipiscing elit.
6635 */
6636 "},
6637 rust_lang.clone(),
6638 &mut cx,
6639 );
6640
6641 // indent is respected
6642 assert_rewrap(
6643 indoc! {"
6644 {}
6645 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6646 "},
6647 indoc! {"
6648 {}
6649 /*
6650 *ˇ Lorem ipsum dolor sit amet,
6651 * consectetur adipiscing elit.
6652 */
6653 "},
6654 rust_lang.clone(),
6655 &mut cx,
6656 );
6657
6658 // short block comments with inline delimiters
6659 assert_rewrap(
6660 indoc! {"
6661 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6662 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6663 */
6664 /*
6665 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6666 "},
6667 indoc! {"
6668 /*
6669 *ˇ Lorem ipsum dolor sit amet,
6670 * consectetur adipiscing elit.
6671 */
6672 /*
6673 *ˇ Lorem ipsum dolor sit amet,
6674 * consectetur adipiscing elit.
6675 */
6676 /*
6677 *ˇ Lorem ipsum dolor sit amet,
6678 * consectetur adipiscing elit.
6679 */
6680 "},
6681 rust_lang.clone(),
6682 &mut cx,
6683 );
6684
6685 // multiline block comment with inline start/end delimiters
6686 assert_rewrap(
6687 indoc! {"
6688 /*ˇ Lorem ipsum dolor sit amet,
6689 * consectetur adipiscing elit. */
6690 "},
6691 indoc! {"
6692 /*
6693 *ˇ Lorem ipsum dolor sit amet,
6694 * consectetur adipiscing elit.
6695 */
6696 "},
6697 rust_lang.clone(),
6698 &mut cx,
6699 );
6700
6701 // block comment rewrap still respects paragraph bounds
6702 assert_rewrap(
6703 indoc! {"
6704 /*
6705 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6706 *
6707 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6708 */
6709 "},
6710 indoc! {"
6711 /*
6712 *ˇ Lorem ipsum dolor sit amet,
6713 * consectetur adipiscing elit.
6714 *
6715 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6716 */
6717 "},
6718 rust_lang.clone(),
6719 &mut cx,
6720 );
6721
6722 // documentation comments
6723 assert_rewrap(
6724 indoc! {"
6725 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6726 /**
6727 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6728 */
6729 "},
6730 indoc! {"
6731 /**
6732 *ˇ Lorem ipsum dolor sit amet,
6733 * consectetur adipiscing elit.
6734 */
6735 /**
6736 *ˇ Lorem ipsum dolor sit amet,
6737 * consectetur adipiscing elit.
6738 */
6739 "},
6740 rust_lang.clone(),
6741 &mut cx,
6742 );
6743
6744 // different, adjacent comments
6745 assert_rewrap(
6746 indoc! {"
6747 /**
6748 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6749 */
6750 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6751 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6752 "},
6753 indoc! {"
6754 /**
6755 *ˇ Lorem ipsum dolor sit amet,
6756 * consectetur adipiscing elit.
6757 */
6758 /*
6759 *ˇ Lorem ipsum dolor sit amet,
6760 * consectetur adipiscing elit.
6761 */
6762 //ˇ Lorem ipsum dolor sit amet,
6763 // consectetur adipiscing elit.
6764 "},
6765 rust_lang.clone(),
6766 &mut cx,
6767 );
6768
6769 // selection w/ single short block comment
6770 assert_rewrap(
6771 indoc! {"
6772 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6773 "},
6774 indoc! {"
6775 «/*
6776 * Lorem ipsum dolor sit amet,
6777 * consectetur adipiscing elit.
6778 */ˇ»
6779 "},
6780 rust_lang.clone(),
6781 &mut cx,
6782 );
6783
6784 // rewrapping a single comment w/ abutting comments
6785 assert_rewrap(
6786 indoc! {"
6787 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6788 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6789 "},
6790 indoc! {"
6791 /*
6792 * ˇLorem ipsum dolor sit amet,
6793 * consectetur adipiscing elit.
6794 */
6795 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6796 "},
6797 rust_lang.clone(),
6798 &mut cx,
6799 );
6800
6801 // selection w/ non-abutting short block comments
6802 assert_rewrap(
6803 indoc! {"
6804 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6805
6806 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6807 "},
6808 indoc! {"
6809 «/*
6810 * Lorem ipsum dolor sit amet,
6811 * consectetur adipiscing elit.
6812 */
6813
6814 /*
6815 * Lorem ipsum dolor sit amet,
6816 * consectetur adipiscing elit.
6817 */ˇ»
6818 "},
6819 rust_lang.clone(),
6820 &mut cx,
6821 );
6822
6823 // selection of multiline block comments
6824 assert_rewrap(
6825 indoc! {"
6826 «/* Lorem ipsum dolor sit amet,
6827 * consectetur adipiscing elit. */ˇ»
6828 "},
6829 indoc! {"
6830 «/*
6831 * Lorem ipsum dolor sit amet,
6832 * consectetur adipiscing elit.
6833 */ˇ»
6834 "},
6835 rust_lang.clone(),
6836 &mut cx,
6837 );
6838
6839 // partial selection of multiline block comments
6840 assert_rewrap(
6841 indoc! {"
6842 «/* Lorem ipsum dolor sit amet,ˇ»
6843 * consectetur adipiscing elit. */
6844 /* Lorem ipsum dolor sit amet,
6845 «* consectetur adipiscing elit. */ˇ»
6846 "},
6847 indoc! {"
6848 «/*
6849 * Lorem ipsum dolor sit amet,ˇ»
6850 * consectetur adipiscing elit. */
6851 /* Lorem ipsum dolor sit amet,
6852 «* consectetur adipiscing elit.
6853 */ˇ»
6854 "},
6855 rust_lang.clone(),
6856 &mut cx,
6857 );
6858
6859 // selection w/ abutting short block comments
6860 // TODO: should not be combined; should rewrap as 2 comments
6861 assert_rewrap(
6862 indoc! {"
6863 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6864 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6865 "},
6866 // desired behavior:
6867 // indoc! {"
6868 // «/*
6869 // * Lorem ipsum dolor sit amet,
6870 // * consectetur adipiscing elit.
6871 // */
6872 // /*
6873 // * Lorem ipsum dolor sit amet,
6874 // * consectetur adipiscing elit.
6875 // */ˇ»
6876 // "},
6877 // actual behaviour:
6878 indoc! {"
6879 «/*
6880 * Lorem ipsum dolor sit amet,
6881 * consectetur adipiscing elit. Lorem
6882 * ipsum dolor sit amet, consectetur
6883 * adipiscing elit.
6884 */ˇ»
6885 "},
6886 rust_lang.clone(),
6887 &mut cx,
6888 );
6889
6890 // TODO: same as above, but with delimiters on separate line
6891 // assert_rewrap(
6892 // indoc! {"
6893 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6894 // */
6895 // /*
6896 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6897 // "},
6898 // // desired:
6899 // // indoc! {"
6900 // // «/*
6901 // // * Lorem ipsum dolor sit amet,
6902 // // * consectetur adipiscing elit.
6903 // // */
6904 // // /*
6905 // // * Lorem ipsum dolor sit amet,
6906 // // * consectetur adipiscing elit.
6907 // // */ˇ»
6908 // // "},
6909 // // actual: (but with trailing w/s on the empty lines)
6910 // indoc! {"
6911 // «/*
6912 // * Lorem ipsum dolor sit amet,
6913 // * consectetur adipiscing elit.
6914 // *
6915 // */
6916 // /*
6917 // *
6918 // * Lorem ipsum dolor sit amet,
6919 // * consectetur adipiscing elit.
6920 // */ˇ»
6921 // "},
6922 // rust_lang.clone(),
6923 // &mut cx,
6924 // );
6925
6926 // TODO these are unhandled edge cases; not correct, just documenting known issues
6927 assert_rewrap(
6928 indoc! {"
6929 /*
6930 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6931 */
6932 /*
6933 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6934 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6935 "},
6936 // desired:
6937 // indoc! {"
6938 // /*
6939 // *ˇ Lorem ipsum dolor sit amet,
6940 // * consectetur adipiscing elit.
6941 // */
6942 // /*
6943 // *ˇ Lorem ipsum dolor sit amet,
6944 // * consectetur adipiscing elit.
6945 // */
6946 // /*
6947 // *ˇ Lorem ipsum dolor sit amet
6948 // */ /* consectetur adipiscing elit. */
6949 // "},
6950 // actual:
6951 indoc! {"
6952 /*
6953 //ˇ Lorem ipsum dolor sit amet,
6954 // consectetur adipiscing elit.
6955 */
6956 /*
6957 * //ˇ Lorem ipsum dolor sit amet,
6958 * consectetur adipiscing elit.
6959 */
6960 /*
6961 *ˇ Lorem ipsum dolor sit amet */ /*
6962 * consectetur adipiscing elit.
6963 */
6964 "},
6965 rust_lang,
6966 &mut cx,
6967 );
6968
6969 #[track_caller]
6970 fn assert_rewrap(
6971 unwrapped_text: &str,
6972 wrapped_text: &str,
6973 language: Arc<Language>,
6974 cx: &mut EditorTestContext,
6975 ) {
6976 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6977 cx.set_state(unwrapped_text);
6978 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6979 cx.assert_editor_state(wrapped_text);
6980 }
6981}
6982
6983#[gpui::test]
6984async fn test_hard_wrap(cx: &mut TestAppContext) {
6985 init_test(cx, |_| {});
6986 let mut cx = EditorTestContext::new(cx).await;
6987
6988 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6989 cx.update_editor(|editor, _, cx| {
6990 editor.set_hard_wrap(Some(14), cx);
6991 });
6992
6993 cx.set_state(indoc!(
6994 "
6995 one two three ˇ
6996 "
6997 ));
6998 cx.simulate_input("four");
6999 cx.run_until_parked();
7000
7001 cx.assert_editor_state(indoc!(
7002 "
7003 one two three
7004 fourˇ
7005 "
7006 ));
7007
7008 cx.update_editor(|editor, window, cx| {
7009 editor.newline(&Default::default(), window, cx);
7010 });
7011 cx.run_until_parked();
7012 cx.assert_editor_state(indoc!(
7013 "
7014 one two three
7015 four
7016 ˇ
7017 "
7018 ));
7019
7020 cx.simulate_input("five");
7021 cx.run_until_parked();
7022 cx.assert_editor_state(indoc!(
7023 "
7024 one two three
7025 four
7026 fiveˇ
7027 "
7028 ));
7029
7030 cx.update_editor(|editor, window, cx| {
7031 editor.newline(&Default::default(), window, cx);
7032 });
7033 cx.run_until_parked();
7034 cx.simulate_input("# ");
7035 cx.run_until_parked();
7036 cx.assert_editor_state(indoc!(
7037 "
7038 one two three
7039 four
7040 five
7041 # ˇ
7042 "
7043 ));
7044
7045 cx.update_editor(|editor, window, cx| {
7046 editor.newline(&Default::default(), window, cx);
7047 });
7048 cx.run_until_parked();
7049 cx.assert_editor_state(indoc!(
7050 "
7051 one two three
7052 four
7053 five
7054 #\x20
7055 #ˇ
7056 "
7057 ));
7058
7059 cx.simulate_input(" 6");
7060 cx.run_until_parked();
7061 cx.assert_editor_state(indoc!(
7062 "
7063 one two three
7064 four
7065 five
7066 #
7067 # 6ˇ
7068 "
7069 ));
7070}
7071
7072#[gpui::test]
7073async fn test_cut_line_ends(cx: &mut TestAppContext) {
7074 init_test(cx, |_| {});
7075
7076 let mut cx = EditorTestContext::new(cx).await;
7077
7078 cx.set_state(indoc! {"The quick brownˇ"});
7079 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7080 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7081
7082 cx.set_state(indoc! {"The emacs foxˇ"});
7083 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7084 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7085
7086 cx.set_state(indoc! {"
7087 The quick« brownˇ»
7088 fox jumps overˇ
7089 the lazy dog"});
7090 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7091 cx.assert_editor_state(indoc! {"
7092 The quickˇ
7093 ˇthe lazy dog"});
7094
7095 cx.set_state(indoc! {"
7096 The quick« brownˇ»
7097 fox jumps overˇ
7098 the lazy dog"});
7099 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7100 cx.assert_editor_state(indoc! {"
7101 The quickˇ
7102 fox jumps overˇthe lazy dog"});
7103
7104 cx.set_state(indoc! {"
7105 The quick« brownˇ»
7106 fox jumps overˇ
7107 the lazy dog"});
7108 cx.update_editor(|e, window, cx| {
7109 e.cut_to_end_of_line(
7110 &CutToEndOfLine {
7111 stop_at_newlines: true,
7112 },
7113 window,
7114 cx,
7115 )
7116 });
7117 cx.assert_editor_state(indoc! {"
7118 The quickˇ
7119 fox jumps overˇ
7120 the lazy dog"});
7121
7122 cx.set_state(indoc! {"
7123 The quick« brownˇ»
7124 fox jumps overˇ
7125 the lazy dog"});
7126 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7127 cx.assert_editor_state(indoc! {"
7128 The quickˇ
7129 fox jumps overˇthe lazy dog"});
7130}
7131
7132#[gpui::test]
7133async fn test_clipboard(cx: &mut TestAppContext) {
7134 init_test(cx, |_| {});
7135
7136 let mut cx = EditorTestContext::new(cx).await;
7137
7138 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7139 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7140 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7141
7142 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7143 cx.set_state("two ˇfour ˇsix ˇ");
7144 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7145 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7146
7147 // Paste again but with only two cursors. Since the number of cursors doesn't
7148 // match the number of slices in the clipboard, the entire clipboard text
7149 // is pasted at each cursor.
7150 cx.set_state("ˇtwo one✅ four three six five ˇ");
7151 cx.update_editor(|e, window, cx| {
7152 e.handle_input("( ", window, cx);
7153 e.paste(&Paste, window, cx);
7154 e.handle_input(") ", window, cx);
7155 });
7156 cx.assert_editor_state(
7157 &([
7158 "( one✅ ",
7159 "three ",
7160 "five ) ˇtwo one✅ four three six five ( one✅ ",
7161 "three ",
7162 "five ) ˇ",
7163 ]
7164 .join("\n")),
7165 );
7166
7167 // Cut with three selections, one of which is full-line.
7168 cx.set_state(indoc! {"
7169 1«2ˇ»3
7170 4ˇ567
7171 «8ˇ»9"});
7172 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7173 cx.assert_editor_state(indoc! {"
7174 1ˇ3
7175 ˇ9"});
7176
7177 // Paste with three selections, noticing how the copied selection that was full-line
7178 // gets inserted before the second cursor.
7179 cx.set_state(indoc! {"
7180 1ˇ3
7181 9ˇ
7182 «oˇ»ne"});
7183 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7184 cx.assert_editor_state(indoc! {"
7185 12ˇ3
7186 4567
7187 9ˇ
7188 8ˇne"});
7189
7190 // Copy with a single cursor only, which writes the whole line into the clipboard.
7191 cx.set_state(indoc! {"
7192 The quick brown
7193 fox juˇmps over
7194 the lazy dog"});
7195 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7196 assert_eq!(
7197 cx.read_from_clipboard()
7198 .and_then(|item| item.text().as_deref().map(str::to_string)),
7199 Some("fox jumps over\n".to_string())
7200 );
7201
7202 // Paste with three selections, noticing how the copied full-line selection is inserted
7203 // before the empty selections but replaces the selection that is non-empty.
7204 cx.set_state(indoc! {"
7205 Tˇhe quick brown
7206 «foˇ»x jumps over
7207 tˇhe lazy dog"});
7208 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7209 cx.assert_editor_state(indoc! {"
7210 fox jumps over
7211 Tˇhe quick brown
7212 fox jumps over
7213 ˇx jumps over
7214 fox jumps over
7215 tˇhe lazy dog"});
7216}
7217
7218#[gpui::test]
7219async fn test_copy_trim(cx: &mut TestAppContext) {
7220 init_test(cx, |_| {});
7221
7222 let mut cx = EditorTestContext::new(cx).await;
7223 cx.set_state(
7224 r#" «for selection in selections.iter() {
7225 let mut start = selection.start;
7226 let mut end = selection.end;
7227 let is_entire_line = selection.is_empty();
7228 if is_entire_line {
7229 start = Point::new(start.row, 0);ˇ»
7230 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7231 }
7232 "#,
7233 );
7234 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7235 assert_eq!(
7236 cx.read_from_clipboard()
7237 .and_then(|item| item.text().as_deref().map(str::to_string)),
7238 Some(
7239 "for selection in selections.iter() {
7240 let mut start = selection.start;
7241 let mut end = selection.end;
7242 let is_entire_line = selection.is_empty();
7243 if is_entire_line {
7244 start = Point::new(start.row, 0);"
7245 .to_string()
7246 ),
7247 "Regular copying preserves all indentation selected",
7248 );
7249 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7250 assert_eq!(
7251 cx.read_from_clipboard()
7252 .and_then(|item| item.text().as_deref().map(str::to_string)),
7253 Some(
7254 "for selection in selections.iter() {
7255let mut start = selection.start;
7256let mut end = selection.end;
7257let is_entire_line = selection.is_empty();
7258if is_entire_line {
7259 start = Point::new(start.row, 0);"
7260 .to_string()
7261 ),
7262 "Copying with stripping should strip all leading whitespaces"
7263 );
7264
7265 cx.set_state(
7266 r#" « for selection in selections.iter() {
7267 let mut start = selection.start;
7268 let mut end = selection.end;
7269 let is_entire_line = selection.is_empty();
7270 if is_entire_line {
7271 start = Point::new(start.row, 0);ˇ»
7272 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7273 }
7274 "#,
7275 );
7276 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7277 assert_eq!(
7278 cx.read_from_clipboard()
7279 .and_then(|item| item.text().as_deref().map(str::to_string)),
7280 Some(
7281 " for selection in selections.iter() {
7282 let mut start = selection.start;
7283 let mut end = selection.end;
7284 let is_entire_line = selection.is_empty();
7285 if is_entire_line {
7286 start = Point::new(start.row, 0);"
7287 .to_string()
7288 ),
7289 "Regular copying preserves all indentation selected",
7290 );
7291 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7292 assert_eq!(
7293 cx.read_from_clipboard()
7294 .and_then(|item| item.text().as_deref().map(str::to_string)),
7295 Some(
7296 "for selection in selections.iter() {
7297let mut start = selection.start;
7298let mut end = selection.end;
7299let is_entire_line = selection.is_empty();
7300if is_entire_line {
7301 start = Point::new(start.row, 0);"
7302 .to_string()
7303 ),
7304 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7305 );
7306
7307 cx.set_state(
7308 r#" «ˇ for selection in selections.iter() {
7309 let mut start = selection.start;
7310 let mut end = selection.end;
7311 let is_entire_line = selection.is_empty();
7312 if is_entire_line {
7313 start = Point::new(start.row, 0);»
7314 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7315 }
7316 "#,
7317 );
7318 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7319 assert_eq!(
7320 cx.read_from_clipboard()
7321 .and_then(|item| item.text().as_deref().map(str::to_string)),
7322 Some(
7323 " for selection in selections.iter() {
7324 let mut start = selection.start;
7325 let mut end = selection.end;
7326 let is_entire_line = selection.is_empty();
7327 if is_entire_line {
7328 start = Point::new(start.row, 0);"
7329 .to_string()
7330 ),
7331 "Regular copying for reverse selection works the same",
7332 );
7333 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7334 assert_eq!(
7335 cx.read_from_clipboard()
7336 .and_then(|item| item.text().as_deref().map(str::to_string)),
7337 Some(
7338 "for selection in selections.iter() {
7339let mut start = selection.start;
7340let mut end = selection.end;
7341let is_entire_line = selection.is_empty();
7342if is_entire_line {
7343 start = Point::new(start.row, 0);"
7344 .to_string()
7345 ),
7346 "Copying with stripping for reverse selection works the same"
7347 );
7348
7349 cx.set_state(
7350 r#" for selection «in selections.iter() {
7351 let mut start = selection.start;
7352 let mut end = selection.end;
7353 let is_entire_line = selection.is_empty();
7354 if is_entire_line {
7355 start = Point::new(start.row, 0);ˇ»
7356 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7357 }
7358 "#,
7359 );
7360 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7361 assert_eq!(
7362 cx.read_from_clipboard()
7363 .and_then(|item| item.text().as_deref().map(str::to_string)),
7364 Some(
7365 "in selections.iter() {
7366 let mut start = selection.start;
7367 let mut end = selection.end;
7368 let is_entire_line = selection.is_empty();
7369 if is_entire_line {
7370 start = Point::new(start.row, 0);"
7371 .to_string()
7372 ),
7373 "When selecting past the indent, the copying works as usual",
7374 );
7375 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7376 assert_eq!(
7377 cx.read_from_clipboard()
7378 .and_then(|item| item.text().as_deref().map(str::to_string)),
7379 Some(
7380 "in selections.iter() {
7381 let mut start = selection.start;
7382 let mut end = selection.end;
7383 let is_entire_line = selection.is_empty();
7384 if is_entire_line {
7385 start = Point::new(start.row, 0);"
7386 .to_string()
7387 ),
7388 "When selecting past the indent, nothing is trimmed"
7389 );
7390
7391 cx.set_state(
7392 r#" «for selection in selections.iter() {
7393 let mut start = selection.start;
7394
7395 let mut end = selection.end;
7396 let is_entire_line = selection.is_empty();
7397 if is_entire_line {
7398 start = Point::new(start.row, 0);
7399ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7400 }
7401 "#,
7402 );
7403 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7404 assert_eq!(
7405 cx.read_from_clipboard()
7406 .and_then(|item| item.text().as_deref().map(str::to_string)),
7407 Some(
7408 "for selection in selections.iter() {
7409let mut start = selection.start;
7410
7411let mut end = selection.end;
7412let is_entire_line = selection.is_empty();
7413if is_entire_line {
7414 start = Point::new(start.row, 0);
7415"
7416 .to_string()
7417 ),
7418 "Copying with stripping should ignore empty lines"
7419 );
7420}
7421
7422#[gpui::test]
7423async fn test_paste_multiline(cx: &mut TestAppContext) {
7424 init_test(cx, |_| {});
7425
7426 let mut cx = EditorTestContext::new(cx).await;
7427 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7428
7429 // Cut an indented block, without the leading whitespace.
7430 cx.set_state(indoc! {"
7431 const a: B = (
7432 c(),
7433 «d(
7434 e,
7435 f
7436 )ˇ»
7437 );
7438 "});
7439 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7440 cx.assert_editor_state(indoc! {"
7441 const a: B = (
7442 c(),
7443 ˇ
7444 );
7445 "});
7446
7447 // Paste it at the same position.
7448 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7449 cx.assert_editor_state(indoc! {"
7450 const a: B = (
7451 c(),
7452 d(
7453 e,
7454 f
7455 )ˇ
7456 );
7457 "});
7458
7459 // Paste it at a line with a lower indent level.
7460 cx.set_state(indoc! {"
7461 ˇ
7462 const a: B = (
7463 c(),
7464 );
7465 "});
7466 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7467 cx.assert_editor_state(indoc! {"
7468 d(
7469 e,
7470 f
7471 )ˇ
7472 const a: B = (
7473 c(),
7474 );
7475 "});
7476
7477 // Cut an indented block, with the leading whitespace.
7478 cx.set_state(indoc! {"
7479 const a: B = (
7480 c(),
7481 « d(
7482 e,
7483 f
7484 )
7485 ˇ»);
7486 "});
7487 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7488 cx.assert_editor_state(indoc! {"
7489 const a: B = (
7490 c(),
7491 ˇ);
7492 "});
7493
7494 // Paste it at the same position.
7495 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7496 cx.assert_editor_state(indoc! {"
7497 const a: B = (
7498 c(),
7499 d(
7500 e,
7501 f
7502 )
7503 ˇ);
7504 "});
7505
7506 // Paste it at a line with a higher indent level.
7507 cx.set_state(indoc! {"
7508 const a: B = (
7509 c(),
7510 d(
7511 e,
7512 fˇ
7513 )
7514 );
7515 "});
7516 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7517 cx.assert_editor_state(indoc! {"
7518 const a: B = (
7519 c(),
7520 d(
7521 e,
7522 f d(
7523 e,
7524 f
7525 )
7526 ˇ
7527 )
7528 );
7529 "});
7530
7531 // Copy an indented block, starting mid-line
7532 cx.set_state(indoc! {"
7533 const a: B = (
7534 c(),
7535 somethin«g(
7536 e,
7537 f
7538 )ˇ»
7539 );
7540 "});
7541 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7542
7543 // Paste it on a line with a lower indent level
7544 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7545 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7546 cx.assert_editor_state(indoc! {"
7547 const a: B = (
7548 c(),
7549 something(
7550 e,
7551 f
7552 )
7553 );
7554 g(
7555 e,
7556 f
7557 )ˇ"});
7558}
7559
7560#[gpui::test]
7561async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7562 init_test(cx, |_| {});
7563
7564 cx.write_to_clipboard(ClipboardItem::new_string(
7565 " d(\n e\n );\n".into(),
7566 ));
7567
7568 let mut cx = EditorTestContext::new(cx).await;
7569 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7570
7571 cx.set_state(indoc! {"
7572 fn a() {
7573 b();
7574 if c() {
7575 ˇ
7576 }
7577 }
7578 "});
7579
7580 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7581 cx.assert_editor_state(indoc! {"
7582 fn a() {
7583 b();
7584 if c() {
7585 d(
7586 e
7587 );
7588 ˇ
7589 }
7590 }
7591 "});
7592
7593 cx.set_state(indoc! {"
7594 fn a() {
7595 b();
7596 ˇ
7597 }
7598 "});
7599
7600 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7601 cx.assert_editor_state(indoc! {"
7602 fn a() {
7603 b();
7604 d(
7605 e
7606 );
7607 ˇ
7608 }
7609 "});
7610}
7611
7612#[gpui::test]
7613fn test_select_all(cx: &mut TestAppContext) {
7614 init_test(cx, |_| {});
7615
7616 let editor = cx.add_window(|window, cx| {
7617 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7618 build_editor(buffer, window, cx)
7619 });
7620 _ = editor.update(cx, |editor, window, cx| {
7621 editor.select_all(&SelectAll, window, cx);
7622 assert_eq!(
7623 display_ranges(editor, cx),
7624 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7625 );
7626 });
7627}
7628
7629#[gpui::test]
7630fn test_select_line(cx: &mut TestAppContext) {
7631 init_test(cx, |_| {});
7632
7633 let editor = cx.add_window(|window, cx| {
7634 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7635 build_editor(buffer, window, cx)
7636 });
7637 _ = editor.update(cx, |editor, window, cx| {
7638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7639 s.select_display_ranges([
7640 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7641 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7642 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7643 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7644 ])
7645 });
7646 editor.select_line(&SelectLine, window, cx);
7647 assert_eq!(
7648 display_ranges(editor, cx),
7649 vec![
7650 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7651 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7652 ]
7653 );
7654 });
7655
7656 _ = editor.update(cx, |editor, window, cx| {
7657 editor.select_line(&SelectLine, window, cx);
7658 assert_eq!(
7659 display_ranges(editor, cx),
7660 vec![
7661 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7662 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7663 ]
7664 );
7665 });
7666
7667 _ = editor.update(cx, |editor, window, cx| {
7668 editor.select_line(&SelectLine, window, cx);
7669 assert_eq!(
7670 display_ranges(editor, cx),
7671 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7672 );
7673 });
7674}
7675
7676#[gpui::test]
7677async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7678 init_test(cx, |_| {});
7679 let mut cx = EditorTestContext::new(cx).await;
7680
7681 #[track_caller]
7682 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7683 cx.set_state(initial_state);
7684 cx.update_editor(|e, window, cx| {
7685 e.split_selection_into_lines(&Default::default(), window, cx)
7686 });
7687 cx.assert_editor_state(expected_state);
7688 }
7689
7690 // Selection starts and ends at the middle of lines, left-to-right
7691 test(
7692 &mut cx,
7693 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7694 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7695 );
7696 // Same thing, right-to-left
7697 test(
7698 &mut cx,
7699 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7700 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7701 );
7702
7703 // Whole buffer, left-to-right, last line *doesn't* end with newline
7704 test(
7705 &mut cx,
7706 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7707 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7708 );
7709 // Same thing, right-to-left
7710 test(
7711 &mut cx,
7712 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7713 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7714 );
7715
7716 // Whole buffer, left-to-right, last line ends with newline
7717 test(
7718 &mut cx,
7719 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7720 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7721 );
7722 // Same thing, right-to-left
7723 test(
7724 &mut cx,
7725 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7726 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7727 );
7728
7729 // Starts at the end of a line, ends at the start of another
7730 test(
7731 &mut cx,
7732 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7733 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7734 );
7735}
7736
7737#[gpui::test]
7738async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7739 init_test(cx, |_| {});
7740
7741 let editor = cx.add_window(|window, cx| {
7742 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7743 build_editor(buffer, window, cx)
7744 });
7745
7746 // setup
7747 _ = editor.update(cx, |editor, window, cx| {
7748 editor.fold_creases(
7749 vec![
7750 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7751 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7752 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7753 ],
7754 true,
7755 window,
7756 cx,
7757 );
7758 assert_eq!(
7759 editor.display_text(cx),
7760 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7761 );
7762 });
7763
7764 _ = editor.update(cx, |editor, window, cx| {
7765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7766 s.select_display_ranges([
7767 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7768 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7769 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7770 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7771 ])
7772 });
7773 editor.split_selection_into_lines(&Default::default(), window, cx);
7774 assert_eq!(
7775 editor.display_text(cx),
7776 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7777 );
7778 });
7779 EditorTestContext::for_editor(editor, cx)
7780 .await
7781 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7782
7783 _ = editor.update(cx, |editor, window, cx| {
7784 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7785 s.select_display_ranges([
7786 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7787 ])
7788 });
7789 editor.split_selection_into_lines(&Default::default(), window, cx);
7790 assert_eq!(
7791 editor.display_text(cx),
7792 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7793 );
7794 assert_eq!(
7795 display_ranges(editor, cx),
7796 [
7797 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7798 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7799 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7800 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7801 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7802 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7803 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7804 ]
7805 );
7806 });
7807 EditorTestContext::for_editor(editor, cx)
7808 .await
7809 .assert_editor_state(
7810 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7811 );
7812}
7813
7814#[gpui::test]
7815async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7816 init_test(cx, |_| {});
7817
7818 let mut cx = EditorTestContext::new(cx).await;
7819
7820 cx.set_state(indoc!(
7821 r#"abc
7822 defˇghi
7823
7824 jk
7825 nlmo
7826 "#
7827 ));
7828
7829 cx.update_editor(|editor, window, cx| {
7830 editor.add_selection_above(&Default::default(), window, cx);
7831 });
7832
7833 cx.assert_editor_state(indoc!(
7834 r#"abcˇ
7835 defˇghi
7836
7837 jk
7838 nlmo
7839 "#
7840 ));
7841
7842 cx.update_editor(|editor, window, cx| {
7843 editor.add_selection_above(&Default::default(), window, cx);
7844 });
7845
7846 cx.assert_editor_state(indoc!(
7847 r#"abcˇ
7848 defˇghi
7849
7850 jk
7851 nlmo
7852 "#
7853 ));
7854
7855 cx.update_editor(|editor, window, cx| {
7856 editor.add_selection_below(&Default::default(), window, cx);
7857 });
7858
7859 cx.assert_editor_state(indoc!(
7860 r#"abc
7861 defˇghi
7862
7863 jk
7864 nlmo
7865 "#
7866 ));
7867
7868 cx.update_editor(|editor, window, cx| {
7869 editor.undo_selection(&Default::default(), window, cx);
7870 });
7871
7872 cx.assert_editor_state(indoc!(
7873 r#"abcˇ
7874 defˇghi
7875
7876 jk
7877 nlmo
7878 "#
7879 ));
7880
7881 cx.update_editor(|editor, window, cx| {
7882 editor.redo_selection(&Default::default(), window, cx);
7883 });
7884
7885 cx.assert_editor_state(indoc!(
7886 r#"abc
7887 defˇghi
7888
7889 jk
7890 nlmo
7891 "#
7892 ));
7893
7894 cx.update_editor(|editor, window, cx| {
7895 editor.add_selection_below(&Default::default(), window, cx);
7896 });
7897
7898 cx.assert_editor_state(indoc!(
7899 r#"abc
7900 defˇghi
7901 ˇ
7902 jk
7903 nlmo
7904 "#
7905 ));
7906
7907 cx.update_editor(|editor, window, cx| {
7908 editor.add_selection_below(&Default::default(), window, cx);
7909 });
7910
7911 cx.assert_editor_state(indoc!(
7912 r#"abc
7913 defˇghi
7914 ˇ
7915 jkˇ
7916 nlmo
7917 "#
7918 ));
7919
7920 cx.update_editor(|editor, window, cx| {
7921 editor.add_selection_below(&Default::default(), window, cx);
7922 });
7923
7924 cx.assert_editor_state(indoc!(
7925 r#"abc
7926 defˇghi
7927 ˇ
7928 jkˇ
7929 nlmˇo
7930 "#
7931 ));
7932
7933 cx.update_editor(|editor, window, cx| {
7934 editor.add_selection_below(&Default::default(), window, cx);
7935 });
7936
7937 cx.assert_editor_state(indoc!(
7938 r#"abc
7939 defˇghi
7940 ˇ
7941 jkˇ
7942 nlmˇo
7943 ˇ"#
7944 ));
7945
7946 // change selections
7947 cx.set_state(indoc!(
7948 r#"abc
7949 def«ˇg»hi
7950
7951 jk
7952 nlmo
7953 "#
7954 ));
7955
7956 cx.update_editor(|editor, window, cx| {
7957 editor.add_selection_below(&Default::default(), window, cx);
7958 });
7959
7960 cx.assert_editor_state(indoc!(
7961 r#"abc
7962 def«ˇg»hi
7963
7964 jk
7965 nlm«ˇo»
7966 "#
7967 ));
7968
7969 cx.update_editor(|editor, window, cx| {
7970 editor.add_selection_below(&Default::default(), window, cx);
7971 });
7972
7973 cx.assert_editor_state(indoc!(
7974 r#"abc
7975 def«ˇg»hi
7976
7977 jk
7978 nlm«ˇo»
7979 "#
7980 ));
7981
7982 cx.update_editor(|editor, window, cx| {
7983 editor.add_selection_above(&Default::default(), window, cx);
7984 });
7985
7986 cx.assert_editor_state(indoc!(
7987 r#"abc
7988 def«ˇg»hi
7989
7990 jk
7991 nlmo
7992 "#
7993 ));
7994
7995 cx.update_editor(|editor, window, cx| {
7996 editor.add_selection_above(&Default::default(), window, cx);
7997 });
7998
7999 cx.assert_editor_state(indoc!(
8000 r#"abc
8001 def«ˇg»hi
8002
8003 jk
8004 nlmo
8005 "#
8006 ));
8007
8008 // Change selections again
8009 cx.set_state(indoc!(
8010 r#"a«bc
8011 defgˇ»hi
8012
8013 jk
8014 nlmo
8015 "#
8016 ));
8017
8018 cx.update_editor(|editor, window, cx| {
8019 editor.add_selection_below(&Default::default(), window, cx);
8020 });
8021
8022 cx.assert_editor_state(indoc!(
8023 r#"a«bcˇ»
8024 d«efgˇ»hi
8025
8026 j«kˇ»
8027 nlmo
8028 "#
8029 ));
8030
8031 cx.update_editor(|editor, window, cx| {
8032 editor.add_selection_below(&Default::default(), window, cx);
8033 });
8034 cx.assert_editor_state(indoc!(
8035 r#"a«bcˇ»
8036 d«efgˇ»hi
8037
8038 j«kˇ»
8039 n«lmoˇ»
8040 "#
8041 ));
8042 cx.update_editor(|editor, window, cx| {
8043 editor.add_selection_above(&Default::default(), window, cx);
8044 });
8045
8046 cx.assert_editor_state(indoc!(
8047 r#"a«bcˇ»
8048 d«efgˇ»hi
8049
8050 j«kˇ»
8051 nlmo
8052 "#
8053 ));
8054
8055 // Change selections again
8056 cx.set_state(indoc!(
8057 r#"abc
8058 d«ˇefghi
8059
8060 jk
8061 nlm»o
8062 "#
8063 ));
8064
8065 cx.update_editor(|editor, window, cx| {
8066 editor.add_selection_above(&Default::default(), window, cx);
8067 });
8068
8069 cx.assert_editor_state(indoc!(
8070 r#"a«ˇbc»
8071 d«ˇef»ghi
8072
8073 j«ˇk»
8074 n«ˇlm»o
8075 "#
8076 ));
8077
8078 cx.update_editor(|editor, window, cx| {
8079 editor.add_selection_below(&Default::default(), window, cx);
8080 });
8081
8082 cx.assert_editor_state(indoc!(
8083 r#"abc
8084 d«ˇef»ghi
8085
8086 j«ˇk»
8087 n«ˇlm»o
8088 "#
8089 ));
8090}
8091
8092#[gpui::test]
8093async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8094 init_test(cx, |_| {});
8095 let mut cx = EditorTestContext::new(cx).await;
8096
8097 cx.set_state(indoc!(
8098 r#"line onˇe
8099 liˇne two
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.add_selection_below(&Default::default(), window, cx);
8106 });
8107
8108 // test multiple cursors expand in the same direction
8109 cx.assert_editor_state(indoc!(
8110 r#"line onˇe
8111 liˇne twˇo
8112 liˇne three
8113 line four"#
8114 ));
8115
8116 cx.update_editor(|editor, window, cx| {
8117 editor.add_selection_below(&Default::default(), window, cx);
8118 });
8119
8120 cx.update_editor(|editor, window, cx| {
8121 editor.add_selection_below(&Default::default(), window, cx);
8122 });
8123
8124 // test multiple cursors expand below overflow
8125 cx.assert_editor_state(indoc!(
8126 r#"line onˇe
8127 liˇne twˇo
8128 liˇne thˇree
8129 liˇne foˇur"#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_above(&Default::default(), window, cx);
8134 });
8135
8136 // test multiple cursors retrieves back correctly
8137 cx.assert_editor_state(indoc!(
8138 r#"line onˇe
8139 liˇne twˇo
8140 liˇne thˇree
8141 line four"#
8142 ));
8143
8144 cx.update_editor(|editor, window, cx| {
8145 editor.add_selection_above(&Default::default(), window, cx);
8146 });
8147
8148 cx.update_editor(|editor, window, cx| {
8149 editor.add_selection_above(&Default::default(), window, cx);
8150 });
8151
8152 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8153 cx.assert_editor_state(indoc!(
8154 r#"liˇne onˇe
8155 liˇne two
8156 line three
8157 line four"#
8158 ));
8159
8160 cx.update_editor(|editor, window, cx| {
8161 editor.undo_selection(&Default::default(), window, cx);
8162 });
8163
8164 // test undo
8165 cx.assert_editor_state(indoc!(
8166 r#"line onˇe
8167 liˇne twˇo
8168 line three
8169 line four"#
8170 ));
8171
8172 cx.update_editor(|editor, window, cx| {
8173 editor.redo_selection(&Default::default(), window, cx);
8174 });
8175
8176 // test redo
8177 cx.assert_editor_state(indoc!(
8178 r#"liˇne onˇe
8179 liˇne two
8180 line three
8181 line four"#
8182 ));
8183
8184 cx.set_state(indoc!(
8185 r#"abcd
8186 ef«ghˇ»
8187 ijkl
8188 «mˇ»nop"#
8189 ));
8190
8191 cx.update_editor(|editor, window, cx| {
8192 editor.add_selection_above(&Default::default(), window, cx);
8193 });
8194
8195 // test multiple selections expand in the same direction
8196 cx.assert_editor_state(indoc!(
8197 r#"ab«cdˇ»
8198 ef«ghˇ»
8199 «iˇ»jkl
8200 «mˇ»nop"#
8201 ));
8202
8203 cx.update_editor(|editor, window, cx| {
8204 editor.add_selection_above(&Default::default(), window, cx);
8205 });
8206
8207 // test multiple selection upward overflow
8208 cx.assert_editor_state(indoc!(
8209 r#"ab«cdˇ»
8210 «eˇ»f«ghˇ»
8211 «iˇ»jkl
8212 «mˇ»nop"#
8213 ));
8214
8215 cx.update_editor(|editor, window, cx| {
8216 editor.add_selection_below(&Default::default(), window, cx);
8217 });
8218
8219 // test multiple selection retrieves back correctly
8220 cx.assert_editor_state(indoc!(
8221 r#"abcd
8222 ef«ghˇ»
8223 «iˇ»jkl
8224 «mˇ»nop"#
8225 ));
8226
8227 cx.update_editor(|editor, window, cx| {
8228 editor.add_selection_below(&Default::default(), window, cx);
8229 });
8230
8231 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8232 cx.assert_editor_state(indoc!(
8233 r#"abcd
8234 ef«ghˇ»
8235 ij«klˇ»
8236 «mˇ»nop"#
8237 ));
8238
8239 cx.update_editor(|editor, window, cx| {
8240 editor.undo_selection(&Default::default(), window, cx);
8241 });
8242
8243 // test undo
8244 cx.assert_editor_state(indoc!(
8245 r#"abcd
8246 ef«ghˇ»
8247 «iˇ»jkl
8248 «mˇ»nop"#
8249 ));
8250
8251 cx.update_editor(|editor, window, cx| {
8252 editor.redo_selection(&Default::default(), window, cx);
8253 });
8254
8255 // test redo
8256 cx.assert_editor_state(indoc!(
8257 r#"abcd
8258 ef«ghˇ»
8259 ij«klˇ»
8260 «mˇ»nop"#
8261 ));
8262}
8263
8264#[gpui::test]
8265async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8266 init_test(cx, |_| {});
8267 let mut cx = EditorTestContext::new(cx).await;
8268
8269 cx.set_state(indoc!(
8270 r#"line onˇe
8271 liˇne two
8272 line three
8273 line four"#
8274 ));
8275
8276 cx.update_editor(|editor, window, cx| {
8277 editor.add_selection_below(&Default::default(), window, cx);
8278 editor.add_selection_below(&Default::default(), window, cx);
8279 editor.add_selection_below(&Default::default(), window, cx);
8280 });
8281
8282 // initial state with two multi cursor groups
8283 cx.assert_editor_state(indoc!(
8284 r#"line onˇe
8285 liˇne twˇo
8286 liˇne thˇree
8287 liˇne foˇur"#
8288 ));
8289
8290 // add single cursor in middle - simulate opt click
8291 cx.update_editor(|editor, window, cx| {
8292 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8293 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8294 editor.end_selection(window, cx);
8295 });
8296
8297 cx.assert_editor_state(indoc!(
8298 r#"line onˇe
8299 liˇne twˇo
8300 liˇneˇ thˇree
8301 liˇne foˇur"#
8302 ));
8303
8304 cx.update_editor(|editor, window, cx| {
8305 editor.add_selection_above(&Default::default(), window, cx);
8306 });
8307
8308 // test new added selection expands above and existing selection shrinks
8309 cx.assert_editor_state(indoc!(
8310 r#"line onˇe
8311 liˇneˇ twˇo
8312 liˇneˇ thˇree
8313 line four"#
8314 ));
8315
8316 cx.update_editor(|editor, window, cx| {
8317 editor.add_selection_above(&Default::default(), window, cx);
8318 });
8319
8320 // test new added selection expands above and existing selection shrinks
8321 cx.assert_editor_state(indoc!(
8322 r#"lineˇ onˇe
8323 liˇneˇ twˇo
8324 lineˇ three
8325 line four"#
8326 ));
8327
8328 // intial state with two selection groups
8329 cx.set_state(indoc!(
8330 r#"abcd
8331 ef«ghˇ»
8332 ijkl
8333 «mˇ»nop"#
8334 ));
8335
8336 cx.update_editor(|editor, window, cx| {
8337 editor.add_selection_above(&Default::default(), window, cx);
8338 editor.add_selection_above(&Default::default(), window, cx);
8339 });
8340
8341 cx.assert_editor_state(indoc!(
8342 r#"ab«cdˇ»
8343 «eˇ»f«ghˇ»
8344 «iˇ»jkl
8345 «mˇ»nop"#
8346 ));
8347
8348 // add single selection in middle - simulate opt drag
8349 cx.update_editor(|editor, window, cx| {
8350 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8351 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8352 editor.update_selection(
8353 DisplayPoint::new(DisplayRow(2), 4),
8354 0,
8355 gpui::Point::<f32>::default(),
8356 window,
8357 cx,
8358 );
8359 editor.end_selection(window, cx);
8360 });
8361
8362 cx.assert_editor_state(indoc!(
8363 r#"ab«cdˇ»
8364 «eˇ»f«ghˇ»
8365 «iˇ»jk«lˇ»
8366 «mˇ»nop"#
8367 ));
8368
8369 cx.update_editor(|editor, window, cx| {
8370 editor.add_selection_below(&Default::default(), window, cx);
8371 });
8372
8373 // test new added selection expands below, others shrinks from above
8374 cx.assert_editor_state(indoc!(
8375 r#"abcd
8376 ef«ghˇ»
8377 «iˇ»jk«lˇ»
8378 «mˇ»no«pˇ»"#
8379 ));
8380}
8381
8382#[gpui::test]
8383async fn test_select_next(cx: &mut TestAppContext) {
8384 init_test(cx, |_| {});
8385 let mut cx = EditorTestContext::new(cx).await;
8386
8387 // Enable case sensitive search.
8388 update_test_editor_settings(&mut cx, |settings| {
8389 let mut search_settings = SearchSettingsContent::default();
8390 search_settings.case_sensitive = Some(true);
8391 settings.search = Some(search_settings);
8392 });
8393
8394 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8395
8396 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8397 .unwrap();
8398 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8399
8400 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8401 .unwrap();
8402 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8403
8404 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8405 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8406
8407 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8408 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8409
8410 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8411 .unwrap();
8412 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8413
8414 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8415 .unwrap();
8416 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8417
8418 // Test selection direction should be preserved
8419 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8420
8421 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8422 .unwrap();
8423 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8424
8425 // Test case sensitivity
8426 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8427 cx.update_editor(|e, window, cx| {
8428 e.select_next(&SelectNext::default(), window, cx).unwrap();
8429 });
8430 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8431
8432 // Disable case sensitive search.
8433 update_test_editor_settings(&mut cx, |settings| {
8434 let mut search_settings = SearchSettingsContent::default();
8435 search_settings.case_sensitive = Some(false);
8436 settings.search = Some(search_settings);
8437 });
8438
8439 cx.set_state("«ˇfoo»\nFOO\nFoo");
8440 cx.update_editor(|e, window, cx| {
8441 e.select_next(&SelectNext::default(), window, cx).unwrap();
8442 e.select_next(&SelectNext::default(), window, cx).unwrap();
8443 });
8444 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8445}
8446
8447#[gpui::test]
8448async fn test_select_all_matches(cx: &mut TestAppContext) {
8449 init_test(cx, |_| {});
8450 let mut cx = EditorTestContext::new(cx).await;
8451
8452 // Enable case sensitive search.
8453 update_test_editor_settings(&mut cx, |settings| {
8454 let mut search_settings = SearchSettingsContent::default();
8455 search_settings.case_sensitive = Some(true);
8456 settings.search = Some(search_settings);
8457 });
8458
8459 // Test caret-only selections
8460 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8461 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8462 .unwrap();
8463 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8464
8465 // Test left-to-right selections
8466 cx.set_state("abc\n«abcˇ»\nabc");
8467 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8468 .unwrap();
8469 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8470
8471 // Test right-to-left selections
8472 cx.set_state("abc\n«ˇabc»\nabc");
8473 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8474 .unwrap();
8475 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8476
8477 // Test selecting whitespace with caret selection
8478 cx.set_state("abc\nˇ abc\nabc");
8479 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8480 .unwrap();
8481 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8482
8483 // Test selecting whitespace with left-to-right selection
8484 cx.set_state("abc\n«ˇ »abc\nabc");
8485 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8486 .unwrap();
8487 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8488
8489 // Test no matches with right-to-left selection
8490 cx.set_state("abc\n« ˇ»abc\nabc");
8491 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8492 .unwrap();
8493 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8494
8495 // Test with a single word and clip_at_line_ends=true (#29823)
8496 cx.set_state("aˇbc");
8497 cx.update_editor(|e, window, cx| {
8498 e.set_clip_at_line_ends(true, cx);
8499 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8500 e.set_clip_at_line_ends(false, cx);
8501 });
8502 cx.assert_editor_state("«abcˇ»");
8503
8504 // Test case sensitivity
8505 cx.set_state("fˇoo\nFOO\nFoo");
8506 cx.update_editor(|e, window, cx| {
8507 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8508 });
8509 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8510
8511 // Disable case sensitive search.
8512 update_test_editor_settings(&mut cx, |settings| {
8513 let mut search_settings = SearchSettingsContent::default();
8514 search_settings.case_sensitive = Some(false);
8515 settings.search = Some(search_settings);
8516 });
8517
8518 cx.set_state("fˇoo\nFOO\nFoo");
8519 cx.update_editor(|e, window, cx| {
8520 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8521 });
8522 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8523}
8524
8525#[gpui::test]
8526async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8527 init_test(cx, |_| {});
8528
8529 let mut cx = EditorTestContext::new(cx).await;
8530
8531 let large_body_1 = "\nd".repeat(200);
8532 let large_body_2 = "\ne".repeat(200);
8533
8534 cx.set_state(&format!(
8535 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8536 ));
8537 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8538 let scroll_position = editor.scroll_position(cx);
8539 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8540 scroll_position
8541 });
8542
8543 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8544 .unwrap();
8545 cx.assert_editor_state(&format!(
8546 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8547 ));
8548 let scroll_position_after_selection =
8549 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8550 assert_eq!(
8551 initial_scroll_position, scroll_position_after_selection,
8552 "Scroll position should not change after selecting all matches"
8553 );
8554}
8555
8556#[gpui::test]
8557async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8558 init_test(cx, |_| {});
8559
8560 let mut cx = EditorLspTestContext::new_rust(
8561 lsp::ServerCapabilities {
8562 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8563 ..Default::default()
8564 },
8565 cx,
8566 )
8567 .await;
8568
8569 cx.set_state(indoc! {"
8570 line 1
8571 line 2
8572 linˇe 3
8573 line 4
8574 line 5
8575 "});
8576
8577 // Make an edit
8578 cx.update_editor(|editor, window, cx| {
8579 editor.handle_input("X", window, cx);
8580 });
8581
8582 // Move cursor to a different position
8583 cx.update_editor(|editor, window, cx| {
8584 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8585 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8586 });
8587 });
8588
8589 cx.assert_editor_state(indoc! {"
8590 line 1
8591 line 2
8592 linXe 3
8593 line 4
8594 liˇne 5
8595 "});
8596
8597 cx.lsp
8598 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8599 Ok(Some(vec![lsp::TextEdit::new(
8600 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8601 "PREFIX ".to_string(),
8602 )]))
8603 });
8604
8605 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8606 .unwrap()
8607 .await
8608 .unwrap();
8609
8610 cx.assert_editor_state(indoc! {"
8611 PREFIX line 1
8612 line 2
8613 linXe 3
8614 line 4
8615 liˇne 5
8616 "});
8617
8618 // Undo formatting
8619 cx.update_editor(|editor, window, cx| {
8620 editor.undo(&Default::default(), window, cx);
8621 });
8622
8623 // Verify cursor moved back to position after edit
8624 cx.assert_editor_state(indoc! {"
8625 line 1
8626 line 2
8627 linXˇe 3
8628 line 4
8629 line 5
8630 "});
8631}
8632
8633#[gpui::test]
8634async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8635 init_test(cx, |_| {});
8636
8637 let mut cx = EditorTestContext::new(cx).await;
8638
8639 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8640 cx.update_editor(|editor, window, cx| {
8641 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8642 });
8643
8644 cx.set_state(indoc! {"
8645 line 1
8646 line 2
8647 linˇe 3
8648 line 4
8649 line 5
8650 line 6
8651 line 7
8652 line 8
8653 line 9
8654 line 10
8655 "});
8656
8657 let snapshot = cx.buffer_snapshot();
8658 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8659
8660 cx.update(|_, cx| {
8661 provider.update(cx, |provider, _| {
8662 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8663 id: None,
8664 edits: vec![(edit_position..edit_position, "X".into())],
8665 edit_preview: None,
8666 }))
8667 })
8668 });
8669
8670 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8671 cx.update_editor(|editor, window, cx| {
8672 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8673 });
8674
8675 cx.assert_editor_state(indoc! {"
8676 line 1
8677 line 2
8678 lineXˇ 3
8679 line 4
8680 line 5
8681 line 6
8682 line 7
8683 line 8
8684 line 9
8685 line 10
8686 "});
8687
8688 cx.update_editor(|editor, window, cx| {
8689 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8690 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8691 });
8692 });
8693
8694 cx.assert_editor_state(indoc! {"
8695 line 1
8696 line 2
8697 lineX 3
8698 line 4
8699 line 5
8700 line 6
8701 line 7
8702 line 8
8703 line 9
8704 liˇne 10
8705 "});
8706
8707 cx.update_editor(|editor, window, cx| {
8708 editor.undo(&Default::default(), window, cx);
8709 });
8710
8711 cx.assert_editor_state(indoc! {"
8712 line 1
8713 line 2
8714 lineˇ 3
8715 line 4
8716 line 5
8717 line 6
8718 line 7
8719 line 8
8720 line 9
8721 line 10
8722 "});
8723}
8724
8725#[gpui::test]
8726async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8727 init_test(cx, |_| {});
8728
8729 let mut cx = EditorTestContext::new(cx).await;
8730 cx.set_state(
8731 r#"let foo = 2;
8732lˇet foo = 2;
8733let fooˇ = 2;
8734let foo = 2;
8735let foo = ˇ2;"#,
8736 );
8737
8738 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8739 .unwrap();
8740 cx.assert_editor_state(
8741 r#"let foo = 2;
8742«letˇ» foo = 2;
8743let «fooˇ» = 2;
8744let foo = 2;
8745let foo = «2ˇ»;"#,
8746 );
8747
8748 // noop for multiple selections with different contents
8749 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8750 .unwrap();
8751 cx.assert_editor_state(
8752 r#"let foo = 2;
8753«letˇ» foo = 2;
8754let «fooˇ» = 2;
8755let foo = 2;
8756let foo = «2ˇ»;"#,
8757 );
8758
8759 // Test last selection direction should be preserved
8760 cx.set_state(
8761 r#"let foo = 2;
8762let foo = 2;
8763let «fooˇ» = 2;
8764let «ˇfoo» = 2;
8765let foo = 2;"#,
8766 );
8767
8768 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8769 .unwrap();
8770 cx.assert_editor_state(
8771 r#"let foo = 2;
8772let foo = 2;
8773let «fooˇ» = 2;
8774let «ˇfoo» = 2;
8775let «ˇfoo» = 2;"#,
8776 );
8777}
8778
8779#[gpui::test]
8780async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8781 init_test(cx, |_| {});
8782
8783 let mut cx =
8784 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8785
8786 cx.assert_editor_state(indoc! {"
8787 ˇbbb
8788 ccc
8789
8790 bbb
8791 ccc
8792 "});
8793 cx.dispatch_action(SelectPrevious::default());
8794 cx.assert_editor_state(indoc! {"
8795 «bbbˇ»
8796 ccc
8797
8798 bbb
8799 ccc
8800 "});
8801 cx.dispatch_action(SelectPrevious::default());
8802 cx.assert_editor_state(indoc! {"
8803 «bbbˇ»
8804 ccc
8805
8806 «bbbˇ»
8807 ccc
8808 "});
8809}
8810
8811#[gpui::test]
8812async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8813 init_test(cx, |_| {});
8814
8815 let mut cx = EditorTestContext::new(cx).await;
8816 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8817
8818 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8819 .unwrap();
8820 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8821
8822 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8823 .unwrap();
8824 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8825
8826 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8827 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8828
8829 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8830 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8831
8832 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8833 .unwrap();
8834 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8835
8836 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8837 .unwrap();
8838 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8839}
8840
8841#[gpui::test]
8842async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8843 init_test(cx, |_| {});
8844
8845 let mut cx = EditorTestContext::new(cx).await;
8846 cx.set_state("aˇ");
8847
8848 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8849 .unwrap();
8850 cx.assert_editor_state("«aˇ»");
8851 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8852 .unwrap();
8853 cx.assert_editor_state("«aˇ»");
8854}
8855
8856#[gpui::test]
8857async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8858 init_test(cx, |_| {});
8859
8860 let mut cx = EditorTestContext::new(cx).await;
8861 cx.set_state(
8862 r#"let foo = 2;
8863lˇet foo = 2;
8864let fooˇ = 2;
8865let foo = 2;
8866let foo = ˇ2;"#,
8867 );
8868
8869 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8870 .unwrap();
8871 cx.assert_editor_state(
8872 r#"let foo = 2;
8873«letˇ» foo = 2;
8874let «fooˇ» = 2;
8875let foo = 2;
8876let foo = «2ˇ»;"#,
8877 );
8878
8879 // noop for multiple selections with different contents
8880 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8881 .unwrap();
8882 cx.assert_editor_state(
8883 r#"let foo = 2;
8884«letˇ» foo = 2;
8885let «fooˇ» = 2;
8886let foo = 2;
8887let foo = «2ˇ»;"#,
8888 );
8889}
8890
8891#[gpui::test]
8892async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8893 init_test(cx, |_| {});
8894 let mut cx = EditorTestContext::new(cx).await;
8895
8896 // Enable case sensitive search.
8897 update_test_editor_settings(&mut cx, |settings| {
8898 let mut search_settings = SearchSettingsContent::default();
8899 search_settings.case_sensitive = Some(true);
8900 settings.search = Some(search_settings);
8901 });
8902
8903 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8904
8905 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8906 .unwrap();
8907 // selection direction is preserved
8908 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8909
8910 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8911 .unwrap();
8912 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8913
8914 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8915 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8916
8917 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8918 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8919
8920 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8921 .unwrap();
8922 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8923
8924 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8925 .unwrap();
8926 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8927
8928 // Test case sensitivity
8929 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
8930 cx.update_editor(|e, window, cx| {
8931 e.select_previous(&SelectPrevious::default(), window, cx)
8932 .unwrap();
8933 e.select_previous(&SelectPrevious::default(), window, cx)
8934 .unwrap();
8935 });
8936 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8937
8938 // Disable case sensitive search.
8939 update_test_editor_settings(&mut cx, |settings| {
8940 let mut search_settings = SearchSettingsContent::default();
8941 search_settings.case_sensitive = Some(false);
8942 settings.search = Some(search_settings);
8943 });
8944
8945 cx.set_state("foo\nFOO\n«ˇFoo»");
8946 cx.update_editor(|e, window, cx| {
8947 e.select_previous(&SelectPrevious::default(), window, cx)
8948 .unwrap();
8949 e.select_previous(&SelectPrevious::default(), window, cx)
8950 .unwrap();
8951 });
8952 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8953}
8954
8955#[gpui::test]
8956async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8957 init_test(cx, |_| {});
8958
8959 let language = Arc::new(Language::new(
8960 LanguageConfig::default(),
8961 Some(tree_sitter_rust::LANGUAGE.into()),
8962 ));
8963
8964 let text = r#"
8965 use mod1::mod2::{mod3, mod4};
8966
8967 fn fn_1(param1: bool, param2: &str) {
8968 let var1 = "text";
8969 }
8970 "#
8971 .unindent();
8972
8973 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8974 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8975 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8976
8977 editor
8978 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8979 .await;
8980
8981 editor.update_in(cx, |editor, window, cx| {
8982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8983 s.select_display_ranges([
8984 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8985 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8986 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8987 ]);
8988 });
8989 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8990 });
8991 editor.update(cx, |editor, cx| {
8992 assert_text_with_selections(
8993 editor,
8994 indoc! {r#"
8995 use mod1::mod2::{mod3, «mod4ˇ»};
8996
8997 fn fn_1«ˇ(param1: bool, param2: &str)» {
8998 let var1 = "«ˇtext»";
8999 }
9000 "#},
9001 cx,
9002 );
9003 });
9004
9005 editor.update_in(cx, |editor, window, cx| {
9006 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9007 });
9008 editor.update(cx, |editor, cx| {
9009 assert_text_with_selections(
9010 editor,
9011 indoc! {r#"
9012 use mod1::mod2::«{mod3, mod4}ˇ»;
9013
9014 «ˇfn fn_1(param1: bool, param2: &str) {
9015 let var1 = "text";
9016 }»
9017 "#},
9018 cx,
9019 );
9020 });
9021
9022 editor.update_in(cx, |editor, window, cx| {
9023 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9024 });
9025 assert_eq!(
9026 editor.update(cx, |editor, cx| editor
9027 .selections
9028 .display_ranges(&editor.display_snapshot(cx))),
9029 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9030 );
9031
9032 // Trying to expand the selected syntax node one more time has no effect.
9033 editor.update_in(cx, |editor, window, cx| {
9034 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9035 });
9036 assert_eq!(
9037 editor.update(cx, |editor, cx| editor
9038 .selections
9039 .display_ranges(&editor.display_snapshot(cx))),
9040 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9041 );
9042
9043 editor.update_in(cx, |editor, window, cx| {
9044 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9045 });
9046 editor.update(cx, |editor, cx| {
9047 assert_text_with_selections(
9048 editor,
9049 indoc! {r#"
9050 use mod1::mod2::«{mod3, mod4}ˇ»;
9051
9052 «ˇfn fn_1(param1: bool, param2: &str) {
9053 let var1 = "text";
9054 }»
9055 "#},
9056 cx,
9057 );
9058 });
9059
9060 editor.update_in(cx, |editor, window, cx| {
9061 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9062 });
9063 editor.update(cx, |editor, cx| {
9064 assert_text_with_selections(
9065 editor,
9066 indoc! {r#"
9067 use mod1::mod2::{mod3, «mod4ˇ»};
9068
9069 fn fn_1«ˇ(param1: bool, param2: &str)» {
9070 let var1 = "«ˇtext»";
9071 }
9072 "#},
9073 cx,
9074 );
9075 });
9076
9077 editor.update_in(cx, |editor, window, cx| {
9078 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9079 });
9080 editor.update(cx, |editor, cx| {
9081 assert_text_with_selections(
9082 editor,
9083 indoc! {r#"
9084 use mod1::mod2::{mod3, moˇd4};
9085
9086 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9087 let var1 = "teˇxt";
9088 }
9089 "#},
9090 cx,
9091 );
9092 });
9093
9094 // Trying to shrink the selected syntax node one more time has no effect.
9095 editor.update_in(cx, |editor, window, cx| {
9096 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9097 });
9098 editor.update_in(cx, |editor, _, cx| {
9099 assert_text_with_selections(
9100 editor,
9101 indoc! {r#"
9102 use mod1::mod2::{mod3, moˇd4};
9103
9104 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9105 let var1 = "teˇxt";
9106 }
9107 "#},
9108 cx,
9109 );
9110 });
9111
9112 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9113 // a fold.
9114 editor.update_in(cx, |editor, window, cx| {
9115 editor.fold_creases(
9116 vec![
9117 Crease::simple(
9118 Point::new(0, 21)..Point::new(0, 24),
9119 FoldPlaceholder::test(),
9120 ),
9121 Crease::simple(
9122 Point::new(3, 20)..Point::new(3, 22),
9123 FoldPlaceholder::test(),
9124 ),
9125 ],
9126 true,
9127 window,
9128 cx,
9129 );
9130 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9131 });
9132 editor.update(cx, |editor, cx| {
9133 assert_text_with_selections(
9134 editor,
9135 indoc! {r#"
9136 use mod1::mod2::«{mod3, mod4}ˇ»;
9137
9138 fn fn_1«ˇ(param1: bool, param2: &str)» {
9139 let var1 = "«ˇtext»";
9140 }
9141 "#},
9142 cx,
9143 );
9144 });
9145}
9146
9147#[gpui::test]
9148async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9149 init_test(cx, |_| {});
9150
9151 let language = Arc::new(Language::new(
9152 LanguageConfig::default(),
9153 Some(tree_sitter_rust::LANGUAGE.into()),
9154 ));
9155
9156 let text = "let a = 2;";
9157
9158 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9159 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9160 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9161
9162 editor
9163 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9164 .await;
9165
9166 // Test case 1: Cursor at end of word
9167 editor.update_in(cx, |editor, window, cx| {
9168 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9169 s.select_display_ranges([
9170 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9171 ]);
9172 });
9173 });
9174 editor.update(cx, |editor, cx| {
9175 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9176 });
9177 editor.update_in(cx, |editor, window, cx| {
9178 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9179 });
9180 editor.update(cx, |editor, cx| {
9181 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9182 });
9183 editor.update_in(cx, |editor, window, cx| {
9184 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9185 });
9186 editor.update(cx, |editor, cx| {
9187 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9188 });
9189
9190 // Test case 2: Cursor at end of statement
9191 editor.update_in(cx, |editor, window, cx| {
9192 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9193 s.select_display_ranges([
9194 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9195 ]);
9196 });
9197 });
9198 editor.update(cx, |editor, cx| {
9199 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9200 });
9201 editor.update_in(cx, |editor, window, cx| {
9202 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9203 });
9204 editor.update(cx, |editor, cx| {
9205 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9206 });
9207}
9208
9209#[gpui::test]
9210async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9211 init_test(cx, |_| {});
9212
9213 let language = Arc::new(Language::new(
9214 LanguageConfig {
9215 name: "JavaScript".into(),
9216 ..Default::default()
9217 },
9218 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9219 ));
9220
9221 let text = r#"
9222 let a = {
9223 key: "value",
9224 };
9225 "#
9226 .unindent();
9227
9228 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9229 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9230 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9231
9232 editor
9233 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9234 .await;
9235
9236 // Test case 1: Cursor after '{'
9237 editor.update_in(cx, |editor, window, cx| {
9238 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9239 s.select_display_ranges([
9240 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9241 ]);
9242 });
9243 });
9244 editor.update(cx, |editor, cx| {
9245 assert_text_with_selections(
9246 editor,
9247 indoc! {r#"
9248 let a = {ˇ
9249 key: "value",
9250 };
9251 "#},
9252 cx,
9253 );
9254 });
9255 editor.update_in(cx, |editor, window, cx| {
9256 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9257 });
9258 editor.update(cx, |editor, cx| {
9259 assert_text_with_selections(
9260 editor,
9261 indoc! {r#"
9262 let a = «ˇ{
9263 key: "value",
9264 }»;
9265 "#},
9266 cx,
9267 );
9268 });
9269
9270 // Test case 2: Cursor after ':'
9271 editor.update_in(cx, |editor, window, cx| {
9272 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9273 s.select_display_ranges([
9274 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9275 ]);
9276 });
9277 });
9278 editor.update(cx, |editor, cx| {
9279 assert_text_with_selections(
9280 editor,
9281 indoc! {r#"
9282 let a = {
9283 key:ˇ "value",
9284 };
9285 "#},
9286 cx,
9287 );
9288 });
9289 editor.update_in(cx, |editor, window, cx| {
9290 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9291 });
9292 editor.update(cx, |editor, cx| {
9293 assert_text_with_selections(
9294 editor,
9295 indoc! {r#"
9296 let a = {
9297 «ˇkey: "value"»,
9298 };
9299 "#},
9300 cx,
9301 );
9302 });
9303 editor.update_in(cx, |editor, window, cx| {
9304 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9305 });
9306 editor.update(cx, |editor, cx| {
9307 assert_text_with_selections(
9308 editor,
9309 indoc! {r#"
9310 let a = «ˇ{
9311 key: "value",
9312 }»;
9313 "#},
9314 cx,
9315 );
9316 });
9317
9318 // Test case 3: Cursor after ','
9319 editor.update_in(cx, |editor, window, cx| {
9320 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9321 s.select_display_ranges([
9322 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9323 ]);
9324 });
9325 });
9326 editor.update(cx, |editor, cx| {
9327 assert_text_with_selections(
9328 editor,
9329 indoc! {r#"
9330 let a = {
9331 key: "value",ˇ
9332 };
9333 "#},
9334 cx,
9335 );
9336 });
9337 editor.update_in(cx, |editor, window, cx| {
9338 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9339 });
9340 editor.update(cx, |editor, cx| {
9341 assert_text_with_selections(
9342 editor,
9343 indoc! {r#"
9344 let a = «ˇ{
9345 key: "value",
9346 }»;
9347 "#},
9348 cx,
9349 );
9350 });
9351
9352 // Test case 4: Cursor after ';'
9353 editor.update_in(cx, |editor, window, cx| {
9354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9355 s.select_display_ranges([
9356 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9357 ]);
9358 });
9359 });
9360 editor.update(cx, |editor, cx| {
9361 assert_text_with_selections(
9362 editor,
9363 indoc! {r#"
9364 let a = {
9365 key: "value",
9366 };ˇ
9367 "#},
9368 cx,
9369 );
9370 });
9371 editor.update_in(cx, |editor, window, cx| {
9372 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9373 });
9374 editor.update(cx, |editor, cx| {
9375 assert_text_with_selections(
9376 editor,
9377 indoc! {r#"
9378 «ˇlet a = {
9379 key: "value",
9380 };
9381 »"#},
9382 cx,
9383 );
9384 });
9385}
9386
9387#[gpui::test]
9388async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9389 init_test(cx, |_| {});
9390
9391 let language = Arc::new(Language::new(
9392 LanguageConfig::default(),
9393 Some(tree_sitter_rust::LANGUAGE.into()),
9394 ));
9395
9396 let text = r#"
9397 use mod1::mod2::{mod3, mod4};
9398
9399 fn fn_1(param1: bool, param2: &str) {
9400 let var1 = "hello world";
9401 }
9402 "#
9403 .unindent();
9404
9405 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9406 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9407 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9408
9409 editor
9410 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9411 .await;
9412
9413 // Test 1: Cursor on a letter of a string word
9414 editor.update_in(cx, |editor, window, cx| {
9415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9416 s.select_display_ranges([
9417 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9418 ]);
9419 });
9420 });
9421 editor.update_in(cx, |editor, window, cx| {
9422 assert_text_with_selections(
9423 editor,
9424 indoc! {r#"
9425 use mod1::mod2::{mod3, mod4};
9426
9427 fn fn_1(param1: bool, param2: &str) {
9428 let var1 = "hˇello world";
9429 }
9430 "#},
9431 cx,
9432 );
9433 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9434 assert_text_with_selections(
9435 editor,
9436 indoc! {r#"
9437 use mod1::mod2::{mod3, mod4};
9438
9439 fn fn_1(param1: bool, param2: &str) {
9440 let var1 = "«ˇhello» world";
9441 }
9442 "#},
9443 cx,
9444 );
9445 });
9446
9447 // Test 2: Partial selection within a word
9448 editor.update_in(cx, |editor, window, cx| {
9449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9450 s.select_display_ranges([
9451 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9452 ]);
9453 });
9454 });
9455 editor.update_in(cx, |editor, window, cx| {
9456 assert_text_with_selections(
9457 editor,
9458 indoc! {r#"
9459 use mod1::mod2::{mod3, mod4};
9460
9461 fn fn_1(param1: bool, param2: &str) {
9462 let var1 = "h«elˇ»lo world";
9463 }
9464 "#},
9465 cx,
9466 );
9467 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9468 assert_text_with_selections(
9469 editor,
9470 indoc! {r#"
9471 use mod1::mod2::{mod3, mod4};
9472
9473 fn fn_1(param1: bool, param2: &str) {
9474 let var1 = "«ˇhello» world";
9475 }
9476 "#},
9477 cx,
9478 );
9479 });
9480
9481 // Test 3: Complete word already selected
9482 editor.update_in(cx, |editor, window, cx| {
9483 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9484 s.select_display_ranges([
9485 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9486 ]);
9487 });
9488 });
9489 editor.update_in(cx, |editor, window, cx| {
9490 assert_text_with_selections(
9491 editor,
9492 indoc! {r#"
9493 use mod1::mod2::{mod3, mod4};
9494
9495 fn fn_1(param1: bool, param2: &str) {
9496 let var1 = "«helloˇ» world";
9497 }
9498 "#},
9499 cx,
9500 );
9501 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9502 assert_text_with_selections(
9503 editor,
9504 indoc! {r#"
9505 use mod1::mod2::{mod3, mod4};
9506
9507 fn fn_1(param1: bool, param2: &str) {
9508 let var1 = "«hello worldˇ»";
9509 }
9510 "#},
9511 cx,
9512 );
9513 });
9514
9515 // Test 4: Selection spanning across words
9516 editor.update_in(cx, |editor, window, cx| {
9517 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9518 s.select_display_ranges([
9519 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9520 ]);
9521 });
9522 });
9523 editor.update_in(cx, |editor, window, cx| {
9524 assert_text_with_selections(
9525 editor,
9526 indoc! {r#"
9527 use mod1::mod2::{mod3, mod4};
9528
9529 fn fn_1(param1: bool, param2: &str) {
9530 let var1 = "hel«lo woˇ»rld";
9531 }
9532 "#},
9533 cx,
9534 );
9535 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9536 assert_text_with_selections(
9537 editor,
9538 indoc! {r#"
9539 use mod1::mod2::{mod3, mod4};
9540
9541 fn fn_1(param1: bool, param2: &str) {
9542 let var1 = "«ˇhello world»";
9543 }
9544 "#},
9545 cx,
9546 );
9547 });
9548
9549 // Test 5: Expansion beyond string
9550 editor.update_in(cx, |editor, window, cx| {
9551 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9552 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9553 assert_text_with_selections(
9554 editor,
9555 indoc! {r#"
9556 use mod1::mod2::{mod3, mod4};
9557
9558 fn fn_1(param1: bool, param2: &str) {
9559 «ˇlet var1 = "hello world";»
9560 }
9561 "#},
9562 cx,
9563 );
9564 });
9565}
9566
9567#[gpui::test]
9568async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9569 init_test(cx, |_| {});
9570
9571 let mut cx = EditorTestContext::new(cx).await;
9572
9573 let language = Arc::new(Language::new(
9574 LanguageConfig::default(),
9575 Some(tree_sitter_rust::LANGUAGE.into()),
9576 ));
9577
9578 cx.update_buffer(|buffer, cx| {
9579 buffer.set_language(Some(language), cx);
9580 });
9581
9582 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9583 cx.update_editor(|editor, window, cx| {
9584 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9585 });
9586
9587 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9588
9589 cx.set_state(indoc! { r#"fn a() {
9590 // what
9591 // a
9592 // ˇlong
9593 // method
9594 // I
9595 // sure
9596 // hope
9597 // it
9598 // works
9599 }"# });
9600
9601 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9602 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9603 cx.update(|_, cx| {
9604 multi_buffer.update(cx, |multi_buffer, cx| {
9605 multi_buffer.set_excerpts_for_path(
9606 PathKey::for_buffer(&buffer, cx),
9607 buffer,
9608 [Point::new(1, 0)..Point::new(1, 0)],
9609 3,
9610 cx,
9611 );
9612 });
9613 });
9614
9615 let editor2 = cx.new_window_entity(|window, cx| {
9616 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9617 });
9618
9619 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9620 cx.update_editor(|editor, window, cx| {
9621 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9622 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9623 })
9624 });
9625
9626 cx.assert_editor_state(indoc! { "
9627 fn a() {
9628 // what
9629 // a
9630 ˇ // long
9631 // method"});
9632
9633 cx.update_editor(|editor, window, cx| {
9634 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9635 });
9636
9637 // Although we could potentially make the action work when the syntax node
9638 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9639 // did. Maybe we could also expand the excerpt to contain the range?
9640 cx.assert_editor_state(indoc! { "
9641 fn a() {
9642 // what
9643 // a
9644 ˇ // long
9645 // method"});
9646}
9647
9648#[gpui::test]
9649async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9650 init_test(cx, |_| {});
9651
9652 let base_text = r#"
9653 impl A {
9654 // this is an uncommitted comment
9655
9656 fn b() {
9657 c();
9658 }
9659
9660 // this is another uncommitted comment
9661
9662 fn d() {
9663 // e
9664 // f
9665 }
9666 }
9667
9668 fn g() {
9669 // h
9670 }
9671 "#
9672 .unindent();
9673
9674 let text = r#"
9675 ˇimpl A {
9676
9677 fn b() {
9678 c();
9679 }
9680
9681 fn d() {
9682 // e
9683 // f
9684 }
9685 }
9686
9687 fn g() {
9688 // h
9689 }
9690 "#
9691 .unindent();
9692
9693 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9694 cx.set_state(&text);
9695 cx.set_head_text(&base_text);
9696 cx.update_editor(|editor, window, cx| {
9697 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9698 });
9699
9700 cx.assert_state_with_diff(
9701 "
9702 ˇimpl A {
9703 - // this is an uncommitted comment
9704
9705 fn b() {
9706 c();
9707 }
9708
9709 - // this is another uncommitted comment
9710 -
9711 fn d() {
9712 // e
9713 // f
9714 }
9715 }
9716
9717 fn g() {
9718 // h
9719 }
9720 "
9721 .unindent(),
9722 );
9723
9724 let expected_display_text = "
9725 impl A {
9726 // this is an uncommitted comment
9727
9728 fn b() {
9729 ⋯
9730 }
9731
9732 // this is another uncommitted comment
9733
9734 fn d() {
9735 ⋯
9736 }
9737 }
9738
9739 fn g() {
9740 ⋯
9741 }
9742 "
9743 .unindent();
9744
9745 cx.update_editor(|editor, window, cx| {
9746 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9747 assert_eq!(editor.display_text(cx), expected_display_text);
9748 });
9749}
9750
9751#[gpui::test]
9752async fn test_autoindent(cx: &mut TestAppContext) {
9753 init_test(cx, |_| {});
9754
9755 let language = Arc::new(
9756 Language::new(
9757 LanguageConfig {
9758 brackets: BracketPairConfig {
9759 pairs: vec![
9760 BracketPair {
9761 start: "{".to_string(),
9762 end: "}".to_string(),
9763 close: false,
9764 surround: false,
9765 newline: true,
9766 },
9767 BracketPair {
9768 start: "(".to_string(),
9769 end: ")".to_string(),
9770 close: false,
9771 surround: false,
9772 newline: true,
9773 },
9774 ],
9775 ..Default::default()
9776 },
9777 ..Default::default()
9778 },
9779 Some(tree_sitter_rust::LANGUAGE.into()),
9780 )
9781 .with_indents_query(
9782 r#"
9783 (_ "(" ")" @end) @indent
9784 (_ "{" "}" @end) @indent
9785 "#,
9786 )
9787 .unwrap(),
9788 );
9789
9790 let text = "fn a() {}";
9791
9792 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9793 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9794 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9795 editor
9796 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9797 .await;
9798
9799 editor.update_in(cx, |editor, window, cx| {
9800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9801 s.select_ranges([
9802 MultiBufferOffset(5)..MultiBufferOffset(5),
9803 MultiBufferOffset(8)..MultiBufferOffset(8),
9804 MultiBufferOffset(9)..MultiBufferOffset(9),
9805 ])
9806 });
9807 editor.newline(&Newline, window, cx);
9808 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9809 assert_eq!(
9810 editor.selections.ranges(&editor.display_snapshot(cx)),
9811 &[
9812 Point::new(1, 4)..Point::new(1, 4),
9813 Point::new(3, 4)..Point::new(3, 4),
9814 Point::new(5, 0)..Point::new(5, 0)
9815 ]
9816 );
9817 });
9818}
9819
9820#[gpui::test]
9821async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9822 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9823
9824 let language = Arc::new(
9825 Language::new(
9826 LanguageConfig {
9827 brackets: BracketPairConfig {
9828 pairs: vec![
9829 BracketPair {
9830 start: "{".to_string(),
9831 end: "}".to_string(),
9832 close: false,
9833 surround: false,
9834 newline: true,
9835 },
9836 BracketPair {
9837 start: "(".to_string(),
9838 end: ")".to_string(),
9839 close: false,
9840 surround: false,
9841 newline: true,
9842 },
9843 ],
9844 ..Default::default()
9845 },
9846 ..Default::default()
9847 },
9848 Some(tree_sitter_rust::LANGUAGE.into()),
9849 )
9850 .with_indents_query(
9851 r#"
9852 (_ "(" ")" @end) @indent
9853 (_ "{" "}" @end) @indent
9854 "#,
9855 )
9856 .unwrap(),
9857 );
9858
9859 let text = "fn a() {}";
9860
9861 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9862 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9863 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9864 editor
9865 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9866 .await;
9867
9868 editor.update_in(cx, |editor, window, cx| {
9869 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9870 s.select_ranges([
9871 MultiBufferOffset(5)..MultiBufferOffset(5),
9872 MultiBufferOffset(8)..MultiBufferOffset(8),
9873 MultiBufferOffset(9)..MultiBufferOffset(9),
9874 ])
9875 });
9876 editor.newline(&Newline, window, cx);
9877 assert_eq!(
9878 editor.text(cx),
9879 indoc!(
9880 "
9881 fn a(
9882
9883 ) {
9884
9885 }
9886 "
9887 )
9888 );
9889 assert_eq!(
9890 editor.selections.ranges(&editor.display_snapshot(cx)),
9891 &[
9892 Point::new(1, 0)..Point::new(1, 0),
9893 Point::new(3, 0)..Point::new(3, 0),
9894 Point::new(5, 0)..Point::new(5, 0)
9895 ]
9896 );
9897 });
9898}
9899
9900#[gpui::test]
9901async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9902 init_test(cx, |settings| {
9903 settings.defaults.auto_indent = Some(true);
9904 settings.languages.0.insert(
9905 "python".into(),
9906 LanguageSettingsContent {
9907 auto_indent: Some(false),
9908 ..Default::default()
9909 },
9910 );
9911 });
9912
9913 let mut cx = EditorTestContext::new(cx).await;
9914
9915 let injected_language = Arc::new(
9916 Language::new(
9917 LanguageConfig {
9918 brackets: BracketPairConfig {
9919 pairs: vec![
9920 BracketPair {
9921 start: "{".to_string(),
9922 end: "}".to_string(),
9923 close: false,
9924 surround: false,
9925 newline: true,
9926 },
9927 BracketPair {
9928 start: "(".to_string(),
9929 end: ")".to_string(),
9930 close: true,
9931 surround: false,
9932 newline: true,
9933 },
9934 ],
9935 ..Default::default()
9936 },
9937 name: "python".into(),
9938 ..Default::default()
9939 },
9940 Some(tree_sitter_python::LANGUAGE.into()),
9941 )
9942 .with_indents_query(
9943 r#"
9944 (_ "(" ")" @end) @indent
9945 (_ "{" "}" @end) @indent
9946 "#,
9947 )
9948 .unwrap(),
9949 );
9950
9951 let language = Arc::new(
9952 Language::new(
9953 LanguageConfig {
9954 brackets: BracketPairConfig {
9955 pairs: vec![
9956 BracketPair {
9957 start: "{".to_string(),
9958 end: "}".to_string(),
9959 close: false,
9960 surround: false,
9961 newline: true,
9962 },
9963 BracketPair {
9964 start: "(".to_string(),
9965 end: ")".to_string(),
9966 close: true,
9967 surround: false,
9968 newline: true,
9969 },
9970 ],
9971 ..Default::default()
9972 },
9973 name: LanguageName::new("rust"),
9974 ..Default::default()
9975 },
9976 Some(tree_sitter_rust::LANGUAGE.into()),
9977 )
9978 .with_indents_query(
9979 r#"
9980 (_ "(" ")" @end) @indent
9981 (_ "{" "}" @end) @indent
9982 "#,
9983 )
9984 .unwrap()
9985 .with_injection_query(
9986 r#"
9987 (macro_invocation
9988 macro: (identifier) @_macro_name
9989 (token_tree) @injection.content
9990 (#set! injection.language "python"))
9991 "#,
9992 )
9993 .unwrap(),
9994 );
9995
9996 cx.language_registry().add(injected_language);
9997 cx.language_registry().add(language.clone());
9998
9999 cx.update_buffer(|buffer, cx| {
10000 buffer.set_language(Some(language), cx);
10001 });
10002
10003 cx.set_state(r#"struct A {ˇ}"#);
10004
10005 cx.update_editor(|editor, window, cx| {
10006 editor.newline(&Default::default(), window, cx);
10007 });
10008
10009 cx.assert_editor_state(indoc!(
10010 "struct A {
10011 ˇ
10012 }"
10013 ));
10014
10015 cx.set_state(r#"select_biased!(ˇ)"#);
10016
10017 cx.update_editor(|editor, window, cx| {
10018 editor.newline(&Default::default(), window, cx);
10019 editor.handle_input("def ", window, cx);
10020 editor.handle_input("(", window, cx);
10021 editor.newline(&Default::default(), window, cx);
10022 editor.handle_input("a", window, cx);
10023 });
10024
10025 cx.assert_editor_state(indoc!(
10026 "select_biased!(
10027 def (
10028 aˇ
10029 )
10030 )"
10031 ));
10032}
10033
10034#[gpui::test]
10035async fn test_autoindent_selections(cx: &mut TestAppContext) {
10036 init_test(cx, |_| {});
10037
10038 {
10039 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10040 cx.set_state(indoc! {"
10041 impl A {
10042
10043 fn b() {}
10044
10045 «fn c() {
10046
10047 }ˇ»
10048 }
10049 "});
10050
10051 cx.update_editor(|editor, window, cx| {
10052 editor.autoindent(&Default::default(), window, cx);
10053 });
10054
10055 cx.assert_editor_state(indoc! {"
10056 impl A {
10057
10058 fn b() {}
10059
10060 «fn c() {
10061
10062 }ˇ»
10063 }
10064 "});
10065 }
10066
10067 {
10068 let mut cx = EditorTestContext::new_multibuffer(
10069 cx,
10070 [indoc! { "
10071 impl A {
10072 «
10073 // a
10074 fn b(){}
10075 »
10076 «
10077 }
10078 fn c(){}
10079 »
10080 "}],
10081 );
10082
10083 let buffer = cx.update_editor(|editor, _, cx| {
10084 let buffer = editor.buffer().update(cx, |buffer, _| {
10085 buffer.all_buffers().iter().next().unwrap().clone()
10086 });
10087 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10088 buffer
10089 });
10090
10091 cx.run_until_parked();
10092 cx.update_editor(|editor, window, cx| {
10093 editor.select_all(&Default::default(), window, cx);
10094 editor.autoindent(&Default::default(), window, cx)
10095 });
10096 cx.run_until_parked();
10097
10098 cx.update(|_, cx| {
10099 assert_eq!(
10100 buffer.read(cx).text(),
10101 indoc! { "
10102 impl A {
10103
10104 // a
10105 fn b(){}
10106
10107
10108 }
10109 fn c(){}
10110
10111 " }
10112 )
10113 });
10114 }
10115}
10116
10117#[gpui::test]
10118async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10119 init_test(cx, |_| {});
10120
10121 let mut cx = EditorTestContext::new(cx).await;
10122
10123 let language = Arc::new(Language::new(
10124 LanguageConfig {
10125 brackets: BracketPairConfig {
10126 pairs: vec![
10127 BracketPair {
10128 start: "{".to_string(),
10129 end: "}".to_string(),
10130 close: true,
10131 surround: true,
10132 newline: true,
10133 },
10134 BracketPair {
10135 start: "(".to_string(),
10136 end: ")".to_string(),
10137 close: true,
10138 surround: true,
10139 newline: true,
10140 },
10141 BracketPair {
10142 start: "/*".to_string(),
10143 end: " */".to_string(),
10144 close: true,
10145 surround: true,
10146 newline: true,
10147 },
10148 BracketPair {
10149 start: "[".to_string(),
10150 end: "]".to_string(),
10151 close: false,
10152 surround: false,
10153 newline: true,
10154 },
10155 BracketPair {
10156 start: "\"".to_string(),
10157 end: "\"".to_string(),
10158 close: true,
10159 surround: true,
10160 newline: false,
10161 },
10162 BracketPair {
10163 start: "<".to_string(),
10164 end: ">".to_string(),
10165 close: false,
10166 surround: true,
10167 newline: true,
10168 },
10169 ],
10170 ..Default::default()
10171 },
10172 autoclose_before: "})]".to_string(),
10173 ..Default::default()
10174 },
10175 Some(tree_sitter_rust::LANGUAGE.into()),
10176 ));
10177
10178 cx.language_registry().add(language.clone());
10179 cx.update_buffer(|buffer, cx| {
10180 buffer.set_language(Some(language), cx);
10181 });
10182
10183 cx.set_state(
10184 &r#"
10185 🏀ˇ
10186 εˇ
10187 ❤️ˇ
10188 "#
10189 .unindent(),
10190 );
10191
10192 // autoclose multiple nested brackets at multiple cursors
10193 cx.update_editor(|editor, window, cx| {
10194 editor.handle_input("{", window, cx);
10195 editor.handle_input("{", window, cx);
10196 editor.handle_input("{", window, cx);
10197 });
10198 cx.assert_editor_state(
10199 &"
10200 🏀{{{ˇ}}}
10201 ε{{{ˇ}}}
10202 ❤️{{{ˇ}}}
10203 "
10204 .unindent(),
10205 );
10206
10207 // insert a different closing bracket
10208 cx.update_editor(|editor, window, cx| {
10209 editor.handle_input(")", window, cx);
10210 });
10211 cx.assert_editor_state(
10212 &"
10213 🏀{{{)ˇ}}}
10214 ε{{{)ˇ}}}
10215 ❤️{{{)ˇ}}}
10216 "
10217 .unindent(),
10218 );
10219
10220 // skip over the auto-closed brackets when typing a closing bracket
10221 cx.update_editor(|editor, window, cx| {
10222 editor.move_right(&MoveRight, window, cx);
10223 editor.handle_input("}", window, cx);
10224 editor.handle_input("}", window, cx);
10225 editor.handle_input("}", window, cx);
10226 });
10227 cx.assert_editor_state(
10228 &"
10229 🏀{{{)}}}}ˇ
10230 ε{{{)}}}}ˇ
10231 ❤️{{{)}}}}ˇ
10232 "
10233 .unindent(),
10234 );
10235
10236 // autoclose multi-character pairs
10237 cx.set_state(
10238 &"
10239 ˇ
10240 ˇ
10241 "
10242 .unindent(),
10243 );
10244 cx.update_editor(|editor, window, cx| {
10245 editor.handle_input("/", window, cx);
10246 editor.handle_input("*", window, cx);
10247 });
10248 cx.assert_editor_state(
10249 &"
10250 /*ˇ */
10251 /*ˇ */
10252 "
10253 .unindent(),
10254 );
10255
10256 // one cursor autocloses a multi-character pair, one cursor
10257 // does not autoclose.
10258 cx.set_state(
10259 &"
10260 /ˇ
10261 ˇ
10262 "
10263 .unindent(),
10264 );
10265 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10266 cx.assert_editor_state(
10267 &"
10268 /*ˇ */
10269 *ˇ
10270 "
10271 .unindent(),
10272 );
10273
10274 // Don't autoclose if the next character isn't whitespace and isn't
10275 // listed in the language's "autoclose_before" section.
10276 cx.set_state("ˇa b");
10277 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10278 cx.assert_editor_state("{ˇa b");
10279
10280 // Don't autoclose if `close` is false for the bracket pair
10281 cx.set_state("ˇ");
10282 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10283 cx.assert_editor_state("[ˇ");
10284
10285 // Surround with brackets if text is selected
10286 cx.set_state("«aˇ» b");
10287 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10288 cx.assert_editor_state("{«aˇ»} b");
10289
10290 // Autoclose when not immediately after a word character
10291 cx.set_state("a ˇ");
10292 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10293 cx.assert_editor_state("a \"ˇ\"");
10294
10295 // Autoclose pair where the start and end characters are the same
10296 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10297 cx.assert_editor_state("a \"\"ˇ");
10298
10299 // Don't autoclose when immediately after a word character
10300 cx.set_state("aˇ");
10301 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10302 cx.assert_editor_state("a\"ˇ");
10303
10304 // Do autoclose when after a non-word character
10305 cx.set_state("{ˇ");
10306 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10307 cx.assert_editor_state("{\"ˇ\"");
10308
10309 // Non identical pairs autoclose regardless of preceding character
10310 cx.set_state("aˇ");
10311 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10312 cx.assert_editor_state("a{ˇ}");
10313
10314 // Don't autoclose pair if autoclose is disabled
10315 cx.set_state("ˇ");
10316 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10317 cx.assert_editor_state("<ˇ");
10318
10319 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10320 cx.set_state("«aˇ» b");
10321 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10322 cx.assert_editor_state("<«aˇ»> b");
10323}
10324
10325#[gpui::test]
10326async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10327 init_test(cx, |settings| {
10328 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10329 });
10330
10331 let mut cx = EditorTestContext::new(cx).await;
10332
10333 let language = Arc::new(Language::new(
10334 LanguageConfig {
10335 brackets: BracketPairConfig {
10336 pairs: vec![
10337 BracketPair {
10338 start: "{".to_string(),
10339 end: "}".to_string(),
10340 close: true,
10341 surround: true,
10342 newline: true,
10343 },
10344 BracketPair {
10345 start: "(".to_string(),
10346 end: ")".to_string(),
10347 close: true,
10348 surround: true,
10349 newline: true,
10350 },
10351 BracketPair {
10352 start: "[".to_string(),
10353 end: "]".to_string(),
10354 close: false,
10355 surround: false,
10356 newline: true,
10357 },
10358 ],
10359 ..Default::default()
10360 },
10361 autoclose_before: "})]".to_string(),
10362 ..Default::default()
10363 },
10364 Some(tree_sitter_rust::LANGUAGE.into()),
10365 ));
10366
10367 cx.language_registry().add(language.clone());
10368 cx.update_buffer(|buffer, cx| {
10369 buffer.set_language(Some(language), cx);
10370 });
10371
10372 cx.set_state(
10373 &"
10374 ˇ
10375 ˇ
10376 ˇ
10377 "
10378 .unindent(),
10379 );
10380
10381 // ensure only matching closing brackets are skipped over
10382 cx.update_editor(|editor, window, cx| {
10383 editor.handle_input("}", window, cx);
10384 editor.move_left(&MoveLeft, window, cx);
10385 editor.handle_input(")", window, cx);
10386 editor.move_left(&MoveLeft, window, cx);
10387 });
10388 cx.assert_editor_state(
10389 &"
10390 ˇ)}
10391 ˇ)}
10392 ˇ)}
10393 "
10394 .unindent(),
10395 );
10396
10397 // skip-over closing brackets at multiple cursors
10398 cx.update_editor(|editor, window, cx| {
10399 editor.handle_input(")", window, cx);
10400 editor.handle_input("}", window, cx);
10401 });
10402 cx.assert_editor_state(
10403 &"
10404 )}ˇ
10405 )}ˇ
10406 )}ˇ
10407 "
10408 .unindent(),
10409 );
10410
10411 // ignore non-close brackets
10412 cx.update_editor(|editor, window, cx| {
10413 editor.handle_input("]", window, cx);
10414 editor.move_left(&MoveLeft, window, cx);
10415 editor.handle_input("]", window, cx);
10416 });
10417 cx.assert_editor_state(
10418 &"
10419 )}]ˇ]
10420 )}]ˇ]
10421 )}]ˇ]
10422 "
10423 .unindent(),
10424 );
10425}
10426
10427#[gpui::test]
10428async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10429 init_test(cx, |_| {});
10430
10431 let mut cx = EditorTestContext::new(cx).await;
10432
10433 let html_language = Arc::new(
10434 Language::new(
10435 LanguageConfig {
10436 name: "HTML".into(),
10437 brackets: BracketPairConfig {
10438 pairs: vec![
10439 BracketPair {
10440 start: "<".into(),
10441 end: ">".into(),
10442 close: true,
10443 ..Default::default()
10444 },
10445 BracketPair {
10446 start: "{".into(),
10447 end: "}".into(),
10448 close: true,
10449 ..Default::default()
10450 },
10451 BracketPair {
10452 start: "(".into(),
10453 end: ")".into(),
10454 close: true,
10455 ..Default::default()
10456 },
10457 ],
10458 ..Default::default()
10459 },
10460 autoclose_before: "})]>".into(),
10461 ..Default::default()
10462 },
10463 Some(tree_sitter_html::LANGUAGE.into()),
10464 )
10465 .with_injection_query(
10466 r#"
10467 (script_element
10468 (raw_text) @injection.content
10469 (#set! injection.language "javascript"))
10470 "#,
10471 )
10472 .unwrap(),
10473 );
10474
10475 let javascript_language = Arc::new(Language::new(
10476 LanguageConfig {
10477 name: "JavaScript".into(),
10478 brackets: BracketPairConfig {
10479 pairs: vec![
10480 BracketPair {
10481 start: "/*".into(),
10482 end: " */".into(),
10483 close: true,
10484 ..Default::default()
10485 },
10486 BracketPair {
10487 start: "{".into(),
10488 end: "}".into(),
10489 close: true,
10490 ..Default::default()
10491 },
10492 BracketPair {
10493 start: "(".into(),
10494 end: ")".into(),
10495 close: true,
10496 ..Default::default()
10497 },
10498 ],
10499 ..Default::default()
10500 },
10501 autoclose_before: "})]>".into(),
10502 ..Default::default()
10503 },
10504 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10505 ));
10506
10507 cx.language_registry().add(html_language.clone());
10508 cx.language_registry().add(javascript_language);
10509 cx.executor().run_until_parked();
10510
10511 cx.update_buffer(|buffer, cx| {
10512 buffer.set_language(Some(html_language), cx);
10513 });
10514
10515 cx.set_state(
10516 &r#"
10517 <body>ˇ
10518 <script>
10519 var x = 1;ˇ
10520 </script>
10521 </body>ˇ
10522 "#
10523 .unindent(),
10524 );
10525
10526 // Precondition: different languages are active at different locations.
10527 cx.update_editor(|editor, window, cx| {
10528 let snapshot = editor.snapshot(window, cx);
10529 let cursors = editor
10530 .selections
10531 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10532 let languages = cursors
10533 .iter()
10534 .map(|c| snapshot.language_at(c.start).unwrap().name())
10535 .collect::<Vec<_>>();
10536 assert_eq!(
10537 languages,
10538 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10539 );
10540 });
10541
10542 // Angle brackets autoclose in HTML, but not JavaScript.
10543 cx.update_editor(|editor, window, cx| {
10544 editor.handle_input("<", window, cx);
10545 editor.handle_input("a", window, cx);
10546 });
10547 cx.assert_editor_state(
10548 &r#"
10549 <body><aˇ>
10550 <script>
10551 var x = 1;<aˇ
10552 </script>
10553 </body><aˇ>
10554 "#
10555 .unindent(),
10556 );
10557
10558 // Curly braces and parens autoclose in both HTML and JavaScript.
10559 cx.update_editor(|editor, window, cx| {
10560 editor.handle_input(" b=", window, cx);
10561 editor.handle_input("{", window, cx);
10562 editor.handle_input("c", window, cx);
10563 editor.handle_input("(", window, cx);
10564 });
10565 cx.assert_editor_state(
10566 &r#"
10567 <body><a b={c(ˇ)}>
10568 <script>
10569 var x = 1;<a b={c(ˇ)}
10570 </script>
10571 </body><a b={c(ˇ)}>
10572 "#
10573 .unindent(),
10574 );
10575
10576 // Brackets that were already autoclosed are skipped.
10577 cx.update_editor(|editor, window, cx| {
10578 editor.handle_input(")", window, cx);
10579 editor.handle_input("d", window, cx);
10580 editor.handle_input("}", window, cx);
10581 });
10582 cx.assert_editor_state(
10583 &r#"
10584 <body><a b={c()d}ˇ>
10585 <script>
10586 var x = 1;<a b={c()d}ˇ
10587 </script>
10588 </body><a b={c()d}ˇ>
10589 "#
10590 .unindent(),
10591 );
10592 cx.update_editor(|editor, window, cx| {
10593 editor.handle_input(">", window, cx);
10594 });
10595 cx.assert_editor_state(
10596 &r#"
10597 <body><a b={c()d}>ˇ
10598 <script>
10599 var x = 1;<a b={c()d}>ˇ
10600 </script>
10601 </body><a b={c()d}>ˇ
10602 "#
10603 .unindent(),
10604 );
10605
10606 // Reset
10607 cx.set_state(
10608 &r#"
10609 <body>ˇ
10610 <script>
10611 var x = 1;ˇ
10612 </script>
10613 </body>ˇ
10614 "#
10615 .unindent(),
10616 );
10617
10618 cx.update_editor(|editor, window, cx| {
10619 editor.handle_input("<", window, cx);
10620 });
10621 cx.assert_editor_state(
10622 &r#"
10623 <body><ˇ>
10624 <script>
10625 var x = 1;<ˇ
10626 </script>
10627 </body><ˇ>
10628 "#
10629 .unindent(),
10630 );
10631
10632 // When backspacing, the closing angle brackets are removed.
10633 cx.update_editor(|editor, window, cx| {
10634 editor.backspace(&Backspace, window, cx);
10635 });
10636 cx.assert_editor_state(
10637 &r#"
10638 <body>ˇ
10639 <script>
10640 var x = 1;ˇ
10641 </script>
10642 </body>ˇ
10643 "#
10644 .unindent(),
10645 );
10646
10647 // Block comments autoclose in JavaScript, but not HTML.
10648 cx.update_editor(|editor, window, cx| {
10649 editor.handle_input("/", window, cx);
10650 editor.handle_input("*", window, cx);
10651 });
10652 cx.assert_editor_state(
10653 &r#"
10654 <body>/*ˇ
10655 <script>
10656 var x = 1;/*ˇ */
10657 </script>
10658 </body>/*ˇ
10659 "#
10660 .unindent(),
10661 );
10662}
10663
10664#[gpui::test]
10665async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10666 init_test(cx, |_| {});
10667
10668 let mut cx = EditorTestContext::new(cx).await;
10669
10670 let rust_language = Arc::new(
10671 Language::new(
10672 LanguageConfig {
10673 name: "Rust".into(),
10674 brackets: serde_json::from_value(json!([
10675 { "start": "{", "end": "}", "close": true, "newline": true },
10676 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10677 ]))
10678 .unwrap(),
10679 autoclose_before: "})]>".into(),
10680 ..Default::default()
10681 },
10682 Some(tree_sitter_rust::LANGUAGE.into()),
10683 )
10684 .with_override_query("(string_literal) @string")
10685 .unwrap(),
10686 );
10687
10688 cx.language_registry().add(rust_language.clone());
10689 cx.update_buffer(|buffer, cx| {
10690 buffer.set_language(Some(rust_language), cx);
10691 });
10692
10693 cx.set_state(
10694 &r#"
10695 let x = ˇ
10696 "#
10697 .unindent(),
10698 );
10699
10700 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10701 cx.update_editor(|editor, window, cx| {
10702 editor.handle_input("\"", window, cx);
10703 });
10704 cx.assert_editor_state(
10705 &r#"
10706 let x = "ˇ"
10707 "#
10708 .unindent(),
10709 );
10710
10711 // Inserting another quotation mark. The cursor moves across the existing
10712 // automatically-inserted quotation mark.
10713 cx.update_editor(|editor, window, cx| {
10714 editor.handle_input("\"", window, cx);
10715 });
10716 cx.assert_editor_state(
10717 &r#"
10718 let x = ""ˇ
10719 "#
10720 .unindent(),
10721 );
10722
10723 // Reset
10724 cx.set_state(
10725 &r#"
10726 let x = ˇ
10727 "#
10728 .unindent(),
10729 );
10730
10731 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10732 cx.update_editor(|editor, window, cx| {
10733 editor.handle_input("\"", window, cx);
10734 editor.handle_input(" ", window, cx);
10735 editor.move_left(&Default::default(), window, cx);
10736 editor.handle_input("\\", window, cx);
10737 editor.handle_input("\"", window, cx);
10738 });
10739 cx.assert_editor_state(
10740 &r#"
10741 let x = "\"ˇ "
10742 "#
10743 .unindent(),
10744 );
10745
10746 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10747 // mark. Nothing is inserted.
10748 cx.update_editor(|editor, window, cx| {
10749 editor.move_right(&Default::default(), window, cx);
10750 editor.handle_input("\"", window, cx);
10751 });
10752 cx.assert_editor_state(
10753 &r#"
10754 let x = "\" "ˇ
10755 "#
10756 .unindent(),
10757 );
10758}
10759
10760#[gpui::test]
10761async fn test_surround_with_pair(cx: &mut TestAppContext) {
10762 init_test(cx, |_| {});
10763
10764 let language = Arc::new(Language::new(
10765 LanguageConfig {
10766 brackets: BracketPairConfig {
10767 pairs: vec![
10768 BracketPair {
10769 start: "{".to_string(),
10770 end: "}".to_string(),
10771 close: true,
10772 surround: true,
10773 newline: true,
10774 },
10775 BracketPair {
10776 start: "/* ".to_string(),
10777 end: "*/".to_string(),
10778 close: true,
10779 surround: true,
10780 ..Default::default()
10781 },
10782 ],
10783 ..Default::default()
10784 },
10785 ..Default::default()
10786 },
10787 Some(tree_sitter_rust::LANGUAGE.into()),
10788 ));
10789
10790 let text = r#"
10791 a
10792 b
10793 c
10794 "#
10795 .unindent();
10796
10797 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10798 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10799 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10800 editor
10801 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10802 .await;
10803
10804 editor.update_in(cx, |editor, window, cx| {
10805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10806 s.select_display_ranges([
10807 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10808 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10809 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10810 ])
10811 });
10812
10813 editor.handle_input("{", window, cx);
10814 editor.handle_input("{", window, cx);
10815 editor.handle_input("{", window, cx);
10816 assert_eq!(
10817 editor.text(cx),
10818 "
10819 {{{a}}}
10820 {{{b}}}
10821 {{{c}}}
10822 "
10823 .unindent()
10824 );
10825 assert_eq!(
10826 display_ranges(editor, cx),
10827 [
10828 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10829 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10830 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10831 ]
10832 );
10833
10834 editor.undo(&Undo, window, cx);
10835 editor.undo(&Undo, window, cx);
10836 editor.undo(&Undo, window, cx);
10837 assert_eq!(
10838 editor.text(cx),
10839 "
10840 a
10841 b
10842 c
10843 "
10844 .unindent()
10845 );
10846 assert_eq!(
10847 display_ranges(editor, cx),
10848 [
10849 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10850 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10851 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10852 ]
10853 );
10854
10855 // Ensure inserting the first character of a multi-byte bracket pair
10856 // doesn't surround the selections with the bracket.
10857 editor.handle_input("/", window, cx);
10858 assert_eq!(
10859 editor.text(cx),
10860 "
10861 /
10862 /
10863 /
10864 "
10865 .unindent()
10866 );
10867 assert_eq!(
10868 display_ranges(editor, cx),
10869 [
10870 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10871 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10872 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10873 ]
10874 );
10875
10876 editor.undo(&Undo, window, cx);
10877 assert_eq!(
10878 editor.text(cx),
10879 "
10880 a
10881 b
10882 c
10883 "
10884 .unindent()
10885 );
10886 assert_eq!(
10887 display_ranges(editor, cx),
10888 [
10889 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10890 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10891 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10892 ]
10893 );
10894
10895 // Ensure inserting the last character of a multi-byte bracket pair
10896 // doesn't surround the selections with the bracket.
10897 editor.handle_input("*", window, cx);
10898 assert_eq!(
10899 editor.text(cx),
10900 "
10901 *
10902 *
10903 *
10904 "
10905 .unindent()
10906 );
10907 assert_eq!(
10908 display_ranges(editor, cx),
10909 [
10910 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10911 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10912 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10913 ]
10914 );
10915 });
10916}
10917
10918#[gpui::test]
10919async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10920 init_test(cx, |_| {});
10921
10922 let language = Arc::new(Language::new(
10923 LanguageConfig {
10924 brackets: BracketPairConfig {
10925 pairs: vec![BracketPair {
10926 start: "{".to_string(),
10927 end: "}".to_string(),
10928 close: true,
10929 surround: true,
10930 newline: true,
10931 }],
10932 ..Default::default()
10933 },
10934 autoclose_before: "}".to_string(),
10935 ..Default::default()
10936 },
10937 Some(tree_sitter_rust::LANGUAGE.into()),
10938 ));
10939
10940 let text = r#"
10941 a
10942 b
10943 c
10944 "#
10945 .unindent();
10946
10947 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10948 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10949 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10950 editor
10951 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10952 .await;
10953
10954 editor.update_in(cx, |editor, window, cx| {
10955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10956 s.select_ranges([
10957 Point::new(0, 1)..Point::new(0, 1),
10958 Point::new(1, 1)..Point::new(1, 1),
10959 Point::new(2, 1)..Point::new(2, 1),
10960 ])
10961 });
10962
10963 editor.handle_input("{", window, cx);
10964 editor.handle_input("{", window, cx);
10965 editor.handle_input("_", window, cx);
10966 assert_eq!(
10967 editor.text(cx),
10968 "
10969 a{{_}}
10970 b{{_}}
10971 c{{_}}
10972 "
10973 .unindent()
10974 );
10975 assert_eq!(
10976 editor
10977 .selections
10978 .ranges::<Point>(&editor.display_snapshot(cx)),
10979 [
10980 Point::new(0, 4)..Point::new(0, 4),
10981 Point::new(1, 4)..Point::new(1, 4),
10982 Point::new(2, 4)..Point::new(2, 4)
10983 ]
10984 );
10985
10986 editor.backspace(&Default::default(), window, cx);
10987 editor.backspace(&Default::default(), window, cx);
10988 assert_eq!(
10989 editor.text(cx),
10990 "
10991 a{}
10992 b{}
10993 c{}
10994 "
10995 .unindent()
10996 );
10997 assert_eq!(
10998 editor
10999 .selections
11000 .ranges::<Point>(&editor.display_snapshot(cx)),
11001 [
11002 Point::new(0, 2)..Point::new(0, 2),
11003 Point::new(1, 2)..Point::new(1, 2),
11004 Point::new(2, 2)..Point::new(2, 2)
11005 ]
11006 );
11007
11008 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11009 assert_eq!(
11010 editor.text(cx),
11011 "
11012 a
11013 b
11014 c
11015 "
11016 .unindent()
11017 );
11018 assert_eq!(
11019 editor
11020 .selections
11021 .ranges::<Point>(&editor.display_snapshot(cx)),
11022 [
11023 Point::new(0, 1)..Point::new(0, 1),
11024 Point::new(1, 1)..Point::new(1, 1),
11025 Point::new(2, 1)..Point::new(2, 1)
11026 ]
11027 );
11028 });
11029}
11030
11031#[gpui::test]
11032async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11033 init_test(cx, |settings| {
11034 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11035 });
11036
11037 let mut cx = EditorTestContext::new(cx).await;
11038
11039 let language = Arc::new(Language::new(
11040 LanguageConfig {
11041 brackets: BracketPairConfig {
11042 pairs: vec![
11043 BracketPair {
11044 start: "{".to_string(),
11045 end: "}".to_string(),
11046 close: true,
11047 surround: true,
11048 newline: true,
11049 },
11050 BracketPair {
11051 start: "(".to_string(),
11052 end: ")".to_string(),
11053 close: true,
11054 surround: true,
11055 newline: true,
11056 },
11057 BracketPair {
11058 start: "[".to_string(),
11059 end: "]".to_string(),
11060 close: false,
11061 surround: true,
11062 newline: true,
11063 },
11064 ],
11065 ..Default::default()
11066 },
11067 autoclose_before: "})]".to_string(),
11068 ..Default::default()
11069 },
11070 Some(tree_sitter_rust::LANGUAGE.into()),
11071 ));
11072
11073 cx.language_registry().add(language.clone());
11074 cx.update_buffer(|buffer, cx| {
11075 buffer.set_language(Some(language), cx);
11076 });
11077
11078 cx.set_state(
11079 &"
11080 {(ˇ)}
11081 [[ˇ]]
11082 {(ˇ)}
11083 "
11084 .unindent(),
11085 );
11086
11087 cx.update_editor(|editor, window, cx| {
11088 editor.backspace(&Default::default(), window, cx);
11089 editor.backspace(&Default::default(), window, cx);
11090 });
11091
11092 cx.assert_editor_state(
11093 &"
11094 ˇ
11095 ˇ]]
11096 ˇ
11097 "
11098 .unindent(),
11099 );
11100
11101 cx.update_editor(|editor, window, cx| {
11102 editor.handle_input("{", window, cx);
11103 editor.handle_input("{", window, cx);
11104 editor.move_right(&MoveRight, window, cx);
11105 editor.move_right(&MoveRight, window, cx);
11106 editor.move_left(&MoveLeft, window, cx);
11107 editor.move_left(&MoveLeft, window, cx);
11108 editor.backspace(&Default::default(), window, cx);
11109 });
11110
11111 cx.assert_editor_state(
11112 &"
11113 {ˇ}
11114 {ˇ}]]
11115 {ˇ}
11116 "
11117 .unindent(),
11118 );
11119
11120 cx.update_editor(|editor, window, cx| {
11121 editor.backspace(&Default::default(), window, cx);
11122 });
11123
11124 cx.assert_editor_state(
11125 &"
11126 ˇ
11127 ˇ]]
11128 ˇ
11129 "
11130 .unindent(),
11131 );
11132}
11133
11134#[gpui::test]
11135async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11136 init_test(cx, |_| {});
11137
11138 let language = Arc::new(Language::new(
11139 LanguageConfig::default(),
11140 Some(tree_sitter_rust::LANGUAGE.into()),
11141 ));
11142
11143 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11144 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11145 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11146 editor
11147 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11148 .await;
11149
11150 editor.update_in(cx, |editor, window, cx| {
11151 editor.set_auto_replace_emoji_shortcode(true);
11152
11153 editor.handle_input("Hello ", window, cx);
11154 editor.handle_input(":wave", window, cx);
11155 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11156
11157 editor.handle_input(":", window, cx);
11158 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11159
11160 editor.handle_input(" :smile", window, cx);
11161 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11162
11163 editor.handle_input(":", window, cx);
11164 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11165
11166 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11167 editor.handle_input(":wave", window, cx);
11168 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11169
11170 editor.handle_input(":", window, cx);
11171 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11172
11173 editor.handle_input(":1", window, cx);
11174 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11175
11176 editor.handle_input(":", window, cx);
11177 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11178
11179 // Ensure shortcode does not get replaced when it is part of a word
11180 editor.handle_input(" Test:wave", window, cx);
11181 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11182
11183 editor.handle_input(":", window, cx);
11184 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11185
11186 editor.set_auto_replace_emoji_shortcode(false);
11187
11188 // Ensure shortcode does not get replaced when auto replace is off
11189 editor.handle_input(" :wave", window, cx);
11190 assert_eq!(
11191 editor.text(cx),
11192 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11193 );
11194
11195 editor.handle_input(":", window, cx);
11196 assert_eq!(
11197 editor.text(cx),
11198 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11199 );
11200 });
11201}
11202
11203#[gpui::test]
11204async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11205 init_test(cx, |_| {});
11206
11207 let (text, insertion_ranges) = marked_text_ranges(
11208 indoc! {"
11209 ˇ
11210 "},
11211 false,
11212 );
11213
11214 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11215 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11216
11217 _ = editor.update_in(cx, |editor, window, cx| {
11218 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11219
11220 editor
11221 .insert_snippet(
11222 &insertion_ranges
11223 .iter()
11224 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11225 .collect::<Vec<_>>(),
11226 snippet,
11227 window,
11228 cx,
11229 )
11230 .unwrap();
11231
11232 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11233 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11234 assert_eq!(editor.text(cx), expected_text);
11235 assert_eq!(
11236 editor.selections.ranges(&editor.display_snapshot(cx)),
11237 selection_ranges
11238 .iter()
11239 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11240 .collect::<Vec<_>>()
11241 );
11242 }
11243
11244 assert(
11245 editor,
11246 cx,
11247 indoc! {"
11248 type «» =•
11249 "},
11250 );
11251
11252 assert!(editor.context_menu_visible(), "There should be a matches");
11253 });
11254}
11255
11256#[gpui::test]
11257async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11258 init_test(cx, |_| {});
11259
11260 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11261 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11262 assert_eq!(editor.text(cx), expected_text);
11263 assert_eq!(
11264 editor.selections.ranges(&editor.display_snapshot(cx)),
11265 selection_ranges
11266 .iter()
11267 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11268 .collect::<Vec<_>>()
11269 );
11270 }
11271
11272 let (text, insertion_ranges) = marked_text_ranges(
11273 indoc! {"
11274 ˇ
11275 "},
11276 false,
11277 );
11278
11279 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11280 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11281
11282 _ = editor.update_in(cx, |editor, window, cx| {
11283 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11284
11285 editor
11286 .insert_snippet(
11287 &insertion_ranges
11288 .iter()
11289 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11290 .collect::<Vec<_>>(),
11291 snippet,
11292 window,
11293 cx,
11294 )
11295 .unwrap();
11296
11297 assert_state(
11298 editor,
11299 cx,
11300 indoc! {"
11301 type «» = ;•
11302 "},
11303 );
11304
11305 assert!(
11306 editor.context_menu_visible(),
11307 "Context menu should be visible for placeholder choices"
11308 );
11309
11310 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11311
11312 assert_state(
11313 editor,
11314 cx,
11315 indoc! {"
11316 type = «»;•
11317 "},
11318 );
11319
11320 assert!(
11321 !editor.context_menu_visible(),
11322 "Context menu should be hidden after moving to next tabstop"
11323 );
11324
11325 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11326
11327 assert_state(
11328 editor,
11329 cx,
11330 indoc! {"
11331 type = ; ˇ
11332 "},
11333 );
11334
11335 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11336
11337 assert_state(
11338 editor,
11339 cx,
11340 indoc! {"
11341 type = ; ˇ
11342 "},
11343 );
11344 });
11345
11346 _ = editor.update_in(cx, |editor, window, cx| {
11347 editor.select_all(&SelectAll, window, cx);
11348 editor.backspace(&Backspace, window, cx);
11349
11350 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11351 let insertion_ranges = editor
11352 .selections
11353 .all(&editor.display_snapshot(cx))
11354 .iter()
11355 .map(|s| s.range())
11356 .collect::<Vec<_>>();
11357
11358 editor
11359 .insert_snippet(&insertion_ranges, snippet, window, cx)
11360 .unwrap();
11361
11362 assert_state(editor, cx, "fn «» = value;•");
11363
11364 assert!(
11365 editor.context_menu_visible(),
11366 "Context menu should be visible for placeholder choices"
11367 );
11368
11369 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11370
11371 assert_state(editor, cx, "fn = «valueˇ»;•");
11372
11373 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11374
11375 assert_state(editor, cx, "fn «» = value;•");
11376
11377 assert!(
11378 editor.context_menu_visible(),
11379 "Context menu should be visible again after returning to first tabstop"
11380 );
11381
11382 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11383
11384 assert_state(editor, cx, "fn «» = value;•");
11385 });
11386}
11387
11388#[gpui::test]
11389async fn test_snippets(cx: &mut TestAppContext) {
11390 init_test(cx, |_| {});
11391
11392 let mut cx = EditorTestContext::new(cx).await;
11393
11394 cx.set_state(indoc! {"
11395 a.ˇ b
11396 a.ˇ b
11397 a.ˇ b
11398 "});
11399
11400 cx.update_editor(|editor, window, cx| {
11401 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11402 let insertion_ranges = editor
11403 .selections
11404 .all(&editor.display_snapshot(cx))
11405 .iter()
11406 .map(|s| s.range())
11407 .collect::<Vec<_>>();
11408 editor
11409 .insert_snippet(&insertion_ranges, snippet, window, cx)
11410 .unwrap();
11411 });
11412
11413 cx.assert_editor_state(indoc! {"
11414 a.f(«oneˇ», two, «threeˇ») b
11415 a.f(«oneˇ», two, «threeˇ») b
11416 a.f(«oneˇ», two, «threeˇ») b
11417 "});
11418
11419 // Can't move earlier than the first tab stop
11420 cx.update_editor(|editor, window, cx| {
11421 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11422 });
11423 cx.assert_editor_state(indoc! {"
11424 a.f(«oneˇ», two, «threeˇ») b
11425 a.f(«oneˇ», two, «threeˇ») b
11426 a.f(«oneˇ», two, «threeˇ») b
11427 "});
11428
11429 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11430 cx.assert_editor_state(indoc! {"
11431 a.f(one, «twoˇ», three) b
11432 a.f(one, «twoˇ», three) b
11433 a.f(one, «twoˇ», three) b
11434 "});
11435
11436 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11437 cx.assert_editor_state(indoc! {"
11438 a.f(«oneˇ», two, «threeˇ») b
11439 a.f(«oneˇ», two, «threeˇ») b
11440 a.f(«oneˇ», two, «threeˇ») b
11441 "});
11442
11443 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11444 cx.assert_editor_state(indoc! {"
11445 a.f(one, «twoˇ», three) b
11446 a.f(one, «twoˇ», three) b
11447 a.f(one, «twoˇ», three) b
11448 "});
11449 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11450 cx.assert_editor_state(indoc! {"
11451 a.f(one, two, three)ˇ b
11452 a.f(one, two, three)ˇ b
11453 a.f(one, two, three)ˇ b
11454 "});
11455
11456 // As soon as the last tab stop is reached, snippet state is gone
11457 cx.update_editor(|editor, window, cx| {
11458 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11459 });
11460 cx.assert_editor_state(indoc! {"
11461 a.f(one, two, three)ˇ b
11462 a.f(one, two, three)ˇ b
11463 a.f(one, two, three)ˇ b
11464 "});
11465}
11466
11467#[gpui::test]
11468async fn test_snippet_indentation(cx: &mut TestAppContext) {
11469 init_test(cx, |_| {});
11470
11471 let mut cx = EditorTestContext::new(cx).await;
11472
11473 cx.update_editor(|editor, window, cx| {
11474 let snippet = Snippet::parse(indoc! {"
11475 /*
11476 * Multiline comment with leading indentation
11477 *
11478 * $1
11479 */
11480 $0"})
11481 .unwrap();
11482 let insertion_ranges = editor
11483 .selections
11484 .all(&editor.display_snapshot(cx))
11485 .iter()
11486 .map(|s| s.range())
11487 .collect::<Vec<_>>();
11488 editor
11489 .insert_snippet(&insertion_ranges, snippet, window, cx)
11490 .unwrap();
11491 });
11492
11493 cx.assert_editor_state(indoc! {"
11494 /*
11495 * Multiline comment with leading indentation
11496 *
11497 * ˇ
11498 */
11499 "});
11500
11501 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11502 cx.assert_editor_state(indoc! {"
11503 /*
11504 * Multiline comment with leading indentation
11505 *
11506 *•
11507 */
11508 ˇ"});
11509}
11510
11511#[gpui::test]
11512async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11513 init_test(cx, |_| {});
11514
11515 let mut cx = EditorTestContext::new(cx).await;
11516 cx.update_editor(|editor, _, cx| {
11517 editor.project().unwrap().update(cx, |project, cx| {
11518 project.snippets().update(cx, |snippets, _cx| {
11519 let snippet = project::snippet_provider::Snippet {
11520 prefix: vec!["multi word".to_string()],
11521 body: "this is many words".to_string(),
11522 description: Some("description".to_string()),
11523 name: "multi-word snippet test".to_string(),
11524 };
11525 snippets.add_snippet_for_test(
11526 None,
11527 PathBuf::from("test_snippets.json"),
11528 vec![Arc::new(snippet)],
11529 );
11530 });
11531 })
11532 });
11533
11534 for (input_to_simulate, should_match_snippet) in [
11535 ("m", true),
11536 ("m ", true),
11537 ("m w", true),
11538 ("aa m w", true),
11539 ("aa m g", false),
11540 ] {
11541 cx.set_state("ˇ");
11542 cx.simulate_input(input_to_simulate); // fails correctly
11543
11544 cx.update_editor(|editor, _, _| {
11545 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11546 else {
11547 assert!(!should_match_snippet); // no completions! don't even show the menu
11548 return;
11549 };
11550 assert!(context_menu.visible());
11551 let completions = context_menu.completions.borrow();
11552
11553 assert_eq!(!completions.is_empty(), should_match_snippet);
11554 });
11555 }
11556}
11557
11558#[gpui::test]
11559async fn test_document_format_during_save(cx: &mut TestAppContext) {
11560 init_test(cx, |_| {});
11561
11562 let fs = FakeFs::new(cx.executor());
11563 fs.insert_file(path!("/file.rs"), Default::default()).await;
11564
11565 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11566
11567 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11568 language_registry.add(rust_lang());
11569 let mut fake_servers = language_registry.register_fake_lsp(
11570 "Rust",
11571 FakeLspAdapter {
11572 capabilities: lsp::ServerCapabilities {
11573 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11574 ..Default::default()
11575 },
11576 ..Default::default()
11577 },
11578 );
11579
11580 let buffer = project
11581 .update(cx, |project, cx| {
11582 project.open_local_buffer(path!("/file.rs"), cx)
11583 })
11584 .await
11585 .unwrap();
11586
11587 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11588 let (editor, cx) = cx.add_window_view(|window, cx| {
11589 build_editor_with_project(project.clone(), buffer, window, cx)
11590 });
11591 editor.update_in(cx, |editor, window, cx| {
11592 editor.set_text("one\ntwo\nthree\n", window, cx)
11593 });
11594 assert!(cx.read(|cx| editor.is_dirty(cx)));
11595
11596 cx.executor().start_waiting();
11597 let fake_server = fake_servers.next().await.unwrap();
11598
11599 {
11600 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11601 move |params, _| async move {
11602 assert_eq!(
11603 params.text_document.uri,
11604 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11605 );
11606 assert_eq!(params.options.tab_size, 4);
11607 Ok(Some(vec![lsp::TextEdit::new(
11608 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11609 ", ".to_string(),
11610 )]))
11611 },
11612 );
11613 let save = editor
11614 .update_in(cx, |editor, window, cx| {
11615 editor.save(
11616 SaveOptions {
11617 format: true,
11618 autosave: false,
11619 },
11620 project.clone(),
11621 window,
11622 cx,
11623 )
11624 })
11625 .unwrap();
11626 cx.executor().start_waiting();
11627 save.await;
11628
11629 assert_eq!(
11630 editor.update(cx, |editor, cx| editor.text(cx)),
11631 "one, two\nthree\n"
11632 );
11633 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11634 }
11635
11636 {
11637 editor.update_in(cx, |editor, window, cx| {
11638 editor.set_text("one\ntwo\nthree\n", window, cx)
11639 });
11640 assert!(cx.read(|cx| editor.is_dirty(cx)));
11641
11642 // Ensure we can still save even if formatting hangs.
11643 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11644 move |params, _| async move {
11645 assert_eq!(
11646 params.text_document.uri,
11647 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11648 );
11649 futures::future::pending::<()>().await;
11650 unreachable!()
11651 },
11652 );
11653 let save = editor
11654 .update_in(cx, |editor, window, cx| {
11655 editor.save(
11656 SaveOptions {
11657 format: true,
11658 autosave: false,
11659 },
11660 project.clone(),
11661 window,
11662 cx,
11663 )
11664 })
11665 .unwrap();
11666 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11667 cx.executor().start_waiting();
11668 save.await;
11669 assert_eq!(
11670 editor.update(cx, |editor, cx| editor.text(cx)),
11671 "one\ntwo\nthree\n"
11672 );
11673 }
11674
11675 // Set rust language override and assert overridden tabsize is sent to language server
11676 update_test_language_settings(cx, |settings| {
11677 settings.languages.0.insert(
11678 "Rust".into(),
11679 LanguageSettingsContent {
11680 tab_size: NonZeroU32::new(8),
11681 ..Default::default()
11682 },
11683 );
11684 });
11685
11686 {
11687 editor.update_in(cx, |editor, window, cx| {
11688 editor.set_text("somehting_new\n", window, cx)
11689 });
11690 assert!(cx.read(|cx| editor.is_dirty(cx)));
11691 let _formatting_request_signal = fake_server
11692 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11693 assert_eq!(
11694 params.text_document.uri,
11695 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11696 );
11697 assert_eq!(params.options.tab_size, 8);
11698 Ok(Some(vec![]))
11699 });
11700 let save = editor
11701 .update_in(cx, |editor, window, cx| {
11702 editor.save(
11703 SaveOptions {
11704 format: true,
11705 autosave: false,
11706 },
11707 project.clone(),
11708 window,
11709 cx,
11710 )
11711 })
11712 .unwrap();
11713 cx.executor().start_waiting();
11714 save.await;
11715 }
11716}
11717
11718#[gpui::test]
11719async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11720 init_test(cx, |settings| {
11721 settings.defaults.ensure_final_newline_on_save = Some(false);
11722 });
11723
11724 let fs = FakeFs::new(cx.executor());
11725 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11726
11727 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11728
11729 let buffer = project
11730 .update(cx, |project, cx| {
11731 project.open_local_buffer(path!("/file.txt"), cx)
11732 })
11733 .await
11734 .unwrap();
11735
11736 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11737 let (editor, cx) = cx.add_window_view(|window, cx| {
11738 build_editor_with_project(project.clone(), buffer, window, cx)
11739 });
11740 editor.update_in(cx, |editor, window, cx| {
11741 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11742 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11743 });
11744 });
11745 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11746
11747 editor.update_in(cx, |editor, window, cx| {
11748 editor.handle_input("\n", window, cx)
11749 });
11750 cx.run_until_parked();
11751 save(&editor, &project, cx).await;
11752 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11753
11754 editor.update_in(cx, |editor, window, cx| {
11755 editor.undo(&Default::default(), window, cx);
11756 });
11757 save(&editor, &project, cx).await;
11758 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11759
11760 editor.update_in(cx, |editor, window, cx| {
11761 editor.redo(&Default::default(), window, cx);
11762 });
11763 cx.run_until_parked();
11764 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11765
11766 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11767 let save = editor
11768 .update_in(cx, |editor, window, cx| {
11769 editor.save(
11770 SaveOptions {
11771 format: true,
11772 autosave: false,
11773 },
11774 project.clone(),
11775 window,
11776 cx,
11777 )
11778 })
11779 .unwrap();
11780 cx.executor().start_waiting();
11781 save.await;
11782 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11783 }
11784}
11785
11786#[gpui::test]
11787async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11788 init_test(cx, |_| {});
11789
11790 let cols = 4;
11791 let rows = 10;
11792 let sample_text_1 = sample_text(rows, cols, 'a');
11793 assert_eq!(
11794 sample_text_1,
11795 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11796 );
11797 let sample_text_2 = sample_text(rows, cols, 'l');
11798 assert_eq!(
11799 sample_text_2,
11800 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11801 );
11802 let sample_text_3 = sample_text(rows, cols, 'v');
11803 assert_eq!(
11804 sample_text_3,
11805 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11806 );
11807
11808 let fs = FakeFs::new(cx.executor());
11809 fs.insert_tree(
11810 path!("/a"),
11811 json!({
11812 "main.rs": sample_text_1,
11813 "other.rs": sample_text_2,
11814 "lib.rs": sample_text_3,
11815 }),
11816 )
11817 .await;
11818
11819 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11820 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11821 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11822
11823 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11824 language_registry.add(rust_lang());
11825 let mut fake_servers = language_registry.register_fake_lsp(
11826 "Rust",
11827 FakeLspAdapter {
11828 capabilities: lsp::ServerCapabilities {
11829 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11830 ..Default::default()
11831 },
11832 ..Default::default()
11833 },
11834 );
11835
11836 let worktree = project.update(cx, |project, cx| {
11837 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11838 assert_eq!(worktrees.len(), 1);
11839 worktrees.pop().unwrap()
11840 });
11841 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11842
11843 let buffer_1 = project
11844 .update(cx, |project, cx| {
11845 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11846 })
11847 .await
11848 .unwrap();
11849 let buffer_2 = project
11850 .update(cx, |project, cx| {
11851 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11852 })
11853 .await
11854 .unwrap();
11855 let buffer_3 = project
11856 .update(cx, |project, cx| {
11857 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11858 })
11859 .await
11860 .unwrap();
11861
11862 let multi_buffer = cx.new(|cx| {
11863 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11864 multi_buffer.push_excerpts(
11865 buffer_1.clone(),
11866 [
11867 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11868 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11869 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11870 ],
11871 cx,
11872 );
11873 multi_buffer.push_excerpts(
11874 buffer_2.clone(),
11875 [
11876 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11877 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11878 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11879 ],
11880 cx,
11881 );
11882 multi_buffer.push_excerpts(
11883 buffer_3.clone(),
11884 [
11885 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11886 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11887 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11888 ],
11889 cx,
11890 );
11891 multi_buffer
11892 });
11893 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11894 Editor::new(
11895 EditorMode::full(),
11896 multi_buffer,
11897 Some(project.clone()),
11898 window,
11899 cx,
11900 )
11901 });
11902
11903 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11904 editor.change_selections(
11905 SelectionEffects::scroll(Autoscroll::Next),
11906 window,
11907 cx,
11908 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
11909 );
11910 editor.insert("|one|two|three|", window, cx);
11911 });
11912 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11913 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11914 editor.change_selections(
11915 SelectionEffects::scroll(Autoscroll::Next),
11916 window,
11917 cx,
11918 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
11919 );
11920 editor.insert("|four|five|six|", window, cx);
11921 });
11922 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11923
11924 // First two buffers should be edited, but not the third one.
11925 assert_eq!(
11926 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11927 "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}",
11928 );
11929 buffer_1.update(cx, |buffer, _| {
11930 assert!(buffer.is_dirty());
11931 assert_eq!(
11932 buffer.text(),
11933 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11934 )
11935 });
11936 buffer_2.update(cx, |buffer, _| {
11937 assert!(buffer.is_dirty());
11938 assert_eq!(
11939 buffer.text(),
11940 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11941 )
11942 });
11943 buffer_3.update(cx, |buffer, _| {
11944 assert!(!buffer.is_dirty());
11945 assert_eq!(buffer.text(), sample_text_3,)
11946 });
11947 cx.executor().run_until_parked();
11948
11949 cx.executor().start_waiting();
11950 let save = multi_buffer_editor
11951 .update_in(cx, |editor, window, cx| {
11952 editor.save(
11953 SaveOptions {
11954 format: true,
11955 autosave: false,
11956 },
11957 project.clone(),
11958 window,
11959 cx,
11960 )
11961 })
11962 .unwrap();
11963
11964 let fake_server = fake_servers.next().await.unwrap();
11965 fake_server
11966 .server
11967 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11968 Ok(Some(vec![lsp::TextEdit::new(
11969 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11970 format!("[{} formatted]", params.text_document.uri),
11971 )]))
11972 })
11973 .detach();
11974 save.await;
11975
11976 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11977 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11978 assert_eq!(
11979 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11980 uri!(
11981 "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}"
11982 ),
11983 );
11984 buffer_1.update(cx, |buffer, _| {
11985 assert!(!buffer.is_dirty());
11986 assert_eq!(
11987 buffer.text(),
11988 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11989 )
11990 });
11991 buffer_2.update(cx, |buffer, _| {
11992 assert!(!buffer.is_dirty());
11993 assert_eq!(
11994 buffer.text(),
11995 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11996 )
11997 });
11998 buffer_3.update(cx, |buffer, _| {
11999 assert!(!buffer.is_dirty());
12000 assert_eq!(buffer.text(), sample_text_3,)
12001 });
12002}
12003
12004#[gpui::test]
12005async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12006 init_test(cx, |_| {});
12007
12008 let fs = FakeFs::new(cx.executor());
12009 fs.insert_tree(
12010 path!("/dir"),
12011 json!({
12012 "file1.rs": "fn main() { println!(\"hello\"); }",
12013 "file2.rs": "fn test() { println!(\"test\"); }",
12014 "file3.rs": "fn other() { println!(\"other\"); }\n",
12015 }),
12016 )
12017 .await;
12018
12019 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12020 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12021 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12022
12023 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12024 language_registry.add(rust_lang());
12025
12026 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12027 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12028
12029 // Open three buffers
12030 let buffer_1 = project
12031 .update(cx, |project, cx| {
12032 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12033 })
12034 .await
12035 .unwrap();
12036 let buffer_2 = project
12037 .update(cx, |project, cx| {
12038 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12039 })
12040 .await
12041 .unwrap();
12042 let buffer_3 = project
12043 .update(cx, |project, cx| {
12044 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12045 })
12046 .await
12047 .unwrap();
12048
12049 // Create a multi-buffer with all three buffers
12050 let multi_buffer = cx.new(|cx| {
12051 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12052 multi_buffer.push_excerpts(
12053 buffer_1.clone(),
12054 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12055 cx,
12056 );
12057 multi_buffer.push_excerpts(
12058 buffer_2.clone(),
12059 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12060 cx,
12061 );
12062 multi_buffer.push_excerpts(
12063 buffer_3.clone(),
12064 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12065 cx,
12066 );
12067 multi_buffer
12068 });
12069
12070 let editor = cx.new_window_entity(|window, cx| {
12071 Editor::new(
12072 EditorMode::full(),
12073 multi_buffer,
12074 Some(project.clone()),
12075 window,
12076 cx,
12077 )
12078 });
12079
12080 // Edit only the first buffer
12081 editor.update_in(cx, |editor, window, cx| {
12082 editor.change_selections(
12083 SelectionEffects::scroll(Autoscroll::Next),
12084 window,
12085 cx,
12086 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12087 );
12088 editor.insert("// edited", window, cx);
12089 });
12090
12091 // Verify that only buffer 1 is dirty
12092 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12093 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12094 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12095
12096 // Get write counts after file creation (files were created with initial content)
12097 // We expect each file to have been written once during creation
12098 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12099 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12100 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12101
12102 // Perform autosave
12103 let save_task = editor.update_in(cx, |editor, window, cx| {
12104 editor.save(
12105 SaveOptions {
12106 format: true,
12107 autosave: true,
12108 },
12109 project.clone(),
12110 window,
12111 cx,
12112 )
12113 });
12114 save_task.await.unwrap();
12115
12116 // Only the dirty buffer should have been saved
12117 assert_eq!(
12118 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12119 1,
12120 "Buffer 1 was dirty, so it should have been written once during autosave"
12121 );
12122 assert_eq!(
12123 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12124 0,
12125 "Buffer 2 was clean, so it should not have been written during autosave"
12126 );
12127 assert_eq!(
12128 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12129 0,
12130 "Buffer 3 was clean, so it should not have been written during autosave"
12131 );
12132
12133 // Verify buffer states after autosave
12134 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12135 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12136 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12137
12138 // Now perform a manual save (format = true)
12139 let save_task = editor.update_in(cx, |editor, window, cx| {
12140 editor.save(
12141 SaveOptions {
12142 format: true,
12143 autosave: false,
12144 },
12145 project.clone(),
12146 window,
12147 cx,
12148 )
12149 });
12150 save_task.await.unwrap();
12151
12152 // During manual save, clean buffers don't get written to disk
12153 // They just get did_save called for language server notifications
12154 assert_eq!(
12155 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12156 1,
12157 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12158 );
12159 assert_eq!(
12160 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12161 0,
12162 "Buffer 2 should not have been written at all"
12163 );
12164 assert_eq!(
12165 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12166 0,
12167 "Buffer 3 should not have been written at all"
12168 );
12169}
12170
12171async fn setup_range_format_test(
12172 cx: &mut TestAppContext,
12173) -> (
12174 Entity<Project>,
12175 Entity<Editor>,
12176 &mut gpui::VisualTestContext,
12177 lsp::FakeLanguageServer,
12178) {
12179 init_test(cx, |_| {});
12180
12181 let fs = FakeFs::new(cx.executor());
12182 fs.insert_file(path!("/file.rs"), Default::default()).await;
12183
12184 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12185
12186 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12187 language_registry.add(rust_lang());
12188 let mut fake_servers = language_registry.register_fake_lsp(
12189 "Rust",
12190 FakeLspAdapter {
12191 capabilities: lsp::ServerCapabilities {
12192 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12193 ..lsp::ServerCapabilities::default()
12194 },
12195 ..FakeLspAdapter::default()
12196 },
12197 );
12198
12199 let buffer = project
12200 .update(cx, |project, cx| {
12201 project.open_local_buffer(path!("/file.rs"), cx)
12202 })
12203 .await
12204 .unwrap();
12205
12206 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12207 let (editor, cx) = cx.add_window_view(|window, cx| {
12208 build_editor_with_project(project.clone(), buffer, window, cx)
12209 });
12210
12211 cx.executor().start_waiting();
12212 let fake_server = fake_servers.next().await.unwrap();
12213
12214 (project, editor, cx, fake_server)
12215}
12216
12217#[gpui::test]
12218async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12219 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12220
12221 editor.update_in(cx, |editor, window, cx| {
12222 editor.set_text("one\ntwo\nthree\n", window, cx)
12223 });
12224 assert!(cx.read(|cx| editor.is_dirty(cx)));
12225
12226 let save = editor
12227 .update_in(cx, |editor, window, cx| {
12228 editor.save(
12229 SaveOptions {
12230 format: true,
12231 autosave: false,
12232 },
12233 project.clone(),
12234 window,
12235 cx,
12236 )
12237 })
12238 .unwrap();
12239 fake_server
12240 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12241 assert_eq!(
12242 params.text_document.uri,
12243 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12244 );
12245 assert_eq!(params.options.tab_size, 4);
12246 Ok(Some(vec![lsp::TextEdit::new(
12247 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12248 ", ".to_string(),
12249 )]))
12250 })
12251 .next()
12252 .await;
12253 cx.executor().start_waiting();
12254 save.await;
12255 assert_eq!(
12256 editor.update(cx, |editor, cx| editor.text(cx)),
12257 "one, two\nthree\n"
12258 );
12259 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12260}
12261
12262#[gpui::test]
12263async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12264 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12265
12266 editor.update_in(cx, |editor, window, cx| {
12267 editor.set_text("one\ntwo\nthree\n", window, cx)
12268 });
12269 assert!(cx.read(|cx| editor.is_dirty(cx)));
12270
12271 // Test that save still works when formatting hangs
12272 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12273 move |params, _| async move {
12274 assert_eq!(
12275 params.text_document.uri,
12276 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12277 );
12278 futures::future::pending::<()>().await;
12279 unreachable!()
12280 },
12281 );
12282 let save = editor
12283 .update_in(cx, |editor, window, cx| {
12284 editor.save(
12285 SaveOptions {
12286 format: true,
12287 autosave: false,
12288 },
12289 project.clone(),
12290 window,
12291 cx,
12292 )
12293 })
12294 .unwrap();
12295 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12296 cx.executor().start_waiting();
12297 save.await;
12298 assert_eq!(
12299 editor.update(cx, |editor, cx| editor.text(cx)),
12300 "one\ntwo\nthree\n"
12301 );
12302 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12303}
12304
12305#[gpui::test]
12306async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12307 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12308
12309 // Buffer starts clean, no formatting should be requested
12310 let save = editor
12311 .update_in(cx, |editor, window, cx| {
12312 editor.save(
12313 SaveOptions {
12314 format: false,
12315 autosave: false,
12316 },
12317 project.clone(),
12318 window,
12319 cx,
12320 )
12321 })
12322 .unwrap();
12323 let _pending_format_request = fake_server
12324 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12325 panic!("Should not be invoked");
12326 })
12327 .next();
12328 cx.executor().start_waiting();
12329 save.await;
12330 cx.run_until_parked();
12331}
12332
12333#[gpui::test]
12334async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12335 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12336
12337 // Set Rust language override and assert overridden tabsize is sent to language server
12338 update_test_language_settings(cx, |settings| {
12339 settings.languages.0.insert(
12340 "Rust".into(),
12341 LanguageSettingsContent {
12342 tab_size: NonZeroU32::new(8),
12343 ..Default::default()
12344 },
12345 );
12346 });
12347
12348 editor.update_in(cx, |editor, window, cx| {
12349 editor.set_text("something_new\n", window, cx)
12350 });
12351 assert!(cx.read(|cx| editor.is_dirty(cx)));
12352 let save = editor
12353 .update_in(cx, |editor, window, cx| {
12354 editor.save(
12355 SaveOptions {
12356 format: true,
12357 autosave: false,
12358 },
12359 project.clone(),
12360 window,
12361 cx,
12362 )
12363 })
12364 .unwrap();
12365 fake_server
12366 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12367 assert_eq!(
12368 params.text_document.uri,
12369 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12370 );
12371 assert_eq!(params.options.tab_size, 8);
12372 Ok(Some(Vec::new()))
12373 })
12374 .next()
12375 .await;
12376 save.await;
12377}
12378
12379#[gpui::test]
12380async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12381 init_test(cx, |settings| {
12382 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12383 settings::LanguageServerFormatterSpecifier::Current,
12384 )))
12385 });
12386
12387 let fs = FakeFs::new(cx.executor());
12388 fs.insert_file(path!("/file.rs"), Default::default()).await;
12389
12390 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12391
12392 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12393 language_registry.add(Arc::new(Language::new(
12394 LanguageConfig {
12395 name: "Rust".into(),
12396 matcher: LanguageMatcher {
12397 path_suffixes: vec!["rs".to_string()],
12398 ..Default::default()
12399 },
12400 ..LanguageConfig::default()
12401 },
12402 Some(tree_sitter_rust::LANGUAGE.into()),
12403 )));
12404 update_test_language_settings(cx, |settings| {
12405 // Enable Prettier formatting for the same buffer, and ensure
12406 // LSP is called instead of Prettier.
12407 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12408 });
12409 let mut fake_servers = language_registry.register_fake_lsp(
12410 "Rust",
12411 FakeLspAdapter {
12412 capabilities: lsp::ServerCapabilities {
12413 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12414 ..Default::default()
12415 },
12416 ..Default::default()
12417 },
12418 );
12419
12420 let buffer = project
12421 .update(cx, |project, cx| {
12422 project.open_local_buffer(path!("/file.rs"), cx)
12423 })
12424 .await
12425 .unwrap();
12426
12427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12428 let (editor, cx) = cx.add_window_view(|window, cx| {
12429 build_editor_with_project(project.clone(), buffer, window, cx)
12430 });
12431 editor.update_in(cx, |editor, window, cx| {
12432 editor.set_text("one\ntwo\nthree\n", window, cx)
12433 });
12434
12435 cx.executor().start_waiting();
12436 let fake_server = fake_servers.next().await.unwrap();
12437
12438 let format = editor
12439 .update_in(cx, |editor, window, cx| {
12440 editor.perform_format(
12441 project.clone(),
12442 FormatTrigger::Manual,
12443 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12444 window,
12445 cx,
12446 )
12447 })
12448 .unwrap();
12449 fake_server
12450 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12451 assert_eq!(
12452 params.text_document.uri,
12453 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12454 );
12455 assert_eq!(params.options.tab_size, 4);
12456 Ok(Some(vec![lsp::TextEdit::new(
12457 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12458 ", ".to_string(),
12459 )]))
12460 })
12461 .next()
12462 .await;
12463 cx.executor().start_waiting();
12464 format.await;
12465 assert_eq!(
12466 editor.update(cx, |editor, cx| editor.text(cx)),
12467 "one, two\nthree\n"
12468 );
12469
12470 editor.update_in(cx, |editor, window, cx| {
12471 editor.set_text("one\ntwo\nthree\n", window, cx)
12472 });
12473 // Ensure we don't lock if formatting hangs.
12474 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12475 move |params, _| async move {
12476 assert_eq!(
12477 params.text_document.uri,
12478 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12479 );
12480 futures::future::pending::<()>().await;
12481 unreachable!()
12482 },
12483 );
12484 let format = editor
12485 .update_in(cx, |editor, window, cx| {
12486 editor.perform_format(
12487 project,
12488 FormatTrigger::Manual,
12489 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12490 window,
12491 cx,
12492 )
12493 })
12494 .unwrap();
12495 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12496 cx.executor().start_waiting();
12497 format.await;
12498 assert_eq!(
12499 editor.update(cx, |editor, cx| editor.text(cx)),
12500 "one\ntwo\nthree\n"
12501 );
12502}
12503
12504#[gpui::test]
12505async fn test_multiple_formatters(cx: &mut TestAppContext) {
12506 init_test(cx, |settings| {
12507 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12508 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12509 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12510 Formatter::CodeAction("code-action-1".into()),
12511 Formatter::CodeAction("code-action-2".into()),
12512 ]))
12513 });
12514
12515 let fs = FakeFs::new(cx.executor());
12516 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12517 .await;
12518
12519 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12520 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12521 language_registry.add(rust_lang());
12522
12523 let mut fake_servers = language_registry.register_fake_lsp(
12524 "Rust",
12525 FakeLspAdapter {
12526 capabilities: lsp::ServerCapabilities {
12527 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12528 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12529 commands: vec!["the-command-for-code-action-1".into()],
12530 ..Default::default()
12531 }),
12532 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12533 ..Default::default()
12534 },
12535 ..Default::default()
12536 },
12537 );
12538
12539 let buffer = project
12540 .update(cx, |project, cx| {
12541 project.open_local_buffer(path!("/file.rs"), cx)
12542 })
12543 .await
12544 .unwrap();
12545
12546 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12547 let (editor, cx) = cx.add_window_view(|window, cx| {
12548 build_editor_with_project(project.clone(), buffer, window, cx)
12549 });
12550
12551 cx.executor().start_waiting();
12552
12553 let fake_server = fake_servers.next().await.unwrap();
12554 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12555 move |_params, _| async move {
12556 Ok(Some(vec![lsp::TextEdit::new(
12557 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12558 "applied-formatting\n".to_string(),
12559 )]))
12560 },
12561 );
12562 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12563 move |params, _| async move {
12564 let requested_code_actions = params.context.only.expect("Expected code action request");
12565 assert_eq!(requested_code_actions.len(), 1);
12566
12567 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12568 let code_action = match requested_code_actions[0].as_str() {
12569 "code-action-1" => lsp::CodeAction {
12570 kind: Some("code-action-1".into()),
12571 edit: Some(lsp::WorkspaceEdit::new(
12572 [(
12573 uri,
12574 vec![lsp::TextEdit::new(
12575 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12576 "applied-code-action-1-edit\n".to_string(),
12577 )],
12578 )]
12579 .into_iter()
12580 .collect(),
12581 )),
12582 command: Some(lsp::Command {
12583 command: "the-command-for-code-action-1".into(),
12584 ..Default::default()
12585 }),
12586 ..Default::default()
12587 },
12588 "code-action-2" => lsp::CodeAction {
12589 kind: Some("code-action-2".into()),
12590 edit: Some(lsp::WorkspaceEdit::new(
12591 [(
12592 uri,
12593 vec![lsp::TextEdit::new(
12594 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12595 "applied-code-action-2-edit\n".to_string(),
12596 )],
12597 )]
12598 .into_iter()
12599 .collect(),
12600 )),
12601 ..Default::default()
12602 },
12603 req => panic!("Unexpected code action request: {:?}", req),
12604 };
12605 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12606 code_action,
12607 )]))
12608 },
12609 );
12610
12611 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12612 move |params, _| async move { Ok(params) }
12613 });
12614
12615 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12616 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12617 let fake = fake_server.clone();
12618 let lock = command_lock.clone();
12619 move |params, _| {
12620 assert_eq!(params.command, "the-command-for-code-action-1");
12621 let fake = fake.clone();
12622 let lock = lock.clone();
12623 async move {
12624 lock.lock().await;
12625 fake.server
12626 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12627 label: None,
12628 edit: lsp::WorkspaceEdit {
12629 changes: Some(
12630 [(
12631 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12632 vec![lsp::TextEdit {
12633 range: lsp::Range::new(
12634 lsp::Position::new(0, 0),
12635 lsp::Position::new(0, 0),
12636 ),
12637 new_text: "applied-code-action-1-command\n".into(),
12638 }],
12639 )]
12640 .into_iter()
12641 .collect(),
12642 ),
12643 ..Default::default()
12644 },
12645 })
12646 .await
12647 .into_response()
12648 .unwrap();
12649 Ok(Some(json!(null)))
12650 }
12651 }
12652 });
12653
12654 cx.executor().start_waiting();
12655 editor
12656 .update_in(cx, |editor, window, cx| {
12657 editor.perform_format(
12658 project.clone(),
12659 FormatTrigger::Manual,
12660 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12661 window,
12662 cx,
12663 )
12664 })
12665 .unwrap()
12666 .await;
12667 editor.update(cx, |editor, cx| {
12668 assert_eq!(
12669 editor.text(cx),
12670 r#"
12671 applied-code-action-2-edit
12672 applied-code-action-1-command
12673 applied-code-action-1-edit
12674 applied-formatting
12675 one
12676 two
12677 three
12678 "#
12679 .unindent()
12680 );
12681 });
12682
12683 editor.update_in(cx, |editor, window, cx| {
12684 editor.undo(&Default::default(), window, cx);
12685 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12686 });
12687
12688 // Perform a manual edit while waiting for an LSP command
12689 // that's being run as part of a formatting code action.
12690 let lock_guard = command_lock.lock().await;
12691 let format = editor
12692 .update_in(cx, |editor, window, cx| {
12693 editor.perform_format(
12694 project.clone(),
12695 FormatTrigger::Manual,
12696 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12697 window,
12698 cx,
12699 )
12700 })
12701 .unwrap();
12702 cx.run_until_parked();
12703 editor.update(cx, |editor, cx| {
12704 assert_eq!(
12705 editor.text(cx),
12706 r#"
12707 applied-code-action-1-edit
12708 applied-formatting
12709 one
12710 two
12711 three
12712 "#
12713 .unindent()
12714 );
12715
12716 editor.buffer.update(cx, |buffer, cx| {
12717 let ix = buffer.len(cx);
12718 buffer.edit([(ix..ix, "edited\n")], None, cx);
12719 });
12720 });
12721
12722 // Allow the LSP command to proceed. Because the buffer was edited,
12723 // the second code action will not be run.
12724 drop(lock_guard);
12725 format.await;
12726 editor.update_in(cx, |editor, window, cx| {
12727 assert_eq!(
12728 editor.text(cx),
12729 r#"
12730 applied-code-action-1-command
12731 applied-code-action-1-edit
12732 applied-formatting
12733 one
12734 two
12735 three
12736 edited
12737 "#
12738 .unindent()
12739 );
12740
12741 // The manual edit is undone first, because it is the last thing the user did
12742 // (even though the command completed afterwards).
12743 editor.undo(&Default::default(), window, cx);
12744 assert_eq!(
12745 editor.text(cx),
12746 r#"
12747 applied-code-action-1-command
12748 applied-code-action-1-edit
12749 applied-formatting
12750 one
12751 two
12752 three
12753 "#
12754 .unindent()
12755 );
12756
12757 // All the formatting (including the command, which completed after the manual edit)
12758 // is undone together.
12759 editor.undo(&Default::default(), window, cx);
12760 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12761 });
12762}
12763
12764#[gpui::test]
12765async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12766 init_test(cx, |settings| {
12767 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12768 settings::LanguageServerFormatterSpecifier::Current,
12769 )]))
12770 });
12771
12772 let fs = FakeFs::new(cx.executor());
12773 fs.insert_file(path!("/file.ts"), Default::default()).await;
12774
12775 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12776
12777 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12778 language_registry.add(Arc::new(Language::new(
12779 LanguageConfig {
12780 name: "TypeScript".into(),
12781 matcher: LanguageMatcher {
12782 path_suffixes: vec!["ts".to_string()],
12783 ..Default::default()
12784 },
12785 ..LanguageConfig::default()
12786 },
12787 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12788 )));
12789 update_test_language_settings(cx, |settings| {
12790 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12791 });
12792 let mut fake_servers = language_registry.register_fake_lsp(
12793 "TypeScript",
12794 FakeLspAdapter {
12795 capabilities: lsp::ServerCapabilities {
12796 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12797 ..Default::default()
12798 },
12799 ..Default::default()
12800 },
12801 );
12802
12803 let buffer = project
12804 .update(cx, |project, cx| {
12805 project.open_local_buffer(path!("/file.ts"), cx)
12806 })
12807 .await
12808 .unwrap();
12809
12810 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12811 let (editor, cx) = cx.add_window_view(|window, cx| {
12812 build_editor_with_project(project.clone(), buffer, window, cx)
12813 });
12814 editor.update_in(cx, |editor, window, cx| {
12815 editor.set_text(
12816 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12817 window,
12818 cx,
12819 )
12820 });
12821
12822 cx.executor().start_waiting();
12823 let fake_server = fake_servers.next().await.unwrap();
12824
12825 let format = editor
12826 .update_in(cx, |editor, window, cx| {
12827 editor.perform_code_action_kind(
12828 project.clone(),
12829 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12830 window,
12831 cx,
12832 )
12833 })
12834 .unwrap();
12835 fake_server
12836 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12837 assert_eq!(
12838 params.text_document.uri,
12839 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12840 );
12841 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12842 lsp::CodeAction {
12843 title: "Organize Imports".to_string(),
12844 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12845 edit: Some(lsp::WorkspaceEdit {
12846 changes: Some(
12847 [(
12848 params.text_document.uri.clone(),
12849 vec![lsp::TextEdit::new(
12850 lsp::Range::new(
12851 lsp::Position::new(1, 0),
12852 lsp::Position::new(2, 0),
12853 ),
12854 "".to_string(),
12855 )],
12856 )]
12857 .into_iter()
12858 .collect(),
12859 ),
12860 ..Default::default()
12861 }),
12862 ..Default::default()
12863 },
12864 )]))
12865 })
12866 .next()
12867 .await;
12868 cx.executor().start_waiting();
12869 format.await;
12870 assert_eq!(
12871 editor.update(cx, |editor, cx| editor.text(cx)),
12872 "import { a } from 'module';\n\nconst x = a;\n"
12873 );
12874
12875 editor.update_in(cx, |editor, window, cx| {
12876 editor.set_text(
12877 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12878 window,
12879 cx,
12880 )
12881 });
12882 // Ensure we don't lock if code action hangs.
12883 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12884 move |params, _| async move {
12885 assert_eq!(
12886 params.text_document.uri,
12887 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12888 );
12889 futures::future::pending::<()>().await;
12890 unreachable!()
12891 },
12892 );
12893 let format = editor
12894 .update_in(cx, |editor, window, cx| {
12895 editor.perform_code_action_kind(
12896 project,
12897 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12898 window,
12899 cx,
12900 )
12901 })
12902 .unwrap();
12903 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12904 cx.executor().start_waiting();
12905 format.await;
12906 assert_eq!(
12907 editor.update(cx, |editor, cx| editor.text(cx)),
12908 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12909 );
12910}
12911
12912#[gpui::test]
12913async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12914 init_test(cx, |_| {});
12915
12916 let mut cx = EditorLspTestContext::new_rust(
12917 lsp::ServerCapabilities {
12918 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12919 ..Default::default()
12920 },
12921 cx,
12922 )
12923 .await;
12924
12925 cx.set_state(indoc! {"
12926 one.twoˇ
12927 "});
12928
12929 // The format request takes a long time. When it completes, it inserts
12930 // a newline and an indent before the `.`
12931 cx.lsp
12932 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12933 let executor = cx.background_executor().clone();
12934 async move {
12935 executor.timer(Duration::from_millis(100)).await;
12936 Ok(Some(vec![lsp::TextEdit {
12937 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12938 new_text: "\n ".into(),
12939 }]))
12940 }
12941 });
12942
12943 // Submit a format request.
12944 let format_1 = cx
12945 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12946 .unwrap();
12947 cx.executor().run_until_parked();
12948
12949 // Submit a second format request.
12950 let format_2 = cx
12951 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12952 .unwrap();
12953 cx.executor().run_until_parked();
12954
12955 // Wait for both format requests to complete
12956 cx.executor().advance_clock(Duration::from_millis(200));
12957 cx.executor().start_waiting();
12958 format_1.await.unwrap();
12959 cx.executor().start_waiting();
12960 format_2.await.unwrap();
12961
12962 // The formatting edits only happens once.
12963 cx.assert_editor_state(indoc! {"
12964 one
12965 .twoˇ
12966 "});
12967}
12968
12969#[gpui::test]
12970async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12971 init_test(cx, |settings| {
12972 settings.defaults.formatter = Some(FormatterList::default())
12973 });
12974
12975 let mut cx = EditorLspTestContext::new_rust(
12976 lsp::ServerCapabilities {
12977 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12978 ..Default::default()
12979 },
12980 cx,
12981 )
12982 .await;
12983
12984 // Record which buffer changes have been sent to the language server
12985 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12986 cx.lsp
12987 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12988 let buffer_changes = buffer_changes.clone();
12989 move |params, _| {
12990 buffer_changes.lock().extend(
12991 params
12992 .content_changes
12993 .into_iter()
12994 .map(|e| (e.range.unwrap(), e.text)),
12995 );
12996 }
12997 });
12998 // Handle formatting requests to the language server.
12999 cx.lsp
13000 .set_request_handler::<lsp::request::Formatting, _, _>({
13001 let buffer_changes = buffer_changes.clone();
13002 move |_, _| {
13003 let buffer_changes = buffer_changes.clone();
13004 // Insert blank lines between each line of the buffer.
13005 async move {
13006 // When formatting is requested, trailing whitespace has already been stripped,
13007 // and the trailing newline has already been added.
13008 assert_eq!(
13009 &buffer_changes.lock()[1..],
13010 &[
13011 (
13012 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13013 "".into()
13014 ),
13015 (
13016 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13017 "".into()
13018 ),
13019 (
13020 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13021 "\n".into()
13022 ),
13023 ]
13024 );
13025
13026 Ok(Some(vec![
13027 lsp::TextEdit {
13028 range: lsp::Range::new(
13029 lsp::Position::new(1, 0),
13030 lsp::Position::new(1, 0),
13031 ),
13032 new_text: "\n".into(),
13033 },
13034 lsp::TextEdit {
13035 range: lsp::Range::new(
13036 lsp::Position::new(2, 0),
13037 lsp::Position::new(2, 0),
13038 ),
13039 new_text: "\n".into(),
13040 },
13041 ]))
13042 }
13043 }
13044 });
13045
13046 // Set up a buffer white some trailing whitespace and no trailing newline.
13047 cx.set_state(
13048 &[
13049 "one ", //
13050 "twoˇ", //
13051 "three ", //
13052 "four", //
13053 ]
13054 .join("\n"),
13055 );
13056 cx.run_until_parked();
13057
13058 // Submit a format request.
13059 let format = cx
13060 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13061 .unwrap();
13062
13063 cx.run_until_parked();
13064 // After formatting the buffer, the trailing whitespace is stripped,
13065 // a newline is appended, and the edits provided by the language server
13066 // have been applied.
13067 format.await.unwrap();
13068
13069 cx.assert_editor_state(
13070 &[
13071 "one", //
13072 "", //
13073 "twoˇ", //
13074 "", //
13075 "three", //
13076 "four", //
13077 "", //
13078 ]
13079 .join("\n"),
13080 );
13081
13082 // Undoing the formatting undoes the trailing whitespace removal, the
13083 // trailing newline, and the LSP edits.
13084 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13085 cx.assert_editor_state(
13086 &[
13087 "one ", //
13088 "twoˇ", //
13089 "three ", //
13090 "four", //
13091 ]
13092 .join("\n"),
13093 );
13094}
13095
13096#[gpui::test]
13097async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13098 cx: &mut TestAppContext,
13099) {
13100 init_test(cx, |_| {});
13101
13102 cx.update(|cx| {
13103 cx.update_global::<SettingsStore, _>(|settings, cx| {
13104 settings.update_user_settings(cx, |settings| {
13105 settings.editor.auto_signature_help = Some(true);
13106 });
13107 });
13108 });
13109
13110 let mut cx = EditorLspTestContext::new_rust(
13111 lsp::ServerCapabilities {
13112 signature_help_provider: Some(lsp::SignatureHelpOptions {
13113 ..Default::default()
13114 }),
13115 ..Default::default()
13116 },
13117 cx,
13118 )
13119 .await;
13120
13121 let language = Language::new(
13122 LanguageConfig {
13123 name: "Rust".into(),
13124 brackets: BracketPairConfig {
13125 pairs: vec![
13126 BracketPair {
13127 start: "{".to_string(),
13128 end: "}".to_string(),
13129 close: true,
13130 surround: true,
13131 newline: true,
13132 },
13133 BracketPair {
13134 start: "(".to_string(),
13135 end: ")".to_string(),
13136 close: true,
13137 surround: true,
13138 newline: true,
13139 },
13140 BracketPair {
13141 start: "/*".to_string(),
13142 end: " */".to_string(),
13143 close: true,
13144 surround: true,
13145 newline: true,
13146 },
13147 BracketPair {
13148 start: "[".to_string(),
13149 end: "]".to_string(),
13150 close: false,
13151 surround: false,
13152 newline: true,
13153 },
13154 BracketPair {
13155 start: "\"".to_string(),
13156 end: "\"".to_string(),
13157 close: true,
13158 surround: true,
13159 newline: false,
13160 },
13161 BracketPair {
13162 start: "<".to_string(),
13163 end: ">".to_string(),
13164 close: false,
13165 surround: true,
13166 newline: true,
13167 },
13168 ],
13169 ..Default::default()
13170 },
13171 autoclose_before: "})]".to_string(),
13172 ..Default::default()
13173 },
13174 Some(tree_sitter_rust::LANGUAGE.into()),
13175 );
13176 let language = Arc::new(language);
13177
13178 cx.language_registry().add(language.clone());
13179 cx.update_buffer(|buffer, cx| {
13180 buffer.set_language(Some(language), cx);
13181 });
13182
13183 cx.set_state(
13184 &r#"
13185 fn main() {
13186 sampleˇ
13187 }
13188 "#
13189 .unindent(),
13190 );
13191
13192 cx.update_editor(|editor, window, cx| {
13193 editor.handle_input("(", window, cx);
13194 });
13195 cx.assert_editor_state(
13196 &"
13197 fn main() {
13198 sample(ˇ)
13199 }
13200 "
13201 .unindent(),
13202 );
13203
13204 let mocked_response = lsp::SignatureHelp {
13205 signatures: vec![lsp::SignatureInformation {
13206 label: "fn sample(param1: u8, param2: u8)".to_string(),
13207 documentation: None,
13208 parameters: Some(vec![
13209 lsp::ParameterInformation {
13210 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13211 documentation: None,
13212 },
13213 lsp::ParameterInformation {
13214 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13215 documentation: None,
13216 },
13217 ]),
13218 active_parameter: None,
13219 }],
13220 active_signature: Some(0),
13221 active_parameter: Some(0),
13222 };
13223 handle_signature_help_request(&mut cx, mocked_response).await;
13224
13225 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13226 .await;
13227
13228 cx.editor(|editor, _, _| {
13229 let signature_help_state = editor.signature_help_state.popover().cloned();
13230 let signature = signature_help_state.unwrap();
13231 assert_eq!(
13232 signature.signatures[signature.current_signature].label,
13233 "fn sample(param1: u8, param2: u8)"
13234 );
13235 });
13236}
13237
13238#[gpui::test]
13239async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13240 init_test(cx, |_| {});
13241
13242 cx.update(|cx| {
13243 cx.update_global::<SettingsStore, _>(|settings, cx| {
13244 settings.update_user_settings(cx, |settings| {
13245 settings.editor.auto_signature_help = Some(false);
13246 settings.editor.show_signature_help_after_edits = Some(false);
13247 });
13248 });
13249 });
13250
13251 let mut cx = EditorLspTestContext::new_rust(
13252 lsp::ServerCapabilities {
13253 signature_help_provider: Some(lsp::SignatureHelpOptions {
13254 ..Default::default()
13255 }),
13256 ..Default::default()
13257 },
13258 cx,
13259 )
13260 .await;
13261
13262 let language = Language::new(
13263 LanguageConfig {
13264 name: "Rust".into(),
13265 brackets: BracketPairConfig {
13266 pairs: vec![
13267 BracketPair {
13268 start: "{".to_string(),
13269 end: "}".to_string(),
13270 close: true,
13271 surround: true,
13272 newline: true,
13273 },
13274 BracketPair {
13275 start: "(".to_string(),
13276 end: ")".to_string(),
13277 close: true,
13278 surround: true,
13279 newline: true,
13280 },
13281 BracketPair {
13282 start: "/*".to_string(),
13283 end: " */".to_string(),
13284 close: true,
13285 surround: true,
13286 newline: true,
13287 },
13288 BracketPair {
13289 start: "[".to_string(),
13290 end: "]".to_string(),
13291 close: false,
13292 surround: false,
13293 newline: true,
13294 },
13295 BracketPair {
13296 start: "\"".to_string(),
13297 end: "\"".to_string(),
13298 close: true,
13299 surround: true,
13300 newline: false,
13301 },
13302 BracketPair {
13303 start: "<".to_string(),
13304 end: ">".to_string(),
13305 close: false,
13306 surround: true,
13307 newline: true,
13308 },
13309 ],
13310 ..Default::default()
13311 },
13312 autoclose_before: "})]".to_string(),
13313 ..Default::default()
13314 },
13315 Some(tree_sitter_rust::LANGUAGE.into()),
13316 );
13317 let language = Arc::new(language);
13318
13319 cx.language_registry().add(language.clone());
13320 cx.update_buffer(|buffer, cx| {
13321 buffer.set_language(Some(language), cx);
13322 });
13323
13324 // Ensure that signature_help is not called when no signature help is enabled.
13325 cx.set_state(
13326 &r#"
13327 fn main() {
13328 sampleˇ
13329 }
13330 "#
13331 .unindent(),
13332 );
13333 cx.update_editor(|editor, window, cx| {
13334 editor.handle_input("(", window, cx);
13335 });
13336 cx.assert_editor_state(
13337 &"
13338 fn main() {
13339 sample(ˇ)
13340 }
13341 "
13342 .unindent(),
13343 );
13344 cx.editor(|editor, _, _| {
13345 assert!(editor.signature_help_state.task().is_none());
13346 });
13347
13348 let mocked_response = lsp::SignatureHelp {
13349 signatures: vec![lsp::SignatureInformation {
13350 label: "fn sample(param1: u8, param2: u8)".to_string(),
13351 documentation: None,
13352 parameters: Some(vec![
13353 lsp::ParameterInformation {
13354 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13355 documentation: None,
13356 },
13357 lsp::ParameterInformation {
13358 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13359 documentation: None,
13360 },
13361 ]),
13362 active_parameter: None,
13363 }],
13364 active_signature: Some(0),
13365 active_parameter: Some(0),
13366 };
13367
13368 // Ensure that signature_help is called when enabled afte edits
13369 cx.update(|_, cx| {
13370 cx.update_global::<SettingsStore, _>(|settings, cx| {
13371 settings.update_user_settings(cx, |settings| {
13372 settings.editor.auto_signature_help = Some(false);
13373 settings.editor.show_signature_help_after_edits = Some(true);
13374 });
13375 });
13376 });
13377 cx.set_state(
13378 &r#"
13379 fn main() {
13380 sampleˇ
13381 }
13382 "#
13383 .unindent(),
13384 );
13385 cx.update_editor(|editor, window, cx| {
13386 editor.handle_input("(", window, cx);
13387 });
13388 cx.assert_editor_state(
13389 &"
13390 fn main() {
13391 sample(ˇ)
13392 }
13393 "
13394 .unindent(),
13395 );
13396 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13397 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13398 .await;
13399 cx.update_editor(|editor, _, _| {
13400 let signature_help_state = editor.signature_help_state.popover().cloned();
13401 assert!(signature_help_state.is_some());
13402 let signature = signature_help_state.unwrap();
13403 assert_eq!(
13404 signature.signatures[signature.current_signature].label,
13405 "fn sample(param1: u8, param2: u8)"
13406 );
13407 editor.signature_help_state = SignatureHelpState::default();
13408 });
13409
13410 // Ensure that signature_help is called when auto signature help override is enabled
13411 cx.update(|_, cx| {
13412 cx.update_global::<SettingsStore, _>(|settings, cx| {
13413 settings.update_user_settings(cx, |settings| {
13414 settings.editor.auto_signature_help = Some(true);
13415 settings.editor.show_signature_help_after_edits = Some(false);
13416 });
13417 });
13418 });
13419 cx.set_state(
13420 &r#"
13421 fn main() {
13422 sampleˇ
13423 }
13424 "#
13425 .unindent(),
13426 );
13427 cx.update_editor(|editor, window, cx| {
13428 editor.handle_input("(", window, cx);
13429 });
13430 cx.assert_editor_state(
13431 &"
13432 fn main() {
13433 sample(ˇ)
13434 }
13435 "
13436 .unindent(),
13437 );
13438 handle_signature_help_request(&mut cx, mocked_response).await;
13439 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13440 .await;
13441 cx.editor(|editor, _, _| {
13442 let signature_help_state = editor.signature_help_state.popover().cloned();
13443 assert!(signature_help_state.is_some());
13444 let signature = signature_help_state.unwrap();
13445 assert_eq!(
13446 signature.signatures[signature.current_signature].label,
13447 "fn sample(param1: u8, param2: u8)"
13448 );
13449 });
13450}
13451
13452#[gpui::test]
13453async fn test_signature_help(cx: &mut TestAppContext) {
13454 init_test(cx, |_| {});
13455 cx.update(|cx| {
13456 cx.update_global::<SettingsStore, _>(|settings, cx| {
13457 settings.update_user_settings(cx, |settings| {
13458 settings.editor.auto_signature_help = Some(true);
13459 });
13460 });
13461 });
13462
13463 let mut cx = EditorLspTestContext::new_rust(
13464 lsp::ServerCapabilities {
13465 signature_help_provider: Some(lsp::SignatureHelpOptions {
13466 ..Default::default()
13467 }),
13468 ..Default::default()
13469 },
13470 cx,
13471 )
13472 .await;
13473
13474 // A test that directly calls `show_signature_help`
13475 cx.update_editor(|editor, window, cx| {
13476 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13477 });
13478
13479 let mocked_response = lsp::SignatureHelp {
13480 signatures: vec![lsp::SignatureInformation {
13481 label: "fn sample(param1: u8, param2: u8)".to_string(),
13482 documentation: None,
13483 parameters: Some(vec![
13484 lsp::ParameterInformation {
13485 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13486 documentation: None,
13487 },
13488 lsp::ParameterInformation {
13489 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13490 documentation: None,
13491 },
13492 ]),
13493 active_parameter: None,
13494 }],
13495 active_signature: Some(0),
13496 active_parameter: Some(0),
13497 };
13498 handle_signature_help_request(&mut cx, mocked_response).await;
13499
13500 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13501 .await;
13502
13503 cx.editor(|editor, _, _| {
13504 let signature_help_state = editor.signature_help_state.popover().cloned();
13505 assert!(signature_help_state.is_some());
13506 let signature = signature_help_state.unwrap();
13507 assert_eq!(
13508 signature.signatures[signature.current_signature].label,
13509 "fn sample(param1: u8, param2: u8)"
13510 );
13511 });
13512
13513 // When exiting outside from inside the brackets, `signature_help` is closed.
13514 cx.set_state(indoc! {"
13515 fn main() {
13516 sample(ˇ);
13517 }
13518
13519 fn sample(param1: u8, param2: u8) {}
13520 "});
13521
13522 cx.update_editor(|editor, window, cx| {
13523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13524 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13525 });
13526 });
13527
13528 let mocked_response = lsp::SignatureHelp {
13529 signatures: Vec::new(),
13530 active_signature: None,
13531 active_parameter: None,
13532 };
13533 handle_signature_help_request(&mut cx, mocked_response).await;
13534
13535 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13536 .await;
13537
13538 cx.editor(|editor, _, _| {
13539 assert!(!editor.signature_help_state.is_shown());
13540 });
13541
13542 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13543 cx.set_state(indoc! {"
13544 fn main() {
13545 sample(ˇ);
13546 }
13547
13548 fn sample(param1: u8, param2: u8) {}
13549 "});
13550
13551 let mocked_response = lsp::SignatureHelp {
13552 signatures: vec![lsp::SignatureInformation {
13553 label: "fn sample(param1: u8, param2: u8)".to_string(),
13554 documentation: None,
13555 parameters: Some(vec![
13556 lsp::ParameterInformation {
13557 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13558 documentation: None,
13559 },
13560 lsp::ParameterInformation {
13561 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13562 documentation: None,
13563 },
13564 ]),
13565 active_parameter: None,
13566 }],
13567 active_signature: Some(0),
13568 active_parameter: Some(0),
13569 };
13570 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13571 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13572 .await;
13573 cx.editor(|editor, _, _| {
13574 assert!(editor.signature_help_state.is_shown());
13575 });
13576
13577 // Restore the popover with more parameter input
13578 cx.set_state(indoc! {"
13579 fn main() {
13580 sample(param1, param2ˇ);
13581 }
13582
13583 fn sample(param1: u8, param2: u8) {}
13584 "});
13585
13586 let mocked_response = lsp::SignatureHelp {
13587 signatures: vec![lsp::SignatureInformation {
13588 label: "fn sample(param1: u8, param2: u8)".to_string(),
13589 documentation: None,
13590 parameters: Some(vec![
13591 lsp::ParameterInformation {
13592 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13593 documentation: None,
13594 },
13595 lsp::ParameterInformation {
13596 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13597 documentation: None,
13598 },
13599 ]),
13600 active_parameter: None,
13601 }],
13602 active_signature: Some(0),
13603 active_parameter: Some(1),
13604 };
13605 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13606 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13607 .await;
13608
13609 // When selecting a range, the popover is gone.
13610 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13611 cx.update_editor(|editor, window, cx| {
13612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13613 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13614 })
13615 });
13616 cx.assert_editor_state(indoc! {"
13617 fn main() {
13618 sample(param1, «ˇparam2»);
13619 }
13620
13621 fn sample(param1: u8, param2: u8) {}
13622 "});
13623 cx.editor(|editor, _, _| {
13624 assert!(!editor.signature_help_state.is_shown());
13625 });
13626
13627 // When unselecting again, the popover is back if within the brackets.
13628 cx.update_editor(|editor, window, cx| {
13629 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13630 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13631 })
13632 });
13633 cx.assert_editor_state(indoc! {"
13634 fn main() {
13635 sample(param1, ˇparam2);
13636 }
13637
13638 fn sample(param1: u8, param2: u8) {}
13639 "});
13640 handle_signature_help_request(&mut cx, mocked_response).await;
13641 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13642 .await;
13643 cx.editor(|editor, _, _| {
13644 assert!(editor.signature_help_state.is_shown());
13645 });
13646
13647 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13648 cx.update_editor(|editor, window, cx| {
13649 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13650 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13651 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13652 })
13653 });
13654 cx.assert_editor_state(indoc! {"
13655 fn main() {
13656 sample(param1, ˇparam2);
13657 }
13658
13659 fn sample(param1: u8, param2: u8) {}
13660 "});
13661
13662 let mocked_response = lsp::SignatureHelp {
13663 signatures: vec![lsp::SignatureInformation {
13664 label: "fn sample(param1: u8, param2: u8)".to_string(),
13665 documentation: None,
13666 parameters: Some(vec![
13667 lsp::ParameterInformation {
13668 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13669 documentation: None,
13670 },
13671 lsp::ParameterInformation {
13672 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13673 documentation: None,
13674 },
13675 ]),
13676 active_parameter: None,
13677 }],
13678 active_signature: Some(0),
13679 active_parameter: Some(1),
13680 };
13681 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13682 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13683 .await;
13684 cx.update_editor(|editor, _, cx| {
13685 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13686 });
13687 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13688 .await;
13689 cx.update_editor(|editor, window, cx| {
13690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13691 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13692 })
13693 });
13694 cx.assert_editor_state(indoc! {"
13695 fn main() {
13696 sample(param1, «ˇparam2»);
13697 }
13698
13699 fn sample(param1: u8, param2: u8) {}
13700 "});
13701 cx.update_editor(|editor, window, cx| {
13702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13703 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13704 })
13705 });
13706 cx.assert_editor_state(indoc! {"
13707 fn main() {
13708 sample(param1, ˇparam2);
13709 }
13710
13711 fn sample(param1: u8, param2: u8) {}
13712 "});
13713 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13714 .await;
13715}
13716
13717#[gpui::test]
13718async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13719 init_test(cx, |_| {});
13720
13721 let mut cx = EditorLspTestContext::new_rust(
13722 lsp::ServerCapabilities {
13723 signature_help_provider: Some(lsp::SignatureHelpOptions {
13724 ..Default::default()
13725 }),
13726 ..Default::default()
13727 },
13728 cx,
13729 )
13730 .await;
13731
13732 cx.set_state(indoc! {"
13733 fn main() {
13734 overloadedˇ
13735 }
13736 "});
13737
13738 cx.update_editor(|editor, window, cx| {
13739 editor.handle_input("(", window, cx);
13740 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13741 });
13742
13743 // Mock response with 3 signatures
13744 let mocked_response = lsp::SignatureHelp {
13745 signatures: vec![
13746 lsp::SignatureInformation {
13747 label: "fn overloaded(x: i32)".to_string(),
13748 documentation: None,
13749 parameters: Some(vec![lsp::ParameterInformation {
13750 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13751 documentation: None,
13752 }]),
13753 active_parameter: None,
13754 },
13755 lsp::SignatureInformation {
13756 label: "fn overloaded(x: i32, y: i32)".to_string(),
13757 documentation: None,
13758 parameters: Some(vec![
13759 lsp::ParameterInformation {
13760 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13761 documentation: None,
13762 },
13763 lsp::ParameterInformation {
13764 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13765 documentation: None,
13766 },
13767 ]),
13768 active_parameter: None,
13769 },
13770 lsp::SignatureInformation {
13771 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13772 documentation: None,
13773 parameters: Some(vec![
13774 lsp::ParameterInformation {
13775 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13776 documentation: None,
13777 },
13778 lsp::ParameterInformation {
13779 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13780 documentation: None,
13781 },
13782 lsp::ParameterInformation {
13783 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13784 documentation: None,
13785 },
13786 ]),
13787 active_parameter: None,
13788 },
13789 ],
13790 active_signature: Some(1),
13791 active_parameter: Some(0),
13792 };
13793 handle_signature_help_request(&mut cx, mocked_response).await;
13794
13795 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13796 .await;
13797
13798 // Verify we have multiple signatures and the right one is selected
13799 cx.editor(|editor, _, _| {
13800 let popover = editor.signature_help_state.popover().cloned().unwrap();
13801 assert_eq!(popover.signatures.len(), 3);
13802 // active_signature was 1, so that should be the current
13803 assert_eq!(popover.current_signature, 1);
13804 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13805 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13806 assert_eq!(
13807 popover.signatures[2].label,
13808 "fn overloaded(x: i32, y: i32, z: i32)"
13809 );
13810 });
13811
13812 // Test navigation functionality
13813 cx.update_editor(|editor, window, cx| {
13814 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13815 });
13816
13817 cx.editor(|editor, _, _| {
13818 let popover = editor.signature_help_state.popover().cloned().unwrap();
13819 assert_eq!(popover.current_signature, 2);
13820 });
13821
13822 // Test wrap around
13823 cx.update_editor(|editor, window, cx| {
13824 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13825 });
13826
13827 cx.editor(|editor, _, _| {
13828 let popover = editor.signature_help_state.popover().cloned().unwrap();
13829 assert_eq!(popover.current_signature, 0);
13830 });
13831
13832 // Test previous navigation
13833 cx.update_editor(|editor, window, cx| {
13834 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13835 });
13836
13837 cx.editor(|editor, _, _| {
13838 let popover = editor.signature_help_state.popover().cloned().unwrap();
13839 assert_eq!(popover.current_signature, 2);
13840 });
13841}
13842
13843#[gpui::test]
13844async fn test_completion_mode(cx: &mut TestAppContext) {
13845 init_test(cx, |_| {});
13846 let mut cx = EditorLspTestContext::new_rust(
13847 lsp::ServerCapabilities {
13848 completion_provider: Some(lsp::CompletionOptions {
13849 resolve_provider: Some(true),
13850 ..Default::default()
13851 }),
13852 ..Default::default()
13853 },
13854 cx,
13855 )
13856 .await;
13857
13858 struct Run {
13859 run_description: &'static str,
13860 initial_state: String,
13861 buffer_marked_text: String,
13862 completion_label: &'static str,
13863 completion_text: &'static str,
13864 expected_with_insert_mode: String,
13865 expected_with_replace_mode: String,
13866 expected_with_replace_subsequence_mode: String,
13867 expected_with_replace_suffix_mode: String,
13868 }
13869
13870 let runs = [
13871 Run {
13872 run_description: "Start of word matches completion text",
13873 initial_state: "before ediˇ after".into(),
13874 buffer_marked_text: "before <edi|> after".into(),
13875 completion_label: "editor",
13876 completion_text: "editor",
13877 expected_with_insert_mode: "before editorˇ after".into(),
13878 expected_with_replace_mode: "before editorˇ after".into(),
13879 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13880 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13881 },
13882 Run {
13883 run_description: "Accept same text at the middle of the word",
13884 initial_state: "before ediˇtor after".into(),
13885 buffer_marked_text: "before <edi|tor> after".into(),
13886 completion_label: "editor",
13887 completion_text: "editor",
13888 expected_with_insert_mode: "before editorˇtor after".into(),
13889 expected_with_replace_mode: "before editorˇ after".into(),
13890 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13891 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13892 },
13893 Run {
13894 run_description: "End of word matches completion text -- cursor at end",
13895 initial_state: "before torˇ after".into(),
13896 buffer_marked_text: "before <tor|> after".into(),
13897 completion_label: "editor",
13898 completion_text: "editor",
13899 expected_with_insert_mode: "before editorˇ after".into(),
13900 expected_with_replace_mode: "before editorˇ after".into(),
13901 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13902 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13903 },
13904 Run {
13905 run_description: "End of word matches completion text -- cursor at start",
13906 initial_state: "before ˇtor after".into(),
13907 buffer_marked_text: "before <|tor> after".into(),
13908 completion_label: "editor",
13909 completion_text: "editor",
13910 expected_with_insert_mode: "before editorˇtor after".into(),
13911 expected_with_replace_mode: "before editorˇ after".into(),
13912 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13913 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13914 },
13915 Run {
13916 run_description: "Prepend text containing whitespace",
13917 initial_state: "pˇfield: bool".into(),
13918 buffer_marked_text: "<p|field>: bool".into(),
13919 completion_label: "pub ",
13920 completion_text: "pub ",
13921 expected_with_insert_mode: "pub ˇfield: bool".into(),
13922 expected_with_replace_mode: "pub ˇ: bool".into(),
13923 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13924 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13925 },
13926 Run {
13927 run_description: "Add element to start of list",
13928 initial_state: "[element_ˇelement_2]".into(),
13929 buffer_marked_text: "[<element_|element_2>]".into(),
13930 completion_label: "element_1",
13931 completion_text: "element_1",
13932 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13933 expected_with_replace_mode: "[element_1ˇ]".into(),
13934 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13935 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13936 },
13937 Run {
13938 run_description: "Add element to start of list -- first and second elements are equal",
13939 initial_state: "[elˇelement]".into(),
13940 buffer_marked_text: "[<el|element>]".into(),
13941 completion_label: "element",
13942 completion_text: "element",
13943 expected_with_insert_mode: "[elementˇelement]".into(),
13944 expected_with_replace_mode: "[elementˇ]".into(),
13945 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13946 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13947 },
13948 Run {
13949 run_description: "Ends with matching suffix",
13950 initial_state: "SubˇError".into(),
13951 buffer_marked_text: "<Sub|Error>".into(),
13952 completion_label: "SubscriptionError",
13953 completion_text: "SubscriptionError",
13954 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13955 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13956 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13957 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13958 },
13959 Run {
13960 run_description: "Suffix is a subsequence -- contiguous",
13961 initial_state: "SubˇErr".into(),
13962 buffer_marked_text: "<Sub|Err>".into(),
13963 completion_label: "SubscriptionError",
13964 completion_text: "SubscriptionError",
13965 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13966 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13967 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13968 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13969 },
13970 Run {
13971 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13972 initial_state: "Suˇscrirr".into(),
13973 buffer_marked_text: "<Su|scrirr>".into(),
13974 completion_label: "SubscriptionError",
13975 completion_text: "SubscriptionError",
13976 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13977 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13978 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13979 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13980 },
13981 Run {
13982 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13983 initial_state: "foo(indˇix)".into(),
13984 buffer_marked_text: "foo(<ind|ix>)".into(),
13985 completion_label: "node_index",
13986 completion_text: "node_index",
13987 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13988 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13989 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13990 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13991 },
13992 Run {
13993 run_description: "Replace range ends before cursor - should extend to cursor",
13994 initial_state: "before editˇo after".into(),
13995 buffer_marked_text: "before <{ed}>it|o after".into(),
13996 completion_label: "editor",
13997 completion_text: "editor",
13998 expected_with_insert_mode: "before editorˇo after".into(),
13999 expected_with_replace_mode: "before editorˇo after".into(),
14000 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14001 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14002 },
14003 Run {
14004 run_description: "Uses label for suffix matching",
14005 initial_state: "before ediˇtor after".into(),
14006 buffer_marked_text: "before <edi|tor> after".into(),
14007 completion_label: "editor",
14008 completion_text: "editor()",
14009 expected_with_insert_mode: "before editor()ˇtor after".into(),
14010 expected_with_replace_mode: "before editor()ˇ after".into(),
14011 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14012 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14013 },
14014 Run {
14015 run_description: "Case insensitive subsequence and suffix matching",
14016 initial_state: "before EDiˇtoR after".into(),
14017 buffer_marked_text: "before <EDi|toR> after".into(),
14018 completion_label: "editor",
14019 completion_text: "editor",
14020 expected_with_insert_mode: "before editorˇtoR after".into(),
14021 expected_with_replace_mode: "before editorˇ after".into(),
14022 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14023 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14024 },
14025 ];
14026
14027 for run in runs {
14028 let run_variations = [
14029 (LspInsertMode::Insert, run.expected_with_insert_mode),
14030 (LspInsertMode::Replace, run.expected_with_replace_mode),
14031 (
14032 LspInsertMode::ReplaceSubsequence,
14033 run.expected_with_replace_subsequence_mode,
14034 ),
14035 (
14036 LspInsertMode::ReplaceSuffix,
14037 run.expected_with_replace_suffix_mode,
14038 ),
14039 ];
14040
14041 for (lsp_insert_mode, expected_text) in run_variations {
14042 eprintln!(
14043 "run = {:?}, mode = {lsp_insert_mode:.?}",
14044 run.run_description,
14045 );
14046
14047 update_test_language_settings(&mut cx, |settings| {
14048 settings.defaults.completions = Some(CompletionSettingsContent {
14049 lsp_insert_mode: Some(lsp_insert_mode),
14050 words: Some(WordsCompletionMode::Disabled),
14051 words_min_length: Some(0),
14052 ..Default::default()
14053 });
14054 });
14055
14056 cx.set_state(&run.initial_state);
14057 cx.update_editor(|editor, window, cx| {
14058 editor.show_completions(&ShowCompletions, window, cx);
14059 });
14060
14061 let counter = Arc::new(AtomicUsize::new(0));
14062 handle_completion_request_with_insert_and_replace(
14063 &mut cx,
14064 &run.buffer_marked_text,
14065 vec![(run.completion_label, run.completion_text)],
14066 counter.clone(),
14067 )
14068 .await;
14069 cx.condition(|editor, _| editor.context_menu_visible())
14070 .await;
14071 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14072
14073 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14074 editor
14075 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14076 .unwrap()
14077 });
14078 cx.assert_editor_state(&expected_text);
14079 handle_resolve_completion_request(&mut cx, None).await;
14080 apply_additional_edits.await.unwrap();
14081 }
14082 }
14083}
14084
14085#[gpui::test]
14086async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14087 init_test(cx, |_| {});
14088 let mut cx = EditorLspTestContext::new_rust(
14089 lsp::ServerCapabilities {
14090 completion_provider: Some(lsp::CompletionOptions {
14091 resolve_provider: Some(true),
14092 ..Default::default()
14093 }),
14094 ..Default::default()
14095 },
14096 cx,
14097 )
14098 .await;
14099
14100 let initial_state = "SubˇError";
14101 let buffer_marked_text = "<Sub|Error>";
14102 let completion_text = "SubscriptionError";
14103 let expected_with_insert_mode = "SubscriptionErrorˇError";
14104 let expected_with_replace_mode = "SubscriptionErrorˇ";
14105
14106 update_test_language_settings(&mut cx, |settings| {
14107 settings.defaults.completions = Some(CompletionSettingsContent {
14108 words: Some(WordsCompletionMode::Disabled),
14109 words_min_length: Some(0),
14110 // set the opposite here to ensure that the action is overriding the default behavior
14111 lsp_insert_mode: Some(LspInsertMode::Insert),
14112 ..Default::default()
14113 });
14114 });
14115
14116 cx.set_state(initial_state);
14117 cx.update_editor(|editor, window, cx| {
14118 editor.show_completions(&ShowCompletions, window, cx);
14119 });
14120
14121 let counter = Arc::new(AtomicUsize::new(0));
14122 handle_completion_request_with_insert_and_replace(
14123 &mut cx,
14124 buffer_marked_text,
14125 vec![(completion_text, completion_text)],
14126 counter.clone(),
14127 )
14128 .await;
14129 cx.condition(|editor, _| editor.context_menu_visible())
14130 .await;
14131 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14132
14133 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14134 editor
14135 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14136 .unwrap()
14137 });
14138 cx.assert_editor_state(expected_with_replace_mode);
14139 handle_resolve_completion_request(&mut cx, None).await;
14140 apply_additional_edits.await.unwrap();
14141
14142 update_test_language_settings(&mut cx, |settings| {
14143 settings.defaults.completions = Some(CompletionSettingsContent {
14144 words: Some(WordsCompletionMode::Disabled),
14145 words_min_length: Some(0),
14146 // set the opposite here to ensure that the action is overriding the default behavior
14147 lsp_insert_mode: Some(LspInsertMode::Replace),
14148 ..Default::default()
14149 });
14150 });
14151
14152 cx.set_state(initial_state);
14153 cx.update_editor(|editor, window, cx| {
14154 editor.show_completions(&ShowCompletions, window, cx);
14155 });
14156 handle_completion_request_with_insert_and_replace(
14157 &mut cx,
14158 buffer_marked_text,
14159 vec![(completion_text, completion_text)],
14160 counter.clone(),
14161 )
14162 .await;
14163 cx.condition(|editor, _| editor.context_menu_visible())
14164 .await;
14165 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14166
14167 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14168 editor
14169 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14170 .unwrap()
14171 });
14172 cx.assert_editor_state(expected_with_insert_mode);
14173 handle_resolve_completion_request(&mut cx, None).await;
14174 apply_additional_edits.await.unwrap();
14175}
14176
14177#[gpui::test]
14178async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14179 init_test(cx, |_| {});
14180 let mut cx = EditorLspTestContext::new_rust(
14181 lsp::ServerCapabilities {
14182 completion_provider: Some(lsp::CompletionOptions {
14183 resolve_provider: Some(true),
14184 ..Default::default()
14185 }),
14186 ..Default::default()
14187 },
14188 cx,
14189 )
14190 .await;
14191
14192 // scenario: surrounding text matches completion text
14193 let completion_text = "to_offset";
14194 let initial_state = indoc! {"
14195 1. buf.to_offˇsuffix
14196 2. buf.to_offˇsuf
14197 3. buf.to_offˇfix
14198 4. buf.to_offˇ
14199 5. into_offˇensive
14200 6. ˇsuffix
14201 7. let ˇ //
14202 8. aaˇzz
14203 9. buf.to_off«zzzzzˇ»suffix
14204 10. buf.«ˇzzzzz»suffix
14205 11. to_off«ˇzzzzz»
14206
14207 buf.to_offˇsuffix // newest cursor
14208 "};
14209 let completion_marked_buffer = indoc! {"
14210 1. buf.to_offsuffix
14211 2. buf.to_offsuf
14212 3. buf.to_offfix
14213 4. buf.to_off
14214 5. into_offensive
14215 6. suffix
14216 7. let //
14217 8. aazz
14218 9. buf.to_offzzzzzsuffix
14219 10. buf.zzzzzsuffix
14220 11. to_offzzzzz
14221
14222 buf.<to_off|suffix> // newest cursor
14223 "};
14224 let expected = indoc! {"
14225 1. buf.to_offsetˇ
14226 2. buf.to_offsetˇsuf
14227 3. buf.to_offsetˇfix
14228 4. buf.to_offsetˇ
14229 5. into_offsetˇensive
14230 6. to_offsetˇsuffix
14231 7. let to_offsetˇ //
14232 8. aato_offsetˇzz
14233 9. buf.to_offsetˇ
14234 10. buf.to_offsetˇsuffix
14235 11. to_offsetˇ
14236
14237 buf.to_offsetˇ // newest cursor
14238 "};
14239 cx.set_state(initial_state);
14240 cx.update_editor(|editor, window, cx| {
14241 editor.show_completions(&ShowCompletions, window, cx);
14242 });
14243 handle_completion_request_with_insert_and_replace(
14244 &mut cx,
14245 completion_marked_buffer,
14246 vec![(completion_text, completion_text)],
14247 Arc::new(AtomicUsize::new(0)),
14248 )
14249 .await;
14250 cx.condition(|editor, _| editor.context_menu_visible())
14251 .await;
14252 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14253 editor
14254 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14255 .unwrap()
14256 });
14257 cx.assert_editor_state(expected);
14258 handle_resolve_completion_request(&mut cx, None).await;
14259 apply_additional_edits.await.unwrap();
14260
14261 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14262 let completion_text = "foo_and_bar";
14263 let initial_state = indoc! {"
14264 1. ooanbˇ
14265 2. zooanbˇ
14266 3. ooanbˇz
14267 4. zooanbˇz
14268 5. ooanˇ
14269 6. oanbˇ
14270
14271 ooanbˇ
14272 "};
14273 let completion_marked_buffer = indoc! {"
14274 1. ooanb
14275 2. zooanb
14276 3. ooanbz
14277 4. zooanbz
14278 5. ooan
14279 6. oanb
14280
14281 <ooanb|>
14282 "};
14283 let expected = indoc! {"
14284 1. foo_and_barˇ
14285 2. zfoo_and_barˇ
14286 3. foo_and_barˇz
14287 4. zfoo_and_barˇz
14288 5. ooanfoo_and_barˇ
14289 6. oanbfoo_and_barˇ
14290
14291 foo_and_barˇ
14292 "};
14293 cx.set_state(initial_state);
14294 cx.update_editor(|editor, window, cx| {
14295 editor.show_completions(&ShowCompletions, window, cx);
14296 });
14297 handle_completion_request_with_insert_and_replace(
14298 &mut cx,
14299 completion_marked_buffer,
14300 vec![(completion_text, completion_text)],
14301 Arc::new(AtomicUsize::new(0)),
14302 )
14303 .await;
14304 cx.condition(|editor, _| editor.context_menu_visible())
14305 .await;
14306 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14307 editor
14308 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14309 .unwrap()
14310 });
14311 cx.assert_editor_state(expected);
14312 handle_resolve_completion_request(&mut cx, None).await;
14313 apply_additional_edits.await.unwrap();
14314
14315 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14316 // (expects the same as if it was inserted at the end)
14317 let completion_text = "foo_and_bar";
14318 let initial_state = indoc! {"
14319 1. ooˇanb
14320 2. zooˇanb
14321 3. ooˇanbz
14322 4. zooˇanbz
14323
14324 ooˇanb
14325 "};
14326 let completion_marked_buffer = indoc! {"
14327 1. ooanb
14328 2. zooanb
14329 3. ooanbz
14330 4. zooanbz
14331
14332 <oo|anb>
14333 "};
14334 let expected = indoc! {"
14335 1. foo_and_barˇ
14336 2. zfoo_and_barˇ
14337 3. foo_and_barˇz
14338 4. zfoo_and_barˇz
14339
14340 foo_and_barˇ
14341 "};
14342 cx.set_state(initial_state);
14343 cx.update_editor(|editor, window, cx| {
14344 editor.show_completions(&ShowCompletions, window, cx);
14345 });
14346 handle_completion_request_with_insert_and_replace(
14347 &mut cx,
14348 completion_marked_buffer,
14349 vec![(completion_text, completion_text)],
14350 Arc::new(AtomicUsize::new(0)),
14351 )
14352 .await;
14353 cx.condition(|editor, _| editor.context_menu_visible())
14354 .await;
14355 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14356 editor
14357 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14358 .unwrap()
14359 });
14360 cx.assert_editor_state(expected);
14361 handle_resolve_completion_request(&mut cx, None).await;
14362 apply_additional_edits.await.unwrap();
14363}
14364
14365// This used to crash
14366#[gpui::test]
14367async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14368 init_test(cx, |_| {});
14369
14370 let buffer_text = indoc! {"
14371 fn main() {
14372 10.satu;
14373
14374 //
14375 // separate cursors so they open in different excerpts (manually reproducible)
14376 //
14377
14378 10.satu20;
14379 }
14380 "};
14381 let multibuffer_text_with_selections = indoc! {"
14382 fn main() {
14383 10.satuˇ;
14384
14385 //
14386
14387 //
14388
14389 10.satuˇ20;
14390 }
14391 "};
14392 let expected_multibuffer = indoc! {"
14393 fn main() {
14394 10.saturating_sub()ˇ;
14395
14396 //
14397
14398 //
14399
14400 10.saturating_sub()ˇ;
14401 }
14402 "};
14403
14404 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14405 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14406
14407 let fs = FakeFs::new(cx.executor());
14408 fs.insert_tree(
14409 path!("/a"),
14410 json!({
14411 "main.rs": buffer_text,
14412 }),
14413 )
14414 .await;
14415
14416 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14417 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14418 language_registry.add(rust_lang());
14419 let mut fake_servers = language_registry.register_fake_lsp(
14420 "Rust",
14421 FakeLspAdapter {
14422 capabilities: lsp::ServerCapabilities {
14423 completion_provider: Some(lsp::CompletionOptions {
14424 resolve_provider: None,
14425 ..lsp::CompletionOptions::default()
14426 }),
14427 ..lsp::ServerCapabilities::default()
14428 },
14429 ..FakeLspAdapter::default()
14430 },
14431 );
14432 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14433 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14434 let buffer = project
14435 .update(cx, |project, cx| {
14436 project.open_local_buffer(path!("/a/main.rs"), cx)
14437 })
14438 .await
14439 .unwrap();
14440
14441 let multi_buffer = cx.new(|cx| {
14442 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14443 multi_buffer.push_excerpts(
14444 buffer.clone(),
14445 [ExcerptRange::new(0..first_excerpt_end)],
14446 cx,
14447 );
14448 multi_buffer.push_excerpts(
14449 buffer.clone(),
14450 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14451 cx,
14452 );
14453 multi_buffer
14454 });
14455
14456 let editor = workspace
14457 .update(cx, |_, window, cx| {
14458 cx.new(|cx| {
14459 Editor::new(
14460 EditorMode::Full {
14461 scale_ui_elements_with_buffer_font_size: false,
14462 show_active_line_background: false,
14463 sizing_behavior: SizingBehavior::Default,
14464 },
14465 multi_buffer.clone(),
14466 Some(project.clone()),
14467 window,
14468 cx,
14469 )
14470 })
14471 })
14472 .unwrap();
14473
14474 let pane = workspace
14475 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14476 .unwrap();
14477 pane.update_in(cx, |pane, window, cx| {
14478 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14479 });
14480
14481 let fake_server = fake_servers.next().await.unwrap();
14482
14483 editor.update_in(cx, |editor, window, cx| {
14484 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14485 s.select_ranges([
14486 Point::new(1, 11)..Point::new(1, 11),
14487 Point::new(7, 11)..Point::new(7, 11),
14488 ])
14489 });
14490
14491 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14492 });
14493
14494 editor.update_in(cx, |editor, window, cx| {
14495 editor.show_completions(&ShowCompletions, window, cx);
14496 });
14497
14498 fake_server
14499 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14500 let completion_item = lsp::CompletionItem {
14501 label: "saturating_sub()".into(),
14502 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14503 lsp::InsertReplaceEdit {
14504 new_text: "saturating_sub()".to_owned(),
14505 insert: lsp::Range::new(
14506 lsp::Position::new(7, 7),
14507 lsp::Position::new(7, 11),
14508 ),
14509 replace: lsp::Range::new(
14510 lsp::Position::new(7, 7),
14511 lsp::Position::new(7, 13),
14512 ),
14513 },
14514 )),
14515 ..lsp::CompletionItem::default()
14516 };
14517
14518 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14519 })
14520 .next()
14521 .await
14522 .unwrap();
14523
14524 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14525 .await;
14526
14527 editor
14528 .update_in(cx, |editor, window, cx| {
14529 editor
14530 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14531 .unwrap()
14532 })
14533 .await
14534 .unwrap();
14535
14536 editor.update(cx, |editor, cx| {
14537 assert_text_with_selections(editor, expected_multibuffer, cx);
14538 })
14539}
14540
14541#[gpui::test]
14542async fn test_completion(cx: &mut TestAppContext) {
14543 init_test(cx, |_| {});
14544
14545 let mut cx = EditorLspTestContext::new_rust(
14546 lsp::ServerCapabilities {
14547 completion_provider: Some(lsp::CompletionOptions {
14548 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14549 resolve_provider: Some(true),
14550 ..Default::default()
14551 }),
14552 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14553 ..Default::default()
14554 },
14555 cx,
14556 )
14557 .await;
14558 let counter = Arc::new(AtomicUsize::new(0));
14559
14560 cx.set_state(indoc! {"
14561 oneˇ
14562 two
14563 three
14564 "});
14565 cx.simulate_keystroke(".");
14566 handle_completion_request(
14567 indoc! {"
14568 one.|<>
14569 two
14570 three
14571 "},
14572 vec!["first_completion", "second_completion"],
14573 true,
14574 counter.clone(),
14575 &mut cx,
14576 )
14577 .await;
14578 cx.condition(|editor, _| editor.context_menu_visible())
14579 .await;
14580 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14581
14582 let _handler = handle_signature_help_request(
14583 &mut cx,
14584 lsp::SignatureHelp {
14585 signatures: vec![lsp::SignatureInformation {
14586 label: "test signature".to_string(),
14587 documentation: None,
14588 parameters: Some(vec![lsp::ParameterInformation {
14589 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14590 documentation: None,
14591 }]),
14592 active_parameter: None,
14593 }],
14594 active_signature: None,
14595 active_parameter: None,
14596 },
14597 );
14598 cx.update_editor(|editor, window, cx| {
14599 assert!(
14600 !editor.signature_help_state.is_shown(),
14601 "No signature help was called for"
14602 );
14603 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14604 });
14605 cx.run_until_parked();
14606 cx.update_editor(|editor, _, _| {
14607 assert!(
14608 !editor.signature_help_state.is_shown(),
14609 "No signature help should be shown when completions menu is open"
14610 );
14611 });
14612
14613 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14614 editor.context_menu_next(&Default::default(), window, cx);
14615 editor
14616 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14617 .unwrap()
14618 });
14619 cx.assert_editor_state(indoc! {"
14620 one.second_completionˇ
14621 two
14622 three
14623 "});
14624
14625 handle_resolve_completion_request(
14626 &mut cx,
14627 Some(vec![
14628 (
14629 //This overlaps with the primary completion edit which is
14630 //misbehavior from the LSP spec, test that we filter it out
14631 indoc! {"
14632 one.second_ˇcompletion
14633 two
14634 threeˇ
14635 "},
14636 "overlapping additional edit",
14637 ),
14638 (
14639 indoc! {"
14640 one.second_completion
14641 two
14642 threeˇ
14643 "},
14644 "\nadditional edit",
14645 ),
14646 ]),
14647 )
14648 .await;
14649 apply_additional_edits.await.unwrap();
14650 cx.assert_editor_state(indoc! {"
14651 one.second_completionˇ
14652 two
14653 three
14654 additional edit
14655 "});
14656
14657 cx.set_state(indoc! {"
14658 one.second_completion
14659 twoˇ
14660 threeˇ
14661 additional edit
14662 "});
14663 cx.simulate_keystroke(" ");
14664 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14665 cx.simulate_keystroke("s");
14666 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14667
14668 cx.assert_editor_state(indoc! {"
14669 one.second_completion
14670 two sˇ
14671 three sˇ
14672 additional edit
14673 "});
14674 handle_completion_request(
14675 indoc! {"
14676 one.second_completion
14677 two s
14678 three <s|>
14679 additional edit
14680 "},
14681 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14682 true,
14683 counter.clone(),
14684 &mut cx,
14685 )
14686 .await;
14687 cx.condition(|editor, _| editor.context_menu_visible())
14688 .await;
14689 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14690
14691 cx.simulate_keystroke("i");
14692
14693 handle_completion_request(
14694 indoc! {"
14695 one.second_completion
14696 two si
14697 three <si|>
14698 additional edit
14699 "},
14700 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14701 true,
14702 counter.clone(),
14703 &mut cx,
14704 )
14705 .await;
14706 cx.condition(|editor, _| editor.context_menu_visible())
14707 .await;
14708 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14709
14710 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14711 editor
14712 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14713 .unwrap()
14714 });
14715 cx.assert_editor_state(indoc! {"
14716 one.second_completion
14717 two sixth_completionˇ
14718 three sixth_completionˇ
14719 additional edit
14720 "});
14721
14722 apply_additional_edits.await.unwrap();
14723
14724 update_test_language_settings(&mut cx, |settings| {
14725 settings.defaults.show_completions_on_input = Some(false);
14726 });
14727 cx.set_state("editorˇ");
14728 cx.simulate_keystroke(".");
14729 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14730 cx.simulate_keystrokes("c l o");
14731 cx.assert_editor_state("editor.cloˇ");
14732 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14733 cx.update_editor(|editor, window, cx| {
14734 editor.show_completions(&ShowCompletions, window, cx);
14735 });
14736 handle_completion_request(
14737 "editor.<clo|>",
14738 vec!["close", "clobber"],
14739 true,
14740 counter.clone(),
14741 &mut cx,
14742 )
14743 .await;
14744 cx.condition(|editor, _| editor.context_menu_visible())
14745 .await;
14746 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14747
14748 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14749 editor
14750 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14751 .unwrap()
14752 });
14753 cx.assert_editor_state("editor.clobberˇ");
14754 handle_resolve_completion_request(&mut cx, None).await;
14755 apply_additional_edits.await.unwrap();
14756}
14757
14758#[gpui::test]
14759async fn test_completion_reuse(cx: &mut TestAppContext) {
14760 init_test(cx, |_| {});
14761
14762 let mut cx = EditorLspTestContext::new_rust(
14763 lsp::ServerCapabilities {
14764 completion_provider: Some(lsp::CompletionOptions {
14765 trigger_characters: Some(vec![".".to_string()]),
14766 ..Default::default()
14767 }),
14768 ..Default::default()
14769 },
14770 cx,
14771 )
14772 .await;
14773
14774 let counter = Arc::new(AtomicUsize::new(0));
14775 cx.set_state("objˇ");
14776 cx.simulate_keystroke(".");
14777
14778 // Initial completion request returns complete results
14779 let is_incomplete = false;
14780 handle_completion_request(
14781 "obj.|<>",
14782 vec!["a", "ab", "abc"],
14783 is_incomplete,
14784 counter.clone(),
14785 &mut cx,
14786 )
14787 .await;
14788 cx.run_until_parked();
14789 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14790 cx.assert_editor_state("obj.ˇ");
14791 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14792
14793 // Type "a" - filters existing completions
14794 cx.simulate_keystroke("a");
14795 cx.run_until_parked();
14796 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14797 cx.assert_editor_state("obj.aˇ");
14798 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14799
14800 // Type "b" - filters existing completions
14801 cx.simulate_keystroke("b");
14802 cx.run_until_parked();
14803 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14804 cx.assert_editor_state("obj.abˇ");
14805 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14806
14807 // Type "c" - filters existing completions
14808 cx.simulate_keystroke("c");
14809 cx.run_until_parked();
14810 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14811 cx.assert_editor_state("obj.abcˇ");
14812 check_displayed_completions(vec!["abc"], &mut cx);
14813
14814 // Backspace to delete "c" - filters existing completions
14815 cx.update_editor(|editor, window, cx| {
14816 editor.backspace(&Backspace, window, cx);
14817 });
14818 cx.run_until_parked();
14819 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14820 cx.assert_editor_state("obj.abˇ");
14821 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14822
14823 // Moving cursor to the left dismisses menu.
14824 cx.update_editor(|editor, window, cx| {
14825 editor.move_left(&MoveLeft, window, cx);
14826 });
14827 cx.run_until_parked();
14828 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14829 cx.assert_editor_state("obj.aˇb");
14830 cx.update_editor(|editor, _, _| {
14831 assert_eq!(editor.context_menu_visible(), false);
14832 });
14833
14834 // Type "b" - new request
14835 cx.simulate_keystroke("b");
14836 let is_incomplete = false;
14837 handle_completion_request(
14838 "obj.<ab|>a",
14839 vec!["ab", "abc"],
14840 is_incomplete,
14841 counter.clone(),
14842 &mut cx,
14843 )
14844 .await;
14845 cx.run_until_parked();
14846 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14847 cx.assert_editor_state("obj.abˇb");
14848 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14849
14850 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14851 cx.update_editor(|editor, window, cx| {
14852 editor.backspace(&Backspace, window, cx);
14853 });
14854 let is_incomplete = false;
14855 handle_completion_request(
14856 "obj.<a|>b",
14857 vec!["a", "ab", "abc"],
14858 is_incomplete,
14859 counter.clone(),
14860 &mut cx,
14861 )
14862 .await;
14863 cx.run_until_parked();
14864 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14865 cx.assert_editor_state("obj.aˇb");
14866 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14867
14868 // Backspace to delete "a" - dismisses menu.
14869 cx.update_editor(|editor, window, cx| {
14870 editor.backspace(&Backspace, window, cx);
14871 });
14872 cx.run_until_parked();
14873 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14874 cx.assert_editor_state("obj.ˇb");
14875 cx.update_editor(|editor, _, _| {
14876 assert_eq!(editor.context_menu_visible(), false);
14877 });
14878}
14879
14880#[gpui::test]
14881async fn test_word_completion(cx: &mut TestAppContext) {
14882 let lsp_fetch_timeout_ms = 10;
14883 init_test(cx, |language_settings| {
14884 language_settings.defaults.completions = Some(CompletionSettingsContent {
14885 words_min_length: Some(0),
14886 lsp_fetch_timeout_ms: Some(10),
14887 lsp_insert_mode: Some(LspInsertMode::Insert),
14888 ..Default::default()
14889 });
14890 });
14891
14892 let mut cx = EditorLspTestContext::new_rust(
14893 lsp::ServerCapabilities {
14894 completion_provider: Some(lsp::CompletionOptions {
14895 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14896 ..lsp::CompletionOptions::default()
14897 }),
14898 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14899 ..lsp::ServerCapabilities::default()
14900 },
14901 cx,
14902 )
14903 .await;
14904
14905 let throttle_completions = Arc::new(AtomicBool::new(false));
14906
14907 let lsp_throttle_completions = throttle_completions.clone();
14908 let _completion_requests_handler =
14909 cx.lsp
14910 .server
14911 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14912 let lsp_throttle_completions = lsp_throttle_completions.clone();
14913 let cx = cx.clone();
14914 async move {
14915 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14916 cx.background_executor()
14917 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14918 .await;
14919 }
14920 Ok(Some(lsp::CompletionResponse::Array(vec![
14921 lsp::CompletionItem {
14922 label: "first".into(),
14923 ..lsp::CompletionItem::default()
14924 },
14925 lsp::CompletionItem {
14926 label: "last".into(),
14927 ..lsp::CompletionItem::default()
14928 },
14929 ])))
14930 }
14931 });
14932
14933 cx.set_state(indoc! {"
14934 oneˇ
14935 two
14936 three
14937 "});
14938 cx.simulate_keystroke(".");
14939 cx.executor().run_until_parked();
14940 cx.condition(|editor, _| editor.context_menu_visible())
14941 .await;
14942 cx.update_editor(|editor, window, cx| {
14943 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14944 {
14945 assert_eq!(
14946 completion_menu_entries(menu),
14947 &["first", "last"],
14948 "When LSP server is fast to reply, no fallback word completions are used"
14949 );
14950 } else {
14951 panic!("expected completion menu to be open");
14952 }
14953 editor.cancel(&Cancel, window, cx);
14954 });
14955 cx.executor().run_until_parked();
14956 cx.condition(|editor, _| !editor.context_menu_visible())
14957 .await;
14958
14959 throttle_completions.store(true, atomic::Ordering::Release);
14960 cx.simulate_keystroke(".");
14961 cx.executor()
14962 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14963 cx.executor().run_until_parked();
14964 cx.condition(|editor, _| editor.context_menu_visible())
14965 .await;
14966 cx.update_editor(|editor, _, _| {
14967 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14968 {
14969 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14970 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14971 } else {
14972 panic!("expected completion menu to be open");
14973 }
14974 });
14975}
14976
14977#[gpui::test]
14978async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14979 init_test(cx, |language_settings| {
14980 language_settings.defaults.completions = Some(CompletionSettingsContent {
14981 words: Some(WordsCompletionMode::Enabled),
14982 words_min_length: Some(0),
14983 lsp_insert_mode: Some(LspInsertMode::Insert),
14984 ..Default::default()
14985 });
14986 });
14987
14988 let mut cx = EditorLspTestContext::new_rust(
14989 lsp::ServerCapabilities {
14990 completion_provider: Some(lsp::CompletionOptions {
14991 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14992 ..lsp::CompletionOptions::default()
14993 }),
14994 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14995 ..lsp::ServerCapabilities::default()
14996 },
14997 cx,
14998 )
14999 .await;
15000
15001 let _completion_requests_handler =
15002 cx.lsp
15003 .server
15004 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15005 Ok(Some(lsp::CompletionResponse::Array(vec![
15006 lsp::CompletionItem {
15007 label: "first".into(),
15008 ..lsp::CompletionItem::default()
15009 },
15010 lsp::CompletionItem {
15011 label: "last".into(),
15012 ..lsp::CompletionItem::default()
15013 },
15014 ])))
15015 });
15016
15017 cx.set_state(indoc! {"ˇ
15018 first
15019 last
15020 second
15021 "});
15022 cx.simulate_keystroke(".");
15023 cx.executor().run_until_parked();
15024 cx.condition(|editor, _| editor.context_menu_visible())
15025 .await;
15026 cx.update_editor(|editor, _, _| {
15027 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15028 {
15029 assert_eq!(
15030 completion_menu_entries(menu),
15031 &["first", "last", "second"],
15032 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15033 );
15034 } else {
15035 panic!("expected completion menu to be open");
15036 }
15037 });
15038}
15039
15040#[gpui::test]
15041async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15042 init_test(cx, |language_settings| {
15043 language_settings.defaults.completions = Some(CompletionSettingsContent {
15044 words: Some(WordsCompletionMode::Disabled),
15045 words_min_length: Some(0),
15046 lsp_insert_mode: Some(LspInsertMode::Insert),
15047 ..Default::default()
15048 });
15049 });
15050
15051 let mut cx = EditorLspTestContext::new_rust(
15052 lsp::ServerCapabilities {
15053 completion_provider: Some(lsp::CompletionOptions {
15054 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15055 ..lsp::CompletionOptions::default()
15056 }),
15057 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15058 ..lsp::ServerCapabilities::default()
15059 },
15060 cx,
15061 )
15062 .await;
15063
15064 let _completion_requests_handler =
15065 cx.lsp
15066 .server
15067 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15068 panic!("LSP completions should not be queried when dealing with word completions")
15069 });
15070
15071 cx.set_state(indoc! {"ˇ
15072 first
15073 last
15074 second
15075 "});
15076 cx.update_editor(|editor, window, cx| {
15077 editor.show_word_completions(&ShowWordCompletions, window, cx);
15078 });
15079 cx.executor().run_until_parked();
15080 cx.condition(|editor, _| editor.context_menu_visible())
15081 .await;
15082 cx.update_editor(|editor, _, _| {
15083 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15084 {
15085 assert_eq!(
15086 completion_menu_entries(menu),
15087 &["first", "last", "second"],
15088 "`ShowWordCompletions` action should show word completions"
15089 );
15090 } else {
15091 panic!("expected completion menu to be open");
15092 }
15093 });
15094
15095 cx.simulate_keystroke("l");
15096 cx.executor().run_until_parked();
15097 cx.condition(|editor, _| editor.context_menu_visible())
15098 .await;
15099 cx.update_editor(|editor, _, _| {
15100 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15101 {
15102 assert_eq!(
15103 completion_menu_entries(menu),
15104 &["last"],
15105 "After showing word completions, further editing should filter them and not query the LSP"
15106 );
15107 } else {
15108 panic!("expected completion menu to be open");
15109 }
15110 });
15111}
15112
15113#[gpui::test]
15114async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15115 init_test(cx, |language_settings| {
15116 language_settings.defaults.completions = Some(CompletionSettingsContent {
15117 words_min_length: Some(0),
15118 lsp: Some(false),
15119 lsp_insert_mode: Some(LspInsertMode::Insert),
15120 ..Default::default()
15121 });
15122 });
15123
15124 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15125
15126 cx.set_state(indoc! {"ˇ
15127 0_usize
15128 let
15129 33
15130 4.5f32
15131 "});
15132 cx.update_editor(|editor, window, cx| {
15133 editor.show_completions(&ShowCompletions, window, cx);
15134 });
15135 cx.executor().run_until_parked();
15136 cx.condition(|editor, _| editor.context_menu_visible())
15137 .await;
15138 cx.update_editor(|editor, window, cx| {
15139 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15140 {
15141 assert_eq!(
15142 completion_menu_entries(menu),
15143 &["let"],
15144 "With no digits in the completion query, no digits should be in the word completions"
15145 );
15146 } else {
15147 panic!("expected completion menu to be open");
15148 }
15149 editor.cancel(&Cancel, window, cx);
15150 });
15151
15152 cx.set_state(indoc! {"3ˇ
15153 0_usize
15154 let
15155 3
15156 33.35f32
15157 "});
15158 cx.update_editor(|editor, window, cx| {
15159 editor.show_completions(&ShowCompletions, window, cx);
15160 });
15161 cx.executor().run_until_parked();
15162 cx.condition(|editor, _| editor.context_menu_visible())
15163 .await;
15164 cx.update_editor(|editor, _, _| {
15165 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15166 {
15167 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15168 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15169 } else {
15170 panic!("expected completion menu to be open");
15171 }
15172 });
15173}
15174
15175#[gpui::test]
15176async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15177 init_test(cx, |language_settings| {
15178 language_settings.defaults.completions = Some(CompletionSettingsContent {
15179 words: Some(WordsCompletionMode::Enabled),
15180 words_min_length: Some(3),
15181 lsp_insert_mode: Some(LspInsertMode::Insert),
15182 ..Default::default()
15183 });
15184 });
15185
15186 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15187 cx.set_state(indoc! {"ˇ
15188 wow
15189 wowen
15190 wowser
15191 "});
15192 cx.simulate_keystroke("w");
15193 cx.executor().run_until_parked();
15194 cx.update_editor(|editor, _, _| {
15195 if editor.context_menu.borrow_mut().is_some() {
15196 panic!(
15197 "expected completion menu to be hidden, as words completion threshold is not met"
15198 );
15199 }
15200 });
15201
15202 cx.update_editor(|editor, window, cx| {
15203 editor.show_word_completions(&ShowWordCompletions, window, cx);
15204 });
15205 cx.executor().run_until_parked();
15206 cx.update_editor(|editor, window, cx| {
15207 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15208 {
15209 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");
15210 } else {
15211 panic!("expected completion menu to be open after the word completions are called with an action");
15212 }
15213
15214 editor.cancel(&Cancel, window, cx);
15215 });
15216 cx.update_editor(|editor, _, _| {
15217 if editor.context_menu.borrow_mut().is_some() {
15218 panic!("expected completion menu to be hidden after canceling");
15219 }
15220 });
15221
15222 cx.simulate_keystroke("o");
15223 cx.executor().run_until_parked();
15224 cx.update_editor(|editor, _, _| {
15225 if editor.context_menu.borrow_mut().is_some() {
15226 panic!(
15227 "expected completion menu to be hidden, as words completion threshold is not met still"
15228 );
15229 }
15230 });
15231
15232 cx.simulate_keystroke("w");
15233 cx.executor().run_until_parked();
15234 cx.update_editor(|editor, _, _| {
15235 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15236 {
15237 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15238 } else {
15239 panic!("expected completion menu to be open after the word completions threshold is met");
15240 }
15241 });
15242}
15243
15244#[gpui::test]
15245async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15246 init_test(cx, |language_settings| {
15247 language_settings.defaults.completions = Some(CompletionSettingsContent {
15248 words: Some(WordsCompletionMode::Enabled),
15249 words_min_length: Some(0),
15250 lsp_insert_mode: Some(LspInsertMode::Insert),
15251 ..Default::default()
15252 });
15253 });
15254
15255 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15256 cx.update_editor(|editor, _, _| {
15257 editor.disable_word_completions();
15258 });
15259 cx.set_state(indoc! {"ˇ
15260 wow
15261 wowen
15262 wowser
15263 "});
15264 cx.simulate_keystroke("w");
15265 cx.executor().run_until_parked();
15266 cx.update_editor(|editor, _, _| {
15267 if editor.context_menu.borrow_mut().is_some() {
15268 panic!(
15269 "expected completion menu to be hidden, as words completion are disabled for this editor"
15270 );
15271 }
15272 });
15273
15274 cx.update_editor(|editor, window, cx| {
15275 editor.show_word_completions(&ShowWordCompletions, window, cx);
15276 });
15277 cx.executor().run_until_parked();
15278 cx.update_editor(|editor, _, _| {
15279 if editor.context_menu.borrow_mut().is_some() {
15280 panic!(
15281 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15282 );
15283 }
15284 });
15285}
15286
15287#[gpui::test]
15288async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15289 init_test(cx, |language_settings| {
15290 language_settings.defaults.completions = Some(CompletionSettingsContent {
15291 words: Some(WordsCompletionMode::Disabled),
15292 words_min_length: Some(0),
15293 lsp_insert_mode: Some(LspInsertMode::Insert),
15294 ..Default::default()
15295 });
15296 });
15297
15298 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15299 cx.update_editor(|editor, _, _| {
15300 editor.set_completion_provider(None);
15301 });
15302 cx.set_state(indoc! {"ˇ
15303 wow
15304 wowen
15305 wowser
15306 "});
15307 cx.simulate_keystroke("w");
15308 cx.executor().run_until_parked();
15309 cx.update_editor(|editor, _, _| {
15310 if editor.context_menu.borrow_mut().is_some() {
15311 panic!("expected completion menu to be hidden, as disabled in settings");
15312 }
15313 });
15314}
15315
15316fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15317 let position = || lsp::Position {
15318 line: params.text_document_position.position.line,
15319 character: params.text_document_position.position.character,
15320 };
15321 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15322 range: lsp::Range {
15323 start: position(),
15324 end: position(),
15325 },
15326 new_text: text.to_string(),
15327 }))
15328}
15329
15330#[gpui::test]
15331async fn test_multiline_completion(cx: &mut TestAppContext) {
15332 init_test(cx, |_| {});
15333
15334 let fs = FakeFs::new(cx.executor());
15335 fs.insert_tree(
15336 path!("/a"),
15337 json!({
15338 "main.ts": "a",
15339 }),
15340 )
15341 .await;
15342
15343 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15344 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15345 let typescript_language = Arc::new(Language::new(
15346 LanguageConfig {
15347 name: "TypeScript".into(),
15348 matcher: LanguageMatcher {
15349 path_suffixes: vec!["ts".to_string()],
15350 ..LanguageMatcher::default()
15351 },
15352 line_comments: vec!["// ".into()],
15353 ..LanguageConfig::default()
15354 },
15355 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15356 ));
15357 language_registry.add(typescript_language.clone());
15358 let mut fake_servers = language_registry.register_fake_lsp(
15359 "TypeScript",
15360 FakeLspAdapter {
15361 capabilities: lsp::ServerCapabilities {
15362 completion_provider: Some(lsp::CompletionOptions {
15363 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15364 ..lsp::CompletionOptions::default()
15365 }),
15366 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15367 ..lsp::ServerCapabilities::default()
15368 },
15369 // Emulate vtsls label generation
15370 label_for_completion: Some(Box::new(|item, _| {
15371 let text = if let Some(description) = item
15372 .label_details
15373 .as_ref()
15374 .and_then(|label_details| label_details.description.as_ref())
15375 {
15376 format!("{} {}", item.label, description)
15377 } else if let Some(detail) = &item.detail {
15378 format!("{} {}", item.label, detail)
15379 } else {
15380 item.label.clone()
15381 };
15382 Some(language::CodeLabel::plain(text, None))
15383 })),
15384 ..FakeLspAdapter::default()
15385 },
15386 );
15387 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15388 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15389 let worktree_id = workspace
15390 .update(cx, |workspace, _window, cx| {
15391 workspace.project().update(cx, |project, cx| {
15392 project.worktrees(cx).next().unwrap().read(cx).id()
15393 })
15394 })
15395 .unwrap();
15396 let _buffer = project
15397 .update(cx, |project, cx| {
15398 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15399 })
15400 .await
15401 .unwrap();
15402 let editor = workspace
15403 .update(cx, |workspace, window, cx| {
15404 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15405 })
15406 .unwrap()
15407 .await
15408 .unwrap()
15409 .downcast::<Editor>()
15410 .unwrap();
15411 let fake_server = fake_servers.next().await.unwrap();
15412
15413 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15414 let multiline_label_2 = "a\nb\nc\n";
15415 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15416 let multiline_description = "d\ne\nf\n";
15417 let multiline_detail_2 = "g\nh\ni\n";
15418
15419 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15420 move |params, _| async move {
15421 Ok(Some(lsp::CompletionResponse::Array(vec![
15422 lsp::CompletionItem {
15423 label: multiline_label.to_string(),
15424 text_edit: gen_text_edit(¶ms, "new_text_1"),
15425 ..lsp::CompletionItem::default()
15426 },
15427 lsp::CompletionItem {
15428 label: "single line label 1".to_string(),
15429 detail: Some(multiline_detail.to_string()),
15430 text_edit: gen_text_edit(¶ms, "new_text_2"),
15431 ..lsp::CompletionItem::default()
15432 },
15433 lsp::CompletionItem {
15434 label: "single line label 2".to_string(),
15435 label_details: Some(lsp::CompletionItemLabelDetails {
15436 description: Some(multiline_description.to_string()),
15437 detail: None,
15438 }),
15439 text_edit: gen_text_edit(¶ms, "new_text_2"),
15440 ..lsp::CompletionItem::default()
15441 },
15442 lsp::CompletionItem {
15443 label: multiline_label_2.to_string(),
15444 detail: Some(multiline_detail_2.to_string()),
15445 text_edit: gen_text_edit(¶ms, "new_text_3"),
15446 ..lsp::CompletionItem::default()
15447 },
15448 lsp::CompletionItem {
15449 label: "Label with many spaces and \t but without newlines".to_string(),
15450 detail: Some(
15451 "Details with many spaces and \t but without newlines".to_string(),
15452 ),
15453 text_edit: gen_text_edit(¶ms, "new_text_4"),
15454 ..lsp::CompletionItem::default()
15455 },
15456 ])))
15457 },
15458 );
15459
15460 editor.update_in(cx, |editor, window, cx| {
15461 cx.focus_self(window);
15462 editor.move_to_end(&MoveToEnd, window, cx);
15463 editor.handle_input(".", window, cx);
15464 });
15465 cx.run_until_parked();
15466 completion_handle.next().await.unwrap();
15467
15468 editor.update(cx, |editor, _| {
15469 assert!(editor.context_menu_visible());
15470 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15471 {
15472 let completion_labels = menu
15473 .completions
15474 .borrow()
15475 .iter()
15476 .map(|c| c.label.text.clone())
15477 .collect::<Vec<_>>();
15478 assert_eq!(
15479 completion_labels,
15480 &[
15481 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15482 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15483 "single line label 2 d e f ",
15484 "a b c g h i ",
15485 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15486 ],
15487 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15488 );
15489
15490 for completion in menu
15491 .completions
15492 .borrow()
15493 .iter() {
15494 assert_eq!(
15495 completion.label.filter_range,
15496 0..completion.label.text.len(),
15497 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15498 );
15499 }
15500 } else {
15501 panic!("expected completion menu to be open");
15502 }
15503 });
15504}
15505
15506#[gpui::test]
15507async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15508 init_test(cx, |_| {});
15509 let mut cx = EditorLspTestContext::new_rust(
15510 lsp::ServerCapabilities {
15511 completion_provider: Some(lsp::CompletionOptions {
15512 trigger_characters: Some(vec![".".to_string()]),
15513 ..Default::default()
15514 }),
15515 ..Default::default()
15516 },
15517 cx,
15518 )
15519 .await;
15520 cx.lsp
15521 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15522 Ok(Some(lsp::CompletionResponse::Array(vec![
15523 lsp::CompletionItem {
15524 label: "first".into(),
15525 ..Default::default()
15526 },
15527 lsp::CompletionItem {
15528 label: "last".into(),
15529 ..Default::default()
15530 },
15531 ])))
15532 });
15533 cx.set_state("variableˇ");
15534 cx.simulate_keystroke(".");
15535 cx.executor().run_until_parked();
15536
15537 cx.update_editor(|editor, _, _| {
15538 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15539 {
15540 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15541 } else {
15542 panic!("expected completion menu to be open");
15543 }
15544 });
15545
15546 cx.update_editor(|editor, window, cx| {
15547 editor.move_page_down(&MovePageDown::default(), window, cx);
15548 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15549 {
15550 assert!(
15551 menu.selected_item == 1,
15552 "expected PageDown to select the last item from the context menu"
15553 );
15554 } else {
15555 panic!("expected completion menu to stay open after PageDown");
15556 }
15557 });
15558
15559 cx.update_editor(|editor, window, cx| {
15560 editor.move_page_up(&MovePageUp::default(), window, cx);
15561 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15562 {
15563 assert!(
15564 menu.selected_item == 0,
15565 "expected PageUp to select the first item from the context menu"
15566 );
15567 } else {
15568 panic!("expected completion menu to stay open after PageUp");
15569 }
15570 });
15571}
15572
15573#[gpui::test]
15574async fn test_as_is_completions(cx: &mut TestAppContext) {
15575 init_test(cx, |_| {});
15576 let mut cx = EditorLspTestContext::new_rust(
15577 lsp::ServerCapabilities {
15578 completion_provider: Some(lsp::CompletionOptions {
15579 ..Default::default()
15580 }),
15581 ..Default::default()
15582 },
15583 cx,
15584 )
15585 .await;
15586 cx.lsp
15587 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15588 Ok(Some(lsp::CompletionResponse::Array(vec![
15589 lsp::CompletionItem {
15590 label: "unsafe".into(),
15591 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15592 range: lsp::Range {
15593 start: lsp::Position {
15594 line: 1,
15595 character: 2,
15596 },
15597 end: lsp::Position {
15598 line: 1,
15599 character: 3,
15600 },
15601 },
15602 new_text: "unsafe".to_string(),
15603 })),
15604 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15605 ..Default::default()
15606 },
15607 ])))
15608 });
15609 cx.set_state("fn a() {}\n nˇ");
15610 cx.executor().run_until_parked();
15611 cx.update_editor(|editor, window, cx| {
15612 editor.trigger_completion_on_input("n", true, window, cx)
15613 });
15614 cx.executor().run_until_parked();
15615
15616 cx.update_editor(|editor, window, cx| {
15617 editor.confirm_completion(&Default::default(), window, cx)
15618 });
15619 cx.executor().run_until_parked();
15620 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15621}
15622
15623#[gpui::test]
15624async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15625 init_test(cx, |_| {});
15626 let language =
15627 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15628 let mut cx = EditorLspTestContext::new(
15629 language,
15630 lsp::ServerCapabilities {
15631 completion_provider: Some(lsp::CompletionOptions {
15632 ..lsp::CompletionOptions::default()
15633 }),
15634 ..lsp::ServerCapabilities::default()
15635 },
15636 cx,
15637 )
15638 .await;
15639
15640 cx.set_state(
15641 "#ifndef BAR_H
15642#define BAR_H
15643
15644#include <stdbool.h>
15645
15646int fn_branch(bool do_branch1, bool do_branch2);
15647
15648#endif // BAR_H
15649ˇ",
15650 );
15651 cx.executor().run_until_parked();
15652 cx.update_editor(|editor, window, cx| {
15653 editor.handle_input("#", window, cx);
15654 });
15655 cx.executor().run_until_parked();
15656 cx.update_editor(|editor, window, cx| {
15657 editor.handle_input("i", window, cx);
15658 });
15659 cx.executor().run_until_parked();
15660 cx.update_editor(|editor, window, cx| {
15661 editor.handle_input("n", window, cx);
15662 });
15663 cx.executor().run_until_parked();
15664 cx.assert_editor_state(
15665 "#ifndef BAR_H
15666#define BAR_H
15667
15668#include <stdbool.h>
15669
15670int fn_branch(bool do_branch1, bool do_branch2);
15671
15672#endif // BAR_H
15673#inˇ",
15674 );
15675
15676 cx.lsp
15677 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15678 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15679 is_incomplete: false,
15680 item_defaults: None,
15681 items: vec![lsp::CompletionItem {
15682 kind: Some(lsp::CompletionItemKind::SNIPPET),
15683 label_details: Some(lsp::CompletionItemLabelDetails {
15684 detail: Some("header".to_string()),
15685 description: None,
15686 }),
15687 label: " include".to_string(),
15688 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15689 range: lsp::Range {
15690 start: lsp::Position {
15691 line: 8,
15692 character: 1,
15693 },
15694 end: lsp::Position {
15695 line: 8,
15696 character: 1,
15697 },
15698 },
15699 new_text: "include \"$0\"".to_string(),
15700 })),
15701 sort_text: Some("40b67681include".to_string()),
15702 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15703 filter_text: Some("include".to_string()),
15704 insert_text: Some("include \"$0\"".to_string()),
15705 ..lsp::CompletionItem::default()
15706 }],
15707 })))
15708 });
15709 cx.update_editor(|editor, window, cx| {
15710 editor.show_completions(&ShowCompletions, window, cx);
15711 });
15712 cx.executor().run_until_parked();
15713 cx.update_editor(|editor, window, cx| {
15714 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15715 });
15716 cx.executor().run_until_parked();
15717 cx.assert_editor_state(
15718 "#ifndef BAR_H
15719#define BAR_H
15720
15721#include <stdbool.h>
15722
15723int fn_branch(bool do_branch1, bool do_branch2);
15724
15725#endif // BAR_H
15726#include \"ˇ\"",
15727 );
15728
15729 cx.lsp
15730 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15731 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15732 is_incomplete: true,
15733 item_defaults: None,
15734 items: vec![lsp::CompletionItem {
15735 kind: Some(lsp::CompletionItemKind::FILE),
15736 label: "AGL/".to_string(),
15737 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15738 range: lsp::Range {
15739 start: lsp::Position {
15740 line: 8,
15741 character: 10,
15742 },
15743 end: lsp::Position {
15744 line: 8,
15745 character: 11,
15746 },
15747 },
15748 new_text: "AGL/".to_string(),
15749 })),
15750 sort_text: Some("40b67681AGL/".to_string()),
15751 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15752 filter_text: Some("AGL/".to_string()),
15753 insert_text: Some("AGL/".to_string()),
15754 ..lsp::CompletionItem::default()
15755 }],
15756 })))
15757 });
15758 cx.update_editor(|editor, window, cx| {
15759 editor.show_completions(&ShowCompletions, window, cx);
15760 });
15761 cx.executor().run_until_parked();
15762 cx.update_editor(|editor, window, cx| {
15763 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15764 });
15765 cx.executor().run_until_parked();
15766 cx.assert_editor_state(
15767 r##"#ifndef BAR_H
15768#define BAR_H
15769
15770#include <stdbool.h>
15771
15772int fn_branch(bool do_branch1, bool do_branch2);
15773
15774#endif // BAR_H
15775#include "AGL/ˇ"##,
15776 );
15777
15778 cx.update_editor(|editor, window, cx| {
15779 editor.handle_input("\"", window, cx);
15780 });
15781 cx.executor().run_until_parked();
15782 cx.assert_editor_state(
15783 r##"#ifndef BAR_H
15784#define BAR_H
15785
15786#include <stdbool.h>
15787
15788int fn_branch(bool do_branch1, bool do_branch2);
15789
15790#endif // BAR_H
15791#include "AGL/"ˇ"##,
15792 );
15793}
15794
15795#[gpui::test]
15796async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15797 init_test(cx, |_| {});
15798
15799 let mut cx = EditorLspTestContext::new_rust(
15800 lsp::ServerCapabilities {
15801 completion_provider: Some(lsp::CompletionOptions {
15802 trigger_characters: Some(vec![".".to_string()]),
15803 resolve_provider: Some(true),
15804 ..Default::default()
15805 }),
15806 ..Default::default()
15807 },
15808 cx,
15809 )
15810 .await;
15811
15812 cx.set_state("fn main() { let a = 2ˇ; }");
15813 cx.simulate_keystroke(".");
15814 let completion_item = lsp::CompletionItem {
15815 label: "Some".into(),
15816 kind: Some(lsp::CompletionItemKind::SNIPPET),
15817 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15818 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15819 kind: lsp::MarkupKind::Markdown,
15820 value: "```rust\nSome(2)\n```".to_string(),
15821 })),
15822 deprecated: Some(false),
15823 sort_text: Some("Some".to_string()),
15824 filter_text: Some("Some".to_string()),
15825 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15826 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15827 range: lsp::Range {
15828 start: lsp::Position {
15829 line: 0,
15830 character: 22,
15831 },
15832 end: lsp::Position {
15833 line: 0,
15834 character: 22,
15835 },
15836 },
15837 new_text: "Some(2)".to_string(),
15838 })),
15839 additional_text_edits: Some(vec![lsp::TextEdit {
15840 range: lsp::Range {
15841 start: lsp::Position {
15842 line: 0,
15843 character: 20,
15844 },
15845 end: lsp::Position {
15846 line: 0,
15847 character: 22,
15848 },
15849 },
15850 new_text: "".to_string(),
15851 }]),
15852 ..Default::default()
15853 };
15854
15855 let closure_completion_item = completion_item.clone();
15856 let counter = Arc::new(AtomicUsize::new(0));
15857 let counter_clone = counter.clone();
15858 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15859 let task_completion_item = closure_completion_item.clone();
15860 counter_clone.fetch_add(1, atomic::Ordering::Release);
15861 async move {
15862 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15863 is_incomplete: true,
15864 item_defaults: None,
15865 items: vec![task_completion_item],
15866 })))
15867 }
15868 });
15869
15870 cx.condition(|editor, _| editor.context_menu_visible())
15871 .await;
15872 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15873 assert!(request.next().await.is_some());
15874 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15875
15876 cx.simulate_keystrokes("S o m");
15877 cx.condition(|editor, _| editor.context_menu_visible())
15878 .await;
15879 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15880 assert!(request.next().await.is_some());
15881 assert!(request.next().await.is_some());
15882 assert!(request.next().await.is_some());
15883 request.close();
15884 assert!(request.next().await.is_none());
15885 assert_eq!(
15886 counter.load(atomic::Ordering::Acquire),
15887 4,
15888 "With the completions menu open, only one LSP request should happen per input"
15889 );
15890}
15891
15892#[gpui::test]
15893async fn test_toggle_comment(cx: &mut TestAppContext) {
15894 init_test(cx, |_| {});
15895 let mut cx = EditorTestContext::new(cx).await;
15896 let language = Arc::new(Language::new(
15897 LanguageConfig {
15898 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15899 ..Default::default()
15900 },
15901 Some(tree_sitter_rust::LANGUAGE.into()),
15902 ));
15903 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15904
15905 // If multiple selections intersect a line, the line is only toggled once.
15906 cx.set_state(indoc! {"
15907 fn a() {
15908 «//b();
15909 ˇ»// «c();
15910 //ˇ» d();
15911 }
15912 "});
15913
15914 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15915
15916 cx.assert_editor_state(indoc! {"
15917 fn a() {
15918 «b();
15919 c();
15920 ˇ» d();
15921 }
15922 "});
15923
15924 // The comment prefix is inserted at the same column for every line in a
15925 // selection.
15926 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15927
15928 cx.assert_editor_state(indoc! {"
15929 fn a() {
15930 // «b();
15931 // c();
15932 ˇ»// d();
15933 }
15934 "});
15935
15936 // If a selection ends at the beginning of a line, that line is not toggled.
15937 cx.set_selections_state(indoc! {"
15938 fn a() {
15939 // b();
15940 «// c();
15941 ˇ» // d();
15942 }
15943 "});
15944
15945 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15946
15947 cx.assert_editor_state(indoc! {"
15948 fn a() {
15949 // b();
15950 «c();
15951 ˇ» // d();
15952 }
15953 "});
15954
15955 // If a selection span a single line and is empty, the line is toggled.
15956 cx.set_state(indoc! {"
15957 fn a() {
15958 a();
15959 b();
15960 ˇ
15961 }
15962 "});
15963
15964 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15965
15966 cx.assert_editor_state(indoc! {"
15967 fn a() {
15968 a();
15969 b();
15970 //•ˇ
15971 }
15972 "});
15973
15974 // If a selection span multiple lines, empty lines are not toggled.
15975 cx.set_state(indoc! {"
15976 fn a() {
15977 «a();
15978
15979 c();ˇ»
15980 }
15981 "});
15982
15983 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15984
15985 cx.assert_editor_state(indoc! {"
15986 fn a() {
15987 // «a();
15988
15989 // c();ˇ»
15990 }
15991 "});
15992
15993 // If a selection includes multiple comment prefixes, all lines are uncommented.
15994 cx.set_state(indoc! {"
15995 fn a() {
15996 «// a();
15997 /// b();
15998 //! c();ˇ»
15999 }
16000 "});
16001
16002 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16003
16004 cx.assert_editor_state(indoc! {"
16005 fn a() {
16006 «a();
16007 b();
16008 c();ˇ»
16009 }
16010 "});
16011}
16012
16013#[gpui::test]
16014async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16015 init_test(cx, |_| {});
16016 let mut cx = EditorTestContext::new(cx).await;
16017 let language = Arc::new(Language::new(
16018 LanguageConfig {
16019 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16020 ..Default::default()
16021 },
16022 Some(tree_sitter_rust::LANGUAGE.into()),
16023 ));
16024 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16025
16026 let toggle_comments = &ToggleComments {
16027 advance_downwards: false,
16028 ignore_indent: true,
16029 };
16030
16031 // If multiple selections intersect a line, the line is only toggled once.
16032 cx.set_state(indoc! {"
16033 fn a() {
16034 // «b();
16035 // c();
16036 // ˇ» d();
16037 }
16038 "});
16039
16040 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16041
16042 cx.assert_editor_state(indoc! {"
16043 fn a() {
16044 «b();
16045 c();
16046 ˇ» d();
16047 }
16048 "});
16049
16050 // The comment prefix is inserted at the beginning of each line
16051 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16052
16053 cx.assert_editor_state(indoc! {"
16054 fn a() {
16055 // «b();
16056 // c();
16057 // ˇ» d();
16058 }
16059 "});
16060
16061 // If a selection ends at the beginning of a line, that line is not toggled.
16062 cx.set_selections_state(indoc! {"
16063 fn a() {
16064 // b();
16065 // «c();
16066 ˇ»// d();
16067 }
16068 "});
16069
16070 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16071
16072 cx.assert_editor_state(indoc! {"
16073 fn a() {
16074 // b();
16075 «c();
16076 ˇ»// d();
16077 }
16078 "});
16079
16080 // If a selection span a single line and is empty, the line is toggled.
16081 cx.set_state(indoc! {"
16082 fn a() {
16083 a();
16084 b();
16085 ˇ
16086 }
16087 "});
16088
16089 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16090
16091 cx.assert_editor_state(indoc! {"
16092 fn a() {
16093 a();
16094 b();
16095 //ˇ
16096 }
16097 "});
16098
16099 // If a selection span multiple lines, empty lines are not toggled.
16100 cx.set_state(indoc! {"
16101 fn a() {
16102 «a();
16103
16104 c();ˇ»
16105 }
16106 "});
16107
16108 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16109
16110 cx.assert_editor_state(indoc! {"
16111 fn a() {
16112 // «a();
16113
16114 // c();ˇ»
16115 }
16116 "});
16117
16118 // If a selection includes multiple comment prefixes, all lines are uncommented.
16119 cx.set_state(indoc! {"
16120 fn a() {
16121 // «a();
16122 /// b();
16123 //! c();ˇ»
16124 }
16125 "});
16126
16127 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16128
16129 cx.assert_editor_state(indoc! {"
16130 fn a() {
16131 «a();
16132 b();
16133 c();ˇ»
16134 }
16135 "});
16136}
16137
16138#[gpui::test]
16139async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16140 init_test(cx, |_| {});
16141
16142 let language = Arc::new(Language::new(
16143 LanguageConfig {
16144 line_comments: vec!["// ".into()],
16145 ..Default::default()
16146 },
16147 Some(tree_sitter_rust::LANGUAGE.into()),
16148 ));
16149
16150 let mut cx = EditorTestContext::new(cx).await;
16151
16152 cx.language_registry().add(language.clone());
16153 cx.update_buffer(|buffer, cx| {
16154 buffer.set_language(Some(language), cx);
16155 });
16156
16157 let toggle_comments = &ToggleComments {
16158 advance_downwards: true,
16159 ignore_indent: false,
16160 };
16161
16162 // Single cursor on one line -> advance
16163 // Cursor moves horizontally 3 characters as well on non-blank line
16164 cx.set_state(indoc!(
16165 "fn a() {
16166 ˇdog();
16167 cat();
16168 }"
16169 ));
16170 cx.update_editor(|editor, window, cx| {
16171 editor.toggle_comments(toggle_comments, window, cx);
16172 });
16173 cx.assert_editor_state(indoc!(
16174 "fn a() {
16175 // dog();
16176 catˇ();
16177 }"
16178 ));
16179
16180 // Single selection on one line -> don't advance
16181 cx.set_state(indoc!(
16182 "fn a() {
16183 «dog()ˇ»;
16184 cat();
16185 }"
16186 ));
16187 cx.update_editor(|editor, window, cx| {
16188 editor.toggle_comments(toggle_comments, window, cx);
16189 });
16190 cx.assert_editor_state(indoc!(
16191 "fn a() {
16192 // «dog()ˇ»;
16193 cat();
16194 }"
16195 ));
16196
16197 // Multiple cursors on one line -> advance
16198 cx.set_state(indoc!(
16199 "fn a() {
16200 ˇdˇog();
16201 cat();
16202 }"
16203 ));
16204 cx.update_editor(|editor, window, cx| {
16205 editor.toggle_comments(toggle_comments, window, cx);
16206 });
16207 cx.assert_editor_state(indoc!(
16208 "fn a() {
16209 // dog();
16210 catˇ(ˇ);
16211 }"
16212 ));
16213
16214 // Multiple cursors on one line, with selection -> don't advance
16215 cx.set_state(indoc!(
16216 "fn a() {
16217 ˇdˇog«()ˇ»;
16218 cat();
16219 }"
16220 ));
16221 cx.update_editor(|editor, window, cx| {
16222 editor.toggle_comments(toggle_comments, window, cx);
16223 });
16224 cx.assert_editor_state(indoc!(
16225 "fn a() {
16226 // ˇdˇog«()ˇ»;
16227 cat();
16228 }"
16229 ));
16230
16231 // Single cursor on one line -> advance
16232 // Cursor moves to column 0 on blank line
16233 cx.set_state(indoc!(
16234 "fn a() {
16235 ˇdog();
16236
16237 cat();
16238 }"
16239 ));
16240 cx.update_editor(|editor, window, cx| {
16241 editor.toggle_comments(toggle_comments, window, cx);
16242 });
16243 cx.assert_editor_state(indoc!(
16244 "fn a() {
16245 // dog();
16246 ˇ
16247 cat();
16248 }"
16249 ));
16250
16251 // Single cursor on one line -> advance
16252 // Cursor starts and ends at column 0
16253 cx.set_state(indoc!(
16254 "fn a() {
16255 ˇ dog();
16256 cat();
16257 }"
16258 ));
16259 cx.update_editor(|editor, window, cx| {
16260 editor.toggle_comments(toggle_comments, window, cx);
16261 });
16262 cx.assert_editor_state(indoc!(
16263 "fn a() {
16264 // dog();
16265 ˇ cat();
16266 }"
16267 ));
16268}
16269
16270#[gpui::test]
16271async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16272 init_test(cx, |_| {});
16273
16274 let mut cx = EditorTestContext::new(cx).await;
16275
16276 let html_language = Arc::new(
16277 Language::new(
16278 LanguageConfig {
16279 name: "HTML".into(),
16280 block_comment: Some(BlockCommentConfig {
16281 start: "<!-- ".into(),
16282 prefix: "".into(),
16283 end: " -->".into(),
16284 tab_size: 0,
16285 }),
16286 ..Default::default()
16287 },
16288 Some(tree_sitter_html::LANGUAGE.into()),
16289 )
16290 .with_injection_query(
16291 r#"
16292 (script_element
16293 (raw_text) @injection.content
16294 (#set! injection.language "javascript"))
16295 "#,
16296 )
16297 .unwrap(),
16298 );
16299
16300 let javascript_language = Arc::new(Language::new(
16301 LanguageConfig {
16302 name: "JavaScript".into(),
16303 line_comments: vec!["// ".into()],
16304 ..Default::default()
16305 },
16306 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16307 ));
16308
16309 cx.language_registry().add(html_language.clone());
16310 cx.language_registry().add(javascript_language);
16311 cx.update_buffer(|buffer, cx| {
16312 buffer.set_language(Some(html_language), cx);
16313 });
16314
16315 // Toggle comments for empty selections
16316 cx.set_state(
16317 &r#"
16318 <p>A</p>ˇ
16319 <p>B</p>ˇ
16320 <p>C</p>ˇ
16321 "#
16322 .unindent(),
16323 );
16324 cx.update_editor(|editor, window, cx| {
16325 editor.toggle_comments(&ToggleComments::default(), window, cx)
16326 });
16327 cx.assert_editor_state(
16328 &r#"
16329 <!-- <p>A</p>ˇ -->
16330 <!-- <p>B</p>ˇ -->
16331 <!-- <p>C</p>ˇ -->
16332 "#
16333 .unindent(),
16334 );
16335 cx.update_editor(|editor, window, cx| {
16336 editor.toggle_comments(&ToggleComments::default(), window, cx)
16337 });
16338 cx.assert_editor_state(
16339 &r#"
16340 <p>A</p>ˇ
16341 <p>B</p>ˇ
16342 <p>C</p>ˇ
16343 "#
16344 .unindent(),
16345 );
16346
16347 // Toggle comments for mixture of empty and non-empty selections, where
16348 // multiple selections occupy a given line.
16349 cx.set_state(
16350 &r#"
16351 <p>A«</p>
16352 <p>ˇ»B</p>ˇ
16353 <p>C«</p>
16354 <p>ˇ»D</p>ˇ
16355 "#
16356 .unindent(),
16357 );
16358
16359 cx.update_editor(|editor, window, cx| {
16360 editor.toggle_comments(&ToggleComments::default(), window, cx)
16361 });
16362 cx.assert_editor_state(
16363 &r#"
16364 <!-- <p>A«</p>
16365 <p>ˇ»B</p>ˇ -->
16366 <!-- <p>C«</p>
16367 <p>ˇ»D</p>ˇ -->
16368 "#
16369 .unindent(),
16370 );
16371 cx.update_editor(|editor, window, cx| {
16372 editor.toggle_comments(&ToggleComments::default(), window, cx)
16373 });
16374 cx.assert_editor_state(
16375 &r#"
16376 <p>A«</p>
16377 <p>ˇ»B</p>ˇ
16378 <p>C«</p>
16379 <p>ˇ»D</p>ˇ
16380 "#
16381 .unindent(),
16382 );
16383
16384 // Toggle comments when different languages are active for different
16385 // selections.
16386 cx.set_state(
16387 &r#"
16388 ˇ<script>
16389 ˇvar x = new Y();
16390 ˇ</script>
16391 "#
16392 .unindent(),
16393 );
16394 cx.executor().run_until_parked();
16395 cx.update_editor(|editor, window, cx| {
16396 editor.toggle_comments(&ToggleComments::default(), window, cx)
16397 });
16398 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16399 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16400 cx.assert_editor_state(
16401 &r#"
16402 <!-- ˇ<script> -->
16403 // ˇvar x = new Y();
16404 <!-- ˇ</script> -->
16405 "#
16406 .unindent(),
16407 );
16408}
16409
16410#[gpui::test]
16411fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16412 init_test(cx, |_| {});
16413
16414 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16415 let multibuffer = cx.new(|cx| {
16416 let mut multibuffer = MultiBuffer::new(ReadWrite);
16417 multibuffer.push_excerpts(
16418 buffer.clone(),
16419 [
16420 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16421 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16422 ],
16423 cx,
16424 );
16425 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16426 multibuffer
16427 });
16428
16429 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16430 editor.update_in(cx, |editor, window, cx| {
16431 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16433 s.select_ranges([
16434 Point::new(0, 0)..Point::new(0, 0),
16435 Point::new(1, 0)..Point::new(1, 0),
16436 ])
16437 });
16438
16439 editor.handle_input("X", window, cx);
16440 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16441 assert_eq!(
16442 editor.selections.ranges(&editor.display_snapshot(cx)),
16443 [
16444 Point::new(0, 1)..Point::new(0, 1),
16445 Point::new(1, 1)..Point::new(1, 1),
16446 ]
16447 );
16448
16449 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16451 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16452 });
16453 editor.backspace(&Default::default(), window, cx);
16454 assert_eq!(editor.text(cx), "Xa\nbbb");
16455 assert_eq!(
16456 editor.selections.ranges(&editor.display_snapshot(cx)),
16457 [Point::new(1, 0)..Point::new(1, 0)]
16458 );
16459
16460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16461 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16462 });
16463 editor.backspace(&Default::default(), window, cx);
16464 assert_eq!(editor.text(cx), "X\nbb");
16465 assert_eq!(
16466 editor.selections.ranges(&editor.display_snapshot(cx)),
16467 [Point::new(0, 1)..Point::new(0, 1)]
16468 );
16469 });
16470}
16471
16472#[gpui::test]
16473fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16474 init_test(cx, |_| {});
16475
16476 let markers = vec![('[', ']').into(), ('(', ')').into()];
16477 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16478 indoc! {"
16479 [aaaa
16480 (bbbb]
16481 cccc)",
16482 },
16483 markers.clone(),
16484 );
16485 let excerpt_ranges = markers.into_iter().map(|marker| {
16486 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16487 ExcerptRange::new(context)
16488 });
16489 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16490 let multibuffer = cx.new(|cx| {
16491 let mut multibuffer = MultiBuffer::new(ReadWrite);
16492 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16493 multibuffer
16494 });
16495
16496 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16497 editor.update_in(cx, |editor, window, cx| {
16498 let (expected_text, selection_ranges) = marked_text_ranges(
16499 indoc! {"
16500 aaaa
16501 bˇbbb
16502 bˇbbˇb
16503 cccc"
16504 },
16505 true,
16506 );
16507 assert_eq!(editor.text(cx), expected_text);
16508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16509 s.select_ranges(
16510 selection_ranges
16511 .iter()
16512 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16513 )
16514 });
16515
16516 editor.handle_input("X", window, cx);
16517
16518 let (expected_text, expected_selections) = marked_text_ranges(
16519 indoc! {"
16520 aaaa
16521 bXˇbbXb
16522 bXˇbbXˇb
16523 cccc"
16524 },
16525 false,
16526 );
16527 assert_eq!(editor.text(cx), expected_text);
16528 assert_eq!(
16529 editor.selections.ranges(&editor.display_snapshot(cx)),
16530 expected_selections
16531 .iter()
16532 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16533 .collect::<Vec<_>>()
16534 );
16535
16536 editor.newline(&Newline, window, cx);
16537 let (expected_text, expected_selections) = marked_text_ranges(
16538 indoc! {"
16539 aaaa
16540 bX
16541 ˇbbX
16542 b
16543 bX
16544 ˇbbX
16545 ˇb
16546 cccc"
16547 },
16548 false,
16549 );
16550 assert_eq!(editor.text(cx), expected_text);
16551 assert_eq!(
16552 editor.selections.ranges(&editor.display_snapshot(cx)),
16553 expected_selections
16554 .iter()
16555 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16556 .collect::<Vec<_>>()
16557 );
16558 });
16559}
16560
16561#[gpui::test]
16562fn test_refresh_selections(cx: &mut TestAppContext) {
16563 init_test(cx, |_| {});
16564
16565 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16566 let mut excerpt1_id = None;
16567 let multibuffer = cx.new(|cx| {
16568 let mut multibuffer = MultiBuffer::new(ReadWrite);
16569 excerpt1_id = multibuffer
16570 .push_excerpts(
16571 buffer.clone(),
16572 [
16573 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16574 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16575 ],
16576 cx,
16577 )
16578 .into_iter()
16579 .next();
16580 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16581 multibuffer
16582 });
16583
16584 let editor = cx.add_window(|window, cx| {
16585 let mut editor = build_editor(multibuffer.clone(), window, cx);
16586 let snapshot = editor.snapshot(window, cx);
16587 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16588 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16589 });
16590 editor.begin_selection(
16591 Point::new(2, 1).to_display_point(&snapshot),
16592 true,
16593 1,
16594 window,
16595 cx,
16596 );
16597 assert_eq!(
16598 editor.selections.ranges(&editor.display_snapshot(cx)),
16599 [
16600 Point::new(1, 3)..Point::new(1, 3),
16601 Point::new(2, 1)..Point::new(2, 1),
16602 ]
16603 );
16604 editor
16605 });
16606
16607 // Refreshing selections is a no-op when excerpts haven't changed.
16608 _ = editor.update(cx, |editor, window, cx| {
16609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16610 assert_eq!(
16611 editor.selections.ranges(&editor.display_snapshot(cx)),
16612 [
16613 Point::new(1, 3)..Point::new(1, 3),
16614 Point::new(2, 1)..Point::new(2, 1),
16615 ]
16616 );
16617 });
16618
16619 multibuffer.update(cx, |multibuffer, cx| {
16620 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16621 });
16622 _ = editor.update(cx, |editor, window, cx| {
16623 // Removing an excerpt causes the first selection to become degenerate.
16624 assert_eq!(
16625 editor.selections.ranges(&editor.display_snapshot(cx)),
16626 [
16627 Point::new(0, 0)..Point::new(0, 0),
16628 Point::new(0, 1)..Point::new(0, 1)
16629 ]
16630 );
16631
16632 // Refreshing selections will relocate the first selection to the original buffer
16633 // location.
16634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16635 assert_eq!(
16636 editor.selections.ranges(&editor.display_snapshot(cx)),
16637 [
16638 Point::new(0, 1)..Point::new(0, 1),
16639 Point::new(0, 3)..Point::new(0, 3)
16640 ]
16641 );
16642 assert!(editor.selections.pending_anchor().is_some());
16643 });
16644}
16645
16646#[gpui::test]
16647fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16648 init_test(cx, |_| {});
16649
16650 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16651 let mut excerpt1_id = None;
16652 let multibuffer = cx.new(|cx| {
16653 let mut multibuffer = MultiBuffer::new(ReadWrite);
16654 excerpt1_id = multibuffer
16655 .push_excerpts(
16656 buffer.clone(),
16657 [
16658 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16659 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16660 ],
16661 cx,
16662 )
16663 .into_iter()
16664 .next();
16665 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16666 multibuffer
16667 });
16668
16669 let editor = cx.add_window(|window, cx| {
16670 let mut editor = build_editor(multibuffer.clone(), window, cx);
16671 let snapshot = editor.snapshot(window, cx);
16672 editor.begin_selection(
16673 Point::new(1, 3).to_display_point(&snapshot),
16674 false,
16675 1,
16676 window,
16677 cx,
16678 );
16679 assert_eq!(
16680 editor.selections.ranges(&editor.display_snapshot(cx)),
16681 [Point::new(1, 3)..Point::new(1, 3)]
16682 );
16683 editor
16684 });
16685
16686 multibuffer.update(cx, |multibuffer, cx| {
16687 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16688 });
16689 _ = editor.update(cx, |editor, window, cx| {
16690 assert_eq!(
16691 editor.selections.ranges(&editor.display_snapshot(cx)),
16692 [Point::new(0, 0)..Point::new(0, 0)]
16693 );
16694
16695 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16696 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16697 assert_eq!(
16698 editor.selections.ranges(&editor.display_snapshot(cx)),
16699 [Point::new(0, 3)..Point::new(0, 3)]
16700 );
16701 assert!(editor.selections.pending_anchor().is_some());
16702 });
16703}
16704
16705#[gpui::test]
16706async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16707 init_test(cx, |_| {});
16708
16709 let language = Arc::new(
16710 Language::new(
16711 LanguageConfig {
16712 brackets: BracketPairConfig {
16713 pairs: vec![
16714 BracketPair {
16715 start: "{".to_string(),
16716 end: "}".to_string(),
16717 close: true,
16718 surround: true,
16719 newline: true,
16720 },
16721 BracketPair {
16722 start: "/* ".to_string(),
16723 end: " */".to_string(),
16724 close: true,
16725 surround: true,
16726 newline: true,
16727 },
16728 ],
16729 ..Default::default()
16730 },
16731 ..Default::default()
16732 },
16733 Some(tree_sitter_rust::LANGUAGE.into()),
16734 )
16735 .with_indents_query("")
16736 .unwrap(),
16737 );
16738
16739 let text = concat!(
16740 "{ }\n", //
16741 " x\n", //
16742 " /* */\n", //
16743 "x\n", //
16744 "{{} }\n", //
16745 );
16746
16747 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16748 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16749 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16750 editor
16751 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16752 .await;
16753
16754 editor.update_in(cx, |editor, window, cx| {
16755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16756 s.select_display_ranges([
16757 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16758 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16759 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16760 ])
16761 });
16762 editor.newline(&Newline, window, cx);
16763
16764 assert_eq!(
16765 editor.buffer().read(cx).read(cx).text(),
16766 concat!(
16767 "{ \n", // Suppress rustfmt
16768 "\n", //
16769 "}\n", //
16770 " x\n", //
16771 " /* \n", //
16772 " \n", //
16773 " */\n", //
16774 "x\n", //
16775 "{{} \n", //
16776 "}\n", //
16777 )
16778 );
16779 });
16780}
16781
16782#[gpui::test]
16783fn test_highlighted_ranges(cx: &mut TestAppContext) {
16784 init_test(cx, |_| {});
16785
16786 let editor = cx.add_window(|window, cx| {
16787 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16788 build_editor(buffer, window, cx)
16789 });
16790
16791 _ = editor.update(cx, |editor, window, cx| {
16792 struct Type1;
16793 struct Type2;
16794
16795 let buffer = editor.buffer.read(cx).snapshot(cx);
16796
16797 let anchor_range =
16798 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16799
16800 editor.highlight_background::<Type1>(
16801 &[
16802 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16803 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16804 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16805 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16806 ],
16807 |_| Hsla::red(),
16808 cx,
16809 );
16810 editor.highlight_background::<Type2>(
16811 &[
16812 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16813 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16814 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16815 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16816 ],
16817 |_| Hsla::green(),
16818 cx,
16819 );
16820
16821 let snapshot = editor.snapshot(window, cx);
16822 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16823 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16824 &snapshot,
16825 cx.theme(),
16826 );
16827 assert_eq!(
16828 highlighted_ranges,
16829 &[
16830 (
16831 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16832 Hsla::green(),
16833 ),
16834 (
16835 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16836 Hsla::red(),
16837 ),
16838 (
16839 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16840 Hsla::green(),
16841 ),
16842 (
16843 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16844 Hsla::red(),
16845 ),
16846 ]
16847 );
16848 assert_eq!(
16849 editor.sorted_background_highlights_in_range(
16850 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16851 &snapshot,
16852 cx.theme(),
16853 ),
16854 &[(
16855 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16856 Hsla::red(),
16857 )]
16858 );
16859 });
16860}
16861
16862#[gpui::test]
16863async fn test_following(cx: &mut TestAppContext) {
16864 init_test(cx, |_| {});
16865
16866 let fs = FakeFs::new(cx.executor());
16867 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16868
16869 let buffer = project.update(cx, |project, cx| {
16870 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16871 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16872 });
16873 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16874 let follower = cx.update(|cx| {
16875 cx.open_window(
16876 WindowOptions {
16877 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16878 gpui::Point::new(px(0.), px(0.)),
16879 gpui::Point::new(px(10.), px(80.)),
16880 ))),
16881 ..Default::default()
16882 },
16883 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16884 )
16885 .unwrap()
16886 });
16887
16888 let is_still_following = Rc::new(RefCell::new(true));
16889 let follower_edit_event_count = Rc::new(RefCell::new(0));
16890 let pending_update = Rc::new(RefCell::new(None));
16891 let leader_entity = leader.root(cx).unwrap();
16892 let follower_entity = follower.root(cx).unwrap();
16893 _ = follower.update(cx, {
16894 let update = pending_update.clone();
16895 let is_still_following = is_still_following.clone();
16896 let follower_edit_event_count = follower_edit_event_count.clone();
16897 |_, window, cx| {
16898 cx.subscribe_in(
16899 &leader_entity,
16900 window,
16901 move |_, leader, event, window, cx| {
16902 leader.read(cx).add_event_to_update_proto(
16903 event,
16904 &mut update.borrow_mut(),
16905 window,
16906 cx,
16907 );
16908 },
16909 )
16910 .detach();
16911
16912 cx.subscribe_in(
16913 &follower_entity,
16914 window,
16915 move |_, _, event: &EditorEvent, _window, _cx| {
16916 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16917 *is_still_following.borrow_mut() = false;
16918 }
16919
16920 if let EditorEvent::BufferEdited = event {
16921 *follower_edit_event_count.borrow_mut() += 1;
16922 }
16923 },
16924 )
16925 .detach();
16926 }
16927 });
16928
16929 // Update the selections only
16930 _ = leader.update(cx, |leader, window, cx| {
16931 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16932 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
16933 });
16934 });
16935 follower
16936 .update(cx, |follower, window, cx| {
16937 follower.apply_update_proto(
16938 &project,
16939 pending_update.borrow_mut().take().unwrap(),
16940 window,
16941 cx,
16942 )
16943 })
16944 .unwrap()
16945 .await
16946 .unwrap();
16947 _ = follower.update(cx, |follower, _, cx| {
16948 assert_eq!(
16949 follower.selections.ranges(&follower.display_snapshot(cx)),
16950 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
16951 );
16952 });
16953 assert!(*is_still_following.borrow());
16954 assert_eq!(*follower_edit_event_count.borrow(), 0);
16955
16956 // Update the scroll position only
16957 _ = leader.update(cx, |leader, window, cx| {
16958 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16959 });
16960 follower
16961 .update(cx, |follower, window, cx| {
16962 follower.apply_update_proto(
16963 &project,
16964 pending_update.borrow_mut().take().unwrap(),
16965 window,
16966 cx,
16967 )
16968 })
16969 .unwrap()
16970 .await
16971 .unwrap();
16972 assert_eq!(
16973 follower
16974 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16975 .unwrap(),
16976 gpui::Point::new(1.5, 3.5)
16977 );
16978 assert!(*is_still_following.borrow());
16979 assert_eq!(*follower_edit_event_count.borrow(), 0);
16980
16981 // Update the selections and scroll position. The follower's scroll position is updated
16982 // via autoscroll, not via the leader's exact scroll position.
16983 _ = leader.update(cx, |leader, window, cx| {
16984 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16985 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
16986 });
16987 leader.request_autoscroll(Autoscroll::newest(), cx);
16988 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16989 });
16990 follower
16991 .update(cx, |follower, window, cx| {
16992 follower.apply_update_proto(
16993 &project,
16994 pending_update.borrow_mut().take().unwrap(),
16995 window,
16996 cx,
16997 )
16998 })
16999 .unwrap()
17000 .await
17001 .unwrap();
17002 _ = follower.update(cx, |follower, _, cx| {
17003 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17004 assert_eq!(
17005 follower.selections.ranges(&follower.display_snapshot(cx)),
17006 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17007 );
17008 });
17009 assert!(*is_still_following.borrow());
17010
17011 // Creating a pending selection that precedes another selection
17012 _ = leader.update(cx, |leader, window, cx| {
17013 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17014 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17015 });
17016 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17017 });
17018 follower
17019 .update(cx, |follower, window, cx| {
17020 follower.apply_update_proto(
17021 &project,
17022 pending_update.borrow_mut().take().unwrap(),
17023 window,
17024 cx,
17025 )
17026 })
17027 .unwrap()
17028 .await
17029 .unwrap();
17030 _ = follower.update(cx, |follower, _, cx| {
17031 assert_eq!(
17032 follower.selections.ranges(&follower.display_snapshot(cx)),
17033 vec![
17034 MultiBufferOffset(0)..MultiBufferOffset(0),
17035 MultiBufferOffset(1)..MultiBufferOffset(1)
17036 ]
17037 );
17038 });
17039 assert!(*is_still_following.borrow());
17040
17041 // Extend the pending selection so that it surrounds another selection
17042 _ = leader.update(cx, |leader, window, cx| {
17043 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17044 });
17045 follower
17046 .update(cx, |follower, window, cx| {
17047 follower.apply_update_proto(
17048 &project,
17049 pending_update.borrow_mut().take().unwrap(),
17050 window,
17051 cx,
17052 )
17053 })
17054 .unwrap()
17055 .await
17056 .unwrap();
17057 _ = follower.update(cx, |follower, _, cx| {
17058 assert_eq!(
17059 follower.selections.ranges(&follower.display_snapshot(cx)),
17060 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17061 );
17062 });
17063
17064 // Scrolling locally breaks the follow
17065 _ = follower.update(cx, |follower, window, cx| {
17066 let top_anchor = follower
17067 .buffer()
17068 .read(cx)
17069 .read(cx)
17070 .anchor_after(MultiBufferOffset(0));
17071 follower.set_scroll_anchor(
17072 ScrollAnchor {
17073 anchor: top_anchor,
17074 offset: gpui::Point::new(0.0, 0.5),
17075 },
17076 window,
17077 cx,
17078 );
17079 });
17080 assert!(!(*is_still_following.borrow()));
17081}
17082
17083#[gpui::test]
17084async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17085 init_test(cx, |_| {});
17086
17087 let fs = FakeFs::new(cx.executor());
17088 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17089 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17090 let pane = workspace
17091 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17092 .unwrap();
17093
17094 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17095
17096 let leader = pane.update_in(cx, |_, window, cx| {
17097 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17098 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17099 });
17100
17101 // Start following the editor when it has no excerpts.
17102 let mut state_message =
17103 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17104 let workspace_entity = workspace.root(cx).unwrap();
17105 let follower_1 = cx
17106 .update_window(*workspace.deref(), |_, window, cx| {
17107 Editor::from_state_proto(
17108 workspace_entity,
17109 ViewId {
17110 creator: CollaboratorId::PeerId(PeerId::default()),
17111 id: 0,
17112 },
17113 &mut state_message,
17114 window,
17115 cx,
17116 )
17117 })
17118 .unwrap()
17119 .unwrap()
17120 .await
17121 .unwrap();
17122
17123 let update_message = Rc::new(RefCell::new(None));
17124 follower_1.update_in(cx, {
17125 let update = update_message.clone();
17126 |_, window, cx| {
17127 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17128 leader.read(cx).add_event_to_update_proto(
17129 event,
17130 &mut update.borrow_mut(),
17131 window,
17132 cx,
17133 );
17134 })
17135 .detach();
17136 }
17137 });
17138
17139 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17140 (
17141 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17142 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17143 )
17144 });
17145
17146 // Insert some excerpts.
17147 leader.update(cx, |leader, cx| {
17148 leader.buffer.update(cx, |multibuffer, cx| {
17149 multibuffer.set_excerpts_for_path(
17150 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17151 buffer_1.clone(),
17152 vec![
17153 Point::row_range(0..3),
17154 Point::row_range(1..6),
17155 Point::row_range(12..15),
17156 ],
17157 0,
17158 cx,
17159 );
17160 multibuffer.set_excerpts_for_path(
17161 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17162 buffer_2.clone(),
17163 vec![Point::row_range(0..6), Point::row_range(8..12)],
17164 0,
17165 cx,
17166 );
17167 });
17168 });
17169
17170 // Apply the update of adding the excerpts.
17171 follower_1
17172 .update_in(cx, |follower, window, cx| {
17173 follower.apply_update_proto(
17174 &project,
17175 update_message.borrow().clone().unwrap(),
17176 window,
17177 cx,
17178 )
17179 })
17180 .await
17181 .unwrap();
17182 assert_eq!(
17183 follower_1.update(cx, |editor, cx| editor.text(cx)),
17184 leader.update(cx, |editor, cx| editor.text(cx))
17185 );
17186 update_message.borrow_mut().take();
17187
17188 // Start following separately after it already has excerpts.
17189 let mut state_message =
17190 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17191 let workspace_entity = workspace.root(cx).unwrap();
17192 let follower_2 = cx
17193 .update_window(*workspace.deref(), |_, window, cx| {
17194 Editor::from_state_proto(
17195 workspace_entity,
17196 ViewId {
17197 creator: CollaboratorId::PeerId(PeerId::default()),
17198 id: 0,
17199 },
17200 &mut state_message,
17201 window,
17202 cx,
17203 )
17204 })
17205 .unwrap()
17206 .unwrap()
17207 .await
17208 .unwrap();
17209 assert_eq!(
17210 follower_2.update(cx, |editor, cx| editor.text(cx)),
17211 leader.update(cx, |editor, cx| editor.text(cx))
17212 );
17213
17214 // Remove some excerpts.
17215 leader.update(cx, |leader, cx| {
17216 leader.buffer.update(cx, |multibuffer, cx| {
17217 let excerpt_ids = multibuffer.excerpt_ids();
17218 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17219 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17220 });
17221 });
17222
17223 // Apply the update of removing the excerpts.
17224 follower_1
17225 .update_in(cx, |follower, window, cx| {
17226 follower.apply_update_proto(
17227 &project,
17228 update_message.borrow().clone().unwrap(),
17229 window,
17230 cx,
17231 )
17232 })
17233 .await
17234 .unwrap();
17235 follower_2
17236 .update_in(cx, |follower, window, cx| {
17237 follower.apply_update_proto(
17238 &project,
17239 update_message.borrow().clone().unwrap(),
17240 window,
17241 cx,
17242 )
17243 })
17244 .await
17245 .unwrap();
17246 update_message.borrow_mut().take();
17247 assert_eq!(
17248 follower_1.update(cx, |editor, cx| editor.text(cx)),
17249 leader.update(cx, |editor, cx| editor.text(cx))
17250 );
17251}
17252
17253#[gpui::test]
17254async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17255 init_test(cx, |_| {});
17256
17257 let mut cx = EditorTestContext::new(cx).await;
17258 let lsp_store =
17259 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17260
17261 cx.set_state(indoc! {"
17262 ˇfn func(abc def: i32) -> u32 {
17263 }
17264 "});
17265
17266 cx.update(|_, cx| {
17267 lsp_store.update(cx, |lsp_store, cx| {
17268 lsp_store
17269 .update_diagnostics(
17270 LanguageServerId(0),
17271 lsp::PublishDiagnosticsParams {
17272 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17273 version: None,
17274 diagnostics: vec![
17275 lsp::Diagnostic {
17276 range: lsp::Range::new(
17277 lsp::Position::new(0, 11),
17278 lsp::Position::new(0, 12),
17279 ),
17280 severity: Some(lsp::DiagnosticSeverity::ERROR),
17281 ..Default::default()
17282 },
17283 lsp::Diagnostic {
17284 range: lsp::Range::new(
17285 lsp::Position::new(0, 12),
17286 lsp::Position::new(0, 15),
17287 ),
17288 severity: Some(lsp::DiagnosticSeverity::ERROR),
17289 ..Default::default()
17290 },
17291 lsp::Diagnostic {
17292 range: lsp::Range::new(
17293 lsp::Position::new(0, 25),
17294 lsp::Position::new(0, 28),
17295 ),
17296 severity: Some(lsp::DiagnosticSeverity::ERROR),
17297 ..Default::default()
17298 },
17299 ],
17300 },
17301 None,
17302 DiagnosticSourceKind::Pushed,
17303 &[],
17304 cx,
17305 )
17306 .unwrap()
17307 });
17308 });
17309
17310 executor.run_until_parked();
17311
17312 cx.update_editor(|editor, window, cx| {
17313 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17314 });
17315
17316 cx.assert_editor_state(indoc! {"
17317 fn func(abc def: i32) -> ˇu32 {
17318 }
17319 "});
17320
17321 cx.update_editor(|editor, window, cx| {
17322 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17323 });
17324
17325 cx.assert_editor_state(indoc! {"
17326 fn func(abc ˇdef: i32) -> u32 {
17327 }
17328 "});
17329
17330 cx.update_editor(|editor, window, cx| {
17331 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17332 });
17333
17334 cx.assert_editor_state(indoc! {"
17335 fn func(abcˇ def: i32) -> u32 {
17336 }
17337 "});
17338
17339 cx.update_editor(|editor, window, cx| {
17340 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17341 });
17342
17343 cx.assert_editor_state(indoc! {"
17344 fn func(abc def: i32) -> ˇu32 {
17345 }
17346 "});
17347}
17348
17349#[gpui::test]
17350async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17351 init_test(cx, |_| {});
17352
17353 let mut cx = EditorTestContext::new(cx).await;
17354
17355 let diff_base = r#"
17356 use some::mod;
17357
17358 const A: u32 = 42;
17359
17360 fn main() {
17361 println!("hello");
17362
17363 println!("world");
17364 }
17365 "#
17366 .unindent();
17367
17368 // Edits are modified, removed, modified, added
17369 cx.set_state(
17370 &r#"
17371 use some::modified;
17372
17373 ˇ
17374 fn main() {
17375 println!("hello there");
17376
17377 println!("around the");
17378 println!("world");
17379 }
17380 "#
17381 .unindent(),
17382 );
17383
17384 cx.set_head_text(&diff_base);
17385 executor.run_until_parked();
17386
17387 cx.update_editor(|editor, window, cx| {
17388 //Wrap around the bottom of the buffer
17389 for _ in 0..3 {
17390 editor.go_to_next_hunk(&GoToHunk, window, cx);
17391 }
17392 });
17393
17394 cx.assert_editor_state(
17395 &r#"
17396 ˇuse some::modified;
17397
17398
17399 fn main() {
17400 println!("hello there");
17401
17402 println!("around the");
17403 println!("world");
17404 }
17405 "#
17406 .unindent(),
17407 );
17408
17409 cx.update_editor(|editor, window, cx| {
17410 //Wrap around the top of the buffer
17411 for _ in 0..2 {
17412 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17413 }
17414 });
17415
17416 cx.assert_editor_state(
17417 &r#"
17418 use some::modified;
17419
17420
17421 fn main() {
17422 ˇ println!("hello there");
17423
17424 println!("around the");
17425 println!("world");
17426 }
17427 "#
17428 .unindent(),
17429 );
17430
17431 cx.update_editor(|editor, window, cx| {
17432 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17433 });
17434
17435 cx.assert_editor_state(
17436 &r#"
17437 use some::modified;
17438
17439 ˇ
17440 fn main() {
17441 println!("hello there");
17442
17443 println!("around the");
17444 println!("world");
17445 }
17446 "#
17447 .unindent(),
17448 );
17449
17450 cx.update_editor(|editor, window, cx| {
17451 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17452 });
17453
17454 cx.assert_editor_state(
17455 &r#"
17456 ˇuse some::modified;
17457
17458
17459 fn main() {
17460 println!("hello there");
17461
17462 println!("around the");
17463 println!("world");
17464 }
17465 "#
17466 .unindent(),
17467 );
17468
17469 cx.update_editor(|editor, window, cx| {
17470 for _ in 0..2 {
17471 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17472 }
17473 });
17474
17475 cx.assert_editor_state(
17476 &r#"
17477 use some::modified;
17478
17479
17480 fn main() {
17481 ˇ println!("hello there");
17482
17483 println!("around the");
17484 println!("world");
17485 }
17486 "#
17487 .unindent(),
17488 );
17489
17490 cx.update_editor(|editor, window, cx| {
17491 editor.fold(&Fold, window, cx);
17492 });
17493
17494 cx.update_editor(|editor, window, cx| {
17495 editor.go_to_next_hunk(&GoToHunk, window, cx);
17496 });
17497
17498 cx.assert_editor_state(
17499 &r#"
17500 ˇuse some::modified;
17501
17502
17503 fn main() {
17504 println!("hello there");
17505
17506 println!("around the");
17507 println!("world");
17508 }
17509 "#
17510 .unindent(),
17511 );
17512}
17513
17514#[test]
17515fn test_split_words() {
17516 fn split(text: &str) -> Vec<&str> {
17517 split_words(text).collect()
17518 }
17519
17520 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17521 assert_eq!(split("hello_world"), &["hello_", "world"]);
17522 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17523 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17524 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17525 assert_eq!(split("helloworld"), &["helloworld"]);
17526
17527 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17528}
17529
17530#[test]
17531fn test_split_words_for_snippet_prefix() {
17532 fn split(text: &str) -> Vec<&str> {
17533 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17534 }
17535
17536 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17537 assert_eq!(split("hello_world"), &["hello_world"]);
17538 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17539 assert_eq!(split("Hello_World"), &["Hello_World"]);
17540 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17541 assert_eq!(split("helloworld"), &["helloworld"]);
17542 assert_eq!(
17543 split("this@is!@#$^many . symbols"),
17544 &[
17545 "symbols",
17546 " symbols",
17547 ". symbols",
17548 " . symbols",
17549 " . symbols",
17550 " . symbols",
17551 "many . symbols",
17552 "^many . symbols",
17553 "$^many . symbols",
17554 "#$^many . symbols",
17555 "@#$^many . symbols",
17556 "!@#$^many . symbols",
17557 "is!@#$^many . symbols",
17558 "@is!@#$^many . symbols",
17559 "this@is!@#$^many . symbols",
17560 ],
17561 );
17562 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17563}
17564
17565#[gpui::test]
17566async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17567 init_test(cx, |_| {});
17568
17569 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17570
17571 #[track_caller]
17572 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17573 let _state_context = cx.set_state(before);
17574 cx.run_until_parked();
17575 cx.update_editor(|editor, window, cx| {
17576 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17577 });
17578 cx.run_until_parked();
17579 cx.assert_editor_state(after);
17580 }
17581
17582 // Outside bracket jumps to outside of matching bracket
17583 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17584 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17585
17586 // Inside bracket jumps to inside of matching bracket
17587 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17588 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17589
17590 // When outside a bracket and inside, favor jumping to the inside bracket
17591 assert(
17592 "console.log('foo', [1, 2, 3]ˇ);",
17593 "console.log('foo', ˇ[1, 2, 3]);",
17594 &mut cx,
17595 );
17596 assert(
17597 "console.log(ˇ'foo', [1, 2, 3]);",
17598 "console.log('foo'ˇ, [1, 2, 3]);",
17599 &mut cx,
17600 );
17601
17602 // Bias forward if two options are equally likely
17603 assert(
17604 "let result = curried_fun()ˇ();",
17605 "let result = curried_fun()()ˇ;",
17606 &mut cx,
17607 );
17608
17609 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17610 assert(
17611 indoc! {"
17612 function test() {
17613 console.log('test')ˇ
17614 }"},
17615 indoc! {"
17616 function test() {
17617 console.logˇ('test')
17618 }"},
17619 &mut cx,
17620 );
17621}
17622
17623#[gpui::test]
17624async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
17625 init_test(cx, |_| {});
17626 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
17627 language_registry.add(markdown_lang());
17628 language_registry.add(rust_lang());
17629 let buffer = cx.new(|cx| {
17630 let mut buffer = language::Buffer::local(
17631 indoc! {"
17632 ```rs
17633 impl Worktree {
17634 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17635 }
17636 }
17637 ```
17638 "},
17639 cx,
17640 );
17641 buffer.set_language_registry(language_registry.clone());
17642 buffer.set_language(Some(markdown_lang()), cx);
17643 buffer
17644 });
17645 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17646 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17647 cx.executor().run_until_parked();
17648 _ = editor.update(cx, |editor, window, cx| {
17649 // Case 1: Test outer enclosing brackets
17650 select_ranges(
17651 editor,
17652 &indoc! {"
17653 ```rs
17654 impl Worktree {
17655 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17656 }
17657 }ˇ
17658 ```
17659 "},
17660 window,
17661 cx,
17662 );
17663 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17664 assert_text_with_selections(
17665 editor,
17666 &indoc! {"
17667 ```rs
17668 impl Worktree ˇ{
17669 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17670 }
17671 }
17672 ```
17673 "},
17674 cx,
17675 );
17676 // Case 2: Test inner enclosing brackets
17677 select_ranges(
17678 editor,
17679 &indoc! {"
17680 ```rs
17681 impl Worktree {
17682 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17683 }ˇ
17684 }
17685 ```
17686 "},
17687 window,
17688 cx,
17689 );
17690 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17691 assert_text_with_selections(
17692 editor,
17693 &indoc! {"
17694 ```rs
17695 impl Worktree {
17696 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
17697 }
17698 }
17699 ```
17700 "},
17701 cx,
17702 );
17703 });
17704}
17705
17706#[gpui::test]
17707async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17708 init_test(cx, |_| {});
17709
17710 let fs = FakeFs::new(cx.executor());
17711 fs.insert_tree(
17712 path!("/a"),
17713 json!({
17714 "main.rs": "fn main() { let a = 5; }",
17715 "other.rs": "// Test file",
17716 }),
17717 )
17718 .await;
17719 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17720
17721 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17722 language_registry.add(Arc::new(Language::new(
17723 LanguageConfig {
17724 name: "Rust".into(),
17725 matcher: LanguageMatcher {
17726 path_suffixes: vec!["rs".to_string()],
17727 ..Default::default()
17728 },
17729 brackets: BracketPairConfig {
17730 pairs: vec![BracketPair {
17731 start: "{".to_string(),
17732 end: "}".to_string(),
17733 close: true,
17734 surround: true,
17735 newline: true,
17736 }],
17737 disabled_scopes_by_bracket_ix: Vec::new(),
17738 },
17739 ..Default::default()
17740 },
17741 Some(tree_sitter_rust::LANGUAGE.into()),
17742 )));
17743 let mut fake_servers = language_registry.register_fake_lsp(
17744 "Rust",
17745 FakeLspAdapter {
17746 capabilities: lsp::ServerCapabilities {
17747 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17748 first_trigger_character: "{".to_string(),
17749 more_trigger_character: None,
17750 }),
17751 ..Default::default()
17752 },
17753 ..Default::default()
17754 },
17755 );
17756
17757 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17758
17759 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17760
17761 let worktree_id = workspace
17762 .update(cx, |workspace, _, cx| {
17763 workspace.project().update(cx, |project, cx| {
17764 project.worktrees(cx).next().unwrap().read(cx).id()
17765 })
17766 })
17767 .unwrap();
17768
17769 let buffer = project
17770 .update(cx, |project, cx| {
17771 project.open_local_buffer(path!("/a/main.rs"), cx)
17772 })
17773 .await
17774 .unwrap();
17775 let editor_handle = workspace
17776 .update(cx, |workspace, window, cx| {
17777 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17778 })
17779 .unwrap()
17780 .await
17781 .unwrap()
17782 .downcast::<Editor>()
17783 .unwrap();
17784
17785 cx.executor().start_waiting();
17786 let fake_server = fake_servers.next().await.unwrap();
17787
17788 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17789 |params, _| async move {
17790 assert_eq!(
17791 params.text_document_position.text_document.uri,
17792 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17793 );
17794 assert_eq!(
17795 params.text_document_position.position,
17796 lsp::Position::new(0, 21),
17797 );
17798
17799 Ok(Some(vec![lsp::TextEdit {
17800 new_text: "]".to_string(),
17801 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17802 }]))
17803 },
17804 );
17805
17806 editor_handle.update_in(cx, |editor, window, cx| {
17807 window.focus(&editor.focus_handle(cx));
17808 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17809 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17810 });
17811 editor.handle_input("{", window, cx);
17812 });
17813
17814 cx.executor().run_until_parked();
17815
17816 buffer.update(cx, |buffer, _| {
17817 assert_eq!(
17818 buffer.text(),
17819 "fn main() { let a = {5}; }",
17820 "No extra braces from on type formatting should appear in the buffer"
17821 )
17822 });
17823}
17824
17825#[gpui::test(iterations = 20, seeds(31))]
17826async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17827 init_test(cx, |_| {});
17828
17829 let mut cx = EditorLspTestContext::new_rust(
17830 lsp::ServerCapabilities {
17831 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17832 first_trigger_character: ".".to_string(),
17833 more_trigger_character: None,
17834 }),
17835 ..Default::default()
17836 },
17837 cx,
17838 )
17839 .await;
17840
17841 cx.update_buffer(|buffer, _| {
17842 // This causes autoindent to be async.
17843 buffer.set_sync_parse_timeout(Duration::ZERO)
17844 });
17845
17846 cx.set_state("fn c() {\n d()ˇ\n}\n");
17847 cx.simulate_keystroke("\n");
17848 cx.run_until_parked();
17849
17850 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17851 let mut request =
17852 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17853 let buffer_cloned = buffer_cloned.clone();
17854 async move {
17855 buffer_cloned.update(&mut cx, |buffer, _| {
17856 assert_eq!(
17857 buffer.text(),
17858 "fn c() {\n d()\n .\n}\n",
17859 "OnTypeFormatting should triggered after autoindent applied"
17860 )
17861 })?;
17862
17863 Ok(Some(vec![]))
17864 }
17865 });
17866
17867 cx.simulate_keystroke(".");
17868 cx.run_until_parked();
17869
17870 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17871 assert!(request.next().await.is_some());
17872 request.close();
17873 assert!(request.next().await.is_none());
17874}
17875
17876#[gpui::test]
17877async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17878 init_test(cx, |_| {});
17879
17880 let fs = FakeFs::new(cx.executor());
17881 fs.insert_tree(
17882 path!("/a"),
17883 json!({
17884 "main.rs": "fn main() { let a = 5; }",
17885 "other.rs": "// Test file",
17886 }),
17887 )
17888 .await;
17889
17890 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17891
17892 let server_restarts = Arc::new(AtomicUsize::new(0));
17893 let closure_restarts = Arc::clone(&server_restarts);
17894 let language_server_name = "test language server";
17895 let language_name: LanguageName = "Rust".into();
17896
17897 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17898 language_registry.add(Arc::new(Language::new(
17899 LanguageConfig {
17900 name: language_name.clone(),
17901 matcher: LanguageMatcher {
17902 path_suffixes: vec!["rs".to_string()],
17903 ..Default::default()
17904 },
17905 ..Default::default()
17906 },
17907 Some(tree_sitter_rust::LANGUAGE.into()),
17908 )));
17909 let mut fake_servers = language_registry.register_fake_lsp(
17910 "Rust",
17911 FakeLspAdapter {
17912 name: language_server_name,
17913 initialization_options: Some(json!({
17914 "testOptionValue": true
17915 })),
17916 initializer: Some(Box::new(move |fake_server| {
17917 let task_restarts = Arc::clone(&closure_restarts);
17918 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17919 task_restarts.fetch_add(1, atomic::Ordering::Release);
17920 futures::future::ready(Ok(()))
17921 });
17922 })),
17923 ..Default::default()
17924 },
17925 );
17926
17927 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17928 let _buffer = project
17929 .update(cx, |project, cx| {
17930 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17931 })
17932 .await
17933 .unwrap();
17934 let _fake_server = fake_servers.next().await.unwrap();
17935 update_test_language_settings(cx, |language_settings| {
17936 language_settings.languages.0.insert(
17937 language_name.clone().0,
17938 LanguageSettingsContent {
17939 tab_size: NonZeroU32::new(8),
17940 ..Default::default()
17941 },
17942 );
17943 });
17944 cx.executor().run_until_parked();
17945 assert_eq!(
17946 server_restarts.load(atomic::Ordering::Acquire),
17947 0,
17948 "Should not restart LSP server on an unrelated change"
17949 );
17950
17951 update_test_project_settings(cx, |project_settings| {
17952 project_settings.lsp.insert(
17953 "Some other server name".into(),
17954 LspSettings {
17955 binary: None,
17956 settings: None,
17957 initialization_options: Some(json!({
17958 "some other init value": false
17959 })),
17960 enable_lsp_tasks: false,
17961 fetch: None,
17962 },
17963 );
17964 });
17965 cx.executor().run_until_parked();
17966 assert_eq!(
17967 server_restarts.load(atomic::Ordering::Acquire),
17968 0,
17969 "Should not restart LSP server on an unrelated LSP settings change"
17970 );
17971
17972 update_test_project_settings(cx, |project_settings| {
17973 project_settings.lsp.insert(
17974 language_server_name.into(),
17975 LspSettings {
17976 binary: None,
17977 settings: None,
17978 initialization_options: Some(json!({
17979 "anotherInitValue": false
17980 })),
17981 enable_lsp_tasks: false,
17982 fetch: None,
17983 },
17984 );
17985 });
17986 cx.executor().run_until_parked();
17987 assert_eq!(
17988 server_restarts.load(atomic::Ordering::Acquire),
17989 1,
17990 "Should restart LSP server on a related LSP settings change"
17991 );
17992
17993 update_test_project_settings(cx, |project_settings| {
17994 project_settings.lsp.insert(
17995 language_server_name.into(),
17996 LspSettings {
17997 binary: None,
17998 settings: None,
17999 initialization_options: Some(json!({
18000 "anotherInitValue": false
18001 })),
18002 enable_lsp_tasks: false,
18003 fetch: None,
18004 },
18005 );
18006 });
18007 cx.executor().run_until_parked();
18008 assert_eq!(
18009 server_restarts.load(atomic::Ordering::Acquire),
18010 1,
18011 "Should not restart LSP server on a related LSP settings change that is the same"
18012 );
18013
18014 update_test_project_settings(cx, |project_settings| {
18015 project_settings.lsp.insert(
18016 language_server_name.into(),
18017 LspSettings {
18018 binary: None,
18019 settings: None,
18020 initialization_options: None,
18021 enable_lsp_tasks: false,
18022 fetch: None,
18023 },
18024 );
18025 });
18026 cx.executor().run_until_parked();
18027 assert_eq!(
18028 server_restarts.load(atomic::Ordering::Acquire),
18029 2,
18030 "Should restart LSP server on another related LSP settings change"
18031 );
18032}
18033
18034#[gpui::test]
18035async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18036 init_test(cx, |_| {});
18037
18038 let mut cx = EditorLspTestContext::new_rust(
18039 lsp::ServerCapabilities {
18040 completion_provider: Some(lsp::CompletionOptions {
18041 trigger_characters: Some(vec![".".to_string()]),
18042 resolve_provider: Some(true),
18043 ..Default::default()
18044 }),
18045 ..Default::default()
18046 },
18047 cx,
18048 )
18049 .await;
18050
18051 cx.set_state("fn main() { let a = 2ˇ; }");
18052 cx.simulate_keystroke(".");
18053 let completion_item = lsp::CompletionItem {
18054 label: "some".into(),
18055 kind: Some(lsp::CompletionItemKind::SNIPPET),
18056 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18057 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18058 kind: lsp::MarkupKind::Markdown,
18059 value: "```rust\nSome(2)\n```".to_string(),
18060 })),
18061 deprecated: Some(false),
18062 sort_text: Some("fffffff2".to_string()),
18063 filter_text: Some("some".to_string()),
18064 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18065 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18066 range: lsp::Range {
18067 start: lsp::Position {
18068 line: 0,
18069 character: 22,
18070 },
18071 end: lsp::Position {
18072 line: 0,
18073 character: 22,
18074 },
18075 },
18076 new_text: "Some(2)".to_string(),
18077 })),
18078 additional_text_edits: Some(vec![lsp::TextEdit {
18079 range: lsp::Range {
18080 start: lsp::Position {
18081 line: 0,
18082 character: 20,
18083 },
18084 end: lsp::Position {
18085 line: 0,
18086 character: 22,
18087 },
18088 },
18089 new_text: "".to_string(),
18090 }]),
18091 ..Default::default()
18092 };
18093
18094 let closure_completion_item = completion_item.clone();
18095 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18096 let task_completion_item = closure_completion_item.clone();
18097 async move {
18098 Ok(Some(lsp::CompletionResponse::Array(vec![
18099 task_completion_item,
18100 ])))
18101 }
18102 });
18103
18104 request.next().await;
18105
18106 cx.condition(|editor, _| editor.context_menu_visible())
18107 .await;
18108 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18109 editor
18110 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18111 .unwrap()
18112 });
18113 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18114
18115 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18116 let task_completion_item = completion_item.clone();
18117 async move { Ok(task_completion_item) }
18118 })
18119 .next()
18120 .await
18121 .unwrap();
18122 apply_additional_edits.await.unwrap();
18123 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18124}
18125
18126#[gpui::test]
18127async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18128 init_test(cx, |_| {});
18129
18130 let mut cx = EditorLspTestContext::new_rust(
18131 lsp::ServerCapabilities {
18132 completion_provider: Some(lsp::CompletionOptions {
18133 trigger_characters: Some(vec![".".to_string()]),
18134 resolve_provider: Some(true),
18135 ..Default::default()
18136 }),
18137 ..Default::default()
18138 },
18139 cx,
18140 )
18141 .await;
18142
18143 cx.set_state("fn main() { let a = 2ˇ; }");
18144 cx.simulate_keystroke(".");
18145
18146 let item1 = lsp::CompletionItem {
18147 label: "method id()".to_string(),
18148 filter_text: Some("id".to_string()),
18149 detail: None,
18150 documentation: None,
18151 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18152 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18153 new_text: ".id".to_string(),
18154 })),
18155 ..lsp::CompletionItem::default()
18156 };
18157
18158 let item2 = lsp::CompletionItem {
18159 label: "other".to_string(),
18160 filter_text: Some("other".to_string()),
18161 detail: None,
18162 documentation: None,
18163 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18164 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18165 new_text: ".other".to_string(),
18166 })),
18167 ..lsp::CompletionItem::default()
18168 };
18169
18170 let item1 = item1.clone();
18171 cx.set_request_handler::<lsp::request::Completion, _, _>({
18172 let item1 = item1.clone();
18173 move |_, _, _| {
18174 let item1 = item1.clone();
18175 let item2 = item2.clone();
18176 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18177 }
18178 })
18179 .next()
18180 .await;
18181
18182 cx.condition(|editor, _| editor.context_menu_visible())
18183 .await;
18184 cx.update_editor(|editor, _, _| {
18185 let context_menu = editor.context_menu.borrow_mut();
18186 let context_menu = context_menu
18187 .as_ref()
18188 .expect("Should have the context menu deployed");
18189 match context_menu {
18190 CodeContextMenu::Completions(completions_menu) => {
18191 let completions = completions_menu.completions.borrow_mut();
18192 assert_eq!(
18193 completions
18194 .iter()
18195 .map(|completion| &completion.label.text)
18196 .collect::<Vec<_>>(),
18197 vec!["method id()", "other"]
18198 )
18199 }
18200 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18201 }
18202 });
18203
18204 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18205 let item1 = item1.clone();
18206 move |_, item_to_resolve, _| {
18207 let item1 = item1.clone();
18208 async move {
18209 if item1 == item_to_resolve {
18210 Ok(lsp::CompletionItem {
18211 label: "method id()".to_string(),
18212 filter_text: Some("id".to_string()),
18213 detail: Some("Now resolved!".to_string()),
18214 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18215 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18216 range: lsp::Range::new(
18217 lsp::Position::new(0, 22),
18218 lsp::Position::new(0, 22),
18219 ),
18220 new_text: ".id".to_string(),
18221 })),
18222 ..lsp::CompletionItem::default()
18223 })
18224 } else {
18225 Ok(item_to_resolve)
18226 }
18227 }
18228 }
18229 })
18230 .next()
18231 .await
18232 .unwrap();
18233 cx.run_until_parked();
18234
18235 cx.update_editor(|editor, window, cx| {
18236 editor.context_menu_next(&Default::default(), window, cx);
18237 });
18238
18239 cx.update_editor(|editor, _, _| {
18240 let context_menu = editor.context_menu.borrow_mut();
18241 let context_menu = context_menu
18242 .as_ref()
18243 .expect("Should have the context menu deployed");
18244 match context_menu {
18245 CodeContextMenu::Completions(completions_menu) => {
18246 let completions = completions_menu.completions.borrow_mut();
18247 assert_eq!(
18248 completions
18249 .iter()
18250 .map(|completion| &completion.label.text)
18251 .collect::<Vec<_>>(),
18252 vec!["method id() Now resolved!", "other"],
18253 "Should update first completion label, but not second as the filter text did not match."
18254 );
18255 }
18256 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18257 }
18258 });
18259}
18260
18261#[gpui::test]
18262async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18263 init_test(cx, |_| {});
18264 let mut cx = EditorLspTestContext::new_rust(
18265 lsp::ServerCapabilities {
18266 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18267 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18268 completion_provider: Some(lsp::CompletionOptions {
18269 resolve_provider: Some(true),
18270 ..Default::default()
18271 }),
18272 ..Default::default()
18273 },
18274 cx,
18275 )
18276 .await;
18277 cx.set_state(indoc! {"
18278 struct TestStruct {
18279 field: i32
18280 }
18281
18282 fn mainˇ() {
18283 let unused_var = 42;
18284 let test_struct = TestStruct { field: 42 };
18285 }
18286 "});
18287 let symbol_range = cx.lsp_range(indoc! {"
18288 struct TestStruct {
18289 field: i32
18290 }
18291
18292 «fn main»() {
18293 let unused_var = 42;
18294 let test_struct = TestStruct { field: 42 };
18295 }
18296 "});
18297 let mut hover_requests =
18298 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18299 Ok(Some(lsp::Hover {
18300 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18301 kind: lsp::MarkupKind::Markdown,
18302 value: "Function documentation".to_string(),
18303 }),
18304 range: Some(symbol_range),
18305 }))
18306 });
18307
18308 // Case 1: Test that code action menu hide hover popover
18309 cx.dispatch_action(Hover);
18310 hover_requests.next().await;
18311 cx.condition(|editor, _| editor.hover_state.visible()).await;
18312 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18313 move |_, _, _| async move {
18314 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18315 lsp::CodeAction {
18316 title: "Remove unused variable".to_string(),
18317 kind: Some(CodeActionKind::QUICKFIX),
18318 edit: Some(lsp::WorkspaceEdit {
18319 changes: Some(
18320 [(
18321 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18322 vec![lsp::TextEdit {
18323 range: lsp::Range::new(
18324 lsp::Position::new(5, 4),
18325 lsp::Position::new(5, 27),
18326 ),
18327 new_text: "".to_string(),
18328 }],
18329 )]
18330 .into_iter()
18331 .collect(),
18332 ),
18333 ..Default::default()
18334 }),
18335 ..Default::default()
18336 },
18337 )]))
18338 },
18339 );
18340 cx.update_editor(|editor, window, cx| {
18341 editor.toggle_code_actions(
18342 &ToggleCodeActions {
18343 deployed_from: None,
18344 quick_launch: false,
18345 },
18346 window,
18347 cx,
18348 );
18349 });
18350 code_action_requests.next().await;
18351 cx.run_until_parked();
18352 cx.condition(|editor, _| editor.context_menu_visible())
18353 .await;
18354 cx.update_editor(|editor, _, _| {
18355 assert!(
18356 !editor.hover_state.visible(),
18357 "Hover popover should be hidden when code action menu is shown"
18358 );
18359 // Hide code actions
18360 editor.context_menu.take();
18361 });
18362
18363 // Case 2: Test that code completions hide hover popover
18364 cx.dispatch_action(Hover);
18365 hover_requests.next().await;
18366 cx.condition(|editor, _| editor.hover_state.visible()).await;
18367 let counter = Arc::new(AtomicUsize::new(0));
18368 let mut completion_requests =
18369 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18370 let counter = counter.clone();
18371 async move {
18372 counter.fetch_add(1, atomic::Ordering::Release);
18373 Ok(Some(lsp::CompletionResponse::Array(vec![
18374 lsp::CompletionItem {
18375 label: "main".into(),
18376 kind: Some(lsp::CompletionItemKind::FUNCTION),
18377 detail: Some("() -> ()".to_string()),
18378 ..Default::default()
18379 },
18380 lsp::CompletionItem {
18381 label: "TestStruct".into(),
18382 kind: Some(lsp::CompletionItemKind::STRUCT),
18383 detail: Some("struct TestStruct".to_string()),
18384 ..Default::default()
18385 },
18386 ])))
18387 }
18388 });
18389 cx.update_editor(|editor, window, cx| {
18390 editor.show_completions(&ShowCompletions, window, cx);
18391 });
18392 completion_requests.next().await;
18393 cx.condition(|editor, _| editor.context_menu_visible())
18394 .await;
18395 cx.update_editor(|editor, _, _| {
18396 assert!(
18397 !editor.hover_state.visible(),
18398 "Hover popover should be hidden when completion menu is shown"
18399 );
18400 });
18401}
18402
18403#[gpui::test]
18404async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18405 init_test(cx, |_| {});
18406
18407 let mut cx = EditorLspTestContext::new_rust(
18408 lsp::ServerCapabilities {
18409 completion_provider: Some(lsp::CompletionOptions {
18410 trigger_characters: Some(vec![".".to_string()]),
18411 resolve_provider: Some(true),
18412 ..Default::default()
18413 }),
18414 ..Default::default()
18415 },
18416 cx,
18417 )
18418 .await;
18419
18420 cx.set_state("fn main() { let a = 2ˇ; }");
18421 cx.simulate_keystroke(".");
18422
18423 let unresolved_item_1 = lsp::CompletionItem {
18424 label: "id".to_string(),
18425 filter_text: Some("id".to_string()),
18426 detail: None,
18427 documentation: None,
18428 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18429 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18430 new_text: ".id".to_string(),
18431 })),
18432 ..lsp::CompletionItem::default()
18433 };
18434 let resolved_item_1 = lsp::CompletionItem {
18435 additional_text_edits: Some(vec![lsp::TextEdit {
18436 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18437 new_text: "!!".to_string(),
18438 }]),
18439 ..unresolved_item_1.clone()
18440 };
18441 let unresolved_item_2 = lsp::CompletionItem {
18442 label: "other".to_string(),
18443 filter_text: Some("other".to_string()),
18444 detail: None,
18445 documentation: None,
18446 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18447 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18448 new_text: ".other".to_string(),
18449 })),
18450 ..lsp::CompletionItem::default()
18451 };
18452 let resolved_item_2 = lsp::CompletionItem {
18453 additional_text_edits: Some(vec![lsp::TextEdit {
18454 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18455 new_text: "??".to_string(),
18456 }]),
18457 ..unresolved_item_2.clone()
18458 };
18459
18460 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18461 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18462 cx.lsp
18463 .server
18464 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18465 let unresolved_item_1 = unresolved_item_1.clone();
18466 let resolved_item_1 = resolved_item_1.clone();
18467 let unresolved_item_2 = unresolved_item_2.clone();
18468 let resolved_item_2 = resolved_item_2.clone();
18469 let resolve_requests_1 = resolve_requests_1.clone();
18470 let resolve_requests_2 = resolve_requests_2.clone();
18471 move |unresolved_request, _| {
18472 let unresolved_item_1 = unresolved_item_1.clone();
18473 let resolved_item_1 = resolved_item_1.clone();
18474 let unresolved_item_2 = unresolved_item_2.clone();
18475 let resolved_item_2 = resolved_item_2.clone();
18476 let resolve_requests_1 = resolve_requests_1.clone();
18477 let resolve_requests_2 = resolve_requests_2.clone();
18478 async move {
18479 if unresolved_request == unresolved_item_1 {
18480 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18481 Ok(resolved_item_1.clone())
18482 } else if unresolved_request == unresolved_item_2 {
18483 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18484 Ok(resolved_item_2.clone())
18485 } else {
18486 panic!("Unexpected completion item {unresolved_request:?}")
18487 }
18488 }
18489 }
18490 })
18491 .detach();
18492
18493 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18494 let unresolved_item_1 = unresolved_item_1.clone();
18495 let unresolved_item_2 = unresolved_item_2.clone();
18496 async move {
18497 Ok(Some(lsp::CompletionResponse::Array(vec![
18498 unresolved_item_1,
18499 unresolved_item_2,
18500 ])))
18501 }
18502 })
18503 .next()
18504 .await;
18505
18506 cx.condition(|editor, _| editor.context_menu_visible())
18507 .await;
18508 cx.update_editor(|editor, _, _| {
18509 let context_menu = editor.context_menu.borrow_mut();
18510 let context_menu = context_menu
18511 .as_ref()
18512 .expect("Should have the context menu deployed");
18513 match context_menu {
18514 CodeContextMenu::Completions(completions_menu) => {
18515 let completions = completions_menu.completions.borrow_mut();
18516 assert_eq!(
18517 completions
18518 .iter()
18519 .map(|completion| &completion.label.text)
18520 .collect::<Vec<_>>(),
18521 vec!["id", "other"]
18522 )
18523 }
18524 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18525 }
18526 });
18527 cx.run_until_parked();
18528
18529 cx.update_editor(|editor, window, cx| {
18530 editor.context_menu_next(&ContextMenuNext, window, cx);
18531 });
18532 cx.run_until_parked();
18533 cx.update_editor(|editor, window, cx| {
18534 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18535 });
18536 cx.run_until_parked();
18537 cx.update_editor(|editor, window, cx| {
18538 editor.context_menu_next(&ContextMenuNext, window, cx);
18539 });
18540 cx.run_until_parked();
18541 cx.update_editor(|editor, window, cx| {
18542 editor
18543 .compose_completion(&ComposeCompletion::default(), window, cx)
18544 .expect("No task returned")
18545 })
18546 .await
18547 .expect("Completion failed");
18548 cx.run_until_parked();
18549
18550 cx.update_editor(|editor, _, cx| {
18551 assert_eq!(
18552 resolve_requests_1.load(atomic::Ordering::Acquire),
18553 1,
18554 "Should always resolve once despite multiple selections"
18555 );
18556 assert_eq!(
18557 resolve_requests_2.load(atomic::Ordering::Acquire),
18558 1,
18559 "Should always resolve once after multiple selections and applying the completion"
18560 );
18561 assert_eq!(
18562 editor.text(cx),
18563 "fn main() { let a = ??.other; }",
18564 "Should use resolved data when applying the completion"
18565 );
18566 });
18567}
18568
18569#[gpui::test]
18570async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18571 init_test(cx, |_| {});
18572
18573 let item_0 = lsp::CompletionItem {
18574 label: "abs".into(),
18575 insert_text: Some("abs".into()),
18576 data: Some(json!({ "very": "special"})),
18577 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18578 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18579 lsp::InsertReplaceEdit {
18580 new_text: "abs".to_string(),
18581 insert: lsp::Range::default(),
18582 replace: lsp::Range::default(),
18583 },
18584 )),
18585 ..lsp::CompletionItem::default()
18586 };
18587 let items = iter::once(item_0.clone())
18588 .chain((11..51).map(|i| lsp::CompletionItem {
18589 label: format!("item_{}", i),
18590 insert_text: Some(format!("item_{}", i)),
18591 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18592 ..lsp::CompletionItem::default()
18593 }))
18594 .collect::<Vec<_>>();
18595
18596 let default_commit_characters = vec!["?".to_string()];
18597 let default_data = json!({ "default": "data"});
18598 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18599 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18600 let default_edit_range = lsp::Range {
18601 start: lsp::Position {
18602 line: 0,
18603 character: 5,
18604 },
18605 end: lsp::Position {
18606 line: 0,
18607 character: 5,
18608 },
18609 };
18610
18611 let mut cx = EditorLspTestContext::new_rust(
18612 lsp::ServerCapabilities {
18613 completion_provider: Some(lsp::CompletionOptions {
18614 trigger_characters: Some(vec![".".to_string()]),
18615 resolve_provider: Some(true),
18616 ..Default::default()
18617 }),
18618 ..Default::default()
18619 },
18620 cx,
18621 )
18622 .await;
18623
18624 cx.set_state("fn main() { let a = 2ˇ; }");
18625 cx.simulate_keystroke(".");
18626
18627 let completion_data = default_data.clone();
18628 let completion_characters = default_commit_characters.clone();
18629 let completion_items = items.clone();
18630 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18631 let default_data = completion_data.clone();
18632 let default_commit_characters = completion_characters.clone();
18633 let items = completion_items.clone();
18634 async move {
18635 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18636 items,
18637 item_defaults: Some(lsp::CompletionListItemDefaults {
18638 data: Some(default_data.clone()),
18639 commit_characters: Some(default_commit_characters.clone()),
18640 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18641 default_edit_range,
18642 )),
18643 insert_text_format: Some(default_insert_text_format),
18644 insert_text_mode: Some(default_insert_text_mode),
18645 }),
18646 ..lsp::CompletionList::default()
18647 })))
18648 }
18649 })
18650 .next()
18651 .await;
18652
18653 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18654 cx.lsp
18655 .server
18656 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18657 let closure_resolved_items = resolved_items.clone();
18658 move |item_to_resolve, _| {
18659 let closure_resolved_items = closure_resolved_items.clone();
18660 async move {
18661 closure_resolved_items.lock().push(item_to_resolve.clone());
18662 Ok(item_to_resolve)
18663 }
18664 }
18665 })
18666 .detach();
18667
18668 cx.condition(|editor, _| editor.context_menu_visible())
18669 .await;
18670 cx.run_until_parked();
18671 cx.update_editor(|editor, _, _| {
18672 let menu = editor.context_menu.borrow_mut();
18673 match menu.as_ref().expect("should have the completions menu") {
18674 CodeContextMenu::Completions(completions_menu) => {
18675 assert_eq!(
18676 completions_menu
18677 .entries
18678 .borrow()
18679 .iter()
18680 .map(|mat| mat.string.clone())
18681 .collect::<Vec<String>>(),
18682 items
18683 .iter()
18684 .map(|completion| completion.label.clone())
18685 .collect::<Vec<String>>()
18686 );
18687 }
18688 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18689 }
18690 });
18691 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18692 // with 4 from the end.
18693 assert_eq!(
18694 *resolved_items.lock(),
18695 [&items[0..16], &items[items.len() - 4..items.len()]]
18696 .concat()
18697 .iter()
18698 .cloned()
18699 .map(|mut item| {
18700 if item.data.is_none() {
18701 item.data = Some(default_data.clone());
18702 }
18703 item
18704 })
18705 .collect::<Vec<lsp::CompletionItem>>(),
18706 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18707 );
18708 resolved_items.lock().clear();
18709
18710 cx.update_editor(|editor, window, cx| {
18711 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18712 });
18713 cx.run_until_parked();
18714 // Completions that have already been resolved are skipped.
18715 assert_eq!(
18716 *resolved_items.lock(),
18717 items[items.len() - 17..items.len() - 4]
18718 .iter()
18719 .cloned()
18720 .map(|mut item| {
18721 if item.data.is_none() {
18722 item.data = Some(default_data.clone());
18723 }
18724 item
18725 })
18726 .collect::<Vec<lsp::CompletionItem>>()
18727 );
18728 resolved_items.lock().clear();
18729}
18730
18731#[gpui::test]
18732async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18733 init_test(cx, |_| {});
18734
18735 let mut cx = EditorLspTestContext::new(
18736 Language::new(
18737 LanguageConfig {
18738 matcher: LanguageMatcher {
18739 path_suffixes: vec!["jsx".into()],
18740 ..Default::default()
18741 },
18742 overrides: [(
18743 "element".into(),
18744 LanguageConfigOverride {
18745 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18746 ..Default::default()
18747 },
18748 )]
18749 .into_iter()
18750 .collect(),
18751 ..Default::default()
18752 },
18753 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18754 )
18755 .with_override_query("(jsx_self_closing_element) @element")
18756 .unwrap(),
18757 lsp::ServerCapabilities {
18758 completion_provider: Some(lsp::CompletionOptions {
18759 trigger_characters: Some(vec![":".to_string()]),
18760 ..Default::default()
18761 }),
18762 ..Default::default()
18763 },
18764 cx,
18765 )
18766 .await;
18767
18768 cx.lsp
18769 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18770 Ok(Some(lsp::CompletionResponse::Array(vec![
18771 lsp::CompletionItem {
18772 label: "bg-blue".into(),
18773 ..Default::default()
18774 },
18775 lsp::CompletionItem {
18776 label: "bg-red".into(),
18777 ..Default::default()
18778 },
18779 lsp::CompletionItem {
18780 label: "bg-yellow".into(),
18781 ..Default::default()
18782 },
18783 ])))
18784 });
18785
18786 cx.set_state(r#"<p class="bgˇ" />"#);
18787
18788 // Trigger completion when typing a dash, because the dash is an extra
18789 // word character in the 'element' scope, which contains the cursor.
18790 cx.simulate_keystroke("-");
18791 cx.executor().run_until_parked();
18792 cx.update_editor(|editor, _, _| {
18793 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18794 {
18795 assert_eq!(
18796 completion_menu_entries(menu),
18797 &["bg-blue", "bg-red", "bg-yellow"]
18798 );
18799 } else {
18800 panic!("expected completion menu to be open");
18801 }
18802 });
18803
18804 cx.simulate_keystroke("l");
18805 cx.executor().run_until_parked();
18806 cx.update_editor(|editor, _, _| {
18807 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18808 {
18809 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18810 } else {
18811 panic!("expected completion menu to be open");
18812 }
18813 });
18814
18815 // When filtering completions, consider the character after the '-' to
18816 // be the start of a subword.
18817 cx.set_state(r#"<p class="yelˇ" />"#);
18818 cx.simulate_keystroke("l");
18819 cx.executor().run_until_parked();
18820 cx.update_editor(|editor, _, _| {
18821 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18822 {
18823 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18824 } else {
18825 panic!("expected completion menu to be open");
18826 }
18827 });
18828}
18829
18830fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18831 let entries = menu.entries.borrow();
18832 entries.iter().map(|mat| mat.string.clone()).collect()
18833}
18834
18835#[gpui::test]
18836async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18837 init_test(cx, |settings| {
18838 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18839 });
18840
18841 let fs = FakeFs::new(cx.executor());
18842 fs.insert_file(path!("/file.ts"), Default::default()).await;
18843
18844 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18845 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18846
18847 language_registry.add(Arc::new(Language::new(
18848 LanguageConfig {
18849 name: "TypeScript".into(),
18850 matcher: LanguageMatcher {
18851 path_suffixes: vec!["ts".to_string()],
18852 ..Default::default()
18853 },
18854 ..Default::default()
18855 },
18856 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18857 )));
18858 update_test_language_settings(cx, |settings| {
18859 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18860 });
18861
18862 let test_plugin = "test_plugin";
18863 let _ = language_registry.register_fake_lsp(
18864 "TypeScript",
18865 FakeLspAdapter {
18866 prettier_plugins: vec![test_plugin],
18867 ..Default::default()
18868 },
18869 );
18870
18871 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18872 let buffer = project
18873 .update(cx, |project, cx| {
18874 project.open_local_buffer(path!("/file.ts"), cx)
18875 })
18876 .await
18877 .unwrap();
18878
18879 let buffer_text = "one\ntwo\nthree\n";
18880 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18881 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18882 editor.update_in(cx, |editor, window, cx| {
18883 editor.set_text(buffer_text, window, cx)
18884 });
18885
18886 editor
18887 .update_in(cx, |editor, window, cx| {
18888 editor.perform_format(
18889 project.clone(),
18890 FormatTrigger::Manual,
18891 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18892 window,
18893 cx,
18894 )
18895 })
18896 .unwrap()
18897 .await;
18898 assert_eq!(
18899 editor.update(cx, |editor, cx| editor.text(cx)),
18900 buffer_text.to_string() + prettier_format_suffix,
18901 "Test prettier formatting was not applied to the original buffer text",
18902 );
18903
18904 update_test_language_settings(cx, |settings| {
18905 settings.defaults.formatter = Some(FormatterList::default())
18906 });
18907 let format = editor.update_in(cx, |editor, window, cx| {
18908 editor.perform_format(
18909 project.clone(),
18910 FormatTrigger::Manual,
18911 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18912 window,
18913 cx,
18914 )
18915 });
18916 format.await.unwrap();
18917 assert_eq!(
18918 editor.update(cx, |editor, cx| editor.text(cx)),
18919 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18920 "Autoformatting (via test prettier) was not applied to the original buffer text",
18921 );
18922}
18923
18924#[gpui::test]
18925async fn test_addition_reverts(cx: &mut TestAppContext) {
18926 init_test(cx, |_| {});
18927 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18928 let base_text = indoc! {r#"
18929 struct Row;
18930 struct Row1;
18931 struct Row2;
18932
18933 struct Row4;
18934 struct Row5;
18935 struct Row6;
18936
18937 struct Row8;
18938 struct Row9;
18939 struct Row10;"#};
18940
18941 // When addition hunks are not adjacent to carets, no hunk revert is performed
18942 assert_hunk_revert(
18943 indoc! {r#"struct Row;
18944 struct Row1;
18945 struct Row1.1;
18946 struct Row1.2;
18947 struct Row2;ˇ
18948
18949 struct Row4;
18950 struct Row5;
18951 struct Row6;
18952
18953 struct Row8;
18954 ˇstruct Row9;
18955 struct Row9.1;
18956 struct Row9.2;
18957 struct Row9.3;
18958 struct Row10;"#},
18959 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18960 indoc! {r#"struct Row;
18961 struct Row1;
18962 struct Row1.1;
18963 struct Row1.2;
18964 struct Row2;ˇ
18965
18966 struct Row4;
18967 struct Row5;
18968 struct Row6;
18969
18970 struct Row8;
18971 ˇstruct Row9;
18972 struct Row9.1;
18973 struct Row9.2;
18974 struct Row9.3;
18975 struct Row10;"#},
18976 base_text,
18977 &mut cx,
18978 );
18979 // Same for selections
18980 assert_hunk_revert(
18981 indoc! {r#"struct Row;
18982 struct Row1;
18983 struct Row2;
18984 struct Row2.1;
18985 struct Row2.2;
18986 «ˇ
18987 struct Row4;
18988 struct» Row5;
18989 «struct Row6;
18990 ˇ»
18991 struct Row9.1;
18992 struct Row9.2;
18993 struct Row9.3;
18994 struct Row8;
18995 struct Row9;
18996 struct Row10;"#},
18997 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18998 indoc! {r#"struct Row;
18999 struct Row1;
19000 struct Row2;
19001 struct Row2.1;
19002 struct Row2.2;
19003 «ˇ
19004 struct Row4;
19005 struct» Row5;
19006 «struct Row6;
19007 ˇ»
19008 struct Row9.1;
19009 struct Row9.2;
19010 struct Row9.3;
19011 struct Row8;
19012 struct Row9;
19013 struct Row10;"#},
19014 base_text,
19015 &mut cx,
19016 );
19017
19018 // When carets and selections intersect the addition hunks, those are reverted.
19019 // Adjacent carets got merged.
19020 assert_hunk_revert(
19021 indoc! {r#"struct Row;
19022 ˇ// something on the top
19023 struct Row1;
19024 struct Row2;
19025 struct Roˇw3.1;
19026 struct Row2.2;
19027 struct Row2.3;ˇ
19028
19029 struct Row4;
19030 struct ˇRow5.1;
19031 struct Row5.2;
19032 struct «Rowˇ»5.3;
19033 struct Row5;
19034 struct Row6;
19035 ˇ
19036 struct Row9.1;
19037 struct «Rowˇ»9.2;
19038 struct «ˇRow»9.3;
19039 struct Row8;
19040 struct Row9;
19041 «ˇ// something on bottom»
19042 struct Row10;"#},
19043 vec![
19044 DiffHunkStatusKind::Added,
19045 DiffHunkStatusKind::Added,
19046 DiffHunkStatusKind::Added,
19047 DiffHunkStatusKind::Added,
19048 DiffHunkStatusKind::Added,
19049 ],
19050 indoc! {r#"struct Row;
19051 ˇstruct Row1;
19052 struct Row2;
19053 ˇ
19054 struct Row4;
19055 ˇstruct Row5;
19056 struct Row6;
19057 ˇ
19058 ˇstruct Row8;
19059 struct Row9;
19060 ˇstruct Row10;"#},
19061 base_text,
19062 &mut cx,
19063 );
19064}
19065
19066#[gpui::test]
19067async fn test_modification_reverts(cx: &mut TestAppContext) {
19068 init_test(cx, |_| {});
19069 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19070 let base_text = indoc! {r#"
19071 struct Row;
19072 struct Row1;
19073 struct Row2;
19074
19075 struct Row4;
19076 struct Row5;
19077 struct Row6;
19078
19079 struct Row8;
19080 struct Row9;
19081 struct Row10;"#};
19082
19083 // Modification hunks behave the same as the addition ones.
19084 assert_hunk_revert(
19085 indoc! {r#"struct Row;
19086 struct Row1;
19087 struct Row33;
19088 ˇ
19089 struct Row4;
19090 struct Row5;
19091 struct Row6;
19092 ˇ
19093 struct Row99;
19094 struct Row9;
19095 struct Row10;"#},
19096 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19097 indoc! {r#"struct Row;
19098 struct Row1;
19099 struct Row33;
19100 ˇ
19101 struct Row4;
19102 struct Row5;
19103 struct Row6;
19104 ˇ
19105 struct Row99;
19106 struct Row9;
19107 struct Row10;"#},
19108 base_text,
19109 &mut cx,
19110 );
19111 assert_hunk_revert(
19112 indoc! {r#"struct Row;
19113 struct Row1;
19114 struct Row33;
19115 «ˇ
19116 struct Row4;
19117 struct» Row5;
19118 «struct Row6;
19119 ˇ»
19120 struct Row99;
19121 struct Row9;
19122 struct Row10;"#},
19123 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19124 indoc! {r#"struct Row;
19125 struct Row1;
19126 struct Row33;
19127 «ˇ
19128 struct Row4;
19129 struct» Row5;
19130 «struct Row6;
19131 ˇ»
19132 struct Row99;
19133 struct Row9;
19134 struct Row10;"#},
19135 base_text,
19136 &mut cx,
19137 );
19138
19139 assert_hunk_revert(
19140 indoc! {r#"ˇstruct Row1.1;
19141 struct Row1;
19142 «ˇstr»uct Row22;
19143
19144 struct ˇRow44;
19145 struct Row5;
19146 struct «Rˇ»ow66;ˇ
19147
19148 «struˇ»ct Row88;
19149 struct Row9;
19150 struct Row1011;ˇ"#},
19151 vec![
19152 DiffHunkStatusKind::Modified,
19153 DiffHunkStatusKind::Modified,
19154 DiffHunkStatusKind::Modified,
19155 DiffHunkStatusKind::Modified,
19156 DiffHunkStatusKind::Modified,
19157 DiffHunkStatusKind::Modified,
19158 ],
19159 indoc! {r#"struct Row;
19160 ˇstruct Row1;
19161 struct Row2;
19162 ˇ
19163 struct Row4;
19164 ˇstruct Row5;
19165 struct Row6;
19166 ˇ
19167 struct Row8;
19168 ˇstruct Row9;
19169 struct Row10;ˇ"#},
19170 base_text,
19171 &mut cx,
19172 );
19173}
19174
19175#[gpui::test]
19176async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19177 init_test(cx, |_| {});
19178 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19179 let base_text = indoc! {r#"
19180 one
19181
19182 two
19183 three
19184 "#};
19185
19186 cx.set_head_text(base_text);
19187 cx.set_state("\nˇ\n");
19188 cx.executor().run_until_parked();
19189 cx.update_editor(|editor, _window, cx| {
19190 editor.expand_selected_diff_hunks(cx);
19191 });
19192 cx.executor().run_until_parked();
19193 cx.update_editor(|editor, window, cx| {
19194 editor.backspace(&Default::default(), window, cx);
19195 });
19196 cx.run_until_parked();
19197 cx.assert_state_with_diff(
19198 indoc! {r#"
19199
19200 - two
19201 - threeˇ
19202 +
19203 "#}
19204 .to_string(),
19205 );
19206}
19207
19208#[gpui::test]
19209async fn test_deletion_reverts(cx: &mut TestAppContext) {
19210 init_test(cx, |_| {});
19211 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19212 let base_text = indoc! {r#"struct Row;
19213struct Row1;
19214struct Row2;
19215
19216struct Row4;
19217struct Row5;
19218struct Row6;
19219
19220struct Row8;
19221struct Row9;
19222struct Row10;"#};
19223
19224 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19225 assert_hunk_revert(
19226 indoc! {r#"struct Row;
19227 struct Row2;
19228
19229 ˇstruct Row4;
19230 struct Row5;
19231 struct Row6;
19232 ˇ
19233 struct Row8;
19234 struct Row10;"#},
19235 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19236 indoc! {r#"struct Row;
19237 struct Row2;
19238
19239 ˇstruct Row4;
19240 struct Row5;
19241 struct Row6;
19242 ˇ
19243 struct Row8;
19244 struct Row10;"#},
19245 base_text,
19246 &mut cx,
19247 );
19248 assert_hunk_revert(
19249 indoc! {r#"struct Row;
19250 struct Row2;
19251
19252 «ˇstruct Row4;
19253 struct» Row5;
19254 «struct Row6;
19255 ˇ»
19256 struct Row8;
19257 struct Row10;"#},
19258 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19259 indoc! {r#"struct Row;
19260 struct Row2;
19261
19262 «ˇstruct Row4;
19263 struct» Row5;
19264 «struct Row6;
19265 ˇ»
19266 struct Row8;
19267 struct Row10;"#},
19268 base_text,
19269 &mut cx,
19270 );
19271
19272 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19273 assert_hunk_revert(
19274 indoc! {r#"struct Row;
19275 ˇstruct Row2;
19276
19277 struct Row4;
19278 struct Row5;
19279 struct Row6;
19280
19281 struct Row8;ˇ
19282 struct Row10;"#},
19283 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19284 indoc! {r#"struct Row;
19285 struct Row1;
19286 ˇstruct Row2;
19287
19288 struct Row4;
19289 struct Row5;
19290 struct Row6;
19291
19292 struct Row8;ˇ
19293 struct Row9;
19294 struct Row10;"#},
19295 base_text,
19296 &mut cx,
19297 );
19298 assert_hunk_revert(
19299 indoc! {r#"struct Row;
19300 struct Row2«ˇ;
19301 struct Row4;
19302 struct» Row5;
19303 «struct Row6;
19304
19305 struct Row8;ˇ»
19306 struct Row10;"#},
19307 vec![
19308 DiffHunkStatusKind::Deleted,
19309 DiffHunkStatusKind::Deleted,
19310 DiffHunkStatusKind::Deleted,
19311 ],
19312 indoc! {r#"struct Row;
19313 struct Row1;
19314 struct Row2«ˇ;
19315
19316 struct Row4;
19317 struct» Row5;
19318 «struct Row6;
19319
19320 struct Row8;ˇ»
19321 struct Row9;
19322 struct Row10;"#},
19323 base_text,
19324 &mut cx,
19325 );
19326}
19327
19328#[gpui::test]
19329async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19330 init_test(cx, |_| {});
19331
19332 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19333 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19334 let base_text_3 =
19335 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19336
19337 let text_1 = edit_first_char_of_every_line(base_text_1);
19338 let text_2 = edit_first_char_of_every_line(base_text_2);
19339 let text_3 = edit_first_char_of_every_line(base_text_3);
19340
19341 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19342 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19343 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19344
19345 let multibuffer = cx.new(|cx| {
19346 let mut multibuffer = MultiBuffer::new(ReadWrite);
19347 multibuffer.push_excerpts(
19348 buffer_1.clone(),
19349 [
19350 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19351 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19352 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19353 ],
19354 cx,
19355 );
19356 multibuffer.push_excerpts(
19357 buffer_2.clone(),
19358 [
19359 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19360 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19361 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19362 ],
19363 cx,
19364 );
19365 multibuffer.push_excerpts(
19366 buffer_3.clone(),
19367 [
19368 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19369 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19370 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19371 ],
19372 cx,
19373 );
19374 multibuffer
19375 });
19376
19377 let fs = FakeFs::new(cx.executor());
19378 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19379 let (editor, cx) = cx
19380 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19381 editor.update_in(cx, |editor, _window, cx| {
19382 for (buffer, diff_base) in [
19383 (buffer_1.clone(), base_text_1),
19384 (buffer_2.clone(), base_text_2),
19385 (buffer_3.clone(), base_text_3),
19386 ] {
19387 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19388 editor
19389 .buffer
19390 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19391 }
19392 });
19393 cx.executor().run_until_parked();
19394
19395 editor.update_in(cx, |editor, window, cx| {
19396 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}");
19397 editor.select_all(&SelectAll, window, cx);
19398 editor.git_restore(&Default::default(), window, cx);
19399 });
19400 cx.executor().run_until_parked();
19401
19402 // When all ranges are selected, all buffer hunks are reverted.
19403 editor.update(cx, |editor, cx| {
19404 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");
19405 });
19406 buffer_1.update(cx, |buffer, _| {
19407 assert_eq!(buffer.text(), base_text_1);
19408 });
19409 buffer_2.update(cx, |buffer, _| {
19410 assert_eq!(buffer.text(), base_text_2);
19411 });
19412 buffer_3.update(cx, |buffer, _| {
19413 assert_eq!(buffer.text(), base_text_3);
19414 });
19415
19416 editor.update_in(cx, |editor, window, cx| {
19417 editor.undo(&Default::default(), window, cx);
19418 });
19419
19420 editor.update_in(cx, |editor, window, cx| {
19421 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19422 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19423 });
19424 editor.git_restore(&Default::default(), window, cx);
19425 });
19426
19427 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19428 // but not affect buffer_2 and its related excerpts.
19429 editor.update(cx, |editor, cx| {
19430 assert_eq!(
19431 editor.text(cx),
19432 "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}"
19433 );
19434 });
19435 buffer_1.update(cx, |buffer, _| {
19436 assert_eq!(buffer.text(), base_text_1);
19437 });
19438 buffer_2.update(cx, |buffer, _| {
19439 assert_eq!(
19440 buffer.text(),
19441 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19442 );
19443 });
19444 buffer_3.update(cx, |buffer, _| {
19445 assert_eq!(
19446 buffer.text(),
19447 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19448 );
19449 });
19450
19451 fn edit_first_char_of_every_line(text: &str) -> String {
19452 text.split('\n')
19453 .map(|line| format!("X{}", &line[1..]))
19454 .collect::<Vec<_>>()
19455 .join("\n")
19456 }
19457}
19458
19459#[gpui::test]
19460async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19461 init_test(cx, |_| {});
19462
19463 let cols = 4;
19464 let rows = 10;
19465 let sample_text_1 = sample_text(rows, cols, 'a');
19466 assert_eq!(
19467 sample_text_1,
19468 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19469 );
19470 let sample_text_2 = sample_text(rows, cols, 'l');
19471 assert_eq!(
19472 sample_text_2,
19473 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19474 );
19475 let sample_text_3 = sample_text(rows, cols, 'v');
19476 assert_eq!(
19477 sample_text_3,
19478 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19479 );
19480
19481 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19482 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19483 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19484
19485 let multi_buffer = cx.new(|cx| {
19486 let mut multibuffer = MultiBuffer::new(ReadWrite);
19487 multibuffer.push_excerpts(
19488 buffer_1.clone(),
19489 [
19490 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19491 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19492 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19493 ],
19494 cx,
19495 );
19496 multibuffer.push_excerpts(
19497 buffer_2.clone(),
19498 [
19499 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19500 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19501 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19502 ],
19503 cx,
19504 );
19505 multibuffer.push_excerpts(
19506 buffer_3.clone(),
19507 [
19508 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19509 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19510 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19511 ],
19512 cx,
19513 );
19514 multibuffer
19515 });
19516
19517 let fs = FakeFs::new(cx.executor());
19518 fs.insert_tree(
19519 "/a",
19520 json!({
19521 "main.rs": sample_text_1,
19522 "other.rs": sample_text_2,
19523 "lib.rs": sample_text_3,
19524 }),
19525 )
19526 .await;
19527 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19528 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19529 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19530 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19531 Editor::new(
19532 EditorMode::full(),
19533 multi_buffer,
19534 Some(project.clone()),
19535 window,
19536 cx,
19537 )
19538 });
19539 let multibuffer_item_id = workspace
19540 .update(cx, |workspace, window, cx| {
19541 assert!(
19542 workspace.active_item(cx).is_none(),
19543 "active item should be None before the first item is added"
19544 );
19545 workspace.add_item_to_active_pane(
19546 Box::new(multi_buffer_editor.clone()),
19547 None,
19548 true,
19549 window,
19550 cx,
19551 );
19552 let active_item = workspace
19553 .active_item(cx)
19554 .expect("should have an active item after adding the multi buffer");
19555 assert_eq!(
19556 active_item.buffer_kind(cx),
19557 ItemBufferKind::Multibuffer,
19558 "A multi buffer was expected to active after adding"
19559 );
19560 active_item.item_id()
19561 })
19562 .unwrap();
19563 cx.executor().run_until_parked();
19564
19565 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19566 editor.change_selections(
19567 SelectionEffects::scroll(Autoscroll::Next),
19568 window,
19569 cx,
19570 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
19571 );
19572 editor.open_excerpts(&OpenExcerpts, window, cx);
19573 });
19574 cx.executor().run_until_parked();
19575 let first_item_id = workspace
19576 .update(cx, |workspace, window, cx| {
19577 let active_item = workspace
19578 .active_item(cx)
19579 .expect("should have an active item after navigating into the 1st buffer");
19580 let first_item_id = active_item.item_id();
19581 assert_ne!(
19582 first_item_id, multibuffer_item_id,
19583 "Should navigate into the 1st buffer and activate it"
19584 );
19585 assert_eq!(
19586 active_item.buffer_kind(cx),
19587 ItemBufferKind::Singleton,
19588 "New active item should be a singleton buffer"
19589 );
19590 assert_eq!(
19591 active_item
19592 .act_as::<Editor>(cx)
19593 .expect("should have navigated into an editor for the 1st buffer")
19594 .read(cx)
19595 .text(cx),
19596 sample_text_1
19597 );
19598
19599 workspace
19600 .go_back(workspace.active_pane().downgrade(), window, cx)
19601 .detach_and_log_err(cx);
19602
19603 first_item_id
19604 })
19605 .unwrap();
19606 cx.executor().run_until_parked();
19607 workspace
19608 .update(cx, |workspace, _, cx| {
19609 let active_item = workspace
19610 .active_item(cx)
19611 .expect("should have an active item after navigating back");
19612 assert_eq!(
19613 active_item.item_id(),
19614 multibuffer_item_id,
19615 "Should navigate back to the multi buffer"
19616 );
19617 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19618 })
19619 .unwrap();
19620
19621 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19622 editor.change_selections(
19623 SelectionEffects::scroll(Autoscroll::Next),
19624 window,
19625 cx,
19626 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
19627 );
19628 editor.open_excerpts(&OpenExcerpts, window, cx);
19629 });
19630 cx.executor().run_until_parked();
19631 let second_item_id = workspace
19632 .update(cx, |workspace, window, cx| {
19633 let active_item = workspace
19634 .active_item(cx)
19635 .expect("should have an active item after navigating into the 2nd buffer");
19636 let second_item_id = active_item.item_id();
19637 assert_ne!(
19638 second_item_id, multibuffer_item_id,
19639 "Should navigate away from the multibuffer"
19640 );
19641 assert_ne!(
19642 second_item_id, first_item_id,
19643 "Should navigate into the 2nd buffer and activate it"
19644 );
19645 assert_eq!(
19646 active_item.buffer_kind(cx),
19647 ItemBufferKind::Singleton,
19648 "New active item should be a singleton buffer"
19649 );
19650 assert_eq!(
19651 active_item
19652 .act_as::<Editor>(cx)
19653 .expect("should have navigated into an editor")
19654 .read(cx)
19655 .text(cx),
19656 sample_text_2
19657 );
19658
19659 workspace
19660 .go_back(workspace.active_pane().downgrade(), window, cx)
19661 .detach_and_log_err(cx);
19662
19663 second_item_id
19664 })
19665 .unwrap();
19666 cx.executor().run_until_parked();
19667 workspace
19668 .update(cx, |workspace, _, cx| {
19669 let active_item = workspace
19670 .active_item(cx)
19671 .expect("should have an active item after navigating back from the 2nd buffer");
19672 assert_eq!(
19673 active_item.item_id(),
19674 multibuffer_item_id,
19675 "Should navigate back from the 2nd buffer to the multi buffer"
19676 );
19677 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19678 })
19679 .unwrap();
19680
19681 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19682 editor.change_selections(
19683 SelectionEffects::scroll(Autoscroll::Next),
19684 window,
19685 cx,
19686 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
19687 );
19688 editor.open_excerpts(&OpenExcerpts, window, cx);
19689 });
19690 cx.executor().run_until_parked();
19691 workspace
19692 .update(cx, |workspace, window, cx| {
19693 let active_item = workspace
19694 .active_item(cx)
19695 .expect("should have an active item after navigating into the 3rd buffer");
19696 let third_item_id = active_item.item_id();
19697 assert_ne!(
19698 third_item_id, multibuffer_item_id,
19699 "Should navigate into the 3rd buffer and activate it"
19700 );
19701 assert_ne!(third_item_id, first_item_id);
19702 assert_ne!(third_item_id, second_item_id);
19703 assert_eq!(
19704 active_item.buffer_kind(cx),
19705 ItemBufferKind::Singleton,
19706 "New active item should be a singleton buffer"
19707 );
19708 assert_eq!(
19709 active_item
19710 .act_as::<Editor>(cx)
19711 .expect("should have navigated into an editor")
19712 .read(cx)
19713 .text(cx),
19714 sample_text_3
19715 );
19716
19717 workspace
19718 .go_back(workspace.active_pane().downgrade(), window, cx)
19719 .detach_and_log_err(cx);
19720 })
19721 .unwrap();
19722 cx.executor().run_until_parked();
19723 workspace
19724 .update(cx, |workspace, _, cx| {
19725 let active_item = workspace
19726 .active_item(cx)
19727 .expect("should have an active item after navigating back from the 3rd buffer");
19728 assert_eq!(
19729 active_item.item_id(),
19730 multibuffer_item_id,
19731 "Should navigate back from the 3rd buffer to the multi buffer"
19732 );
19733 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19734 })
19735 .unwrap();
19736}
19737
19738#[gpui::test]
19739async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19740 init_test(cx, |_| {});
19741
19742 let mut cx = EditorTestContext::new(cx).await;
19743
19744 let diff_base = r#"
19745 use some::mod;
19746
19747 const A: u32 = 42;
19748
19749 fn main() {
19750 println!("hello");
19751
19752 println!("world");
19753 }
19754 "#
19755 .unindent();
19756
19757 cx.set_state(
19758 &r#"
19759 use some::modified;
19760
19761 ˇ
19762 fn main() {
19763 println!("hello there");
19764
19765 println!("around the");
19766 println!("world");
19767 }
19768 "#
19769 .unindent(),
19770 );
19771
19772 cx.set_head_text(&diff_base);
19773 executor.run_until_parked();
19774
19775 cx.update_editor(|editor, window, cx| {
19776 editor.go_to_next_hunk(&GoToHunk, window, cx);
19777 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19778 });
19779 executor.run_until_parked();
19780 cx.assert_state_with_diff(
19781 r#"
19782 use some::modified;
19783
19784
19785 fn main() {
19786 - println!("hello");
19787 + ˇ println!("hello there");
19788
19789 println!("around the");
19790 println!("world");
19791 }
19792 "#
19793 .unindent(),
19794 );
19795
19796 cx.update_editor(|editor, window, cx| {
19797 for _ in 0..2 {
19798 editor.go_to_next_hunk(&GoToHunk, window, cx);
19799 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19800 }
19801 });
19802 executor.run_until_parked();
19803 cx.assert_state_with_diff(
19804 r#"
19805 - use some::mod;
19806 + ˇuse some::modified;
19807
19808
19809 fn main() {
19810 - println!("hello");
19811 + println!("hello there");
19812
19813 + println!("around the");
19814 println!("world");
19815 }
19816 "#
19817 .unindent(),
19818 );
19819
19820 cx.update_editor(|editor, window, cx| {
19821 editor.go_to_next_hunk(&GoToHunk, window, cx);
19822 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19823 });
19824 executor.run_until_parked();
19825 cx.assert_state_with_diff(
19826 r#"
19827 - use some::mod;
19828 + use some::modified;
19829
19830 - const A: u32 = 42;
19831 ˇ
19832 fn main() {
19833 - println!("hello");
19834 + println!("hello there");
19835
19836 + println!("around the");
19837 println!("world");
19838 }
19839 "#
19840 .unindent(),
19841 );
19842
19843 cx.update_editor(|editor, window, cx| {
19844 editor.cancel(&Cancel, window, cx);
19845 });
19846
19847 cx.assert_state_with_diff(
19848 r#"
19849 use some::modified;
19850
19851 ˇ
19852 fn main() {
19853 println!("hello there");
19854
19855 println!("around the");
19856 println!("world");
19857 }
19858 "#
19859 .unindent(),
19860 );
19861}
19862
19863#[gpui::test]
19864async fn test_diff_base_change_with_expanded_diff_hunks(
19865 executor: BackgroundExecutor,
19866 cx: &mut TestAppContext,
19867) {
19868 init_test(cx, |_| {});
19869
19870 let mut cx = EditorTestContext::new(cx).await;
19871
19872 let diff_base = r#"
19873 use some::mod1;
19874 use some::mod2;
19875
19876 const A: u32 = 42;
19877 const B: u32 = 42;
19878 const C: u32 = 42;
19879
19880 fn main() {
19881 println!("hello");
19882
19883 println!("world");
19884 }
19885 "#
19886 .unindent();
19887
19888 cx.set_state(
19889 &r#"
19890 use some::mod2;
19891
19892 const A: u32 = 42;
19893 const C: u32 = 42;
19894
19895 fn main(ˇ) {
19896 //println!("hello");
19897
19898 println!("world");
19899 //
19900 //
19901 }
19902 "#
19903 .unindent(),
19904 );
19905
19906 cx.set_head_text(&diff_base);
19907 executor.run_until_parked();
19908
19909 cx.update_editor(|editor, window, cx| {
19910 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19911 });
19912 executor.run_until_parked();
19913 cx.assert_state_with_diff(
19914 r#"
19915 - use some::mod1;
19916 use some::mod2;
19917
19918 const A: u32 = 42;
19919 - const B: u32 = 42;
19920 const C: u32 = 42;
19921
19922 fn main(ˇ) {
19923 - println!("hello");
19924 + //println!("hello");
19925
19926 println!("world");
19927 + //
19928 + //
19929 }
19930 "#
19931 .unindent(),
19932 );
19933
19934 cx.set_head_text("new diff base!");
19935 executor.run_until_parked();
19936 cx.assert_state_with_diff(
19937 r#"
19938 - new diff base!
19939 + use some::mod2;
19940 +
19941 + const A: u32 = 42;
19942 + const C: u32 = 42;
19943 +
19944 + fn main(ˇ) {
19945 + //println!("hello");
19946 +
19947 + println!("world");
19948 + //
19949 + //
19950 + }
19951 "#
19952 .unindent(),
19953 );
19954}
19955
19956#[gpui::test]
19957async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19958 init_test(cx, |_| {});
19959
19960 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19961 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19962 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19963 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19964 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19965 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19966
19967 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19968 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19969 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19970
19971 let multi_buffer = cx.new(|cx| {
19972 let mut multibuffer = MultiBuffer::new(ReadWrite);
19973 multibuffer.push_excerpts(
19974 buffer_1.clone(),
19975 [
19976 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19977 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19978 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19979 ],
19980 cx,
19981 );
19982 multibuffer.push_excerpts(
19983 buffer_2.clone(),
19984 [
19985 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19986 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19987 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19988 ],
19989 cx,
19990 );
19991 multibuffer.push_excerpts(
19992 buffer_3.clone(),
19993 [
19994 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19995 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19996 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19997 ],
19998 cx,
19999 );
20000 multibuffer
20001 });
20002
20003 let editor =
20004 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20005 editor
20006 .update(cx, |editor, _window, cx| {
20007 for (buffer, diff_base) in [
20008 (buffer_1.clone(), file_1_old),
20009 (buffer_2.clone(), file_2_old),
20010 (buffer_3.clone(), file_3_old),
20011 ] {
20012 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20013 editor
20014 .buffer
20015 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20016 }
20017 })
20018 .unwrap();
20019
20020 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20021 cx.run_until_parked();
20022
20023 cx.assert_editor_state(
20024 &"
20025 ˇaaa
20026 ccc
20027 ddd
20028
20029 ggg
20030 hhh
20031
20032
20033 lll
20034 mmm
20035 NNN
20036
20037 qqq
20038 rrr
20039
20040 uuu
20041 111
20042 222
20043 333
20044
20045 666
20046 777
20047
20048 000
20049 !!!"
20050 .unindent(),
20051 );
20052
20053 cx.update_editor(|editor, window, cx| {
20054 editor.select_all(&SelectAll, window, cx);
20055 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20056 });
20057 cx.executor().run_until_parked();
20058
20059 cx.assert_state_with_diff(
20060 "
20061 «aaa
20062 - bbb
20063 ccc
20064 ddd
20065
20066 ggg
20067 hhh
20068
20069
20070 lll
20071 mmm
20072 - nnn
20073 + NNN
20074
20075 qqq
20076 rrr
20077
20078 uuu
20079 111
20080 222
20081 333
20082
20083 + 666
20084 777
20085
20086 000
20087 !!!ˇ»"
20088 .unindent(),
20089 );
20090}
20091
20092#[gpui::test]
20093async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20094 init_test(cx, |_| {});
20095
20096 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20097 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20098
20099 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20100 let multi_buffer = cx.new(|cx| {
20101 let mut multibuffer = MultiBuffer::new(ReadWrite);
20102 multibuffer.push_excerpts(
20103 buffer.clone(),
20104 [
20105 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20106 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20107 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20108 ],
20109 cx,
20110 );
20111 multibuffer
20112 });
20113
20114 let editor =
20115 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20116 editor
20117 .update(cx, |editor, _window, cx| {
20118 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20119 editor
20120 .buffer
20121 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20122 })
20123 .unwrap();
20124
20125 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20126 cx.run_until_parked();
20127
20128 cx.update_editor(|editor, window, cx| {
20129 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20130 });
20131 cx.executor().run_until_parked();
20132
20133 // When the start of a hunk coincides with the start of its excerpt,
20134 // the hunk is expanded. When the start of a hunk is earlier than
20135 // the start of its excerpt, the hunk is not expanded.
20136 cx.assert_state_with_diff(
20137 "
20138 ˇaaa
20139 - bbb
20140 + BBB
20141
20142 - ddd
20143 - eee
20144 + DDD
20145 + EEE
20146 fff
20147
20148 iii
20149 "
20150 .unindent(),
20151 );
20152}
20153
20154#[gpui::test]
20155async fn test_edits_around_expanded_insertion_hunks(
20156 executor: BackgroundExecutor,
20157 cx: &mut TestAppContext,
20158) {
20159 init_test(cx, |_| {});
20160
20161 let mut cx = EditorTestContext::new(cx).await;
20162
20163 let diff_base = r#"
20164 use some::mod1;
20165 use some::mod2;
20166
20167 const A: u32 = 42;
20168
20169 fn main() {
20170 println!("hello");
20171
20172 println!("world");
20173 }
20174 "#
20175 .unindent();
20176 executor.run_until_parked();
20177 cx.set_state(
20178 &r#"
20179 use some::mod1;
20180 use some::mod2;
20181
20182 const A: u32 = 42;
20183 const B: u32 = 42;
20184 const C: u32 = 42;
20185 ˇ
20186
20187 fn main() {
20188 println!("hello");
20189
20190 println!("world");
20191 }
20192 "#
20193 .unindent(),
20194 );
20195
20196 cx.set_head_text(&diff_base);
20197 executor.run_until_parked();
20198
20199 cx.update_editor(|editor, window, cx| {
20200 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20201 });
20202 executor.run_until_parked();
20203
20204 cx.assert_state_with_diff(
20205 r#"
20206 use some::mod1;
20207 use some::mod2;
20208
20209 const A: u32 = 42;
20210 + const B: u32 = 42;
20211 + const C: u32 = 42;
20212 + ˇ
20213
20214 fn main() {
20215 println!("hello");
20216
20217 println!("world");
20218 }
20219 "#
20220 .unindent(),
20221 );
20222
20223 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20224 executor.run_until_parked();
20225
20226 cx.assert_state_with_diff(
20227 r#"
20228 use some::mod1;
20229 use some::mod2;
20230
20231 const A: u32 = 42;
20232 + const B: u32 = 42;
20233 + const C: u32 = 42;
20234 + const D: u32 = 42;
20235 + ˇ
20236
20237 fn main() {
20238 println!("hello");
20239
20240 println!("world");
20241 }
20242 "#
20243 .unindent(),
20244 );
20245
20246 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20247 executor.run_until_parked();
20248
20249 cx.assert_state_with_diff(
20250 r#"
20251 use some::mod1;
20252 use some::mod2;
20253
20254 const A: u32 = 42;
20255 + const B: u32 = 42;
20256 + const C: u32 = 42;
20257 + const D: u32 = 42;
20258 + const E: u32 = 42;
20259 + ˇ
20260
20261 fn main() {
20262 println!("hello");
20263
20264 println!("world");
20265 }
20266 "#
20267 .unindent(),
20268 );
20269
20270 cx.update_editor(|editor, window, cx| {
20271 editor.delete_line(&DeleteLine, window, cx);
20272 });
20273 executor.run_until_parked();
20274
20275 cx.assert_state_with_diff(
20276 r#"
20277 use some::mod1;
20278 use some::mod2;
20279
20280 const A: u32 = 42;
20281 + const B: u32 = 42;
20282 + const C: u32 = 42;
20283 + const D: u32 = 42;
20284 + const E: u32 = 42;
20285 ˇ
20286 fn main() {
20287 println!("hello");
20288
20289 println!("world");
20290 }
20291 "#
20292 .unindent(),
20293 );
20294
20295 cx.update_editor(|editor, 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 editor.move_up(&MoveUp, window, cx);
20301 editor.delete_line(&DeleteLine, window, cx);
20302 });
20303 executor.run_until_parked();
20304 cx.assert_state_with_diff(
20305 r#"
20306 use some::mod1;
20307 use some::mod2;
20308
20309 const A: u32 = 42;
20310 + const B: u32 = 42;
20311 ˇ
20312 fn main() {
20313 println!("hello");
20314
20315 println!("world");
20316 }
20317 "#
20318 .unindent(),
20319 );
20320
20321 cx.update_editor(|editor, window, cx| {
20322 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20323 editor.delete_line(&DeleteLine, window, cx);
20324 });
20325 executor.run_until_parked();
20326 cx.assert_state_with_diff(
20327 r#"
20328 ˇ
20329 fn main() {
20330 println!("hello");
20331
20332 println!("world");
20333 }
20334 "#
20335 .unindent(),
20336 );
20337}
20338
20339#[gpui::test]
20340async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20341 init_test(cx, |_| {});
20342
20343 let mut cx = EditorTestContext::new(cx).await;
20344 cx.set_head_text(indoc! { "
20345 one
20346 two
20347 three
20348 four
20349 five
20350 "
20351 });
20352 cx.set_state(indoc! { "
20353 one
20354 ˇthree
20355 five
20356 "});
20357 cx.run_until_parked();
20358 cx.update_editor(|editor, window, cx| {
20359 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20360 });
20361 cx.assert_state_with_diff(
20362 indoc! { "
20363 one
20364 - two
20365 ˇthree
20366 - four
20367 five
20368 "}
20369 .to_string(),
20370 );
20371 cx.update_editor(|editor, window, cx| {
20372 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20373 });
20374
20375 cx.assert_state_with_diff(
20376 indoc! { "
20377 one
20378 ˇthree
20379 five
20380 "}
20381 .to_string(),
20382 );
20383
20384 cx.set_state(indoc! { "
20385 one
20386 ˇTWO
20387 three
20388 four
20389 five
20390 "});
20391 cx.run_until_parked();
20392 cx.update_editor(|editor, window, cx| {
20393 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20394 });
20395
20396 cx.assert_state_with_diff(
20397 indoc! { "
20398 one
20399 - two
20400 + ˇTWO
20401 three
20402 four
20403 five
20404 "}
20405 .to_string(),
20406 );
20407 cx.update_editor(|editor, window, cx| {
20408 editor.move_up(&Default::default(), window, cx);
20409 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20410 });
20411 cx.assert_state_with_diff(
20412 indoc! { "
20413 one
20414 ˇTWO
20415 three
20416 four
20417 five
20418 "}
20419 .to_string(),
20420 );
20421}
20422
20423#[gpui::test]
20424async fn test_edits_around_expanded_deletion_hunks(
20425 executor: BackgroundExecutor,
20426 cx: &mut TestAppContext,
20427) {
20428 init_test(cx, |_| {});
20429
20430 let mut cx = EditorTestContext::new(cx).await;
20431
20432 let diff_base = r#"
20433 use some::mod1;
20434 use some::mod2;
20435
20436 const A: u32 = 42;
20437 const B: u32 = 42;
20438 const C: u32 = 42;
20439
20440
20441 fn main() {
20442 println!("hello");
20443
20444 println!("world");
20445 }
20446 "#
20447 .unindent();
20448 executor.run_until_parked();
20449 cx.set_state(
20450 &r#"
20451 use some::mod1;
20452 use some::mod2;
20453
20454 ˇconst B: u32 = 42;
20455 const C: u32 = 42;
20456
20457
20458 fn main() {
20459 println!("hello");
20460
20461 println!("world");
20462 }
20463 "#
20464 .unindent(),
20465 );
20466
20467 cx.set_head_text(&diff_base);
20468 executor.run_until_parked();
20469
20470 cx.update_editor(|editor, window, cx| {
20471 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20472 });
20473 executor.run_until_parked();
20474
20475 cx.assert_state_with_diff(
20476 r#"
20477 use some::mod1;
20478 use some::mod2;
20479
20480 - const A: u32 = 42;
20481 ˇconst B: u32 = 42;
20482 const C: u32 = 42;
20483
20484
20485 fn main() {
20486 println!("hello");
20487
20488 println!("world");
20489 }
20490 "#
20491 .unindent(),
20492 );
20493
20494 cx.update_editor(|editor, window, cx| {
20495 editor.delete_line(&DeleteLine, window, cx);
20496 });
20497 executor.run_until_parked();
20498 cx.assert_state_with_diff(
20499 r#"
20500 use some::mod1;
20501 use some::mod2;
20502
20503 - const A: u32 = 42;
20504 - const B: u32 = 42;
20505 ˇconst C: u32 = 42;
20506
20507
20508 fn main() {
20509 println!("hello");
20510
20511 println!("world");
20512 }
20513 "#
20514 .unindent(),
20515 );
20516
20517 cx.update_editor(|editor, window, cx| {
20518 editor.delete_line(&DeleteLine, window, cx);
20519 });
20520 executor.run_until_parked();
20521 cx.assert_state_with_diff(
20522 r#"
20523 use some::mod1;
20524 use some::mod2;
20525
20526 - const A: u32 = 42;
20527 - const B: u32 = 42;
20528 - const C: u32 = 42;
20529 ˇ
20530
20531 fn main() {
20532 println!("hello");
20533
20534 println!("world");
20535 }
20536 "#
20537 .unindent(),
20538 );
20539
20540 cx.update_editor(|editor, window, cx| {
20541 editor.handle_input("replacement", window, cx);
20542 });
20543 executor.run_until_parked();
20544 cx.assert_state_with_diff(
20545 r#"
20546 use some::mod1;
20547 use some::mod2;
20548
20549 - const A: u32 = 42;
20550 - const B: u32 = 42;
20551 - const C: u32 = 42;
20552 -
20553 + replacementˇ
20554
20555 fn main() {
20556 println!("hello");
20557
20558 println!("world");
20559 }
20560 "#
20561 .unindent(),
20562 );
20563}
20564
20565#[gpui::test]
20566async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20567 init_test(cx, |_| {});
20568
20569 let mut cx = EditorTestContext::new(cx).await;
20570
20571 let base_text = r#"
20572 one
20573 two
20574 three
20575 four
20576 five
20577 "#
20578 .unindent();
20579 executor.run_until_parked();
20580 cx.set_state(
20581 &r#"
20582 one
20583 two
20584 fˇour
20585 five
20586 "#
20587 .unindent(),
20588 );
20589
20590 cx.set_head_text(&base_text);
20591 executor.run_until_parked();
20592
20593 cx.update_editor(|editor, window, cx| {
20594 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20595 });
20596 executor.run_until_parked();
20597
20598 cx.assert_state_with_diff(
20599 r#"
20600 one
20601 two
20602 - three
20603 fˇour
20604 five
20605 "#
20606 .unindent(),
20607 );
20608
20609 cx.update_editor(|editor, window, cx| {
20610 editor.backspace(&Backspace, window, cx);
20611 editor.backspace(&Backspace, window, cx);
20612 });
20613 executor.run_until_parked();
20614 cx.assert_state_with_diff(
20615 r#"
20616 one
20617 two
20618 - threeˇ
20619 - four
20620 + our
20621 five
20622 "#
20623 .unindent(),
20624 );
20625}
20626
20627#[gpui::test]
20628async fn test_edit_after_expanded_modification_hunk(
20629 executor: BackgroundExecutor,
20630 cx: &mut TestAppContext,
20631) {
20632 init_test(cx, |_| {});
20633
20634 let mut cx = EditorTestContext::new(cx).await;
20635
20636 let diff_base = r#"
20637 use some::mod1;
20638 use some::mod2;
20639
20640 const A: u32 = 42;
20641 const B: u32 = 42;
20642 const C: u32 = 42;
20643 const D: u32 = 42;
20644
20645
20646 fn main() {
20647 println!("hello");
20648
20649 println!("world");
20650 }"#
20651 .unindent();
20652
20653 cx.set_state(
20654 &r#"
20655 use some::mod1;
20656 use some::mod2;
20657
20658 const A: u32 = 42;
20659 const B: u32 = 42;
20660 const C: u32 = 43ˇ
20661 const D: u32 = 42;
20662
20663
20664 fn main() {
20665 println!("hello");
20666
20667 println!("world");
20668 }"#
20669 .unindent(),
20670 );
20671
20672 cx.set_head_text(&diff_base);
20673 executor.run_until_parked();
20674 cx.update_editor(|editor, window, cx| {
20675 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20676 });
20677 executor.run_until_parked();
20678
20679 cx.assert_state_with_diff(
20680 r#"
20681 use some::mod1;
20682 use some::mod2;
20683
20684 const A: u32 = 42;
20685 const B: u32 = 42;
20686 - const C: u32 = 42;
20687 + const C: u32 = 43ˇ
20688 const D: u32 = 42;
20689
20690
20691 fn main() {
20692 println!("hello");
20693
20694 println!("world");
20695 }"#
20696 .unindent(),
20697 );
20698
20699 cx.update_editor(|editor, window, cx| {
20700 editor.handle_input("\nnew_line\n", window, cx);
20701 });
20702 executor.run_until_parked();
20703
20704 cx.assert_state_with_diff(
20705 r#"
20706 use some::mod1;
20707 use some::mod2;
20708
20709 const A: u32 = 42;
20710 const B: u32 = 42;
20711 - const C: u32 = 42;
20712 + const C: u32 = 43
20713 + new_line
20714 + ˇ
20715 const D: u32 = 42;
20716
20717
20718 fn main() {
20719 println!("hello");
20720
20721 println!("world");
20722 }"#
20723 .unindent(),
20724 );
20725}
20726
20727#[gpui::test]
20728async fn test_stage_and_unstage_added_file_hunk(
20729 executor: BackgroundExecutor,
20730 cx: &mut TestAppContext,
20731) {
20732 init_test(cx, |_| {});
20733
20734 let mut cx = EditorTestContext::new(cx).await;
20735 cx.update_editor(|editor, _, cx| {
20736 editor.set_expand_all_diff_hunks(cx);
20737 });
20738
20739 let working_copy = r#"
20740 ˇfn main() {
20741 println!("hello, world!");
20742 }
20743 "#
20744 .unindent();
20745
20746 cx.set_state(&working_copy);
20747 executor.run_until_parked();
20748
20749 cx.assert_state_with_diff(
20750 r#"
20751 + ˇfn main() {
20752 + println!("hello, world!");
20753 + }
20754 "#
20755 .unindent(),
20756 );
20757 cx.assert_index_text(None);
20758
20759 cx.update_editor(|editor, window, cx| {
20760 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20761 });
20762 executor.run_until_parked();
20763 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20764 cx.assert_state_with_diff(
20765 r#"
20766 + ˇfn main() {
20767 + println!("hello, world!");
20768 + }
20769 "#
20770 .unindent(),
20771 );
20772
20773 cx.update_editor(|editor, window, cx| {
20774 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20775 });
20776 executor.run_until_parked();
20777 cx.assert_index_text(None);
20778}
20779
20780async fn setup_indent_guides_editor(
20781 text: &str,
20782 cx: &mut TestAppContext,
20783) -> (BufferId, EditorTestContext) {
20784 init_test(cx, |_| {});
20785
20786 let mut cx = EditorTestContext::new(cx).await;
20787
20788 let buffer_id = cx.update_editor(|editor, window, cx| {
20789 editor.set_text(text, window, cx);
20790 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20791
20792 buffer_ids[0]
20793 });
20794
20795 (buffer_id, cx)
20796}
20797
20798fn assert_indent_guides(
20799 range: Range<u32>,
20800 expected: Vec<IndentGuide>,
20801 active_indices: Option<Vec<usize>>,
20802 cx: &mut EditorTestContext,
20803) {
20804 let indent_guides = cx.update_editor(|editor, window, cx| {
20805 let snapshot = editor.snapshot(window, cx).display_snapshot;
20806 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20807 editor,
20808 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20809 true,
20810 &snapshot,
20811 cx,
20812 );
20813
20814 indent_guides.sort_by(|a, b| {
20815 a.depth.cmp(&b.depth).then(
20816 a.start_row
20817 .cmp(&b.start_row)
20818 .then(a.end_row.cmp(&b.end_row)),
20819 )
20820 });
20821 indent_guides
20822 });
20823
20824 if let Some(expected) = active_indices {
20825 let active_indices = cx.update_editor(|editor, window, cx| {
20826 let snapshot = editor.snapshot(window, cx).display_snapshot;
20827 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20828 });
20829
20830 assert_eq!(
20831 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20832 expected,
20833 "Active indent guide indices do not match"
20834 );
20835 }
20836
20837 assert_eq!(indent_guides, expected, "Indent guides do not match");
20838}
20839
20840fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20841 IndentGuide {
20842 buffer_id,
20843 start_row: MultiBufferRow(start_row),
20844 end_row: MultiBufferRow(end_row),
20845 depth,
20846 tab_size: 4,
20847 settings: IndentGuideSettings {
20848 enabled: true,
20849 line_width: 1,
20850 active_line_width: 1,
20851 coloring: IndentGuideColoring::default(),
20852 background_coloring: IndentGuideBackgroundColoring::default(),
20853 },
20854 }
20855}
20856
20857#[gpui::test]
20858async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20859 let (buffer_id, mut cx) = setup_indent_guides_editor(
20860 &"
20861 fn main() {
20862 let a = 1;
20863 }"
20864 .unindent(),
20865 cx,
20866 )
20867 .await;
20868
20869 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20870}
20871
20872#[gpui::test]
20873async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20874 let (buffer_id, mut cx) = setup_indent_guides_editor(
20875 &"
20876 fn main() {
20877 let a = 1;
20878 let b = 2;
20879 }"
20880 .unindent(),
20881 cx,
20882 )
20883 .await;
20884
20885 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20886}
20887
20888#[gpui::test]
20889async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20890 let (buffer_id, mut cx) = setup_indent_guides_editor(
20891 &"
20892 fn main() {
20893 let a = 1;
20894 if a == 3 {
20895 let b = 2;
20896 } else {
20897 let c = 3;
20898 }
20899 }"
20900 .unindent(),
20901 cx,
20902 )
20903 .await;
20904
20905 assert_indent_guides(
20906 0..8,
20907 vec![
20908 indent_guide(buffer_id, 1, 6, 0),
20909 indent_guide(buffer_id, 3, 3, 1),
20910 indent_guide(buffer_id, 5, 5, 1),
20911 ],
20912 None,
20913 &mut cx,
20914 );
20915}
20916
20917#[gpui::test]
20918async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20919 let (buffer_id, mut cx) = setup_indent_guides_editor(
20920 &"
20921 fn main() {
20922 let a = 1;
20923 let b = 2;
20924 let c = 3;
20925 }"
20926 .unindent(),
20927 cx,
20928 )
20929 .await;
20930
20931 assert_indent_guides(
20932 0..5,
20933 vec![
20934 indent_guide(buffer_id, 1, 3, 0),
20935 indent_guide(buffer_id, 2, 2, 1),
20936 ],
20937 None,
20938 &mut cx,
20939 );
20940}
20941
20942#[gpui::test]
20943async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20944 let (buffer_id, mut cx) = setup_indent_guides_editor(
20945 &"
20946 fn main() {
20947 let a = 1;
20948
20949 let c = 3;
20950 }"
20951 .unindent(),
20952 cx,
20953 )
20954 .await;
20955
20956 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20957}
20958
20959#[gpui::test]
20960async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20961 let (buffer_id, mut cx) = setup_indent_guides_editor(
20962 &"
20963 fn main() {
20964 let a = 1;
20965
20966 let c = 3;
20967
20968 if a == 3 {
20969 let b = 2;
20970 } else {
20971 let c = 3;
20972 }
20973 }"
20974 .unindent(),
20975 cx,
20976 )
20977 .await;
20978
20979 assert_indent_guides(
20980 0..11,
20981 vec![
20982 indent_guide(buffer_id, 1, 9, 0),
20983 indent_guide(buffer_id, 6, 6, 1),
20984 indent_guide(buffer_id, 8, 8, 1),
20985 ],
20986 None,
20987 &mut cx,
20988 );
20989}
20990
20991#[gpui::test]
20992async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20993 let (buffer_id, mut cx) = setup_indent_guides_editor(
20994 &"
20995 fn main() {
20996 let a = 1;
20997
20998 let c = 3;
20999
21000 if a == 3 {
21001 let b = 2;
21002 } else {
21003 let c = 3;
21004 }
21005 }"
21006 .unindent(),
21007 cx,
21008 )
21009 .await;
21010
21011 assert_indent_guides(
21012 1..11,
21013 vec![
21014 indent_guide(buffer_id, 1, 9, 0),
21015 indent_guide(buffer_id, 6, 6, 1),
21016 indent_guide(buffer_id, 8, 8, 1),
21017 ],
21018 None,
21019 &mut cx,
21020 );
21021}
21022
21023#[gpui::test]
21024async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21025 let (buffer_id, mut cx) = setup_indent_guides_editor(
21026 &"
21027 fn main() {
21028 let a = 1;
21029
21030 let c = 3;
21031
21032 if a == 3 {
21033 let b = 2;
21034 } else {
21035 let c = 3;
21036 }
21037 }"
21038 .unindent(),
21039 cx,
21040 )
21041 .await;
21042
21043 assert_indent_guides(
21044 1..10,
21045 vec![
21046 indent_guide(buffer_id, 1, 9, 0),
21047 indent_guide(buffer_id, 6, 6, 1),
21048 indent_guide(buffer_id, 8, 8, 1),
21049 ],
21050 None,
21051 &mut cx,
21052 );
21053}
21054
21055#[gpui::test]
21056async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21057 let (buffer_id, mut cx) = setup_indent_guides_editor(
21058 &"
21059 fn main() {
21060 if a {
21061 b(
21062 c,
21063 d,
21064 )
21065 } else {
21066 e(
21067 f
21068 )
21069 }
21070 }"
21071 .unindent(),
21072 cx,
21073 )
21074 .await;
21075
21076 assert_indent_guides(
21077 0..11,
21078 vec![
21079 indent_guide(buffer_id, 1, 10, 0),
21080 indent_guide(buffer_id, 2, 5, 1),
21081 indent_guide(buffer_id, 7, 9, 1),
21082 indent_guide(buffer_id, 3, 4, 2),
21083 indent_guide(buffer_id, 8, 8, 2),
21084 ],
21085 None,
21086 &mut cx,
21087 );
21088
21089 cx.update_editor(|editor, window, cx| {
21090 editor.fold_at(MultiBufferRow(2), window, cx);
21091 assert_eq!(
21092 editor.display_text(cx),
21093 "
21094 fn main() {
21095 if a {
21096 b(⋯
21097 )
21098 } else {
21099 e(
21100 f
21101 )
21102 }
21103 }"
21104 .unindent()
21105 );
21106 });
21107
21108 assert_indent_guides(
21109 0..11,
21110 vec![
21111 indent_guide(buffer_id, 1, 10, 0),
21112 indent_guide(buffer_id, 2, 5, 1),
21113 indent_guide(buffer_id, 7, 9, 1),
21114 indent_guide(buffer_id, 8, 8, 2),
21115 ],
21116 None,
21117 &mut cx,
21118 );
21119}
21120
21121#[gpui::test]
21122async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21123 let (buffer_id, mut cx) = setup_indent_guides_editor(
21124 &"
21125 block1
21126 block2
21127 block3
21128 block4
21129 block2
21130 block1
21131 block1"
21132 .unindent(),
21133 cx,
21134 )
21135 .await;
21136
21137 assert_indent_guides(
21138 1..10,
21139 vec![
21140 indent_guide(buffer_id, 1, 4, 0),
21141 indent_guide(buffer_id, 2, 3, 1),
21142 indent_guide(buffer_id, 3, 3, 2),
21143 ],
21144 None,
21145 &mut cx,
21146 );
21147}
21148
21149#[gpui::test]
21150async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21151 let (buffer_id, mut cx) = setup_indent_guides_editor(
21152 &"
21153 block1
21154 block2
21155 block3
21156
21157 block1
21158 block1"
21159 .unindent(),
21160 cx,
21161 )
21162 .await;
21163
21164 assert_indent_guides(
21165 0..6,
21166 vec![
21167 indent_guide(buffer_id, 1, 2, 0),
21168 indent_guide(buffer_id, 2, 2, 1),
21169 ],
21170 None,
21171 &mut cx,
21172 );
21173}
21174
21175#[gpui::test]
21176async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21177 let (buffer_id, mut cx) = setup_indent_guides_editor(
21178 &"
21179 function component() {
21180 \treturn (
21181 \t\t\t
21182 \t\t<div>
21183 \t\t\t<abc></abc>
21184 \t\t</div>
21185 \t)
21186 }"
21187 .unindent(),
21188 cx,
21189 )
21190 .await;
21191
21192 assert_indent_guides(
21193 0..8,
21194 vec![
21195 indent_guide(buffer_id, 1, 6, 0),
21196 indent_guide(buffer_id, 2, 5, 1),
21197 indent_guide(buffer_id, 4, 4, 2),
21198 ],
21199 None,
21200 &mut cx,
21201 );
21202}
21203
21204#[gpui::test]
21205async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21206 let (buffer_id, mut cx) = setup_indent_guides_editor(
21207 &"
21208 function component() {
21209 \treturn (
21210 \t
21211 \t\t<div>
21212 \t\t\t<abc></abc>
21213 \t\t</div>
21214 \t)
21215 }"
21216 .unindent(),
21217 cx,
21218 )
21219 .await;
21220
21221 assert_indent_guides(
21222 0..8,
21223 vec![
21224 indent_guide(buffer_id, 1, 6, 0),
21225 indent_guide(buffer_id, 2, 5, 1),
21226 indent_guide(buffer_id, 4, 4, 2),
21227 ],
21228 None,
21229 &mut cx,
21230 );
21231}
21232
21233#[gpui::test]
21234async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21235 let (buffer_id, mut cx) = setup_indent_guides_editor(
21236 &"
21237 block1
21238
21239
21240
21241 block2
21242 "
21243 .unindent(),
21244 cx,
21245 )
21246 .await;
21247
21248 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21249}
21250
21251#[gpui::test]
21252async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21253 let (buffer_id, mut cx) = setup_indent_guides_editor(
21254 &"
21255 def a:
21256 \tb = 3
21257 \tif True:
21258 \t\tc = 4
21259 \t\td = 5
21260 \tprint(b)
21261 "
21262 .unindent(),
21263 cx,
21264 )
21265 .await;
21266
21267 assert_indent_guides(
21268 0..6,
21269 vec![
21270 indent_guide(buffer_id, 1, 5, 0),
21271 indent_guide(buffer_id, 3, 4, 1),
21272 ],
21273 None,
21274 &mut cx,
21275 );
21276}
21277
21278#[gpui::test]
21279async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21280 let (buffer_id, mut cx) = setup_indent_guides_editor(
21281 &"
21282 fn main() {
21283 let a = 1;
21284 }"
21285 .unindent(),
21286 cx,
21287 )
21288 .await;
21289
21290 cx.update_editor(|editor, window, cx| {
21291 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21292 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21293 });
21294 });
21295
21296 assert_indent_guides(
21297 0..3,
21298 vec![indent_guide(buffer_id, 1, 1, 0)],
21299 Some(vec![0]),
21300 &mut cx,
21301 );
21302}
21303
21304#[gpui::test]
21305async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21306 let (buffer_id, mut cx) = setup_indent_guides_editor(
21307 &"
21308 fn main() {
21309 if 1 == 2 {
21310 let a = 1;
21311 }
21312 }"
21313 .unindent(),
21314 cx,
21315 )
21316 .await;
21317
21318 cx.update_editor(|editor, window, cx| {
21319 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21320 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21321 });
21322 });
21323
21324 assert_indent_guides(
21325 0..4,
21326 vec![
21327 indent_guide(buffer_id, 1, 3, 0),
21328 indent_guide(buffer_id, 2, 2, 1),
21329 ],
21330 Some(vec![1]),
21331 &mut cx,
21332 );
21333
21334 cx.update_editor(|editor, window, cx| {
21335 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21336 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21337 });
21338 });
21339
21340 assert_indent_guides(
21341 0..4,
21342 vec![
21343 indent_guide(buffer_id, 1, 3, 0),
21344 indent_guide(buffer_id, 2, 2, 1),
21345 ],
21346 Some(vec![1]),
21347 &mut cx,
21348 );
21349
21350 cx.update_editor(|editor, window, cx| {
21351 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21352 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21353 });
21354 });
21355
21356 assert_indent_guides(
21357 0..4,
21358 vec![
21359 indent_guide(buffer_id, 1, 3, 0),
21360 indent_guide(buffer_id, 2, 2, 1),
21361 ],
21362 Some(vec![0]),
21363 &mut cx,
21364 );
21365}
21366
21367#[gpui::test]
21368async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21369 let (buffer_id, mut cx) = setup_indent_guides_editor(
21370 &"
21371 fn main() {
21372 let a = 1;
21373
21374 let b = 2;
21375 }"
21376 .unindent(),
21377 cx,
21378 )
21379 .await;
21380
21381 cx.update_editor(|editor, window, cx| {
21382 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21383 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21384 });
21385 });
21386
21387 assert_indent_guides(
21388 0..5,
21389 vec![indent_guide(buffer_id, 1, 3, 0)],
21390 Some(vec![0]),
21391 &mut cx,
21392 );
21393}
21394
21395#[gpui::test]
21396async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21397 let (buffer_id, mut cx) = setup_indent_guides_editor(
21398 &"
21399 def m:
21400 a = 1
21401 pass"
21402 .unindent(),
21403 cx,
21404 )
21405 .await;
21406
21407 cx.update_editor(|editor, window, cx| {
21408 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21409 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21410 });
21411 });
21412
21413 assert_indent_guides(
21414 0..3,
21415 vec![indent_guide(buffer_id, 1, 2, 0)],
21416 Some(vec![0]),
21417 &mut cx,
21418 );
21419}
21420
21421#[gpui::test]
21422async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21423 init_test(cx, |_| {});
21424 let mut cx = EditorTestContext::new(cx).await;
21425 let text = indoc! {
21426 "
21427 impl A {
21428 fn b() {
21429 0;
21430 3;
21431 5;
21432 6;
21433 7;
21434 }
21435 }
21436 "
21437 };
21438 let base_text = indoc! {
21439 "
21440 impl A {
21441 fn b() {
21442 0;
21443 1;
21444 2;
21445 3;
21446 4;
21447 }
21448 fn c() {
21449 5;
21450 6;
21451 7;
21452 }
21453 }
21454 "
21455 };
21456
21457 cx.update_editor(|editor, window, cx| {
21458 editor.set_text(text, window, cx);
21459
21460 editor.buffer().update(cx, |multibuffer, cx| {
21461 let buffer = multibuffer.as_singleton().unwrap();
21462 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21463
21464 multibuffer.set_all_diff_hunks_expanded(cx);
21465 multibuffer.add_diff(diff, cx);
21466
21467 buffer.read(cx).remote_id()
21468 })
21469 });
21470 cx.run_until_parked();
21471
21472 cx.assert_state_with_diff(
21473 indoc! { "
21474 impl A {
21475 fn b() {
21476 0;
21477 - 1;
21478 - 2;
21479 3;
21480 - 4;
21481 - }
21482 - fn c() {
21483 5;
21484 6;
21485 7;
21486 }
21487 }
21488 ˇ"
21489 }
21490 .to_string(),
21491 );
21492
21493 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21494 editor
21495 .snapshot(window, cx)
21496 .buffer_snapshot()
21497 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21498 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21499 .collect::<Vec<_>>()
21500 });
21501 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21502 assert_eq!(
21503 actual_guides,
21504 vec![
21505 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21506 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21507 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21508 ]
21509 );
21510}
21511
21512#[gpui::test]
21513async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21514 init_test(cx, |_| {});
21515 let mut cx = EditorTestContext::new(cx).await;
21516
21517 let diff_base = r#"
21518 a
21519 b
21520 c
21521 "#
21522 .unindent();
21523
21524 cx.set_state(
21525 &r#"
21526 ˇA
21527 b
21528 C
21529 "#
21530 .unindent(),
21531 );
21532 cx.set_head_text(&diff_base);
21533 cx.update_editor(|editor, window, cx| {
21534 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21535 });
21536 executor.run_until_parked();
21537
21538 let both_hunks_expanded = r#"
21539 - a
21540 + ˇA
21541 b
21542 - c
21543 + C
21544 "#
21545 .unindent();
21546
21547 cx.assert_state_with_diff(both_hunks_expanded.clone());
21548
21549 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21550 let snapshot = editor.snapshot(window, cx);
21551 let hunks = editor
21552 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21553 .collect::<Vec<_>>();
21554 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21555 hunks
21556 .into_iter()
21557 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21558 .collect::<Vec<_>>()
21559 });
21560 assert_eq!(hunk_ranges.len(), 2);
21561
21562 cx.update_editor(|editor, _, cx| {
21563 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21564 });
21565 executor.run_until_parked();
21566
21567 let second_hunk_expanded = r#"
21568 ˇA
21569 b
21570 - c
21571 + C
21572 "#
21573 .unindent();
21574
21575 cx.assert_state_with_diff(second_hunk_expanded);
21576
21577 cx.update_editor(|editor, _, cx| {
21578 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21579 });
21580 executor.run_until_parked();
21581
21582 cx.assert_state_with_diff(both_hunks_expanded.clone());
21583
21584 cx.update_editor(|editor, _, cx| {
21585 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21586 });
21587 executor.run_until_parked();
21588
21589 let first_hunk_expanded = r#"
21590 - a
21591 + ˇA
21592 b
21593 C
21594 "#
21595 .unindent();
21596
21597 cx.assert_state_with_diff(first_hunk_expanded);
21598
21599 cx.update_editor(|editor, _, cx| {
21600 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21601 });
21602 executor.run_until_parked();
21603
21604 cx.assert_state_with_diff(both_hunks_expanded);
21605
21606 cx.set_state(
21607 &r#"
21608 ˇA
21609 b
21610 "#
21611 .unindent(),
21612 );
21613 cx.run_until_parked();
21614
21615 // TODO this cursor position seems bad
21616 cx.assert_state_with_diff(
21617 r#"
21618 - ˇa
21619 + A
21620 b
21621 "#
21622 .unindent(),
21623 );
21624
21625 cx.update_editor(|editor, window, cx| {
21626 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21627 });
21628
21629 cx.assert_state_with_diff(
21630 r#"
21631 - ˇa
21632 + A
21633 b
21634 - c
21635 "#
21636 .unindent(),
21637 );
21638
21639 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21640 let snapshot = editor.snapshot(window, cx);
21641 let hunks = editor
21642 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21643 .collect::<Vec<_>>();
21644 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21645 hunks
21646 .into_iter()
21647 .map(|hunk| Anchor::range_in_buffer(excerpt_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 hunks
21711 .into_iter()
21712 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21713 .collect::<Vec<_>>()
21714 });
21715 assert_eq!(hunk_ranges.len(), 1);
21716
21717 cx.update_editor(|editor, _, cx| {
21718 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21719 });
21720 executor.run_until_parked();
21721
21722 let hunk_collapsed = r#"
21723 ˇb
21724 c
21725 "#
21726 .unindent();
21727
21728 cx.assert_state_with_diff(hunk_collapsed);
21729
21730 cx.update_editor(|editor, _, cx| {
21731 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21732 });
21733 executor.run_until_parked();
21734
21735 cx.assert_state_with_diff(hunk_expanded);
21736}
21737
21738#[gpui::test]
21739async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21740 init_test(cx, |_| {});
21741
21742 let fs = FakeFs::new(cx.executor());
21743 fs.insert_tree(
21744 path!("/test"),
21745 json!({
21746 ".git": {},
21747 "file-1": "ONE\n",
21748 "file-2": "TWO\n",
21749 "file-3": "THREE\n",
21750 }),
21751 )
21752 .await;
21753
21754 fs.set_head_for_repo(
21755 path!("/test/.git").as_ref(),
21756 &[
21757 ("file-1", "one\n".into()),
21758 ("file-2", "two\n".into()),
21759 ("file-3", "three\n".into()),
21760 ],
21761 "deadbeef",
21762 );
21763
21764 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21765 let mut buffers = vec![];
21766 for i in 1..=3 {
21767 let buffer = project
21768 .update(cx, |project, cx| {
21769 let path = format!(path!("/test/file-{}"), i);
21770 project.open_local_buffer(path, cx)
21771 })
21772 .await
21773 .unwrap();
21774 buffers.push(buffer);
21775 }
21776
21777 let multibuffer = cx.new(|cx| {
21778 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21779 multibuffer.set_all_diff_hunks_expanded(cx);
21780 for buffer in &buffers {
21781 let snapshot = buffer.read(cx).snapshot();
21782 multibuffer.set_excerpts_for_path(
21783 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21784 buffer.clone(),
21785 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21786 2,
21787 cx,
21788 );
21789 }
21790 multibuffer
21791 });
21792
21793 let editor = cx.add_window(|window, cx| {
21794 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21795 });
21796 cx.run_until_parked();
21797
21798 let snapshot = editor
21799 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21800 .unwrap();
21801 let hunks = snapshot
21802 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21803 .map(|hunk| match hunk {
21804 DisplayDiffHunk::Unfolded {
21805 display_row_range, ..
21806 } => display_row_range,
21807 DisplayDiffHunk::Folded { .. } => unreachable!(),
21808 })
21809 .collect::<Vec<_>>();
21810 assert_eq!(
21811 hunks,
21812 [
21813 DisplayRow(2)..DisplayRow(4),
21814 DisplayRow(7)..DisplayRow(9),
21815 DisplayRow(12)..DisplayRow(14),
21816 ]
21817 );
21818}
21819
21820#[gpui::test]
21821async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21822 init_test(cx, |_| {});
21823
21824 let mut cx = EditorTestContext::new(cx).await;
21825 cx.set_head_text(indoc! { "
21826 one
21827 two
21828 three
21829 four
21830 five
21831 "
21832 });
21833 cx.set_index_text(indoc! { "
21834 one
21835 two
21836 three
21837 four
21838 five
21839 "
21840 });
21841 cx.set_state(indoc! {"
21842 one
21843 TWO
21844 ˇTHREE
21845 FOUR
21846 five
21847 "});
21848 cx.run_until_parked();
21849 cx.update_editor(|editor, window, cx| {
21850 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21851 });
21852 cx.run_until_parked();
21853 cx.assert_index_text(Some(indoc! {"
21854 one
21855 TWO
21856 THREE
21857 FOUR
21858 five
21859 "}));
21860 cx.set_state(indoc! { "
21861 one
21862 TWO
21863 ˇTHREE-HUNDRED
21864 FOUR
21865 five
21866 "});
21867 cx.run_until_parked();
21868 cx.update_editor(|editor, window, cx| {
21869 let snapshot = editor.snapshot(window, cx);
21870 let hunks = editor
21871 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21872 .collect::<Vec<_>>();
21873 assert_eq!(hunks.len(), 1);
21874 assert_eq!(
21875 hunks[0].status(),
21876 DiffHunkStatus {
21877 kind: DiffHunkStatusKind::Modified,
21878 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21879 }
21880 );
21881
21882 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21883 });
21884 cx.run_until_parked();
21885 cx.assert_index_text(Some(indoc! {"
21886 one
21887 TWO
21888 THREE-HUNDRED
21889 FOUR
21890 five
21891 "}));
21892}
21893
21894#[gpui::test]
21895fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21896 init_test(cx, |_| {});
21897
21898 let editor = cx.add_window(|window, cx| {
21899 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21900 build_editor(buffer, window, cx)
21901 });
21902
21903 let render_args = Arc::new(Mutex::new(None));
21904 let snapshot = editor
21905 .update(cx, |editor, window, cx| {
21906 let snapshot = editor.buffer().read(cx).snapshot(cx);
21907 let range =
21908 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21909
21910 struct RenderArgs {
21911 row: MultiBufferRow,
21912 folded: bool,
21913 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21914 }
21915
21916 let crease = Crease::inline(
21917 range,
21918 FoldPlaceholder::test(),
21919 {
21920 let toggle_callback = render_args.clone();
21921 move |row, folded, callback, _window, _cx| {
21922 *toggle_callback.lock() = Some(RenderArgs {
21923 row,
21924 folded,
21925 callback,
21926 });
21927 div()
21928 }
21929 },
21930 |_row, _folded, _window, _cx| div(),
21931 );
21932
21933 editor.insert_creases(Some(crease), cx);
21934 let snapshot = editor.snapshot(window, cx);
21935 let _div =
21936 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21937 snapshot
21938 })
21939 .unwrap();
21940
21941 let render_args = render_args.lock().take().unwrap();
21942 assert_eq!(render_args.row, MultiBufferRow(1));
21943 assert!(!render_args.folded);
21944 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21945
21946 cx.update_window(*editor, |_, window, cx| {
21947 (render_args.callback)(true, window, cx)
21948 })
21949 .unwrap();
21950 let snapshot = editor
21951 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21952 .unwrap();
21953 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21954
21955 cx.update_window(*editor, |_, window, cx| {
21956 (render_args.callback)(false, window, cx)
21957 })
21958 .unwrap();
21959 let snapshot = editor
21960 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21961 .unwrap();
21962 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21963}
21964
21965#[gpui::test]
21966async fn test_input_text(cx: &mut TestAppContext) {
21967 init_test(cx, |_| {});
21968 let mut cx = EditorTestContext::new(cx).await;
21969
21970 cx.set_state(
21971 &r#"ˇone
21972 two
21973
21974 three
21975 fourˇ
21976 five
21977
21978 siˇx"#
21979 .unindent(),
21980 );
21981
21982 cx.dispatch_action(HandleInput(String::new()));
21983 cx.assert_editor_state(
21984 &r#"ˇone
21985 two
21986
21987 three
21988 fourˇ
21989 five
21990
21991 siˇx"#
21992 .unindent(),
21993 );
21994
21995 cx.dispatch_action(HandleInput("AAAA".to_string()));
21996 cx.assert_editor_state(
21997 &r#"AAAAˇone
21998 two
21999
22000 three
22001 fourAAAAˇ
22002 five
22003
22004 siAAAAˇx"#
22005 .unindent(),
22006 );
22007}
22008
22009#[gpui::test]
22010async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22011 init_test(cx, |_| {});
22012
22013 let mut cx = EditorTestContext::new(cx).await;
22014 cx.set_state(
22015 r#"let foo = 1;
22016let foo = 2;
22017let foo = 3;
22018let fooˇ = 4;
22019let foo = 5;
22020let foo = 6;
22021let foo = 7;
22022let foo = 8;
22023let foo = 9;
22024let foo = 10;
22025let foo = 11;
22026let foo = 12;
22027let foo = 13;
22028let foo = 14;
22029let foo = 15;"#,
22030 );
22031
22032 cx.update_editor(|e, window, cx| {
22033 assert_eq!(
22034 e.next_scroll_position,
22035 NextScrollCursorCenterTopBottom::Center,
22036 "Default next scroll direction is center",
22037 );
22038
22039 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22040 assert_eq!(
22041 e.next_scroll_position,
22042 NextScrollCursorCenterTopBottom::Top,
22043 "After center, next scroll direction should be top",
22044 );
22045
22046 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22047 assert_eq!(
22048 e.next_scroll_position,
22049 NextScrollCursorCenterTopBottom::Bottom,
22050 "After top, next scroll direction should be bottom",
22051 );
22052
22053 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22054 assert_eq!(
22055 e.next_scroll_position,
22056 NextScrollCursorCenterTopBottom::Center,
22057 "After bottom, scrolling should start over",
22058 );
22059
22060 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22061 assert_eq!(
22062 e.next_scroll_position,
22063 NextScrollCursorCenterTopBottom::Top,
22064 "Scrolling continues if retriggered fast enough"
22065 );
22066 });
22067
22068 cx.executor()
22069 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22070 cx.executor().run_until_parked();
22071 cx.update_editor(|e, _, _| {
22072 assert_eq!(
22073 e.next_scroll_position,
22074 NextScrollCursorCenterTopBottom::Center,
22075 "If scrolling is not triggered fast enough, it should reset"
22076 );
22077 });
22078}
22079
22080#[gpui::test]
22081async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22082 init_test(cx, |_| {});
22083 let mut cx = EditorLspTestContext::new_rust(
22084 lsp::ServerCapabilities {
22085 definition_provider: Some(lsp::OneOf::Left(true)),
22086 references_provider: Some(lsp::OneOf::Left(true)),
22087 ..lsp::ServerCapabilities::default()
22088 },
22089 cx,
22090 )
22091 .await;
22092
22093 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22094 let go_to_definition = cx
22095 .lsp
22096 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22097 move |params, _| async move {
22098 if empty_go_to_definition {
22099 Ok(None)
22100 } else {
22101 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22102 uri: params.text_document_position_params.text_document.uri,
22103 range: lsp::Range::new(
22104 lsp::Position::new(4, 3),
22105 lsp::Position::new(4, 6),
22106 ),
22107 })))
22108 }
22109 },
22110 );
22111 let references = cx
22112 .lsp
22113 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22114 Ok(Some(vec![lsp::Location {
22115 uri: params.text_document_position.text_document.uri,
22116 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22117 }]))
22118 });
22119 (go_to_definition, references)
22120 };
22121
22122 cx.set_state(
22123 &r#"fn one() {
22124 let mut a = ˇtwo();
22125 }
22126
22127 fn two() {}"#
22128 .unindent(),
22129 );
22130 set_up_lsp_handlers(false, &mut cx);
22131 let navigated = cx
22132 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22133 .await
22134 .expect("Failed to navigate to definition");
22135 assert_eq!(
22136 navigated,
22137 Navigated::Yes,
22138 "Should have navigated to definition from the GetDefinition response"
22139 );
22140 cx.assert_editor_state(
22141 &r#"fn one() {
22142 let mut a = two();
22143 }
22144
22145 fn «twoˇ»() {}"#
22146 .unindent(),
22147 );
22148
22149 let editors = cx.update_workspace(|workspace, _, cx| {
22150 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22151 });
22152 cx.update_editor(|_, _, test_editor_cx| {
22153 assert_eq!(
22154 editors.len(),
22155 1,
22156 "Initially, only one, test, editor should be open in the workspace"
22157 );
22158 assert_eq!(
22159 test_editor_cx.entity(),
22160 editors.last().expect("Asserted len is 1").clone()
22161 );
22162 });
22163
22164 set_up_lsp_handlers(true, &mut cx);
22165 let navigated = cx
22166 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22167 .await
22168 .expect("Failed to navigate to lookup references");
22169 assert_eq!(
22170 navigated,
22171 Navigated::Yes,
22172 "Should have navigated to references as a fallback after empty GoToDefinition response"
22173 );
22174 // We should not change the selections in the existing file,
22175 // if opening another milti buffer with the references
22176 cx.assert_editor_state(
22177 &r#"fn one() {
22178 let mut a = two();
22179 }
22180
22181 fn «twoˇ»() {}"#
22182 .unindent(),
22183 );
22184 let editors = cx.update_workspace(|workspace, _, cx| {
22185 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22186 });
22187 cx.update_editor(|_, _, test_editor_cx| {
22188 assert_eq!(
22189 editors.len(),
22190 2,
22191 "After falling back to references search, we open a new editor with the results"
22192 );
22193 let references_fallback_text = editors
22194 .into_iter()
22195 .find(|new_editor| *new_editor != test_editor_cx.entity())
22196 .expect("Should have one non-test editor now")
22197 .read(test_editor_cx)
22198 .text(test_editor_cx);
22199 assert_eq!(
22200 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22201 "Should use the range from the references response and not the GoToDefinition one"
22202 );
22203 });
22204}
22205
22206#[gpui::test]
22207async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22208 init_test(cx, |_| {});
22209 cx.update(|cx| {
22210 let mut editor_settings = EditorSettings::get_global(cx).clone();
22211 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22212 EditorSettings::override_global(editor_settings, cx);
22213 });
22214 let mut cx = EditorLspTestContext::new_rust(
22215 lsp::ServerCapabilities {
22216 definition_provider: Some(lsp::OneOf::Left(true)),
22217 references_provider: Some(lsp::OneOf::Left(true)),
22218 ..lsp::ServerCapabilities::default()
22219 },
22220 cx,
22221 )
22222 .await;
22223 let original_state = r#"fn one() {
22224 let mut a = ˇtwo();
22225 }
22226
22227 fn two() {}"#
22228 .unindent();
22229 cx.set_state(&original_state);
22230
22231 let mut go_to_definition = cx
22232 .lsp
22233 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22234 move |_, _| async move { Ok(None) },
22235 );
22236 let _references = cx
22237 .lsp
22238 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22239 panic!("Should not call for references with no go to definition fallback")
22240 });
22241
22242 let navigated = cx
22243 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22244 .await
22245 .expect("Failed to navigate to lookup references");
22246 go_to_definition
22247 .next()
22248 .await
22249 .expect("Should have called the go_to_definition handler");
22250
22251 assert_eq!(
22252 navigated,
22253 Navigated::No,
22254 "Should have navigated to references as a fallback after empty GoToDefinition response"
22255 );
22256 cx.assert_editor_state(&original_state);
22257 let editors = cx.update_workspace(|workspace, _, cx| {
22258 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22259 });
22260 cx.update_editor(|_, _, _| {
22261 assert_eq!(
22262 editors.len(),
22263 1,
22264 "After unsuccessful fallback, no other editor should have been opened"
22265 );
22266 });
22267}
22268
22269#[gpui::test]
22270async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22271 init_test(cx, |_| {});
22272 let mut cx = EditorLspTestContext::new_rust(
22273 lsp::ServerCapabilities {
22274 references_provider: Some(lsp::OneOf::Left(true)),
22275 ..lsp::ServerCapabilities::default()
22276 },
22277 cx,
22278 )
22279 .await;
22280
22281 cx.set_state(
22282 &r#"
22283 fn one() {
22284 let mut a = two();
22285 }
22286
22287 fn ˇtwo() {}"#
22288 .unindent(),
22289 );
22290 cx.lsp
22291 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22292 Ok(Some(vec![
22293 lsp::Location {
22294 uri: params.text_document_position.text_document.uri.clone(),
22295 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22296 },
22297 lsp::Location {
22298 uri: params.text_document_position.text_document.uri,
22299 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22300 },
22301 ]))
22302 });
22303 let navigated = cx
22304 .update_editor(|editor, window, cx| {
22305 editor.find_all_references(&FindAllReferences, window, cx)
22306 })
22307 .unwrap()
22308 .await
22309 .expect("Failed to navigate to references");
22310 assert_eq!(
22311 navigated,
22312 Navigated::Yes,
22313 "Should have navigated to references from the FindAllReferences response"
22314 );
22315 cx.assert_editor_state(
22316 &r#"fn one() {
22317 let mut a = two();
22318 }
22319
22320 fn ˇtwo() {}"#
22321 .unindent(),
22322 );
22323
22324 let editors = cx.update_workspace(|workspace, _, cx| {
22325 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22326 });
22327 cx.update_editor(|_, _, _| {
22328 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22329 });
22330
22331 cx.set_state(
22332 &r#"fn one() {
22333 let mut a = ˇtwo();
22334 }
22335
22336 fn two() {}"#
22337 .unindent(),
22338 );
22339 let navigated = cx
22340 .update_editor(|editor, window, cx| {
22341 editor.find_all_references(&FindAllReferences, window, cx)
22342 })
22343 .unwrap()
22344 .await
22345 .expect("Failed to navigate to references");
22346 assert_eq!(
22347 navigated,
22348 Navigated::Yes,
22349 "Should have navigated to references from the FindAllReferences response"
22350 );
22351 cx.assert_editor_state(
22352 &r#"fn one() {
22353 let mut a = ˇtwo();
22354 }
22355
22356 fn two() {}"#
22357 .unindent(),
22358 );
22359 let editors = cx.update_workspace(|workspace, _, cx| {
22360 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22361 });
22362 cx.update_editor(|_, _, _| {
22363 assert_eq!(
22364 editors.len(),
22365 2,
22366 "should have re-used the previous multibuffer"
22367 );
22368 });
22369
22370 cx.set_state(
22371 &r#"fn one() {
22372 let mut a = ˇtwo();
22373 }
22374 fn three() {}
22375 fn two() {}"#
22376 .unindent(),
22377 );
22378 cx.lsp
22379 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22380 Ok(Some(vec![
22381 lsp::Location {
22382 uri: params.text_document_position.text_document.uri.clone(),
22383 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22384 },
22385 lsp::Location {
22386 uri: params.text_document_position.text_document.uri,
22387 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22388 },
22389 ]))
22390 });
22391 let navigated = cx
22392 .update_editor(|editor, window, cx| {
22393 editor.find_all_references(&FindAllReferences, window, cx)
22394 })
22395 .unwrap()
22396 .await
22397 .expect("Failed to navigate to references");
22398 assert_eq!(
22399 navigated,
22400 Navigated::Yes,
22401 "Should have navigated to references from the FindAllReferences response"
22402 );
22403 cx.assert_editor_state(
22404 &r#"fn one() {
22405 let mut a = ˇtwo();
22406 }
22407 fn three() {}
22408 fn two() {}"#
22409 .unindent(),
22410 );
22411 let editors = cx.update_workspace(|workspace, _, cx| {
22412 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22413 });
22414 cx.update_editor(|_, _, _| {
22415 assert_eq!(
22416 editors.len(),
22417 3,
22418 "should have used a new multibuffer as offsets changed"
22419 );
22420 });
22421}
22422#[gpui::test]
22423async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22424 init_test(cx, |_| {});
22425
22426 let language = Arc::new(Language::new(
22427 LanguageConfig::default(),
22428 Some(tree_sitter_rust::LANGUAGE.into()),
22429 ));
22430
22431 let text = r#"
22432 #[cfg(test)]
22433 mod tests() {
22434 #[test]
22435 fn runnable_1() {
22436 let a = 1;
22437 }
22438
22439 #[test]
22440 fn runnable_2() {
22441 let a = 1;
22442 let b = 2;
22443 }
22444 }
22445 "#
22446 .unindent();
22447
22448 let fs = FakeFs::new(cx.executor());
22449 fs.insert_file("/file.rs", Default::default()).await;
22450
22451 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22453 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22454 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22455 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22456
22457 let editor = cx.new_window_entity(|window, cx| {
22458 Editor::new(
22459 EditorMode::full(),
22460 multi_buffer,
22461 Some(project.clone()),
22462 window,
22463 cx,
22464 )
22465 });
22466
22467 editor.update_in(cx, |editor, window, cx| {
22468 let snapshot = editor.buffer().read(cx).snapshot(cx);
22469 editor.tasks.insert(
22470 (buffer.read(cx).remote_id(), 3),
22471 RunnableTasks {
22472 templates: vec![],
22473 offset: snapshot.anchor_before(MultiBufferOffset(43)),
22474 column: 0,
22475 extra_variables: HashMap::default(),
22476 context_range: BufferOffset(43)..BufferOffset(85),
22477 },
22478 );
22479 editor.tasks.insert(
22480 (buffer.read(cx).remote_id(), 8),
22481 RunnableTasks {
22482 templates: vec![],
22483 offset: snapshot.anchor_before(MultiBufferOffset(86)),
22484 column: 0,
22485 extra_variables: HashMap::default(),
22486 context_range: BufferOffset(86)..BufferOffset(191),
22487 },
22488 );
22489
22490 // Test finding task when cursor is inside function body
22491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22492 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22493 });
22494 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22495 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22496
22497 // Test finding task when cursor is on function name
22498 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22499 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22500 });
22501 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22502 assert_eq!(row, 8, "Should find task when cursor is on function name");
22503 });
22504}
22505
22506#[gpui::test]
22507async fn test_folding_buffers(cx: &mut TestAppContext) {
22508 init_test(cx, |_| {});
22509
22510 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22511 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22512 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22513
22514 let fs = FakeFs::new(cx.executor());
22515 fs.insert_tree(
22516 path!("/a"),
22517 json!({
22518 "first.rs": sample_text_1,
22519 "second.rs": sample_text_2,
22520 "third.rs": sample_text_3,
22521 }),
22522 )
22523 .await;
22524 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22525 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22526 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22527 let worktree = project.update(cx, |project, cx| {
22528 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22529 assert_eq!(worktrees.len(), 1);
22530 worktrees.pop().unwrap()
22531 });
22532 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22533
22534 let buffer_1 = project
22535 .update(cx, |project, cx| {
22536 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22537 })
22538 .await
22539 .unwrap();
22540 let buffer_2 = project
22541 .update(cx, |project, cx| {
22542 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22543 })
22544 .await
22545 .unwrap();
22546 let buffer_3 = project
22547 .update(cx, |project, cx| {
22548 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22549 })
22550 .await
22551 .unwrap();
22552
22553 let multi_buffer = cx.new(|cx| {
22554 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22555 multi_buffer.push_excerpts(
22556 buffer_1.clone(),
22557 [
22558 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22559 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22560 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22561 ],
22562 cx,
22563 );
22564 multi_buffer.push_excerpts(
22565 buffer_2.clone(),
22566 [
22567 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22568 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22569 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22570 ],
22571 cx,
22572 );
22573 multi_buffer.push_excerpts(
22574 buffer_3.clone(),
22575 [
22576 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22577 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22578 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22579 ],
22580 cx,
22581 );
22582 multi_buffer
22583 });
22584 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22585 Editor::new(
22586 EditorMode::full(),
22587 multi_buffer.clone(),
22588 Some(project.clone()),
22589 window,
22590 cx,
22591 )
22592 });
22593
22594 assert_eq!(
22595 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22596 "\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",
22597 );
22598
22599 multi_buffer_editor.update(cx, |editor, cx| {
22600 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22601 });
22602 assert_eq!(
22603 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22604 "\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",
22605 "After folding the first buffer, its text should not be displayed"
22606 );
22607
22608 multi_buffer_editor.update(cx, |editor, cx| {
22609 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22610 });
22611 assert_eq!(
22612 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22613 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22614 "After folding the second buffer, its text should not be displayed"
22615 );
22616
22617 multi_buffer_editor.update(cx, |editor, cx| {
22618 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22619 });
22620 assert_eq!(
22621 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22622 "\n\n\n\n\n",
22623 "After folding the third buffer, its text should not be displayed"
22624 );
22625
22626 // Emulate selection inside the fold logic, that should work
22627 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22628 editor
22629 .snapshot(window, cx)
22630 .next_line_boundary(Point::new(0, 4));
22631 });
22632
22633 multi_buffer_editor.update(cx, |editor, cx| {
22634 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22635 });
22636 assert_eq!(
22637 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22638 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22639 "After unfolding the second buffer, its text should be displayed"
22640 );
22641
22642 // Typing inside of buffer 1 causes that buffer to be unfolded.
22643 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22644 assert_eq!(
22645 multi_buffer
22646 .read(cx)
22647 .snapshot(cx)
22648 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22649 .collect::<String>(),
22650 "bbbb"
22651 );
22652 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22653 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22654 });
22655 editor.handle_input("B", window, cx);
22656 });
22657
22658 assert_eq!(
22659 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22660 "\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",
22661 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22662 );
22663
22664 multi_buffer_editor.update(cx, |editor, cx| {
22665 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22666 });
22667 assert_eq!(
22668 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22669 "\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",
22670 "After unfolding the all buffers, all original text should be displayed"
22671 );
22672}
22673
22674#[gpui::test]
22675async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22676 init_test(cx, |_| {});
22677
22678 let sample_text_1 = "1111\n2222\n3333".to_string();
22679 let sample_text_2 = "4444\n5555\n6666".to_string();
22680 let sample_text_3 = "7777\n8888\n9999".to_string();
22681
22682 let fs = FakeFs::new(cx.executor());
22683 fs.insert_tree(
22684 path!("/a"),
22685 json!({
22686 "first.rs": sample_text_1,
22687 "second.rs": sample_text_2,
22688 "third.rs": sample_text_3,
22689 }),
22690 )
22691 .await;
22692 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22694 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22695 let worktree = project.update(cx, |project, cx| {
22696 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22697 assert_eq!(worktrees.len(), 1);
22698 worktrees.pop().unwrap()
22699 });
22700 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22701
22702 let buffer_1 = project
22703 .update(cx, |project, cx| {
22704 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22705 })
22706 .await
22707 .unwrap();
22708 let buffer_2 = project
22709 .update(cx, |project, cx| {
22710 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22711 })
22712 .await
22713 .unwrap();
22714 let buffer_3 = project
22715 .update(cx, |project, cx| {
22716 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22717 })
22718 .await
22719 .unwrap();
22720
22721 let multi_buffer = cx.new(|cx| {
22722 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22723 multi_buffer.push_excerpts(
22724 buffer_1.clone(),
22725 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22726 cx,
22727 );
22728 multi_buffer.push_excerpts(
22729 buffer_2.clone(),
22730 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22731 cx,
22732 );
22733 multi_buffer.push_excerpts(
22734 buffer_3.clone(),
22735 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22736 cx,
22737 );
22738 multi_buffer
22739 });
22740
22741 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22742 Editor::new(
22743 EditorMode::full(),
22744 multi_buffer,
22745 Some(project.clone()),
22746 window,
22747 cx,
22748 )
22749 });
22750
22751 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22752 assert_eq!(
22753 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22754 full_text,
22755 );
22756
22757 multi_buffer_editor.update(cx, |editor, cx| {
22758 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22759 });
22760 assert_eq!(
22761 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22762 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22763 "After folding the first buffer, its text should not be displayed"
22764 );
22765
22766 multi_buffer_editor.update(cx, |editor, cx| {
22767 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22768 });
22769
22770 assert_eq!(
22771 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22772 "\n\n\n\n\n\n7777\n8888\n9999",
22773 "After folding the second buffer, its text should not be displayed"
22774 );
22775
22776 multi_buffer_editor.update(cx, |editor, cx| {
22777 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22778 });
22779 assert_eq!(
22780 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22781 "\n\n\n\n\n",
22782 "After folding the third buffer, its text should not be displayed"
22783 );
22784
22785 multi_buffer_editor.update(cx, |editor, cx| {
22786 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22787 });
22788 assert_eq!(
22789 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22790 "\n\n\n\n4444\n5555\n6666\n\n",
22791 "After unfolding the second buffer, its text should be displayed"
22792 );
22793
22794 multi_buffer_editor.update(cx, |editor, cx| {
22795 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22796 });
22797 assert_eq!(
22798 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22799 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22800 "After unfolding the first buffer, its text should be displayed"
22801 );
22802
22803 multi_buffer_editor.update(cx, |editor, cx| {
22804 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22805 });
22806 assert_eq!(
22807 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22808 full_text,
22809 "After unfolding all buffers, all original text should be displayed"
22810 );
22811}
22812
22813#[gpui::test]
22814async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22815 init_test(cx, |_| {});
22816
22817 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22818
22819 let fs = FakeFs::new(cx.executor());
22820 fs.insert_tree(
22821 path!("/a"),
22822 json!({
22823 "main.rs": sample_text,
22824 }),
22825 )
22826 .await;
22827 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22828 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22829 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22830 let worktree = project.update(cx, |project, cx| {
22831 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22832 assert_eq!(worktrees.len(), 1);
22833 worktrees.pop().unwrap()
22834 });
22835 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22836
22837 let buffer_1 = project
22838 .update(cx, |project, cx| {
22839 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22840 })
22841 .await
22842 .unwrap();
22843
22844 let multi_buffer = cx.new(|cx| {
22845 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22846 multi_buffer.push_excerpts(
22847 buffer_1.clone(),
22848 [ExcerptRange::new(
22849 Point::new(0, 0)
22850 ..Point::new(
22851 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22852 0,
22853 ),
22854 )],
22855 cx,
22856 );
22857 multi_buffer
22858 });
22859 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22860 Editor::new(
22861 EditorMode::full(),
22862 multi_buffer,
22863 Some(project.clone()),
22864 window,
22865 cx,
22866 )
22867 });
22868
22869 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22870 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22871 enum TestHighlight {}
22872 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22873 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22874 editor.highlight_text::<TestHighlight>(
22875 vec![highlight_range.clone()],
22876 HighlightStyle::color(Hsla::green()),
22877 cx,
22878 );
22879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22880 s.select_ranges(Some(highlight_range))
22881 });
22882 });
22883
22884 let full_text = format!("\n\n{sample_text}");
22885 assert_eq!(
22886 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22887 full_text,
22888 );
22889}
22890
22891#[gpui::test]
22892async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22893 init_test(cx, |_| {});
22894 cx.update(|cx| {
22895 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22896 "keymaps/default-linux.json",
22897 cx,
22898 )
22899 .unwrap();
22900 cx.bind_keys(default_key_bindings);
22901 });
22902
22903 let (editor, cx) = cx.add_window_view(|window, cx| {
22904 let multi_buffer = MultiBuffer::build_multi(
22905 [
22906 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22907 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22908 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22909 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22910 ],
22911 cx,
22912 );
22913 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22914
22915 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22916 // fold all but the second buffer, so that we test navigating between two
22917 // adjacent folded buffers, as well as folded buffers at the start and
22918 // end the multibuffer
22919 editor.fold_buffer(buffer_ids[0], cx);
22920 editor.fold_buffer(buffer_ids[2], cx);
22921 editor.fold_buffer(buffer_ids[3], cx);
22922
22923 editor
22924 });
22925 cx.simulate_resize(size(px(1000.), px(1000.)));
22926
22927 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22928 cx.assert_excerpts_with_selections(indoc! {"
22929 [EXCERPT]
22930 ˇ[FOLDED]
22931 [EXCERPT]
22932 a1
22933 b1
22934 [EXCERPT]
22935 [FOLDED]
22936 [EXCERPT]
22937 [FOLDED]
22938 "
22939 });
22940 cx.simulate_keystroke("down");
22941 cx.assert_excerpts_with_selections(indoc! {"
22942 [EXCERPT]
22943 [FOLDED]
22944 [EXCERPT]
22945 ˇa1
22946 b1
22947 [EXCERPT]
22948 [FOLDED]
22949 [EXCERPT]
22950 [FOLDED]
22951 "
22952 });
22953 cx.simulate_keystroke("down");
22954 cx.assert_excerpts_with_selections(indoc! {"
22955 [EXCERPT]
22956 [FOLDED]
22957 [EXCERPT]
22958 a1
22959 ˇb1
22960 [EXCERPT]
22961 [FOLDED]
22962 [EXCERPT]
22963 [FOLDED]
22964 "
22965 });
22966 cx.simulate_keystroke("down");
22967 cx.assert_excerpts_with_selections(indoc! {"
22968 [EXCERPT]
22969 [FOLDED]
22970 [EXCERPT]
22971 a1
22972 b1
22973 ˇ[EXCERPT]
22974 [FOLDED]
22975 [EXCERPT]
22976 [FOLDED]
22977 "
22978 });
22979 cx.simulate_keystroke("down");
22980 cx.assert_excerpts_with_selections(indoc! {"
22981 [EXCERPT]
22982 [FOLDED]
22983 [EXCERPT]
22984 a1
22985 b1
22986 [EXCERPT]
22987 ˇ[FOLDED]
22988 [EXCERPT]
22989 [FOLDED]
22990 "
22991 });
22992 for _ in 0..5 {
22993 cx.simulate_keystroke("down");
22994 cx.assert_excerpts_with_selections(indoc! {"
22995 [EXCERPT]
22996 [FOLDED]
22997 [EXCERPT]
22998 a1
22999 b1
23000 [EXCERPT]
23001 [FOLDED]
23002 [EXCERPT]
23003 ˇ[FOLDED]
23004 "
23005 });
23006 }
23007
23008 cx.simulate_keystroke("up");
23009 cx.assert_excerpts_with_selections(indoc! {"
23010 [EXCERPT]
23011 [FOLDED]
23012 [EXCERPT]
23013 a1
23014 b1
23015 [EXCERPT]
23016 ˇ[FOLDED]
23017 [EXCERPT]
23018 [FOLDED]
23019 "
23020 });
23021 cx.simulate_keystroke("up");
23022 cx.assert_excerpts_with_selections(indoc! {"
23023 [EXCERPT]
23024 [FOLDED]
23025 [EXCERPT]
23026 a1
23027 b1
23028 ˇ[EXCERPT]
23029 [FOLDED]
23030 [EXCERPT]
23031 [FOLDED]
23032 "
23033 });
23034 cx.simulate_keystroke("up");
23035 cx.assert_excerpts_with_selections(indoc! {"
23036 [EXCERPT]
23037 [FOLDED]
23038 [EXCERPT]
23039 a1
23040 ˇb1
23041 [EXCERPT]
23042 [FOLDED]
23043 [EXCERPT]
23044 [FOLDED]
23045 "
23046 });
23047 cx.simulate_keystroke("up");
23048 cx.assert_excerpts_with_selections(indoc! {"
23049 [EXCERPT]
23050 [FOLDED]
23051 [EXCERPT]
23052 ˇa1
23053 b1
23054 [EXCERPT]
23055 [FOLDED]
23056 [EXCERPT]
23057 [FOLDED]
23058 "
23059 });
23060 for _ in 0..5 {
23061 cx.simulate_keystroke("up");
23062 cx.assert_excerpts_with_selections(indoc! {"
23063 [EXCERPT]
23064 ˇ[FOLDED]
23065 [EXCERPT]
23066 a1
23067 b1
23068 [EXCERPT]
23069 [FOLDED]
23070 [EXCERPT]
23071 [FOLDED]
23072 "
23073 });
23074 }
23075}
23076
23077#[gpui::test]
23078async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23079 init_test(cx, |_| {});
23080
23081 // Simple insertion
23082 assert_highlighted_edits(
23083 "Hello, world!",
23084 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23085 true,
23086 cx,
23087 |highlighted_edits, cx| {
23088 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23089 assert_eq!(highlighted_edits.highlights.len(), 1);
23090 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23091 assert_eq!(
23092 highlighted_edits.highlights[0].1.background_color,
23093 Some(cx.theme().status().created_background)
23094 );
23095 },
23096 )
23097 .await;
23098
23099 // Replacement
23100 assert_highlighted_edits(
23101 "This is a test.",
23102 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23103 false,
23104 cx,
23105 |highlighted_edits, cx| {
23106 assert_eq!(highlighted_edits.text, "That is a test.");
23107 assert_eq!(highlighted_edits.highlights.len(), 1);
23108 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23109 assert_eq!(
23110 highlighted_edits.highlights[0].1.background_color,
23111 Some(cx.theme().status().created_background)
23112 );
23113 },
23114 )
23115 .await;
23116
23117 // Multiple edits
23118 assert_highlighted_edits(
23119 "Hello, world!",
23120 vec![
23121 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23122 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23123 ],
23124 false,
23125 cx,
23126 |highlighted_edits, cx| {
23127 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23128 assert_eq!(highlighted_edits.highlights.len(), 2);
23129 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23130 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23131 assert_eq!(
23132 highlighted_edits.highlights[0].1.background_color,
23133 Some(cx.theme().status().created_background)
23134 );
23135 assert_eq!(
23136 highlighted_edits.highlights[1].1.background_color,
23137 Some(cx.theme().status().created_background)
23138 );
23139 },
23140 )
23141 .await;
23142
23143 // Multiple lines with edits
23144 assert_highlighted_edits(
23145 "First line\nSecond line\nThird line\nFourth line",
23146 vec![
23147 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23148 (
23149 Point::new(2, 0)..Point::new(2, 10),
23150 "New third line".to_string(),
23151 ),
23152 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23153 ],
23154 false,
23155 cx,
23156 |highlighted_edits, cx| {
23157 assert_eq!(
23158 highlighted_edits.text,
23159 "Second modified\nNew third line\nFourth updated line"
23160 );
23161 assert_eq!(highlighted_edits.highlights.len(), 3);
23162 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23163 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23164 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23165 for highlight in &highlighted_edits.highlights {
23166 assert_eq!(
23167 highlight.1.background_color,
23168 Some(cx.theme().status().created_background)
23169 );
23170 }
23171 },
23172 )
23173 .await;
23174}
23175
23176#[gpui::test]
23177async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23178 init_test(cx, |_| {});
23179
23180 // Deletion
23181 assert_highlighted_edits(
23182 "Hello, world!",
23183 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23184 true,
23185 cx,
23186 |highlighted_edits, cx| {
23187 assert_eq!(highlighted_edits.text, "Hello, world!");
23188 assert_eq!(highlighted_edits.highlights.len(), 1);
23189 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23190 assert_eq!(
23191 highlighted_edits.highlights[0].1.background_color,
23192 Some(cx.theme().status().deleted_background)
23193 );
23194 },
23195 )
23196 .await;
23197
23198 // Insertion
23199 assert_highlighted_edits(
23200 "Hello, world!",
23201 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23202 true,
23203 cx,
23204 |highlighted_edits, cx| {
23205 assert_eq!(highlighted_edits.highlights.len(), 1);
23206 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23207 assert_eq!(
23208 highlighted_edits.highlights[0].1.background_color,
23209 Some(cx.theme().status().created_background)
23210 );
23211 },
23212 )
23213 .await;
23214}
23215
23216async fn assert_highlighted_edits(
23217 text: &str,
23218 edits: Vec<(Range<Point>, String)>,
23219 include_deletions: bool,
23220 cx: &mut TestAppContext,
23221 assertion_fn: impl Fn(HighlightedText, &App),
23222) {
23223 let window = cx.add_window(|window, cx| {
23224 let buffer = MultiBuffer::build_simple(text, cx);
23225 Editor::new(EditorMode::full(), buffer, None, window, cx)
23226 });
23227 let cx = &mut VisualTestContext::from_window(*window, cx);
23228
23229 let (buffer, snapshot) = window
23230 .update(cx, |editor, _window, cx| {
23231 (
23232 editor.buffer().clone(),
23233 editor.buffer().read(cx).snapshot(cx),
23234 )
23235 })
23236 .unwrap();
23237
23238 let edits = edits
23239 .into_iter()
23240 .map(|(range, edit)| {
23241 (
23242 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23243 edit,
23244 )
23245 })
23246 .collect::<Vec<_>>();
23247
23248 let text_anchor_edits = edits
23249 .clone()
23250 .into_iter()
23251 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23252 .collect::<Vec<_>>();
23253
23254 let edit_preview = window
23255 .update(cx, |_, _window, cx| {
23256 buffer
23257 .read(cx)
23258 .as_singleton()
23259 .unwrap()
23260 .read(cx)
23261 .preview_edits(text_anchor_edits.into(), cx)
23262 })
23263 .unwrap()
23264 .await;
23265
23266 cx.update(|_window, cx| {
23267 let highlighted_edits = edit_prediction_edit_text(
23268 snapshot.as_singleton().unwrap().2,
23269 &edits,
23270 &edit_preview,
23271 include_deletions,
23272 cx,
23273 );
23274 assertion_fn(highlighted_edits, cx)
23275 });
23276}
23277
23278#[track_caller]
23279fn assert_breakpoint(
23280 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23281 path: &Arc<Path>,
23282 expected: Vec<(u32, Breakpoint)>,
23283) {
23284 if expected.is_empty() {
23285 assert!(!breakpoints.contains_key(path), "{}", path.display());
23286 } else {
23287 let mut breakpoint = breakpoints
23288 .get(path)
23289 .unwrap()
23290 .iter()
23291 .map(|breakpoint| {
23292 (
23293 breakpoint.row,
23294 Breakpoint {
23295 message: breakpoint.message.clone(),
23296 state: breakpoint.state,
23297 condition: breakpoint.condition.clone(),
23298 hit_condition: breakpoint.hit_condition.clone(),
23299 },
23300 )
23301 })
23302 .collect::<Vec<_>>();
23303
23304 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23305
23306 assert_eq!(expected, breakpoint);
23307 }
23308}
23309
23310fn add_log_breakpoint_at_cursor(
23311 editor: &mut Editor,
23312 log_message: &str,
23313 window: &mut Window,
23314 cx: &mut Context<Editor>,
23315) {
23316 let (anchor, bp) = editor
23317 .breakpoints_at_cursors(window, cx)
23318 .first()
23319 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23320 .unwrap_or_else(|| {
23321 let snapshot = editor.snapshot(window, cx);
23322 let cursor_position: Point =
23323 editor.selections.newest(&snapshot.display_snapshot).head();
23324
23325 let breakpoint_position = snapshot
23326 .buffer_snapshot()
23327 .anchor_before(Point::new(cursor_position.row, 0));
23328
23329 (breakpoint_position, Breakpoint::new_log(log_message))
23330 });
23331
23332 editor.edit_breakpoint_at_anchor(
23333 anchor,
23334 bp,
23335 BreakpointEditAction::EditLogMessage(log_message.into()),
23336 cx,
23337 );
23338}
23339
23340#[gpui::test]
23341async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23342 init_test(cx, |_| {});
23343
23344 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23345 let fs = FakeFs::new(cx.executor());
23346 fs.insert_tree(
23347 path!("/a"),
23348 json!({
23349 "main.rs": sample_text,
23350 }),
23351 )
23352 .await;
23353 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23354 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23355 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23356
23357 let fs = FakeFs::new(cx.executor());
23358 fs.insert_tree(
23359 path!("/a"),
23360 json!({
23361 "main.rs": sample_text,
23362 }),
23363 )
23364 .await;
23365 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23366 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23367 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23368 let worktree_id = workspace
23369 .update(cx, |workspace, _window, cx| {
23370 workspace.project().update(cx, |project, cx| {
23371 project.worktrees(cx).next().unwrap().read(cx).id()
23372 })
23373 })
23374 .unwrap();
23375
23376 let buffer = project
23377 .update(cx, |project, cx| {
23378 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23379 })
23380 .await
23381 .unwrap();
23382
23383 let (editor, cx) = cx.add_window_view(|window, cx| {
23384 Editor::new(
23385 EditorMode::full(),
23386 MultiBuffer::build_from_buffer(buffer, cx),
23387 Some(project.clone()),
23388 window,
23389 cx,
23390 )
23391 });
23392
23393 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23394 let abs_path = project.read_with(cx, |project, cx| {
23395 project
23396 .absolute_path(&project_path, cx)
23397 .map(Arc::from)
23398 .unwrap()
23399 });
23400
23401 // assert we can add breakpoint on the first line
23402 editor.update_in(cx, |editor, window, cx| {
23403 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23404 editor.move_to_end(&MoveToEnd, window, cx);
23405 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23406 });
23407
23408 let breakpoints = editor.update(cx, |editor, cx| {
23409 editor
23410 .breakpoint_store()
23411 .as_ref()
23412 .unwrap()
23413 .read(cx)
23414 .all_source_breakpoints(cx)
23415 });
23416
23417 assert_eq!(1, breakpoints.len());
23418 assert_breakpoint(
23419 &breakpoints,
23420 &abs_path,
23421 vec![
23422 (0, Breakpoint::new_standard()),
23423 (3, Breakpoint::new_standard()),
23424 ],
23425 );
23426
23427 editor.update_in(cx, |editor, window, cx| {
23428 editor.move_to_beginning(&MoveToBeginning, window, cx);
23429 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23430 });
23431
23432 let breakpoints = editor.update(cx, |editor, cx| {
23433 editor
23434 .breakpoint_store()
23435 .as_ref()
23436 .unwrap()
23437 .read(cx)
23438 .all_source_breakpoints(cx)
23439 });
23440
23441 assert_eq!(1, breakpoints.len());
23442 assert_breakpoint(
23443 &breakpoints,
23444 &abs_path,
23445 vec![(3, Breakpoint::new_standard())],
23446 );
23447
23448 editor.update_in(cx, |editor, window, cx| {
23449 editor.move_to_end(&MoveToEnd, window, cx);
23450 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23451 });
23452
23453 let breakpoints = editor.update(cx, |editor, cx| {
23454 editor
23455 .breakpoint_store()
23456 .as_ref()
23457 .unwrap()
23458 .read(cx)
23459 .all_source_breakpoints(cx)
23460 });
23461
23462 assert_eq!(0, breakpoints.len());
23463 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23464}
23465
23466#[gpui::test]
23467async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23468 init_test(cx, |_| {});
23469
23470 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23471
23472 let fs = FakeFs::new(cx.executor());
23473 fs.insert_tree(
23474 path!("/a"),
23475 json!({
23476 "main.rs": sample_text,
23477 }),
23478 )
23479 .await;
23480 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23481 let (workspace, cx) =
23482 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23483
23484 let worktree_id = workspace.update(cx, |workspace, cx| {
23485 workspace.project().update(cx, |project, cx| {
23486 project.worktrees(cx).next().unwrap().read(cx).id()
23487 })
23488 });
23489
23490 let buffer = project
23491 .update(cx, |project, cx| {
23492 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23493 })
23494 .await
23495 .unwrap();
23496
23497 let (editor, cx) = cx.add_window_view(|window, cx| {
23498 Editor::new(
23499 EditorMode::full(),
23500 MultiBuffer::build_from_buffer(buffer, cx),
23501 Some(project.clone()),
23502 window,
23503 cx,
23504 )
23505 });
23506
23507 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23508 let abs_path = project.read_with(cx, |project, cx| {
23509 project
23510 .absolute_path(&project_path, cx)
23511 .map(Arc::from)
23512 .unwrap()
23513 });
23514
23515 editor.update_in(cx, |editor, window, cx| {
23516 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23517 });
23518
23519 let breakpoints = editor.update(cx, |editor, cx| {
23520 editor
23521 .breakpoint_store()
23522 .as_ref()
23523 .unwrap()
23524 .read(cx)
23525 .all_source_breakpoints(cx)
23526 });
23527
23528 assert_breakpoint(
23529 &breakpoints,
23530 &abs_path,
23531 vec![(0, Breakpoint::new_log("hello world"))],
23532 );
23533
23534 // Removing a log message from a log breakpoint should remove it
23535 editor.update_in(cx, |editor, window, cx| {
23536 add_log_breakpoint_at_cursor(editor, "", window, cx);
23537 });
23538
23539 let breakpoints = editor.update(cx, |editor, cx| {
23540 editor
23541 .breakpoint_store()
23542 .as_ref()
23543 .unwrap()
23544 .read(cx)
23545 .all_source_breakpoints(cx)
23546 });
23547
23548 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23549
23550 editor.update_in(cx, |editor, window, cx| {
23551 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23552 editor.move_to_end(&MoveToEnd, window, cx);
23553 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23554 // Not adding a log message to a standard breakpoint shouldn't remove it
23555 add_log_breakpoint_at_cursor(editor, "", window, cx);
23556 });
23557
23558 let breakpoints = editor.update(cx, |editor, cx| {
23559 editor
23560 .breakpoint_store()
23561 .as_ref()
23562 .unwrap()
23563 .read(cx)
23564 .all_source_breakpoints(cx)
23565 });
23566
23567 assert_breakpoint(
23568 &breakpoints,
23569 &abs_path,
23570 vec![
23571 (0, Breakpoint::new_standard()),
23572 (3, Breakpoint::new_standard()),
23573 ],
23574 );
23575
23576 editor.update_in(cx, |editor, window, cx| {
23577 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23578 });
23579
23580 let breakpoints = editor.update(cx, |editor, cx| {
23581 editor
23582 .breakpoint_store()
23583 .as_ref()
23584 .unwrap()
23585 .read(cx)
23586 .all_source_breakpoints(cx)
23587 });
23588
23589 assert_breakpoint(
23590 &breakpoints,
23591 &abs_path,
23592 vec![
23593 (0, Breakpoint::new_standard()),
23594 (3, Breakpoint::new_log("hello world")),
23595 ],
23596 );
23597
23598 editor.update_in(cx, |editor, window, cx| {
23599 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23600 });
23601
23602 let breakpoints = editor.update(cx, |editor, cx| {
23603 editor
23604 .breakpoint_store()
23605 .as_ref()
23606 .unwrap()
23607 .read(cx)
23608 .all_source_breakpoints(cx)
23609 });
23610
23611 assert_breakpoint(
23612 &breakpoints,
23613 &abs_path,
23614 vec![
23615 (0, Breakpoint::new_standard()),
23616 (3, Breakpoint::new_log("hello Earth!!")),
23617 ],
23618 );
23619}
23620
23621/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23622/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23623/// or when breakpoints were placed out of order. This tests for a regression too
23624#[gpui::test]
23625async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23626 init_test(cx, |_| {});
23627
23628 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23629 let fs = FakeFs::new(cx.executor());
23630 fs.insert_tree(
23631 path!("/a"),
23632 json!({
23633 "main.rs": sample_text,
23634 }),
23635 )
23636 .await;
23637 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23638 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23639 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23640
23641 let fs = FakeFs::new(cx.executor());
23642 fs.insert_tree(
23643 path!("/a"),
23644 json!({
23645 "main.rs": sample_text,
23646 }),
23647 )
23648 .await;
23649 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23650 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23651 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23652 let worktree_id = workspace
23653 .update(cx, |workspace, _window, cx| {
23654 workspace.project().update(cx, |project, cx| {
23655 project.worktrees(cx).next().unwrap().read(cx).id()
23656 })
23657 })
23658 .unwrap();
23659
23660 let buffer = project
23661 .update(cx, |project, cx| {
23662 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23663 })
23664 .await
23665 .unwrap();
23666
23667 let (editor, cx) = cx.add_window_view(|window, cx| {
23668 Editor::new(
23669 EditorMode::full(),
23670 MultiBuffer::build_from_buffer(buffer, cx),
23671 Some(project.clone()),
23672 window,
23673 cx,
23674 )
23675 });
23676
23677 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23678 let abs_path = project.read_with(cx, |project, cx| {
23679 project
23680 .absolute_path(&project_path, cx)
23681 .map(Arc::from)
23682 .unwrap()
23683 });
23684
23685 // assert we can add breakpoint on the first line
23686 editor.update_in(cx, |editor, window, cx| {
23687 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23688 editor.move_to_end(&MoveToEnd, window, cx);
23689 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23690 editor.move_up(&MoveUp, window, cx);
23691 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23692 });
23693
23694 let breakpoints = editor.update(cx, |editor, cx| {
23695 editor
23696 .breakpoint_store()
23697 .as_ref()
23698 .unwrap()
23699 .read(cx)
23700 .all_source_breakpoints(cx)
23701 });
23702
23703 assert_eq!(1, breakpoints.len());
23704 assert_breakpoint(
23705 &breakpoints,
23706 &abs_path,
23707 vec![
23708 (0, Breakpoint::new_standard()),
23709 (2, Breakpoint::new_standard()),
23710 (3, Breakpoint::new_standard()),
23711 ],
23712 );
23713
23714 editor.update_in(cx, |editor, window, cx| {
23715 editor.move_to_beginning(&MoveToBeginning, window, cx);
23716 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23717 editor.move_to_end(&MoveToEnd, window, cx);
23718 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23719 // Disabling a breakpoint that doesn't exist should do nothing
23720 editor.move_up(&MoveUp, window, cx);
23721 editor.move_up(&MoveUp, window, cx);
23722 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23723 });
23724
23725 let breakpoints = editor.update(cx, |editor, cx| {
23726 editor
23727 .breakpoint_store()
23728 .as_ref()
23729 .unwrap()
23730 .read(cx)
23731 .all_source_breakpoints(cx)
23732 });
23733
23734 let disable_breakpoint = {
23735 let mut bp = Breakpoint::new_standard();
23736 bp.state = BreakpointState::Disabled;
23737 bp
23738 };
23739
23740 assert_eq!(1, breakpoints.len());
23741 assert_breakpoint(
23742 &breakpoints,
23743 &abs_path,
23744 vec![
23745 (0, disable_breakpoint.clone()),
23746 (2, Breakpoint::new_standard()),
23747 (3, disable_breakpoint.clone()),
23748 ],
23749 );
23750
23751 editor.update_in(cx, |editor, window, cx| {
23752 editor.move_to_beginning(&MoveToBeginning, window, cx);
23753 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23754 editor.move_to_end(&MoveToEnd, window, cx);
23755 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23756 editor.move_up(&MoveUp, window, cx);
23757 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23758 });
23759
23760 let breakpoints = editor.update(cx, |editor, cx| {
23761 editor
23762 .breakpoint_store()
23763 .as_ref()
23764 .unwrap()
23765 .read(cx)
23766 .all_source_breakpoints(cx)
23767 });
23768
23769 assert_eq!(1, breakpoints.len());
23770 assert_breakpoint(
23771 &breakpoints,
23772 &abs_path,
23773 vec![
23774 (0, Breakpoint::new_standard()),
23775 (2, disable_breakpoint),
23776 (3, Breakpoint::new_standard()),
23777 ],
23778 );
23779}
23780
23781#[gpui::test]
23782async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23783 init_test(cx, |_| {});
23784 let capabilities = lsp::ServerCapabilities {
23785 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23786 prepare_provider: Some(true),
23787 work_done_progress_options: Default::default(),
23788 })),
23789 ..Default::default()
23790 };
23791 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23792
23793 cx.set_state(indoc! {"
23794 struct Fˇoo {}
23795 "});
23796
23797 cx.update_editor(|editor, _, cx| {
23798 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23799 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23800 editor.highlight_background::<DocumentHighlightRead>(
23801 &[highlight_range],
23802 |theme| theme.colors().editor_document_highlight_read_background,
23803 cx,
23804 );
23805 });
23806
23807 let mut prepare_rename_handler = cx
23808 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23809 move |_, _, _| async move {
23810 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23811 start: lsp::Position {
23812 line: 0,
23813 character: 7,
23814 },
23815 end: lsp::Position {
23816 line: 0,
23817 character: 10,
23818 },
23819 })))
23820 },
23821 );
23822 let prepare_rename_task = cx
23823 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23824 .expect("Prepare rename was not started");
23825 prepare_rename_handler.next().await.unwrap();
23826 prepare_rename_task.await.expect("Prepare rename failed");
23827
23828 let mut rename_handler =
23829 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23830 let edit = lsp::TextEdit {
23831 range: lsp::Range {
23832 start: lsp::Position {
23833 line: 0,
23834 character: 7,
23835 },
23836 end: lsp::Position {
23837 line: 0,
23838 character: 10,
23839 },
23840 },
23841 new_text: "FooRenamed".to_string(),
23842 };
23843 Ok(Some(lsp::WorkspaceEdit::new(
23844 // Specify the same edit twice
23845 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23846 )))
23847 });
23848 let rename_task = cx
23849 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23850 .expect("Confirm rename was not started");
23851 rename_handler.next().await.unwrap();
23852 rename_task.await.expect("Confirm rename failed");
23853 cx.run_until_parked();
23854
23855 // Despite two edits, only one is actually applied as those are identical
23856 cx.assert_editor_state(indoc! {"
23857 struct FooRenamedˇ {}
23858 "});
23859}
23860
23861#[gpui::test]
23862async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23863 init_test(cx, |_| {});
23864 // These capabilities indicate that the server does not support prepare rename.
23865 let capabilities = lsp::ServerCapabilities {
23866 rename_provider: Some(lsp::OneOf::Left(true)),
23867 ..Default::default()
23868 };
23869 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23870
23871 cx.set_state(indoc! {"
23872 struct Fˇoo {}
23873 "});
23874
23875 cx.update_editor(|editor, _window, cx| {
23876 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23877 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23878 editor.highlight_background::<DocumentHighlightRead>(
23879 &[highlight_range],
23880 |theme| theme.colors().editor_document_highlight_read_background,
23881 cx,
23882 );
23883 });
23884
23885 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23886 .expect("Prepare rename was not started")
23887 .await
23888 .expect("Prepare rename failed");
23889
23890 let mut rename_handler =
23891 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23892 let edit = lsp::TextEdit {
23893 range: lsp::Range {
23894 start: lsp::Position {
23895 line: 0,
23896 character: 7,
23897 },
23898 end: lsp::Position {
23899 line: 0,
23900 character: 10,
23901 },
23902 },
23903 new_text: "FooRenamed".to_string(),
23904 };
23905 Ok(Some(lsp::WorkspaceEdit::new(
23906 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23907 )))
23908 });
23909 let rename_task = cx
23910 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23911 .expect("Confirm rename was not started");
23912 rename_handler.next().await.unwrap();
23913 rename_task.await.expect("Confirm rename failed");
23914 cx.run_until_parked();
23915
23916 // Correct range is renamed, as `surrounding_word` is used to find it.
23917 cx.assert_editor_state(indoc! {"
23918 struct FooRenamedˇ {}
23919 "});
23920}
23921
23922#[gpui::test]
23923async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23924 init_test(cx, |_| {});
23925 let mut cx = EditorTestContext::new(cx).await;
23926
23927 let language = Arc::new(
23928 Language::new(
23929 LanguageConfig::default(),
23930 Some(tree_sitter_html::LANGUAGE.into()),
23931 )
23932 .with_brackets_query(
23933 r#"
23934 ("<" @open "/>" @close)
23935 ("</" @open ">" @close)
23936 ("<" @open ">" @close)
23937 ("\"" @open "\"" @close)
23938 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23939 "#,
23940 )
23941 .unwrap(),
23942 );
23943 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23944
23945 cx.set_state(indoc! {"
23946 <span>ˇ</span>
23947 "});
23948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23949 cx.assert_editor_state(indoc! {"
23950 <span>
23951 ˇ
23952 </span>
23953 "});
23954
23955 cx.set_state(indoc! {"
23956 <span><span></span>ˇ</span>
23957 "});
23958 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23959 cx.assert_editor_state(indoc! {"
23960 <span><span></span>
23961 ˇ</span>
23962 "});
23963
23964 cx.set_state(indoc! {"
23965 <span>ˇ
23966 </span>
23967 "});
23968 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23969 cx.assert_editor_state(indoc! {"
23970 <span>
23971 ˇ
23972 </span>
23973 "});
23974}
23975
23976#[gpui::test(iterations = 10)]
23977async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23978 init_test(cx, |_| {});
23979
23980 let fs = FakeFs::new(cx.executor());
23981 fs.insert_tree(
23982 path!("/dir"),
23983 json!({
23984 "a.ts": "a",
23985 }),
23986 )
23987 .await;
23988
23989 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23990 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23991 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23992
23993 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23994 language_registry.add(Arc::new(Language::new(
23995 LanguageConfig {
23996 name: "TypeScript".into(),
23997 matcher: LanguageMatcher {
23998 path_suffixes: vec!["ts".to_string()],
23999 ..Default::default()
24000 },
24001 ..Default::default()
24002 },
24003 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24004 )));
24005 let mut fake_language_servers = language_registry.register_fake_lsp(
24006 "TypeScript",
24007 FakeLspAdapter {
24008 capabilities: lsp::ServerCapabilities {
24009 code_lens_provider: Some(lsp::CodeLensOptions {
24010 resolve_provider: Some(true),
24011 }),
24012 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24013 commands: vec!["_the/command".to_string()],
24014 ..lsp::ExecuteCommandOptions::default()
24015 }),
24016 ..lsp::ServerCapabilities::default()
24017 },
24018 ..FakeLspAdapter::default()
24019 },
24020 );
24021
24022 let editor = workspace
24023 .update(cx, |workspace, window, cx| {
24024 workspace.open_abs_path(
24025 PathBuf::from(path!("/dir/a.ts")),
24026 OpenOptions::default(),
24027 window,
24028 cx,
24029 )
24030 })
24031 .unwrap()
24032 .await
24033 .unwrap()
24034 .downcast::<Editor>()
24035 .unwrap();
24036 cx.executor().run_until_parked();
24037
24038 let fake_server = fake_language_servers.next().await.unwrap();
24039
24040 let buffer = editor.update(cx, |editor, cx| {
24041 editor
24042 .buffer()
24043 .read(cx)
24044 .as_singleton()
24045 .expect("have opened a single file by path")
24046 });
24047
24048 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24049 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24050 drop(buffer_snapshot);
24051 let actions = cx
24052 .update_window(*workspace, |_, window, cx| {
24053 project.code_actions(&buffer, anchor..anchor, window, cx)
24054 })
24055 .unwrap();
24056
24057 fake_server
24058 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24059 Ok(Some(vec![
24060 lsp::CodeLens {
24061 range: lsp::Range::default(),
24062 command: Some(lsp::Command {
24063 title: "Code lens command".to_owned(),
24064 command: "_the/command".to_owned(),
24065 arguments: None,
24066 }),
24067 data: None,
24068 },
24069 lsp::CodeLens {
24070 range: lsp::Range::default(),
24071 command: Some(lsp::Command {
24072 title: "Command not in capabilities".to_owned(),
24073 command: "not in capabilities".to_owned(),
24074 arguments: None,
24075 }),
24076 data: None,
24077 },
24078 lsp::CodeLens {
24079 range: lsp::Range {
24080 start: lsp::Position {
24081 line: 1,
24082 character: 1,
24083 },
24084 end: lsp::Position {
24085 line: 1,
24086 character: 1,
24087 },
24088 },
24089 command: Some(lsp::Command {
24090 title: "Command not in range".to_owned(),
24091 command: "_the/command".to_owned(),
24092 arguments: None,
24093 }),
24094 data: None,
24095 },
24096 ]))
24097 })
24098 .next()
24099 .await;
24100
24101 let actions = actions.await.unwrap();
24102 assert_eq!(
24103 actions.len(),
24104 1,
24105 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24106 );
24107 let action = actions[0].clone();
24108 let apply = project.update(cx, |project, cx| {
24109 project.apply_code_action(buffer.clone(), action, true, cx)
24110 });
24111
24112 // Resolving the code action does not populate its edits. In absence of
24113 // edits, we must execute the given command.
24114 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24115 |mut lens, _| async move {
24116 let lens_command = lens.command.as_mut().expect("should have a command");
24117 assert_eq!(lens_command.title, "Code lens command");
24118 lens_command.arguments = Some(vec![json!("the-argument")]);
24119 Ok(lens)
24120 },
24121 );
24122
24123 // While executing the command, the language server sends the editor
24124 // a `workspaceEdit` request.
24125 fake_server
24126 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24127 let fake = fake_server.clone();
24128 move |params, _| {
24129 assert_eq!(params.command, "_the/command");
24130 let fake = fake.clone();
24131 async move {
24132 fake.server
24133 .request::<lsp::request::ApplyWorkspaceEdit>(
24134 lsp::ApplyWorkspaceEditParams {
24135 label: None,
24136 edit: lsp::WorkspaceEdit {
24137 changes: Some(
24138 [(
24139 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24140 vec![lsp::TextEdit {
24141 range: lsp::Range::new(
24142 lsp::Position::new(0, 0),
24143 lsp::Position::new(0, 0),
24144 ),
24145 new_text: "X".into(),
24146 }],
24147 )]
24148 .into_iter()
24149 .collect(),
24150 ),
24151 ..lsp::WorkspaceEdit::default()
24152 },
24153 },
24154 )
24155 .await
24156 .into_response()
24157 .unwrap();
24158 Ok(Some(json!(null)))
24159 }
24160 }
24161 })
24162 .next()
24163 .await;
24164
24165 // Applying the code lens command returns a project transaction containing the edits
24166 // sent by the language server in its `workspaceEdit` request.
24167 let transaction = apply.await.unwrap();
24168 assert!(transaction.0.contains_key(&buffer));
24169 buffer.update(cx, |buffer, cx| {
24170 assert_eq!(buffer.text(), "Xa");
24171 buffer.undo(cx);
24172 assert_eq!(buffer.text(), "a");
24173 });
24174
24175 let actions_after_edits = cx
24176 .update_window(*workspace, |_, window, cx| {
24177 project.code_actions(&buffer, anchor..anchor, window, cx)
24178 })
24179 .unwrap()
24180 .await
24181 .unwrap();
24182 assert_eq!(
24183 actions, actions_after_edits,
24184 "For the same selection, same code lens actions should be returned"
24185 );
24186
24187 let _responses =
24188 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24189 panic!("No more code lens requests are expected");
24190 });
24191 editor.update_in(cx, |editor, window, cx| {
24192 editor.select_all(&SelectAll, window, cx);
24193 });
24194 cx.executor().run_until_parked();
24195 let new_actions = cx
24196 .update_window(*workspace, |_, window, cx| {
24197 project.code_actions(&buffer, anchor..anchor, window, cx)
24198 })
24199 .unwrap()
24200 .await
24201 .unwrap();
24202 assert_eq!(
24203 actions, new_actions,
24204 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24205 );
24206}
24207
24208#[gpui::test]
24209async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24210 init_test(cx, |_| {});
24211
24212 let fs = FakeFs::new(cx.executor());
24213 let main_text = r#"fn main() {
24214println!("1");
24215println!("2");
24216println!("3");
24217println!("4");
24218println!("5");
24219}"#;
24220 let lib_text = "mod foo {}";
24221 fs.insert_tree(
24222 path!("/a"),
24223 json!({
24224 "lib.rs": lib_text,
24225 "main.rs": main_text,
24226 }),
24227 )
24228 .await;
24229
24230 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24231 let (workspace, cx) =
24232 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24233 let worktree_id = workspace.update(cx, |workspace, cx| {
24234 workspace.project().update(cx, |project, cx| {
24235 project.worktrees(cx).next().unwrap().read(cx).id()
24236 })
24237 });
24238
24239 let expected_ranges = vec![
24240 Point::new(0, 0)..Point::new(0, 0),
24241 Point::new(1, 0)..Point::new(1, 1),
24242 Point::new(2, 0)..Point::new(2, 2),
24243 Point::new(3, 0)..Point::new(3, 3),
24244 ];
24245
24246 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24247 let editor_1 = workspace
24248 .update_in(cx, |workspace, window, cx| {
24249 workspace.open_path(
24250 (worktree_id, rel_path("main.rs")),
24251 Some(pane_1.downgrade()),
24252 true,
24253 window,
24254 cx,
24255 )
24256 })
24257 .unwrap()
24258 .await
24259 .downcast::<Editor>()
24260 .unwrap();
24261 pane_1.update(cx, |pane, cx| {
24262 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24263 open_editor.update(cx, |editor, cx| {
24264 assert_eq!(
24265 editor.display_text(cx),
24266 main_text,
24267 "Original main.rs text on initial open",
24268 );
24269 assert_eq!(
24270 editor
24271 .selections
24272 .all::<Point>(&editor.display_snapshot(cx))
24273 .into_iter()
24274 .map(|s| s.range())
24275 .collect::<Vec<_>>(),
24276 vec![Point::zero()..Point::zero()],
24277 "Default selections on initial open",
24278 );
24279 })
24280 });
24281 editor_1.update_in(cx, |editor, window, cx| {
24282 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24283 s.select_ranges(expected_ranges.clone());
24284 });
24285 });
24286
24287 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24288 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24289 });
24290 let editor_2 = workspace
24291 .update_in(cx, |workspace, window, cx| {
24292 workspace.open_path(
24293 (worktree_id, rel_path("main.rs")),
24294 Some(pane_2.downgrade()),
24295 true,
24296 window,
24297 cx,
24298 )
24299 })
24300 .unwrap()
24301 .await
24302 .downcast::<Editor>()
24303 .unwrap();
24304 pane_2.update(cx, |pane, cx| {
24305 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24306 open_editor.update(cx, |editor, cx| {
24307 assert_eq!(
24308 editor.display_text(cx),
24309 main_text,
24310 "Original main.rs text on initial open in another panel",
24311 );
24312 assert_eq!(
24313 editor
24314 .selections
24315 .all::<Point>(&editor.display_snapshot(cx))
24316 .into_iter()
24317 .map(|s| s.range())
24318 .collect::<Vec<_>>(),
24319 vec![Point::zero()..Point::zero()],
24320 "Default selections on initial open in another panel",
24321 );
24322 })
24323 });
24324
24325 editor_2.update_in(cx, |editor, window, cx| {
24326 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24327 });
24328
24329 let _other_editor_1 = workspace
24330 .update_in(cx, |workspace, window, cx| {
24331 workspace.open_path(
24332 (worktree_id, rel_path("lib.rs")),
24333 Some(pane_1.downgrade()),
24334 true,
24335 window,
24336 cx,
24337 )
24338 })
24339 .unwrap()
24340 .await
24341 .downcast::<Editor>()
24342 .unwrap();
24343 pane_1
24344 .update_in(cx, |pane, window, cx| {
24345 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24346 })
24347 .await
24348 .unwrap();
24349 drop(editor_1);
24350 pane_1.update(cx, |pane, cx| {
24351 pane.active_item()
24352 .unwrap()
24353 .downcast::<Editor>()
24354 .unwrap()
24355 .update(cx, |editor, cx| {
24356 assert_eq!(
24357 editor.display_text(cx),
24358 lib_text,
24359 "Other file should be open and active",
24360 );
24361 });
24362 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24363 });
24364
24365 let _other_editor_2 = workspace
24366 .update_in(cx, |workspace, window, cx| {
24367 workspace.open_path(
24368 (worktree_id, rel_path("lib.rs")),
24369 Some(pane_2.downgrade()),
24370 true,
24371 window,
24372 cx,
24373 )
24374 })
24375 .unwrap()
24376 .await
24377 .downcast::<Editor>()
24378 .unwrap();
24379 pane_2
24380 .update_in(cx, |pane, window, cx| {
24381 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24382 })
24383 .await
24384 .unwrap();
24385 drop(editor_2);
24386 pane_2.update(cx, |pane, cx| {
24387 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24388 open_editor.update(cx, |editor, cx| {
24389 assert_eq!(
24390 editor.display_text(cx),
24391 lib_text,
24392 "Other file should be open and active in another panel too",
24393 );
24394 });
24395 assert_eq!(
24396 pane.items().count(),
24397 1,
24398 "No other editors should be open in another pane",
24399 );
24400 });
24401
24402 let _editor_1_reopened = workspace
24403 .update_in(cx, |workspace, window, cx| {
24404 workspace.open_path(
24405 (worktree_id, rel_path("main.rs")),
24406 Some(pane_1.downgrade()),
24407 true,
24408 window,
24409 cx,
24410 )
24411 })
24412 .unwrap()
24413 .await
24414 .downcast::<Editor>()
24415 .unwrap();
24416 let _editor_2_reopened = workspace
24417 .update_in(cx, |workspace, window, cx| {
24418 workspace.open_path(
24419 (worktree_id, rel_path("main.rs")),
24420 Some(pane_2.downgrade()),
24421 true,
24422 window,
24423 cx,
24424 )
24425 })
24426 .unwrap()
24427 .await
24428 .downcast::<Editor>()
24429 .unwrap();
24430 pane_1.update(cx, |pane, cx| {
24431 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24432 open_editor.update(cx, |editor, cx| {
24433 assert_eq!(
24434 editor.display_text(cx),
24435 main_text,
24436 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24437 );
24438 assert_eq!(
24439 editor
24440 .selections
24441 .all::<Point>(&editor.display_snapshot(cx))
24442 .into_iter()
24443 .map(|s| s.range())
24444 .collect::<Vec<_>>(),
24445 expected_ranges,
24446 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24447 );
24448 })
24449 });
24450 pane_2.update(cx, |pane, cx| {
24451 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24452 open_editor.update(cx, |editor, cx| {
24453 assert_eq!(
24454 editor.display_text(cx),
24455 r#"fn main() {
24456⋯rintln!("1");
24457⋯intln!("2");
24458⋯ntln!("3");
24459println!("4");
24460println!("5");
24461}"#,
24462 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24463 );
24464 assert_eq!(
24465 editor
24466 .selections
24467 .all::<Point>(&editor.display_snapshot(cx))
24468 .into_iter()
24469 .map(|s| s.range())
24470 .collect::<Vec<_>>(),
24471 vec![Point::zero()..Point::zero()],
24472 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24473 );
24474 })
24475 });
24476}
24477
24478#[gpui::test]
24479async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24480 init_test(cx, |_| {});
24481
24482 let fs = FakeFs::new(cx.executor());
24483 let main_text = r#"fn main() {
24484println!("1");
24485println!("2");
24486println!("3");
24487println!("4");
24488println!("5");
24489}"#;
24490 let lib_text = "mod foo {}";
24491 fs.insert_tree(
24492 path!("/a"),
24493 json!({
24494 "lib.rs": lib_text,
24495 "main.rs": main_text,
24496 }),
24497 )
24498 .await;
24499
24500 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24501 let (workspace, cx) =
24502 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24503 let worktree_id = workspace.update(cx, |workspace, cx| {
24504 workspace.project().update(cx, |project, cx| {
24505 project.worktrees(cx).next().unwrap().read(cx).id()
24506 })
24507 });
24508
24509 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24510 let editor = workspace
24511 .update_in(cx, |workspace, window, cx| {
24512 workspace.open_path(
24513 (worktree_id, rel_path("main.rs")),
24514 Some(pane.downgrade()),
24515 true,
24516 window,
24517 cx,
24518 )
24519 })
24520 .unwrap()
24521 .await
24522 .downcast::<Editor>()
24523 .unwrap();
24524 pane.update(cx, |pane, cx| {
24525 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24526 open_editor.update(cx, |editor, cx| {
24527 assert_eq!(
24528 editor.display_text(cx),
24529 main_text,
24530 "Original main.rs text on initial open",
24531 );
24532 })
24533 });
24534 editor.update_in(cx, |editor, window, cx| {
24535 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24536 });
24537
24538 cx.update_global(|store: &mut SettingsStore, cx| {
24539 store.update_user_settings(cx, |s| {
24540 s.workspace.restore_on_file_reopen = Some(false);
24541 });
24542 });
24543 editor.update_in(cx, |editor, window, cx| {
24544 editor.fold_ranges(
24545 vec![
24546 Point::new(1, 0)..Point::new(1, 1),
24547 Point::new(2, 0)..Point::new(2, 2),
24548 Point::new(3, 0)..Point::new(3, 3),
24549 ],
24550 false,
24551 window,
24552 cx,
24553 );
24554 });
24555 pane.update_in(cx, |pane, window, cx| {
24556 pane.close_all_items(&CloseAllItems::default(), window, cx)
24557 })
24558 .await
24559 .unwrap();
24560 pane.update(cx, |pane, _| {
24561 assert!(pane.active_item().is_none());
24562 });
24563 cx.update_global(|store: &mut SettingsStore, cx| {
24564 store.update_user_settings(cx, |s| {
24565 s.workspace.restore_on_file_reopen = Some(true);
24566 });
24567 });
24568
24569 let _editor_reopened = workspace
24570 .update_in(cx, |workspace, window, cx| {
24571 workspace.open_path(
24572 (worktree_id, rel_path("main.rs")),
24573 Some(pane.downgrade()),
24574 true,
24575 window,
24576 cx,
24577 )
24578 })
24579 .unwrap()
24580 .await
24581 .downcast::<Editor>()
24582 .unwrap();
24583 pane.update(cx, |pane, cx| {
24584 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24585 open_editor.update(cx, |editor, cx| {
24586 assert_eq!(
24587 editor.display_text(cx),
24588 main_text,
24589 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24590 );
24591 })
24592 });
24593}
24594
24595#[gpui::test]
24596async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24597 struct EmptyModalView {
24598 focus_handle: gpui::FocusHandle,
24599 }
24600 impl EventEmitter<DismissEvent> for EmptyModalView {}
24601 impl Render for EmptyModalView {
24602 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24603 div()
24604 }
24605 }
24606 impl Focusable for EmptyModalView {
24607 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24608 self.focus_handle.clone()
24609 }
24610 }
24611 impl workspace::ModalView for EmptyModalView {}
24612 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24613 EmptyModalView {
24614 focus_handle: cx.focus_handle(),
24615 }
24616 }
24617
24618 init_test(cx, |_| {});
24619
24620 let fs = FakeFs::new(cx.executor());
24621 let project = Project::test(fs, [], cx).await;
24622 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24623 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24624 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24625 let editor = cx.new_window_entity(|window, cx| {
24626 Editor::new(
24627 EditorMode::full(),
24628 buffer,
24629 Some(project.clone()),
24630 window,
24631 cx,
24632 )
24633 });
24634 workspace
24635 .update(cx, |workspace, window, cx| {
24636 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24637 })
24638 .unwrap();
24639 editor.update_in(cx, |editor, window, cx| {
24640 editor.open_context_menu(&OpenContextMenu, window, cx);
24641 assert!(editor.mouse_context_menu.is_some());
24642 });
24643 workspace
24644 .update(cx, |workspace, window, cx| {
24645 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24646 })
24647 .unwrap();
24648 cx.read(|cx| {
24649 assert!(editor.read(cx).mouse_context_menu.is_none());
24650 });
24651}
24652
24653fn set_linked_edit_ranges(
24654 opening: (Point, Point),
24655 closing: (Point, Point),
24656 editor: &mut Editor,
24657 cx: &mut Context<Editor>,
24658) {
24659 let Some((buffer, _)) = editor
24660 .buffer
24661 .read(cx)
24662 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24663 else {
24664 panic!("Failed to get buffer for selection position");
24665 };
24666 let buffer = buffer.read(cx);
24667 let buffer_id = buffer.remote_id();
24668 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24669 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24670 let mut linked_ranges = HashMap::default();
24671 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24672 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24673}
24674
24675#[gpui::test]
24676async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24677 init_test(cx, |_| {});
24678
24679 let fs = FakeFs::new(cx.executor());
24680 fs.insert_file(path!("/file.html"), Default::default())
24681 .await;
24682
24683 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24684
24685 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24686 let html_language = Arc::new(Language::new(
24687 LanguageConfig {
24688 name: "HTML".into(),
24689 matcher: LanguageMatcher {
24690 path_suffixes: vec!["html".to_string()],
24691 ..LanguageMatcher::default()
24692 },
24693 brackets: BracketPairConfig {
24694 pairs: vec![BracketPair {
24695 start: "<".into(),
24696 end: ">".into(),
24697 close: true,
24698 ..Default::default()
24699 }],
24700 ..Default::default()
24701 },
24702 ..Default::default()
24703 },
24704 Some(tree_sitter_html::LANGUAGE.into()),
24705 ));
24706 language_registry.add(html_language);
24707 let mut fake_servers = language_registry.register_fake_lsp(
24708 "HTML",
24709 FakeLspAdapter {
24710 capabilities: lsp::ServerCapabilities {
24711 completion_provider: Some(lsp::CompletionOptions {
24712 resolve_provider: Some(true),
24713 ..Default::default()
24714 }),
24715 ..Default::default()
24716 },
24717 ..Default::default()
24718 },
24719 );
24720
24721 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24722 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24723
24724 let worktree_id = workspace
24725 .update(cx, |workspace, _window, cx| {
24726 workspace.project().update(cx, |project, cx| {
24727 project.worktrees(cx).next().unwrap().read(cx).id()
24728 })
24729 })
24730 .unwrap();
24731 project
24732 .update(cx, |project, cx| {
24733 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24734 })
24735 .await
24736 .unwrap();
24737 let editor = workspace
24738 .update(cx, |workspace, window, cx| {
24739 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24740 })
24741 .unwrap()
24742 .await
24743 .unwrap()
24744 .downcast::<Editor>()
24745 .unwrap();
24746
24747 let fake_server = fake_servers.next().await.unwrap();
24748 editor.update_in(cx, |editor, window, cx| {
24749 editor.set_text("<ad></ad>", window, cx);
24750 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24751 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24752 });
24753 set_linked_edit_ranges(
24754 (Point::new(0, 1), Point::new(0, 3)),
24755 (Point::new(0, 6), Point::new(0, 8)),
24756 editor,
24757 cx,
24758 );
24759 });
24760 let mut completion_handle =
24761 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24762 Ok(Some(lsp::CompletionResponse::Array(vec![
24763 lsp::CompletionItem {
24764 label: "head".to_string(),
24765 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24766 lsp::InsertReplaceEdit {
24767 new_text: "head".to_string(),
24768 insert: lsp::Range::new(
24769 lsp::Position::new(0, 1),
24770 lsp::Position::new(0, 3),
24771 ),
24772 replace: lsp::Range::new(
24773 lsp::Position::new(0, 1),
24774 lsp::Position::new(0, 3),
24775 ),
24776 },
24777 )),
24778 ..Default::default()
24779 },
24780 ])))
24781 });
24782 editor.update_in(cx, |editor, window, cx| {
24783 editor.show_completions(&ShowCompletions, window, cx);
24784 });
24785 cx.run_until_parked();
24786 completion_handle.next().await.unwrap();
24787 editor.update(cx, |editor, _| {
24788 assert!(
24789 editor.context_menu_visible(),
24790 "Completion menu should be visible"
24791 );
24792 });
24793 editor.update_in(cx, |editor, window, cx| {
24794 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24795 });
24796 cx.executor().run_until_parked();
24797 editor.update(cx, |editor, cx| {
24798 assert_eq!(editor.text(cx), "<head></head>");
24799 });
24800}
24801
24802#[gpui::test]
24803async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24804 init_test(cx, |_| {});
24805
24806 let mut cx = EditorTestContext::new(cx).await;
24807 let language = Arc::new(Language::new(
24808 LanguageConfig {
24809 name: "TSX".into(),
24810 matcher: LanguageMatcher {
24811 path_suffixes: vec!["tsx".to_string()],
24812 ..LanguageMatcher::default()
24813 },
24814 brackets: BracketPairConfig {
24815 pairs: vec![BracketPair {
24816 start: "<".into(),
24817 end: ">".into(),
24818 close: true,
24819 ..Default::default()
24820 }],
24821 ..Default::default()
24822 },
24823 linked_edit_characters: HashSet::from_iter(['.']),
24824 ..Default::default()
24825 },
24826 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24827 ));
24828 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24829
24830 // Test typing > does not extend linked pair
24831 cx.set_state("<divˇ<div></div>");
24832 cx.update_editor(|editor, _, cx| {
24833 set_linked_edit_ranges(
24834 (Point::new(0, 1), Point::new(0, 4)),
24835 (Point::new(0, 11), Point::new(0, 14)),
24836 editor,
24837 cx,
24838 );
24839 });
24840 cx.update_editor(|editor, window, cx| {
24841 editor.handle_input(">", window, cx);
24842 });
24843 cx.assert_editor_state("<div>ˇ<div></div>");
24844
24845 // Test typing . do extend linked pair
24846 cx.set_state("<Animatedˇ></Animated>");
24847 cx.update_editor(|editor, _, cx| {
24848 set_linked_edit_ranges(
24849 (Point::new(0, 1), Point::new(0, 9)),
24850 (Point::new(0, 12), Point::new(0, 20)),
24851 editor,
24852 cx,
24853 );
24854 });
24855 cx.update_editor(|editor, window, cx| {
24856 editor.handle_input(".", window, cx);
24857 });
24858 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24859 cx.update_editor(|editor, _, cx| {
24860 set_linked_edit_ranges(
24861 (Point::new(0, 1), Point::new(0, 10)),
24862 (Point::new(0, 13), Point::new(0, 21)),
24863 editor,
24864 cx,
24865 );
24866 });
24867 cx.update_editor(|editor, window, cx| {
24868 editor.handle_input("V", window, cx);
24869 });
24870 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24871}
24872
24873#[gpui::test]
24874async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24875 init_test(cx, |_| {});
24876
24877 let fs = FakeFs::new(cx.executor());
24878 fs.insert_tree(
24879 path!("/root"),
24880 json!({
24881 "a": {
24882 "main.rs": "fn main() {}",
24883 },
24884 "foo": {
24885 "bar": {
24886 "external_file.rs": "pub mod external {}",
24887 }
24888 }
24889 }),
24890 )
24891 .await;
24892
24893 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24894 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24895 language_registry.add(rust_lang());
24896 let _fake_servers = language_registry.register_fake_lsp(
24897 "Rust",
24898 FakeLspAdapter {
24899 ..FakeLspAdapter::default()
24900 },
24901 );
24902 let (workspace, cx) =
24903 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24904 let worktree_id = workspace.update(cx, |workspace, cx| {
24905 workspace.project().update(cx, |project, cx| {
24906 project.worktrees(cx).next().unwrap().read(cx).id()
24907 })
24908 });
24909
24910 let assert_language_servers_count =
24911 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24912 project.update(cx, |project, cx| {
24913 let current = project
24914 .lsp_store()
24915 .read(cx)
24916 .as_local()
24917 .unwrap()
24918 .language_servers
24919 .len();
24920 assert_eq!(expected, current, "{context}");
24921 });
24922 };
24923
24924 assert_language_servers_count(
24925 0,
24926 "No servers should be running before any file is open",
24927 cx,
24928 );
24929 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24930 let main_editor = workspace
24931 .update_in(cx, |workspace, window, cx| {
24932 workspace.open_path(
24933 (worktree_id, rel_path("main.rs")),
24934 Some(pane.downgrade()),
24935 true,
24936 window,
24937 cx,
24938 )
24939 })
24940 .unwrap()
24941 .await
24942 .downcast::<Editor>()
24943 .unwrap();
24944 pane.update(cx, |pane, cx| {
24945 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24946 open_editor.update(cx, |editor, cx| {
24947 assert_eq!(
24948 editor.display_text(cx),
24949 "fn main() {}",
24950 "Original main.rs text on initial open",
24951 );
24952 });
24953 assert_eq!(open_editor, main_editor);
24954 });
24955 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24956
24957 let external_editor = workspace
24958 .update_in(cx, |workspace, window, cx| {
24959 workspace.open_abs_path(
24960 PathBuf::from("/root/foo/bar/external_file.rs"),
24961 OpenOptions::default(),
24962 window,
24963 cx,
24964 )
24965 })
24966 .await
24967 .expect("opening external file")
24968 .downcast::<Editor>()
24969 .expect("downcasted external file's open element to editor");
24970 pane.update(cx, |pane, cx| {
24971 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24972 open_editor.update(cx, |editor, cx| {
24973 assert_eq!(
24974 editor.display_text(cx),
24975 "pub mod external {}",
24976 "External file is open now",
24977 );
24978 });
24979 assert_eq!(open_editor, external_editor);
24980 });
24981 assert_language_servers_count(
24982 1,
24983 "Second, external, *.rs file should join the existing server",
24984 cx,
24985 );
24986
24987 pane.update_in(cx, |pane, window, cx| {
24988 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24989 })
24990 .await
24991 .unwrap();
24992 pane.update_in(cx, |pane, window, cx| {
24993 pane.navigate_backward(&Default::default(), window, cx);
24994 });
24995 cx.run_until_parked();
24996 pane.update(cx, |pane, cx| {
24997 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24998 open_editor.update(cx, |editor, cx| {
24999 assert_eq!(
25000 editor.display_text(cx),
25001 "pub mod external {}",
25002 "External file is open now",
25003 );
25004 });
25005 });
25006 assert_language_servers_count(
25007 1,
25008 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25009 cx,
25010 );
25011
25012 cx.update(|_, cx| {
25013 workspace::reload(cx);
25014 });
25015 assert_language_servers_count(
25016 1,
25017 "After reloading the worktree with local and external files opened, only one project should be started",
25018 cx,
25019 );
25020}
25021
25022#[gpui::test]
25023async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25024 init_test(cx, |_| {});
25025
25026 let mut cx = EditorTestContext::new(cx).await;
25027 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25028 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25029
25030 // test cursor move to start of each line on tab
25031 // for `if`, `elif`, `else`, `while`, `with` and `for`
25032 cx.set_state(indoc! {"
25033 def main():
25034 ˇ for item in items:
25035 ˇ while item.active:
25036 ˇ if item.value > 10:
25037 ˇ continue
25038 ˇ elif item.value < 0:
25039 ˇ break
25040 ˇ else:
25041 ˇ with item.context() as ctx:
25042 ˇ yield count
25043 ˇ else:
25044 ˇ log('while else')
25045 ˇ else:
25046 ˇ log('for else')
25047 "});
25048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25049 cx.assert_editor_state(indoc! {"
25050 def main():
25051 ˇfor item in items:
25052 ˇwhile item.active:
25053 ˇif item.value > 10:
25054 ˇcontinue
25055 ˇelif item.value < 0:
25056 ˇbreak
25057 ˇelse:
25058 ˇwith item.context() as ctx:
25059 ˇyield count
25060 ˇelse:
25061 ˇlog('while else')
25062 ˇelse:
25063 ˇlog('for else')
25064 "});
25065 // test relative indent is preserved when tab
25066 // for `if`, `elif`, `else`, `while`, `with` and `for`
25067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25068 cx.assert_editor_state(indoc! {"
25069 def main():
25070 ˇfor item in items:
25071 ˇwhile item.active:
25072 ˇif item.value > 10:
25073 ˇcontinue
25074 ˇelif item.value < 0:
25075 ˇbreak
25076 ˇelse:
25077 ˇwith item.context() as ctx:
25078 ˇyield count
25079 ˇelse:
25080 ˇlog('while else')
25081 ˇelse:
25082 ˇlog('for else')
25083 "});
25084
25085 // test cursor move to start of each line on tab
25086 // for `try`, `except`, `else`, `finally`, `match` and `def`
25087 cx.set_state(indoc! {"
25088 def main():
25089 ˇ try:
25090 ˇ fetch()
25091 ˇ except ValueError:
25092 ˇ handle_error()
25093 ˇ else:
25094 ˇ match value:
25095 ˇ case _:
25096 ˇ finally:
25097 ˇ def status():
25098 ˇ return 0
25099 "});
25100 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25101 cx.assert_editor_state(indoc! {"
25102 def main():
25103 ˇtry:
25104 ˇfetch()
25105 ˇexcept ValueError:
25106 ˇhandle_error()
25107 ˇelse:
25108 ˇmatch value:
25109 ˇcase _:
25110 ˇfinally:
25111 ˇdef status():
25112 ˇreturn 0
25113 "});
25114 // test relative indent is preserved when tab
25115 // for `try`, `except`, `else`, `finally`, `match` and `def`
25116 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25117 cx.assert_editor_state(indoc! {"
25118 def main():
25119 ˇtry:
25120 ˇfetch()
25121 ˇexcept ValueError:
25122 ˇhandle_error()
25123 ˇelse:
25124 ˇmatch value:
25125 ˇcase _:
25126 ˇfinally:
25127 ˇdef status():
25128 ˇreturn 0
25129 "});
25130}
25131
25132#[gpui::test]
25133async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25134 init_test(cx, |_| {});
25135
25136 let mut cx = EditorTestContext::new(cx).await;
25137 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25138 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25139
25140 // test `else` auto outdents when typed inside `if` block
25141 cx.set_state(indoc! {"
25142 def main():
25143 if i == 2:
25144 return
25145 ˇ
25146 "});
25147 cx.update_editor(|editor, window, cx| {
25148 editor.handle_input("else:", window, cx);
25149 });
25150 cx.assert_editor_state(indoc! {"
25151 def main():
25152 if i == 2:
25153 return
25154 else:ˇ
25155 "});
25156
25157 // test `except` auto outdents when typed inside `try` block
25158 cx.set_state(indoc! {"
25159 def main():
25160 try:
25161 i = 2
25162 ˇ
25163 "});
25164 cx.update_editor(|editor, window, cx| {
25165 editor.handle_input("except:", window, cx);
25166 });
25167 cx.assert_editor_state(indoc! {"
25168 def main():
25169 try:
25170 i = 2
25171 except:ˇ
25172 "});
25173
25174 // test `else` auto outdents when typed inside `except` block
25175 cx.set_state(indoc! {"
25176 def main():
25177 try:
25178 i = 2
25179 except:
25180 j = 2
25181 ˇ
25182 "});
25183 cx.update_editor(|editor, window, cx| {
25184 editor.handle_input("else:", window, cx);
25185 });
25186 cx.assert_editor_state(indoc! {"
25187 def main():
25188 try:
25189 i = 2
25190 except:
25191 j = 2
25192 else:ˇ
25193 "});
25194
25195 // test `finally` auto outdents when typed inside `else` block
25196 cx.set_state(indoc! {"
25197 def main():
25198 try:
25199 i = 2
25200 except:
25201 j = 2
25202 else:
25203 k = 2
25204 ˇ
25205 "});
25206 cx.update_editor(|editor, window, cx| {
25207 editor.handle_input("finally:", window, cx);
25208 });
25209 cx.assert_editor_state(indoc! {"
25210 def main():
25211 try:
25212 i = 2
25213 except:
25214 j = 2
25215 else:
25216 k = 2
25217 finally:ˇ
25218 "});
25219
25220 // test `else` does not outdents when typed inside `except` block right after for block
25221 cx.set_state(indoc! {"
25222 def main():
25223 try:
25224 i = 2
25225 except:
25226 for i in range(n):
25227 pass
25228 ˇ
25229 "});
25230 cx.update_editor(|editor, window, cx| {
25231 editor.handle_input("else:", window, cx);
25232 });
25233 cx.assert_editor_state(indoc! {"
25234 def main():
25235 try:
25236 i = 2
25237 except:
25238 for i in range(n):
25239 pass
25240 else:ˇ
25241 "});
25242
25243 // test `finally` auto outdents when typed inside `else` block right after for block
25244 cx.set_state(indoc! {"
25245 def main():
25246 try:
25247 i = 2
25248 except:
25249 j = 2
25250 else:
25251 for i in range(n):
25252 pass
25253 ˇ
25254 "});
25255 cx.update_editor(|editor, window, cx| {
25256 editor.handle_input("finally:", window, cx);
25257 });
25258 cx.assert_editor_state(indoc! {"
25259 def main():
25260 try:
25261 i = 2
25262 except:
25263 j = 2
25264 else:
25265 for i in range(n):
25266 pass
25267 finally:ˇ
25268 "});
25269
25270 // test `except` outdents to inner "try" block
25271 cx.set_state(indoc! {"
25272 def main():
25273 try:
25274 i = 2
25275 if i == 2:
25276 try:
25277 i = 3
25278 ˇ
25279 "});
25280 cx.update_editor(|editor, window, cx| {
25281 editor.handle_input("except:", window, cx);
25282 });
25283 cx.assert_editor_state(indoc! {"
25284 def main():
25285 try:
25286 i = 2
25287 if i == 2:
25288 try:
25289 i = 3
25290 except:ˇ
25291 "});
25292
25293 // test `except` outdents to outer "try" block
25294 cx.set_state(indoc! {"
25295 def main():
25296 try:
25297 i = 2
25298 if i == 2:
25299 try:
25300 i = 3
25301 ˇ
25302 "});
25303 cx.update_editor(|editor, window, cx| {
25304 editor.handle_input("except:", window, cx);
25305 });
25306 cx.assert_editor_state(indoc! {"
25307 def main():
25308 try:
25309 i = 2
25310 if i == 2:
25311 try:
25312 i = 3
25313 except:ˇ
25314 "});
25315
25316 // test `else` stays at correct indent when typed after `for` block
25317 cx.set_state(indoc! {"
25318 def main():
25319 for i in range(10):
25320 if i == 3:
25321 break
25322 ˇ
25323 "});
25324 cx.update_editor(|editor, window, cx| {
25325 editor.handle_input("else:", window, cx);
25326 });
25327 cx.assert_editor_state(indoc! {"
25328 def main():
25329 for i in range(10):
25330 if i == 3:
25331 break
25332 else:ˇ
25333 "});
25334
25335 // test does not outdent on typing after line with square brackets
25336 cx.set_state(indoc! {"
25337 def f() -> list[str]:
25338 ˇ
25339 "});
25340 cx.update_editor(|editor, window, cx| {
25341 editor.handle_input("a", window, cx);
25342 });
25343 cx.assert_editor_state(indoc! {"
25344 def f() -> list[str]:
25345 aˇ
25346 "});
25347
25348 // test does not outdent on typing : after case keyword
25349 cx.set_state(indoc! {"
25350 match 1:
25351 caseˇ
25352 "});
25353 cx.update_editor(|editor, window, cx| {
25354 editor.handle_input(":", window, cx);
25355 });
25356 cx.assert_editor_state(indoc! {"
25357 match 1:
25358 case:ˇ
25359 "});
25360}
25361
25362#[gpui::test]
25363async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25364 init_test(cx, |_| {});
25365 update_test_language_settings(cx, |settings| {
25366 settings.defaults.extend_comment_on_newline = Some(false);
25367 });
25368 let mut cx = EditorTestContext::new(cx).await;
25369 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25371
25372 // test correct indent after newline on comment
25373 cx.set_state(indoc! {"
25374 # COMMENT:ˇ
25375 "});
25376 cx.update_editor(|editor, window, cx| {
25377 editor.newline(&Newline, window, cx);
25378 });
25379 cx.assert_editor_state(indoc! {"
25380 # COMMENT:
25381 ˇ
25382 "});
25383
25384 // test correct indent after newline in brackets
25385 cx.set_state(indoc! {"
25386 {ˇ}
25387 "});
25388 cx.update_editor(|editor, window, cx| {
25389 editor.newline(&Newline, window, cx);
25390 });
25391 cx.run_until_parked();
25392 cx.assert_editor_state(indoc! {"
25393 {
25394 ˇ
25395 }
25396 "});
25397
25398 cx.set_state(indoc! {"
25399 (ˇ)
25400 "});
25401 cx.update_editor(|editor, window, cx| {
25402 editor.newline(&Newline, window, cx);
25403 });
25404 cx.run_until_parked();
25405 cx.assert_editor_state(indoc! {"
25406 (
25407 ˇ
25408 )
25409 "});
25410
25411 // do not indent after empty lists or dictionaries
25412 cx.set_state(indoc! {"
25413 a = []ˇ
25414 "});
25415 cx.update_editor(|editor, window, cx| {
25416 editor.newline(&Newline, window, cx);
25417 });
25418 cx.run_until_parked();
25419 cx.assert_editor_state(indoc! {"
25420 a = []
25421 ˇ
25422 "});
25423}
25424
25425#[gpui::test]
25426async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25427 init_test(cx, |_| {});
25428
25429 let mut cx = EditorTestContext::new(cx).await;
25430 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25431 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25432
25433 // test cursor move to start of each line on tab
25434 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25435 cx.set_state(indoc! {"
25436 function main() {
25437 ˇ for item in $items; do
25438 ˇ while [ -n \"$item\" ]; do
25439 ˇ if [ \"$value\" -gt 10 ]; then
25440 ˇ continue
25441 ˇ elif [ \"$value\" -lt 0 ]; then
25442 ˇ break
25443 ˇ else
25444 ˇ echo \"$item\"
25445 ˇ fi
25446 ˇ done
25447 ˇ done
25448 ˇ}
25449 "});
25450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25451 cx.assert_editor_state(indoc! {"
25452 function main() {
25453 ˇfor item in $items; do
25454 ˇwhile [ -n \"$item\" ]; do
25455 ˇif [ \"$value\" -gt 10 ]; then
25456 ˇcontinue
25457 ˇelif [ \"$value\" -lt 0 ]; then
25458 ˇbreak
25459 ˇelse
25460 ˇecho \"$item\"
25461 ˇfi
25462 ˇdone
25463 ˇdone
25464 ˇ}
25465 "});
25466 // test relative indent is preserved when tab
25467 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25468 cx.assert_editor_state(indoc! {"
25469 function main() {
25470 ˇfor item in $items; do
25471 ˇwhile [ -n \"$item\" ]; do
25472 ˇif [ \"$value\" -gt 10 ]; then
25473 ˇcontinue
25474 ˇelif [ \"$value\" -lt 0 ]; then
25475 ˇbreak
25476 ˇelse
25477 ˇecho \"$item\"
25478 ˇfi
25479 ˇdone
25480 ˇdone
25481 ˇ}
25482 "});
25483
25484 // test cursor move to start of each line on tab
25485 // for `case` statement with patterns
25486 cx.set_state(indoc! {"
25487 function handle() {
25488 ˇ case \"$1\" in
25489 ˇ start)
25490 ˇ echo \"a\"
25491 ˇ ;;
25492 ˇ stop)
25493 ˇ echo \"b\"
25494 ˇ ;;
25495 ˇ *)
25496 ˇ echo \"c\"
25497 ˇ ;;
25498 ˇ esac
25499 ˇ}
25500 "});
25501 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25502 cx.assert_editor_state(indoc! {"
25503 function handle() {
25504 ˇcase \"$1\" in
25505 ˇstart)
25506 ˇecho \"a\"
25507 ˇ;;
25508 ˇstop)
25509 ˇecho \"b\"
25510 ˇ;;
25511 ˇ*)
25512 ˇecho \"c\"
25513 ˇ;;
25514 ˇesac
25515 ˇ}
25516 "});
25517}
25518
25519#[gpui::test]
25520async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25521 init_test(cx, |_| {});
25522
25523 let mut cx = EditorTestContext::new(cx).await;
25524 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25525 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25526
25527 // test indents on comment insert
25528 cx.set_state(indoc! {"
25529 function main() {
25530 ˇ for item in $items; do
25531 ˇ while [ -n \"$item\" ]; do
25532 ˇ if [ \"$value\" -gt 10 ]; then
25533 ˇ continue
25534 ˇ elif [ \"$value\" -lt 0 ]; then
25535 ˇ break
25536 ˇ else
25537 ˇ echo \"$item\"
25538 ˇ fi
25539 ˇ done
25540 ˇ done
25541 ˇ}
25542 "});
25543 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25544 cx.assert_editor_state(indoc! {"
25545 function main() {
25546 #ˇ for item in $items; do
25547 #ˇ while [ -n \"$item\" ]; do
25548 #ˇ if [ \"$value\" -gt 10 ]; then
25549 #ˇ continue
25550 #ˇ elif [ \"$value\" -lt 0 ]; then
25551 #ˇ break
25552 #ˇ else
25553 #ˇ echo \"$item\"
25554 #ˇ fi
25555 #ˇ done
25556 #ˇ done
25557 #ˇ}
25558 "});
25559}
25560
25561#[gpui::test]
25562async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25563 init_test(cx, |_| {});
25564
25565 let mut cx = EditorTestContext::new(cx).await;
25566 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25567 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25568
25569 // test `else` auto outdents when typed inside `if` block
25570 cx.set_state(indoc! {"
25571 if [ \"$1\" = \"test\" ]; then
25572 echo \"foo bar\"
25573 ˇ
25574 "});
25575 cx.update_editor(|editor, window, cx| {
25576 editor.handle_input("else", window, cx);
25577 });
25578 cx.assert_editor_state(indoc! {"
25579 if [ \"$1\" = \"test\" ]; then
25580 echo \"foo bar\"
25581 elseˇ
25582 "});
25583
25584 // test `elif` auto outdents when typed inside `if` block
25585 cx.set_state(indoc! {"
25586 if [ \"$1\" = \"test\" ]; then
25587 echo \"foo bar\"
25588 ˇ
25589 "});
25590 cx.update_editor(|editor, window, cx| {
25591 editor.handle_input("elif", window, cx);
25592 });
25593 cx.assert_editor_state(indoc! {"
25594 if [ \"$1\" = \"test\" ]; then
25595 echo \"foo bar\"
25596 elifˇ
25597 "});
25598
25599 // test `fi` auto outdents when typed inside `else` block
25600 cx.set_state(indoc! {"
25601 if [ \"$1\" = \"test\" ]; then
25602 echo \"foo bar\"
25603 else
25604 echo \"bar baz\"
25605 ˇ
25606 "});
25607 cx.update_editor(|editor, window, cx| {
25608 editor.handle_input("fi", window, cx);
25609 });
25610 cx.assert_editor_state(indoc! {"
25611 if [ \"$1\" = \"test\" ]; then
25612 echo \"foo bar\"
25613 else
25614 echo \"bar baz\"
25615 fiˇ
25616 "});
25617
25618 // test `done` auto outdents when typed inside `while` block
25619 cx.set_state(indoc! {"
25620 while read line; do
25621 echo \"$line\"
25622 ˇ
25623 "});
25624 cx.update_editor(|editor, window, cx| {
25625 editor.handle_input("done", window, cx);
25626 });
25627 cx.assert_editor_state(indoc! {"
25628 while read line; do
25629 echo \"$line\"
25630 doneˇ
25631 "});
25632
25633 // test `done` auto outdents when typed inside `for` block
25634 cx.set_state(indoc! {"
25635 for file in *.txt; do
25636 cat \"$file\"
25637 ˇ
25638 "});
25639 cx.update_editor(|editor, window, cx| {
25640 editor.handle_input("done", window, cx);
25641 });
25642 cx.assert_editor_state(indoc! {"
25643 for file in *.txt; do
25644 cat \"$file\"
25645 doneˇ
25646 "});
25647
25648 // test `esac` auto outdents when typed inside `case` block
25649 cx.set_state(indoc! {"
25650 case \"$1\" in
25651 start)
25652 echo \"foo bar\"
25653 ;;
25654 stop)
25655 echo \"bar baz\"
25656 ;;
25657 ˇ
25658 "});
25659 cx.update_editor(|editor, window, cx| {
25660 editor.handle_input("esac", window, cx);
25661 });
25662 cx.assert_editor_state(indoc! {"
25663 case \"$1\" in
25664 start)
25665 echo \"foo bar\"
25666 ;;
25667 stop)
25668 echo \"bar baz\"
25669 ;;
25670 esacˇ
25671 "});
25672
25673 // test `*)` auto outdents when typed inside `case` block
25674 cx.set_state(indoc! {"
25675 case \"$1\" in
25676 start)
25677 echo \"foo bar\"
25678 ;;
25679 ˇ
25680 "});
25681 cx.update_editor(|editor, window, cx| {
25682 editor.handle_input("*)", window, cx);
25683 });
25684 cx.assert_editor_state(indoc! {"
25685 case \"$1\" in
25686 start)
25687 echo \"foo bar\"
25688 ;;
25689 *)ˇ
25690 "});
25691
25692 // test `fi` outdents to correct level with nested if blocks
25693 cx.set_state(indoc! {"
25694 if [ \"$1\" = \"test\" ]; then
25695 echo \"outer if\"
25696 if [ \"$2\" = \"debug\" ]; then
25697 echo \"inner if\"
25698 ˇ
25699 "});
25700 cx.update_editor(|editor, window, cx| {
25701 editor.handle_input("fi", window, cx);
25702 });
25703 cx.assert_editor_state(indoc! {"
25704 if [ \"$1\" = \"test\" ]; then
25705 echo \"outer if\"
25706 if [ \"$2\" = \"debug\" ]; then
25707 echo \"inner if\"
25708 fiˇ
25709 "});
25710}
25711
25712#[gpui::test]
25713async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25714 init_test(cx, |_| {});
25715 update_test_language_settings(cx, |settings| {
25716 settings.defaults.extend_comment_on_newline = Some(false);
25717 });
25718 let mut cx = EditorTestContext::new(cx).await;
25719 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25721
25722 // test correct indent after newline on comment
25723 cx.set_state(indoc! {"
25724 # COMMENT:ˇ
25725 "});
25726 cx.update_editor(|editor, window, cx| {
25727 editor.newline(&Newline, window, cx);
25728 });
25729 cx.assert_editor_state(indoc! {"
25730 # COMMENT:
25731 ˇ
25732 "});
25733
25734 // test correct indent after newline after `then`
25735 cx.set_state(indoc! {"
25736
25737 if [ \"$1\" = \"test\" ]; thenˇ
25738 "});
25739 cx.update_editor(|editor, window, cx| {
25740 editor.newline(&Newline, window, cx);
25741 });
25742 cx.run_until_parked();
25743 cx.assert_editor_state(indoc! {"
25744
25745 if [ \"$1\" = \"test\" ]; then
25746 ˇ
25747 "});
25748
25749 // test correct indent after newline after `else`
25750 cx.set_state(indoc! {"
25751 if [ \"$1\" = \"test\" ]; then
25752 elseˇ
25753 "});
25754 cx.update_editor(|editor, window, cx| {
25755 editor.newline(&Newline, window, cx);
25756 });
25757 cx.run_until_parked();
25758 cx.assert_editor_state(indoc! {"
25759 if [ \"$1\" = \"test\" ]; then
25760 else
25761 ˇ
25762 "});
25763
25764 // test correct indent after newline after `elif`
25765 cx.set_state(indoc! {"
25766 if [ \"$1\" = \"test\" ]; then
25767 elifˇ
25768 "});
25769 cx.update_editor(|editor, window, cx| {
25770 editor.newline(&Newline, window, cx);
25771 });
25772 cx.run_until_parked();
25773 cx.assert_editor_state(indoc! {"
25774 if [ \"$1\" = \"test\" ]; then
25775 elif
25776 ˇ
25777 "});
25778
25779 // test correct indent after newline after `do`
25780 cx.set_state(indoc! {"
25781 for file in *.txt; doˇ
25782 "});
25783 cx.update_editor(|editor, window, cx| {
25784 editor.newline(&Newline, window, cx);
25785 });
25786 cx.run_until_parked();
25787 cx.assert_editor_state(indoc! {"
25788 for file in *.txt; do
25789 ˇ
25790 "});
25791
25792 // test correct indent after newline after case pattern
25793 cx.set_state(indoc! {"
25794 case \"$1\" in
25795 start)ˇ
25796 "});
25797 cx.update_editor(|editor, window, cx| {
25798 editor.newline(&Newline, window, cx);
25799 });
25800 cx.run_until_parked();
25801 cx.assert_editor_state(indoc! {"
25802 case \"$1\" in
25803 start)
25804 ˇ
25805 "});
25806
25807 // test correct indent after newline after case pattern
25808 cx.set_state(indoc! {"
25809 case \"$1\" in
25810 start)
25811 ;;
25812 *)ˇ
25813 "});
25814 cx.update_editor(|editor, window, cx| {
25815 editor.newline(&Newline, window, cx);
25816 });
25817 cx.run_until_parked();
25818 cx.assert_editor_state(indoc! {"
25819 case \"$1\" in
25820 start)
25821 ;;
25822 *)
25823 ˇ
25824 "});
25825
25826 // test correct indent after newline after function opening brace
25827 cx.set_state(indoc! {"
25828 function test() {ˇ}
25829 "});
25830 cx.update_editor(|editor, window, cx| {
25831 editor.newline(&Newline, window, cx);
25832 });
25833 cx.run_until_parked();
25834 cx.assert_editor_state(indoc! {"
25835 function test() {
25836 ˇ
25837 }
25838 "});
25839
25840 // test no extra indent after semicolon on same line
25841 cx.set_state(indoc! {"
25842 echo \"test\";ˇ
25843 "});
25844 cx.update_editor(|editor, window, cx| {
25845 editor.newline(&Newline, window, cx);
25846 });
25847 cx.run_until_parked();
25848 cx.assert_editor_state(indoc! {"
25849 echo \"test\";
25850 ˇ
25851 "});
25852}
25853
25854fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25855 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25856 point..point
25857}
25858
25859#[track_caller]
25860fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25861 let (text, ranges) = marked_text_ranges(marked_text, true);
25862 assert_eq!(editor.text(cx), text);
25863 assert_eq!(
25864 editor.selections.ranges(&editor.display_snapshot(cx)),
25865 ranges
25866 .iter()
25867 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
25868 .collect::<Vec<_>>(),
25869 "Assert selections are {}",
25870 marked_text
25871 );
25872}
25873
25874pub fn handle_signature_help_request(
25875 cx: &mut EditorLspTestContext,
25876 mocked_response: lsp::SignatureHelp,
25877) -> impl Future<Output = ()> + use<> {
25878 let mut request =
25879 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25880 let mocked_response = mocked_response.clone();
25881 async move { Ok(Some(mocked_response)) }
25882 });
25883
25884 async move {
25885 request.next().await;
25886 }
25887}
25888
25889#[track_caller]
25890pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25891 cx.update_editor(|editor, _, _| {
25892 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25893 let entries = menu.entries.borrow();
25894 let entries = entries
25895 .iter()
25896 .map(|entry| entry.string.as_str())
25897 .collect::<Vec<_>>();
25898 assert_eq!(entries, expected);
25899 } else {
25900 panic!("Expected completions menu");
25901 }
25902 });
25903}
25904
25905#[gpui::test]
25906async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
25907 init_test(cx, |_| {});
25908 let mut cx = EditorLspTestContext::new_rust(
25909 lsp::ServerCapabilities {
25910 completion_provider: Some(lsp::CompletionOptions {
25911 ..Default::default()
25912 }),
25913 ..Default::default()
25914 },
25915 cx,
25916 )
25917 .await;
25918 cx.lsp
25919 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25920 Ok(Some(lsp::CompletionResponse::Array(vec![
25921 lsp::CompletionItem {
25922 label: "unsafe".into(),
25923 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25924 range: lsp::Range {
25925 start: lsp::Position {
25926 line: 0,
25927 character: 9,
25928 },
25929 end: lsp::Position {
25930 line: 0,
25931 character: 11,
25932 },
25933 },
25934 new_text: "unsafe".to_string(),
25935 })),
25936 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
25937 ..Default::default()
25938 },
25939 ])))
25940 });
25941
25942 cx.update_editor(|editor, _, cx| {
25943 editor.project().unwrap().update(cx, |project, cx| {
25944 project.snippets().update(cx, |snippets, _cx| {
25945 snippets.add_snippet_for_test(
25946 None,
25947 PathBuf::from("test_snippets.json"),
25948 vec![
25949 Arc::new(project::snippet_provider::Snippet {
25950 prefix: vec![
25951 "unlimited word count".to_string(),
25952 "unlimit word count".to_string(),
25953 "unlimited unknown".to_string(),
25954 ],
25955 body: "this is many words".to_string(),
25956 description: Some("description".to_string()),
25957 name: "multi-word snippet test".to_string(),
25958 }),
25959 Arc::new(project::snippet_provider::Snippet {
25960 prefix: vec!["unsnip".to_string(), "@few".to_string()],
25961 body: "fewer words".to_string(),
25962 description: Some("alt description".to_string()),
25963 name: "other name".to_string(),
25964 }),
25965 Arc::new(project::snippet_provider::Snippet {
25966 prefix: vec!["ab aa".to_string()],
25967 body: "abcd".to_string(),
25968 description: None,
25969 name: "alphabet".to_string(),
25970 }),
25971 ],
25972 );
25973 });
25974 })
25975 });
25976
25977 let get_completions = |cx: &mut EditorLspTestContext| {
25978 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
25979 Some(CodeContextMenu::Completions(context_menu)) => {
25980 let entries = context_menu.entries.borrow();
25981 entries
25982 .iter()
25983 .map(|entry| entry.string.clone())
25984 .collect_vec()
25985 }
25986 _ => vec![],
25987 })
25988 };
25989
25990 // snippets:
25991 // @foo
25992 // foo bar
25993 //
25994 // when typing:
25995 //
25996 // when typing:
25997 // - if I type a symbol "open the completions with snippets only"
25998 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
25999 //
26000 // stuff we need:
26001 // - filtering logic change?
26002 // - remember how far back the completion started.
26003
26004 let test_cases: &[(&str, &[&str])] = &[
26005 (
26006 "un",
26007 &[
26008 "unsafe",
26009 "unlimit word count",
26010 "unlimited unknown",
26011 "unlimited word count",
26012 "unsnip",
26013 ],
26014 ),
26015 (
26016 "u ",
26017 &[
26018 "unlimit word count",
26019 "unlimited unknown",
26020 "unlimited word count",
26021 ],
26022 ),
26023 ("u a", &["ab aa", "unsafe"]), // unsAfe
26024 (
26025 "u u",
26026 &[
26027 "unsafe",
26028 "unlimit word count",
26029 "unlimited unknown", // ranked highest among snippets
26030 "unlimited word count",
26031 "unsnip",
26032 ],
26033 ),
26034 ("uw c", &["unlimit word count", "unlimited word count"]),
26035 (
26036 "u w",
26037 &[
26038 "unlimit word count",
26039 "unlimited word count",
26040 "unlimited unknown",
26041 ],
26042 ),
26043 ("u w ", &["unlimit word count", "unlimited word count"]),
26044 (
26045 "u ",
26046 &[
26047 "unlimit word count",
26048 "unlimited unknown",
26049 "unlimited word count",
26050 ],
26051 ),
26052 ("wor", &[]),
26053 ("uf", &["unsafe"]),
26054 ("af", &["unsafe"]),
26055 ("afu", &[]),
26056 (
26057 "ue",
26058 &["unsafe", "unlimited unknown", "unlimited word count"],
26059 ),
26060 ("@", &["@few"]),
26061 ("@few", &["@few"]),
26062 ("@ ", &[]),
26063 ("a@", &["@few"]),
26064 ("a@f", &["@few", "unsafe"]),
26065 ("a@fw", &["@few"]),
26066 ("a", &["ab aa", "unsafe"]),
26067 ("aa", &["ab aa"]),
26068 ("aaa", &["ab aa"]),
26069 ("ab", &["ab aa"]),
26070 ("ab ", &["ab aa"]),
26071 ("ab a", &["ab aa", "unsafe"]),
26072 ("ab ab", &["ab aa"]),
26073 ("ab ab aa", &["ab aa"]),
26074 ];
26075
26076 for &(input_to_simulate, expected_completions) in test_cases {
26077 cx.set_state("fn a() { ˇ }\n");
26078 for c in input_to_simulate.split("") {
26079 cx.simulate_input(c);
26080 cx.run_until_parked();
26081 }
26082 let expected_completions = expected_completions
26083 .iter()
26084 .map(|s| s.to_string())
26085 .collect_vec();
26086 assert_eq!(
26087 get_completions(&mut cx),
26088 expected_completions,
26089 "< actual / expected >, input = {input_to_simulate:?}",
26090 );
26091 }
26092}
26093
26094/// Handle completion request passing a marked string specifying where the completion
26095/// should be triggered from using '|' character, what range should be replaced, and what completions
26096/// should be returned using '<' and '>' to delimit the range.
26097///
26098/// Also see `handle_completion_request_with_insert_and_replace`.
26099#[track_caller]
26100pub fn handle_completion_request(
26101 marked_string: &str,
26102 completions: Vec<&'static str>,
26103 is_incomplete: bool,
26104 counter: Arc<AtomicUsize>,
26105 cx: &mut EditorLspTestContext,
26106) -> impl Future<Output = ()> {
26107 let complete_from_marker: TextRangeMarker = '|'.into();
26108 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26109 let (_, mut marked_ranges) = marked_text_ranges_by(
26110 marked_string,
26111 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26112 );
26113
26114 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26115 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26116 ));
26117 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26118 let replace_range =
26119 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26120
26121 let mut request =
26122 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26123 let completions = completions.clone();
26124 counter.fetch_add(1, atomic::Ordering::Release);
26125 async move {
26126 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26127 assert_eq!(
26128 params.text_document_position.position,
26129 complete_from_position
26130 );
26131 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26132 is_incomplete,
26133 item_defaults: None,
26134 items: completions
26135 .iter()
26136 .map(|completion_text| lsp::CompletionItem {
26137 label: completion_text.to_string(),
26138 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26139 range: replace_range,
26140 new_text: completion_text.to_string(),
26141 })),
26142 ..Default::default()
26143 })
26144 .collect(),
26145 })))
26146 }
26147 });
26148
26149 async move {
26150 request.next().await;
26151 }
26152}
26153
26154/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26155/// given instead, which also contains an `insert` range.
26156///
26157/// This function uses markers to define ranges:
26158/// - `|` marks the cursor position
26159/// - `<>` marks the replace range
26160/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26161pub fn handle_completion_request_with_insert_and_replace(
26162 cx: &mut EditorLspTestContext,
26163 marked_string: &str,
26164 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26165 counter: Arc<AtomicUsize>,
26166) -> impl Future<Output = ()> {
26167 let complete_from_marker: TextRangeMarker = '|'.into();
26168 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26169 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26170
26171 let (_, mut marked_ranges) = marked_text_ranges_by(
26172 marked_string,
26173 vec![
26174 complete_from_marker.clone(),
26175 replace_range_marker.clone(),
26176 insert_range_marker.clone(),
26177 ],
26178 );
26179
26180 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26181 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26182 ));
26183 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26184 let replace_range =
26185 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26186
26187 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26188 Some(ranges) if !ranges.is_empty() => {
26189 let range1 = ranges[0].clone();
26190 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26191 }
26192 _ => lsp::Range {
26193 start: replace_range.start,
26194 end: complete_from_position,
26195 },
26196 };
26197
26198 let mut request =
26199 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26200 let completions = completions.clone();
26201 counter.fetch_add(1, atomic::Ordering::Release);
26202 async move {
26203 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26204 assert_eq!(
26205 params.text_document_position.position, complete_from_position,
26206 "marker `|` position doesn't match",
26207 );
26208 Ok(Some(lsp::CompletionResponse::Array(
26209 completions
26210 .iter()
26211 .map(|(label, new_text)| lsp::CompletionItem {
26212 label: label.to_string(),
26213 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26214 lsp::InsertReplaceEdit {
26215 insert: insert_range,
26216 replace: replace_range,
26217 new_text: new_text.to_string(),
26218 },
26219 )),
26220 ..Default::default()
26221 })
26222 .collect(),
26223 )))
26224 }
26225 });
26226
26227 async move {
26228 request.next().await;
26229 }
26230}
26231
26232fn handle_resolve_completion_request(
26233 cx: &mut EditorLspTestContext,
26234 edits: Option<Vec<(&'static str, &'static str)>>,
26235) -> impl Future<Output = ()> {
26236 let edits = edits.map(|edits| {
26237 edits
26238 .iter()
26239 .map(|(marked_string, new_text)| {
26240 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26241 let replace_range = cx.to_lsp_range(
26242 MultiBufferOffset(marked_ranges[0].start)
26243 ..MultiBufferOffset(marked_ranges[0].end),
26244 );
26245 lsp::TextEdit::new(replace_range, new_text.to_string())
26246 })
26247 .collect::<Vec<_>>()
26248 });
26249
26250 let mut request =
26251 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26252 let edits = edits.clone();
26253 async move {
26254 Ok(lsp::CompletionItem {
26255 additional_text_edits: edits,
26256 ..Default::default()
26257 })
26258 }
26259 });
26260
26261 async move {
26262 request.next().await;
26263 }
26264}
26265
26266pub(crate) fn update_test_language_settings(
26267 cx: &mut TestAppContext,
26268 f: impl Fn(&mut AllLanguageSettingsContent),
26269) {
26270 cx.update(|cx| {
26271 SettingsStore::update_global(cx, |store, cx| {
26272 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26273 });
26274 });
26275}
26276
26277pub(crate) fn update_test_project_settings(
26278 cx: &mut TestAppContext,
26279 f: impl Fn(&mut ProjectSettingsContent),
26280) {
26281 cx.update(|cx| {
26282 SettingsStore::update_global(cx, |store, cx| {
26283 store.update_user_settings(cx, |settings| f(&mut settings.project));
26284 });
26285 });
26286}
26287
26288pub(crate) fn update_test_editor_settings(
26289 cx: &mut TestAppContext,
26290 f: impl Fn(&mut EditorSettingsContent),
26291) {
26292 cx.update(|cx| {
26293 SettingsStore::update_global(cx, |store, cx| {
26294 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26295 })
26296 })
26297}
26298
26299pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26300 cx.update(|cx| {
26301 assets::Assets.load_test_fonts(cx);
26302 let store = SettingsStore::test(cx);
26303 cx.set_global(store);
26304 theme::init(theme::LoadThemes::JustBase, cx);
26305 release_channel::init(semver::Version::new(0, 0, 0), cx);
26306 crate::init(cx);
26307 });
26308 zlog::init_test();
26309 update_test_language_settings(cx, f);
26310}
26311
26312#[track_caller]
26313fn assert_hunk_revert(
26314 not_reverted_text_with_selections: &str,
26315 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26316 expected_reverted_text_with_selections: &str,
26317 base_text: &str,
26318 cx: &mut EditorLspTestContext,
26319) {
26320 cx.set_state(not_reverted_text_with_selections);
26321 cx.set_head_text(base_text);
26322 cx.executor().run_until_parked();
26323
26324 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26325 let snapshot = editor.snapshot(window, cx);
26326 let reverted_hunk_statuses = snapshot
26327 .buffer_snapshot()
26328 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26329 .map(|hunk| hunk.status().kind)
26330 .collect::<Vec<_>>();
26331
26332 editor.git_restore(&Default::default(), window, cx);
26333 reverted_hunk_statuses
26334 });
26335 cx.executor().run_until_parked();
26336 cx.assert_editor_state(expected_reverted_text_with_selections);
26337 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26338}
26339
26340#[gpui::test(iterations = 10)]
26341async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26342 init_test(cx, |_| {});
26343
26344 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26345 let counter = diagnostic_requests.clone();
26346
26347 let fs = FakeFs::new(cx.executor());
26348 fs.insert_tree(
26349 path!("/a"),
26350 json!({
26351 "first.rs": "fn main() { let a = 5; }",
26352 "second.rs": "// Test file",
26353 }),
26354 )
26355 .await;
26356
26357 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26358 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26359 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26360
26361 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26362 language_registry.add(rust_lang());
26363 let mut fake_servers = language_registry.register_fake_lsp(
26364 "Rust",
26365 FakeLspAdapter {
26366 capabilities: lsp::ServerCapabilities {
26367 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26368 lsp::DiagnosticOptions {
26369 identifier: None,
26370 inter_file_dependencies: true,
26371 workspace_diagnostics: true,
26372 work_done_progress_options: Default::default(),
26373 },
26374 )),
26375 ..Default::default()
26376 },
26377 ..Default::default()
26378 },
26379 );
26380
26381 let editor = workspace
26382 .update(cx, |workspace, window, cx| {
26383 workspace.open_abs_path(
26384 PathBuf::from(path!("/a/first.rs")),
26385 OpenOptions::default(),
26386 window,
26387 cx,
26388 )
26389 })
26390 .unwrap()
26391 .await
26392 .unwrap()
26393 .downcast::<Editor>()
26394 .unwrap();
26395 let fake_server = fake_servers.next().await.unwrap();
26396 let server_id = fake_server.server.server_id();
26397 let mut first_request = fake_server
26398 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26399 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26400 let result_id = Some(new_result_id.to_string());
26401 assert_eq!(
26402 params.text_document.uri,
26403 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26404 );
26405 async move {
26406 Ok(lsp::DocumentDiagnosticReportResult::Report(
26407 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26408 related_documents: None,
26409 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26410 items: Vec::new(),
26411 result_id,
26412 },
26413 }),
26414 ))
26415 }
26416 });
26417
26418 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
26419 project.update(cx, |project, cx| {
26420 let buffer_id = editor
26421 .read(cx)
26422 .buffer()
26423 .read(cx)
26424 .as_singleton()
26425 .expect("created a singleton buffer")
26426 .read(cx)
26427 .remote_id();
26428 let buffer_result_id = project
26429 .lsp_store()
26430 .read(cx)
26431 .result_id(server_id, buffer_id, cx);
26432 assert_eq!(expected, buffer_result_id);
26433 });
26434 };
26435
26436 ensure_result_id(None, cx);
26437 cx.executor().advance_clock(Duration::from_millis(60));
26438 cx.executor().run_until_parked();
26439 assert_eq!(
26440 diagnostic_requests.load(atomic::Ordering::Acquire),
26441 1,
26442 "Opening file should trigger diagnostic request"
26443 );
26444 first_request
26445 .next()
26446 .await
26447 .expect("should have sent the first diagnostics pull request");
26448 ensure_result_id(Some("1".to_string()), cx);
26449
26450 // Editing should trigger diagnostics
26451 editor.update_in(cx, |editor, window, cx| {
26452 editor.handle_input("2", window, cx)
26453 });
26454 cx.executor().advance_clock(Duration::from_millis(60));
26455 cx.executor().run_until_parked();
26456 assert_eq!(
26457 diagnostic_requests.load(atomic::Ordering::Acquire),
26458 2,
26459 "Editing should trigger diagnostic request"
26460 );
26461 ensure_result_id(Some("2".to_string()), cx);
26462
26463 // Moving cursor should not trigger diagnostic request
26464 editor.update_in(cx, |editor, window, cx| {
26465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26466 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26467 });
26468 });
26469 cx.executor().advance_clock(Duration::from_millis(60));
26470 cx.executor().run_until_parked();
26471 assert_eq!(
26472 diagnostic_requests.load(atomic::Ordering::Acquire),
26473 2,
26474 "Cursor movement should not trigger diagnostic request"
26475 );
26476 ensure_result_id(Some("2".to_string()), cx);
26477 // Multiple rapid edits should be debounced
26478 for _ in 0..5 {
26479 editor.update_in(cx, |editor, window, cx| {
26480 editor.handle_input("x", window, cx)
26481 });
26482 }
26483 cx.executor().advance_clock(Duration::from_millis(60));
26484 cx.executor().run_until_parked();
26485
26486 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26487 assert!(
26488 final_requests <= 4,
26489 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26490 );
26491 ensure_result_id(Some(final_requests.to_string()), cx);
26492}
26493
26494#[gpui::test]
26495async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26496 // Regression test for issue #11671
26497 // Previously, adding a cursor after moving multiple cursors would reset
26498 // the cursor count instead of adding to the existing cursors.
26499 init_test(cx, |_| {});
26500 let mut cx = EditorTestContext::new(cx).await;
26501
26502 // Create a simple buffer with cursor at start
26503 cx.set_state(indoc! {"
26504 ˇaaaa
26505 bbbb
26506 cccc
26507 dddd
26508 eeee
26509 ffff
26510 gggg
26511 hhhh"});
26512
26513 // Add 2 cursors below (so we have 3 total)
26514 cx.update_editor(|editor, window, cx| {
26515 editor.add_selection_below(&Default::default(), window, cx);
26516 editor.add_selection_below(&Default::default(), window, cx);
26517 });
26518
26519 // Verify we have 3 cursors
26520 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26521 assert_eq!(
26522 initial_count, 3,
26523 "Should have 3 cursors after adding 2 below"
26524 );
26525
26526 // Move down one line
26527 cx.update_editor(|editor, window, cx| {
26528 editor.move_down(&MoveDown, window, cx);
26529 });
26530
26531 // Add another cursor below
26532 cx.update_editor(|editor, window, cx| {
26533 editor.add_selection_below(&Default::default(), window, cx);
26534 });
26535
26536 // Should now have 4 cursors (3 original + 1 new)
26537 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26538 assert_eq!(
26539 final_count, 4,
26540 "Should have 4 cursors after moving and adding another"
26541 );
26542}
26543
26544#[gpui::test]
26545async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26546 init_test(cx, |_| {});
26547
26548 let mut cx = EditorTestContext::new(cx).await;
26549
26550 cx.set_state(indoc!(
26551 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26552 Second line here"#
26553 ));
26554
26555 cx.update_editor(|editor, window, cx| {
26556 // Enable soft wrapping with a narrow width to force soft wrapping and
26557 // confirm that more than 2 rows are being displayed.
26558 editor.set_wrap_width(Some(100.0.into()), cx);
26559 assert!(editor.display_text(cx).lines().count() > 2);
26560
26561 editor.add_selection_below(
26562 &AddSelectionBelow {
26563 skip_soft_wrap: true,
26564 },
26565 window,
26566 cx,
26567 );
26568
26569 assert_eq!(
26570 display_ranges(editor, cx),
26571 &[
26572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26573 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26574 ]
26575 );
26576
26577 editor.add_selection_above(
26578 &AddSelectionAbove {
26579 skip_soft_wrap: true,
26580 },
26581 window,
26582 cx,
26583 );
26584
26585 assert_eq!(
26586 display_ranges(editor, cx),
26587 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26588 );
26589
26590 editor.add_selection_below(
26591 &AddSelectionBelow {
26592 skip_soft_wrap: false,
26593 },
26594 window,
26595 cx,
26596 );
26597
26598 assert_eq!(
26599 display_ranges(editor, cx),
26600 &[
26601 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26602 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26603 ]
26604 );
26605
26606 editor.add_selection_above(
26607 &AddSelectionAbove {
26608 skip_soft_wrap: false,
26609 },
26610 window,
26611 cx,
26612 );
26613
26614 assert_eq!(
26615 display_ranges(editor, cx),
26616 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26617 );
26618 });
26619}
26620
26621#[gpui::test(iterations = 10)]
26622async fn test_document_colors(cx: &mut TestAppContext) {
26623 let expected_color = Rgba {
26624 r: 0.33,
26625 g: 0.33,
26626 b: 0.33,
26627 a: 0.33,
26628 };
26629
26630 init_test(cx, |_| {});
26631
26632 let fs = FakeFs::new(cx.executor());
26633 fs.insert_tree(
26634 path!("/a"),
26635 json!({
26636 "first.rs": "fn main() { let a = 5; }",
26637 }),
26638 )
26639 .await;
26640
26641 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26642 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26643 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26644
26645 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26646 language_registry.add(rust_lang());
26647 let mut fake_servers = language_registry.register_fake_lsp(
26648 "Rust",
26649 FakeLspAdapter {
26650 capabilities: lsp::ServerCapabilities {
26651 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26652 ..lsp::ServerCapabilities::default()
26653 },
26654 name: "rust-analyzer",
26655 ..FakeLspAdapter::default()
26656 },
26657 );
26658 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26659 "Rust",
26660 FakeLspAdapter {
26661 capabilities: lsp::ServerCapabilities {
26662 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26663 ..lsp::ServerCapabilities::default()
26664 },
26665 name: "not-rust-analyzer",
26666 ..FakeLspAdapter::default()
26667 },
26668 );
26669
26670 let editor = workspace
26671 .update(cx, |workspace, window, cx| {
26672 workspace.open_abs_path(
26673 PathBuf::from(path!("/a/first.rs")),
26674 OpenOptions::default(),
26675 window,
26676 cx,
26677 )
26678 })
26679 .unwrap()
26680 .await
26681 .unwrap()
26682 .downcast::<Editor>()
26683 .unwrap();
26684 let fake_language_server = fake_servers.next().await.unwrap();
26685 let fake_language_server_without_capabilities =
26686 fake_servers_without_capabilities.next().await.unwrap();
26687 let requests_made = Arc::new(AtomicUsize::new(0));
26688 let closure_requests_made = Arc::clone(&requests_made);
26689 let mut color_request_handle = fake_language_server
26690 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26691 let requests_made = Arc::clone(&closure_requests_made);
26692 async move {
26693 assert_eq!(
26694 params.text_document.uri,
26695 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26696 );
26697 requests_made.fetch_add(1, atomic::Ordering::Release);
26698 Ok(vec![
26699 lsp::ColorInformation {
26700 range: lsp::Range {
26701 start: lsp::Position {
26702 line: 0,
26703 character: 0,
26704 },
26705 end: lsp::Position {
26706 line: 0,
26707 character: 1,
26708 },
26709 },
26710 color: lsp::Color {
26711 red: 0.33,
26712 green: 0.33,
26713 blue: 0.33,
26714 alpha: 0.33,
26715 },
26716 },
26717 lsp::ColorInformation {
26718 range: lsp::Range {
26719 start: lsp::Position {
26720 line: 0,
26721 character: 0,
26722 },
26723 end: lsp::Position {
26724 line: 0,
26725 character: 1,
26726 },
26727 },
26728 color: lsp::Color {
26729 red: 0.33,
26730 green: 0.33,
26731 blue: 0.33,
26732 alpha: 0.33,
26733 },
26734 },
26735 ])
26736 }
26737 });
26738
26739 let _handle = fake_language_server_without_capabilities
26740 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26741 panic!("Should not be called");
26742 });
26743 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26744 color_request_handle.next().await.unwrap();
26745 cx.run_until_parked();
26746 assert_eq!(
26747 1,
26748 requests_made.load(atomic::Ordering::Acquire),
26749 "Should query for colors once per editor open"
26750 );
26751 editor.update_in(cx, |editor, _, cx| {
26752 assert_eq!(
26753 vec![expected_color],
26754 extract_color_inlays(editor, cx),
26755 "Should have an initial inlay"
26756 );
26757 });
26758
26759 // opening another file in a split should not influence the LSP query counter
26760 workspace
26761 .update(cx, |workspace, window, cx| {
26762 assert_eq!(
26763 workspace.panes().len(),
26764 1,
26765 "Should have one pane with one editor"
26766 );
26767 workspace.move_item_to_pane_in_direction(
26768 &MoveItemToPaneInDirection {
26769 direction: SplitDirection::Right,
26770 focus: false,
26771 clone: true,
26772 },
26773 window,
26774 cx,
26775 );
26776 })
26777 .unwrap();
26778 cx.run_until_parked();
26779 workspace
26780 .update(cx, |workspace, _, cx| {
26781 let panes = workspace.panes();
26782 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26783 for pane in panes {
26784 let editor = pane
26785 .read(cx)
26786 .active_item()
26787 .and_then(|item| item.downcast::<Editor>())
26788 .expect("Should have opened an editor in each split");
26789 let editor_file = editor
26790 .read(cx)
26791 .buffer()
26792 .read(cx)
26793 .as_singleton()
26794 .expect("test deals with singleton buffers")
26795 .read(cx)
26796 .file()
26797 .expect("test buffese should have a file")
26798 .path();
26799 assert_eq!(
26800 editor_file.as_ref(),
26801 rel_path("first.rs"),
26802 "Both editors should be opened for the same file"
26803 )
26804 }
26805 })
26806 .unwrap();
26807
26808 cx.executor().advance_clock(Duration::from_millis(500));
26809 let save = editor.update_in(cx, |editor, window, cx| {
26810 editor.move_to_end(&MoveToEnd, window, cx);
26811 editor.handle_input("dirty", window, cx);
26812 editor.save(
26813 SaveOptions {
26814 format: true,
26815 autosave: true,
26816 },
26817 project.clone(),
26818 window,
26819 cx,
26820 )
26821 });
26822 save.await.unwrap();
26823
26824 color_request_handle.next().await.unwrap();
26825 cx.run_until_parked();
26826 assert_eq!(
26827 2,
26828 requests_made.load(atomic::Ordering::Acquire),
26829 "Should query for colors once per save (deduplicated) and once per formatting after save"
26830 );
26831
26832 drop(editor);
26833 let close = workspace
26834 .update(cx, |workspace, window, cx| {
26835 workspace.active_pane().update(cx, |pane, cx| {
26836 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26837 })
26838 })
26839 .unwrap();
26840 close.await.unwrap();
26841 let close = workspace
26842 .update(cx, |workspace, window, cx| {
26843 workspace.active_pane().update(cx, |pane, cx| {
26844 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26845 })
26846 })
26847 .unwrap();
26848 close.await.unwrap();
26849 assert_eq!(
26850 2,
26851 requests_made.load(atomic::Ordering::Acquire),
26852 "After saving and closing all editors, no extra requests should be made"
26853 );
26854 workspace
26855 .update(cx, |workspace, _, cx| {
26856 assert!(
26857 workspace.active_item(cx).is_none(),
26858 "Should close all editors"
26859 )
26860 })
26861 .unwrap();
26862
26863 workspace
26864 .update(cx, |workspace, window, cx| {
26865 workspace.active_pane().update(cx, |pane, cx| {
26866 pane.navigate_backward(&workspace::GoBack, window, cx);
26867 })
26868 })
26869 .unwrap();
26870 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26871 cx.run_until_parked();
26872 let editor = workspace
26873 .update(cx, |workspace, _, cx| {
26874 workspace
26875 .active_item(cx)
26876 .expect("Should have reopened the editor again after navigating back")
26877 .downcast::<Editor>()
26878 .expect("Should be an editor")
26879 })
26880 .unwrap();
26881
26882 assert_eq!(
26883 2,
26884 requests_made.load(atomic::Ordering::Acquire),
26885 "Cache should be reused on buffer close and reopen"
26886 );
26887 editor.update(cx, |editor, cx| {
26888 assert_eq!(
26889 vec![expected_color],
26890 extract_color_inlays(editor, cx),
26891 "Should have an initial inlay"
26892 );
26893 });
26894
26895 drop(color_request_handle);
26896 let closure_requests_made = Arc::clone(&requests_made);
26897 let mut empty_color_request_handle = fake_language_server
26898 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26899 let requests_made = Arc::clone(&closure_requests_made);
26900 async move {
26901 assert_eq!(
26902 params.text_document.uri,
26903 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26904 );
26905 requests_made.fetch_add(1, atomic::Ordering::Release);
26906 Ok(Vec::new())
26907 }
26908 });
26909 let save = editor.update_in(cx, |editor, window, cx| {
26910 editor.move_to_end(&MoveToEnd, window, cx);
26911 editor.handle_input("dirty_again", window, cx);
26912 editor.save(
26913 SaveOptions {
26914 format: false,
26915 autosave: true,
26916 },
26917 project.clone(),
26918 window,
26919 cx,
26920 )
26921 });
26922 save.await.unwrap();
26923
26924 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26925 empty_color_request_handle.next().await.unwrap();
26926 cx.run_until_parked();
26927 assert_eq!(
26928 3,
26929 requests_made.load(atomic::Ordering::Acquire),
26930 "Should query for colors once per save only, as formatting was not requested"
26931 );
26932 editor.update(cx, |editor, cx| {
26933 assert_eq!(
26934 Vec::<Rgba>::new(),
26935 extract_color_inlays(editor, cx),
26936 "Should clear all colors when the server returns an empty response"
26937 );
26938 });
26939}
26940
26941#[gpui::test]
26942async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26943 init_test(cx, |_| {});
26944 let (editor, cx) = cx.add_window_view(Editor::single_line);
26945 editor.update_in(cx, |editor, window, cx| {
26946 editor.set_text("oops\n\nwow\n", window, cx)
26947 });
26948 cx.run_until_parked();
26949 editor.update(cx, |editor, cx| {
26950 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26951 });
26952 editor.update(cx, |editor, cx| {
26953 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
26954 });
26955 cx.run_until_parked();
26956 editor.update(cx, |editor, cx| {
26957 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26958 });
26959}
26960
26961#[gpui::test]
26962async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26963 init_test(cx, |_| {});
26964
26965 cx.update(|cx| {
26966 register_project_item::<Editor>(cx);
26967 });
26968
26969 let fs = FakeFs::new(cx.executor());
26970 fs.insert_tree("/root1", json!({})).await;
26971 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26972 .await;
26973
26974 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26975 let (workspace, cx) =
26976 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26977
26978 let worktree_id = project.update(cx, |project, cx| {
26979 project.worktrees(cx).next().unwrap().read(cx).id()
26980 });
26981
26982 let handle = workspace
26983 .update_in(cx, |workspace, window, cx| {
26984 let project_path = (worktree_id, rel_path("one.pdf"));
26985 workspace.open_path(project_path, None, true, window, cx)
26986 })
26987 .await
26988 .unwrap();
26989
26990 assert_eq!(
26991 handle.to_any_view().entity_type(),
26992 TypeId::of::<InvalidItemView>()
26993 );
26994}
26995
26996#[gpui::test]
26997async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26998 init_test(cx, |_| {});
26999
27000 let language = Arc::new(Language::new(
27001 LanguageConfig::default(),
27002 Some(tree_sitter_rust::LANGUAGE.into()),
27003 ));
27004
27005 // Test hierarchical sibling navigation
27006 let text = r#"
27007 fn outer() {
27008 if condition {
27009 let a = 1;
27010 }
27011 let b = 2;
27012 }
27013
27014 fn another() {
27015 let c = 3;
27016 }
27017 "#;
27018
27019 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27020 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27021 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27022
27023 // Wait for parsing to complete
27024 editor
27025 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27026 .await;
27027
27028 editor.update_in(cx, |editor, window, cx| {
27029 // Start by selecting "let a = 1;" inside the if block
27030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27031 s.select_display_ranges([
27032 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27033 ]);
27034 });
27035
27036 let initial_selection = editor
27037 .selections
27038 .display_ranges(&editor.display_snapshot(cx));
27039 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27040
27041 // Test select next sibling - should move up levels to find the next sibling
27042 // Since "let a = 1;" has no siblings in the if block, it should move up
27043 // to find "let b = 2;" which is a sibling of the if block
27044 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27045 let next_selection = editor
27046 .selections
27047 .display_ranges(&editor.display_snapshot(cx));
27048
27049 // Should have a selection and it should be different from the initial
27050 assert_eq!(
27051 next_selection.len(),
27052 1,
27053 "Should have one selection after next"
27054 );
27055 assert_ne!(
27056 next_selection[0], initial_selection[0],
27057 "Next sibling selection should be different"
27058 );
27059
27060 // Test hierarchical navigation by going to the end of the current function
27061 // and trying to navigate to the next function
27062 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27063 s.select_display_ranges([
27064 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27065 ]);
27066 });
27067
27068 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27069 let function_next_selection = editor
27070 .selections
27071 .display_ranges(&editor.display_snapshot(cx));
27072
27073 // Should move to the next function
27074 assert_eq!(
27075 function_next_selection.len(),
27076 1,
27077 "Should have one selection after function next"
27078 );
27079
27080 // Test select previous sibling navigation
27081 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27082 let prev_selection = editor
27083 .selections
27084 .display_ranges(&editor.display_snapshot(cx));
27085
27086 // Should have a selection and it should be different
27087 assert_eq!(
27088 prev_selection.len(),
27089 1,
27090 "Should have one selection after prev"
27091 );
27092 assert_ne!(
27093 prev_selection[0], function_next_selection[0],
27094 "Previous sibling selection should be different from next"
27095 );
27096 });
27097}
27098
27099#[gpui::test]
27100async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27101 init_test(cx, |_| {});
27102
27103 let mut cx = EditorTestContext::new(cx).await;
27104 cx.set_state(
27105 "let ˇvariable = 42;
27106let another = variable + 1;
27107let result = variable * 2;",
27108 );
27109
27110 // Set up document highlights manually (simulating LSP response)
27111 cx.update_editor(|editor, _window, cx| {
27112 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27113
27114 // Create highlights for "variable" occurrences
27115 let highlight_ranges = [
27116 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27117 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27118 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27119 ];
27120
27121 let anchor_ranges: Vec<_> = highlight_ranges
27122 .iter()
27123 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27124 .collect();
27125
27126 editor.highlight_background::<DocumentHighlightRead>(
27127 &anchor_ranges,
27128 |theme| theme.colors().editor_document_highlight_read_background,
27129 cx,
27130 );
27131 });
27132
27133 // Go to next highlight - should move to second "variable"
27134 cx.update_editor(|editor, window, cx| {
27135 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27136 });
27137 cx.assert_editor_state(
27138 "let variable = 42;
27139let another = ˇvariable + 1;
27140let result = variable * 2;",
27141 );
27142
27143 // Go to next highlight - should move to third "variable"
27144 cx.update_editor(|editor, window, cx| {
27145 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27146 });
27147 cx.assert_editor_state(
27148 "let variable = 42;
27149let another = variable + 1;
27150let result = ˇvariable * 2;",
27151 );
27152
27153 // Go to next highlight - should stay at third "variable" (no wrap-around)
27154 cx.update_editor(|editor, window, cx| {
27155 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27156 });
27157 cx.assert_editor_state(
27158 "let variable = 42;
27159let another = variable + 1;
27160let result = ˇvariable * 2;",
27161 );
27162
27163 // Now test going backwards from third position
27164 cx.update_editor(|editor, window, cx| {
27165 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27166 });
27167 cx.assert_editor_state(
27168 "let variable = 42;
27169let another = ˇvariable + 1;
27170let result = variable * 2;",
27171 );
27172
27173 // Go to previous highlight - should move to first "variable"
27174 cx.update_editor(|editor, window, cx| {
27175 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27176 });
27177 cx.assert_editor_state(
27178 "let ˇvariable = 42;
27179let another = variable + 1;
27180let result = variable * 2;",
27181 );
27182
27183 // Go to previous highlight - should stay on first "variable"
27184 cx.update_editor(|editor, window, cx| {
27185 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27186 });
27187 cx.assert_editor_state(
27188 "let ˇvariable = 42;
27189let another = variable + 1;
27190let result = variable * 2;",
27191 );
27192}
27193
27194#[gpui::test]
27195async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27196 cx: &mut gpui::TestAppContext,
27197) {
27198 init_test(cx, |_| {});
27199
27200 let url = "https://zed.dev";
27201
27202 let markdown_language = Arc::new(Language::new(
27203 LanguageConfig {
27204 name: "Markdown".into(),
27205 ..LanguageConfig::default()
27206 },
27207 None,
27208 ));
27209
27210 let mut cx = EditorTestContext::new(cx).await;
27211 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27212 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27213
27214 cx.update_editor(|editor, window, cx| {
27215 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27216 editor.paste(&Paste, window, cx);
27217 });
27218
27219 cx.assert_editor_state(&format!(
27220 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27221 ));
27222}
27223
27224#[gpui::test]
27225async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
27226 cx: &mut gpui::TestAppContext,
27227) {
27228 init_test(cx, |_| {});
27229
27230 let url = "https://zed.dev";
27231
27232 let markdown_language = Arc::new(Language::new(
27233 LanguageConfig {
27234 name: "Markdown".into(),
27235 ..LanguageConfig::default()
27236 },
27237 None,
27238 ));
27239
27240 let mut cx = EditorTestContext::new(cx).await;
27241 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27242 cx.set_state(&format!(
27243 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
27244 ));
27245
27246 cx.update_editor(|editor, window, cx| {
27247 editor.copy(&Copy, window, cx);
27248 });
27249
27250 cx.set_state(&format!(
27251 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
27252 ));
27253
27254 cx.update_editor(|editor, window, cx| {
27255 editor.paste(&Paste, window, cx);
27256 });
27257
27258 cx.assert_editor_state(&format!(
27259 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
27260 ));
27261}
27262
27263#[gpui::test]
27264async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
27265 cx: &mut gpui::TestAppContext,
27266) {
27267 init_test(cx, |_| {});
27268
27269 let url = "https://zed.dev";
27270
27271 let markdown_language = Arc::new(Language::new(
27272 LanguageConfig {
27273 name: "Markdown".into(),
27274 ..LanguageConfig::default()
27275 },
27276 None,
27277 ));
27278
27279 let mut cx = EditorTestContext::new(cx).await;
27280 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27281 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
27282
27283 cx.update_editor(|editor, window, cx| {
27284 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27285 editor.paste(&Paste, window, cx);
27286 });
27287
27288 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
27289}
27290
27291#[gpui::test]
27292async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
27293 cx: &mut gpui::TestAppContext,
27294) {
27295 init_test(cx, |_| {});
27296
27297 let text = "Awesome";
27298
27299 let markdown_language = Arc::new(Language::new(
27300 LanguageConfig {
27301 name: "Markdown".into(),
27302 ..LanguageConfig::default()
27303 },
27304 None,
27305 ));
27306
27307 let mut cx = EditorTestContext::new(cx).await;
27308 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27309 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
27310
27311 cx.update_editor(|editor, window, cx| {
27312 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
27313 editor.paste(&Paste, window, cx);
27314 });
27315
27316 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
27317}
27318
27319#[gpui::test]
27320async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
27321 cx: &mut gpui::TestAppContext,
27322) {
27323 init_test(cx, |_| {});
27324
27325 let url = "https://zed.dev";
27326
27327 let markdown_language = Arc::new(Language::new(
27328 LanguageConfig {
27329 name: "Rust".into(),
27330 ..LanguageConfig::default()
27331 },
27332 None,
27333 ));
27334
27335 let mut cx = EditorTestContext::new(cx).await;
27336 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27337 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
27338
27339 cx.update_editor(|editor, window, cx| {
27340 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27341 editor.paste(&Paste, window, cx);
27342 });
27343
27344 cx.assert_editor_state(&format!(
27345 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
27346 ));
27347}
27348
27349#[gpui::test]
27350async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
27351 cx: &mut TestAppContext,
27352) {
27353 init_test(cx, |_| {});
27354
27355 let url = "https://zed.dev";
27356
27357 let markdown_language = Arc::new(Language::new(
27358 LanguageConfig {
27359 name: "Markdown".into(),
27360 ..LanguageConfig::default()
27361 },
27362 None,
27363 ));
27364
27365 let (editor, cx) = cx.add_window_view(|window, cx| {
27366 let multi_buffer = MultiBuffer::build_multi(
27367 [
27368 ("this will embed -> link", vec![Point::row_range(0..1)]),
27369 ("this will replace -> link", vec![Point::row_range(0..1)]),
27370 ],
27371 cx,
27372 );
27373 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
27374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27375 s.select_ranges(vec![
27376 Point::new(0, 19)..Point::new(0, 23),
27377 Point::new(1, 21)..Point::new(1, 25),
27378 ])
27379 });
27380 let first_buffer_id = multi_buffer
27381 .read(cx)
27382 .excerpt_buffer_ids()
27383 .into_iter()
27384 .next()
27385 .unwrap();
27386 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
27387 first_buffer.update(cx, |buffer, cx| {
27388 buffer.set_language(Some(markdown_language.clone()), cx);
27389 });
27390
27391 editor
27392 });
27393 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27394
27395 cx.update_editor(|editor, window, cx| {
27396 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27397 editor.paste(&Paste, window, cx);
27398 });
27399
27400 cx.assert_editor_state(&format!(
27401 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
27402 ));
27403}
27404
27405#[gpui::test]
27406async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
27407 init_test(cx, |_| {});
27408
27409 let fs = FakeFs::new(cx.executor());
27410 fs.insert_tree(
27411 path!("/project"),
27412 json!({
27413 "first.rs": "# First Document\nSome content here.",
27414 "second.rs": "Plain text content for second file.",
27415 }),
27416 )
27417 .await;
27418
27419 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
27420 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27421 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27422
27423 let language = rust_lang();
27424 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27425 language_registry.add(language.clone());
27426 let mut fake_servers = language_registry.register_fake_lsp(
27427 "Rust",
27428 FakeLspAdapter {
27429 ..FakeLspAdapter::default()
27430 },
27431 );
27432
27433 let buffer1 = project
27434 .update(cx, |project, cx| {
27435 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
27436 })
27437 .await
27438 .unwrap();
27439 let buffer2 = project
27440 .update(cx, |project, cx| {
27441 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
27442 })
27443 .await
27444 .unwrap();
27445
27446 let multi_buffer = cx.new(|cx| {
27447 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
27448 multi_buffer.set_excerpts_for_path(
27449 PathKey::for_buffer(&buffer1, cx),
27450 buffer1.clone(),
27451 [Point::zero()..buffer1.read(cx).max_point()],
27452 3,
27453 cx,
27454 );
27455 multi_buffer.set_excerpts_for_path(
27456 PathKey::for_buffer(&buffer2, cx),
27457 buffer2.clone(),
27458 [Point::zero()..buffer1.read(cx).max_point()],
27459 3,
27460 cx,
27461 );
27462 multi_buffer
27463 });
27464
27465 let (editor, cx) = cx.add_window_view(|window, cx| {
27466 Editor::new(
27467 EditorMode::full(),
27468 multi_buffer,
27469 Some(project.clone()),
27470 window,
27471 cx,
27472 )
27473 });
27474
27475 let fake_language_server = fake_servers.next().await.unwrap();
27476
27477 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
27478
27479 let save = editor.update_in(cx, |editor, window, cx| {
27480 assert!(editor.is_dirty(cx));
27481
27482 editor.save(
27483 SaveOptions {
27484 format: true,
27485 autosave: true,
27486 },
27487 project,
27488 window,
27489 cx,
27490 )
27491 });
27492 let (start_edit_tx, start_edit_rx) = oneshot::channel();
27493 let (done_edit_tx, done_edit_rx) = oneshot::channel();
27494 let mut done_edit_rx = Some(done_edit_rx);
27495 let mut start_edit_tx = Some(start_edit_tx);
27496
27497 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
27498 start_edit_tx.take().unwrap().send(()).unwrap();
27499 let done_edit_rx = done_edit_rx.take().unwrap();
27500 async move {
27501 done_edit_rx.await.unwrap();
27502 Ok(None)
27503 }
27504 });
27505
27506 start_edit_rx.await.unwrap();
27507 buffer2
27508 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
27509 .unwrap();
27510
27511 done_edit_tx.send(()).unwrap();
27512
27513 save.await.unwrap();
27514 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
27515}
27516
27517#[track_caller]
27518fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
27519 editor
27520 .all_inlays(cx)
27521 .into_iter()
27522 .filter_map(|inlay| inlay.get_color())
27523 .map(Rgba::from)
27524 .collect()
27525}
27526
27527#[gpui::test]
27528fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
27529 init_test(cx, |_| {});
27530
27531 let editor = cx.add_window(|window, cx| {
27532 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
27533 build_editor(buffer, window, cx)
27534 });
27535
27536 editor
27537 .update(cx, |editor, window, cx| {
27538 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27539 s.select_display_ranges([
27540 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
27541 ])
27542 });
27543
27544 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
27545
27546 assert_eq!(
27547 editor.display_text(cx),
27548 "line1\nline2\nline2",
27549 "Duplicating last line upward should create duplicate above, not on same line"
27550 );
27551
27552 assert_eq!(
27553 editor
27554 .selections
27555 .display_ranges(&editor.display_snapshot(cx)),
27556 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27557 "Selection should move to the duplicated line"
27558 );
27559 })
27560 .unwrap();
27561}
27562
27563#[gpui::test]
27564async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27565 init_test(cx, |_| {});
27566
27567 let mut cx = EditorTestContext::new(cx).await;
27568
27569 cx.set_state("line1\nline2ˇ");
27570
27571 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27572
27573 let clipboard_text = cx
27574 .read_from_clipboard()
27575 .and_then(|item| item.text().as_deref().map(str::to_string));
27576
27577 assert_eq!(
27578 clipboard_text,
27579 Some("line2\n".to_string()),
27580 "Copying a line without trailing newline should include a newline"
27581 );
27582
27583 cx.set_state("line1\nˇ");
27584
27585 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27586
27587 cx.assert_editor_state("line1\nline2\nˇ");
27588}
27589
27590#[gpui::test]
27591async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27592 init_test(cx, |_| {});
27593
27594 let mut cx = EditorTestContext::new(cx).await;
27595
27596 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27597
27598 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27599
27600 let clipboard_text = cx
27601 .read_from_clipboard()
27602 .and_then(|item| item.text().as_deref().map(str::to_string));
27603
27604 assert_eq!(
27605 clipboard_text,
27606 Some("line1\nline2\nline3\n".to_string()),
27607 "Copying multiple lines should include a single newline between lines"
27608 );
27609
27610 cx.set_state("lineA\nˇ");
27611
27612 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27613
27614 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27615}
27616
27617#[gpui::test]
27618async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27619 init_test(cx, |_| {});
27620
27621 let mut cx = EditorTestContext::new(cx).await;
27622
27623 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27624
27625 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
27626
27627 let clipboard_text = cx
27628 .read_from_clipboard()
27629 .and_then(|item| item.text().as_deref().map(str::to_string));
27630
27631 assert_eq!(
27632 clipboard_text,
27633 Some("line1\nline2\nline3\n".to_string()),
27634 "Copying multiple lines should include a single newline between lines"
27635 );
27636
27637 cx.set_state("lineA\nˇ");
27638
27639 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27640
27641 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27642}
27643
27644#[gpui::test]
27645async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27646 init_test(cx, |_| {});
27647
27648 let mut cx = EditorTestContext::new(cx).await;
27649
27650 cx.set_state("line1\nline2ˇ");
27651 cx.update_editor(|e, window, cx| {
27652 e.set_mode(EditorMode::SingleLine);
27653 assert!(e.key_context(window, cx).contains("end_of_input"));
27654 });
27655 cx.set_state("ˇline1\nline2");
27656 cx.update_editor(|e, window, cx| {
27657 assert!(!e.key_context(window, cx).contains("end_of_input"));
27658 });
27659 cx.set_state("line1ˇ\nline2");
27660 cx.update_editor(|e, window, cx| {
27661 assert!(!e.key_context(window, cx).contains("end_of_input"));
27662 });
27663}
27664
27665#[gpui::test]
27666async fn test_sticky_scroll(cx: &mut TestAppContext) {
27667 init_test(cx, |_| {});
27668 let mut cx = EditorTestContext::new(cx).await;
27669
27670 let buffer = indoc! {"
27671 ˇfn foo() {
27672 let abc = 123;
27673 }
27674 struct Bar;
27675 impl Bar {
27676 fn new() -> Self {
27677 Self
27678 }
27679 }
27680 fn baz() {
27681 }
27682 "};
27683 cx.set_state(&buffer);
27684
27685 cx.update_editor(|e, _, cx| {
27686 e.buffer()
27687 .read(cx)
27688 .as_singleton()
27689 .unwrap()
27690 .update(cx, |buffer, cx| {
27691 buffer.set_language(Some(rust_lang()), cx);
27692 })
27693 });
27694
27695 let mut sticky_headers = |offset: ScrollOffset| {
27696 cx.update_editor(|e, window, cx| {
27697 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27698 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27699 .into_iter()
27700 .map(
27701 |StickyHeader {
27702 start_point,
27703 offset,
27704 ..
27705 }| { (start_point, offset) },
27706 )
27707 .collect::<Vec<_>>()
27708 })
27709 };
27710
27711 let fn_foo = Point { row: 0, column: 0 };
27712 let impl_bar = Point { row: 4, column: 0 };
27713 let fn_new = Point { row: 5, column: 4 };
27714
27715 assert_eq!(sticky_headers(0.0), vec![]);
27716 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27717 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27718 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27719 assert_eq!(sticky_headers(2.0), vec![]);
27720 assert_eq!(sticky_headers(2.5), vec![]);
27721 assert_eq!(sticky_headers(3.0), vec![]);
27722 assert_eq!(sticky_headers(3.5), vec![]);
27723 assert_eq!(sticky_headers(4.0), vec![]);
27724 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27725 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27726 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27727 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27728 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27729 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27730 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27731 assert_eq!(sticky_headers(8.0), vec![]);
27732 assert_eq!(sticky_headers(8.5), vec![]);
27733 assert_eq!(sticky_headers(9.0), vec![]);
27734 assert_eq!(sticky_headers(9.5), vec![]);
27735 assert_eq!(sticky_headers(10.0), vec![]);
27736}
27737
27738#[gpui::test]
27739async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27740 init_test(cx, |_| {});
27741 cx.update(|cx| {
27742 SettingsStore::update_global(cx, |store, cx| {
27743 store.update_user_settings(cx, |settings| {
27744 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27745 enabled: Some(true),
27746 })
27747 });
27748 });
27749 });
27750 let mut cx = EditorTestContext::new(cx).await;
27751
27752 let line_height = cx.editor(|editor, window, _cx| {
27753 editor
27754 .style()
27755 .unwrap()
27756 .text
27757 .line_height_in_pixels(window.rem_size())
27758 });
27759
27760 let buffer = indoc! {"
27761 ˇfn foo() {
27762 let abc = 123;
27763 }
27764 struct Bar;
27765 impl Bar {
27766 fn new() -> Self {
27767 Self
27768 }
27769 }
27770 fn baz() {
27771 }
27772 "};
27773 cx.set_state(&buffer);
27774
27775 cx.update_editor(|e, _, cx| {
27776 e.buffer()
27777 .read(cx)
27778 .as_singleton()
27779 .unwrap()
27780 .update(cx, |buffer, cx| {
27781 buffer.set_language(Some(rust_lang()), cx);
27782 })
27783 });
27784
27785 let fn_foo = || empty_range(0, 0);
27786 let impl_bar = || empty_range(4, 0);
27787 let fn_new = || empty_range(5, 4);
27788
27789 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27790 cx.update_editor(|e, window, cx| {
27791 e.scroll(
27792 gpui::Point {
27793 x: 0.,
27794 y: scroll_offset,
27795 },
27796 None,
27797 window,
27798 cx,
27799 );
27800 });
27801 cx.simulate_click(
27802 gpui::Point {
27803 x: px(0.),
27804 y: click_offset as f32 * line_height,
27805 },
27806 Modifiers::none(),
27807 );
27808 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27809 };
27810
27811 assert_eq!(
27812 scroll_and_click(
27813 4.5, // impl Bar is halfway off the screen
27814 0.0 // click top of screen
27815 ),
27816 // scrolled to impl Bar
27817 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27818 );
27819
27820 assert_eq!(
27821 scroll_and_click(
27822 4.5, // impl Bar is halfway off the screen
27823 0.25 // click middle of impl Bar
27824 ),
27825 // scrolled to impl Bar
27826 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27827 );
27828
27829 assert_eq!(
27830 scroll_and_click(
27831 4.5, // impl Bar is halfway off the screen
27832 1.5 // click below impl Bar (e.g. fn new())
27833 ),
27834 // scrolled to fn new() - this is below the impl Bar header which has persisted
27835 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27836 );
27837
27838 assert_eq!(
27839 scroll_and_click(
27840 5.5, // fn new is halfway underneath impl Bar
27841 0.75 // click on the overlap of impl Bar and fn new()
27842 ),
27843 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27844 );
27845
27846 assert_eq!(
27847 scroll_and_click(
27848 5.5, // fn new is halfway underneath impl Bar
27849 1.25 // click on the visible part of fn new()
27850 ),
27851 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27852 );
27853
27854 assert_eq!(
27855 scroll_and_click(
27856 1.5, // fn foo is halfway off the screen
27857 0.0 // click top of screen
27858 ),
27859 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
27860 );
27861
27862 assert_eq!(
27863 scroll_and_click(
27864 1.5, // fn foo is halfway off the screen
27865 0.75 // click visible part of let abc...
27866 )
27867 .0,
27868 // no change in scroll
27869 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
27870 (gpui::Point { x: 0., y: 1.5 })
27871 );
27872}
27873
27874#[gpui::test]
27875async fn test_next_prev_reference(cx: &mut TestAppContext) {
27876 const CYCLE_POSITIONS: &[&'static str] = &[
27877 indoc! {"
27878 fn foo() {
27879 let ˇabc = 123;
27880 let x = abc + 1;
27881 let y = abc + 2;
27882 let z = abc + 2;
27883 }
27884 "},
27885 indoc! {"
27886 fn foo() {
27887 let abc = 123;
27888 let x = ˇabc + 1;
27889 let y = abc + 2;
27890 let z = abc + 2;
27891 }
27892 "},
27893 indoc! {"
27894 fn foo() {
27895 let abc = 123;
27896 let x = abc + 1;
27897 let y = ˇabc + 2;
27898 let z = abc + 2;
27899 }
27900 "},
27901 indoc! {"
27902 fn foo() {
27903 let abc = 123;
27904 let x = abc + 1;
27905 let y = abc + 2;
27906 let z = ˇabc + 2;
27907 }
27908 "},
27909 ];
27910
27911 init_test(cx, |_| {});
27912
27913 let mut cx = EditorLspTestContext::new_rust(
27914 lsp::ServerCapabilities {
27915 references_provider: Some(lsp::OneOf::Left(true)),
27916 ..Default::default()
27917 },
27918 cx,
27919 )
27920 .await;
27921
27922 // importantly, the cursor is in the middle
27923 cx.set_state(indoc! {"
27924 fn foo() {
27925 let aˇbc = 123;
27926 let x = abc + 1;
27927 let y = abc + 2;
27928 let z = abc + 2;
27929 }
27930 "});
27931
27932 let reference_ranges = [
27933 lsp::Position::new(1, 8),
27934 lsp::Position::new(2, 12),
27935 lsp::Position::new(3, 12),
27936 lsp::Position::new(4, 12),
27937 ]
27938 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27939
27940 cx.lsp
27941 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27942 Ok(Some(
27943 reference_ranges
27944 .map(|range| lsp::Location {
27945 uri: params.text_document_position.text_document.uri.clone(),
27946 range,
27947 })
27948 .to_vec(),
27949 ))
27950 });
27951
27952 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27953 cx.update_editor(|editor, window, cx| {
27954 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27955 })
27956 .unwrap()
27957 .await
27958 .unwrap()
27959 };
27960
27961 _move(Direction::Next, 1, &mut cx).await;
27962 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27963
27964 _move(Direction::Next, 1, &mut cx).await;
27965 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27966
27967 _move(Direction::Next, 1, &mut cx).await;
27968 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27969
27970 // loops back to the start
27971 _move(Direction::Next, 1, &mut cx).await;
27972 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27973
27974 // loops back to the end
27975 _move(Direction::Prev, 1, &mut cx).await;
27976 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27977
27978 _move(Direction::Prev, 1, &mut cx).await;
27979 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27980
27981 _move(Direction::Prev, 1, &mut cx).await;
27982 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27983
27984 _move(Direction::Prev, 1, &mut cx).await;
27985 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27986
27987 _move(Direction::Next, 3, &mut cx).await;
27988 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27989
27990 _move(Direction::Prev, 2, &mut cx).await;
27991 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27992}
27993
27994#[gpui::test]
27995async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
27996 init_test(cx, |_| {});
27997
27998 let (editor, cx) = cx.add_window_view(|window, cx| {
27999 let multi_buffer = MultiBuffer::build_multi(
28000 [
28001 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28002 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28003 ],
28004 cx,
28005 );
28006 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28007 });
28008
28009 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28010 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28011
28012 cx.assert_excerpts_with_selections(indoc! {"
28013 [EXCERPT]
28014 ˇ1
28015 2
28016 3
28017 [EXCERPT]
28018 1
28019 2
28020 3
28021 "});
28022
28023 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28024 cx.update_editor(|editor, window, cx| {
28025 editor.change_selections(None.into(), window, cx, |s| {
28026 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28027 });
28028 });
28029 cx.assert_excerpts_with_selections(indoc! {"
28030 [EXCERPT]
28031 1
28032 2ˇ
28033 3
28034 [EXCERPT]
28035 1
28036 2
28037 3
28038 "});
28039
28040 cx.update_editor(|editor, window, cx| {
28041 editor
28042 .select_all_matches(&SelectAllMatches, window, cx)
28043 .unwrap();
28044 });
28045 cx.assert_excerpts_with_selections(indoc! {"
28046 [EXCERPT]
28047 1
28048 2ˇ
28049 3
28050 [EXCERPT]
28051 1
28052 2ˇ
28053 3
28054 "});
28055
28056 cx.update_editor(|editor, window, cx| {
28057 editor.handle_input("X", window, cx);
28058 });
28059 cx.assert_excerpts_with_selections(indoc! {"
28060 [EXCERPT]
28061 1
28062 Xˇ
28063 3
28064 [EXCERPT]
28065 1
28066 Xˇ
28067 3
28068 "});
28069
28070 // Scenario 2: Select "2", then fold second buffer before insertion
28071 cx.update_multibuffer(|mb, cx| {
28072 for buffer_id in buffer_ids.iter() {
28073 let buffer = mb.buffer(*buffer_id).unwrap();
28074 buffer.update(cx, |buffer, cx| {
28075 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28076 });
28077 }
28078 });
28079
28080 // Select "2" and select all matches
28081 cx.update_editor(|editor, window, cx| {
28082 editor.change_selections(None.into(), window, cx, |s| {
28083 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28084 });
28085 editor
28086 .select_all_matches(&SelectAllMatches, window, cx)
28087 .unwrap();
28088 });
28089
28090 // Fold second buffer - should remove selections from folded buffer
28091 cx.update_editor(|editor, _, cx| {
28092 editor.fold_buffer(buffer_ids[1], cx);
28093 });
28094 cx.assert_excerpts_with_selections(indoc! {"
28095 [EXCERPT]
28096 1
28097 2ˇ
28098 3
28099 [EXCERPT]
28100 [FOLDED]
28101 "});
28102
28103 // Insert text - should only affect first buffer
28104 cx.update_editor(|editor, window, cx| {
28105 editor.handle_input("Y", window, cx);
28106 });
28107 cx.update_editor(|editor, _, cx| {
28108 editor.unfold_buffer(buffer_ids[1], cx);
28109 });
28110 cx.assert_excerpts_with_selections(indoc! {"
28111 [EXCERPT]
28112 1
28113 Yˇ
28114 3
28115 [EXCERPT]
28116 1
28117 2
28118 3
28119 "});
28120
28121 // Scenario 3: Select "2", then fold first buffer before insertion
28122 cx.update_multibuffer(|mb, cx| {
28123 for buffer_id in buffer_ids.iter() {
28124 let buffer = mb.buffer(*buffer_id).unwrap();
28125 buffer.update(cx, |buffer, cx| {
28126 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28127 });
28128 }
28129 });
28130
28131 // Select "2" and select all matches
28132 cx.update_editor(|editor, window, cx| {
28133 editor.change_selections(None.into(), window, cx, |s| {
28134 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28135 });
28136 editor
28137 .select_all_matches(&SelectAllMatches, window, cx)
28138 .unwrap();
28139 });
28140
28141 // Fold first buffer - should remove selections from folded buffer
28142 cx.update_editor(|editor, _, cx| {
28143 editor.fold_buffer(buffer_ids[0], cx);
28144 });
28145 cx.assert_excerpts_with_selections(indoc! {"
28146 [EXCERPT]
28147 [FOLDED]
28148 [EXCERPT]
28149 1
28150 2ˇ
28151 3
28152 "});
28153
28154 // Insert text - should only affect second buffer
28155 cx.update_editor(|editor, window, cx| {
28156 editor.handle_input("Z", window, cx);
28157 });
28158 cx.update_editor(|editor, _, cx| {
28159 editor.unfold_buffer(buffer_ids[0], cx);
28160 });
28161 cx.assert_excerpts_with_selections(indoc! {"
28162 [EXCERPT]
28163 1
28164 2
28165 3
28166 [EXCERPT]
28167 1
28168 Zˇ
28169 3
28170 "});
28171
28172 // Edge case scenario: fold all buffers, then try to insert
28173 cx.update_editor(|editor, _, cx| {
28174 editor.fold_buffer(buffer_ids[0], cx);
28175 editor.fold_buffer(buffer_ids[1], cx);
28176 });
28177 cx.assert_excerpts_with_selections(indoc! {"
28178 [EXCERPT]
28179 ˇ[FOLDED]
28180 [EXCERPT]
28181 [FOLDED]
28182 "});
28183
28184 // Insert should work via default selection
28185 cx.update_editor(|editor, window, cx| {
28186 editor.handle_input("W", window, cx);
28187 });
28188 cx.update_editor(|editor, _, cx| {
28189 editor.unfold_buffer(buffer_ids[0], cx);
28190 editor.unfold_buffer(buffer_ids[1], cx);
28191 });
28192 cx.assert_excerpts_with_selections(indoc! {"
28193 [EXCERPT]
28194 Wˇ1
28195 2
28196 3
28197 [EXCERPT]
28198 1
28199 Z
28200 3
28201 "});
28202}
28203
28204#[gpui::test]
28205async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
28206 init_test(cx, |_| {});
28207 let mut leader_cx = EditorTestContext::new(cx).await;
28208
28209 let diff_base = indoc!(
28210 r#"
28211 one
28212 two
28213 three
28214 four
28215 five
28216 six
28217 "#
28218 );
28219
28220 let initial_state = indoc!(
28221 r#"
28222 ˇone
28223 two
28224 THREE
28225 four
28226 five
28227 six
28228 "#
28229 );
28230
28231 leader_cx.set_state(initial_state);
28232
28233 leader_cx.set_head_text(&diff_base);
28234 leader_cx.run_until_parked();
28235
28236 let follower = leader_cx.update_multibuffer(|leader, cx| {
28237 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28238 leader.set_all_diff_hunks_expanded(cx);
28239 leader.get_or_create_follower(cx)
28240 });
28241 follower.update(cx, |follower, cx| {
28242 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28243 follower.set_all_diff_hunks_expanded(cx);
28244 });
28245
28246 let follower_editor =
28247 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
28248 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
28249
28250 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
28251 cx.run_until_parked();
28252
28253 leader_cx.assert_editor_state(initial_state);
28254 follower_cx.assert_editor_state(indoc! {
28255 r#"
28256 ˇone
28257 two
28258 three
28259 four
28260 five
28261 six
28262 "#
28263 });
28264
28265 follower_cx.editor(|editor, _window, cx| {
28266 assert!(editor.read_only(cx));
28267 });
28268
28269 leader_cx.update_editor(|editor, _window, cx| {
28270 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
28271 });
28272 cx.run_until_parked();
28273
28274 leader_cx.assert_editor_state(indoc! {
28275 r#"
28276 ˇone
28277 two
28278 THREE
28279 four
28280 FIVE
28281 six
28282 "#
28283 });
28284
28285 follower_cx.assert_editor_state(indoc! {
28286 r#"
28287 ˇone
28288 two
28289 three
28290 four
28291 five
28292 six
28293 "#
28294 });
28295
28296 leader_cx.update_editor(|editor, _window, cx| {
28297 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
28298 });
28299 cx.run_until_parked();
28300
28301 leader_cx.assert_editor_state(indoc! {
28302 r#"
28303 ˇone
28304 two
28305 THREE
28306 four
28307 FIVE
28308 six
28309 SEVEN"#
28310 });
28311
28312 follower_cx.assert_editor_state(indoc! {
28313 r#"
28314 ˇone
28315 two
28316 three
28317 four
28318 five
28319 six
28320 "#
28321 });
28322
28323 leader_cx.update_editor(|editor, window, cx| {
28324 editor.move_down(&MoveDown, window, cx);
28325 editor.refresh_selected_text_highlights(true, window, cx);
28326 });
28327 leader_cx.run_until_parked();
28328}
28329
28330#[gpui::test]
28331async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
28332 init_test(cx, |_| {});
28333 let base_text = "base\n";
28334 let buffer_text = "buffer\n";
28335
28336 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
28337 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
28338
28339 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
28340 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
28341 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
28342 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
28343
28344 let leader = cx.new(|cx| {
28345 let mut leader = MultiBuffer::new(Capability::ReadWrite);
28346 leader.set_all_diff_hunks_expanded(cx);
28347 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28348 leader
28349 });
28350 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
28351 follower.update(cx, |follower, _| {
28352 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28353 });
28354
28355 leader.update(cx, |leader, cx| {
28356 leader.insert_excerpts_after(
28357 ExcerptId::min(),
28358 extra_buffer_2.clone(),
28359 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28360 cx,
28361 );
28362 leader.add_diff(extra_diff_2.clone(), cx);
28363
28364 leader.insert_excerpts_after(
28365 ExcerptId::min(),
28366 extra_buffer_1.clone(),
28367 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28368 cx,
28369 );
28370 leader.add_diff(extra_diff_1.clone(), cx);
28371
28372 leader.insert_excerpts_after(
28373 ExcerptId::min(),
28374 buffer1.clone(),
28375 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28376 cx,
28377 );
28378 leader.add_diff(diff1.clone(), cx);
28379 });
28380
28381 cx.run_until_parked();
28382 let mut cx = cx.add_empty_window();
28383
28384 let leader_editor = cx
28385 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
28386 let follower_editor = cx.new_window_entity(|window, cx| {
28387 Editor::for_multibuffer(follower.clone(), None, window, cx)
28388 });
28389
28390 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
28391 leader_cx.assert_editor_state(indoc! {"
28392 ˇbuffer
28393
28394 dummy text 1
28395
28396 dummy text 2
28397 "});
28398 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
28399 follower_cx.assert_editor_state(indoc! {"
28400 ˇbase
28401
28402
28403 "});
28404}
28405
28406#[gpui::test]
28407async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
28408 init_test(cx, |_| {});
28409
28410 let (editor, cx) = cx.add_window_view(|window, cx| {
28411 let multi_buffer = MultiBuffer::build_multi(
28412 [
28413 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28414 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
28415 ],
28416 cx,
28417 );
28418 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28419 });
28420
28421 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28422
28423 cx.assert_excerpts_with_selections(indoc! {"
28424 [EXCERPT]
28425 ˇ1
28426 2
28427 3
28428 [EXCERPT]
28429 1
28430 2
28431 3
28432 4
28433 5
28434 6
28435 7
28436 8
28437 9
28438 "});
28439
28440 cx.update_editor(|editor, window, cx| {
28441 editor.change_selections(None.into(), window, cx, |s| {
28442 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
28443 });
28444 });
28445
28446 cx.assert_excerpts_with_selections(indoc! {"
28447 [EXCERPT]
28448 1
28449 2
28450 3
28451 [EXCERPT]
28452 1
28453 2
28454 3
28455 4
28456 5
28457 6
28458 ˇ7
28459 8
28460 9
28461 "});
28462
28463 cx.update_editor(|editor, _window, cx| {
28464 editor.set_vertical_scroll_margin(0, cx);
28465 });
28466
28467 cx.update_editor(|editor, window, cx| {
28468 assert_eq!(editor.vertical_scroll_margin(), 0);
28469 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28470 assert_eq!(
28471 editor.snapshot(window, cx).scroll_position(),
28472 gpui::Point::new(0., 12.0)
28473 );
28474 });
28475
28476 cx.update_editor(|editor, _window, cx| {
28477 editor.set_vertical_scroll_margin(3, cx);
28478 });
28479
28480 cx.update_editor(|editor, window, cx| {
28481 assert_eq!(editor.vertical_scroll_margin(), 3);
28482 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28483 assert_eq!(
28484 editor.snapshot(window, cx).scroll_position(),
28485 gpui::Point::new(0., 9.0)
28486 );
28487 });
28488}