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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(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_immediate(toml_language, cx));
4333 let rust_buffer = cx.new(|cx| {
4334 Buffer::local("const c: usize = 3;\n", cx).with_language_immediate(rust_language, cx)
4335 });
4336 let multibuffer = cx.new(|cx| {
4337 let mut multibuffer = MultiBuffer::new(ReadWrite);
4338 multibuffer.push_excerpts(
4339 toml_buffer.clone(),
4340 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4341 cx,
4342 );
4343 multibuffer.push_excerpts(
4344 rust_buffer.clone(),
4345 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4346 cx,
4347 );
4348 multibuffer
4349 });
4350
4351 cx.add_window(|window, cx| {
4352 let mut editor = build_editor(multibuffer, window, cx);
4353
4354 assert_eq!(
4355 editor.text(cx),
4356 indoc! {"
4357 a = 1
4358 b = 2
4359
4360 const c: usize = 3;
4361 "}
4362 );
4363
4364 select_ranges(
4365 &mut editor,
4366 indoc! {"
4367 «aˇ» = 1
4368 b = 2
4369
4370 «const c:ˇ» usize = 3;
4371 "},
4372 window,
4373 cx,
4374 );
4375
4376 editor.tab(&Tab, window, cx);
4377 assert_text_with_selections(
4378 &mut editor,
4379 indoc! {"
4380 «aˇ» = 1
4381 b = 2
4382
4383 «const c:ˇ» usize = 3;
4384 "},
4385 cx,
4386 );
4387 editor.backtab(&Backtab, window, cx);
4388 assert_text_with_selections(
4389 &mut editor,
4390 indoc! {"
4391 «aˇ» = 1
4392 b = 2
4393
4394 «const c:ˇ» usize = 3;
4395 "},
4396 cx,
4397 );
4398
4399 editor
4400 });
4401}
4402
4403#[gpui::test]
4404async fn test_backspace(cx: &mut TestAppContext) {
4405 init_test(cx, |_| {});
4406
4407 let mut cx = EditorTestContext::new(cx).await;
4408
4409 // Basic backspace
4410 cx.set_state(indoc! {"
4411 onˇe two three
4412 fou«rˇ» five six
4413 seven «ˇeight nine
4414 »ten
4415 "});
4416 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4417 cx.assert_editor_state(indoc! {"
4418 oˇe two three
4419 fouˇ five six
4420 seven ˇten
4421 "});
4422
4423 // Test backspace inside and around indents
4424 cx.set_state(indoc! {"
4425 zero
4426 ˇone
4427 ˇtwo
4428 ˇ ˇ ˇ three
4429 ˇ ˇ four
4430 "});
4431 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4432 cx.assert_editor_state(indoc! {"
4433 zero
4434 ˇone
4435 ˇtwo
4436 ˇ threeˇ four
4437 "});
4438}
4439
4440#[gpui::test]
4441async fn test_delete(cx: &mut TestAppContext) {
4442 init_test(cx, |_| {});
4443
4444 let mut cx = EditorTestContext::new(cx).await;
4445 cx.set_state(indoc! {"
4446 onˇe two three
4447 fou«rˇ» five six
4448 seven «ˇeight nine
4449 »ten
4450 "});
4451 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4452 cx.assert_editor_state(indoc! {"
4453 onˇ two three
4454 fouˇ five six
4455 seven ˇten
4456 "});
4457}
4458
4459#[gpui::test]
4460fn test_delete_line(cx: &mut TestAppContext) {
4461 init_test(cx, |_| {});
4462
4463 let editor = cx.add_window(|window, cx| {
4464 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4465 build_editor(buffer, window, cx)
4466 });
4467 _ = editor.update(cx, |editor, window, cx| {
4468 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4469 s.select_display_ranges([
4470 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4471 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4472 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4473 ])
4474 });
4475 editor.delete_line(&DeleteLine, window, cx);
4476 assert_eq!(editor.display_text(cx), "ghi");
4477 assert_eq!(
4478 display_ranges(editor, cx),
4479 vec![
4480 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4481 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4482 ]
4483 );
4484 });
4485
4486 let editor = cx.add_window(|window, cx| {
4487 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4488 build_editor(buffer, window, cx)
4489 });
4490 _ = editor.update(cx, |editor, window, cx| {
4491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4492 s.select_display_ranges([
4493 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4494 ])
4495 });
4496 editor.delete_line(&DeleteLine, window, cx);
4497 assert_eq!(editor.display_text(cx), "ghi\n");
4498 assert_eq!(
4499 display_ranges(editor, cx),
4500 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4501 );
4502 });
4503
4504 let editor = cx.add_window(|window, cx| {
4505 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4506 build_editor(buffer, window, cx)
4507 });
4508 _ = editor.update(cx, |editor, window, cx| {
4509 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4510 s.select_display_ranges([
4511 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4512 ])
4513 });
4514 editor.delete_line(&DeleteLine, window, cx);
4515 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4516 assert_eq!(
4517 display_ranges(editor, cx),
4518 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4519 );
4520 });
4521}
4522
4523#[gpui::test]
4524fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4525 init_test(cx, |_| {});
4526
4527 cx.add_window(|window, cx| {
4528 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4529 let mut editor = build_editor(buffer.clone(), window, cx);
4530 let buffer = buffer.read(cx).as_singleton().unwrap();
4531
4532 assert_eq!(
4533 editor
4534 .selections
4535 .ranges::<Point>(&editor.display_snapshot(cx)),
4536 &[Point::new(0, 0)..Point::new(0, 0)]
4537 );
4538
4539 // When on single line, replace newline at end by space
4540 editor.join_lines(&JoinLines, window, cx);
4541 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4542 assert_eq!(
4543 editor
4544 .selections
4545 .ranges::<Point>(&editor.display_snapshot(cx)),
4546 &[Point::new(0, 3)..Point::new(0, 3)]
4547 );
4548
4549 // When multiple lines are selected, remove newlines that are spanned by the selection
4550 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4551 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4552 });
4553 editor.join_lines(&JoinLines, window, cx);
4554 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4555 assert_eq!(
4556 editor
4557 .selections
4558 .ranges::<Point>(&editor.display_snapshot(cx)),
4559 &[Point::new(0, 11)..Point::new(0, 11)]
4560 );
4561
4562 // Undo should be transactional
4563 editor.undo(&Undo, window, cx);
4564 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4565 assert_eq!(
4566 editor
4567 .selections
4568 .ranges::<Point>(&editor.display_snapshot(cx)),
4569 &[Point::new(0, 5)..Point::new(2, 2)]
4570 );
4571
4572 // When joining an empty line don't insert a space
4573 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4574 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4575 });
4576 editor.join_lines(&JoinLines, window, cx);
4577 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4578 assert_eq!(
4579 editor
4580 .selections
4581 .ranges::<Point>(&editor.display_snapshot(cx)),
4582 [Point::new(2, 3)..Point::new(2, 3)]
4583 );
4584
4585 // We can remove trailing newlines
4586 editor.join_lines(&JoinLines, window, cx);
4587 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4588 assert_eq!(
4589 editor
4590 .selections
4591 .ranges::<Point>(&editor.display_snapshot(cx)),
4592 [Point::new(2, 3)..Point::new(2, 3)]
4593 );
4594
4595 // We don't blow up on the last line
4596 editor.join_lines(&JoinLines, window, cx);
4597 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4598 assert_eq!(
4599 editor
4600 .selections
4601 .ranges::<Point>(&editor.display_snapshot(cx)),
4602 [Point::new(2, 3)..Point::new(2, 3)]
4603 );
4604
4605 // reset to test indentation
4606 editor.buffer.update(cx, |buffer, cx| {
4607 buffer.edit(
4608 [
4609 (Point::new(1, 0)..Point::new(1, 2), " "),
4610 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4611 ],
4612 None,
4613 cx,
4614 )
4615 });
4616
4617 // We remove any leading spaces
4618 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4619 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4620 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4621 });
4622 editor.join_lines(&JoinLines, window, cx);
4623 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4624
4625 // We don't insert a space for a line containing only spaces
4626 editor.join_lines(&JoinLines, window, cx);
4627 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4628
4629 // We ignore any leading tabs
4630 editor.join_lines(&JoinLines, window, cx);
4631 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4632
4633 editor
4634 });
4635}
4636
4637#[gpui::test]
4638fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4639 init_test(cx, |_| {});
4640
4641 cx.add_window(|window, cx| {
4642 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4643 let mut editor = build_editor(buffer.clone(), window, cx);
4644 let buffer = buffer.read(cx).as_singleton().unwrap();
4645
4646 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4647 s.select_ranges([
4648 Point::new(0, 2)..Point::new(1, 1),
4649 Point::new(1, 2)..Point::new(1, 2),
4650 Point::new(3, 1)..Point::new(3, 2),
4651 ])
4652 });
4653
4654 editor.join_lines(&JoinLines, window, cx);
4655 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4656
4657 assert_eq!(
4658 editor
4659 .selections
4660 .ranges::<Point>(&editor.display_snapshot(cx)),
4661 [
4662 Point::new(0, 7)..Point::new(0, 7),
4663 Point::new(1, 3)..Point::new(1, 3)
4664 ]
4665 );
4666 editor
4667 });
4668}
4669
4670#[gpui::test]
4671async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4672 init_test(cx, |_| {});
4673
4674 let mut cx = EditorTestContext::new(cx).await;
4675
4676 let diff_base = r#"
4677 Line 0
4678 Line 1
4679 Line 2
4680 Line 3
4681 "#
4682 .unindent();
4683
4684 cx.set_state(
4685 &r#"
4686 ˇLine 0
4687 Line 1
4688 Line 2
4689 Line 3
4690 "#
4691 .unindent(),
4692 );
4693
4694 cx.set_head_text(&diff_base);
4695 executor.run_until_parked();
4696
4697 // Join lines
4698 cx.update_editor(|editor, window, cx| {
4699 editor.join_lines(&JoinLines, window, cx);
4700 });
4701 executor.run_until_parked();
4702
4703 cx.assert_editor_state(
4704 &r#"
4705 Line 0ˇ Line 1
4706 Line 2
4707 Line 3
4708 "#
4709 .unindent(),
4710 );
4711 // Join again
4712 cx.update_editor(|editor, window, cx| {
4713 editor.join_lines(&JoinLines, window, cx);
4714 });
4715 executor.run_until_parked();
4716
4717 cx.assert_editor_state(
4718 &r#"
4719 Line 0 Line 1ˇ Line 2
4720 Line 3
4721 "#
4722 .unindent(),
4723 );
4724}
4725
4726#[gpui::test]
4727async fn test_custom_newlines_cause_no_false_positive_diffs(
4728 executor: BackgroundExecutor,
4729 cx: &mut TestAppContext,
4730) {
4731 init_test(cx, |_| {});
4732 let mut cx = EditorTestContext::new(cx).await;
4733 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4734 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4735 executor.run_until_parked();
4736
4737 cx.update_editor(|editor, window, cx| {
4738 let snapshot = editor.snapshot(window, cx);
4739 assert_eq!(
4740 snapshot
4741 .buffer_snapshot()
4742 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4743 .collect::<Vec<_>>(),
4744 Vec::new(),
4745 "Should not have any diffs for files with custom newlines"
4746 );
4747 });
4748}
4749
4750#[gpui::test]
4751async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4752 init_test(cx, |_| {});
4753
4754 let mut cx = EditorTestContext::new(cx).await;
4755
4756 // Test sort_lines_case_insensitive()
4757 cx.set_state(indoc! {"
4758 «z
4759 y
4760 x
4761 Z
4762 Y
4763 Xˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| {
4766 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4767 });
4768 cx.assert_editor_state(indoc! {"
4769 «x
4770 X
4771 y
4772 Y
4773 z
4774 Zˇ»
4775 "});
4776
4777 // Test sort_lines_by_length()
4778 //
4779 // Demonstrates:
4780 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4781 // - sort is stable
4782 cx.set_state(indoc! {"
4783 «123
4784 æ
4785 12
4786 ∞
4787 1
4788 æˇ»
4789 "});
4790 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4791 cx.assert_editor_state(indoc! {"
4792 «æ
4793 ∞
4794 1
4795 æ
4796 12
4797 123ˇ»
4798 "});
4799
4800 // Test reverse_lines()
4801 cx.set_state(indoc! {"
4802 «5
4803 4
4804 3
4805 2
4806 1ˇ»
4807 "});
4808 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4809 cx.assert_editor_state(indoc! {"
4810 «1
4811 2
4812 3
4813 4
4814 5ˇ»
4815 "});
4816
4817 // Skip testing shuffle_line()
4818
4819 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4820 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4821
4822 // Don't manipulate when cursor is on single line, but expand the selection
4823 cx.set_state(indoc! {"
4824 ddˇdd
4825 ccc
4826 bb
4827 a
4828 "});
4829 cx.update_editor(|e, window, cx| {
4830 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4831 });
4832 cx.assert_editor_state(indoc! {"
4833 «ddddˇ»
4834 ccc
4835 bb
4836 a
4837 "});
4838
4839 // Basic manipulate case
4840 // Start selection moves to column 0
4841 // End of selection shrinks to fit shorter line
4842 cx.set_state(indoc! {"
4843 dd«d
4844 ccc
4845 bb
4846 aaaaaˇ»
4847 "});
4848 cx.update_editor(|e, window, cx| {
4849 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4850 });
4851 cx.assert_editor_state(indoc! {"
4852 «aaaaa
4853 bb
4854 ccc
4855 dddˇ»
4856 "});
4857
4858 // Manipulate case with newlines
4859 cx.set_state(indoc! {"
4860 dd«d
4861 ccc
4862
4863 bb
4864 aaaaa
4865
4866 ˇ»
4867 "});
4868 cx.update_editor(|e, window, cx| {
4869 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4870 });
4871 cx.assert_editor_state(indoc! {"
4872 «
4873
4874 aaaaa
4875 bb
4876 ccc
4877 dddˇ»
4878
4879 "});
4880
4881 // Adding new line
4882 cx.set_state(indoc! {"
4883 aa«a
4884 bbˇ»b
4885 "});
4886 cx.update_editor(|e, window, cx| {
4887 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4888 });
4889 cx.assert_editor_state(indoc! {"
4890 «aaa
4891 bbb
4892 added_lineˇ»
4893 "});
4894
4895 // Removing line
4896 cx.set_state(indoc! {"
4897 aa«a
4898 bbbˇ»
4899 "});
4900 cx.update_editor(|e, window, cx| {
4901 e.manipulate_immutable_lines(window, cx, |lines| {
4902 lines.pop();
4903 })
4904 });
4905 cx.assert_editor_state(indoc! {"
4906 «aaaˇ»
4907 "});
4908
4909 // Removing all lines
4910 cx.set_state(indoc! {"
4911 aa«a
4912 bbbˇ»
4913 "});
4914 cx.update_editor(|e, window, cx| {
4915 e.manipulate_immutable_lines(window, cx, |lines| {
4916 lines.drain(..);
4917 })
4918 });
4919 cx.assert_editor_state(indoc! {"
4920 ˇ
4921 "});
4922}
4923
4924#[gpui::test]
4925async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4926 init_test(cx, |_| {});
4927
4928 let mut cx = EditorTestContext::new(cx).await;
4929
4930 // Consider continuous selection as single selection
4931 cx.set_state(indoc! {"
4932 Aaa«aa
4933 cˇ»c«c
4934 bb
4935 aaaˇ»aa
4936 "});
4937 cx.update_editor(|e, window, cx| {
4938 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4939 });
4940 cx.assert_editor_state(indoc! {"
4941 «Aaaaa
4942 ccc
4943 bb
4944 aaaaaˇ»
4945 "});
4946
4947 cx.set_state(indoc! {"
4948 Aaa«aa
4949 cˇ»c«c
4950 bb
4951 aaaˇ»aa
4952 "});
4953 cx.update_editor(|e, window, cx| {
4954 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4955 });
4956 cx.assert_editor_state(indoc! {"
4957 «Aaaaa
4958 ccc
4959 bbˇ»
4960 "});
4961
4962 // Consider non continuous selection as distinct dedup operations
4963 cx.set_state(indoc! {"
4964 «aaaaa
4965 bb
4966 aaaaa
4967 aaaaaˇ»
4968
4969 aaa«aaˇ»
4970 "});
4971 cx.update_editor(|e, window, cx| {
4972 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4973 });
4974 cx.assert_editor_state(indoc! {"
4975 «aaaaa
4976 bbˇ»
4977
4978 «aaaaaˇ»
4979 "});
4980}
4981
4982#[gpui::test]
4983async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4984 init_test(cx, |_| {});
4985
4986 let mut cx = EditorTestContext::new(cx).await;
4987
4988 cx.set_state(indoc! {"
4989 «Aaa
4990 aAa
4991 Aaaˇ»
4992 "});
4993 cx.update_editor(|e, window, cx| {
4994 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4995 });
4996 cx.assert_editor_state(indoc! {"
4997 «Aaa
4998 aAaˇ»
4999 "});
5000
5001 cx.set_state(indoc! {"
5002 «Aaa
5003 aAa
5004 aaAˇ»
5005 "});
5006 cx.update_editor(|e, window, cx| {
5007 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5008 });
5009 cx.assert_editor_state(indoc! {"
5010 «Aaaˇ»
5011 "});
5012}
5013
5014#[gpui::test]
5015async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5016 init_test(cx, |_| {});
5017
5018 let mut cx = EditorTestContext::new(cx).await;
5019
5020 let js_language = Arc::new(Language::new(
5021 LanguageConfig {
5022 name: "JavaScript".into(),
5023 wrap_characters: Some(language::WrapCharactersConfig {
5024 start_prefix: "<".into(),
5025 start_suffix: ">".into(),
5026 end_prefix: "</".into(),
5027 end_suffix: ">".into(),
5028 }),
5029 ..LanguageConfig::default()
5030 },
5031 None,
5032 ));
5033
5034 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(js_language), cx));
5035
5036 cx.set_state(indoc! {"
5037 «testˇ»
5038 "});
5039 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5040 cx.assert_editor_state(indoc! {"
5041 <«ˇ»>test</«ˇ»>
5042 "});
5043
5044 cx.set_state(indoc! {"
5045 «test
5046 testˇ»
5047 "});
5048 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5049 cx.assert_editor_state(indoc! {"
5050 <«ˇ»>test
5051 test</«ˇ»>
5052 "});
5053
5054 cx.set_state(indoc! {"
5055 teˇst
5056 "});
5057 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5058 cx.assert_editor_state(indoc! {"
5059 te<«ˇ»></«ˇ»>st
5060 "});
5061}
5062
5063#[gpui::test]
5064async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5065 init_test(cx, |_| {});
5066
5067 let mut cx = EditorTestContext::new(cx).await;
5068
5069 let js_language = Arc::new(Language::new(
5070 LanguageConfig {
5071 name: "JavaScript".into(),
5072 wrap_characters: Some(language::WrapCharactersConfig {
5073 start_prefix: "<".into(),
5074 start_suffix: ">".into(),
5075 end_prefix: "</".into(),
5076 end_suffix: ">".into(),
5077 }),
5078 ..LanguageConfig::default()
5079 },
5080 None,
5081 ));
5082
5083 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(js_language), cx));
5084
5085 cx.set_state(indoc! {"
5086 «testˇ»
5087 «testˇ» «testˇ»
5088 «testˇ»
5089 "});
5090 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5091 cx.assert_editor_state(indoc! {"
5092 <«ˇ»>test</«ˇ»>
5093 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5094 <«ˇ»>test</«ˇ»>
5095 "});
5096
5097 cx.set_state(indoc! {"
5098 «test
5099 testˇ»
5100 «test
5101 testˇ»
5102 "});
5103 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5104 cx.assert_editor_state(indoc! {"
5105 <«ˇ»>test
5106 test</«ˇ»>
5107 <«ˇ»>test
5108 test</«ˇ»>
5109 "});
5110}
5111
5112#[gpui::test]
5113async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5114 init_test(cx, |_| {});
5115
5116 let mut cx = EditorTestContext::new(cx).await;
5117
5118 let plaintext_language = Arc::new(Language::new(
5119 LanguageConfig {
5120 name: "Plain Text".into(),
5121 ..LanguageConfig::default()
5122 },
5123 None,
5124 ));
5125
5126 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(plaintext_language), cx));
5127
5128 cx.set_state(indoc! {"
5129 «testˇ»
5130 "});
5131 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5132 cx.assert_editor_state(indoc! {"
5133 «testˇ»
5134 "});
5135}
5136
5137#[gpui::test]
5138async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5139 init_test(cx, |_| {});
5140
5141 let mut cx = EditorTestContext::new(cx).await;
5142
5143 // Manipulate with multiple selections on a single line
5144 cx.set_state(indoc! {"
5145 dd«dd
5146 cˇ»c«c
5147 bb
5148 aaaˇ»aa
5149 "});
5150 cx.update_editor(|e, window, cx| {
5151 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5152 });
5153 cx.assert_editor_state(indoc! {"
5154 «aaaaa
5155 bb
5156 ccc
5157 ddddˇ»
5158 "});
5159
5160 // Manipulate with multiple disjoin selections
5161 cx.set_state(indoc! {"
5162 5«
5163 4
5164 3
5165 2
5166 1ˇ»
5167
5168 dd«dd
5169 ccc
5170 bb
5171 aaaˇ»aa
5172 "});
5173 cx.update_editor(|e, window, cx| {
5174 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5175 });
5176 cx.assert_editor_state(indoc! {"
5177 «1
5178 2
5179 3
5180 4
5181 5ˇ»
5182
5183 «aaaaa
5184 bb
5185 ccc
5186 ddddˇ»
5187 "});
5188
5189 // Adding lines on each selection
5190 cx.set_state(indoc! {"
5191 2«
5192 1ˇ»
5193
5194 bb«bb
5195 aaaˇ»aa
5196 "});
5197 cx.update_editor(|e, window, cx| {
5198 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5199 });
5200 cx.assert_editor_state(indoc! {"
5201 «2
5202 1
5203 added lineˇ»
5204
5205 «bbbb
5206 aaaaa
5207 added lineˇ»
5208 "});
5209
5210 // Removing lines on each selection
5211 cx.set_state(indoc! {"
5212 2«
5213 1ˇ»
5214
5215 bb«bb
5216 aaaˇ»aa
5217 "});
5218 cx.update_editor(|e, window, cx| {
5219 e.manipulate_immutable_lines(window, cx, |lines| {
5220 lines.pop();
5221 })
5222 });
5223 cx.assert_editor_state(indoc! {"
5224 «2ˇ»
5225
5226 «bbbbˇ»
5227 "});
5228}
5229
5230#[gpui::test]
5231async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5232 init_test(cx, |settings| {
5233 settings.defaults.tab_size = NonZeroU32::new(3)
5234 });
5235
5236 let mut cx = EditorTestContext::new(cx).await;
5237
5238 // MULTI SELECTION
5239 // Ln.1 "«" tests empty lines
5240 // Ln.9 tests just leading whitespace
5241 cx.set_state(indoc! {"
5242 «
5243 abc // No indentationˇ»
5244 «\tabc // 1 tabˇ»
5245 \t\tabc « ˇ» // 2 tabs
5246 \t ab«c // Tab followed by space
5247 \tabc // Space followed by tab (3 spaces should be the result)
5248 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5249 abˇ»ˇc ˇ ˇ // Already space indented«
5250 \t
5251 \tabc\tdef // Only the leading tab is manipulatedˇ»
5252 "});
5253 cx.update_editor(|e, window, cx| {
5254 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5255 });
5256 cx.assert_editor_state(
5257 indoc! {"
5258 «
5259 abc // No indentation
5260 abc // 1 tab
5261 abc // 2 tabs
5262 abc // Tab followed by space
5263 abc // Space followed by tab (3 spaces should be the result)
5264 abc // Mixed indentation (tab conversion depends on the column)
5265 abc // Already space indented
5266 ·
5267 abc\tdef // Only the leading tab is manipulatedˇ»
5268 "}
5269 .replace("·", "")
5270 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5271 );
5272
5273 // Test on just a few lines, the others should remain unchanged
5274 // Only lines (3, 5, 10, 11) should change
5275 cx.set_state(
5276 indoc! {"
5277 ·
5278 abc // No indentation
5279 \tabcˇ // 1 tab
5280 \t\tabc // 2 tabs
5281 \t abcˇ // Tab followed by space
5282 \tabc // Space followed by tab (3 spaces should be the result)
5283 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5284 abc // Already space indented
5285 «\t
5286 \tabc\tdef // Only the leading tab is manipulatedˇ»
5287 "}
5288 .replace("·", "")
5289 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5290 );
5291 cx.update_editor(|e, window, cx| {
5292 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5293 });
5294 cx.assert_editor_state(
5295 indoc! {"
5296 ·
5297 abc // No indentation
5298 « abc // 1 tabˇ»
5299 \t\tabc // 2 tabs
5300 « abc // Tab followed by spaceˇ»
5301 \tabc // Space followed by tab (3 spaces should be the result)
5302 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5303 abc // Already space indented
5304 « ·
5305 abc\tdef // Only the leading tab is manipulatedˇ»
5306 "}
5307 .replace("·", "")
5308 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5309 );
5310
5311 // SINGLE SELECTION
5312 // Ln.1 "«" tests empty lines
5313 // Ln.9 tests just leading whitespace
5314 cx.set_state(indoc! {"
5315 «
5316 abc // No indentation
5317 \tabc // 1 tab
5318 \t\tabc // 2 tabs
5319 \t abc // Tab followed by space
5320 \tabc // Space followed by tab (3 spaces should be the result)
5321 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5322 abc // Already space indented
5323 \t
5324 \tabc\tdef // Only the leading tab is manipulatedˇ»
5325 "});
5326 cx.update_editor(|e, window, cx| {
5327 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5328 });
5329 cx.assert_editor_state(
5330 indoc! {"
5331 «
5332 abc // No indentation
5333 abc // 1 tab
5334 abc // 2 tabs
5335 abc // Tab followed by space
5336 abc // Space followed by tab (3 spaces should be the result)
5337 abc // Mixed indentation (tab conversion depends on the column)
5338 abc // Already space indented
5339 ·
5340 abc\tdef // Only the leading tab is manipulatedˇ»
5341 "}
5342 .replace("·", "")
5343 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5344 );
5345}
5346
5347#[gpui::test]
5348async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5349 init_test(cx, |settings| {
5350 settings.defaults.tab_size = NonZeroU32::new(3)
5351 });
5352
5353 let mut cx = EditorTestContext::new(cx).await;
5354
5355 // MULTI SELECTION
5356 // Ln.1 "«" tests empty lines
5357 // Ln.11 tests just leading whitespace
5358 cx.set_state(indoc! {"
5359 «
5360 abˇ»ˇc // No indentation
5361 abc ˇ ˇ // 1 space (< 3 so dont convert)
5362 abc « // 2 spaces (< 3 so dont convert)
5363 abc // 3 spaces (convert)
5364 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5365 «\tˇ»\t«\tˇ»abc // Already tab indented
5366 «\t abc // Tab followed by space
5367 \tabc // Space followed by tab (should be consumed due to tab)
5368 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5369 \tˇ» «\t
5370 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5371 "});
5372 cx.update_editor(|e, window, cx| {
5373 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5374 });
5375 cx.assert_editor_state(indoc! {"
5376 «
5377 abc // No indentation
5378 abc // 1 space (< 3 so dont convert)
5379 abc // 2 spaces (< 3 so dont convert)
5380 \tabc // 3 spaces (convert)
5381 \t abc // 5 spaces (1 tab + 2 spaces)
5382 \t\t\tabc // Already tab indented
5383 \t abc // Tab followed by space
5384 \tabc // Space followed by tab (should be consumed due to tab)
5385 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5386 \t\t\t
5387 \tabc \t // Only the leading spaces should be convertedˇ»
5388 "});
5389
5390 // Test on just a few lines, the other should remain unchanged
5391 // Only lines (4, 8, 11, 12) should change
5392 cx.set_state(
5393 indoc! {"
5394 ·
5395 abc // No indentation
5396 abc // 1 space (< 3 so dont convert)
5397 abc // 2 spaces (< 3 so dont convert)
5398 « abc // 3 spaces (convert)ˇ»
5399 abc // 5 spaces (1 tab + 2 spaces)
5400 \t\t\tabc // Already tab indented
5401 \t abc // Tab followed by space
5402 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5403 \t\t \tabc // Mixed indentation
5404 \t \t \t \tabc // Mixed indentation
5405 \t \tˇ
5406 « abc \t // Only the leading spaces should be convertedˇ»
5407 "}
5408 .replace("·", "")
5409 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5410 );
5411 cx.update_editor(|e, window, cx| {
5412 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5413 });
5414 cx.assert_editor_state(
5415 indoc! {"
5416 ·
5417 abc // No indentation
5418 abc // 1 space (< 3 so dont convert)
5419 abc // 2 spaces (< 3 so dont convert)
5420 «\tabc // 3 spaces (convert)ˇ»
5421 abc // 5 spaces (1 tab + 2 spaces)
5422 \t\t\tabc // Already tab indented
5423 \t abc // Tab followed by space
5424 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5425 \t\t \tabc // Mixed indentation
5426 \t \t \t \tabc // Mixed indentation
5427 «\t\t\t
5428 \tabc \t // Only the leading spaces should be convertedˇ»
5429 "}
5430 .replace("·", "")
5431 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5432 );
5433
5434 // SINGLE SELECTION
5435 // Ln.1 "«" tests empty lines
5436 // Ln.11 tests just leading whitespace
5437 cx.set_state(indoc! {"
5438 «
5439 abc // No indentation
5440 abc // 1 space (< 3 so dont convert)
5441 abc // 2 spaces (< 3 so dont convert)
5442 abc // 3 spaces (convert)
5443 abc // 5 spaces (1 tab + 2 spaces)
5444 \t\t\tabc // Already tab indented
5445 \t abc // Tab followed by space
5446 \tabc // Space followed by tab (should be consumed due to tab)
5447 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5448 \t \t
5449 abc \t // Only the leading spaces should be convertedˇ»
5450 "});
5451 cx.update_editor(|e, window, cx| {
5452 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5453 });
5454 cx.assert_editor_state(indoc! {"
5455 «
5456 abc // No indentation
5457 abc // 1 space (< 3 so dont convert)
5458 abc // 2 spaces (< 3 so dont convert)
5459 \tabc // 3 spaces (convert)
5460 \t abc // 5 spaces (1 tab + 2 spaces)
5461 \t\t\tabc // Already tab indented
5462 \t abc // Tab followed by space
5463 \tabc // Space followed by tab (should be consumed due to tab)
5464 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5465 \t\t\t
5466 \tabc \t // Only the leading spaces should be convertedˇ»
5467 "});
5468}
5469
5470#[gpui::test]
5471async fn test_toggle_case(cx: &mut TestAppContext) {
5472 init_test(cx, |_| {});
5473
5474 let mut cx = EditorTestContext::new(cx).await;
5475
5476 // If all lower case -> upper case
5477 cx.set_state(indoc! {"
5478 «hello worldˇ»
5479 "});
5480 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5481 cx.assert_editor_state(indoc! {"
5482 «HELLO WORLDˇ»
5483 "});
5484
5485 // If all upper case -> lower case
5486 cx.set_state(indoc! {"
5487 «HELLO WORLDˇ»
5488 "});
5489 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5490 cx.assert_editor_state(indoc! {"
5491 «hello worldˇ»
5492 "});
5493
5494 // If any upper case characters are identified -> lower case
5495 // This matches JetBrains IDEs
5496 cx.set_state(indoc! {"
5497 «hEllo worldˇ»
5498 "});
5499 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5500 cx.assert_editor_state(indoc! {"
5501 «hello worldˇ»
5502 "});
5503}
5504
5505#[gpui::test]
5506async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5507 init_test(cx, |_| {});
5508
5509 let mut cx = EditorTestContext::new(cx).await;
5510
5511 cx.set_state(indoc! {"
5512 «implement-windows-supportˇ»
5513 "});
5514 cx.update_editor(|e, window, cx| {
5515 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5516 });
5517 cx.assert_editor_state(indoc! {"
5518 «Implement windows supportˇ»
5519 "});
5520}
5521
5522#[gpui::test]
5523async fn test_manipulate_text(cx: &mut TestAppContext) {
5524 init_test(cx, |_| {});
5525
5526 let mut cx = EditorTestContext::new(cx).await;
5527
5528 // Test convert_to_upper_case()
5529 cx.set_state(indoc! {"
5530 «hello worldˇ»
5531 "});
5532 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5533 cx.assert_editor_state(indoc! {"
5534 «HELLO WORLDˇ»
5535 "});
5536
5537 // Test convert_to_lower_case()
5538 cx.set_state(indoc! {"
5539 «HELLO WORLDˇ»
5540 "});
5541 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5542 cx.assert_editor_state(indoc! {"
5543 «hello worldˇ»
5544 "});
5545
5546 // Test multiple line, single selection case
5547 cx.set_state(indoc! {"
5548 «The quick brown
5549 fox jumps over
5550 the lazy dogˇ»
5551 "});
5552 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5553 cx.assert_editor_state(indoc! {"
5554 «The Quick Brown
5555 Fox Jumps Over
5556 The Lazy Dogˇ»
5557 "});
5558
5559 // Test multiple line, single selection case
5560 cx.set_state(indoc! {"
5561 «The quick brown
5562 fox jumps over
5563 the lazy dogˇ»
5564 "});
5565 cx.update_editor(|e, window, cx| {
5566 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5567 });
5568 cx.assert_editor_state(indoc! {"
5569 «TheQuickBrown
5570 FoxJumpsOver
5571 TheLazyDogˇ»
5572 "});
5573
5574 // From here on out, test more complex cases of manipulate_text()
5575
5576 // Test no selection case - should affect words cursors are in
5577 // Cursor at beginning, middle, and end of word
5578 cx.set_state(indoc! {"
5579 ˇhello big beauˇtiful worldˇ
5580 "});
5581 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5582 cx.assert_editor_state(indoc! {"
5583 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5584 "});
5585
5586 // Test multiple selections on a single line and across multiple lines
5587 cx.set_state(indoc! {"
5588 «Theˇ» quick «brown
5589 foxˇ» jumps «overˇ»
5590 the «lazyˇ» dog
5591 "});
5592 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5593 cx.assert_editor_state(indoc! {"
5594 «THEˇ» quick «BROWN
5595 FOXˇ» jumps «OVERˇ»
5596 the «LAZYˇ» dog
5597 "});
5598
5599 // Test case where text length grows
5600 cx.set_state(indoc! {"
5601 «tschüߡ»
5602 "});
5603 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5604 cx.assert_editor_state(indoc! {"
5605 «TSCHÜSSˇ»
5606 "});
5607
5608 // Test to make sure we don't crash when text shrinks
5609 cx.set_state(indoc! {"
5610 aaa_bbbˇ
5611 "});
5612 cx.update_editor(|e, window, cx| {
5613 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5614 });
5615 cx.assert_editor_state(indoc! {"
5616 «aaaBbbˇ»
5617 "});
5618
5619 // Test to make sure we all aware of the fact that each word can grow and shrink
5620 // Final selections should be aware of this fact
5621 cx.set_state(indoc! {"
5622 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5623 "});
5624 cx.update_editor(|e, window, cx| {
5625 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5626 });
5627 cx.assert_editor_state(indoc! {"
5628 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5629 "});
5630
5631 cx.set_state(indoc! {"
5632 «hElLo, WoRld!ˇ»
5633 "});
5634 cx.update_editor(|e, window, cx| {
5635 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5636 });
5637 cx.assert_editor_state(indoc! {"
5638 «HeLlO, wOrLD!ˇ»
5639 "});
5640
5641 // Test selections with `line_mode() = true`.
5642 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5643 cx.set_state(indoc! {"
5644 «The quick brown
5645 fox jumps over
5646 tˇ»he lazy dog
5647 "});
5648 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5649 cx.assert_editor_state(indoc! {"
5650 «THE QUICK BROWN
5651 FOX JUMPS OVER
5652 THE LAZY DOGˇ»
5653 "});
5654}
5655
5656#[gpui::test]
5657fn test_duplicate_line(cx: &mut TestAppContext) {
5658 init_test(cx, |_| {});
5659
5660 let editor = cx.add_window(|window, cx| {
5661 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5662 build_editor(buffer, window, cx)
5663 });
5664 _ = editor.update(cx, |editor, window, cx| {
5665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5666 s.select_display_ranges([
5667 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5668 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5669 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5670 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5671 ])
5672 });
5673 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5674 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5675 assert_eq!(
5676 display_ranges(editor, cx),
5677 vec![
5678 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5679 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5680 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5681 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5682 ]
5683 );
5684 });
5685
5686 let editor = cx.add_window(|window, cx| {
5687 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5688 build_editor(buffer, window, cx)
5689 });
5690 _ = editor.update(cx, |editor, window, cx| {
5691 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5692 s.select_display_ranges([
5693 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5694 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5695 ])
5696 });
5697 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5698 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5699 assert_eq!(
5700 display_ranges(editor, cx),
5701 vec![
5702 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5703 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5704 ]
5705 );
5706 });
5707
5708 // With `duplicate_line_up` the selections move to the duplicated lines,
5709 // which are inserted above the original lines
5710 let editor = cx.add_window(|window, cx| {
5711 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5712 build_editor(buffer, window, cx)
5713 });
5714 _ = editor.update(cx, |editor, window, cx| {
5715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5716 s.select_display_ranges([
5717 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5718 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5719 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5720 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5721 ])
5722 });
5723 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5724 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5725 assert_eq!(
5726 display_ranges(editor, cx),
5727 vec![
5728 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5729 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5730 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5731 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5732 ]
5733 );
5734 });
5735
5736 let editor = cx.add_window(|window, cx| {
5737 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5738 build_editor(buffer, window, cx)
5739 });
5740 _ = editor.update(cx, |editor, window, cx| {
5741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5742 s.select_display_ranges([
5743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5744 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5745 ])
5746 });
5747 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5748 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5749 assert_eq!(
5750 display_ranges(editor, cx),
5751 vec![
5752 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5753 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5754 ]
5755 );
5756 });
5757
5758 let editor = cx.add_window(|window, cx| {
5759 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5760 build_editor(buffer, window, cx)
5761 });
5762 _ = editor.update(cx, |editor, window, cx| {
5763 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5764 s.select_display_ranges([
5765 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5766 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5767 ])
5768 });
5769 editor.duplicate_selection(&DuplicateSelection, window, cx);
5770 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5771 assert_eq!(
5772 display_ranges(editor, cx),
5773 vec![
5774 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5775 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5776 ]
5777 );
5778 });
5779}
5780
5781#[gpui::test]
5782fn test_move_line_up_down(cx: &mut TestAppContext) {
5783 init_test(cx, |_| {});
5784
5785 let editor = cx.add_window(|window, cx| {
5786 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5787 build_editor(buffer, window, cx)
5788 });
5789 _ = editor.update(cx, |editor, window, cx| {
5790 editor.fold_creases(
5791 vec![
5792 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5793 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5794 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5795 ],
5796 true,
5797 window,
5798 cx,
5799 );
5800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5801 s.select_display_ranges([
5802 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5803 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5804 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5805 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5806 ])
5807 });
5808 assert_eq!(
5809 editor.display_text(cx),
5810 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5811 );
5812
5813 editor.move_line_up(&MoveLineUp, window, cx);
5814 assert_eq!(
5815 editor.display_text(cx),
5816 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5817 );
5818 assert_eq!(
5819 display_ranges(editor, cx),
5820 vec![
5821 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5822 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5823 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5824 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5825 ]
5826 );
5827 });
5828
5829 _ = editor.update(cx, |editor, window, cx| {
5830 editor.move_line_down(&MoveLineDown, window, cx);
5831 assert_eq!(
5832 editor.display_text(cx),
5833 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5834 );
5835 assert_eq!(
5836 display_ranges(editor, cx),
5837 vec![
5838 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5839 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5840 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5841 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5842 ]
5843 );
5844 });
5845
5846 _ = editor.update(cx, |editor, window, cx| {
5847 editor.move_line_down(&MoveLineDown, window, cx);
5848 assert_eq!(
5849 editor.display_text(cx),
5850 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5851 );
5852 assert_eq!(
5853 display_ranges(editor, cx),
5854 vec![
5855 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5856 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5857 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5858 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5859 ]
5860 );
5861 });
5862
5863 _ = editor.update(cx, |editor, window, cx| {
5864 editor.move_line_up(&MoveLineUp, window, cx);
5865 assert_eq!(
5866 editor.display_text(cx),
5867 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5868 );
5869 assert_eq!(
5870 display_ranges(editor, cx),
5871 vec![
5872 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5873 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5874 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5875 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5876 ]
5877 );
5878 });
5879}
5880
5881#[gpui::test]
5882fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5883 init_test(cx, |_| {});
5884 let editor = cx.add_window(|window, cx| {
5885 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5886 build_editor(buffer, window, cx)
5887 });
5888 _ = editor.update(cx, |editor, window, cx| {
5889 editor.fold_creases(
5890 vec![Crease::simple(
5891 Point::new(6, 4)..Point::new(7, 4),
5892 FoldPlaceholder::test(),
5893 )],
5894 true,
5895 window,
5896 cx,
5897 );
5898 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5899 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5900 });
5901 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5902 editor.move_line_up(&MoveLineUp, window, cx);
5903 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5904 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5905 });
5906}
5907
5908#[gpui::test]
5909fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5910 init_test(cx, |_| {});
5911
5912 let editor = cx.add_window(|window, cx| {
5913 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5914 build_editor(buffer, window, cx)
5915 });
5916 _ = editor.update(cx, |editor, window, cx| {
5917 let snapshot = editor.buffer.read(cx).snapshot(cx);
5918 editor.insert_blocks(
5919 [BlockProperties {
5920 style: BlockStyle::Fixed,
5921 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5922 height: Some(1),
5923 render: Arc::new(|_| div().into_any()),
5924 priority: 0,
5925 }],
5926 Some(Autoscroll::fit()),
5927 cx,
5928 );
5929 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5930 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5931 });
5932 editor.move_line_down(&MoveLineDown, window, cx);
5933 });
5934}
5935
5936#[gpui::test]
5937async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5938 init_test(cx, |_| {});
5939
5940 let mut cx = EditorTestContext::new(cx).await;
5941 cx.set_state(
5942 &"
5943 ˇzero
5944 one
5945 two
5946 three
5947 four
5948 five
5949 "
5950 .unindent(),
5951 );
5952
5953 // Create a four-line block that replaces three lines of text.
5954 cx.update_editor(|editor, window, cx| {
5955 let snapshot = editor.snapshot(window, cx);
5956 let snapshot = &snapshot.buffer_snapshot();
5957 let placement = BlockPlacement::Replace(
5958 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5959 );
5960 editor.insert_blocks(
5961 [BlockProperties {
5962 placement,
5963 height: Some(4),
5964 style: BlockStyle::Sticky,
5965 render: Arc::new(|_| gpui::div().into_any_element()),
5966 priority: 0,
5967 }],
5968 None,
5969 cx,
5970 );
5971 });
5972
5973 // Move down so that the cursor touches the block.
5974 cx.update_editor(|editor, window, cx| {
5975 editor.move_down(&Default::default(), window, cx);
5976 });
5977 cx.assert_editor_state(
5978 &"
5979 zero
5980 «one
5981 two
5982 threeˇ»
5983 four
5984 five
5985 "
5986 .unindent(),
5987 );
5988
5989 // Move down past the block.
5990 cx.update_editor(|editor, window, cx| {
5991 editor.move_down(&Default::default(), window, cx);
5992 });
5993 cx.assert_editor_state(
5994 &"
5995 zero
5996 one
5997 two
5998 three
5999 ˇfour
6000 five
6001 "
6002 .unindent(),
6003 );
6004}
6005
6006#[gpui::test]
6007fn test_transpose(cx: &mut TestAppContext) {
6008 init_test(cx, |_| {});
6009
6010 _ = cx.add_window(|window, cx| {
6011 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6012 editor.set_style(EditorStyle::default(), window, cx);
6013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6014 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6015 });
6016 editor.transpose(&Default::default(), window, cx);
6017 assert_eq!(editor.text(cx), "bac");
6018 assert_eq!(
6019 editor.selections.ranges(&editor.display_snapshot(cx)),
6020 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6021 );
6022
6023 editor.transpose(&Default::default(), window, cx);
6024 assert_eq!(editor.text(cx), "bca");
6025 assert_eq!(
6026 editor.selections.ranges(&editor.display_snapshot(cx)),
6027 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6028 );
6029
6030 editor.transpose(&Default::default(), window, cx);
6031 assert_eq!(editor.text(cx), "bac");
6032 assert_eq!(
6033 editor.selections.ranges(&editor.display_snapshot(cx)),
6034 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6035 );
6036
6037 editor
6038 });
6039
6040 _ = cx.add_window(|window, cx| {
6041 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6042 editor.set_style(EditorStyle::default(), window, cx);
6043 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6044 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6045 });
6046 editor.transpose(&Default::default(), window, cx);
6047 assert_eq!(editor.text(cx), "acb\nde");
6048 assert_eq!(
6049 editor.selections.ranges(&editor.display_snapshot(cx)),
6050 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6051 );
6052
6053 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6054 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6055 });
6056 editor.transpose(&Default::default(), window, cx);
6057 assert_eq!(editor.text(cx), "acbd\ne");
6058 assert_eq!(
6059 editor.selections.ranges(&editor.display_snapshot(cx)),
6060 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6061 );
6062
6063 editor.transpose(&Default::default(), window, cx);
6064 assert_eq!(editor.text(cx), "acbde\n");
6065 assert_eq!(
6066 editor.selections.ranges(&editor.display_snapshot(cx)),
6067 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6068 );
6069
6070 editor.transpose(&Default::default(), window, cx);
6071 assert_eq!(editor.text(cx), "acbd\ne");
6072 assert_eq!(
6073 editor.selections.ranges(&editor.display_snapshot(cx)),
6074 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6075 );
6076
6077 editor
6078 });
6079
6080 _ = cx.add_window(|window, cx| {
6081 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6082 editor.set_style(EditorStyle::default(), window, cx);
6083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6084 s.select_ranges([
6085 MultiBufferOffset(1)..MultiBufferOffset(1),
6086 MultiBufferOffset(2)..MultiBufferOffset(2),
6087 MultiBufferOffset(4)..MultiBufferOffset(4),
6088 ])
6089 });
6090 editor.transpose(&Default::default(), window, cx);
6091 assert_eq!(editor.text(cx), "bacd\ne");
6092 assert_eq!(
6093 editor.selections.ranges(&editor.display_snapshot(cx)),
6094 [
6095 MultiBufferOffset(2)..MultiBufferOffset(2),
6096 MultiBufferOffset(3)..MultiBufferOffset(3),
6097 MultiBufferOffset(5)..MultiBufferOffset(5)
6098 ]
6099 );
6100
6101 editor.transpose(&Default::default(), window, cx);
6102 assert_eq!(editor.text(cx), "bcade\n");
6103 assert_eq!(
6104 editor.selections.ranges(&editor.display_snapshot(cx)),
6105 [
6106 MultiBufferOffset(3)..MultiBufferOffset(3),
6107 MultiBufferOffset(4)..MultiBufferOffset(4),
6108 MultiBufferOffset(6)..MultiBufferOffset(6)
6109 ]
6110 );
6111
6112 editor.transpose(&Default::default(), window, cx);
6113 assert_eq!(editor.text(cx), "bcda\ne");
6114 assert_eq!(
6115 editor.selections.ranges(&editor.display_snapshot(cx)),
6116 [
6117 MultiBufferOffset(4)..MultiBufferOffset(4),
6118 MultiBufferOffset(6)..MultiBufferOffset(6)
6119 ]
6120 );
6121
6122 editor.transpose(&Default::default(), window, cx);
6123 assert_eq!(editor.text(cx), "bcade\n");
6124 assert_eq!(
6125 editor.selections.ranges(&editor.display_snapshot(cx)),
6126 [
6127 MultiBufferOffset(4)..MultiBufferOffset(4),
6128 MultiBufferOffset(6)..MultiBufferOffset(6)
6129 ]
6130 );
6131
6132 editor.transpose(&Default::default(), window, cx);
6133 assert_eq!(editor.text(cx), "bcaed\n");
6134 assert_eq!(
6135 editor.selections.ranges(&editor.display_snapshot(cx)),
6136 [
6137 MultiBufferOffset(5)..MultiBufferOffset(5),
6138 MultiBufferOffset(6)..MultiBufferOffset(6)
6139 ]
6140 );
6141
6142 editor
6143 });
6144
6145 _ = cx.add_window(|window, cx| {
6146 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6147 editor.set_style(EditorStyle::default(), window, cx);
6148 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6149 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6150 });
6151 editor.transpose(&Default::default(), window, cx);
6152 assert_eq!(editor.text(cx), "🏀🍐✋");
6153 assert_eq!(
6154 editor.selections.ranges(&editor.display_snapshot(cx)),
6155 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6156 );
6157
6158 editor.transpose(&Default::default(), window, cx);
6159 assert_eq!(editor.text(cx), "🏀✋🍐");
6160 assert_eq!(
6161 editor.selections.ranges(&editor.display_snapshot(cx)),
6162 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6163 );
6164
6165 editor.transpose(&Default::default(), window, cx);
6166 assert_eq!(editor.text(cx), "🏀🍐✋");
6167 assert_eq!(
6168 editor.selections.ranges(&editor.display_snapshot(cx)),
6169 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6170 );
6171
6172 editor
6173 });
6174}
6175
6176#[gpui::test]
6177async fn test_rewrap(cx: &mut TestAppContext) {
6178 init_test(cx, |settings| {
6179 settings.languages.0.extend([
6180 (
6181 "Markdown".into(),
6182 LanguageSettingsContent {
6183 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6184 preferred_line_length: Some(40),
6185 ..Default::default()
6186 },
6187 ),
6188 (
6189 "Plain Text".into(),
6190 LanguageSettingsContent {
6191 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6192 preferred_line_length: Some(40),
6193 ..Default::default()
6194 },
6195 ),
6196 (
6197 "C++".into(),
6198 LanguageSettingsContent {
6199 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6200 preferred_line_length: Some(40),
6201 ..Default::default()
6202 },
6203 ),
6204 (
6205 "Python".into(),
6206 LanguageSettingsContent {
6207 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6208 preferred_line_length: Some(40),
6209 ..Default::default()
6210 },
6211 ),
6212 (
6213 "Rust".into(),
6214 LanguageSettingsContent {
6215 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6216 preferred_line_length: Some(40),
6217 ..Default::default()
6218 },
6219 ),
6220 ])
6221 });
6222
6223 let mut cx = EditorTestContext::new(cx).await;
6224
6225 let cpp_language = Arc::new(Language::new(
6226 LanguageConfig {
6227 name: "C++".into(),
6228 line_comments: vec!["// ".into()],
6229 ..LanguageConfig::default()
6230 },
6231 None,
6232 ));
6233 let python_language = Arc::new(Language::new(
6234 LanguageConfig {
6235 name: "Python".into(),
6236 line_comments: vec!["# ".into()],
6237 ..LanguageConfig::default()
6238 },
6239 None,
6240 ));
6241 let markdown_language = Arc::new(Language::new(
6242 LanguageConfig {
6243 name: "Markdown".into(),
6244 rewrap_prefixes: vec![
6245 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6246 regex::Regex::new("[-*+]\\s+").unwrap(),
6247 ],
6248 ..LanguageConfig::default()
6249 },
6250 None,
6251 ));
6252 let rust_language = Arc::new(
6253 Language::new(
6254 LanguageConfig {
6255 name: "Rust".into(),
6256 line_comments: vec!["// ".into(), "/// ".into()],
6257 ..LanguageConfig::default()
6258 },
6259 Some(tree_sitter_rust::LANGUAGE.into()),
6260 )
6261 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6262 .unwrap(),
6263 );
6264
6265 let plaintext_language = Arc::new(Language::new(
6266 LanguageConfig {
6267 name: "Plain Text".into(),
6268 ..LanguageConfig::default()
6269 },
6270 None,
6271 ));
6272
6273 // Test basic rewrapping of a long line with a cursor
6274 assert_rewrap(
6275 indoc! {"
6276 // ˇThis is a long comment that needs to be wrapped.
6277 "},
6278 indoc! {"
6279 // ˇThis is a long comment that needs to
6280 // be wrapped.
6281 "},
6282 cpp_language.clone(),
6283 &mut cx,
6284 );
6285
6286 // Test rewrapping a full selection
6287 assert_rewrap(
6288 indoc! {"
6289 «// This selected long comment needs to be wrapped.ˇ»"
6290 },
6291 indoc! {"
6292 «// This selected long comment needs to
6293 // be wrapped.ˇ»"
6294 },
6295 cpp_language.clone(),
6296 &mut cx,
6297 );
6298
6299 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6300 assert_rewrap(
6301 indoc! {"
6302 // ˇThis is the first line.
6303 // Thisˇ is the second line.
6304 // This is the thirdˇ line, all part of one paragraph.
6305 "},
6306 indoc! {"
6307 // ˇThis is the first line. Thisˇ is the
6308 // second line. This is the thirdˇ line,
6309 // all part of one paragraph.
6310 "},
6311 cpp_language.clone(),
6312 &mut cx,
6313 );
6314
6315 // Test multiple cursors in different paragraphs trigger separate rewraps
6316 assert_rewrap(
6317 indoc! {"
6318 // ˇThis is the first paragraph, first line.
6319 // ˇThis is the first paragraph, second line.
6320
6321 // ˇThis is the second paragraph, first line.
6322 // ˇThis is the second paragraph, second line.
6323 "},
6324 indoc! {"
6325 // ˇThis is the first paragraph, first
6326 // line. ˇThis is the first paragraph,
6327 // second line.
6328
6329 // ˇThis is the second paragraph, first
6330 // line. ˇThis is the second paragraph,
6331 // second line.
6332 "},
6333 cpp_language.clone(),
6334 &mut cx,
6335 );
6336
6337 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6338 assert_rewrap(
6339 indoc! {"
6340 «// A regular long long comment to be wrapped.
6341 /// A documentation long comment to be wrapped.ˇ»
6342 "},
6343 indoc! {"
6344 «// A regular long long comment to be
6345 // wrapped.
6346 /// A documentation long comment to be
6347 /// wrapped.ˇ»
6348 "},
6349 rust_language.clone(),
6350 &mut cx,
6351 );
6352
6353 // Test that change in indentation level trigger seperate rewraps
6354 assert_rewrap(
6355 indoc! {"
6356 fn foo() {
6357 «// This is a long comment at the base indent.
6358 // This is a long comment at the next indent.ˇ»
6359 }
6360 "},
6361 indoc! {"
6362 fn foo() {
6363 «// This is a long comment at the
6364 // base indent.
6365 // This is a long comment at the
6366 // next indent.ˇ»
6367 }
6368 "},
6369 rust_language.clone(),
6370 &mut cx,
6371 );
6372
6373 // Test that different comment prefix characters (e.g., '#') are handled correctly
6374 assert_rewrap(
6375 indoc! {"
6376 # ˇThis is a long comment using a pound sign.
6377 "},
6378 indoc! {"
6379 # ˇThis is a long comment using a pound
6380 # sign.
6381 "},
6382 python_language,
6383 &mut cx,
6384 );
6385
6386 // Test rewrapping only affects comments, not code even when selected
6387 assert_rewrap(
6388 indoc! {"
6389 «/// This doc comment is long and should be wrapped.
6390 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6391 "},
6392 indoc! {"
6393 «/// This doc comment is long and should
6394 /// be wrapped.
6395 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6396 "},
6397 rust_language.clone(),
6398 &mut cx,
6399 );
6400
6401 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6402 assert_rewrap(
6403 indoc! {"
6404 # Header
6405
6406 A long long long line of markdown text to wrap.ˇ
6407 "},
6408 indoc! {"
6409 # Header
6410
6411 A long long long line of markdown text
6412 to wrap.ˇ
6413 "},
6414 markdown_language.clone(),
6415 &mut cx,
6416 );
6417
6418 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6419 assert_rewrap(
6420 indoc! {"
6421 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6422 2. This is a numbered list item that is very long and needs to be wrapped properly.
6423 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6424 "},
6425 indoc! {"
6426 «1. This is a numbered list item that is
6427 very long and needs to be wrapped
6428 properly.
6429 2. This is a numbered list item that is
6430 very long and needs to be wrapped
6431 properly.
6432 - This is an unordered list item that is
6433 also very long and should not merge
6434 with the numbered item.ˇ»
6435 "},
6436 markdown_language.clone(),
6437 &mut cx,
6438 );
6439
6440 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6441 assert_rewrap(
6442 indoc! {"
6443 «1. This is a numbered list item that is
6444 very long and needs to be wrapped
6445 properly.
6446 2. This is a numbered list item that is
6447 very long and needs to be wrapped
6448 properly.
6449 - This is an unordered list item that is
6450 also very long and should not merge with
6451 the numbered item.ˇ»
6452 "},
6453 indoc! {"
6454 «1. This is a numbered list item that is
6455 very long and needs to be wrapped
6456 properly.
6457 2. This is a numbered list item that is
6458 very long and needs to be wrapped
6459 properly.
6460 - This is an unordered list item that is
6461 also very long and should not merge
6462 with the numbered item.ˇ»
6463 "},
6464 markdown_language.clone(),
6465 &mut cx,
6466 );
6467
6468 // Test that rewrapping maintain indents even when they already exists.
6469 assert_rewrap(
6470 indoc! {"
6471 «1. This is a numbered list
6472 item that is very long and needs to be wrapped properly.
6473 2. This is a numbered list
6474 item that is very long and needs to be wrapped properly.
6475 - This is an unordered list item that is also very long and
6476 should not merge with the numbered item.ˇ»
6477 "},
6478 indoc! {"
6479 «1. This is a numbered list item that is
6480 very long and needs to be wrapped
6481 properly.
6482 2. This is a numbered list item that is
6483 very long and needs to be wrapped
6484 properly.
6485 - This is an unordered list item that is
6486 also very long and should not merge
6487 with the numbered item.ˇ»
6488 "},
6489 markdown_language,
6490 &mut cx,
6491 );
6492
6493 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6494 assert_rewrap(
6495 indoc! {"
6496 ˇThis is a very long line of plain text that will be wrapped.
6497 "},
6498 indoc! {"
6499 ˇThis is a very long line of plain text
6500 that will be wrapped.
6501 "},
6502 plaintext_language.clone(),
6503 &mut cx,
6504 );
6505
6506 // Test that non-commented code acts as a paragraph boundary within a selection
6507 assert_rewrap(
6508 indoc! {"
6509 «// This is the first long comment block to be wrapped.
6510 fn my_func(a: u32);
6511 // This is the second long comment block to be wrapped.ˇ»
6512 "},
6513 indoc! {"
6514 «// This is the first long comment block
6515 // to be wrapped.
6516 fn my_func(a: u32);
6517 // This is the second long comment block
6518 // to be wrapped.ˇ»
6519 "},
6520 rust_language,
6521 &mut cx,
6522 );
6523
6524 // Test rewrapping multiple selections, including ones with blank lines or tabs
6525 assert_rewrap(
6526 indoc! {"
6527 «ˇThis is a very long line that will be wrapped.
6528
6529 This is another paragraph in the same selection.»
6530
6531 «\tThis is a very long indented line that will be wrapped.ˇ»
6532 "},
6533 indoc! {"
6534 «ˇThis is a very long line that will be
6535 wrapped.
6536
6537 This is another paragraph in the same
6538 selection.»
6539
6540 «\tThis is a very long indented line
6541 \tthat will be wrapped.ˇ»
6542 "},
6543 plaintext_language,
6544 &mut cx,
6545 );
6546
6547 // Test that an empty comment line acts as a paragraph boundary
6548 assert_rewrap(
6549 indoc! {"
6550 // ˇThis is a long comment that will be wrapped.
6551 //
6552 // And this is another long comment that will also be wrapped.ˇ
6553 "},
6554 indoc! {"
6555 // ˇThis is a long comment that will be
6556 // wrapped.
6557 //
6558 // And this is another long comment that
6559 // will also be wrapped.ˇ
6560 "},
6561 cpp_language,
6562 &mut cx,
6563 );
6564
6565 #[track_caller]
6566 fn assert_rewrap(
6567 unwrapped_text: &str,
6568 wrapped_text: &str,
6569 language: Arc<Language>,
6570 cx: &mut EditorTestContext,
6571 ) {
6572 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
6573 cx.set_state(unwrapped_text);
6574 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6575 cx.assert_editor_state(wrapped_text);
6576 }
6577}
6578
6579#[gpui::test]
6580async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6581 init_test(cx, |settings| {
6582 settings.languages.0.extend([(
6583 "Rust".into(),
6584 LanguageSettingsContent {
6585 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6586 preferred_line_length: Some(40),
6587 ..Default::default()
6588 },
6589 )])
6590 });
6591
6592 let mut cx = EditorTestContext::new(cx).await;
6593
6594 let rust_lang = Arc::new(
6595 Language::new(
6596 LanguageConfig {
6597 name: "Rust".into(),
6598 line_comments: vec!["// ".into()],
6599 block_comment: Some(BlockCommentConfig {
6600 start: "/*".into(),
6601 end: "*/".into(),
6602 prefix: "* ".into(),
6603 tab_size: 1,
6604 }),
6605 documentation_comment: Some(BlockCommentConfig {
6606 start: "/**".into(),
6607 end: "*/".into(),
6608 prefix: "* ".into(),
6609 tab_size: 1,
6610 }),
6611
6612 ..LanguageConfig::default()
6613 },
6614 Some(tree_sitter_rust::LANGUAGE.into()),
6615 )
6616 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6617 .unwrap(),
6618 );
6619
6620 // regular block comment
6621 assert_rewrap(
6622 indoc! {"
6623 /*
6624 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6625 */
6626 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6627 "},
6628 indoc! {"
6629 /*
6630 *ˇ Lorem ipsum dolor sit amet,
6631 * consectetur adipiscing elit.
6632 */
6633 /*
6634 *ˇ Lorem ipsum dolor sit amet,
6635 * consectetur adipiscing elit.
6636 */
6637 "},
6638 rust_lang.clone(),
6639 &mut cx,
6640 );
6641
6642 // indent is respected
6643 assert_rewrap(
6644 indoc! {"
6645 {}
6646 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6647 "},
6648 indoc! {"
6649 {}
6650 /*
6651 *ˇ Lorem ipsum dolor sit amet,
6652 * consectetur adipiscing elit.
6653 */
6654 "},
6655 rust_lang.clone(),
6656 &mut cx,
6657 );
6658
6659 // short block comments with inline delimiters
6660 assert_rewrap(
6661 indoc! {"
6662 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6663 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6664 */
6665 /*
6666 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6667 "},
6668 indoc! {"
6669 /*
6670 *ˇ Lorem ipsum dolor sit amet,
6671 * consectetur adipiscing elit.
6672 */
6673 /*
6674 *ˇ Lorem ipsum dolor sit amet,
6675 * consectetur adipiscing elit.
6676 */
6677 /*
6678 *ˇ Lorem ipsum dolor sit amet,
6679 * consectetur adipiscing elit.
6680 */
6681 "},
6682 rust_lang.clone(),
6683 &mut cx,
6684 );
6685
6686 // multiline block comment with inline start/end delimiters
6687 assert_rewrap(
6688 indoc! {"
6689 /*ˇ Lorem ipsum dolor sit amet,
6690 * consectetur adipiscing elit. */
6691 "},
6692 indoc! {"
6693 /*
6694 *ˇ Lorem ipsum dolor sit amet,
6695 * consectetur adipiscing elit.
6696 */
6697 "},
6698 rust_lang.clone(),
6699 &mut cx,
6700 );
6701
6702 // block comment rewrap still respects paragraph bounds
6703 assert_rewrap(
6704 indoc! {"
6705 /*
6706 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6707 *
6708 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6709 */
6710 "},
6711 indoc! {"
6712 /*
6713 *ˇ Lorem ipsum dolor sit amet,
6714 * consectetur adipiscing elit.
6715 *
6716 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6717 */
6718 "},
6719 rust_lang.clone(),
6720 &mut cx,
6721 );
6722
6723 // documentation comments
6724 assert_rewrap(
6725 indoc! {"
6726 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6727 /**
6728 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6729 */
6730 "},
6731 indoc! {"
6732 /**
6733 *ˇ Lorem ipsum dolor sit amet,
6734 * consectetur adipiscing elit.
6735 */
6736 /**
6737 *ˇ Lorem ipsum dolor sit amet,
6738 * consectetur adipiscing elit.
6739 */
6740 "},
6741 rust_lang.clone(),
6742 &mut cx,
6743 );
6744
6745 // different, adjacent comments
6746 assert_rewrap(
6747 indoc! {"
6748 /**
6749 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6750 */
6751 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6752 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6753 "},
6754 indoc! {"
6755 /**
6756 *ˇ Lorem ipsum dolor sit amet,
6757 * consectetur adipiscing elit.
6758 */
6759 /*
6760 *ˇ Lorem ipsum dolor sit amet,
6761 * consectetur adipiscing elit.
6762 */
6763 //ˇ Lorem ipsum dolor sit amet,
6764 // consectetur adipiscing elit.
6765 "},
6766 rust_lang.clone(),
6767 &mut cx,
6768 );
6769
6770 // selection w/ single short block comment
6771 assert_rewrap(
6772 indoc! {"
6773 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6774 "},
6775 indoc! {"
6776 «/*
6777 * Lorem ipsum dolor sit amet,
6778 * consectetur adipiscing elit.
6779 */ˇ»
6780 "},
6781 rust_lang.clone(),
6782 &mut cx,
6783 );
6784
6785 // rewrapping a single comment w/ abutting comments
6786 assert_rewrap(
6787 indoc! {"
6788 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6789 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6790 "},
6791 indoc! {"
6792 /*
6793 * ˇLorem ipsum dolor sit amet,
6794 * consectetur adipiscing elit.
6795 */
6796 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6797 "},
6798 rust_lang.clone(),
6799 &mut cx,
6800 );
6801
6802 // selection w/ non-abutting short block comments
6803 assert_rewrap(
6804 indoc! {"
6805 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6806
6807 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6808 "},
6809 indoc! {"
6810 «/*
6811 * Lorem ipsum dolor sit amet,
6812 * consectetur adipiscing elit.
6813 */
6814
6815 /*
6816 * Lorem ipsum dolor sit amet,
6817 * consectetur adipiscing elit.
6818 */ˇ»
6819 "},
6820 rust_lang.clone(),
6821 &mut cx,
6822 );
6823
6824 // selection of multiline block comments
6825 assert_rewrap(
6826 indoc! {"
6827 «/* Lorem ipsum dolor sit amet,
6828 * consectetur adipiscing elit. */ˇ»
6829 "},
6830 indoc! {"
6831 «/*
6832 * Lorem ipsum dolor sit amet,
6833 * consectetur adipiscing elit.
6834 */ˇ»
6835 "},
6836 rust_lang.clone(),
6837 &mut cx,
6838 );
6839
6840 // partial selection of multiline block comments
6841 assert_rewrap(
6842 indoc! {"
6843 «/* Lorem ipsum dolor sit amet,ˇ»
6844 * consectetur adipiscing elit. */
6845 /* Lorem ipsum dolor sit amet,
6846 «* consectetur adipiscing elit. */ˇ»
6847 "},
6848 indoc! {"
6849 «/*
6850 * Lorem ipsum dolor sit amet,ˇ»
6851 * consectetur adipiscing elit. */
6852 /* Lorem ipsum dolor sit amet,
6853 «* consectetur adipiscing elit.
6854 */ˇ»
6855 "},
6856 rust_lang.clone(),
6857 &mut cx,
6858 );
6859
6860 // selection w/ abutting short block comments
6861 // TODO: should not be combined; should rewrap as 2 comments
6862 assert_rewrap(
6863 indoc! {"
6864 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6865 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6866 "},
6867 // desired behavior:
6868 // indoc! {"
6869 // «/*
6870 // * Lorem ipsum dolor sit amet,
6871 // * consectetur adipiscing elit.
6872 // */
6873 // /*
6874 // * Lorem ipsum dolor sit amet,
6875 // * consectetur adipiscing elit.
6876 // */ˇ»
6877 // "},
6878 // actual behaviour:
6879 indoc! {"
6880 «/*
6881 * Lorem ipsum dolor sit amet,
6882 * consectetur adipiscing elit. Lorem
6883 * ipsum dolor sit amet, consectetur
6884 * adipiscing elit.
6885 */ˇ»
6886 "},
6887 rust_lang.clone(),
6888 &mut cx,
6889 );
6890
6891 // TODO: same as above, but with delimiters on separate line
6892 // assert_rewrap(
6893 // indoc! {"
6894 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6895 // */
6896 // /*
6897 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6898 // "},
6899 // // desired:
6900 // // indoc! {"
6901 // // «/*
6902 // // * Lorem ipsum dolor sit amet,
6903 // // * consectetur adipiscing elit.
6904 // // */
6905 // // /*
6906 // // * Lorem ipsum dolor sit amet,
6907 // // * consectetur adipiscing elit.
6908 // // */ˇ»
6909 // // "},
6910 // // actual: (but with trailing w/s on the empty lines)
6911 // indoc! {"
6912 // «/*
6913 // * Lorem ipsum dolor sit amet,
6914 // * consectetur adipiscing elit.
6915 // *
6916 // */
6917 // /*
6918 // *
6919 // * Lorem ipsum dolor sit amet,
6920 // * consectetur adipiscing elit.
6921 // */ˇ»
6922 // "},
6923 // rust_lang.clone(),
6924 // &mut cx,
6925 // );
6926
6927 // TODO these are unhandled edge cases; not correct, just documenting known issues
6928 assert_rewrap(
6929 indoc! {"
6930 /*
6931 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6932 */
6933 /*
6934 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6935 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6936 "},
6937 // desired:
6938 // indoc! {"
6939 // /*
6940 // *ˇ Lorem ipsum dolor sit amet,
6941 // * consectetur adipiscing elit.
6942 // */
6943 // /*
6944 // *ˇ Lorem ipsum dolor sit amet,
6945 // * consectetur adipiscing elit.
6946 // */
6947 // /*
6948 // *ˇ Lorem ipsum dolor sit amet
6949 // */ /* consectetur adipiscing elit. */
6950 // "},
6951 // actual:
6952 indoc! {"
6953 /*
6954 //ˇ Lorem ipsum dolor sit amet,
6955 // consectetur adipiscing elit.
6956 */
6957 /*
6958 * //ˇ Lorem ipsum dolor sit amet,
6959 * consectetur adipiscing elit.
6960 */
6961 /*
6962 *ˇ Lorem ipsum dolor sit amet */ /*
6963 * consectetur adipiscing elit.
6964 */
6965 "},
6966 rust_lang,
6967 &mut cx,
6968 );
6969
6970 #[track_caller]
6971 fn assert_rewrap(
6972 unwrapped_text: &str,
6973 wrapped_text: &str,
6974 language: Arc<Language>,
6975 cx: &mut EditorTestContext,
6976 ) {
6977 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
6978 cx.set_state(unwrapped_text);
6979 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6980 cx.assert_editor_state(wrapped_text);
6981 }
6982}
6983
6984#[gpui::test]
6985async fn test_hard_wrap(cx: &mut TestAppContext) {
6986 init_test(cx, |_| {});
6987 let mut cx = EditorTestContext::new(cx).await;
6988
6989 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(git_commit_lang()), cx));
6990 cx.update_editor(|editor, _, cx| {
6991 editor.set_hard_wrap(Some(14), cx);
6992 });
6993
6994 cx.set_state(indoc!(
6995 "
6996 one two three ˇ
6997 "
6998 ));
6999 cx.simulate_input("four");
7000 cx.run_until_parked();
7001
7002 cx.assert_editor_state(indoc!(
7003 "
7004 one two three
7005 fourˇ
7006 "
7007 ));
7008
7009 cx.update_editor(|editor, window, cx| {
7010 editor.newline(&Default::default(), window, cx);
7011 });
7012 cx.run_until_parked();
7013 cx.assert_editor_state(indoc!(
7014 "
7015 one two three
7016 four
7017 ˇ
7018 "
7019 ));
7020
7021 cx.simulate_input("five");
7022 cx.run_until_parked();
7023 cx.assert_editor_state(indoc!(
7024 "
7025 one two three
7026 four
7027 fiveˇ
7028 "
7029 ));
7030
7031 cx.update_editor(|editor, window, cx| {
7032 editor.newline(&Default::default(), window, cx);
7033 });
7034 cx.run_until_parked();
7035 cx.simulate_input("# ");
7036 cx.run_until_parked();
7037 cx.assert_editor_state(indoc!(
7038 "
7039 one two three
7040 four
7041 five
7042 # ˇ
7043 "
7044 ));
7045
7046 cx.update_editor(|editor, window, cx| {
7047 editor.newline(&Default::default(), window, cx);
7048 });
7049 cx.run_until_parked();
7050 cx.assert_editor_state(indoc!(
7051 "
7052 one two three
7053 four
7054 five
7055 #\x20
7056 #ˇ
7057 "
7058 ));
7059
7060 cx.simulate_input(" 6");
7061 cx.run_until_parked();
7062 cx.assert_editor_state(indoc!(
7063 "
7064 one two three
7065 four
7066 five
7067 #
7068 # 6ˇ
7069 "
7070 ));
7071}
7072
7073#[gpui::test]
7074async fn test_cut_line_ends(cx: &mut TestAppContext) {
7075 init_test(cx, |_| {});
7076
7077 let mut cx = EditorTestContext::new(cx).await;
7078
7079 cx.set_state(indoc! {"The quick brownˇ"});
7080 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7081 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7082
7083 cx.set_state(indoc! {"The emacs foxˇ"});
7084 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7085 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7086
7087 cx.set_state(indoc! {"
7088 The quick« brownˇ»
7089 fox jumps overˇ
7090 the lazy dog"});
7091 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7092 cx.assert_editor_state(indoc! {"
7093 The quickˇ
7094 ˇthe lazy dog"});
7095
7096 cx.set_state(indoc! {"
7097 The quick« brownˇ»
7098 fox jumps overˇ
7099 the lazy dog"});
7100 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7101 cx.assert_editor_state(indoc! {"
7102 The quickˇ
7103 fox jumps overˇthe lazy dog"});
7104
7105 cx.set_state(indoc! {"
7106 The quick« brownˇ»
7107 fox jumps overˇ
7108 the lazy dog"});
7109 cx.update_editor(|e, window, cx| {
7110 e.cut_to_end_of_line(
7111 &CutToEndOfLine {
7112 stop_at_newlines: true,
7113 },
7114 window,
7115 cx,
7116 )
7117 });
7118 cx.assert_editor_state(indoc! {"
7119 The quickˇ
7120 fox jumps overˇ
7121 the lazy dog"});
7122
7123 cx.set_state(indoc! {"
7124 The quick« brownˇ»
7125 fox jumps overˇ
7126 the lazy dog"});
7127 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7128 cx.assert_editor_state(indoc! {"
7129 The quickˇ
7130 fox jumps overˇthe lazy dog"});
7131}
7132
7133#[gpui::test]
7134async fn test_clipboard(cx: &mut TestAppContext) {
7135 init_test(cx, |_| {});
7136
7137 let mut cx = EditorTestContext::new(cx).await;
7138
7139 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7140 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7141 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7142
7143 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7144 cx.set_state("two ˇfour ˇsix ˇ");
7145 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7146 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7147
7148 // Paste again but with only two cursors. Since the number of cursors doesn't
7149 // match the number of slices in the clipboard, the entire clipboard text
7150 // is pasted at each cursor.
7151 cx.set_state("ˇtwo one✅ four three six five ˇ");
7152 cx.update_editor(|e, window, cx| {
7153 e.handle_input("( ", window, cx);
7154 e.paste(&Paste, window, cx);
7155 e.handle_input(") ", window, cx);
7156 });
7157 cx.assert_editor_state(
7158 &([
7159 "( one✅ ",
7160 "three ",
7161 "five ) ˇtwo one✅ four three six five ( one✅ ",
7162 "three ",
7163 "five ) ˇ",
7164 ]
7165 .join("\n")),
7166 );
7167
7168 // Cut with three selections, one of which is full-line.
7169 cx.set_state(indoc! {"
7170 1«2ˇ»3
7171 4ˇ567
7172 «8ˇ»9"});
7173 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7174 cx.assert_editor_state(indoc! {"
7175 1ˇ3
7176 ˇ9"});
7177
7178 // Paste with three selections, noticing how the copied selection that was full-line
7179 // gets inserted before the second cursor.
7180 cx.set_state(indoc! {"
7181 1ˇ3
7182 9ˇ
7183 «oˇ»ne"});
7184 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7185 cx.assert_editor_state(indoc! {"
7186 12ˇ3
7187 4567
7188 9ˇ
7189 8ˇne"});
7190
7191 // Copy with a single cursor only, which writes the whole line into the clipboard.
7192 cx.set_state(indoc! {"
7193 The quick brown
7194 fox juˇmps over
7195 the lazy dog"});
7196 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7197 assert_eq!(
7198 cx.read_from_clipboard()
7199 .and_then(|item| item.text().as_deref().map(str::to_string)),
7200 Some("fox jumps over\n".to_string())
7201 );
7202
7203 // Paste with three selections, noticing how the copied full-line selection is inserted
7204 // before the empty selections but replaces the selection that is non-empty.
7205 cx.set_state(indoc! {"
7206 Tˇhe quick brown
7207 «foˇ»x jumps over
7208 tˇhe lazy dog"});
7209 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7210 cx.assert_editor_state(indoc! {"
7211 fox jumps over
7212 Tˇhe quick brown
7213 fox jumps over
7214 ˇx jumps over
7215 fox jumps over
7216 tˇhe lazy dog"});
7217}
7218
7219#[gpui::test]
7220async fn test_copy_trim(cx: &mut TestAppContext) {
7221 init_test(cx, |_| {});
7222
7223 let mut cx = EditorTestContext::new(cx).await;
7224 cx.set_state(
7225 r#" «for selection in selections.iter() {
7226 let mut start = selection.start;
7227 let mut end = selection.end;
7228 let is_entire_line = selection.is_empty();
7229 if is_entire_line {
7230 start = Point::new(start.row, 0);ˇ»
7231 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7232 }
7233 "#,
7234 );
7235 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7236 assert_eq!(
7237 cx.read_from_clipboard()
7238 .and_then(|item| item.text().as_deref().map(str::to_string)),
7239 Some(
7240 "for selection in selections.iter() {
7241 let mut start = selection.start;
7242 let mut end = selection.end;
7243 let is_entire_line = selection.is_empty();
7244 if is_entire_line {
7245 start = Point::new(start.row, 0);"
7246 .to_string()
7247 ),
7248 "Regular copying preserves all indentation selected",
7249 );
7250 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7251 assert_eq!(
7252 cx.read_from_clipboard()
7253 .and_then(|item| item.text().as_deref().map(str::to_string)),
7254 Some(
7255 "for selection in selections.iter() {
7256let mut start = selection.start;
7257let mut end = selection.end;
7258let is_entire_line = selection.is_empty();
7259if is_entire_line {
7260 start = Point::new(start.row, 0);"
7261 .to_string()
7262 ),
7263 "Copying with stripping should strip all leading whitespaces"
7264 );
7265
7266 cx.set_state(
7267 r#" « for selection in selections.iter() {
7268 let mut start = selection.start;
7269 let mut end = selection.end;
7270 let is_entire_line = selection.is_empty();
7271 if is_entire_line {
7272 start = Point::new(start.row, 0);ˇ»
7273 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7274 }
7275 "#,
7276 );
7277 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7278 assert_eq!(
7279 cx.read_from_clipboard()
7280 .and_then(|item| item.text().as_deref().map(str::to_string)),
7281 Some(
7282 " for selection in selections.iter() {
7283 let mut start = selection.start;
7284 let mut end = selection.end;
7285 let is_entire_line = selection.is_empty();
7286 if is_entire_line {
7287 start = Point::new(start.row, 0);"
7288 .to_string()
7289 ),
7290 "Regular copying preserves all indentation selected",
7291 );
7292 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7293 assert_eq!(
7294 cx.read_from_clipboard()
7295 .and_then(|item| item.text().as_deref().map(str::to_string)),
7296 Some(
7297 "for selection in selections.iter() {
7298let mut start = selection.start;
7299let mut end = selection.end;
7300let is_entire_line = selection.is_empty();
7301if is_entire_line {
7302 start = Point::new(start.row, 0);"
7303 .to_string()
7304 ),
7305 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7306 );
7307
7308 cx.set_state(
7309 r#" «ˇ for selection in selections.iter() {
7310 let mut start = selection.start;
7311 let mut end = selection.end;
7312 let is_entire_line = selection.is_empty();
7313 if is_entire_line {
7314 start = Point::new(start.row, 0);»
7315 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7316 }
7317 "#,
7318 );
7319 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7320 assert_eq!(
7321 cx.read_from_clipboard()
7322 .and_then(|item| item.text().as_deref().map(str::to_string)),
7323 Some(
7324 " for selection in selections.iter() {
7325 let mut start = selection.start;
7326 let mut end = selection.end;
7327 let is_entire_line = selection.is_empty();
7328 if is_entire_line {
7329 start = Point::new(start.row, 0);"
7330 .to_string()
7331 ),
7332 "Regular copying for reverse selection works the same",
7333 );
7334 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7335 assert_eq!(
7336 cx.read_from_clipboard()
7337 .and_then(|item| item.text().as_deref().map(str::to_string)),
7338 Some(
7339 "for selection in selections.iter() {
7340let mut start = selection.start;
7341let mut end = selection.end;
7342let is_entire_line = selection.is_empty();
7343if is_entire_line {
7344 start = Point::new(start.row, 0);"
7345 .to_string()
7346 ),
7347 "Copying with stripping for reverse selection works the same"
7348 );
7349
7350 cx.set_state(
7351 r#" for selection «in selections.iter() {
7352 let mut start = selection.start;
7353 let mut end = selection.end;
7354 let is_entire_line = selection.is_empty();
7355 if is_entire_line {
7356 start = Point::new(start.row, 0);ˇ»
7357 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7358 }
7359 "#,
7360 );
7361 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7362 assert_eq!(
7363 cx.read_from_clipboard()
7364 .and_then(|item| item.text().as_deref().map(str::to_string)),
7365 Some(
7366 "in selections.iter() {
7367 let mut start = selection.start;
7368 let mut end = selection.end;
7369 let is_entire_line = selection.is_empty();
7370 if is_entire_line {
7371 start = Point::new(start.row, 0);"
7372 .to_string()
7373 ),
7374 "When selecting past the indent, the copying works as usual",
7375 );
7376 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7377 assert_eq!(
7378 cx.read_from_clipboard()
7379 .and_then(|item| item.text().as_deref().map(str::to_string)),
7380 Some(
7381 "in selections.iter() {
7382 let mut start = selection.start;
7383 let mut end = selection.end;
7384 let is_entire_line = selection.is_empty();
7385 if is_entire_line {
7386 start = Point::new(start.row, 0);"
7387 .to_string()
7388 ),
7389 "When selecting past the indent, nothing is trimmed"
7390 );
7391
7392 cx.set_state(
7393 r#" «for selection in selections.iter() {
7394 let mut start = selection.start;
7395
7396 let mut end = selection.end;
7397 let is_entire_line = selection.is_empty();
7398 if is_entire_line {
7399 start = Point::new(start.row, 0);
7400ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7401 }
7402 "#,
7403 );
7404 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7405 assert_eq!(
7406 cx.read_from_clipboard()
7407 .and_then(|item| item.text().as_deref().map(str::to_string)),
7408 Some(
7409 "for selection in selections.iter() {
7410let mut start = selection.start;
7411
7412let mut end = selection.end;
7413let is_entire_line = selection.is_empty();
7414if is_entire_line {
7415 start = Point::new(start.row, 0);
7416"
7417 .to_string()
7418 ),
7419 "Copying with stripping should ignore empty lines"
7420 );
7421}
7422
7423#[gpui::test]
7424async fn test_paste_multiline(cx: &mut TestAppContext) {
7425 init_test(cx, |_| {});
7426
7427 let mut cx = EditorTestContext::new(cx).await;
7428 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(rust_lang()), cx));
7429
7430 // Cut an indented block, without the leading whitespace.
7431 cx.set_state(indoc! {"
7432 const a: B = (
7433 c(),
7434 «d(
7435 e,
7436 f
7437 )ˇ»
7438 );
7439 "});
7440 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7441 cx.assert_editor_state(indoc! {"
7442 const a: B = (
7443 c(),
7444 ˇ
7445 );
7446 "});
7447
7448 // Paste it at the same position.
7449 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7450 cx.assert_editor_state(indoc! {"
7451 const a: B = (
7452 c(),
7453 d(
7454 e,
7455 f
7456 )ˇ
7457 );
7458 "});
7459
7460 // Paste it at a line with a lower indent level.
7461 cx.set_state(indoc! {"
7462 ˇ
7463 const a: B = (
7464 c(),
7465 );
7466 "});
7467 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7468 cx.assert_editor_state(indoc! {"
7469 d(
7470 e,
7471 f
7472 )ˇ
7473 const a: B = (
7474 c(),
7475 );
7476 "});
7477
7478 // Cut an indented block, with the leading whitespace.
7479 cx.set_state(indoc! {"
7480 const a: B = (
7481 c(),
7482 « d(
7483 e,
7484 f
7485 )
7486 ˇ»);
7487 "});
7488 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7489 cx.assert_editor_state(indoc! {"
7490 const a: B = (
7491 c(),
7492 ˇ);
7493 "});
7494
7495 // Paste it at the same position.
7496 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7497 cx.assert_editor_state(indoc! {"
7498 const a: B = (
7499 c(),
7500 d(
7501 e,
7502 f
7503 )
7504 ˇ);
7505 "});
7506
7507 // Paste it at a line with a higher indent level.
7508 cx.set_state(indoc! {"
7509 const a: B = (
7510 c(),
7511 d(
7512 e,
7513 fˇ
7514 )
7515 );
7516 "});
7517 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7518 cx.assert_editor_state(indoc! {"
7519 const a: B = (
7520 c(),
7521 d(
7522 e,
7523 f d(
7524 e,
7525 f
7526 )
7527 ˇ
7528 )
7529 );
7530 "});
7531
7532 // Copy an indented block, starting mid-line
7533 cx.set_state(indoc! {"
7534 const a: B = (
7535 c(),
7536 somethin«g(
7537 e,
7538 f
7539 )ˇ»
7540 );
7541 "});
7542 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7543
7544 // Paste it on a line with a lower indent level
7545 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7546 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7547 cx.assert_editor_state(indoc! {"
7548 const a: B = (
7549 c(),
7550 something(
7551 e,
7552 f
7553 )
7554 );
7555 g(
7556 e,
7557 f
7558 )ˇ"});
7559}
7560
7561#[gpui::test]
7562async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7563 init_test(cx, |_| {});
7564
7565 cx.write_to_clipboard(ClipboardItem::new_string(
7566 " d(\n e\n );\n".into(),
7567 ));
7568
7569 let mut cx = EditorTestContext::new(cx).await;
7570 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(rust_lang()), cx));
7571
7572 cx.set_state(indoc! {"
7573 fn a() {
7574 b();
7575 if c() {
7576 ˇ
7577 }
7578 }
7579 "});
7580
7581 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7582 cx.assert_editor_state(indoc! {"
7583 fn a() {
7584 b();
7585 if c() {
7586 d(
7587 e
7588 );
7589 ˇ
7590 }
7591 }
7592 "});
7593
7594 cx.set_state(indoc! {"
7595 fn a() {
7596 b();
7597 ˇ
7598 }
7599 "});
7600
7601 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7602 cx.assert_editor_state(indoc! {"
7603 fn a() {
7604 b();
7605 d(
7606 e
7607 );
7608 ˇ
7609 }
7610 "});
7611}
7612
7613#[gpui::test]
7614fn test_select_all(cx: &mut TestAppContext) {
7615 init_test(cx, |_| {});
7616
7617 let editor = cx.add_window(|window, cx| {
7618 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7619 build_editor(buffer, window, cx)
7620 });
7621 _ = editor.update(cx, |editor, window, cx| {
7622 editor.select_all(&SelectAll, window, cx);
7623 assert_eq!(
7624 display_ranges(editor, cx),
7625 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7626 );
7627 });
7628}
7629
7630#[gpui::test]
7631fn test_select_line(cx: &mut TestAppContext) {
7632 init_test(cx, |_| {});
7633
7634 let editor = cx.add_window(|window, cx| {
7635 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7636 build_editor(buffer, window, cx)
7637 });
7638 _ = editor.update(cx, |editor, window, cx| {
7639 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7640 s.select_display_ranges([
7641 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7642 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7643 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7644 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7645 ])
7646 });
7647 editor.select_line(&SelectLine, window, cx);
7648 assert_eq!(
7649 display_ranges(editor, cx),
7650 vec![
7651 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7652 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7653 ]
7654 );
7655 });
7656
7657 _ = editor.update(cx, |editor, window, cx| {
7658 editor.select_line(&SelectLine, window, cx);
7659 assert_eq!(
7660 display_ranges(editor, cx),
7661 vec![
7662 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7663 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7664 ]
7665 );
7666 });
7667
7668 _ = editor.update(cx, |editor, window, cx| {
7669 editor.select_line(&SelectLine, window, cx);
7670 assert_eq!(
7671 display_ranges(editor, cx),
7672 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7673 );
7674 });
7675}
7676
7677#[gpui::test]
7678async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7679 init_test(cx, |_| {});
7680 let mut cx = EditorTestContext::new(cx).await;
7681
7682 #[track_caller]
7683 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7684 cx.set_state(initial_state);
7685 cx.update_editor(|e, window, cx| {
7686 e.split_selection_into_lines(&Default::default(), window, cx)
7687 });
7688 cx.assert_editor_state(expected_state);
7689 }
7690
7691 // Selection starts and ends at the middle of lines, left-to-right
7692 test(
7693 &mut cx,
7694 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7695 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7696 );
7697 // Same thing, right-to-left
7698 test(
7699 &mut cx,
7700 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7701 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7702 );
7703
7704 // Whole buffer, left-to-right, last line *doesn't* end with newline
7705 test(
7706 &mut cx,
7707 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7708 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7709 );
7710 // Same thing, right-to-left
7711 test(
7712 &mut cx,
7713 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7714 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7715 );
7716
7717 // Whole buffer, left-to-right, last line ends with newline
7718 test(
7719 &mut cx,
7720 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7721 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7722 );
7723 // Same thing, right-to-left
7724 test(
7725 &mut cx,
7726 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7727 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7728 );
7729
7730 // Starts at the end of a line, ends at the start of another
7731 test(
7732 &mut cx,
7733 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7734 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7735 );
7736}
7737
7738#[gpui::test]
7739async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7740 init_test(cx, |_| {});
7741
7742 let editor = cx.add_window(|window, cx| {
7743 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7744 build_editor(buffer, window, cx)
7745 });
7746
7747 // setup
7748 _ = editor.update(cx, |editor, window, cx| {
7749 editor.fold_creases(
7750 vec![
7751 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7752 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7753 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7754 ],
7755 true,
7756 window,
7757 cx,
7758 );
7759 assert_eq!(
7760 editor.display_text(cx),
7761 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7762 );
7763 });
7764
7765 _ = editor.update(cx, |editor, window, cx| {
7766 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7767 s.select_display_ranges([
7768 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7769 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7770 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7771 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7772 ])
7773 });
7774 editor.split_selection_into_lines(&Default::default(), window, cx);
7775 assert_eq!(
7776 editor.display_text(cx),
7777 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7778 );
7779 });
7780 EditorTestContext::for_editor(editor, cx)
7781 .await
7782 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7783
7784 _ = editor.update(cx, |editor, window, cx| {
7785 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7786 s.select_display_ranges([
7787 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7788 ])
7789 });
7790 editor.split_selection_into_lines(&Default::default(), window, cx);
7791 assert_eq!(
7792 editor.display_text(cx),
7793 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7794 );
7795 assert_eq!(
7796 display_ranges(editor, cx),
7797 [
7798 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7799 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7800 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7801 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7802 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7803 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7804 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7805 ]
7806 );
7807 });
7808 EditorTestContext::for_editor(editor, cx)
7809 .await
7810 .assert_editor_state(
7811 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7812 );
7813}
7814
7815#[gpui::test]
7816async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7817 init_test(cx, |_| {});
7818
7819 let mut cx = EditorTestContext::new(cx).await;
7820
7821 cx.set_state(indoc!(
7822 r#"abc
7823 defˇghi
7824
7825 jk
7826 nlmo
7827 "#
7828 ));
7829
7830 cx.update_editor(|editor, window, cx| {
7831 editor.add_selection_above(&Default::default(), window, cx);
7832 });
7833
7834 cx.assert_editor_state(indoc!(
7835 r#"abcˇ
7836 defˇghi
7837
7838 jk
7839 nlmo
7840 "#
7841 ));
7842
7843 cx.update_editor(|editor, window, cx| {
7844 editor.add_selection_above(&Default::default(), window, cx);
7845 });
7846
7847 cx.assert_editor_state(indoc!(
7848 r#"abcˇ
7849 defˇghi
7850
7851 jk
7852 nlmo
7853 "#
7854 ));
7855
7856 cx.update_editor(|editor, window, cx| {
7857 editor.add_selection_below(&Default::default(), window, cx);
7858 });
7859
7860 cx.assert_editor_state(indoc!(
7861 r#"abc
7862 defˇghi
7863
7864 jk
7865 nlmo
7866 "#
7867 ));
7868
7869 cx.update_editor(|editor, window, cx| {
7870 editor.undo_selection(&Default::default(), window, cx);
7871 });
7872
7873 cx.assert_editor_state(indoc!(
7874 r#"abcˇ
7875 defˇghi
7876
7877 jk
7878 nlmo
7879 "#
7880 ));
7881
7882 cx.update_editor(|editor, window, cx| {
7883 editor.redo_selection(&Default::default(), window, cx);
7884 });
7885
7886 cx.assert_editor_state(indoc!(
7887 r#"abc
7888 defˇghi
7889
7890 jk
7891 nlmo
7892 "#
7893 ));
7894
7895 cx.update_editor(|editor, window, cx| {
7896 editor.add_selection_below(&Default::default(), window, cx);
7897 });
7898
7899 cx.assert_editor_state(indoc!(
7900 r#"abc
7901 defˇghi
7902 ˇ
7903 jk
7904 nlmo
7905 "#
7906 ));
7907
7908 cx.update_editor(|editor, window, cx| {
7909 editor.add_selection_below(&Default::default(), window, cx);
7910 });
7911
7912 cx.assert_editor_state(indoc!(
7913 r#"abc
7914 defˇghi
7915 ˇ
7916 jkˇ
7917 nlmo
7918 "#
7919 ));
7920
7921 cx.update_editor(|editor, window, cx| {
7922 editor.add_selection_below(&Default::default(), window, cx);
7923 });
7924
7925 cx.assert_editor_state(indoc!(
7926 r#"abc
7927 defˇghi
7928 ˇ
7929 jkˇ
7930 nlmˇo
7931 "#
7932 ));
7933
7934 cx.update_editor(|editor, window, cx| {
7935 editor.add_selection_below(&Default::default(), window, cx);
7936 });
7937
7938 cx.assert_editor_state(indoc!(
7939 r#"abc
7940 defˇghi
7941 ˇ
7942 jkˇ
7943 nlmˇo
7944 ˇ"#
7945 ));
7946
7947 // change selections
7948 cx.set_state(indoc!(
7949 r#"abc
7950 def«ˇg»hi
7951
7952 jk
7953 nlmo
7954 "#
7955 ));
7956
7957 cx.update_editor(|editor, window, cx| {
7958 editor.add_selection_below(&Default::default(), window, cx);
7959 });
7960
7961 cx.assert_editor_state(indoc!(
7962 r#"abc
7963 def«ˇg»hi
7964
7965 jk
7966 nlm«ˇo»
7967 "#
7968 ));
7969
7970 cx.update_editor(|editor, window, cx| {
7971 editor.add_selection_below(&Default::default(), window, cx);
7972 });
7973
7974 cx.assert_editor_state(indoc!(
7975 r#"abc
7976 def«ˇg»hi
7977
7978 jk
7979 nlm«ˇo»
7980 "#
7981 ));
7982
7983 cx.update_editor(|editor, window, cx| {
7984 editor.add_selection_above(&Default::default(), window, cx);
7985 });
7986
7987 cx.assert_editor_state(indoc!(
7988 r#"abc
7989 def«ˇg»hi
7990
7991 jk
7992 nlmo
7993 "#
7994 ));
7995
7996 cx.update_editor(|editor, window, cx| {
7997 editor.add_selection_above(&Default::default(), window, cx);
7998 });
7999
8000 cx.assert_editor_state(indoc!(
8001 r#"abc
8002 def«ˇg»hi
8003
8004 jk
8005 nlmo
8006 "#
8007 ));
8008
8009 // Change selections again
8010 cx.set_state(indoc!(
8011 r#"a«bc
8012 defgˇ»hi
8013
8014 jk
8015 nlmo
8016 "#
8017 ));
8018
8019 cx.update_editor(|editor, window, cx| {
8020 editor.add_selection_below(&Default::default(), window, cx);
8021 });
8022
8023 cx.assert_editor_state(indoc!(
8024 r#"a«bcˇ»
8025 d«efgˇ»hi
8026
8027 j«kˇ»
8028 nlmo
8029 "#
8030 ));
8031
8032 cx.update_editor(|editor, window, cx| {
8033 editor.add_selection_below(&Default::default(), window, cx);
8034 });
8035 cx.assert_editor_state(indoc!(
8036 r#"a«bcˇ»
8037 d«efgˇ»hi
8038
8039 j«kˇ»
8040 n«lmoˇ»
8041 "#
8042 ));
8043 cx.update_editor(|editor, window, cx| {
8044 editor.add_selection_above(&Default::default(), window, cx);
8045 });
8046
8047 cx.assert_editor_state(indoc!(
8048 r#"a«bcˇ»
8049 d«efgˇ»hi
8050
8051 j«kˇ»
8052 nlmo
8053 "#
8054 ));
8055
8056 // Change selections again
8057 cx.set_state(indoc!(
8058 r#"abc
8059 d«ˇefghi
8060
8061 jk
8062 nlm»o
8063 "#
8064 ));
8065
8066 cx.update_editor(|editor, window, cx| {
8067 editor.add_selection_above(&Default::default(), window, cx);
8068 });
8069
8070 cx.assert_editor_state(indoc!(
8071 r#"a«ˇbc»
8072 d«ˇef»ghi
8073
8074 j«ˇk»
8075 n«ˇlm»o
8076 "#
8077 ));
8078
8079 cx.update_editor(|editor, window, cx| {
8080 editor.add_selection_below(&Default::default(), window, cx);
8081 });
8082
8083 cx.assert_editor_state(indoc!(
8084 r#"abc
8085 d«ˇef»ghi
8086
8087 j«ˇk»
8088 n«ˇlm»o
8089 "#
8090 ));
8091}
8092
8093#[gpui::test]
8094async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8095 init_test(cx, |_| {});
8096 let mut cx = EditorTestContext::new(cx).await;
8097
8098 cx.set_state(indoc!(
8099 r#"line onˇe
8100 liˇne two
8101 line three
8102 line four"#
8103 ));
8104
8105 cx.update_editor(|editor, window, cx| {
8106 editor.add_selection_below(&Default::default(), window, cx);
8107 });
8108
8109 // test multiple cursors expand in the same direction
8110 cx.assert_editor_state(indoc!(
8111 r#"line onˇe
8112 liˇne twˇo
8113 liˇne three
8114 line four"#
8115 ));
8116
8117 cx.update_editor(|editor, window, cx| {
8118 editor.add_selection_below(&Default::default(), window, cx);
8119 });
8120
8121 cx.update_editor(|editor, window, cx| {
8122 editor.add_selection_below(&Default::default(), window, cx);
8123 });
8124
8125 // test multiple cursors expand below overflow
8126 cx.assert_editor_state(indoc!(
8127 r#"line onˇe
8128 liˇne twˇo
8129 liˇne thˇree
8130 liˇne foˇur"#
8131 ));
8132
8133 cx.update_editor(|editor, window, cx| {
8134 editor.add_selection_above(&Default::default(), window, cx);
8135 });
8136
8137 // test multiple cursors retrieves back correctly
8138 cx.assert_editor_state(indoc!(
8139 r#"line onˇe
8140 liˇne twˇo
8141 liˇne thˇree
8142 line four"#
8143 ));
8144
8145 cx.update_editor(|editor, window, cx| {
8146 editor.add_selection_above(&Default::default(), window, cx);
8147 });
8148
8149 cx.update_editor(|editor, window, cx| {
8150 editor.add_selection_above(&Default::default(), window, cx);
8151 });
8152
8153 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8154 cx.assert_editor_state(indoc!(
8155 r#"liˇne onˇe
8156 liˇne two
8157 line three
8158 line four"#
8159 ));
8160
8161 cx.update_editor(|editor, window, cx| {
8162 editor.undo_selection(&Default::default(), window, cx);
8163 });
8164
8165 // test undo
8166 cx.assert_editor_state(indoc!(
8167 r#"line onˇe
8168 liˇne twˇo
8169 line three
8170 line four"#
8171 ));
8172
8173 cx.update_editor(|editor, window, cx| {
8174 editor.redo_selection(&Default::default(), window, cx);
8175 });
8176
8177 // test redo
8178 cx.assert_editor_state(indoc!(
8179 r#"liˇne onˇe
8180 liˇne two
8181 line three
8182 line four"#
8183 ));
8184
8185 cx.set_state(indoc!(
8186 r#"abcd
8187 ef«ghˇ»
8188 ijkl
8189 «mˇ»nop"#
8190 ));
8191
8192 cx.update_editor(|editor, window, cx| {
8193 editor.add_selection_above(&Default::default(), window, cx);
8194 });
8195
8196 // test multiple selections expand in the same direction
8197 cx.assert_editor_state(indoc!(
8198 r#"ab«cdˇ»
8199 ef«ghˇ»
8200 «iˇ»jkl
8201 «mˇ»nop"#
8202 ));
8203
8204 cx.update_editor(|editor, window, cx| {
8205 editor.add_selection_above(&Default::default(), window, cx);
8206 });
8207
8208 // test multiple selection upward overflow
8209 cx.assert_editor_state(indoc!(
8210 r#"ab«cdˇ»
8211 «eˇ»f«ghˇ»
8212 «iˇ»jkl
8213 «mˇ»nop"#
8214 ));
8215
8216 cx.update_editor(|editor, window, cx| {
8217 editor.add_selection_below(&Default::default(), window, cx);
8218 });
8219
8220 // test multiple selection retrieves back correctly
8221 cx.assert_editor_state(indoc!(
8222 r#"abcd
8223 ef«ghˇ»
8224 «iˇ»jkl
8225 «mˇ»nop"#
8226 ));
8227
8228 cx.update_editor(|editor, window, cx| {
8229 editor.add_selection_below(&Default::default(), window, cx);
8230 });
8231
8232 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8233 cx.assert_editor_state(indoc!(
8234 r#"abcd
8235 ef«ghˇ»
8236 ij«klˇ»
8237 «mˇ»nop"#
8238 ));
8239
8240 cx.update_editor(|editor, window, cx| {
8241 editor.undo_selection(&Default::default(), window, cx);
8242 });
8243
8244 // test undo
8245 cx.assert_editor_state(indoc!(
8246 r#"abcd
8247 ef«ghˇ»
8248 «iˇ»jkl
8249 «mˇ»nop"#
8250 ));
8251
8252 cx.update_editor(|editor, window, cx| {
8253 editor.redo_selection(&Default::default(), window, cx);
8254 });
8255
8256 // test redo
8257 cx.assert_editor_state(indoc!(
8258 r#"abcd
8259 ef«ghˇ»
8260 ij«klˇ»
8261 «mˇ»nop"#
8262 ));
8263}
8264
8265#[gpui::test]
8266async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8267 init_test(cx, |_| {});
8268 let mut cx = EditorTestContext::new(cx).await;
8269
8270 cx.set_state(indoc!(
8271 r#"line onˇe
8272 liˇne two
8273 line three
8274 line four"#
8275 ));
8276
8277 cx.update_editor(|editor, window, cx| {
8278 editor.add_selection_below(&Default::default(), window, cx);
8279 editor.add_selection_below(&Default::default(), window, cx);
8280 editor.add_selection_below(&Default::default(), window, cx);
8281 });
8282
8283 // initial state with two multi cursor groups
8284 cx.assert_editor_state(indoc!(
8285 r#"line onˇe
8286 liˇne twˇo
8287 liˇne thˇree
8288 liˇne foˇur"#
8289 ));
8290
8291 // add single cursor in middle - simulate opt click
8292 cx.update_editor(|editor, window, cx| {
8293 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8294 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8295 editor.end_selection(window, cx);
8296 });
8297
8298 cx.assert_editor_state(indoc!(
8299 r#"line onˇe
8300 liˇne twˇo
8301 liˇneˇ thˇree
8302 liˇne foˇur"#
8303 ));
8304
8305 cx.update_editor(|editor, window, cx| {
8306 editor.add_selection_above(&Default::default(), window, cx);
8307 });
8308
8309 // test new added selection expands above and existing selection shrinks
8310 cx.assert_editor_state(indoc!(
8311 r#"line onˇe
8312 liˇneˇ twˇo
8313 liˇneˇ thˇree
8314 line four"#
8315 ));
8316
8317 cx.update_editor(|editor, window, cx| {
8318 editor.add_selection_above(&Default::default(), window, cx);
8319 });
8320
8321 // test new added selection expands above and existing selection shrinks
8322 cx.assert_editor_state(indoc!(
8323 r#"lineˇ onˇe
8324 liˇneˇ twˇo
8325 lineˇ three
8326 line four"#
8327 ));
8328
8329 // intial state with two selection groups
8330 cx.set_state(indoc!(
8331 r#"abcd
8332 ef«ghˇ»
8333 ijkl
8334 «mˇ»nop"#
8335 ));
8336
8337 cx.update_editor(|editor, window, cx| {
8338 editor.add_selection_above(&Default::default(), window, cx);
8339 editor.add_selection_above(&Default::default(), window, cx);
8340 });
8341
8342 cx.assert_editor_state(indoc!(
8343 r#"ab«cdˇ»
8344 «eˇ»f«ghˇ»
8345 «iˇ»jkl
8346 «mˇ»nop"#
8347 ));
8348
8349 // add single selection in middle - simulate opt drag
8350 cx.update_editor(|editor, window, cx| {
8351 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8352 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8353 editor.update_selection(
8354 DisplayPoint::new(DisplayRow(2), 4),
8355 0,
8356 gpui::Point::<f32>::default(),
8357 window,
8358 cx,
8359 );
8360 editor.end_selection(window, cx);
8361 });
8362
8363 cx.assert_editor_state(indoc!(
8364 r#"ab«cdˇ»
8365 «eˇ»f«ghˇ»
8366 «iˇ»jk«lˇ»
8367 «mˇ»nop"#
8368 ));
8369
8370 cx.update_editor(|editor, window, cx| {
8371 editor.add_selection_below(&Default::default(), window, cx);
8372 });
8373
8374 // test new added selection expands below, others shrinks from above
8375 cx.assert_editor_state(indoc!(
8376 r#"abcd
8377 ef«ghˇ»
8378 «iˇ»jk«lˇ»
8379 «mˇ»no«pˇ»"#
8380 ));
8381}
8382
8383#[gpui::test]
8384async fn test_select_next(cx: &mut TestAppContext) {
8385 init_test(cx, |_| {});
8386 let mut cx = EditorTestContext::new(cx).await;
8387
8388 // Enable case sensitive search.
8389 update_test_editor_settings(&mut cx, |settings| {
8390 let mut search_settings = SearchSettingsContent::default();
8391 search_settings.case_sensitive = Some(true);
8392 settings.search = Some(search_settings);
8393 });
8394
8395 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8396
8397 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8398 .unwrap();
8399 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8400
8401 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8402 .unwrap();
8403 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8404
8405 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8406 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8407
8408 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8409 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8410
8411 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8412 .unwrap();
8413 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8414
8415 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8416 .unwrap();
8417 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8418
8419 // Test selection direction should be preserved
8420 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8421
8422 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8423 .unwrap();
8424 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8425
8426 // Test case sensitivity
8427 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8428 cx.update_editor(|e, window, cx| {
8429 e.select_next(&SelectNext::default(), window, cx).unwrap();
8430 });
8431 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8432
8433 // Disable case sensitive search.
8434 update_test_editor_settings(&mut cx, |settings| {
8435 let mut search_settings = SearchSettingsContent::default();
8436 search_settings.case_sensitive = Some(false);
8437 settings.search = Some(search_settings);
8438 });
8439
8440 cx.set_state("«ˇfoo»\nFOO\nFoo");
8441 cx.update_editor(|e, window, cx| {
8442 e.select_next(&SelectNext::default(), window, cx).unwrap();
8443 e.select_next(&SelectNext::default(), window, cx).unwrap();
8444 });
8445 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8446}
8447
8448#[gpui::test]
8449async fn test_select_all_matches(cx: &mut TestAppContext) {
8450 init_test(cx, |_| {});
8451 let mut cx = EditorTestContext::new(cx).await;
8452
8453 // Enable case sensitive search.
8454 update_test_editor_settings(&mut cx, |settings| {
8455 let mut search_settings = SearchSettingsContent::default();
8456 search_settings.case_sensitive = Some(true);
8457 settings.search = Some(search_settings);
8458 });
8459
8460 // Test caret-only selections
8461 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8462 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8463 .unwrap();
8464 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8465
8466 // Test left-to-right selections
8467 cx.set_state("abc\n«abcˇ»\nabc");
8468 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8469 .unwrap();
8470 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8471
8472 // Test right-to-left selections
8473 cx.set_state("abc\n«ˇabc»\nabc");
8474 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8475 .unwrap();
8476 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8477
8478 // Test selecting whitespace with caret selection
8479 cx.set_state("abc\nˇ abc\nabc");
8480 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8481 .unwrap();
8482 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8483
8484 // Test selecting whitespace with left-to-right selection
8485 cx.set_state("abc\n«ˇ »abc\nabc");
8486 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8487 .unwrap();
8488 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8489
8490 // Test no matches with right-to-left selection
8491 cx.set_state("abc\n« ˇ»abc\nabc");
8492 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8493 .unwrap();
8494 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8495
8496 // Test with a single word and clip_at_line_ends=true (#29823)
8497 cx.set_state("aˇbc");
8498 cx.update_editor(|e, window, cx| {
8499 e.set_clip_at_line_ends(true, cx);
8500 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8501 e.set_clip_at_line_ends(false, cx);
8502 });
8503 cx.assert_editor_state("«abcˇ»");
8504
8505 // Test case sensitivity
8506 cx.set_state("fˇoo\nFOO\nFoo");
8507 cx.update_editor(|e, window, cx| {
8508 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8509 });
8510 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8511
8512 // Disable case sensitive search.
8513 update_test_editor_settings(&mut cx, |settings| {
8514 let mut search_settings = SearchSettingsContent::default();
8515 search_settings.case_sensitive = Some(false);
8516 settings.search = Some(search_settings);
8517 });
8518
8519 cx.set_state("fˇoo\nFOO\nFoo");
8520 cx.update_editor(|e, window, cx| {
8521 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8522 });
8523 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8524}
8525
8526#[gpui::test]
8527async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8528 init_test(cx, |_| {});
8529
8530 let mut cx = EditorTestContext::new(cx).await;
8531
8532 let large_body_1 = "\nd".repeat(200);
8533 let large_body_2 = "\ne".repeat(200);
8534
8535 cx.set_state(&format!(
8536 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8537 ));
8538 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8539 let scroll_position = editor.scroll_position(cx);
8540 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8541 scroll_position
8542 });
8543
8544 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8545 .unwrap();
8546 cx.assert_editor_state(&format!(
8547 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8548 ));
8549 let scroll_position_after_selection =
8550 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8551 assert_eq!(
8552 initial_scroll_position, scroll_position_after_selection,
8553 "Scroll position should not change after selecting all matches"
8554 );
8555}
8556
8557#[gpui::test]
8558async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8559 init_test(cx, |_| {});
8560
8561 let mut cx = EditorLspTestContext::new_rust(
8562 lsp::ServerCapabilities {
8563 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8564 ..Default::default()
8565 },
8566 cx,
8567 )
8568 .await;
8569
8570 cx.set_state(indoc! {"
8571 line 1
8572 line 2
8573 linˇe 3
8574 line 4
8575 line 5
8576 "});
8577
8578 // Make an edit
8579 cx.update_editor(|editor, window, cx| {
8580 editor.handle_input("X", window, cx);
8581 });
8582
8583 // Move cursor to a different position
8584 cx.update_editor(|editor, window, cx| {
8585 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8586 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8587 });
8588 });
8589
8590 cx.assert_editor_state(indoc! {"
8591 line 1
8592 line 2
8593 linXe 3
8594 line 4
8595 liˇne 5
8596 "});
8597
8598 cx.lsp
8599 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8600 Ok(Some(vec![lsp::TextEdit::new(
8601 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8602 "PREFIX ".to_string(),
8603 )]))
8604 });
8605
8606 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8607 .unwrap()
8608 .await
8609 .unwrap();
8610
8611 cx.assert_editor_state(indoc! {"
8612 PREFIX line 1
8613 line 2
8614 linXe 3
8615 line 4
8616 liˇne 5
8617 "});
8618
8619 // Undo formatting
8620 cx.update_editor(|editor, window, cx| {
8621 editor.undo(&Default::default(), window, cx);
8622 });
8623
8624 // Verify cursor moved back to position after edit
8625 cx.assert_editor_state(indoc! {"
8626 line 1
8627 line 2
8628 linXˇe 3
8629 line 4
8630 line 5
8631 "});
8632}
8633
8634#[gpui::test]
8635async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8636 init_test(cx, |_| {});
8637
8638 let mut cx = EditorTestContext::new(cx).await;
8639
8640 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8641 cx.update_editor(|editor, window, cx| {
8642 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8643 });
8644
8645 cx.set_state(indoc! {"
8646 line 1
8647 line 2
8648 linˇe 3
8649 line 4
8650 line 5
8651 line 6
8652 line 7
8653 line 8
8654 line 9
8655 line 10
8656 "});
8657
8658 let snapshot = cx.buffer_snapshot();
8659 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8660
8661 cx.update(|_, cx| {
8662 provider.update(cx, |provider, _| {
8663 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8664 id: None,
8665 edits: vec![(edit_position..edit_position, "X".into())],
8666 edit_preview: None,
8667 }))
8668 })
8669 });
8670
8671 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8672 cx.update_editor(|editor, window, cx| {
8673 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8674 });
8675
8676 cx.assert_editor_state(indoc! {"
8677 line 1
8678 line 2
8679 lineXˇ 3
8680 line 4
8681 line 5
8682 line 6
8683 line 7
8684 line 8
8685 line 9
8686 line 10
8687 "});
8688
8689 cx.update_editor(|editor, window, cx| {
8690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8691 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8692 });
8693 });
8694
8695 cx.assert_editor_state(indoc! {"
8696 line 1
8697 line 2
8698 lineX 3
8699 line 4
8700 line 5
8701 line 6
8702 line 7
8703 line 8
8704 line 9
8705 liˇne 10
8706 "});
8707
8708 cx.update_editor(|editor, window, cx| {
8709 editor.undo(&Default::default(), window, cx);
8710 });
8711
8712 cx.assert_editor_state(indoc! {"
8713 line 1
8714 line 2
8715 lineˇ 3
8716 line 4
8717 line 5
8718 line 6
8719 line 7
8720 line 8
8721 line 9
8722 line 10
8723 "});
8724}
8725
8726#[gpui::test]
8727async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8728 init_test(cx, |_| {});
8729
8730 let mut cx = EditorTestContext::new(cx).await;
8731 cx.set_state(
8732 r#"let foo = 2;
8733lˇet foo = 2;
8734let fooˇ = 2;
8735let foo = 2;
8736let foo = ˇ2;"#,
8737 );
8738
8739 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8740 .unwrap();
8741 cx.assert_editor_state(
8742 r#"let foo = 2;
8743«letˇ» foo = 2;
8744let «fooˇ» = 2;
8745let foo = 2;
8746let foo = «2ˇ»;"#,
8747 );
8748
8749 // noop for multiple selections with different contents
8750 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8751 .unwrap();
8752 cx.assert_editor_state(
8753 r#"let foo = 2;
8754«letˇ» foo = 2;
8755let «fooˇ» = 2;
8756let foo = 2;
8757let foo = «2ˇ»;"#,
8758 );
8759
8760 // Test last selection direction should be preserved
8761 cx.set_state(
8762 r#"let foo = 2;
8763let foo = 2;
8764let «fooˇ» = 2;
8765let «ˇfoo» = 2;
8766let foo = 2;"#,
8767 );
8768
8769 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8770 .unwrap();
8771 cx.assert_editor_state(
8772 r#"let foo = 2;
8773let foo = 2;
8774let «fooˇ» = 2;
8775let «ˇfoo» = 2;
8776let «ˇfoo» = 2;"#,
8777 );
8778}
8779
8780#[gpui::test]
8781async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8782 init_test(cx, |_| {});
8783
8784 let mut cx =
8785 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8786
8787 cx.assert_editor_state(indoc! {"
8788 ˇbbb
8789 ccc
8790
8791 bbb
8792 ccc
8793 "});
8794 cx.dispatch_action(SelectPrevious::default());
8795 cx.assert_editor_state(indoc! {"
8796 «bbbˇ»
8797 ccc
8798
8799 bbb
8800 ccc
8801 "});
8802 cx.dispatch_action(SelectPrevious::default());
8803 cx.assert_editor_state(indoc! {"
8804 «bbbˇ»
8805 ccc
8806
8807 «bbbˇ»
8808 ccc
8809 "});
8810}
8811
8812#[gpui::test]
8813async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8814 init_test(cx, |_| {});
8815
8816 let mut cx = EditorTestContext::new(cx).await;
8817 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8818
8819 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8820 .unwrap();
8821 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8822
8823 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8824 .unwrap();
8825 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8826
8827 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8828 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8829
8830 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8831 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8832
8833 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8834 .unwrap();
8835 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8836
8837 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8838 .unwrap();
8839 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8840}
8841
8842#[gpui::test]
8843async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8844 init_test(cx, |_| {});
8845
8846 let mut cx = EditorTestContext::new(cx).await;
8847 cx.set_state("aˇ");
8848
8849 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8850 .unwrap();
8851 cx.assert_editor_state("«aˇ»");
8852 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8853 .unwrap();
8854 cx.assert_editor_state("«aˇ»");
8855}
8856
8857#[gpui::test]
8858async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8859 init_test(cx, |_| {});
8860
8861 let mut cx = EditorTestContext::new(cx).await;
8862 cx.set_state(
8863 r#"let foo = 2;
8864lˇet foo = 2;
8865let fooˇ = 2;
8866let foo = 2;
8867let foo = ˇ2;"#,
8868 );
8869
8870 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8871 .unwrap();
8872 cx.assert_editor_state(
8873 r#"let foo = 2;
8874«letˇ» foo = 2;
8875let «fooˇ» = 2;
8876let foo = 2;
8877let foo = «2ˇ»;"#,
8878 );
8879
8880 // noop for multiple selections with different contents
8881 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8882 .unwrap();
8883 cx.assert_editor_state(
8884 r#"let foo = 2;
8885«letˇ» foo = 2;
8886let «fooˇ» = 2;
8887let foo = 2;
8888let foo = «2ˇ»;"#,
8889 );
8890}
8891
8892#[gpui::test]
8893async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8894 init_test(cx, |_| {});
8895 let mut cx = EditorTestContext::new(cx).await;
8896
8897 // Enable case sensitive search.
8898 update_test_editor_settings(&mut cx, |settings| {
8899 let mut search_settings = SearchSettingsContent::default();
8900 search_settings.case_sensitive = Some(true);
8901 settings.search = Some(search_settings);
8902 });
8903
8904 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8905
8906 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8907 .unwrap();
8908 // selection direction is preserved
8909 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8910
8911 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8912 .unwrap();
8913 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8914
8915 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8916 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8917
8918 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8919 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8920
8921 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8922 .unwrap();
8923 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8924
8925 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8926 .unwrap();
8927 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8928
8929 // Test case sensitivity
8930 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
8931 cx.update_editor(|e, window, cx| {
8932 e.select_previous(&SelectPrevious::default(), window, cx)
8933 .unwrap();
8934 e.select_previous(&SelectPrevious::default(), window, cx)
8935 .unwrap();
8936 });
8937 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8938
8939 // Disable case sensitive search.
8940 update_test_editor_settings(&mut cx, |settings| {
8941 let mut search_settings = SearchSettingsContent::default();
8942 search_settings.case_sensitive = Some(false);
8943 settings.search = Some(search_settings);
8944 });
8945
8946 cx.set_state("foo\nFOO\n«ˇFoo»");
8947 cx.update_editor(|e, window, cx| {
8948 e.select_previous(&SelectPrevious::default(), window, cx)
8949 .unwrap();
8950 e.select_previous(&SelectPrevious::default(), window, cx)
8951 .unwrap();
8952 });
8953 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8954}
8955
8956#[gpui::test]
8957async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8958 init_test(cx, |_| {});
8959
8960 let language = Arc::new(Language::new(
8961 LanguageConfig::default(),
8962 Some(tree_sitter_rust::LANGUAGE.into()),
8963 ));
8964
8965 let text = r#"
8966 use mod1::mod2::{mod3, mod4};
8967
8968 fn fn_1(param1: bool, param2: &str) {
8969 let var1 = "text";
8970 }
8971 "#
8972 .unindent();
8973
8974 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
8975 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8976 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8977
8978 editor
8979 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8980 .await;
8981
8982 editor.update_in(cx, |editor, window, cx| {
8983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8984 s.select_display_ranges([
8985 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8986 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8987 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8988 ]);
8989 });
8990 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8991 });
8992 editor.update(cx, |editor, cx| {
8993 assert_text_with_selections(
8994 editor,
8995 indoc! {r#"
8996 use mod1::mod2::{mod3, «mod4ˇ»};
8997
8998 fn fn_1«ˇ(param1: bool, param2: &str)» {
8999 let var1 = "«ˇtext»";
9000 }
9001 "#},
9002 cx,
9003 );
9004 });
9005
9006 editor.update_in(cx, |editor, window, cx| {
9007 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9008 });
9009 editor.update(cx, |editor, cx| {
9010 assert_text_with_selections(
9011 editor,
9012 indoc! {r#"
9013 use mod1::mod2::«{mod3, mod4}ˇ»;
9014
9015 «ˇfn fn_1(param1: bool, param2: &str) {
9016 let var1 = "text";
9017 }»
9018 "#},
9019 cx,
9020 );
9021 });
9022
9023 editor.update_in(cx, |editor, window, cx| {
9024 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9025 });
9026 assert_eq!(
9027 editor.update(cx, |editor, cx| editor
9028 .selections
9029 .display_ranges(&editor.display_snapshot(cx))),
9030 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9031 );
9032
9033 // Trying to expand the selected syntax node one more time has no effect.
9034 editor.update_in(cx, |editor, window, cx| {
9035 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9036 });
9037 assert_eq!(
9038 editor.update(cx, |editor, cx| editor
9039 .selections
9040 .display_ranges(&editor.display_snapshot(cx))),
9041 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9042 );
9043
9044 editor.update_in(cx, |editor, window, cx| {
9045 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9046 });
9047 editor.update(cx, |editor, cx| {
9048 assert_text_with_selections(
9049 editor,
9050 indoc! {r#"
9051 use mod1::mod2::«{mod3, mod4}ˇ»;
9052
9053 «ˇfn fn_1(param1: bool, param2: &str) {
9054 let var1 = "text";
9055 }»
9056 "#},
9057 cx,
9058 );
9059 });
9060
9061 editor.update_in(cx, |editor, window, cx| {
9062 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9063 });
9064 editor.update(cx, |editor, cx| {
9065 assert_text_with_selections(
9066 editor,
9067 indoc! {r#"
9068 use mod1::mod2::{mod3, «mod4ˇ»};
9069
9070 fn fn_1«ˇ(param1: bool, param2: &str)» {
9071 let var1 = "«ˇtext»";
9072 }
9073 "#},
9074 cx,
9075 );
9076 });
9077
9078 editor.update_in(cx, |editor, window, cx| {
9079 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9080 });
9081 editor.update(cx, |editor, cx| {
9082 assert_text_with_selections(
9083 editor,
9084 indoc! {r#"
9085 use mod1::mod2::{mod3, moˇd4};
9086
9087 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9088 let var1 = "teˇxt";
9089 }
9090 "#},
9091 cx,
9092 );
9093 });
9094
9095 // Trying to shrink the selected syntax node one more time has no effect.
9096 editor.update_in(cx, |editor, window, cx| {
9097 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9098 });
9099 editor.update_in(cx, |editor, _, cx| {
9100 assert_text_with_selections(
9101 editor,
9102 indoc! {r#"
9103 use mod1::mod2::{mod3, moˇd4};
9104
9105 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9106 let var1 = "teˇxt";
9107 }
9108 "#},
9109 cx,
9110 );
9111 });
9112
9113 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9114 // a fold.
9115 editor.update_in(cx, |editor, window, cx| {
9116 editor.fold_creases(
9117 vec![
9118 Crease::simple(
9119 Point::new(0, 21)..Point::new(0, 24),
9120 FoldPlaceholder::test(),
9121 ),
9122 Crease::simple(
9123 Point::new(3, 20)..Point::new(3, 22),
9124 FoldPlaceholder::test(),
9125 ),
9126 ],
9127 true,
9128 window,
9129 cx,
9130 );
9131 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9132 });
9133 editor.update(cx, |editor, cx| {
9134 assert_text_with_selections(
9135 editor,
9136 indoc! {r#"
9137 use mod1::mod2::«{mod3, mod4}ˇ»;
9138
9139 fn fn_1«ˇ(param1: bool, param2: &str)» {
9140 let var1 = "«ˇtext»";
9141 }
9142 "#},
9143 cx,
9144 );
9145 });
9146}
9147
9148#[gpui::test]
9149async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9150 init_test(cx, |_| {});
9151
9152 let language = Arc::new(Language::new(
9153 LanguageConfig::default(),
9154 Some(tree_sitter_rust::LANGUAGE.into()),
9155 ));
9156
9157 let text = "let a = 2;";
9158
9159 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
9160 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9161 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9162
9163 editor
9164 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9165 .await;
9166
9167 // Test case 1: Cursor at end of word
9168 editor.update_in(cx, |editor, window, cx| {
9169 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9170 s.select_display_ranges([
9171 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9172 ]);
9173 });
9174 });
9175 editor.update(cx, |editor, cx| {
9176 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9177 });
9178 editor.update_in(cx, |editor, window, cx| {
9179 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9180 });
9181 editor.update(cx, |editor, cx| {
9182 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9183 });
9184 editor.update_in(cx, |editor, window, cx| {
9185 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9186 });
9187 editor.update(cx, |editor, cx| {
9188 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9189 });
9190
9191 // Test case 2: Cursor at end of statement
9192 editor.update_in(cx, |editor, window, cx| {
9193 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9194 s.select_display_ranges([
9195 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9196 ]);
9197 });
9198 });
9199 editor.update(cx, |editor, cx| {
9200 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9201 });
9202 editor.update_in(cx, |editor, window, cx| {
9203 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9204 });
9205 editor.update(cx, |editor, cx| {
9206 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9207 });
9208}
9209
9210#[gpui::test]
9211async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9212 init_test(cx, |_| {});
9213
9214 let language = Arc::new(Language::new(
9215 LanguageConfig {
9216 name: "JavaScript".into(),
9217 ..Default::default()
9218 },
9219 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9220 ));
9221
9222 let text = r#"
9223 let a = {
9224 key: "value",
9225 };
9226 "#
9227 .unindent();
9228
9229 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
9230 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9231 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9232
9233 editor
9234 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9235 .await;
9236
9237 // Test case 1: Cursor after '{'
9238 editor.update_in(cx, |editor, window, cx| {
9239 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9240 s.select_display_ranges([
9241 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9242 ]);
9243 });
9244 });
9245 editor.update(cx, |editor, cx| {
9246 assert_text_with_selections(
9247 editor,
9248 indoc! {r#"
9249 let a = {ˇ
9250 key: "value",
9251 };
9252 "#},
9253 cx,
9254 );
9255 });
9256 editor.update_in(cx, |editor, window, cx| {
9257 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9258 });
9259 editor.update(cx, |editor, cx| {
9260 assert_text_with_selections(
9261 editor,
9262 indoc! {r#"
9263 let a = «ˇ{
9264 key: "value",
9265 }»;
9266 "#},
9267 cx,
9268 );
9269 });
9270
9271 // Test case 2: Cursor after ':'
9272 editor.update_in(cx, |editor, window, cx| {
9273 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9274 s.select_display_ranges([
9275 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9276 ]);
9277 });
9278 });
9279 editor.update(cx, |editor, cx| {
9280 assert_text_with_selections(
9281 editor,
9282 indoc! {r#"
9283 let a = {
9284 key:ˇ "value",
9285 };
9286 "#},
9287 cx,
9288 );
9289 });
9290 editor.update_in(cx, |editor, window, cx| {
9291 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9292 });
9293 editor.update(cx, |editor, cx| {
9294 assert_text_with_selections(
9295 editor,
9296 indoc! {r#"
9297 let a = {
9298 «ˇkey: "value"»,
9299 };
9300 "#},
9301 cx,
9302 );
9303 });
9304 editor.update_in(cx, |editor, window, cx| {
9305 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9306 });
9307 editor.update(cx, |editor, cx| {
9308 assert_text_with_selections(
9309 editor,
9310 indoc! {r#"
9311 let a = «ˇ{
9312 key: "value",
9313 }»;
9314 "#},
9315 cx,
9316 );
9317 });
9318
9319 // Test case 3: Cursor after ','
9320 editor.update_in(cx, |editor, window, cx| {
9321 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9322 s.select_display_ranges([
9323 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9324 ]);
9325 });
9326 });
9327 editor.update(cx, |editor, cx| {
9328 assert_text_with_selections(
9329 editor,
9330 indoc! {r#"
9331 let a = {
9332 key: "value",ˇ
9333 };
9334 "#},
9335 cx,
9336 );
9337 });
9338 editor.update_in(cx, |editor, window, cx| {
9339 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9340 });
9341 editor.update(cx, |editor, cx| {
9342 assert_text_with_selections(
9343 editor,
9344 indoc! {r#"
9345 let a = «ˇ{
9346 key: "value",
9347 }»;
9348 "#},
9349 cx,
9350 );
9351 });
9352
9353 // Test case 4: Cursor after ';'
9354 editor.update_in(cx, |editor, window, cx| {
9355 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9356 s.select_display_ranges([
9357 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9358 ]);
9359 });
9360 });
9361 editor.update(cx, |editor, cx| {
9362 assert_text_with_selections(
9363 editor,
9364 indoc! {r#"
9365 let a = {
9366 key: "value",
9367 };ˇ
9368 "#},
9369 cx,
9370 );
9371 });
9372 editor.update_in(cx, |editor, window, cx| {
9373 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9374 });
9375 editor.update(cx, |editor, cx| {
9376 assert_text_with_selections(
9377 editor,
9378 indoc! {r#"
9379 «ˇlet a = {
9380 key: "value",
9381 };
9382 »"#},
9383 cx,
9384 );
9385 });
9386}
9387
9388#[gpui::test]
9389async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9390 init_test(cx, |_| {});
9391
9392 let language = Arc::new(Language::new(
9393 LanguageConfig::default(),
9394 Some(tree_sitter_rust::LANGUAGE.into()),
9395 ));
9396
9397 let text = r#"
9398 use mod1::mod2::{mod3, mod4};
9399
9400 fn fn_1(param1: bool, param2: &str) {
9401 let var1 = "hello world";
9402 }
9403 "#
9404 .unindent();
9405
9406 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
9407 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9408 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9409
9410 editor
9411 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9412 .await;
9413
9414 // Test 1: Cursor on a letter of a string word
9415 editor.update_in(cx, |editor, window, cx| {
9416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9417 s.select_display_ranges([
9418 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9419 ]);
9420 });
9421 });
9422 editor.update_in(cx, |editor, window, cx| {
9423 assert_text_with_selections(
9424 editor,
9425 indoc! {r#"
9426 use mod1::mod2::{mod3, mod4};
9427
9428 fn fn_1(param1: bool, param2: &str) {
9429 let var1 = "hˇello world";
9430 }
9431 "#},
9432 cx,
9433 );
9434 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9435 assert_text_with_selections(
9436 editor,
9437 indoc! {r#"
9438 use mod1::mod2::{mod3, mod4};
9439
9440 fn fn_1(param1: bool, param2: &str) {
9441 let var1 = "«ˇhello» world";
9442 }
9443 "#},
9444 cx,
9445 );
9446 });
9447
9448 // Test 2: Partial selection within a word
9449 editor.update_in(cx, |editor, window, cx| {
9450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9451 s.select_display_ranges([
9452 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9453 ]);
9454 });
9455 });
9456 editor.update_in(cx, |editor, window, cx| {
9457 assert_text_with_selections(
9458 editor,
9459 indoc! {r#"
9460 use mod1::mod2::{mod3, mod4};
9461
9462 fn fn_1(param1: bool, param2: &str) {
9463 let var1 = "h«elˇ»lo world";
9464 }
9465 "#},
9466 cx,
9467 );
9468 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9469 assert_text_with_selections(
9470 editor,
9471 indoc! {r#"
9472 use mod1::mod2::{mod3, mod4};
9473
9474 fn fn_1(param1: bool, param2: &str) {
9475 let var1 = "«ˇhello» world";
9476 }
9477 "#},
9478 cx,
9479 );
9480 });
9481
9482 // Test 3: Complete word already selected
9483 editor.update_in(cx, |editor, window, cx| {
9484 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9485 s.select_display_ranges([
9486 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9487 ]);
9488 });
9489 });
9490 editor.update_in(cx, |editor, window, cx| {
9491 assert_text_with_selections(
9492 editor,
9493 indoc! {r#"
9494 use mod1::mod2::{mod3, mod4};
9495
9496 fn fn_1(param1: bool, param2: &str) {
9497 let var1 = "«helloˇ» world";
9498 }
9499 "#},
9500 cx,
9501 );
9502 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9503 assert_text_with_selections(
9504 editor,
9505 indoc! {r#"
9506 use mod1::mod2::{mod3, mod4};
9507
9508 fn fn_1(param1: bool, param2: &str) {
9509 let var1 = "«hello worldˇ»";
9510 }
9511 "#},
9512 cx,
9513 );
9514 });
9515
9516 // Test 4: Selection spanning across words
9517 editor.update_in(cx, |editor, window, cx| {
9518 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9519 s.select_display_ranges([
9520 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9521 ]);
9522 });
9523 });
9524 editor.update_in(cx, |editor, window, cx| {
9525 assert_text_with_selections(
9526 editor,
9527 indoc! {r#"
9528 use mod1::mod2::{mod3, mod4};
9529
9530 fn fn_1(param1: bool, param2: &str) {
9531 let var1 = "hel«lo woˇ»rld";
9532 }
9533 "#},
9534 cx,
9535 );
9536 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9537 assert_text_with_selections(
9538 editor,
9539 indoc! {r#"
9540 use mod1::mod2::{mod3, mod4};
9541
9542 fn fn_1(param1: bool, param2: &str) {
9543 let var1 = "«ˇhello world»";
9544 }
9545 "#},
9546 cx,
9547 );
9548 });
9549
9550 // Test 5: Expansion beyond string
9551 editor.update_in(cx, |editor, window, cx| {
9552 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9553 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9554 assert_text_with_selections(
9555 editor,
9556 indoc! {r#"
9557 use mod1::mod2::{mod3, mod4};
9558
9559 fn fn_1(param1: bool, param2: &str) {
9560 «ˇlet var1 = "hello world";»
9561 }
9562 "#},
9563 cx,
9564 );
9565 });
9566}
9567
9568#[gpui::test]
9569async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9570 init_test(cx, |_| {});
9571
9572 let mut cx = EditorTestContext::new(cx).await;
9573
9574 let language = Arc::new(Language::new(
9575 LanguageConfig::default(),
9576 Some(tree_sitter_rust::LANGUAGE.into()),
9577 ));
9578
9579 cx.update_buffer(|buffer, cx| {
9580 buffer.set_language_immediate(Some(language), cx);
9581 });
9582
9583 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9584 cx.update_editor(|editor, window, cx| {
9585 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9586 });
9587
9588 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9589
9590 cx.set_state(indoc! { r#"fn a() {
9591 // what
9592 // a
9593 // ˇlong
9594 // method
9595 // I
9596 // sure
9597 // hope
9598 // it
9599 // works
9600 }"# });
9601
9602 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9603 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9604 cx.update(|_, cx| {
9605 multi_buffer.update(cx, |multi_buffer, cx| {
9606 multi_buffer.set_excerpts_for_path(
9607 PathKey::for_buffer(&buffer, cx),
9608 buffer,
9609 [Point::new(1, 0)..Point::new(1, 0)],
9610 3,
9611 cx,
9612 );
9613 });
9614 });
9615
9616 let editor2 = cx.new_window_entity(|window, cx| {
9617 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9618 });
9619
9620 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9621 cx.update_editor(|editor, window, cx| {
9622 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9623 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9624 })
9625 });
9626
9627 cx.assert_editor_state(indoc! { "
9628 fn a() {
9629 // what
9630 // a
9631 ˇ // long
9632 // method"});
9633
9634 cx.update_editor(|editor, window, cx| {
9635 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9636 });
9637
9638 // Although we could potentially make the action work when the syntax node
9639 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9640 // did. Maybe we could also expand the excerpt to contain the range?
9641 cx.assert_editor_state(indoc! { "
9642 fn a() {
9643 // what
9644 // a
9645 ˇ // long
9646 // method"});
9647}
9648
9649#[gpui::test]
9650async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9651 init_test(cx, |_| {});
9652
9653 let base_text = r#"
9654 impl A {
9655 // this is an uncommitted comment
9656
9657 fn b() {
9658 c();
9659 }
9660
9661 // this is another uncommitted comment
9662
9663 fn d() {
9664 // e
9665 // f
9666 }
9667 }
9668
9669 fn g() {
9670 // h
9671 }
9672 "#
9673 .unindent();
9674
9675 let text = r#"
9676 ˇimpl A {
9677
9678 fn b() {
9679 c();
9680 }
9681
9682 fn d() {
9683 // e
9684 // f
9685 }
9686 }
9687
9688 fn g() {
9689 // h
9690 }
9691 "#
9692 .unindent();
9693
9694 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9695 cx.set_state(&text);
9696 cx.set_head_text(&base_text);
9697 cx.update_editor(|editor, window, cx| {
9698 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9699 });
9700
9701 cx.assert_state_with_diff(
9702 "
9703 ˇimpl A {
9704 - // this is an uncommitted comment
9705
9706 fn b() {
9707 c();
9708 }
9709
9710 - // this is another uncommitted comment
9711 -
9712 fn d() {
9713 // e
9714 // f
9715 }
9716 }
9717
9718 fn g() {
9719 // h
9720 }
9721 "
9722 .unindent(),
9723 );
9724
9725 let expected_display_text = "
9726 impl A {
9727 // this is an uncommitted comment
9728
9729 fn b() {
9730 ⋯
9731 }
9732
9733 // this is another uncommitted comment
9734
9735 fn d() {
9736 ⋯
9737 }
9738 }
9739
9740 fn g() {
9741 ⋯
9742 }
9743 "
9744 .unindent();
9745
9746 cx.update_editor(|editor, window, cx| {
9747 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9748 assert_eq!(editor.display_text(cx), expected_display_text);
9749 });
9750}
9751
9752#[gpui::test]
9753async fn test_autoindent(cx: &mut TestAppContext) {
9754 init_test(cx, |_| {});
9755
9756 let language = Arc::new(
9757 Language::new(
9758 LanguageConfig {
9759 brackets: BracketPairConfig {
9760 pairs: vec![
9761 BracketPair {
9762 start: "{".to_string(),
9763 end: "}".to_string(),
9764 close: false,
9765 surround: false,
9766 newline: true,
9767 },
9768 BracketPair {
9769 start: "(".to_string(),
9770 end: ")".to_string(),
9771 close: false,
9772 surround: false,
9773 newline: true,
9774 },
9775 ],
9776 ..Default::default()
9777 },
9778 ..Default::default()
9779 },
9780 Some(tree_sitter_rust::LANGUAGE.into()),
9781 )
9782 .with_indents_query(
9783 r#"
9784 (_ "(" ")" @end) @indent
9785 (_ "{" "}" @end) @indent
9786 "#,
9787 )
9788 .unwrap(),
9789 );
9790
9791 let text = "fn a() {}";
9792
9793 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
9794 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9795 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9796 editor
9797 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9798 .await;
9799
9800 editor.update_in(cx, |editor, window, cx| {
9801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9802 s.select_ranges([
9803 MultiBufferOffset(5)..MultiBufferOffset(5),
9804 MultiBufferOffset(8)..MultiBufferOffset(8),
9805 MultiBufferOffset(9)..MultiBufferOffset(9),
9806 ])
9807 });
9808 editor.newline(&Newline, window, cx);
9809 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9810 assert_eq!(
9811 editor.selections.ranges(&editor.display_snapshot(cx)),
9812 &[
9813 Point::new(1, 4)..Point::new(1, 4),
9814 Point::new(3, 4)..Point::new(3, 4),
9815 Point::new(5, 0)..Point::new(5, 0)
9816 ]
9817 );
9818 });
9819}
9820
9821#[gpui::test]
9822async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9823 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9824
9825 let language = Arc::new(
9826 Language::new(
9827 LanguageConfig {
9828 brackets: BracketPairConfig {
9829 pairs: vec![
9830 BracketPair {
9831 start: "{".to_string(),
9832 end: "}".to_string(),
9833 close: false,
9834 surround: false,
9835 newline: true,
9836 },
9837 BracketPair {
9838 start: "(".to_string(),
9839 end: ")".to_string(),
9840 close: false,
9841 surround: false,
9842 newline: true,
9843 },
9844 ],
9845 ..Default::default()
9846 },
9847 ..Default::default()
9848 },
9849 Some(tree_sitter_rust::LANGUAGE.into()),
9850 )
9851 .with_indents_query(
9852 r#"
9853 (_ "(" ")" @end) @indent
9854 (_ "{" "}" @end) @indent
9855 "#,
9856 )
9857 .unwrap(),
9858 );
9859
9860 let text = "fn a() {}";
9861
9862 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
9863 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9864 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9865 editor
9866 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9867 .await;
9868
9869 editor.update_in(cx, |editor, window, cx| {
9870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9871 s.select_ranges([
9872 MultiBufferOffset(5)..MultiBufferOffset(5),
9873 MultiBufferOffset(8)..MultiBufferOffset(8),
9874 MultiBufferOffset(9)..MultiBufferOffset(9),
9875 ])
9876 });
9877 editor.newline(&Newline, window, cx);
9878 assert_eq!(
9879 editor.text(cx),
9880 indoc!(
9881 "
9882 fn a(
9883
9884 ) {
9885
9886 }
9887 "
9888 )
9889 );
9890 assert_eq!(
9891 editor.selections.ranges(&editor.display_snapshot(cx)),
9892 &[
9893 Point::new(1, 0)..Point::new(1, 0),
9894 Point::new(3, 0)..Point::new(3, 0),
9895 Point::new(5, 0)..Point::new(5, 0)
9896 ]
9897 );
9898 });
9899}
9900
9901#[gpui::test]
9902async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9903 init_test(cx, |settings| {
9904 settings.defaults.auto_indent = Some(true);
9905 settings.languages.0.insert(
9906 "python".into(),
9907 LanguageSettingsContent {
9908 auto_indent: Some(false),
9909 ..Default::default()
9910 },
9911 );
9912 });
9913
9914 let mut cx = EditorTestContext::new(cx).await;
9915
9916 let injected_language = Arc::new(
9917 Language::new(
9918 LanguageConfig {
9919 brackets: BracketPairConfig {
9920 pairs: vec![
9921 BracketPair {
9922 start: "{".to_string(),
9923 end: "}".to_string(),
9924 close: false,
9925 surround: false,
9926 newline: true,
9927 },
9928 BracketPair {
9929 start: "(".to_string(),
9930 end: ")".to_string(),
9931 close: true,
9932 surround: false,
9933 newline: true,
9934 },
9935 ],
9936 ..Default::default()
9937 },
9938 name: "python".into(),
9939 ..Default::default()
9940 },
9941 Some(tree_sitter_python::LANGUAGE.into()),
9942 )
9943 .with_indents_query(
9944 r#"
9945 (_ "(" ")" @end) @indent
9946 (_ "{" "}" @end) @indent
9947 "#,
9948 )
9949 .unwrap(),
9950 );
9951
9952 let language = Arc::new(
9953 Language::new(
9954 LanguageConfig {
9955 brackets: BracketPairConfig {
9956 pairs: vec![
9957 BracketPair {
9958 start: "{".to_string(),
9959 end: "}".to_string(),
9960 close: false,
9961 surround: false,
9962 newline: true,
9963 },
9964 BracketPair {
9965 start: "(".to_string(),
9966 end: ")".to_string(),
9967 close: true,
9968 surround: false,
9969 newline: true,
9970 },
9971 ],
9972 ..Default::default()
9973 },
9974 name: LanguageName::new("rust"),
9975 ..Default::default()
9976 },
9977 Some(tree_sitter_rust::LANGUAGE.into()),
9978 )
9979 .with_indents_query(
9980 r#"
9981 (_ "(" ")" @end) @indent
9982 (_ "{" "}" @end) @indent
9983 "#,
9984 )
9985 .unwrap()
9986 .with_injection_query(
9987 r#"
9988 (macro_invocation
9989 macro: (identifier) @_macro_name
9990 (token_tree) @injection.content
9991 (#set! injection.language "python"))
9992 "#,
9993 )
9994 .unwrap(),
9995 );
9996
9997 cx.language_registry().add(injected_language);
9998 cx.language_registry().add(language.clone());
9999
10000 cx.update_buffer(|buffer, cx| {
10001 buffer.set_language_immediate(Some(language), cx);
10002 });
10003
10004 cx.set_state(r#"struct A {ˇ}"#);
10005
10006 cx.update_editor(|editor, window, cx| {
10007 editor.newline(&Default::default(), window, cx);
10008 });
10009
10010 cx.assert_editor_state(indoc!(
10011 "struct A {
10012 ˇ
10013 }"
10014 ));
10015
10016 cx.set_state(r#"select_biased!(ˇ)"#);
10017
10018 cx.update_editor(|editor, window, cx| {
10019 editor.newline(&Default::default(), window, cx);
10020 editor.handle_input("def ", window, cx);
10021 editor.handle_input("(", window, cx);
10022 editor.newline(&Default::default(), window, cx);
10023 editor.handle_input("a", window, cx);
10024 });
10025
10026 cx.assert_editor_state(indoc!(
10027 "select_biased!(
10028 def (
10029 aˇ
10030 )
10031 )"
10032 ));
10033}
10034
10035#[gpui::test]
10036async fn test_autoindent_selections(cx: &mut TestAppContext) {
10037 init_test(cx, |_| {});
10038
10039 {
10040 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10041 cx.set_state(indoc! {"
10042 impl A {
10043
10044 fn b() {}
10045
10046 «fn c() {
10047
10048 }ˇ»
10049 }
10050 "});
10051
10052 cx.update_editor(|editor, window, cx| {
10053 editor.autoindent(&Default::default(), window, cx);
10054 });
10055
10056 cx.assert_editor_state(indoc! {"
10057 impl A {
10058
10059 fn b() {}
10060
10061 «fn c() {
10062
10063 }ˇ»
10064 }
10065 "});
10066 }
10067
10068 {
10069 let mut cx = EditorTestContext::new_multibuffer(
10070 cx,
10071 [indoc! { "
10072 impl A {
10073 «
10074 // a
10075 fn b(){}
10076 »
10077 «
10078 }
10079 fn c(){}
10080 »
10081 "}],
10082 );
10083
10084 let buffer = cx.update_editor(|editor, _, cx| {
10085 let buffer = editor.buffer().update(cx, |buffer, _| {
10086 buffer.all_buffers().iter().next().unwrap().clone()
10087 });
10088 buffer.update(cx, |buffer, cx| {
10089 buffer.set_language_immediate(Some(rust_lang()), cx)
10090 });
10091 buffer
10092 });
10093
10094 cx.run_until_parked();
10095 cx.update_editor(|editor, window, cx| {
10096 editor.select_all(&Default::default(), window, cx);
10097 editor.autoindent(&Default::default(), window, cx)
10098 });
10099 cx.run_until_parked();
10100
10101 cx.update(|_, cx| {
10102 assert_eq!(
10103 buffer.read(cx).text(),
10104 indoc! { "
10105 impl A {
10106
10107 // a
10108 fn b(){}
10109
10110
10111 }
10112 fn c(){}
10113
10114 " }
10115 )
10116 });
10117 }
10118}
10119
10120#[gpui::test]
10121async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10122 init_test(cx, |_| {});
10123
10124 let mut cx = EditorTestContext::new(cx).await;
10125
10126 let language = Arc::new(Language::new(
10127 LanguageConfig {
10128 brackets: BracketPairConfig {
10129 pairs: vec![
10130 BracketPair {
10131 start: "{".to_string(),
10132 end: "}".to_string(),
10133 close: true,
10134 surround: true,
10135 newline: true,
10136 },
10137 BracketPair {
10138 start: "(".to_string(),
10139 end: ")".to_string(),
10140 close: true,
10141 surround: true,
10142 newline: true,
10143 },
10144 BracketPair {
10145 start: "/*".to_string(),
10146 end: " */".to_string(),
10147 close: true,
10148 surround: true,
10149 newline: true,
10150 },
10151 BracketPair {
10152 start: "[".to_string(),
10153 end: "]".to_string(),
10154 close: false,
10155 surround: false,
10156 newline: true,
10157 },
10158 BracketPair {
10159 start: "\"".to_string(),
10160 end: "\"".to_string(),
10161 close: true,
10162 surround: true,
10163 newline: false,
10164 },
10165 BracketPair {
10166 start: "<".to_string(),
10167 end: ">".to_string(),
10168 close: false,
10169 surround: true,
10170 newline: true,
10171 },
10172 ],
10173 ..Default::default()
10174 },
10175 autoclose_before: "})]".to_string(),
10176 ..Default::default()
10177 },
10178 Some(tree_sitter_rust::LANGUAGE.into()),
10179 ));
10180
10181 cx.language_registry().add(language.clone());
10182 cx.update_buffer(|buffer, cx| {
10183 buffer.set_language_immediate(Some(language), cx);
10184 });
10185
10186 cx.set_state(
10187 &r#"
10188 🏀ˇ
10189 εˇ
10190 ❤️ˇ
10191 "#
10192 .unindent(),
10193 );
10194
10195 // autoclose multiple nested brackets at multiple cursors
10196 cx.update_editor(|editor, window, cx| {
10197 editor.handle_input("{", window, cx);
10198 editor.handle_input("{", window, cx);
10199 editor.handle_input("{", window, cx);
10200 });
10201 cx.assert_editor_state(
10202 &"
10203 🏀{{{ˇ}}}
10204 ε{{{ˇ}}}
10205 ❤️{{{ˇ}}}
10206 "
10207 .unindent(),
10208 );
10209
10210 // insert a different closing bracket
10211 cx.update_editor(|editor, window, cx| {
10212 editor.handle_input(")", window, cx);
10213 });
10214 cx.assert_editor_state(
10215 &"
10216 🏀{{{)ˇ}}}
10217 ε{{{)ˇ}}}
10218 ❤️{{{)ˇ}}}
10219 "
10220 .unindent(),
10221 );
10222
10223 // skip over the auto-closed brackets when typing a closing bracket
10224 cx.update_editor(|editor, window, cx| {
10225 editor.move_right(&MoveRight, window, cx);
10226 editor.handle_input("}", window, cx);
10227 editor.handle_input("}", window, cx);
10228 editor.handle_input("}", window, cx);
10229 });
10230 cx.assert_editor_state(
10231 &"
10232 🏀{{{)}}}}ˇ
10233 ε{{{)}}}}ˇ
10234 ❤️{{{)}}}}ˇ
10235 "
10236 .unindent(),
10237 );
10238
10239 // autoclose multi-character pairs
10240 cx.set_state(
10241 &"
10242 ˇ
10243 ˇ
10244 "
10245 .unindent(),
10246 );
10247 cx.update_editor(|editor, window, cx| {
10248 editor.handle_input("/", window, cx);
10249 editor.handle_input("*", window, cx);
10250 });
10251 cx.assert_editor_state(
10252 &"
10253 /*ˇ */
10254 /*ˇ */
10255 "
10256 .unindent(),
10257 );
10258
10259 // one cursor autocloses a multi-character pair, one cursor
10260 // does not autoclose.
10261 cx.set_state(
10262 &"
10263 /ˇ
10264 ˇ
10265 "
10266 .unindent(),
10267 );
10268 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10269 cx.assert_editor_state(
10270 &"
10271 /*ˇ */
10272 *ˇ
10273 "
10274 .unindent(),
10275 );
10276
10277 // Don't autoclose if the next character isn't whitespace and isn't
10278 // listed in the language's "autoclose_before" section.
10279 cx.set_state("ˇa b");
10280 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10281 cx.assert_editor_state("{ˇa b");
10282
10283 // Don't autoclose if `close` is false for the bracket pair
10284 cx.set_state("ˇ");
10285 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10286 cx.assert_editor_state("[ˇ");
10287
10288 // Surround with brackets if text is selected
10289 cx.set_state("«aˇ» b");
10290 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10291 cx.assert_editor_state("{«aˇ»} b");
10292
10293 // Autoclose when not immediately after a word character
10294 cx.set_state("a ˇ");
10295 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10296 cx.assert_editor_state("a \"ˇ\"");
10297
10298 // Autoclose pair where the start and end characters are the same
10299 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10300 cx.assert_editor_state("a \"\"ˇ");
10301
10302 // Don't autoclose when immediately after a word character
10303 cx.set_state("aˇ");
10304 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10305 cx.assert_editor_state("a\"ˇ");
10306
10307 // Do autoclose when after a non-word character
10308 cx.set_state("{ˇ");
10309 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10310 cx.assert_editor_state("{\"ˇ\"");
10311
10312 // Non identical pairs autoclose regardless of preceding character
10313 cx.set_state("aˇ");
10314 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10315 cx.assert_editor_state("a{ˇ}");
10316
10317 // Don't autoclose pair if autoclose is disabled
10318 cx.set_state("ˇ");
10319 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10320 cx.assert_editor_state("<ˇ");
10321
10322 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10323 cx.set_state("«aˇ» b");
10324 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10325 cx.assert_editor_state("<«aˇ»> b");
10326}
10327
10328#[gpui::test]
10329async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10330 init_test(cx, |settings| {
10331 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10332 });
10333
10334 let mut cx = EditorTestContext::new(cx).await;
10335
10336 let language = Arc::new(Language::new(
10337 LanguageConfig {
10338 brackets: BracketPairConfig {
10339 pairs: vec![
10340 BracketPair {
10341 start: "{".to_string(),
10342 end: "}".to_string(),
10343 close: true,
10344 surround: true,
10345 newline: true,
10346 },
10347 BracketPair {
10348 start: "(".to_string(),
10349 end: ")".to_string(),
10350 close: true,
10351 surround: true,
10352 newline: true,
10353 },
10354 BracketPair {
10355 start: "[".to_string(),
10356 end: "]".to_string(),
10357 close: false,
10358 surround: false,
10359 newline: true,
10360 },
10361 ],
10362 ..Default::default()
10363 },
10364 autoclose_before: "})]".to_string(),
10365 ..Default::default()
10366 },
10367 Some(tree_sitter_rust::LANGUAGE.into()),
10368 ));
10369
10370 cx.language_registry().add(language.clone());
10371 cx.update_buffer(|buffer, cx| {
10372 buffer.set_language_immediate(Some(language), cx);
10373 });
10374
10375 cx.set_state(
10376 &"
10377 ˇ
10378 ˇ
10379 ˇ
10380 "
10381 .unindent(),
10382 );
10383
10384 // ensure only matching closing brackets are skipped over
10385 cx.update_editor(|editor, window, cx| {
10386 editor.handle_input("}", window, cx);
10387 editor.move_left(&MoveLeft, window, cx);
10388 editor.handle_input(")", window, cx);
10389 editor.move_left(&MoveLeft, window, cx);
10390 });
10391 cx.assert_editor_state(
10392 &"
10393 ˇ)}
10394 ˇ)}
10395 ˇ)}
10396 "
10397 .unindent(),
10398 );
10399
10400 // skip-over closing brackets at multiple cursors
10401 cx.update_editor(|editor, window, cx| {
10402 editor.handle_input(")", window, cx);
10403 editor.handle_input("}", window, cx);
10404 });
10405 cx.assert_editor_state(
10406 &"
10407 )}ˇ
10408 )}ˇ
10409 )}ˇ
10410 "
10411 .unindent(),
10412 );
10413
10414 // ignore non-close brackets
10415 cx.update_editor(|editor, window, cx| {
10416 editor.handle_input("]", window, cx);
10417 editor.move_left(&MoveLeft, window, cx);
10418 editor.handle_input("]", window, cx);
10419 });
10420 cx.assert_editor_state(
10421 &"
10422 )}]ˇ]
10423 )}]ˇ]
10424 )}]ˇ]
10425 "
10426 .unindent(),
10427 );
10428}
10429
10430#[gpui::test]
10431async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10432 init_test(cx, |_| {});
10433
10434 let mut cx = EditorTestContext::new(cx).await;
10435
10436 let html_language = Arc::new(
10437 Language::new(
10438 LanguageConfig {
10439 name: "HTML".into(),
10440 brackets: BracketPairConfig {
10441 pairs: vec![
10442 BracketPair {
10443 start: "<".into(),
10444 end: ">".into(),
10445 close: true,
10446 ..Default::default()
10447 },
10448 BracketPair {
10449 start: "{".into(),
10450 end: "}".into(),
10451 close: true,
10452 ..Default::default()
10453 },
10454 BracketPair {
10455 start: "(".into(),
10456 end: ")".into(),
10457 close: true,
10458 ..Default::default()
10459 },
10460 ],
10461 ..Default::default()
10462 },
10463 autoclose_before: "})]>".into(),
10464 ..Default::default()
10465 },
10466 Some(tree_sitter_html::LANGUAGE.into()),
10467 )
10468 .with_injection_query(
10469 r#"
10470 (script_element
10471 (raw_text) @injection.content
10472 (#set! injection.language "javascript"))
10473 "#,
10474 )
10475 .unwrap(),
10476 );
10477
10478 let javascript_language = Arc::new(Language::new(
10479 LanguageConfig {
10480 name: "JavaScript".into(),
10481 brackets: BracketPairConfig {
10482 pairs: vec![
10483 BracketPair {
10484 start: "/*".into(),
10485 end: " */".into(),
10486 close: true,
10487 ..Default::default()
10488 },
10489 BracketPair {
10490 start: "{".into(),
10491 end: "}".into(),
10492 close: true,
10493 ..Default::default()
10494 },
10495 BracketPair {
10496 start: "(".into(),
10497 end: ")".into(),
10498 close: true,
10499 ..Default::default()
10500 },
10501 ],
10502 ..Default::default()
10503 },
10504 autoclose_before: "})]>".into(),
10505 ..Default::default()
10506 },
10507 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10508 ));
10509
10510 cx.language_registry().add(html_language.clone());
10511 cx.language_registry().add(javascript_language);
10512 cx.executor().run_until_parked();
10513
10514 cx.update_buffer(|buffer, cx| {
10515 buffer.set_language_immediate(Some(html_language), cx);
10516 });
10517
10518 cx.set_state(
10519 &r#"
10520 <body>ˇ
10521 <script>
10522 var x = 1;ˇ
10523 </script>
10524 </body>ˇ
10525 "#
10526 .unindent(),
10527 );
10528
10529 // Precondition: different languages are active at different locations.
10530 cx.update_editor(|editor, window, cx| {
10531 let snapshot = editor.snapshot(window, cx);
10532 let cursors = editor
10533 .selections
10534 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10535 let languages = cursors
10536 .iter()
10537 .map(|c| snapshot.language_at(c.start).unwrap().name())
10538 .collect::<Vec<_>>();
10539 assert_eq!(
10540 languages,
10541 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10542 );
10543 });
10544
10545 // Angle brackets autoclose in HTML, but not JavaScript.
10546 cx.update_editor(|editor, window, cx| {
10547 editor.handle_input("<", window, cx);
10548 editor.handle_input("a", window, cx);
10549 });
10550 cx.assert_editor_state(
10551 &r#"
10552 <body><aˇ>
10553 <script>
10554 var x = 1;<aˇ
10555 </script>
10556 </body><aˇ>
10557 "#
10558 .unindent(),
10559 );
10560
10561 // Curly braces and parens autoclose in both HTML and JavaScript.
10562 cx.update_editor(|editor, window, cx| {
10563 editor.handle_input(" b=", window, cx);
10564 editor.handle_input("{", window, cx);
10565 editor.handle_input("c", window, cx);
10566 editor.handle_input("(", window, cx);
10567 });
10568 cx.assert_editor_state(
10569 &r#"
10570 <body><a b={c(ˇ)}>
10571 <script>
10572 var x = 1;<a b={c(ˇ)}
10573 </script>
10574 </body><a b={c(ˇ)}>
10575 "#
10576 .unindent(),
10577 );
10578
10579 // Brackets that were already autoclosed are skipped.
10580 cx.update_editor(|editor, window, cx| {
10581 editor.handle_input(")", window, cx);
10582 editor.handle_input("d", window, cx);
10583 editor.handle_input("}", window, cx);
10584 });
10585 cx.assert_editor_state(
10586 &r#"
10587 <body><a b={c()d}ˇ>
10588 <script>
10589 var x = 1;<a b={c()d}ˇ
10590 </script>
10591 </body><a b={c()d}ˇ>
10592 "#
10593 .unindent(),
10594 );
10595 cx.update_editor(|editor, window, cx| {
10596 editor.handle_input(">", window, cx);
10597 });
10598 cx.assert_editor_state(
10599 &r#"
10600 <body><a b={c()d}>ˇ
10601 <script>
10602 var x = 1;<a b={c()d}>ˇ
10603 </script>
10604 </body><a b={c()d}>ˇ
10605 "#
10606 .unindent(),
10607 );
10608
10609 // Reset
10610 cx.set_state(
10611 &r#"
10612 <body>ˇ
10613 <script>
10614 var x = 1;ˇ
10615 </script>
10616 </body>ˇ
10617 "#
10618 .unindent(),
10619 );
10620
10621 cx.update_editor(|editor, window, cx| {
10622 editor.handle_input("<", window, cx);
10623 });
10624 cx.assert_editor_state(
10625 &r#"
10626 <body><ˇ>
10627 <script>
10628 var x = 1;<ˇ
10629 </script>
10630 </body><ˇ>
10631 "#
10632 .unindent(),
10633 );
10634
10635 // When backspacing, the closing angle brackets are removed.
10636 cx.update_editor(|editor, window, cx| {
10637 editor.backspace(&Backspace, window, cx);
10638 });
10639 cx.assert_editor_state(
10640 &r#"
10641 <body>ˇ
10642 <script>
10643 var x = 1;ˇ
10644 </script>
10645 </body>ˇ
10646 "#
10647 .unindent(),
10648 );
10649
10650 // Block comments autoclose in JavaScript, but not HTML.
10651 cx.update_editor(|editor, window, cx| {
10652 editor.handle_input("/", window, cx);
10653 editor.handle_input("*", window, cx);
10654 });
10655 cx.assert_editor_state(
10656 &r#"
10657 <body>/*ˇ
10658 <script>
10659 var x = 1;/*ˇ */
10660 </script>
10661 </body>/*ˇ
10662 "#
10663 .unindent(),
10664 );
10665}
10666
10667#[gpui::test]
10668async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10669 init_test(cx, |_| {});
10670
10671 let mut cx = EditorTestContext::new(cx).await;
10672
10673 let rust_language = Arc::new(
10674 Language::new(
10675 LanguageConfig {
10676 name: "Rust".into(),
10677 brackets: serde_json::from_value(json!([
10678 { "start": "{", "end": "}", "close": true, "newline": true },
10679 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10680 ]))
10681 .unwrap(),
10682 autoclose_before: "})]>".into(),
10683 ..Default::default()
10684 },
10685 Some(tree_sitter_rust::LANGUAGE.into()),
10686 )
10687 .with_override_query("(string_literal) @string")
10688 .unwrap(),
10689 );
10690
10691 cx.language_registry().add(rust_language.clone());
10692 cx.update_buffer(|buffer, cx| {
10693 buffer.set_language_immediate(Some(rust_language), cx);
10694 });
10695
10696 cx.set_state(
10697 &r#"
10698 let x = ˇ
10699 "#
10700 .unindent(),
10701 );
10702
10703 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10704 cx.update_editor(|editor, window, cx| {
10705 editor.handle_input("\"", window, cx);
10706 });
10707 cx.assert_editor_state(
10708 &r#"
10709 let x = "ˇ"
10710 "#
10711 .unindent(),
10712 );
10713
10714 // Inserting another quotation mark. The cursor moves across the existing
10715 // automatically-inserted quotation mark.
10716 cx.update_editor(|editor, window, cx| {
10717 editor.handle_input("\"", window, cx);
10718 });
10719 cx.assert_editor_state(
10720 &r#"
10721 let x = ""ˇ
10722 "#
10723 .unindent(),
10724 );
10725
10726 // Reset
10727 cx.set_state(
10728 &r#"
10729 let x = ˇ
10730 "#
10731 .unindent(),
10732 );
10733
10734 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10735 cx.update_editor(|editor, window, cx| {
10736 editor.handle_input("\"", window, cx);
10737 editor.handle_input(" ", window, cx);
10738 editor.move_left(&Default::default(), window, cx);
10739 editor.handle_input("\\", window, cx);
10740 editor.handle_input("\"", window, cx);
10741 });
10742 cx.assert_editor_state(
10743 &r#"
10744 let x = "\"ˇ "
10745 "#
10746 .unindent(),
10747 );
10748
10749 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10750 // mark. Nothing is inserted.
10751 cx.update_editor(|editor, window, cx| {
10752 editor.move_right(&Default::default(), window, cx);
10753 editor.handle_input("\"", window, cx);
10754 });
10755 cx.assert_editor_state(
10756 &r#"
10757 let x = "\" "ˇ
10758 "#
10759 .unindent(),
10760 );
10761}
10762
10763#[gpui::test]
10764async fn test_surround_with_pair(cx: &mut TestAppContext) {
10765 init_test(cx, |_| {});
10766
10767 let language = Arc::new(Language::new(
10768 LanguageConfig {
10769 brackets: BracketPairConfig {
10770 pairs: vec![
10771 BracketPair {
10772 start: "{".to_string(),
10773 end: "}".to_string(),
10774 close: true,
10775 surround: true,
10776 newline: true,
10777 },
10778 BracketPair {
10779 start: "/* ".to_string(),
10780 end: "*/".to_string(),
10781 close: true,
10782 surround: true,
10783 ..Default::default()
10784 },
10785 ],
10786 ..Default::default()
10787 },
10788 ..Default::default()
10789 },
10790 Some(tree_sitter_rust::LANGUAGE.into()),
10791 ));
10792
10793 let text = r#"
10794 a
10795 b
10796 c
10797 "#
10798 .unindent();
10799
10800 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
10801 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10802 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10803 editor
10804 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10805 .await;
10806
10807 editor.update_in(cx, |editor, window, cx| {
10808 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10809 s.select_display_ranges([
10810 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10811 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10812 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10813 ])
10814 });
10815
10816 editor.handle_input("{", window, cx);
10817 editor.handle_input("{", window, cx);
10818 editor.handle_input("{", window, cx);
10819 assert_eq!(
10820 editor.text(cx),
10821 "
10822 {{{a}}}
10823 {{{b}}}
10824 {{{c}}}
10825 "
10826 .unindent()
10827 );
10828 assert_eq!(
10829 display_ranges(editor, cx),
10830 [
10831 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10832 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10833 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10834 ]
10835 );
10836
10837 editor.undo(&Undo, window, cx);
10838 editor.undo(&Undo, window, cx);
10839 editor.undo(&Undo, window, cx);
10840 assert_eq!(
10841 editor.text(cx),
10842 "
10843 a
10844 b
10845 c
10846 "
10847 .unindent()
10848 );
10849 assert_eq!(
10850 display_ranges(editor, cx),
10851 [
10852 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10853 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10854 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10855 ]
10856 );
10857
10858 // Ensure inserting the first character of a multi-byte bracket pair
10859 // doesn't surround the selections with the bracket.
10860 editor.handle_input("/", window, cx);
10861 assert_eq!(
10862 editor.text(cx),
10863 "
10864 /
10865 /
10866 /
10867 "
10868 .unindent()
10869 );
10870 assert_eq!(
10871 display_ranges(editor, cx),
10872 [
10873 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10874 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10875 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10876 ]
10877 );
10878
10879 editor.undo(&Undo, window, cx);
10880 assert_eq!(
10881 editor.text(cx),
10882 "
10883 a
10884 b
10885 c
10886 "
10887 .unindent()
10888 );
10889 assert_eq!(
10890 display_ranges(editor, cx),
10891 [
10892 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10893 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10894 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10895 ]
10896 );
10897
10898 // Ensure inserting the last character of a multi-byte bracket pair
10899 // doesn't surround the selections with the bracket.
10900 editor.handle_input("*", window, cx);
10901 assert_eq!(
10902 editor.text(cx),
10903 "
10904 *
10905 *
10906 *
10907 "
10908 .unindent()
10909 );
10910 assert_eq!(
10911 display_ranges(editor, cx),
10912 [
10913 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10914 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10915 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10916 ]
10917 );
10918 });
10919}
10920
10921#[gpui::test]
10922async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10923 init_test(cx, |_| {});
10924
10925 let language = Arc::new(Language::new(
10926 LanguageConfig {
10927 brackets: BracketPairConfig {
10928 pairs: vec![BracketPair {
10929 start: "{".to_string(),
10930 end: "}".to_string(),
10931 close: true,
10932 surround: true,
10933 newline: true,
10934 }],
10935 ..Default::default()
10936 },
10937 autoclose_before: "}".to_string(),
10938 ..Default::default()
10939 },
10940 Some(tree_sitter_rust::LANGUAGE.into()),
10941 ));
10942
10943 let text = r#"
10944 a
10945 b
10946 c
10947 "#
10948 .unindent();
10949
10950 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
10951 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10952 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10953 editor
10954 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10955 .await;
10956
10957 editor.update_in(cx, |editor, window, cx| {
10958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10959 s.select_ranges([
10960 Point::new(0, 1)..Point::new(0, 1),
10961 Point::new(1, 1)..Point::new(1, 1),
10962 Point::new(2, 1)..Point::new(2, 1),
10963 ])
10964 });
10965
10966 editor.handle_input("{", window, cx);
10967 editor.handle_input("{", window, cx);
10968 editor.handle_input("_", window, cx);
10969 assert_eq!(
10970 editor.text(cx),
10971 "
10972 a{{_}}
10973 b{{_}}
10974 c{{_}}
10975 "
10976 .unindent()
10977 );
10978 assert_eq!(
10979 editor
10980 .selections
10981 .ranges::<Point>(&editor.display_snapshot(cx)),
10982 [
10983 Point::new(0, 4)..Point::new(0, 4),
10984 Point::new(1, 4)..Point::new(1, 4),
10985 Point::new(2, 4)..Point::new(2, 4)
10986 ]
10987 );
10988
10989 editor.backspace(&Default::default(), window, cx);
10990 editor.backspace(&Default::default(), window, cx);
10991 assert_eq!(
10992 editor.text(cx),
10993 "
10994 a{}
10995 b{}
10996 c{}
10997 "
10998 .unindent()
10999 );
11000 assert_eq!(
11001 editor
11002 .selections
11003 .ranges::<Point>(&editor.display_snapshot(cx)),
11004 [
11005 Point::new(0, 2)..Point::new(0, 2),
11006 Point::new(1, 2)..Point::new(1, 2),
11007 Point::new(2, 2)..Point::new(2, 2)
11008 ]
11009 );
11010
11011 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11012 assert_eq!(
11013 editor.text(cx),
11014 "
11015 a
11016 b
11017 c
11018 "
11019 .unindent()
11020 );
11021 assert_eq!(
11022 editor
11023 .selections
11024 .ranges::<Point>(&editor.display_snapshot(cx)),
11025 [
11026 Point::new(0, 1)..Point::new(0, 1),
11027 Point::new(1, 1)..Point::new(1, 1),
11028 Point::new(2, 1)..Point::new(2, 1)
11029 ]
11030 );
11031 });
11032}
11033
11034#[gpui::test]
11035async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11036 init_test(cx, |settings| {
11037 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11038 });
11039
11040 let mut cx = EditorTestContext::new(cx).await;
11041
11042 let language = Arc::new(Language::new(
11043 LanguageConfig {
11044 brackets: BracketPairConfig {
11045 pairs: vec![
11046 BracketPair {
11047 start: "{".to_string(),
11048 end: "}".to_string(),
11049 close: true,
11050 surround: true,
11051 newline: true,
11052 },
11053 BracketPair {
11054 start: "(".to_string(),
11055 end: ")".to_string(),
11056 close: true,
11057 surround: true,
11058 newline: true,
11059 },
11060 BracketPair {
11061 start: "[".to_string(),
11062 end: "]".to_string(),
11063 close: false,
11064 surround: true,
11065 newline: true,
11066 },
11067 ],
11068 ..Default::default()
11069 },
11070 autoclose_before: "})]".to_string(),
11071 ..Default::default()
11072 },
11073 Some(tree_sitter_rust::LANGUAGE.into()),
11074 ));
11075
11076 cx.language_registry().add(language.clone());
11077 cx.update_buffer(|buffer, cx| {
11078 buffer.set_language_immediate(Some(language), cx);
11079 });
11080
11081 cx.set_state(
11082 &"
11083 {(ˇ)}
11084 [[ˇ]]
11085 {(ˇ)}
11086 "
11087 .unindent(),
11088 );
11089
11090 cx.update_editor(|editor, window, cx| {
11091 editor.backspace(&Default::default(), window, cx);
11092 editor.backspace(&Default::default(), window, cx);
11093 });
11094
11095 cx.assert_editor_state(
11096 &"
11097 ˇ
11098 ˇ]]
11099 ˇ
11100 "
11101 .unindent(),
11102 );
11103
11104 cx.update_editor(|editor, window, cx| {
11105 editor.handle_input("{", window, cx);
11106 editor.handle_input("{", window, cx);
11107 editor.move_right(&MoveRight, window, cx);
11108 editor.move_right(&MoveRight, window, cx);
11109 editor.move_left(&MoveLeft, window, cx);
11110 editor.move_left(&MoveLeft, window, cx);
11111 editor.backspace(&Default::default(), window, cx);
11112 });
11113
11114 cx.assert_editor_state(
11115 &"
11116 {ˇ}
11117 {ˇ}]]
11118 {ˇ}
11119 "
11120 .unindent(),
11121 );
11122
11123 cx.update_editor(|editor, window, cx| {
11124 editor.backspace(&Default::default(), window, cx);
11125 });
11126
11127 cx.assert_editor_state(
11128 &"
11129 ˇ
11130 ˇ]]
11131 ˇ
11132 "
11133 .unindent(),
11134 );
11135}
11136
11137#[gpui::test]
11138async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11139 init_test(cx, |_| {});
11140
11141 let language = Arc::new(Language::new(
11142 LanguageConfig::default(),
11143 Some(tree_sitter_rust::LANGUAGE.into()),
11144 ));
11145
11146 let buffer = cx.new(|cx| Buffer::local("", cx).with_language_immediate(language, cx));
11147 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11148 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11149 editor
11150 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11151 .await;
11152
11153 editor.update_in(cx, |editor, window, cx| {
11154 editor.set_auto_replace_emoji_shortcode(true);
11155
11156 editor.handle_input("Hello ", window, cx);
11157 editor.handle_input(":wave", window, cx);
11158 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11159
11160 editor.handle_input(":", window, cx);
11161 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11162
11163 editor.handle_input(" :smile", window, cx);
11164 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11165
11166 editor.handle_input(":", window, cx);
11167 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11168
11169 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11170 editor.handle_input(":wave", window, cx);
11171 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11172
11173 editor.handle_input(":", window, cx);
11174 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11175
11176 editor.handle_input(":1", window, cx);
11177 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11178
11179 editor.handle_input(":", window, cx);
11180 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11181
11182 // Ensure shortcode does not get replaced when it is part of a word
11183 editor.handle_input(" Test:wave", window, cx);
11184 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11185
11186 editor.handle_input(":", window, cx);
11187 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11188
11189 editor.set_auto_replace_emoji_shortcode(false);
11190
11191 // Ensure shortcode does not get replaced when auto replace is off
11192 editor.handle_input(" :wave", window, cx);
11193 assert_eq!(
11194 editor.text(cx),
11195 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11196 );
11197
11198 editor.handle_input(":", window, cx);
11199 assert_eq!(
11200 editor.text(cx),
11201 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11202 );
11203 });
11204}
11205
11206#[gpui::test]
11207async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11208 init_test(cx, |_| {});
11209
11210 let (text, insertion_ranges) = marked_text_ranges(
11211 indoc! {"
11212 ˇ
11213 "},
11214 false,
11215 );
11216
11217 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11218 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11219
11220 _ = editor.update_in(cx, |editor, window, cx| {
11221 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11222
11223 editor
11224 .insert_snippet(
11225 &insertion_ranges
11226 .iter()
11227 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11228 .collect::<Vec<_>>(),
11229 snippet,
11230 window,
11231 cx,
11232 )
11233 .unwrap();
11234
11235 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11236 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11237 assert_eq!(editor.text(cx), expected_text);
11238 assert_eq!(
11239 editor.selections.ranges(&editor.display_snapshot(cx)),
11240 selection_ranges
11241 .iter()
11242 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11243 .collect::<Vec<_>>()
11244 );
11245 }
11246
11247 assert(
11248 editor,
11249 cx,
11250 indoc! {"
11251 type «» =•
11252 "},
11253 );
11254
11255 assert!(editor.context_menu_visible(), "There should be a matches");
11256 });
11257}
11258
11259#[gpui::test]
11260async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11261 init_test(cx, |_| {});
11262
11263 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11264 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11265 assert_eq!(editor.text(cx), expected_text);
11266 assert_eq!(
11267 editor.selections.ranges(&editor.display_snapshot(cx)),
11268 selection_ranges
11269 .iter()
11270 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11271 .collect::<Vec<_>>()
11272 );
11273 }
11274
11275 let (text, insertion_ranges) = marked_text_ranges(
11276 indoc! {"
11277 ˇ
11278 "},
11279 false,
11280 );
11281
11282 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11283 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11284
11285 _ = editor.update_in(cx, |editor, window, cx| {
11286 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11287
11288 editor
11289 .insert_snippet(
11290 &insertion_ranges
11291 .iter()
11292 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11293 .collect::<Vec<_>>(),
11294 snippet,
11295 window,
11296 cx,
11297 )
11298 .unwrap();
11299
11300 assert_state(
11301 editor,
11302 cx,
11303 indoc! {"
11304 type «» = ;•
11305 "},
11306 );
11307
11308 assert!(
11309 editor.context_menu_visible(),
11310 "Context menu should be visible for placeholder choices"
11311 );
11312
11313 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11314
11315 assert_state(
11316 editor,
11317 cx,
11318 indoc! {"
11319 type = «»;•
11320 "},
11321 );
11322
11323 assert!(
11324 !editor.context_menu_visible(),
11325 "Context menu should be hidden after moving to next tabstop"
11326 );
11327
11328 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11329
11330 assert_state(
11331 editor,
11332 cx,
11333 indoc! {"
11334 type = ; ˇ
11335 "},
11336 );
11337
11338 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11339
11340 assert_state(
11341 editor,
11342 cx,
11343 indoc! {"
11344 type = ; ˇ
11345 "},
11346 );
11347 });
11348
11349 _ = editor.update_in(cx, |editor, window, cx| {
11350 editor.select_all(&SelectAll, window, cx);
11351 editor.backspace(&Backspace, window, cx);
11352
11353 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11354 let insertion_ranges = editor
11355 .selections
11356 .all(&editor.display_snapshot(cx))
11357 .iter()
11358 .map(|s| s.range())
11359 .collect::<Vec<_>>();
11360
11361 editor
11362 .insert_snippet(&insertion_ranges, snippet, window, cx)
11363 .unwrap();
11364
11365 assert_state(editor, cx, "fn «» = value;•");
11366
11367 assert!(
11368 editor.context_menu_visible(),
11369 "Context menu should be visible for placeholder choices"
11370 );
11371
11372 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11373
11374 assert_state(editor, cx, "fn = «valueˇ»;•");
11375
11376 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11377
11378 assert_state(editor, cx, "fn «» = value;•");
11379
11380 assert!(
11381 editor.context_menu_visible(),
11382 "Context menu should be visible again after returning to first tabstop"
11383 );
11384
11385 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11386
11387 assert_state(editor, cx, "fn «» = value;•");
11388 });
11389}
11390
11391#[gpui::test]
11392async fn test_snippets(cx: &mut TestAppContext) {
11393 init_test(cx, |_| {});
11394
11395 let mut cx = EditorTestContext::new(cx).await;
11396
11397 cx.set_state(indoc! {"
11398 a.ˇ b
11399 a.ˇ b
11400 a.ˇ b
11401 "});
11402
11403 cx.update_editor(|editor, window, cx| {
11404 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11405 let insertion_ranges = editor
11406 .selections
11407 .all(&editor.display_snapshot(cx))
11408 .iter()
11409 .map(|s| s.range())
11410 .collect::<Vec<_>>();
11411 editor
11412 .insert_snippet(&insertion_ranges, snippet, window, cx)
11413 .unwrap();
11414 });
11415
11416 cx.assert_editor_state(indoc! {"
11417 a.f(«oneˇ», two, «threeˇ») b
11418 a.f(«oneˇ», two, «threeˇ») b
11419 a.f(«oneˇ», two, «threeˇ») b
11420 "});
11421
11422 // Can't move earlier than the first tab stop
11423 cx.update_editor(|editor, window, cx| {
11424 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11425 });
11426 cx.assert_editor_state(indoc! {"
11427 a.f(«oneˇ», two, «threeˇ») b
11428 a.f(«oneˇ», two, «threeˇ») b
11429 a.f(«oneˇ», two, «threeˇ») b
11430 "});
11431
11432 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11433 cx.assert_editor_state(indoc! {"
11434 a.f(one, «twoˇ», three) b
11435 a.f(one, «twoˇ», three) b
11436 a.f(one, «twoˇ», three) b
11437 "});
11438
11439 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11440 cx.assert_editor_state(indoc! {"
11441 a.f(«oneˇ», two, «threeˇ») b
11442 a.f(«oneˇ», two, «threeˇ») b
11443 a.f(«oneˇ», two, «threeˇ») b
11444 "});
11445
11446 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11447 cx.assert_editor_state(indoc! {"
11448 a.f(one, «twoˇ», three) b
11449 a.f(one, «twoˇ», three) b
11450 a.f(one, «twoˇ», three) b
11451 "});
11452 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11453 cx.assert_editor_state(indoc! {"
11454 a.f(one, two, three)ˇ b
11455 a.f(one, two, three)ˇ b
11456 a.f(one, two, three)ˇ b
11457 "});
11458
11459 // As soon as the last tab stop is reached, snippet state is gone
11460 cx.update_editor(|editor, window, cx| {
11461 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11462 });
11463 cx.assert_editor_state(indoc! {"
11464 a.f(one, two, three)ˇ b
11465 a.f(one, two, three)ˇ b
11466 a.f(one, two, three)ˇ b
11467 "});
11468}
11469
11470#[gpui::test]
11471async fn test_snippet_indentation(cx: &mut TestAppContext) {
11472 init_test(cx, |_| {});
11473
11474 let mut cx = EditorTestContext::new(cx).await;
11475
11476 cx.update_editor(|editor, window, cx| {
11477 let snippet = Snippet::parse(indoc! {"
11478 /*
11479 * Multiline comment with leading indentation
11480 *
11481 * $1
11482 */
11483 $0"})
11484 .unwrap();
11485 let insertion_ranges = editor
11486 .selections
11487 .all(&editor.display_snapshot(cx))
11488 .iter()
11489 .map(|s| s.range())
11490 .collect::<Vec<_>>();
11491 editor
11492 .insert_snippet(&insertion_ranges, snippet, window, cx)
11493 .unwrap();
11494 });
11495
11496 cx.assert_editor_state(indoc! {"
11497 /*
11498 * Multiline comment with leading indentation
11499 *
11500 * ˇ
11501 */
11502 "});
11503
11504 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11505 cx.assert_editor_state(indoc! {"
11506 /*
11507 * Multiline comment with leading indentation
11508 *
11509 *•
11510 */
11511 ˇ"});
11512}
11513
11514#[gpui::test]
11515async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11516 init_test(cx, |_| {});
11517
11518 let mut cx = EditorTestContext::new(cx).await;
11519 cx.update_editor(|editor, _, cx| {
11520 editor.project().unwrap().update(cx, |project, cx| {
11521 project.snippets().update(cx, |snippets, _cx| {
11522 let snippet = project::snippet_provider::Snippet {
11523 prefix: vec!["multi word".to_string()],
11524 body: "this is many words".to_string(),
11525 description: Some("description".to_string()),
11526 name: "multi-word snippet test".to_string(),
11527 };
11528 snippets.add_snippet_for_test(
11529 None,
11530 PathBuf::from("test_snippets.json"),
11531 vec![Arc::new(snippet)],
11532 );
11533 });
11534 })
11535 });
11536
11537 for (input_to_simulate, should_match_snippet) in [
11538 ("m", true),
11539 ("m ", true),
11540 ("m w", true),
11541 ("aa m w", true),
11542 ("aa m g", false),
11543 ] {
11544 cx.set_state("ˇ");
11545 cx.simulate_input(input_to_simulate); // fails correctly
11546
11547 cx.update_editor(|editor, _, _| {
11548 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11549 else {
11550 assert!(!should_match_snippet); // no completions! don't even show the menu
11551 return;
11552 };
11553 assert!(context_menu.visible());
11554 let completions = context_menu.completions.borrow();
11555
11556 assert_eq!(!completions.is_empty(), should_match_snippet);
11557 });
11558 }
11559}
11560
11561#[gpui::test]
11562async fn test_document_format_during_save(cx: &mut TestAppContext) {
11563 init_test(cx, |_| {});
11564
11565 let fs = FakeFs::new(cx.executor());
11566 fs.insert_file(path!("/file.rs"), Default::default()).await;
11567
11568 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11569
11570 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11571 language_registry.add(rust_lang());
11572 let mut fake_servers = language_registry.register_fake_lsp(
11573 "Rust",
11574 FakeLspAdapter {
11575 capabilities: lsp::ServerCapabilities {
11576 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11577 ..Default::default()
11578 },
11579 ..Default::default()
11580 },
11581 );
11582
11583 let buffer = project
11584 .update(cx, |project, cx| {
11585 project.open_local_buffer(path!("/file.rs"), cx)
11586 })
11587 .await
11588 .unwrap();
11589
11590 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11591 let (editor, cx) = cx.add_window_view(|window, cx| {
11592 build_editor_with_project(project.clone(), buffer, window, cx)
11593 });
11594 editor.update_in(cx, |editor, window, cx| {
11595 editor.set_text("one\ntwo\nthree\n", window, cx)
11596 });
11597 assert!(cx.read(|cx| editor.is_dirty(cx)));
11598
11599 cx.executor().start_waiting();
11600 let fake_server = fake_servers.next().await.unwrap();
11601
11602 {
11603 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11604 move |params, _| async move {
11605 assert_eq!(
11606 params.text_document.uri,
11607 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11608 );
11609 assert_eq!(params.options.tab_size, 4);
11610 Ok(Some(vec![lsp::TextEdit::new(
11611 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11612 ", ".to_string(),
11613 )]))
11614 },
11615 );
11616 let save = editor
11617 .update_in(cx, |editor, window, cx| {
11618 editor.save(
11619 SaveOptions {
11620 format: true,
11621 autosave: false,
11622 },
11623 project.clone(),
11624 window,
11625 cx,
11626 )
11627 })
11628 .unwrap();
11629 cx.executor().start_waiting();
11630 save.await;
11631
11632 assert_eq!(
11633 editor.update(cx, |editor, cx| editor.text(cx)),
11634 "one, two\nthree\n"
11635 );
11636 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11637 }
11638
11639 {
11640 editor.update_in(cx, |editor, window, cx| {
11641 editor.set_text("one\ntwo\nthree\n", window, cx)
11642 });
11643 assert!(cx.read(|cx| editor.is_dirty(cx)));
11644
11645 // Ensure we can still save even if formatting hangs.
11646 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11647 move |params, _| async move {
11648 assert_eq!(
11649 params.text_document.uri,
11650 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11651 );
11652 futures::future::pending::<()>().await;
11653 unreachable!()
11654 },
11655 );
11656 let save = editor
11657 .update_in(cx, |editor, window, cx| {
11658 editor.save(
11659 SaveOptions {
11660 format: true,
11661 autosave: false,
11662 },
11663 project.clone(),
11664 window,
11665 cx,
11666 )
11667 })
11668 .unwrap();
11669 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11670 cx.executor().start_waiting();
11671 save.await;
11672 assert_eq!(
11673 editor.update(cx, |editor, cx| editor.text(cx)),
11674 "one\ntwo\nthree\n"
11675 );
11676 }
11677
11678 // Set rust language override and assert overridden tabsize is sent to language server
11679 update_test_language_settings(cx, |settings| {
11680 settings.languages.0.insert(
11681 "Rust".into(),
11682 LanguageSettingsContent {
11683 tab_size: NonZeroU32::new(8),
11684 ..Default::default()
11685 },
11686 );
11687 });
11688
11689 {
11690 editor.update_in(cx, |editor, window, cx| {
11691 editor.set_text("somehting_new\n", window, cx)
11692 });
11693 assert!(cx.read(|cx| editor.is_dirty(cx)));
11694 let _formatting_request_signal = fake_server
11695 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11696 assert_eq!(
11697 params.text_document.uri,
11698 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11699 );
11700 assert_eq!(params.options.tab_size, 8);
11701 Ok(Some(vec![]))
11702 });
11703 let save = editor
11704 .update_in(cx, |editor, window, cx| {
11705 editor.save(
11706 SaveOptions {
11707 format: true,
11708 autosave: false,
11709 },
11710 project.clone(),
11711 window,
11712 cx,
11713 )
11714 })
11715 .unwrap();
11716 cx.executor().start_waiting();
11717 save.await;
11718 }
11719}
11720
11721#[gpui::test]
11722async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11723 init_test(cx, |settings| {
11724 settings.defaults.ensure_final_newline_on_save = Some(false);
11725 });
11726
11727 let fs = FakeFs::new(cx.executor());
11728 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11729
11730 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11731
11732 let buffer = project
11733 .update(cx, |project, cx| {
11734 project.open_local_buffer(path!("/file.txt"), cx)
11735 })
11736 .await
11737 .unwrap();
11738
11739 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11740 let (editor, cx) = cx.add_window_view(|window, cx| {
11741 build_editor_with_project(project.clone(), buffer, window, cx)
11742 });
11743 editor.update_in(cx, |editor, window, cx| {
11744 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11745 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11746 });
11747 });
11748 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11749
11750 editor.update_in(cx, |editor, window, cx| {
11751 editor.handle_input("\n", window, cx)
11752 });
11753 cx.run_until_parked();
11754 save(&editor, &project, cx).await;
11755 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11756
11757 editor.update_in(cx, |editor, window, cx| {
11758 editor.undo(&Default::default(), window, cx);
11759 });
11760 save(&editor, &project, cx).await;
11761 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11762
11763 editor.update_in(cx, |editor, window, cx| {
11764 editor.redo(&Default::default(), window, cx);
11765 });
11766 cx.run_until_parked();
11767 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11768
11769 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11770 let save = editor
11771 .update_in(cx, |editor, window, cx| {
11772 editor.save(
11773 SaveOptions {
11774 format: true,
11775 autosave: false,
11776 },
11777 project.clone(),
11778 window,
11779 cx,
11780 )
11781 })
11782 .unwrap();
11783 cx.executor().start_waiting();
11784 save.await;
11785 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11786 }
11787}
11788
11789#[gpui::test]
11790async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11791 init_test(cx, |_| {});
11792
11793 let cols = 4;
11794 let rows = 10;
11795 let sample_text_1 = sample_text(rows, cols, 'a');
11796 assert_eq!(
11797 sample_text_1,
11798 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11799 );
11800 let sample_text_2 = sample_text(rows, cols, 'l');
11801 assert_eq!(
11802 sample_text_2,
11803 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11804 );
11805 let sample_text_3 = sample_text(rows, cols, 'v');
11806 assert_eq!(
11807 sample_text_3,
11808 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11809 );
11810
11811 let fs = FakeFs::new(cx.executor());
11812 fs.insert_tree(
11813 path!("/a"),
11814 json!({
11815 "main.rs": sample_text_1,
11816 "other.rs": sample_text_2,
11817 "lib.rs": sample_text_3,
11818 }),
11819 )
11820 .await;
11821
11822 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11823 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11824 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11825
11826 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11827 language_registry.add(rust_lang());
11828 let mut fake_servers = language_registry.register_fake_lsp(
11829 "Rust",
11830 FakeLspAdapter {
11831 capabilities: lsp::ServerCapabilities {
11832 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11833 ..Default::default()
11834 },
11835 ..Default::default()
11836 },
11837 );
11838
11839 let worktree = project.update(cx, |project, cx| {
11840 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11841 assert_eq!(worktrees.len(), 1);
11842 worktrees.pop().unwrap()
11843 });
11844 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11845
11846 let buffer_1 = project
11847 .update(cx, |project, cx| {
11848 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11849 })
11850 .await
11851 .unwrap();
11852 let buffer_2 = project
11853 .update(cx, |project, cx| {
11854 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11855 })
11856 .await
11857 .unwrap();
11858 let buffer_3 = project
11859 .update(cx, |project, cx| {
11860 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11861 })
11862 .await
11863 .unwrap();
11864
11865 let multi_buffer = cx.new(|cx| {
11866 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11867 multi_buffer.push_excerpts(
11868 buffer_1.clone(),
11869 [
11870 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11871 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11872 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11873 ],
11874 cx,
11875 );
11876 multi_buffer.push_excerpts(
11877 buffer_2.clone(),
11878 [
11879 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11880 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11881 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11882 ],
11883 cx,
11884 );
11885 multi_buffer.push_excerpts(
11886 buffer_3.clone(),
11887 [
11888 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11889 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11890 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11891 ],
11892 cx,
11893 );
11894 multi_buffer
11895 });
11896 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11897 Editor::new(
11898 EditorMode::full(),
11899 multi_buffer,
11900 Some(project.clone()),
11901 window,
11902 cx,
11903 )
11904 });
11905
11906 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11907 editor.change_selections(
11908 SelectionEffects::scroll(Autoscroll::Next),
11909 window,
11910 cx,
11911 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
11912 );
11913 editor.insert("|one|two|three|", window, cx);
11914 });
11915 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11916 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11917 editor.change_selections(
11918 SelectionEffects::scroll(Autoscroll::Next),
11919 window,
11920 cx,
11921 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
11922 );
11923 editor.insert("|four|five|six|", window, cx);
11924 });
11925 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11926
11927 // First two buffers should be edited, but not the third one.
11928 assert_eq!(
11929 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11930 "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}",
11931 );
11932 buffer_1.update(cx, |buffer, _| {
11933 assert!(buffer.is_dirty());
11934 assert_eq!(
11935 buffer.text(),
11936 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11937 )
11938 });
11939 buffer_2.update(cx, |buffer, _| {
11940 assert!(buffer.is_dirty());
11941 assert_eq!(
11942 buffer.text(),
11943 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11944 )
11945 });
11946 buffer_3.update(cx, |buffer, _| {
11947 assert!(!buffer.is_dirty());
11948 assert_eq!(buffer.text(), sample_text_3,)
11949 });
11950 cx.executor().run_until_parked();
11951
11952 cx.executor().start_waiting();
11953 let save = multi_buffer_editor
11954 .update_in(cx, |editor, window, cx| {
11955 editor.save(
11956 SaveOptions {
11957 format: true,
11958 autosave: false,
11959 },
11960 project.clone(),
11961 window,
11962 cx,
11963 )
11964 })
11965 .unwrap();
11966
11967 let fake_server = fake_servers.next().await.unwrap();
11968 fake_server
11969 .server
11970 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11971 Ok(Some(vec![lsp::TextEdit::new(
11972 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11973 format!("[{} formatted]", params.text_document.uri),
11974 )]))
11975 })
11976 .detach();
11977 save.await;
11978
11979 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11980 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11981 assert_eq!(
11982 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11983 uri!(
11984 "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}"
11985 ),
11986 );
11987 buffer_1.update(cx, |buffer, _| {
11988 assert!(!buffer.is_dirty());
11989 assert_eq!(
11990 buffer.text(),
11991 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11992 )
11993 });
11994 buffer_2.update(cx, |buffer, _| {
11995 assert!(!buffer.is_dirty());
11996 assert_eq!(
11997 buffer.text(),
11998 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11999 )
12000 });
12001 buffer_3.update(cx, |buffer, _| {
12002 assert!(!buffer.is_dirty());
12003 assert_eq!(buffer.text(), sample_text_3,)
12004 });
12005}
12006
12007#[gpui::test]
12008async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12009 init_test(cx, |_| {});
12010
12011 let fs = FakeFs::new(cx.executor());
12012 fs.insert_tree(
12013 path!("/dir"),
12014 json!({
12015 "file1.rs": "fn main() { println!(\"hello\"); }",
12016 "file2.rs": "fn test() { println!(\"test\"); }",
12017 "file3.rs": "fn other() { println!(\"other\"); }\n",
12018 }),
12019 )
12020 .await;
12021
12022 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12023 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12024 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12025
12026 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12027 language_registry.add(rust_lang());
12028
12029 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12030 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12031
12032 // Open three buffers
12033 let buffer_1 = project
12034 .update(cx, |project, cx| {
12035 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12036 })
12037 .await
12038 .unwrap();
12039 let buffer_2 = project
12040 .update(cx, |project, cx| {
12041 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12042 })
12043 .await
12044 .unwrap();
12045 let buffer_3 = project
12046 .update(cx, |project, cx| {
12047 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12048 })
12049 .await
12050 .unwrap();
12051
12052 // Create a multi-buffer with all three buffers
12053 let multi_buffer = cx.new(|cx| {
12054 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12055 multi_buffer.push_excerpts(
12056 buffer_1.clone(),
12057 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12058 cx,
12059 );
12060 multi_buffer.push_excerpts(
12061 buffer_2.clone(),
12062 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12063 cx,
12064 );
12065 multi_buffer.push_excerpts(
12066 buffer_3.clone(),
12067 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12068 cx,
12069 );
12070 multi_buffer
12071 });
12072
12073 let editor = cx.new_window_entity(|window, cx| {
12074 Editor::new(
12075 EditorMode::full(),
12076 multi_buffer,
12077 Some(project.clone()),
12078 window,
12079 cx,
12080 )
12081 });
12082
12083 // Edit only the first buffer
12084 editor.update_in(cx, |editor, window, cx| {
12085 editor.change_selections(
12086 SelectionEffects::scroll(Autoscroll::Next),
12087 window,
12088 cx,
12089 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12090 );
12091 editor.insert("// edited", window, cx);
12092 });
12093
12094 // Verify that only buffer 1 is dirty
12095 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12096 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12097 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12098
12099 // Get write counts after file creation (files were created with initial content)
12100 // We expect each file to have been written once during creation
12101 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12102 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12103 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12104
12105 // Perform autosave
12106 let save_task = editor.update_in(cx, |editor, window, cx| {
12107 editor.save(
12108 SaveOptions {
12109 format: true,
12110 autosave: true,
12111 },
12112 project.clone(),
12113 window,
12114 cx,
12115 )
12116 });
12117 save_task.await.unwrap();
12118
12119 // Only the dirty buffer should have been saved
12120 assert_eq!(
12121 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12122 1,
12123 "Buffer 1 was dirty, so it should have been written once during autosave"
12124 );
12125 assert_eq!(
12126 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12127 0,
12128 "Buffer 2 was clean, so it should not have been written during autosave"
12129 );
12130 assert_eq!(
12131 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12132 0,
12133 "Buffer 3 was clean, so it should not have been written during autosave"
12134 );
12135
12136 // Verify buffer states after autosave
12137 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12138 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12139 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12140
12141 // Now perform a manual save (format = true)
12142 let save_task = editor.update_in(cx, |editor, window, cx| {
12143 editor.save(
12144 SaveOptions {
12145 format: true,
12146 autosave: false,
12147 },
12148 project.clone(),
12149 window,
12150 cx,
12151 )
12152 });
12153 save_task.await.unwrap();
12154
12155 // During manual save, clean buffers don't get written to disk
12156 // They just get did_save called for language server notifications
12157 assert_eq!(
12158 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12159 1,
12160 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12161 );
12162 assert_eq!(
12163 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12164 0,
12165 "Buffer 2 should not have been written at all"
12166 );
12167 assert_eq!(
12168 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12169 0,
12170 "Buffer 3 should not have been written at all"
12171 );
12172}
12173
12174async fn setup_range_format_test(
12175 cx: &mut TestAppContext,
12176) -> (
12177 Entity<Project>,
12178 Entity<Editor>,
12179 &mut gpui::VisualTestContext,
12180 lsp::FakeLanguageServer,
12181) {
12182 init_test(cx, |_| {});
12183
12184 let fs = FakeFs::new(cx.executor());
12185 fs.insert_file(path!("/file.rs"), Default::default()).await;
12186
12187 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12188
12189 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12190 language_registry.add(rust_lang());
12191 let mut fake_servers = language_registry.register_fake_lsp(
12192 "Rust",
12193 FakeLspAdapter {
12194 capabilities: lsp::ServerCapabilities {
12195 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12196 ..lsp::ServerCapabilities::default()
12197 },
12198 ..FakeLspAdapter::default()
12199 },
12200 );
12201
12202 let buffer = project
12203 .update(cx, |project, cx| {
12204 project.open_local_buffer(path!("/file.rs"), cx)
12205 })
12206 .await
12207 .unwrap();
12208
12209 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12210 let (editor, cx) = cx.add_window_view(|window, cx| {
12211 build_editor_with_project(project.clone(), buffer, window, cx)
12212 });
12213
12214 cx.executor().start_waiting();
12215 let fake_server = fake_servers.next().await.unwrap();
12216
12217 (project, editor, cx, fake_server)
12218}
12219
12220#[gpui::test]
12221async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12222 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12223
12224 editor.update_in(cx, |editor, window, cx| {
12225 editor.set_text("one\ntwo\nthree\n", window, cx)
12226 });
12227 assert!(cx.read(|cx| editor.is_dirty(cx)));
12228
12229 let save = editor
12230 .update_in(cx, |editor, window, cx| {
12231 editor.save(
12232 SaveOptions {
12233 format: true,
12234 autosave: false,
12235 },
12236 project.clone(),
12237 window,
12238 cx,
12239 )
12240 })
12241 .unwrap();
12242 fake_server
12243 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12244 assert_eq!(
12245 params.text_document.uri,
12246 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12247 );
12248 assert_eq!(params.options.tab_size, 4);
12249 Ok(Some(vec![lsp::TextEdit::new(
12250 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12251 ", ".to_string(),
12252 )]))
12253 })
12254 .next()
12255 .await;
12256 cx.executor().start_waiting();
12257 save.await;
12258 assert_eq!(
12259 editor.update(cx, |editor, cx| editor.text(cx)),
12260 "one, two\nthree\n"
12261 );
12262 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12263}
12264
12265#[gpui::test]
12266async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12267 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12268
12269 editor.update_in(cx, |editor, window, cx| {
12270 editor.set_text("one\ntwo\nthree\n", window, cx)
12271 });
12272 assert!(cx.read(|cx| editor.is_dirty(cx)));
12273
12274 // Test that save still works when formatting hangs
12275 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12276 move |params, _| async move {
12277 assert_eq!(
12278 params.text_document.uri,
12279 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12280 );
12281 futures::future::pending::<()>().await;
12282 unreachable!()
12283 },
12284 );
12285 let save = editor
12286 .update_in(cx, |editor, window, cx| {
12287 editor.save(
12288 SaveOptions {
12289 format: true,
12290 autosave: false,
12291 },
12292 project.clone(),
12293 window,
12294 cx,
12295 )
12296 })
12297 .unwrap();
12298 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12299 cx.executor().start_waiting();
12300 save.await;
12301 assert_eq!(
12302 editor.update(cx, |editor, cx| editor.text(cx)),
12303 "one\ntwo\nthree\n"
12304 );
12305 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12306}
12307
12308#[gpui::test]
12309async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12310 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12311
12312 // Buffer starts clean, no formatting should be requested
12313 let save = editor
12314 .update_in(cx, |editor, window, cx| {
12315 editor.save(
12316 SaveOptions {
12317 format: false,
12318 autosave: false,
12319 },
12320 project.clone(),
12321 window,
12322 cx,
12323 )
12324 })
12325 .unwrap();
12326 let _pending_format_request = fake_server
12327 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12328 panic!("Should not be invoked");
12329 })
12330 .next();
12331 cx.executor().start_waiting();
12332 save.await;
12333 cx.run_until_parked();
12334}
12335
12336#[gpui::test]
12337async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12338 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12339
12340 // Set Rust language override and assert overridden tabsize is sent to language server
12341 update_test_language_settings(cx, |settings| {
12342 settings.languages.0.insert(
12343 "Rust".into(),
12344 LanguageSettingsContent {
12345 tab_size: NonZeroU32::new(8),
12346 ..Default::default()
12347 },
12348 );
12349 });
12350
12351 editor.update_in(cx, |editor, window, cx| {
12352 editor.set_text("something_new\n", window, cx)
12353 });
12354 assert!(cx.read(|cx| editor.is_dirty(cx)));
12355 let save = editor
12356 .update_in(cx, |editor, window, cx| {
12357 editor.save(
12358 SaveOptions {
12359 format: true,
12360 autosave: false,
12361 },
12362 project.clone(),
12363 window,
12364 cx,
12365 )
12366 })
12367 .unwrap();
12368 fake_server
12369 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12370 assert_eq!(
12371 params.text_document.uri,
12372 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12373 );
12374 assert_eq!(params.options.tab_size, 8);
12375 Ok(Some(Vec::new()))
12376 })
12377 .next()
12378 .await;
12379 save.await;
12380}
12381
12382#[gpui::test]
12383async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12384 init_test(cx, |settings| {
12385 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12386 settings::LanguageServerFormatterSpecifier::Current,
12387 )))
12388 });
12389
12390 let fs = FakeFs::new(cx.executor());
12391 fs.insert_file(path!("/file.rs"), Default::default()).await;
12392
12393 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12394
12395 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12396 language_registry.add(Arc::new(Language::new(
12397 LanguageConfig {
12398 name: "Rust".into(),
12399 matcher: LanguageMatcher {
12400 path_suffixes: vec!["rs".to_string()],
12401 ..Default::default()
12402 },
12403 ..LanguageConfig::default()
12404 },
12405 Some(tree_sitter_rust::LANGUAGE.into()),
12406 )));
12407 update_test_language_settings(cx, |settings| {
12408 // Enable Prettier formatting for the same buffer, and ensure
12409 // LSP is called instead of Prettier.
12410 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12411 });
12412 let mut fake_servers = language_registry.register_fake_lsp(
12413 "Rust",
12414 FakeLspAdapter {
12415 capabilities: lsp::ServerCapabilities {
12416 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12417 ..Default::default()
12418 },
12419 ..Default::default()
12420 },
12421 );
12422
12423 let buffer = project
12424 .update(cx, |project, cx| {
12425 project.open_local_buffer(path!("/file.rs"), cx)
12426 })
12427 .await
12428 .unwrap();
12429
12430 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12431 let (editor, cx) = cx.add_window_view(|window, cx| {
12432 build_editor_with_project(project.clone(), buffer, window, cx)
12433 });
12434 editor.update_in(cx, |editor, window, cx| {
12435 editor.set_text("one\ntwo\nthree\n", window, cx)
12436 });
12437
12438 cx.executor().start_waiting();
12439 let fake_server = fake_servers.next().await.unwrap();
12440
12441 let format = editor
12442 .update_in(cx, |editor, window, cx| {
12443 editor.perform_format(
12444 project.clone(),
12445 FormatTrigger::Manual,
12446 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12447 window,
12448 cx,
12449 )
12450 })
12451 .unwrap();
12452 fake_server
12453 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12454 assert_eq!(
12455 params.text_document.uri,
12456 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12457 );
12458 assert_eq!(params.options.tab_size, 4);
12459 Ok(Some(vec![lsp::TextEdit::new(
12460 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12461 ", ".to_string(),
12462 )]))
12463 })
12464 .next()
12465 .await;
12466 cx.executor().start_waiting();
12467 format.await;
12468 assert_eq!(
12469 editor.update(cx, |editor, cx| editor.text(cx)),
12470 "one, two\nthree\n"
12471 );
12472
12473 editor.update_in(cx, |editor, window, cx| {
12474 editor.set_text("one\ntwo\nthree\n", window, cx)
12475 });
12476 // Ensure we don't lock if formatting hangs.
12477 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12478 move |params, _| async move {
12479 assert_eq!(
12480 params.text_document.uri,
12481 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12482 );
12483 futures::future::pending::<()>().await;
12484 unreachable!()
12485 },
12486 );
12487 let format = editor
12488 .update_in(cx, |editor, window, cx| {
12489 editor.perform_format(
12490 project,
12491 FormatTrigger::Manual,
12492 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12493 window,
12494 cx,
12495 )
12496 })
12497 .unwrap();
12498 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12499 cx.executor().start_waiting();
12500 format.await;
12501 assert_eq!(
12502 editor.update(cx, |editor, cx| editor.text(cx)),
12503 "one\ntwo\nthree\n"
12504 );
12505}
12506
12507#[gpui::test]
12508async fn test_multiple_formatters(cx: &mut TestAppContext) {
12509 init_test(cx, |settings| {
12510 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12511 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12512 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12513 Formatter::CodeAction("code-action-1".into()),
12514 Formatter::CodeAction("code-action-2".into()),
12515 ]))
12516 });
12517
12518 let fs = FakeFs::new(cx.executor());
12519 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12520 .await;
12521
12522 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12523 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12524 language_registry.add(rust_lang());
12525
12526 let mut fake_servers = language_registry.register_fake_lsp(
12527 "Rust",
12528 FakeLspAdapter {
12529 capabilities: lsp::ServerCapabilities {
12530 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12531 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12532 commands: vec!["the-command-for-code-action-1".into()],
12533 ..Default::default()
12534 }),
12535 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12536 ..Default::default()
12537 },
12538 ..Default::default()
12539 },
12540 );
12541
12542 let buffer = project
12543 .update(cx, |project, cx| {
12544 project.open_local_buffer(path!("/file.rs"), cx)
12545 })
12546 .await
12547 .unwrap();
12548
12549 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12550 let (editor, cx) = cx.add_window_view(|window, cx| {
12551 build_editor_with_project(project.clone(), buffer, window, cx)
12552 });
12553
12554 cx.executor().start_waiting();
12555
12556 let fake_server = fake_servers.next().await.unwrap();
12557 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12558 move |_params, _| async move {
12559 Ok(Some(vec![lsp::TextEdit::new(
12560 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12561 "applied-formatting\n".to_string(),
12562 )]))
12563 },
12564 );
12565 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12566 move |params, _| async move {
12567 let requested_code_actions = params.context.only.expect("Expected code action request");
12568 assert_eq!(requested_code_actions.len(), 1);
12569
12570 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12571 let code_action = match requested_code_actions[0].as_str() {
12572 "code-action-1" => lsp::CodeAction {
12573 kind: Some("code-action-1".into()),
12574 edit: Some(lsp::WorkspaceEdit::new(
12575 [(
12576 uri,
12577 vec![lsp::TextEdit::new(
12578 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12579 "applied-code-action-1-edit\n".to_string(),
12580 )],
12581 )]
12582 .into_iter()
12583 .collect(),
12584 )),
12585 command: Some(lsp::Command {
12586 command: "the-command-for-code-action-1".into(),
12587 ..Default::default()
12588 }),
12589 ..Default::default()
12590 },
12591 "code-action-2" => lsp::CodeAction {
12592 kind: Some("code-action-2".into()),
12593 edit: Some(lsp::WorkspaceEdit::new(
12594 [(
12595 uri,
12596 vec![lsp::TextEdit::new(
12597 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12598 "applied-code-action-2-edit\n".to_string(),
12599 )],
12600 )]
12601 .into_iter()
12602 .collect(),
12603 )),
12604 ..Default::default()
12605 },
12606 req => panic!("Unexpected code action request: {:?}", req),
12607 };
12608 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12609 code_action,
12610 )]))
12611 },
12612 );
12613
12614 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12615 move |params, _| async move { Ok(params) }
12616 });
12617
12618 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12619 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12620 let fake = fake_server.clone();
12621 let lock = command_lock.clone();
12622 move |params, _| {
12623 assert_eq!(params.command, "the-command-for-code-action-1");
12624 let fake = fake.clone();
12625 let lock = lock.clone();
12626 async move {
12627 lock.lock().await;
12628 fake.server
12629 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12630 label: None,
12631 edit: lsp::WorkspaceEdit {
12632 changes: Some(
12633 [(
12634 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12635 vec![lsp::TextEdit {
12636 range: lsp::Range::new(
12637 lsp::Position::new(0, 0),
12638 lsp::Position::new(0, 0),
12639 ),
12640 new_text: "applied-code-action-1-command\n".into(),
12641 }],
12642 )]
12643 .into_iter()
12644 .collect(),
12645 ),
12646 ..Default::default()
12647 },
12648 })
12649 .await
12650 .into_response()
12651 .unwrap();
12652 Ok(Some(json!(null)))
12653 }
12654 }
12655 });
12656
12657 cx.executor().start_waiting();
12658 editor
12659 .update_in(cx, |editor, window, cx| {
12660 editor.perform_format(
12661 project.clone(),
12662 FormatTrigger::Manual,
12663 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12664 window,
12665 cx,
12666 )
12667 })
12668 .unwrap()
12669 .await;
12670 editor.update(cx, |editor, cx| {
12671 assert_eq!(
12672 editor.text(cx),
12673 r#"
12674 applied-code-action-2-edit
12675 applied-code-action-1-command
12676 applied-code-action-1-edit
12677 applied-formatting
12678 one
12679 two
12680 three
12681 "#
12682 .unindent()
12683 );
12684 });
12685
12686 editor.update_in(cx, |editor, window, cx| {
12687 editor.undo(&Default::default(), window, cx);
12688 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12689 });
12690
12691 // Perform a manual edit while waiting for an LSP command
12692 // that's being run as part of a formatting code action.
12693 let lock_guard = command_lock.lock().await;
12694 let format = editor
12695 .update_in(cx, |editor, window, cx| {
12696 editor.perform_format(
12697 project.clone(),
12698 FormatTrigger::Manual,
12699 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12700 window,
12701 cx,
12702 )
12703 })
12704 .unwrap();
12705 cx.run_until_parked();
12706 editor.update(cx, |editor, cx| {
12707 assert_eq!(
12708 editor.text(cx),
12709 r#"
12710 applied-code-action-1-edit
12711 applied-formatting
12712 one
12713 two
12714 three
12715 "#
12716 .unindent()
12717 );
12718
12719 editor.buffer.update(cx, |buffer, cx| {
12720 let ix = buffer.len(cx);
12721 buffer.edit([(ix..ix, "edited\n")], None, cx);
12722 });
12723 });
12724
12725 // Allow the LSP command to proceed. Because the buffer was edited,
12726 // the second code action will not be run.
12727 drop(lock_guard);
12728 format.await;
12729 editor.update_in(cx, |editor, window, cx| {
12730 assert_eq!(
12731 editor.text(cx),
12732 r#"
12733 applied-code-action-1-command
12734 applied-code-action-1-edit
12735 applied-formatting
12736 one
12737 two
12738 three
12739 edited
12740 "#
12741 .unindent()
12742 );
12743
12744 // The manual edit is undone first, because it is the last thing the user did
12745 // (even though the command completed afterwards).
12746 editor.undo(&Default::default(), window, cx);
12747 assert_eq!(
12748 editor.text(cx),
12749 r#"
12750 applied-code-action-1-command
12751 applied-code-action-1-edit
12752 applied-formatting
12753 one
12754 two
12755 three
12756 "#
12757 .unindent()
12758 );
12759
12760 // All the formatting (including the command, which completed after the manual edit)
12761 // is undone together.
12762 editor.undo(&Default::default(), window, cx);
12763 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12764 });
12765}
12766
12767#[gpui::test]
12768async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12769 init_test(cx, |settings| {
12770 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12771 settings::LanguageServerFormatterSpecifier::Current,
12772 )]))
12773 });
12774
12775 let fs = FakeFs::new(cx.executor());
12776 fs.insert_file(path!("/file.ts"), Default::default()).await;
12777
12778 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12779
12780 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12781 language_registry.add(Arc::new(Language::new(
12782 LanguageConfig {
12783 name: "TypeScript".into(),
12784 matcher: LanguageMatcher {
12785 path_suffixes: vec!["ts".to_string()],
12786 ..Default::default()
12787 },
12788 ..LanguageConfig::default()
12789 },
12790 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12791 )));
12792 update_test_language_settings(cx, |settings| {
12793 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12794 });
12795 let mut fake_servers = language_registry.register_fake_lsp(
12796 "TypeScript",
12797 FakeLspAdapter {
12798 capabilities: lsp::ServerCapabilities {
12799 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12800 ..Default::default()
12801 },
12802 ..Default::default()
12803 },
12804 );
12805
12806 let buffer = project
12807 .update(cx, |project, cx| {
12808 project.open_local_buffer(path!("/file.ts"), cx)
12809 })
12810 .await
12811 .unwrap();
12812
12813 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12814 let (editor, cx) = cx.add_window_view(|window, cx| {
12815 build_editor_with_project(project.clone(), buffer, window, cx)
12816 });
12817 editor.update_in(cx, |editor, window, cx| {
12818 editor.set_text(
12819 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12820 window,
12821 cx,
12822 )
12823 });
12824
12825 cx.executor().start_waiting();
12826 let fake_server = fake_servers.next().await.unwrap();
12827
12828 let format = editor
12829 .update_in(cx, |editor, window, cx| {
12830 editor.perform_code_action_kind(
12831 project.clone(),
12832 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12833 window,
12834 cx,
12835 )
12836 })
12837 .unwrap();
12838 fake_server
12839 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12840 assert_eq!(
12841 params.text_document.uri,
12842 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12843 );
12844 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12845 lsp::CodeAction {
12846 title: "Organize Imports".to_string(),
12847 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12848 edit: Some(lsp::WorkspaceEdit {
12849 changes: Some(
12850 [(
12851 params.text_document.uri.clone(),
12852 vec![lsp::TextEdit::new(
12853 lsp::Range::new(
12854 lsp::Position::new(1, 0),
12855 lsp::Position::new(2, 0),
12856 ),
12857 "".to_string(),
12858 )],
12859 )]
12860 .into_iter()
12861 .collect(),
12862 ),
12863 ..Default::default()
12864 }),
12865 ..Default::default()
12866 },
12867 )]))
12868 })
12869 .next()
12870 .await;
12871 cx.executor().start_waiting();
12872 format.await;
12873 assert_eq!(
12874 editor.update(cx, |editor, cx| editor.text(cx)),
12875 "import { a } from 'module';\n\nconst x = a;\n"
12876 );
12877
12878 editor.update_in(cx, |editor, window, cx| {
12879 editor.set_text(
12880 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12881 window,
12882 cx,
12883 )
12884 });
12885 // Ensure we don't lock if code action hangs.
12886 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12887 move |params, _| async move {
12888 assert_eq!(
12889 params.text_document.uri,
12890 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12891 );
12892 futures::future::pending::<()>().await;
12893 unreachable!()
12894 },
12895 );
12896 let format = editor
12897 .update_in(cx, |editor, window, cx| {
12898 editor.perform_code_action_kind(
12899 project,
12900 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12901 window,
12902 cx,
12903 )
12904 })
12905 .unwrap();
12906 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12907 cx.executor().start_waiting();
12908 format.await;
12909 assert_eq!(
12910 editor.update(cx, |editor, cx| editor.text(cx)),
12911 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12912 );
12913}
12914
12915#[gpui::test]
12916async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12917 init_test(cx, |_| {});
12918
12919 let mut cx = EditorLspTestContext::new_rust(
12920 lsp::ServerCapabilities {
12921 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12922 ..Default::default()
12923 },
12924 cx,
12925 )
12926 .await;
12927
12928 cx.set_state(indoc! {"
12929 one.twoˇ
12930 "});
12931
12932 // The format request takes a long time. When it completes, it inserts
12933 // a newline and an indent before the `.`
12934 cx.lsp
12935 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12936 let executor = cx.background_executor().clone();
12937 async move {
12938 executor.timer(Duration::from_millis(100)).await;
12939 Ok(Some(vec![lsp::TextEdit {
12940 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12941 new_text: "\n ".into(),
12942 }]))
12943 }
12944 });
12945
12946 // Submit a format request.
12947 let format_1 = cx
12948 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12949 .unwrap();
12950 cx.executor().run_until_parked();
12951
12952 // Submit a second format request.
12953 let format_2 = cx
12954 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12955 .unwrap();
12956 cx.executor().run_until_parked();
12957
12958 // Wait for both format requests to complete
12959 cx.executor().advance_clock(Duration::from_millis(200));
12960 cx.executor().start_waiting();
12961 format_1.await.unwrap();
12962 cx.executor().start_waiting();
12963 format_2.await.unwrap();
12964
12965 // The formatting edits only happens once.
12966 cx.assert_editor_state(indoc! {"
12967 one
12968 .twoˇ
12969 "});
12970}
12971
12972#[gpui::test]
12973async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12974 init_test(cx, |settings| {
12975 settings.defaults.formatter = Some(FormatterList::default())
12976 });
12977
12978 let mut cx = EditorLspTestContext::new_rust(
12979 lsp::ServerCapabilities {
12980 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12981 ..Default::default()
12982 },
12983 cx,
12984 )
12985 .await;
12986
12987 // Record which buffer changes have been sent to the language server
12988 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12989 cx.lsp
12990 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12991 let buffer_changes = buffer_changes.clone();
12992 move |params, _| {
12993 buffer_changes.lock().extend(
12994 params
12995 .content_changes
12996 .into_iter()
12997 .map(|e| (e.range.unwrap(), e.text)),
12998 );
12999 }
13000 });
13001 // Handle formatting requests to the language server.
13002 cx.lsp
13003 .set_request_handler::<lsp::request::Formatting, _, _>({
13004 let buffer_changes = buffer_changes.clone();
13005 move |_, _| {
13006 let buffer_changes = buffer_changes.clone();
13007 // Insert blank lines between each line of the buffer.
13008 async move {
13009 // When formatting is requested, trailing whitespace has already been stripped,
13010 // and the trailing newline has already been added.
13011 assert_eq!(
13012 &buffer_changes.lock()[1..],
13013 &[
13014 (
13015 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13016 "".into()
13017 ),
13018 (
13019 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13020 "".into()
13021 ),
13022 (
13023 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13024 "\n".into()
13025 ),
13026 ]
13027 );
13028
13029 Ok(Some(vec![
13030 lsp::TextEdit {
13031 range: lsp::Range::new(
13032 lsp::Position::new(1, 0),
13033 lsp::Position::new(1, 0),
13034 ),
13035 new_text: "\n".into(),
13036 },
13037 lsp::TextEdit {
13038 range: lsp::Range::new(
13039 lsp::Position::new(2, 0),
13040 lsp::Position::new(2, 0),
13041 ),
13042 new_text: "\n".into(),
13043 },
13044 ]))
13045 }
13046 }
13047 });
13048
13049 // Set up a buffer white some trailing whitespace and no trailing newline.
13050 cx.set_state(
13051 &[
13052 "one ", //
13053 "twoˇ", //
13054 "three ", //
13055 "four", //
13056 ]
13057 .join("\n"),
13058 );
13059 cx.run_until_parked();
13060
13061 // Submit a format request.
13062 let format = cx
13063 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13064 .unwrap();
13065
13066 cx.run_until_parked();
13067 // After formatting the buffer, the trailing whitespace is stripped,
13068 // a newline is appended, and the edits provided by the language server
13069 // have been applied.
13070 format.await.unwrap();
13071
13072 cx.assert_editor_state(
13073 &[
13074 "one", //
13075 "", //
13076 "twoˇ", //
13077 "", //
13078 "three", //
13079 "four", //
13080 "", //
13081 ]
13082 .join("\n"),
13083 );
13084
13085 // Undoing the formatting undoes the trailing whitespace removal, the
13086 // trailing newline, and the LSP edits.
13087 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13088 cx.assert_editor_state(
13089 &[
13090 "one ", //
13091 "twoˇ", //
13092 "three ", //
13093 "four", //
13094 ]
13095 .join("\n"),
13096 );
13097}
13098
13099#[gpui::test]
13100async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13101 cx: &mut TestAppContext,
13102) {
13103 init_test(cx, |_| {});
13104
13105 cx.update(|cx| {
13106 cx.update_global::<SettingsStore, _>(|settings, cx| {
13107 settings.update_user_settings(cx, |settings| {
13108 settings.editor.auto_signature_help = Some(true);
13109 });
13110 });
13111 });
13112
13113 let mut cx = EditorLspTestContext::new_rust(
13114 lsp::ServerCapabilities {
13115 signature_help_provider: Some(lsp::SignatureHelpOptions {
13116 ..Default::default()
13117 }),
13118 ..Default::default()
13119 },
13120 cx,
13121 )
13122 .await;
13123
13124 let language = Language::new(
13125 LanguageConfig {
13126 name: "Rust".into(),
13127 brackets: BracketPairConfig {
13128 pairs: vec![
13129 BracketPair {
13130 start: "{".to_string(),
13131 end: "}".to_string(),
13132 close: true,
13133 surround: true,
13134 newline: true,
13135 },
13136 BracketPair {
13137 start: "(".to_string(),
13138 end: ")".to_string(),
13139 close: true,
13140 surround: true,
13141 newline: true,
13142 },
13143 BracketPair {
13144 start: "/*".to_string(),
13145 end: " */".to_string(),
13146 close: true,
13147 surround: true,
13148 newline: true,
13149 },
13150 BracketPair {
13151 start: "[".to_string(),
13152 end: "]".to_string(),
13153 close: false,
13154 surround: false,
13155 newline: true,
13156 },
13157 BracketPair {
13158 start: "\"".to_string(),
13159 end: "\"".to_string(),
13160 close: true,
13161 surround: true,
13162 newline: false,
13163 },
13164 BracketPair {
13165 start: "<".to_string(),
13166 end: ">".to_string(),
13167 close: false,
13168 surround: true,
13169 newline: true,
13170 },
13171 ],
13172 ..Default::default()
13173 },
13174 autoclose_before: "})]".to_string(),
13175 ..Default::default()
13176 },
13177 Some(tree_sitter_rust::LANGUAGE.into()),
13178 );
13179 let language = Arc::new(language);
13180
13181 cx.language_registry().add(language.clone());
13182 cx.update_buffer(|buffer, cx| {
13183 buffer.set_language_immediate(Some(language), cx);
13184 });
13185
13186 cx.set_state(
13187 &r#"
13188 fn main() {
13189 sampleˇ
13190 }
13191 "#
13192 .unindent(),
13193 );
13194
13195 cx.update_editor(|editor, window, cx| {
13196 editor.handle_input("(", window, cx);
13197 });
13198 cx.assert_editor_state(
13199 &"
13200 fn main() {
13201 sample(ˇ)
13202 }
13203 "
13204 .unindent(),
13205 );
13206
13207 let mocked_response = lsp::SignatureHelp {
13208 signatures: vec![lsp::SignatureInformation {
13209 label: "fn sample(param1: u8, param2: u8)".to_string(),
13210 documentation: None,
13211 parameters: Some(vec![
13212 lsp::ParameterInformation {
13213 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13214 documentation: None,
13215 },
13216 lsp::ParameterInformation {
13217 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13218 documentation: None,
13219 },
13220 ]),
13221 active_parameter: None,
13222 }],
13223 active_signature: Some(0),
13224 active_parameter: Some(0),
13225 };
13226 handle_signature_help_request(&mut cx, mocked_response).await;
13227
13228 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13229 .await;
13230
13231 cx.editor(|editor, _, _| {
13232 let signature_help_state = editor.signature_help_state.popover().cloned();
13233 let signature = signature_help_state.unwrap();
13234 assert_eq!(
13235 signature.signatures[signature.current_signature].label,
13236 "fn sample(param1: u8, param2: u8)"
13237 );
13238 });
13239}
13240
13241#[gpui::test]
13242async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13243 init_test(cx, |_| {});
13244
13245 cx.update(|cx| {
13246 cx.update_global::<SettingsStore, _>(|settings, cx| {
13247 settings.update_user_settings(cx, |settings| {
13248 settings.editor.auto_signature_help = Some(false);
13249 settings.editor.show_signature_help_after_edits = Some(false);
13250 });
13251 });
13252 });
13253
13254 let mut cx = EditorLspTestContext::new_rust(
13255 lsp::ServerCapabilities {
13256 signature_help_provider: Some(lsp::SignatureHelpOptions {
13257 ..Default::default()
13258 }),
13259 ..Default::default()
13260 },
13261 cx,
13262 )
13263 .await;
13264
13265 let language = Language::new(
13266 LanguageConfig {
13267 name: "Rust".into(),
13268 brackets: BracketPairConfig {
13269 pairs: vec![
13270 BracketPair {
13271 start: "{".to_string(),
13272 end: "}".to_string(),
13273 close: true,
13274 surround: true,
13275 newline: true,
13276 },
13277 BracketPair {
13278 start: "(".to_string(),
13279 end: ")".to_string(),
13280 close: true,
13281 surround: true,
13282 newline: true,
13283 },
13284 BracketPair {
13285 start: "/*".to_string(),
13286 end: " */".to_string(),
13287 close: true,
13288 surround: true,
13289 newline: true,
13290 },
13291 BracketPair {
13292 start: "[".to_string(),
13293 end: "]".to_string(),
13294 close: false,
13295 surround: false,
13296 newline: true,
13297 },
13298 BracketPair {
13299 start: "\"".to_string(),
13300 end: "\"".to_string(),
13301 close: true,
13302 surround: true,
13303 newline: false,
13304 },
13305 BracketPair {
13306 start: "<".to_string(),
13307 end: ">".to_string(),
13308 close: false,
13309 surround: true,
13310 newline: true,
13311 },
13312 ],
13313 ..Default::default()
13314 },
13315 autoclose_before: "})]".to_string(),
13316 ..Default::default()
13317 },
13318 Some(tree_sitter_rust::LANGUAGE.into()),
13319 );
13320 let language = Arc::new(language);
13321
13322 cx.language_registry().add(language.clone());
13323 cx.update_buffer(|buffer, cx| {
13324 buffer.set_language_immediate(Some(language), cx);
13325 });
13326
13327 // Ensure that signature_help is not called when no signature help is enabled.
13328 cx.set_state(
13329 &r#"
13330 fn main() {
13331 sampleˇ
13332 }
13333 "#
13334 .unindent(),
13335 );
13336 cx.update_editor(|editor, window, cx| {
13337 editor.handle_input("(", window, cx);
13338 });
13339 cx.assert_editor_state(
13340 &"
13341 fn main() {
13342 sample(ˇ)
13343 }
13344 "
13345 .unindent(),
13346 );
13347 cx.editor(|editor, _, _| {
13348 assert!(editor.signature_help_state.task().is_none());
13349 });
13350
13351 let mocked_response = lsp::SignatureHelp {
13352 signatures: vec![lsp::SignatureInformation {
13353 label: "fn sample(param1: u8, param2: u8)".to_string(),
13354 documentation: None,
13355 parameters: Some(vec![
13356 lsp::ParameterInformation {
13357 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13358 documentation: None,
13359 },
13360 lsp::ParameterInformation {
13361 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13362 documentation: None,
13363 },
13364 ]),
13365 active_parameter: None,
13366 }],
13367 active_signature: Some(0),
13368 active_parameter: Some(0),
13369 };
13370
13371 // Ensure that signature_help is called when enabled afte edits
13372 cx.update(|_, cx| {
13373 cx.update_global::<SettingsStore, _>(|settings, cx| {
13374 settings.update_user_settings(cx, |settings| {
13375 settings.editor.auto_signature_help = Some(false);
13376 settings.editor.show_signature_help_after_edits = Some(true);
13377 });
13378 });
13379 });
13380 cx.set_state(
13381 &r#"
13382 fn main() {
13383 sampleˇ
13384 }
13385 "#
13386 .unindent(),
13387 );
13388 cx.update_editor(|editor, window, cx| {
13389 editor.handle_input("(", window, cx);
13390 });
13391 cx.assert_editor_state(
13392 &"
13393 fn main() {
13394 sample(ˇ)
13395 }
13396 "
13397 .unindent(),
13398 );
13399 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13400 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13401 .await;
13402 cx.update_editor(|editor, _, _| {
13403 let signature_help_state = editor.signature_help_state.popover().cloned();
13404 assert!(signature_help_state.is_some());
13405 let signature = signature_help_state.unwrap();
13406 assert_eq!(
13407 signature.signatures[signature.current_signature].label,
13408 "fn sample(param1: u8, param2: u8)"
13409 );
13410 editor.signature_help_state = SignatureHelpState::default();
13411 });
13412
13413 // Ensure that signature_help is called when auto signature help override is enabled
13414 cx.update(|_, cx| {
13415 cx.update_global::<SettingsStore, _>(|settings, cx| {
13416 settings.update_user_settings(cx, |settings| {
13417 settings.editor.auto_signature_help = Some(true);
13418 settings.editor.show_signature_help_after_edits = Some(false);
13419 });
13420 });
13421 });
13422 cx.set_state(
13423 &r#"
13424 fn main() {
13425 sampleˇ
13426 }
13427 "#
13428 .unindent(),
13429 );
13430 cx.update_editor(|editor, window, cx| {
13431 editor.handle_input("(", window, cx);
13432 });
13433 cx.assert_editor_state(
13434 &"
13435 fn main() {
13436 sample(ˇ)
13437 }
13438 "
13439 .unindent(),
13440 );
13441 handle_signature_help_request(&mut cx, mocked_response).await;
13442 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13443 .await;
13444 cx.editor(|editor, _, _| {
13445 let signature_help_state = editor.signature_help_state.popover().cloned();
13446 assert!(signature_help_state.is_some());
13447 let signature = signature_help_state.unwrap();
13448 assert_eq!(
13449 signature.signatures[signature.current_signature].label,
13450 "fn sample(param1: u8, param2: u8)"
13451 );
13452 });
13453}
13454
13455#[gpui::test]
13456async fn test_signature_help(cx: &mut TestAppContext) {
13457 init_test(cx, |_| {});
13458 cx.update(|cx| {
13459 cx.update_global::<SettingsStore, _>(|settings, cx| {
13460 settings.update_user_settings(cx, |settings| {
13461 settings.editor.auto_signature_help = Some(true);
13462 });
13463 });
13464 });
13465
13466 let mut cx = EditorLspTestContext::new_rust(
13467 lsp::ServerCapabilities {
13468 signature_help_provider: Some(lsp::SignatureHelpOptions {
13469 ..Default::default()
13470 }),
13471 ..Default::default()
13472 },
13473 cx,
13474 )
13475 .await;
13476
13477 // A test that directly calls `show_signature_help`
13478 cx.update_editor(|editor, window, cx| {
13479 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13480 });
13481
13482 let mocked_response = lsp::SignatureHelp {
13483 signatures: vec![lsp::SignatureInformation {
13484 label: "fn sample(param1: u8, param2: u8)".to_string(),
13485 documentation: None,
13486 parameters: Some(vec![
13487 lsp::ParameterInformation {
13488 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13489 documentation: None,
13490 },
13491 lsp::ParameterInformation {
13492 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13493 documentation: None,
13494 },
13495 ]),
13496 active_parameter: None,
13497 }],
13498 active_signature: Some(0),
13499 active_parameter: Some(0),
13500 };
13501 handle_signature_help_request(&mut cx, mocked_response).await;
13502
13503 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13504 .await;
13505
13506 cx.editor(|editor, _, _| {
13507 let signature_help_state = editor.signature_help_state.popover().cloned();
13508 assert!(signature_help_state.is_some());
13509 let signature = signature_help_state.unwrap();
13510 assert_eq!(
13511 signature.signatures[signature.current_signature].label,
13512 "fn sample(param1: u8, param2: u8)"
13513 );
13514 });
13515
13516 // When exiting outside from inside the brackets, `signature_help` is closed.
13517 cx.set_state(indoc! {"
13518 fn main() {
13519 sample(ˇ);
13520 }
13521
13522 fn sample(param1: u8, param2: u8) {}
13523 "});
13524
13525 cx.update_editor(|editor, window, cx| {
13526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13527 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13528 });
13529 });
13530
13531 let mocked_response = lsp::SignatureHelp {
13532 signatures: Vec::new(),
13533 active_signature: None,
13534 active_parameter: None,
13535 };
13536 handle_signature_help_request(&mut cx, mocked_response).await;
13537
13538 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13539 .await;
13540
13541 cx.editor(|editor, _, _| {
13542 assert!(!editor.signature_help_state.is_shown());
13543 });
13544
13545 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13546 cx.set_state(indoc! {"
13547 fn main() {
13548 sample(ˇ);
13549 }
13550
13551 fn sample(param1: u8, param2: u8) {}
13552 "});
13553
13554 let mocked_response = lsp::SignatureHelp {
13555 signatures: vec![lsp::SignatureInformation {
13556 label: "fn sample(param1: u8, param2: u8)".to_string(),
13557 documentation: None,
13558 parameters: Some(vec![
13559 lsp::ParameterInformation {
13560 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13561 documentation: None,
13562 },
13563 lsp::ParameterInformation {
13564 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13565 documentation: None,
13566 },
13567 ]),
13568 active_parameter: None,
13569 }],
13570 active_signature: Some(0),
13571 active_parameter: Some(0),
13572 };
13573 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13574 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13575 .await;
13576 cx.editor(|editor, _, _| {
13577 assert!(editor.signature_help_state.is_shown());
13578 });
13579
13580 // Restore the popover with more parameter input
13581 cx.set_state(indoc! {"
13582 fn main() {
13583 sample(param1, param2ˇ);
13584 }
13585
13586 fn sample(param1: u8, param2: u8) {}
13587 "});
13588
13589 let mocked_response = lsp::SignatureHelp {
13590 signatures: vec![lsp::SignatureInformation {
13591 label: "fn sample(param1: u8, param2: u8)".to_string(),
13592 documentation: None,
13593 parameters: Some(vec![
13594 lsp::ParameterInformation {
13595 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13596 documentation: None,
13597 },
13598 lsp::ParameterInformation {
13599 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13600 documentation: None,
13601 },
13602 ]),
13603 active_parameter: None,
13604 }],
13605 active_signature: Some(0),
13606 active_parameter: Some(1),
13607 };
13608 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13609 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13610 .await;
13611
13612 // When selecting a range, the popover is gone.
13613 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13614 cx.update_editor(|editor, window, cx| {
13615 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13616 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13617 })
13618 });
13619 cx.assert_editor_state(indoc! {"
13620 fn main() {
13621 sample(param1, «ˇparam2»);
13622 }
13623
13624 fn sample(param1: u8, param2: u8) {}
13625 "});
13626 cx.editor(|editor, _, _| {
13627 assert!(!editor.signature_help_state.is_shown());
13628 });
13629
13630 // When unselecting again, the popover is back if within the brackets.
13631 cx.update_editor(|editor, window, cx| {
13632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13633 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13634 })
13635 });
13636 cx.assert_editor_state(indoc! {"
13637 fn main() {
13638 sample(param1, ˇparam2);
13639 }
13640
13641 fn sample(param1: u8, param2: u8) {}
13642 "});
13643 handle_signature_help_request(&mut cx, mocked_response).await;
13644 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13645 .await;
13646 cx.editor(|editor, _, _| {
13647 assert!(editor.signature_help_state.is_shown());
13648 });
13649
13650 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13651 cx.update_editor(|editor, window, cx| {
13652 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13653 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13654 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13655 })
13656 });
13657 cx.assert_editor_state(indoc! {"
13658 fn main() {
13659 sample(param1, ˇparam2);
13660 }
13661
13662 fn sample(param1: u8, param2: u8) {}
13663 "});
13664
13665 let mocked_response = lsp::SignatureHelp {
13666 signatures: vec![lsp::SignatureInformation {
13667 label: "fn sample(param1: u8, param2: u8)".to_string(),
13668 documentation: None,
13669 parameters: Some(vec![
13670 lsp::ParameterInformation {
13671 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13672 documentation: None,
13673 },
13674 lsp::ParameterInformation {
13675 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13676 documentation: None,
13677 },
13678 ]),
13679 active_parameter: None,
13680 }],
13681 active_signature: Some(0),
13682 active_parameter: Some(1),
13683 };
13684 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13685 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13686 .await;
13687 cx.update_editor(|editor, _, cx| {
13688 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13689 });
13690 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13691 .await;
13692 cx.update_editor(|editor, window, cx| {
13693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13694 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13695 })
13696 });
13697 cx.assert_editor_state(indoc! {"
13698 fn main() {
13699 sample(param1, «ˇparam2»);
13700 }
13701
13702 fn sample(param1: u8, param2: u8) {}
13703 "});
13704 cx.update_editor(|editor, window, cx| {
13705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13706 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13707 })
13708 });
13709 cx.assert_editor_state(indoc! {"
13710 fn main() {
13711 sample(param1, ˇparam2);
13712 }
13713
13714 fn sample(param1: u8, param2: u8) {}
13715 "});
13716 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13717 .await;
13718}
13719
13720#[gpui::test]
13721async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13722 init_test(cx, |_| {});
13723
13724 let mut cx = EditorLspTestContext::new_rust(
13725 lsp::ServerCapabilities {
13726 signature_help_provider: Some(lsp::SignatureHelpOptions {
13727 ..Default::default()
13728 }),
13729 ..Default::default()
13730 },
13731 cx,
13732 )
13733 .await;
13734
13735 cx.set_state(indoc! {"
13736 fn main() {
13737 overloadedˇ
13738 }
13739 "});
13740
13741 cx.update_editor(|editor, window, cx| {
13742 editor.handle_input("(", window, cx);
13743 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13744 });
13745
13746 // Mock response with 3 signatures
13747 let mocked_response = lsp::SignatureHelp {
13748 signatures: vec![
13749 lsp::SignatureInformation {
13750 label: "fn overloaded(x: i32)".to_string(),
13751 documentation: None,
13752 parameters: Some(vec![lsp::ParameterInformation {
13753 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13754 documentation: None,
13755 }]),
13756 active_parameter: None,
13757 },
13758 lsp::SignatureInformation {
13759 label: "fn overloaded(x: i32, y: i32)".to_string(),
13760 documentation: None,
13761 parameters: Some(vec![
13762 lsp::ParameterInformation {
13763 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13764 documentation: None,
13765 },
13766 lsp::ParameterInformation {
13767 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13768 documentation: None,
13769 },
13770 ]),
13771 active_parameter: None,
13772 },
13773 lsp::SignatureInformation {
13774 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13775 documentation: None,
13776 parameters: Some(vec![
13777 lsp::ParameterInformation {
13778 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13779 documentation: None,
13780 },
13781 lsp::ParameterInformation {
13782 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13783 documentation: None,
13784 },
13785 lsp::ParameterInformation {
13786 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13787 documentation: None,
13788 },
13789 ]),
13790 active_parameter: None,
13791 },
13792 ],
13793 active_signature: Some(1),
13794 active_parameter: Some(0),
13795 };
13796 handle_signature_help_request(&mut cx, mocked_response).await;
13797
13798 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13799 .await;
13800
13801 // Verify we have multiple signatures and the right one is selected
13802 cx.editor(|editor, _, _| {
13803 let popover = editor.signature_help_state.popover().cloned().unwrap();
13804 assert_eq!(popover.signatures.len(), 3);
13805 // active_signature was 1, so that should be the current
13806 assert_eq!(popover.current_signature, 1);
13807 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13808 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13809 assert_eq!(
13810 popover.signatures[2].label,
13811 "fn overloaded(x: i32, y: i32, z: i32)"
13812 );
13813 });
13814
13815 // Test navigation functionality
13816 cx.update_editor(|editor, window, cx| {
13817 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13818 });
13819
13820 cx.editor(|editor, _, _| {
13821 let popover = editor.signature_help_state.popover().cloned().unwrap();
13822 assert_eq!(popover.current_signature, 2);
13823 });
13824
13825 // Test wrap around
13826 cx.update_editor(|editor, window, cx| {
13827 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13828 });
13829
13830 cx.editor(|editor, _, _| {
13831 let popover = editor.signature_help_state.popover().cloned().unwrap();
13832 assert_eq!(popover.current_signature, 0);
13833 });
13834
13835 // Test previous navigation
13836 cx.update_editor(|editor, window, cx| {
13837 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13838 });
13839
13840 cx.editor(|editor, _, _| {
13841 let popover = editor.signature_help_state.popover().cloned().unwrap();
13842 assert_eq!(popover.current_signature, 2);
13843 });
13844}
13845
13846#[gpui::test]
13847async fn test_completion_mode(cx: &mut TestAppContext) {
13848 init_test(cx, |_| {});
13849 let mut cx = EditorLspTestContext::new_rust(
13850 lsp::ServerCapabilities {
13851 completion_provider: Some(lsp::CompletionOptions {
13852 resolve_provider: Some(true),
13853 ..Default::default()
13854 }),
13855 ..Default::default()
13856 },
13857 cx,
13858 )
13859 .await;
13860
13861 struct Run {
13862 run_description: &'static str,
13863 initial_state: String,
13864 buffer_marked_text: String,
13865 completion_label: &'static str,
13866 completion_text: &'static str,
13867 expected_with_insert_mode: String,
13868 expected_with_replace_mode: String,
13869 expected_with_replace_subsequence_mode: String,
13870 expected_with_replace_suffix_mode: String,
13871 }
13872
13873 let runs = [
13874 Run {
13875 run_description: "Start of word matches completion text",
13876 initial_state: "before ediˇ after".into(),
13877 buffer_marked_text: "before <edi|> after".into(),
13878 completion_label: "editor",
13879 completion_text: "editor",
13880 expected_with_insert_mode: "before editorˇ after".into(),
13881 expected_with_replace_mode: "before editorˇ after".into(),
13882 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13883 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13884 },
13885 Run {
13886 run_description: "Accept same text at the middle of the word",
13887 initial_state: "before ediˇtor after".into(),
13888 buffer_marked_text: "before <edi|tor> after".into(),
13889 completion_label: "editor",
13890 completion_text: "editor",
13891 expected_with_insert_mode: "before editorˇtor after".into(),
13892 expected_with_replace_mode: "before editorˇ after".into(),
13893 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13894 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13895 },
13896 Run {
13897 run_description: "End of word matches completion text -- cursor at end",
13898 initial_state: "before torˇ after".into(),
13899 buffer_marked_text: "before <tor|> after".into(),
13900 completion_label: "editor",
13901 completion_text: "editor",
13902 expected_with_insert_mode: "before editorˇ after".into(),
13903 expected_with_replace_mode: "before editorˇ after".into(),
13904 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13905 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13906 },
13907 Run {
13908 run_description: "End of word matches completion text -- cursor at start",
13909 initial_state: "before ˇtor after".into(),
13910 buffer_marked_text: "before <|tor> after".into(),
13911 completion_label: "editor",
13912 completion_text: "editor",
13913 expected_with_insert_mode: "before editorˇtor after".into(),
13914 expected_with_replace_mode: "before editorˇ after".into(),
13915 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13916 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13917 },
13918 Run {
13919 run_description: "Prepend text containing whitespace",
13920 initial_state: "pˇfield: bool".into(),
13921 buffer_marked_text: "<p|field>: bool".into(),
13922 completion_label: "pub ",
13923 completion_text: "pub ",
13924 expected_with_insert_mode: "pub ˇfield: bool".into(),
13925 expected_with_replace_mode: "pub ˇ: bool".into(),
13926 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13927 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13928 },
13929 Run {
13930 run_description: "Add element to start of list",
13931 initial_state: "[element_ˇelement_2]".into(),
13932 buffer_marked_text: "[<element_|element_2>]".into(),
13933 completion_label: "element_1",
13934 completion_text: "element_1",
13935 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13936 expected_with_replace_mode: "[element_1ˇ]".into(),
13937 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13938 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13939 },
13940 Run {
13941 run_description: "Add element to start of list -- first and second elements are equal",
13942 initial_state: "[elˇelement]".into(),
13943 buffer_marked_text: "[<el|element>]".into(),
13944 completion_label: "element",
13945 completion_text: "element",
13946 expected_with_insert_mode: "[elementˇelement]".into(),
13947 expected_with_replace_mode: "[elementˇ]".into(),
13948 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13949 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13950 },
13951 Run {
13952 run_description: "Ends with matching suffix",
13953 initial_state: "SubˇError".into(),
13954 buffer_marked_text: "<Sub|Error>".into(),
13955 completion_label: "SubscriptionError",
13956 completion_text: "SubscriptionError",
13957 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13958 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13959 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13960 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13961 },
13962 Run {
13963 run_description: "Suffix is a subsequence -- contiguous",
13964 initial_state: "SubˇErr".into(),
13965 buffer_marked_text: "<Sub|Err>".into(),
13966 completion_label: "SubscriptionError",
13967 completion_text: "SubscriptionError",
13968 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13969 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13970 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13971 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13972 },
13973 Run {
13974 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13975 initial_state: "Suˇscrirr".into(),
13976 buffer_marked_text: "<Su|scrirr>".into(),
13977 completion_label: "SubscriptionError",
13978 completion_text: "SubscriptionError",
13979 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13980 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13981 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13982 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13983 },
13984 Run {
13985 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13986 initial_state: "foo(indˇix)".into(),
13987 buffer_marked_text: "foo(<ind|ix>)".into(),
13988 completion_label: "node_index",
13989 completion_text: "node_index",
13990 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13991 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13992 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13993 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13994 },
13995 Run {
13996 run_description: "Replace range ends before cursor - should extend to cursor",
13997 initial_state: "before editˇo after".into(),
13998 buffer_marked_text: "before <{ed}>it|o after".into(),
13999 completion_label: "editor",
14000 completion_text: "editor",
14001 expected_with_insert_mode: "before editorˇo after".into(),
14002 expected_with_replace_mode: "before editorˇo after".into(),
14003 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14004 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14005 },
14006 Run {
14007 run_description: "Uses label for suffix matching",
14008 initial_state: "before ediˇtor after".into(),
14009 buffer_marked_text: "before <edi|tor> after".into(),
14010 completion_label: "editor",
14011 completion_text: "editor()",
14012 expected_with_insert_mode: "before editor()ˇtor after".into(),
14013 expected_with_replace_mode: "before editor()ˇ after".into(),
14014 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14015 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14016 },
14017 Run {
14018 run_description: "Case insensitive subsequence and suffix matching",
14019 initial_state: "before EDiˇtoR after".into(),
14020 buffer_marked_text: "before <EDi|toR> after".into(),
14021 completion_label: "editor",
14022 completion_text: "editor",
14023 expected_with_insert_mode: "before editorˇtoR after".into(),
14024 expected_with_replace_mode: "before editorˇ after".into(),
14025 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14026 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14027 },
14028 ];
14029
14030 for run in runs {
14031 let run_variations = [
14032 (LspInsertMode::Insert, run.expected_with_insert_mode),
14033 (LspInsertMode::Replace, run.expected_with_replace_mode),
14034 (
14035 LspInsertMode::ReplaceSubsequence,
14036 run.expected_with_replace_subsequence_mode,
14037 ),
14038 (
14039 LspInsertMode::ReplaceSuffix,
14040 run.expected_with_replace_suffix_mode,
14041 ),
14042 ];
14043
14044 for (lsp_insert_mode, expected_text) in run_variations {
14045 eprintln!(
14046 "run = {:?}, mode = {lsp_insert_mode:.?}",
14047 run.run_description,
14048 );
14049
14050 update_test_language_settings(&mut cx, |settings| {
14051 settings.defaults.completions = Some(CompletionSettingsContent {
14052 lsp_insert_mode: Some(lsp_insert_mode),
14053 words: Some(WordsCompletionMode::Disabled),
14054 words_min_length: Some(0),
14055 ..Default::default()
14056 });
14057 });
14058
14059 cx.set_state(&run.initial_state);
14060 cx.update_editor(|editor, window, cx| {
14061 editor.show_completions(&ShowCompletions, window, cx);
14062 });
14063
14064 let counter = Arc::new(AtomicUsize::new(0));
14065 handle_completion_request_with_insert_and_replace(
14066 &mut cx,
14067 &run.buffer_marked_text,
14068 vec![(run.completion_label, run.completion_text)],
14069 counter.clone(),
14070 )
14071 .await;
14072 cx.condition(|editor, _| editor.context_menu_visible())
14073 .await;
14074 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14075
14076 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14077 editor
14078 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14079 .unwrap()
14080 });
14081 cx.assert_editor_state(&expected_text);
14082 handle_resolve_completion_request(&mut cx, None).await;
14083 apply_additional_edits.await.unwrap();
14084 }
14085 }
14086}
14087
14088#[gpui::test]
14089async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14090 init_test(cx, |_| {});
14091 let mut cx = EditorLspTestContext::new_rust(
14092 lsp::ServerCapabilities {
14093 completion_provider: Some(lsp::CompletionOptions {
14094 resolve_provider: Some(true),
14095 ..Default::default()
14096 }),
14097 ..Default::default()
14098 },
14099 cx,
14100 )
14101 .await;
14102
14103 let initial_state = "SubˇError";
14104 let buffer_marked_text = "<Sub|Error>";
14105 let completion_text = "SubscriptionError";
14106 let expected_with_insert_mode = "SubscriptionErrorˇError";
14107 let expected_with_replace_mode = "SubscriptionErrorˇ";
14108
14109 update_test_language_settings(&mut cx, |settings| {
14110 settings.defaults.completions = Some(CompletionSettingsContent {
14111 words: Some(WordsCompletionMode::Disabled),
14112 words_min_length: Some(0),
14113 // set the opposite here to ensure that the action is overriding the default behavior
14114 lsp_insert_mode: Some(LspInsertMode::Insert),
14115 ..Default::default()
14116 });
14117 });
14118
14119 cx.set_state(initial_state);
14120 cx.update_editor(|editor, window, cx| {
14121 editor.show_completions(&ShowCompletions, window, cx);
14122 });
14123
14124 let counter = Arc::new(AtomicUsize::new(0));
14125 handle_completion_request_with_insert_and_replace(
14126 &mut cx,
14127 buffer_marked_text,
14128 vec![(completion_text, completion_text)],
14129 counter.clone(),
14130 )
14131 .await;
14132 cx.condition(|editor, _| editor.context_menu_visible())
14133 .await;
14134 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14135
14136 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14137 editor
14138 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14139 .unwrap()
14140 });
14141 cx.assert_editor_state(expected_with_replace_mode);
14142 handle_resolve_completion_request(&mut cx, None).await;
14143 apply_additional_edits.await.unwrap();
14144
14145 update_test_language_settings(&mut cx, |settings| {
14146 settings.defaults.completions = Some(CompletionSettingsContent {
14147 words: Some(WordsCompletionMode::Disabled),
14148 words_min_length: Some(0),
14149 // set the opposite here to ensure that the action is overriding the default behavior
14150 lsp_insert_mode: Some(LspInsertMode::Replace),
14151 ..Default::default()
14152 });
14153 });
14154
14155 cx.set_state(initial_state);
14156 cx.update_editor(|editor, window, cx| {
14157 editor.show_completions(&ShowCompletions, window, cx);
14158 });
14159 handle_completion_request_with_insert_and_replace(
14160 &mut cx,
14161 buffer_marked_text,
14162 vec![(completion_text, completion_text)],
14163 counter.clone(),
14164 )
14165 .await;
14166 cx.condition(|editor, _| editor.context_menu_visible())
14167 .await;
14168 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14169
14170 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14171 editor
14172 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14173 .unwrap()
14174 });
14175 cx.assert_editor_state(expected_with_insert_mode);
14176 handle_resolve_completion_request(&mut cx, None).await;
14177 apply_additional_edits.await.unwrap();
14178}
14179
14180#[gpui::test]
14181async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14182 init_test(cx, |_| {});
14183 let mut cx = EditorLspTestContext::new_rust(
14184 lsp::ServerCapabilities {
14185 completion_provider: Some(lsp::CompletionOptions {
14186 resolve_provider: Some(true),
14187 ..Default::default()
14188 }),
14189 ..Default::default()
14190 },
14191 cx,
14192 )
14193 .await;
14194
14195 // scenario: surrounding text matches completion text
14196 let completion_text = "to_offset";
14197 let initial_state = indoc! {"
14198 1. buf.to_offˇsuffix
14199 2. buf.to_offˇsuf
14200 3. buf.to_offˇfix
14201 4. buf.to_offˇ
14202 5. into_offˇensive
14203 6. ˇsuffix
14204 7. let ˇ //
14205 8. aaˇzz
14206 9. buf.to_off«zzzzzˇ»suffix
14207 10. buf.«ˇzzzzz»suffix
14208 11. to_off«ˇzzzzz»
14209
14210 buf.to_offˇsuffix // newest cursor
14211 "};
14212 let completion_marked_buffer = indoc! {"
14213 1. buf.to_offsuffix
14214 2. buf.to_offsuf
14215 3. buf.to_offfix
14216 4. buf.to_off
14217 5. into_offensive
14218 6. suffix
14219 7. let //
14220 8. aazz
14221 9. buf.to_offzzzzzsuffix
14222 10. buf.zzzzzsuffix
14223 11. to_offzzzzz
14224
14225 buf.<to_off|suffix> // newest cursor
14226 "};
14227 let expected = indoc! {"
14228 1. buf.to_offsetˇ
14229 2. buf.to_offsetˇsuf
14230 3. buf.to_offsetˇfix
14231 4. buf.to_offsetˇ
14232 5. into_offsetˇensive
14233 6. to_offsetˇsuffix
14234 7. let to_offsetˇ //
14235 8. aato_offsetˇzz
14236 9. buf.to_offsetˇ
14237 10. buf.to_offsetˇsuffix
14238 11. to_offsetˇ
14239
14240 buf.to_offsetˇ // newest cursor
14241 "};
14242 cx.set_state(initial_state);
14243 cx.update_editor(|editor, window, cx| {
14244 editor.show_completions(&ShowCompletions, window, cx);
14245 });
14246 handle_completion_request_with_insert_and_replace(
14247 &mut cx,
14248 completion_marked_buffer,
14249 vec![(completion_text, completion_text)],
14250 Arc::new(AtomicUsize::new(0)),
14251 )
14252 .await;
14253 cx.condition(|editor, _| editor.context_menu_visible())
14254 .await;
14255 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14256 editor
14257 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14258 .unwrap()
14259 });
14260 cx.assert_editor_state(expected);
14261 handle_resolve_completion_request(&mut cx, None).await;
14262 apply_additional_edits.await.unwrap();
14263
14264 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14265 let completion_text = "foo_and_bar";
14266 let initial_state = indoc! {"
14267 1. ooanbˇ
14268 2. zooanbˇ
14269 3. ooanbˇz
14270 4. zooanbˇz
14271 5. ooanˇ
14272 6. oanbˇ
14273
14274 ooanbˇ
14275 "};
14276 let completion_marked_buffer = indoc! {"
14277 1. ooanb
14278 2. zooanb
14279 3. ooanbz
14280 4. zooanbz
14281 5. ooan
14282 6. oanb
14283
14284 <ooanb|>
14285 "};
14286 let expected = indoc! {"
14287 1. foo_and_barˇ
14288 2. zfoo_and_barˇ
14289 3. foo_and_barˇz
14290 4. zfoo_and_barˇz
14291 5. ooanfoo_and_barˇ
14292 6. oanbfoo_and_barˇ
14293
14294 foo_and_barˇ
14295 "};
14296 cx.set_state(initial_state);
14297 cx.update_editor(|editor, window, cx| {
14298 editor.show_completions(&ShowCompletions, window, cx);
14299 });
14300 handle_completion_request_with_insert_and_replace(
14301 &mut cx,
14302 completion_marked_buffer,
14303 vec![(completion_text, completion_text)],
14304 Arc::new(AtomicUsize::new(0)),
14305 )
14306 .await;
14307 cx.condition(|editor, _| editor.context_menu_visible())
14308 .await;
14309 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14310 editor
14311 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14312 .unwrap()
14313 });
14314 cx.assert_editor_state(expected);
14315 handle_resolve_completion_request(&mut cx, None).await;
14316 apply_additional_edits.await.unwrap();
14317
14318 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14319 // (expects the same as if it was inserted at the end)
14320 let completion_text = "foo_and_bar";
14321 let initial_state = indoc! {"
14322 1. ooˇanb
14323 2. zooˇanb
14324 3. ooˇanbz
14325 4. zooˇanbz
14326
14327 ooˇanb
14328 "};
14329 let completion_marked_buffer = indoc! {"
14330 1. ooanb
14331 2. zooanb
14332 3. ooanbz
14333 4. zooanbz
14334
14335 <oo|anb>
14336 "};
14337 let expected = indoc! {"
14338 1. foo_and_barˇ
14339 2. zfoo_and_barˇ
14340 3. foo_and_barˇz
14341 4. zfoo_and_barˇz
14342
14343 foo_and_barˇ
14344 "};
14345 cx.set_state(initial_state);
14346 cx.update_editor(|editor, window, cx| {
14347 editor.show_completions(&ShowCompletions, window, cx);
14348 });
14349 handle_completion_request_with_insert_and_replace(
14350 &mut cx,
14351 completion_marked_buffer,
14352 vec![(completion_text, completion_text)],
14353 Arc::new(AtomicUsize::new(0)),
14354 )
14355 .await;
14356 cx.condition(|editor, _| editor.context_menu_visible())
14357 .await;
14358 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14359 editor
14360 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14361 .unwrap()
14362 });
14363 cx.assert_editor_state(expected);
14364 handle_resolve_completion_request(&mut cx, None).await;
14365 apply_additional_edits.await.unwrap();
14366}
14367
14368// This used to crash
14369#[gpui::test]
14370async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14371 init_test(cx, |_| {});
14372
14373 let buffer_text = indoc! {"
14374 fn main() {
14375 10.satu;
14376
14377 //
14378 // separate cursors so they open in different excerpts (manually reproducible)
14379 //
14380
14381 10.satu20;
14382 }
14383 "};
14384 let multibuffer_text_with_selections = indoc! {"
14385 fn main() {
14386 10.satuˇ;
14387
14388 //
14389
14390 //
14391
14392 10.satuˇ20;
14393 }
14394 "};
14395 let expected_multibuffer = indoc! {"
14396 fn main() {
14397 10.saturating_sub()ˇ;
14398
14399 //
14400
14401 //
14402
14403 10.saturating_sub()ˇ;
14404 }
14405 "};
14406
14407 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14408 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14409
14410 let fs = FakeFs::new(cx.executor());
14411 fs.insert_tree(
14412 path!("/a"),
14413 json!({
14414 "main.rs": buffer_text,
14415 }),
14416 )
14417 .await;
14418
14419 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14420 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14421 language_registry.add(rust_lang());
14422 let mut fake_servers = language_registry.register_fake_lsp(
14423 "Rust",
14424 FakeLspAdapter {
14425 capabilities: lsp::ServerCapabilities {
14426 completion_provider: Some(lsp::CompletionOptions {
14427 resolve_provider: None,
14428 ..lsp::CompletionOptions::default()
14429 }),
14430 ..lsp::ServerCapabilities::default()
14431 },
14432 ..FakeLspAdapter::default()
14433 },
14434 );
14435 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14436 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14437 let buffer = project
14438 .update(cx, |project, cx| {
14439 project.open_local_buffer(path!("/a/main.rs"), cx)
14440 })
14441 .await
14442 .unwrap();
14443
14444 let multi_buffer = cx.new(|cx| {
14445 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14446 multi_buffer.push_excerpts(
14447 buffer.clone(),
14448 [ExcerptRange::new(0..first_excerpt_end)],
14449 cx,
14450 );
14451 multi_buffer.push_excerpts(
14452 buffer.clone(),
14453 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14454 cx,
14455 );
14456 multi_buffer
14457 });
14458
14459 let editor = workspace
14460 .update(cx, |_, window, cx| {
14461 cx.new(|cx| {
14462 Editor::new(
14463 EditorMode::Full {
14464 scale_ui_elements_with_buffer_font_size: false,
14465 show_active_line_background: false,
14466 sizing_behavior: SizingBehavior::Default,
14467 },
14468 multi_buffer.clone(),
14469 Some(project.clone()),
14470 window,
14471 cx,
14472 )
14473 })
14474 })
14475 .unwrap();
14476
14477 let pane = workspace
14478 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14479 .unwrap();
14480 pane.update_in(cx, |pane, window, cx| {
14481 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14482 });
14483
14484 let fake_server = fake_servers.next().await.unwrap();
14485
14486 editor.update_in(cx, |editor, window, cx| {
14487 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14488 s.select_ranges([
14489 Point::new(1, 11)..Point::new(1, 11),
14490 Point::new(7, 11)..Point::new(7, 11),
14491 ])
14492 });
14493
14494 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14495 });
14496
14497 editor.update_in(cx, |editor, window, cx| {
14498 editor.show_completions(&ShowCompletions, window, cx);
14499 });
14500
14501 fake_server
14502 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14503 let completion_item = lsp::CompletionItem {
14504 label: "saturating_sub()".into(),
14505 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14506 lsp::InsertReplaceEdit {
14507 new_text: "saturating_sub()".to_owned(),
14508 insert: lsp::Range::new(
14509 lsp::Position::new(7, 7),
14510 lsp::Position::new(7, 11),
14511 ),
14512 replace: lsp::Range::new(
14513 lsp::Position::new(7, 7),
14514 lsp::Position::new(7, 13),
14515 ),
14516 },
14517 )),
14518 ..lsp::CompletionItem::default()
14519 };
14520
14521 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14522 })
14523 .next()
14524 .await
14525 .unwrap();
14526
14527 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14528 .await;
14529
14530 editor
14531 .update_in(cx, |editor, window, cx| {
14532 editor
14533 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14534 .unwrap()
14535 })
14536 .await
14537 .unwrap();
14538
14539 editor.update(cx, |editor, cx| {
14540 assert_text_with_selections(editor, expected_multibuffer, cx);
14541 })
14542}
14543
14544#[gpui::test]
14545async fn test_completion(cx: &mut TestAppContext) {
14546 init_test(cx, |_| {});
14547
14548 let mut cx = EditorLspTestContext::new_rust(
14549 lsp::ServerCapabilities {
14550 completion_provider: Some(lsp::CompletionOptions {
14551 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14552 resolve_provider: Some(true),
14553 ..Default::default()
14554 }),
14555 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14556 ..Default::default()
14557 },
14558 cx,
14559 )
14560 .await;
14561 let counter = Arc::new(AtomicUsize::new(0));
14562
14563 cx.set_state(indoc! {"
14564 oneˇ
14565 two
14566 three
14567 "});
14568 cx.simulate_keystroke(".");
14569 handle_completion_request(
14570 indoc! {"
14571 one.|<>
14572 two
14573 three
14574 "},
14575 vec!["first_completion", "second_completion"],
14576 true,
14577 counter.clone(),
14578 &mut cx,
14579 )
14580 .await;
14581 cx.condition(|editor, _| editor.context_menu_visible())
14582 .await;
14583 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14584
14585 let _handler = handle_signature_help_request(
14586 &mut cx,
14587 lsp::SignatureHelp {
14588 signatures: vec![lsp::SignatureInformation {
14589 label: "test signature".to_string(),
14590 documentation: None,
14591 parameters: Some(vec![lsp::ParameterInformation {
14592 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14593 documentation: None,
14594 }]),
14595 active_parameter: None,
14596 }],
14597 active_signature: None,
14598 active_parameter: None,
14599 },
14600 );
14601 cx.update_editor(|editor, window, cx| {
14602 assert!(
14603 !editor.signature_help_state.is_shown(),
14604 "No signature help was called for"
14605 );
14606 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14607 });
14608 cx.run_until_parked();
14609 cx.update_editor(|editor, _, _| {
14610 assert!(
14611 !editor.signature_help_state.is_shown(),
14612 "No signature help should be shown when completions menu is open"
14613 );
14614 });
14615
14616 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14617 editor.context_menu_next(&Default::default(), window, cx);
14618 editor
14619 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14620 .unwrap()
14621 });
14622 cx.assert_editor_state(indoc! {"
14623 one.second_completionˇ
14624 two
14625 three
14626 "});
14627
14628 handle_resolve_completion_request(
14629 &mut cx,
14630 Some(vec![
14631 (
14632 //This overlaps with the primary completion edit which is
14633 //misbehavior from the LSP spec, test that we filter it out
14634 indoc! {"
14635 one.second_ˇcompletion
14636 two
14637 threeˇ
14638 "},
14639 "overlapping additional edit",
14640 ),
14641 (
14642 indoc! {"
14643 one.second_completion
14644 two
14645 threeˇ
14646 "},
14647 "\nadditional edit",
14648 ),
14649 ]),
14650 )
14651 .await;
14652 apply_additional_edits.await.unwrap();
14653 cx.assert_editor_state(indoc! {"
14654 one.second_completionˇ
14655 two
14656 three
14657 additional edit
14658 "});
14659
14660 cx.set_state(indoc! {"
14661 one.second_completion
14662 twoˇ
14663 threeˇ
14664 additional edit
14665 "});
14666 cx.simulate_keystroke(" ");
14667 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14668 cx.simulate_keystroke("s");
14669 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14670
14671 cx.assert_editor_state(indoc! {"
14672 one.second_completion
14673 two sˇ
14674 three sˇ
14675 additional edit
14676 "});
14677 handle_completion_request(
14678 indoc! {"
14679 one.second_completion
14680 two s
14681 three <s|>
14682 additional edit
14683 "},
14684 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14685 true,
14686 counter.clone(),
14687 &mut cx,
14688 )
14689 .await;
14690 cx.condition(|editor, _| editor.context_menu_visible())
14691 .await;
14692 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14693
14694 cx.simulate_keystroke("i");
14695
14696 handle_completion_request(
14697 indoc! {"
14698 one.second_completion
14699 two si
14700 three <si|>
14701 additional edit
14702 "},
14703 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14704 true,
14705 counter.clone(),
14706 &mut cx,
14707 )
14708 .await;
14709 cx.condition(|editor, _| editor.context_menu_visible())
14710 .await;
14711 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14712
14713 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14714 editor
14715 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14716 .unwrap()
14717 });
14718 cx.assert_editor_state(indoc! {"
14719 one.second_completion
14720 two sixth_completionˇ
14721 three sixth_completionˇ
14722 additional edit
14723 "});
14724
14725 apply_additional_edits.await.unwrap();
14726
14727 update_test_language_settings(&mut cx, |settings| {
14728 settings.defaults.show_completions_on_input = Some(false);
14729 });
14730 cx.set_state("editorˇ");
14731 cx.simulate_keystroke(".");
14732 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14733 cx.simulate_keystrokes("c l o");
14734 cx.assert_editor_state("editor.cloˇ");
14735 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14736 cx.update_editor(|editor, window, cx| {
14737 editor.show_completions(&ShowCompletions, window, cx);
14738 });
14739 handle_completion_request(
14740 "editor.<clo|>",
14741 vec!["close", "clobber"],
14742 true,
14743 counter.clone(),
14744 &mut cx,
14745 )
14746 .await;
14747 cx.condition(|editor, _| editor.context_menu_visible())
14748 .await;
14749 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14750
14751 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14752 editor
14753 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14754 .unwrap()
14755 });
14756 cx.assert_editor_state("editor.clobberˇ");
14757 handle_resolve_completion_request(&mut cx, None).await;
14758 apply_additional_edits.await.unwrap();
14759}
14760
14761#[gpui::test]
14762async fn test_completion_reuse(cx: &mut TestAppContext) {
14763 init_test(cx, |_| {});
14764
14765 let mut cx = EditorLspTestContext::new_rust(
14766 lsp::ServerCapabilities {
14767 completion_provider: Some(lsp::CompletionOptions {
14768 trigger_characters: Some(vec![".".to_string()]),
14769 ..Default::default()
14770 }),
14771 ..Default::default()
14772 },
14773 cx,
14774 )
14775 .await;
14776
14777 let counter = Arc::new(AtomicUsize::new(0));
14778 cx.set_state("objˇ");
14779 cx.simulate_keystroke(".");
14780
14781 // Initial completion request returns complete results
14782 let is_incomplete = false;
14783 handle_completion_request(
14784 "obj.|<>",
14785 vec!["a", "ab", "abc"],
14786 is_incomplete,
14787 counter.clone(),
14788 &mut cx,
14789 )
14790 .await;
14791 cx.run_until_parked();
14792 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14793 cx.assert_editor_state("obj.ˇ");
14794 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14795
14796 // Type "a" - filters existing completions
14797 cx.simulate_keystroke("a");
14798 cx.run_until_parked();
14799 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14800 cx.assert_editor_state("obj.aˇ");
14801 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14802
14803 // Type "b" - filters existing completions
14804 cx.simulate_keystroke("b");
14805 cx.run_until_parked();
14806 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14807 cx.assert_editor_state("obj.abˇ");
14808 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14809
14810 // Type "c" - filters existing completions
14811 cx.simulate_keystroke("c");
14812 cx.run_until_parked();
14813 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14814 cx.assert_editor_state("obj.abcˇ");
14815 check_displayed_completions(vec!["abc"], &mut cx);
14816
14817 // Backspace to delete "c" - filters existing completions
14818 cx.update_editor(|editor, window, cx| {
14819 editor.backspace(&Backspace, window, cx);
14820 });
14821 cx.run_until_parked();
14822 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14823 cx.assert_editor_state("obj.abˇ");
14824 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14825
14826 // Moving cursor to the left dismisses menu.
14827 cx.update_editor(|editor, window, cx| {
14828 editor.move_left(&MoveLeft, window, cx);
14829 });
14830 cx.run_until_parked();
14831 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14832 cx.assert_editor_state("obj.aˇb");
14833 cx.update_editor(|editor, _, _| {
14834 assert_eq!(editor.context_menu_visible(), false);
14835 });
14836
14837 // Type "b" - new request
14838 cx.simulate_keystroke("b");
14839 let is_incomplete = false;
14840 handle_completion_request(
14841 "obj.<ab|>a",
14842 vec!["ab", "abc"],
14843 is_incomplete,
14844 counter.clone(),
14845 &mut cx,
14846 )
14847 .await;
14848 cx.run_until_parked();
14849 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14850 cx.assert_editor_state("obj.abˇb");
14851 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14852
14853 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14854 cx.update_editor(|editor, window, cx| {
14855 editor.backspace(&Backspace, window, cx);
14856 });
14857 let is_incomplete = false;
14858 handle_completion_request(
14859 "obj.<a|>b",
14860 vec!["a", "ab", "abc"],
14861 is_incomplete,
14862 counter.clone(),
14863 &mut cx,
14864 )
14865 .await;
14866 cx.run_until_parked();
14867 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14868 cx.assert_editor_state("obj.aˇb");
14869 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14870
14871 // Backspace to delete "a" - dismisses menu.
14872 cx.update_editor(|editor, window, cx| {
14873 editor.backspace(&Backspace, window, cx);
14874 });
14875 cx.run_until_parked();
14876 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14877 cx.assert_editor_state("obj.ˇb");
14878 cx.update_editor(|editor, _, _| {
14879 assert_eq!(editor.context_menu_visible(), false);
14880 });
14881}
14882
14883#[gpui::test]
14884async fn test_word_completion(cx: &mut TestAppContext) {
14885 let lsp_fetch_timeout_ms = 10;
14886 init_test(cx, |language_settings| {
14887 language_settings.defaults.completions = Some(CompletionSettingsContent {
14888 words_min_length: Some(0),
14889 lsp_fetch_timeout_ms: Some(10),
14890 lsp_insert_mode: Some(LspInsertMode::Insert),
14891 ..Default::default()
14892 });
14893 });
14894
14895 let mut cx = EditorLspTestContext::new_rust(
14896 lsp::ServerCapabilities {
14897 completion_provider: Some(lsp::CompletionOptions {
14898 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14899 ..lsp::CompletionOptions::default()
14900 }),
14901 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14902 ..lsp::ServerCapabilities::default()
14903 },
14904 cx,
14905 )
14906 .await;
14907
14908 let throttle_completions = Arc::new(AtomicBool::new(false));
14909
14910 let lsp_throttle_completions = throttle_completions.clone();
14911 let _completion_requests_handler =
14912 cx.lsp
14913 .server
14914 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14915 let lsp_throttle_completions = lsp_throttle_completions.clone();
14916 let cx = cx.clone();
14917 async move {
14918 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14919 cx.background_executor()
14920 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14921 .await;
14922 }
14923 Ok(Some(lsp::CompletionResponse::Array(vec![
14924 lsp::CompletionItem {
14925 label: "first".into(),
14926 ..lsp::CompletionItem::default()
14927 },
14928 lsp::CompletionItem {
14929 label: "last".into(),
14930 ..lsp::CompletionItem::default()
14931 },
14932 ])))
14933 }
14934 });
14935
14936 cx.set_state(indoc! {"
14937 oneˇ
14938 two
14939 three
14940 "});
14941 cx.simulate_keystroke(".");
14942 cx.executor().run_until_parked();
14943 cx.condition(|editor, _| editor.context_menu_visible())
14944 .await;
14945 cx.update_editor(|editor, window, cx| {
14946 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14947 {
14948 assert_eq!(
14949 completion_menu_entries(menu),
14950 &["first", "last"],
14951 "When LSP server is fast to reply, no fallback word completions are used"
14952 );
14953 } else {
14954 panic!("expected completion menu to be open");
14955 }
14956 editor.cancel(&Cancel, window, cx);
14957 });
14958 cx.executor().run_until_parked();
14959 cx.condition(|editor, _| !editor.context_menu_visible())
14960 .await;
14961
14962 throttle_completions.store(true, atomic::Ordering::Release);
14963 cx.simulate_keystroke(".");
14964 cx.executor()
14965 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14966 cx.executor().run_until_parked();
14967 cx.condition(|editor, _| editor.context_menu_visible())
14968 .await;
14969 cx.update_editor(|editor, _, _| {
14970 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14971 {
14972 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14973 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14974 } else {
14975 panic!("expected completion menu to be open");
14976 }
14977 });
14978}
14979
14980#[gpui::test]
14981async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14982 init_test(cx, |language_settings| {
14983 language_settings.defaults.completions = Some(CompletionSettingsContent {
14984 words: Some(WordsCompletionMode::Enabled),
14985 words_min_length: Some(0),
14986 lsp_insert_mode: Some(LspInsertMode::Insert),
14987 ..Default::default()
14988 });
14989 });
14990
14991 let mut cx = EditorLspTestContext::new_rust(
14992 lsp::ServerCapabilities {
14993 completion_provider: Some(lsp::CompletionOptions {
14994 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14995 ..lsp::CompletionOptions::default()
14996 }),
14997 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14998 ..lsp::ServerCapabilities::default()
14999 },
15000 cx,
15001 )
15002 .await;
15003
15004 let _completion_requests_handler =
15005 cx.lsp
15006 .server
15007 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15008 Ok(Some(lsp::CompletionResponse::Array(vec![
15009 lsp::CompletionItem {
15010 label: "first".into(),
15011 ..lsp::CompletionItem::default()
15012 },
15013 lsp::CompletionItem {
15014 label: "last".into(),
15015 ..lsp::CompletionItem::default()
15016 },
15017 ])))
15018 });
15019
15020 cx.set_state(indoc! {"ˇ
15021 first
15022 last
15023 second
15024 "});
15025 cx.simulate_keystroke(".");
15026 cx.executor().run_until_parked();
15027 cx.condition(|editor, _| editor.context_menu_visible())
15028 .await;
15029 cx.update_editor(|editor, _, _| {
15030 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15031 {
15032 assert_eq!(
15033 completion_menu_entries(menu),
15034 &["first", "last", "second"],
15035 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15036 );
15037 } else {
15038 panic!("expected completion menu to be open");
15039 }
15040 });
15041}
15042
15043#[gpui::test]
15044async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15045 init_test(cx, |language_settings| {
15046 language_settings.defaults.completions = Some(CompletionSettingsContent {
15047 words: Some(WordsCompletionMode::Disabled),
15048 words_min_length: Some(0),
15049 lsp_insert_mode: Some(LspInsertMode::Insert),
15050 ..Default::default()
15051 });
15052 });
15053
15054 let mut cx = EditorLspTestContext::new_rust(
15055 lsp::ServerCapabilities {
15056 completion_provider: Some(lsp::CompletionOptions {
15057 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15058 ..lsp::CompletionOptions::default()
15059 }),
15060 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15061 ..lsp::ServerCapabilities::default()
15062 },
15063 cx,
15064 )
15065 .await;
15066
15067 let _completion_requests_handler =
15068 cx.lsp
15069 .server
15070 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15071 panic!("LSP completions should not be queried when dealing with word completions")
15072 });
15073
15074 cx.set_state(indoc! {"ˇ
15075 first
15076 last
15077 second
15078 "});
15079 cx.update_editor(|editor, window, cx| {
15080 editor.show_word_completions(&ShowWordCompletions, window, cx);
15081 });
15082 cx.executor().run_until_parked();
15083 cx.condition(|editor, _| editor.context_menu_visible())
15084 .await;
15085 cx.update_editor(|editor, _, _| {
15086 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15087 {
15088 assert_eq!(
15089 completion_menu_entries(menu),
15090 &["first", "last", "second"],
15091 "`ShowWordCompletions` action should show word completions"
15092 );
15093 } else {
15094 panic!("expected completion menu to be open");
15095 }
15096 });
15097
15098 cx.simulate_keystroke("l");
15099 cx.executor().run_until_parked();
15100 cx.condition(|editor, _| editor.context_menu_visible())
15101 .await;
15102 cx.update_editor(|editor, _, _| {
15103 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15104 {
15105 assert_eq!(
15106 completion_menu_entries(menu),
15107 &["last"],
15108 "After showing word completions, further editing should filter them and not query the LSP"
15109 );
15110 } else {
15111 panic!("expected completion menu to be open");
15112 }
15113 });
15114}
15115
15116#[gpui::test]
15117async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15118 init_test(cx, |language_settings| {
15119 language_settings.defaults.completions = Some(CompletionSettingsContent {
15120 words_min_length: Some(0),
15121 lsp: Some(false),
15122 lsp_insert_mode: Some(LspInsertMode::Insert),
15123 ..Default::default()
15124 });
15125 });
15126
15127 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15128
15129 cx.set_state(indoc! {"ˇ
15130 0_usize
15131 let
15132 33
15133 4.5f32
15134 "});
15135 cx.update_editor(|editor, window, cx| {
15136 editor.show_completions(&ShowCompletions, window, cx);
15137 });
15138 cx.executor().run_until_parked();
15139 cx.condition(|editor, _| editor.context_menu_visible())
15140 .await;
15141 cx.update_editor(|editor, window, cx| {
15142 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15143 {
15144 assert_eq!(
15145 completion_menu_entries(menu),
15146 &["let"],
15147 "With no digits in the completion query, no digits should be in the word completions"
15148 );
15149 } else {
15150 panic!("expected completion menu to be open");
15151 }
15152 editor.cancel(&Cancel, window, cx);
15153 });
15154
15155 cx.set_state(indoc! {"3ˇ
15156 0_usize
15157 let
15158 3
15159 33.35f32
15160 "});
15161 cx.update_editor(|editor, window, cx| {
15162 editor.show_completions(&ShowCompletions, window, cx);
15163 });
15164 cx.executor().run_until_parked();
15165 cx.condition(|editor, _| editor.context_menu_visible())
15166 .await;
15167 cx.update_editor(|editor, _, _| {
15168 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15169 {
15170 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15171 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15172 } else {
15173 panic!("expected completion menu to be open");
15174 }
15175 });
15176}
15177
15178#[gpui::test]
15179async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15180 init_test(cx, |language_settings| {
15181 language_settings.defaults.completions = Some(CompletionSettingsContent {
15182 words: Some(WordsCompletionMode::Enabled),
15183 words_min_length: Some(3),
15184 lsp_insert_mode: Some(LspInsertMode::Insert),
15185 ..Default::default()
15186 });
15187 });
15188
15189 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15190 cx.set_state(indoc! {"ˇ
15191 wow
15192 wowen
15193 wowser
15194 "});
15195 cx.simulate_keystroke("w");
15196 cx.executor().run_until_parked();
15197 cx.update_editor(|editor, _, _| {
15198 if editor.context_menu.borrow_mut().is_some() {
15199 panic!(
15200 "expected completion menu to be hidden, as words completion threshold is not met"
15201 );
15202 }
15203 });
15204
15205 cx.update_editor(|editor, window, cx| {
15206 editor.show_word_completions(&ShowWordCompletions, window, cx);
15207 });
15208 cx.executor().run_until_parked();
15209 cx.update_editor(|editor, window, cx| {
15210 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15211 {
15212 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");
15213 } else {
15214 panic!("expected completion menu to be open after the word completions are called with an action");
15215 }
15216
15217 editor.cancel(&Cancel, window, cx);
15218 });
15219 cx.update_editor(|editor, _, _| {
15220 if editor.context_menu.borrow_mut().is_some() {
15221 panic!("expected completion menu to be hidden after canceling");
15222 }
15223 });
15224
15225 cx.simulate_keystroke("o");
15226 cx.executor().run_until_parked();
15227 cx.update_editor(|editor, _, _| {
15228 if editor.context_menu.borrow_mut().is_some() {
15229 panic!(
15230 "expected completion menu to be hidden, as words completion threshold is not met still"
15231 );
15232 }
15233 });
15234
15235 cx.simulate_keystroke("w");
15236 cx.executor().run_until_parked();
15237 cx.update_editor(|editor, _, _| {
15238 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15239 {
15240 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15241 } else {
15242 panic!("expected completion menu to be open after the word completions threshold is met");
15243 }
15244 });
15245}
15246
15247#[gpui::test]
15248async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15249 init_test(cx, |language_settings| {
15250 language_settings.defaults.completions = Some(CompletionSettingsContent {
15251 words: Some(WordsCompletionMode::Enabled),
15252 words_min_length: Some(0),
15253 lsp_insert_mode: Some(LspInsertMode::Insert),
15254 ..Default::default()
15255 });
15256 });
15257
15258 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15259 cx.update_editor(|editor, _, _| {
15260 editor.disable_word_completions();
15261 });
15262 cx.set_state(indoc! {"ˇ
15263 wow
15264 wowen
15265 wowser
15266 "});
15267 cx.simulate_keystroke("w");
15268 cx.executor().run_until_parked();
15269 cx.update_editor(|editor, _, _| {
15270 if editor.context_menu.borrow_mut().is_some() {
15271 panic!(
15272 "expected completion menu to be hidden, as words completion are disabled for this editor"
15273 );
15274 }
15275 });
15276
15277 cx.update_editor(|editor, window, cx| {
15278 editor.show_word_completions(&ShowWordCompletions, window, cx);
15279 });
15280 cx.executor().run_until_parked();
15281 cx.update_editor(|editor, _, _| {
15282 if editor.context_menu.borrow_mut().is_some() {
15283 panic!(
15284 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15285 );
15286 }
15287 });
15288}
15289
15290#[gpui::test]
15291async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15292 init_test(cx, |language_settings| {
15293 language_settings.defaults.completions = Some(CompletionSettingsContent {
15294 words: Some(WordsCompletionMode::Disabled),
15295 words_min_length: Some(0),
15296 lsp_insert_mode: Some(LspInsertMode::Insert),
15297 ..Default::default()
15298 });
15299 });
15300
15301 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15302 cx.update_editor(|editor, _, _| {
15303 editor.set_completion_provider(None);
15304 });
15305 cx.set_state(indoc! {"ˇ
15306 wow
15307 wowen
15308 wowser
15309 "});
15310 cx.simulate_keystroke("w");
15311 cx.executor().run_until_parked();
15312 cx.update_editor(|editor, _, _| {
15313 if editor.context_menu.borrow_mut().is_some() {
15314 panic!("expected completion menu to be hidden, as disabled in settings");
15315 }
15316 });
15317}
15318
15319fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15320 let position = || lsp::Position {
15321 line: params.text_document_position.position.line,
15322 character: params.text_document_position.position.character,
15323 };
15324 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15325 range: lsp::Range {
15326 start: position(),
15327 end: position(),
15328 },
15329 new_text: text.to_string(),
15330 }))
15331}
15332
15333#[gpui::test]
15334async fn test_multiline_completion(cx: &mut TestAppContext) {
15335 init_test(cx, |_| {});
15336
15337 let fs = FakeFs::new(cx.executor());
15338 fs.insert_tree(
15339 path!("/a"),
15340 json!({
15341 "main.ts": "a",
15342 }),
15343 )
15344 .await;
15345
15346 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15347 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15348 let typescript_language = Arc::new(Language::new(
15349 LanguageConfig {
15350 name: "TypeScript".into(),
15351 matcher: LanguageMatcher {
15352 path_suffixes: vec!["ts".to_string()],
15353 ..LanguageMatcher::default()
15354 },
15355 line_comments: vec!["// ".into()],
15356 ..LanguageConfig::default()
15357 },
15358 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15359 ));
15360 language_registry.add(typescript_language.clone());
15361 let mut fake_servers = language_registry.register_fake_lsp(
15362 "TypeScript",
15363 FakeLspAdapter {
15364 capabilities: lsp::ServerCapabilities {
15365 completion_provider: Some(lsp::CompletionOptions {
15366 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15367 ..lsp::CompletionOptions::default()
15368 }),
15369 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15370 ..lsp::ServerCapabilities::default()
15371 },
15372 // Emulate vtsls label generation
15373 label_for_completion: Some(Box::new(|item, _| {
15374 let text = if let Some(description) = item
15375 .label_details
15376 .as_ref()
15377 .and_then(|label_details| label_details.description.as_ref())
15378 {
15379 format!("{} {}", item.label, description)
15380 } else if let Some(detail) = &item.detail {
15381 format!("{} {}", item.label, detail)
15382 } else {
15383 item.label.clone()
15384 };
15385 Some(language::CodeLabel::plain(text, None))
15386 })),
15387 ..FakeLspAdapter::default()
15388 },
15389 );
15390 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15391 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15392 let worktree_id = workspace
15393 .update(cx, |workspace, _window, cx| {
15394 workspace.project().update(cx, |project, cx| {
15395 project.worktrees(cx).next().unwrap().read(cx).id()
15396 })
15397 })
15398 .unwrap();
15399 let _buffer = project
15400 .update(cx, |project, cx| {
15401 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15402 })
15403 .await
15404 .unwrap();
15405 let editor = workspace
15406 .update(cx, |workspace, window, cx| {
15407 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15408 })
15409 .unwrap()
15410 .await
15411 .unwrap()
15412 .downcast::<Editor>()
15413 .unwrap();
15414 let fake_server = fake_servers.next().await.unwrap();
15415
15416 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15417 let multiline_label_2 = "a\nb\nc\n";
15418 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15419 let multiline_description = "d\ne\nf\n";
15420 let multiline_detail_2 = "g\nh\ni\n";
15421
15422 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15423 move |params, _| async move {
15424 Ok(Some(lsp::CompletionResponse::Array(vec![
15425 lsp::CompletionItem {
15426 label: multiline_label.to_string(),
15427 text_edit: gen_text_edit(¶ms, "new_text_1"),
15428 ..lsp::CompletionItem::default()
15429 },
15430 lsp::CompletionItem {
15431 label: "single line label 1".to_string(),
15432 detail: Some(multiline_detail.to_string()),
15433 text_edit: gen_text_edit(¶ms, "new_text_2"),
15434 ..lsp::CompletionItem::default()
15435 },
15436 lsp::CompletionItem {
15437 label: "single line label 2".to_string(),
15438 label_details: Some(lsp::CompletionItemLabelDetails {
15439 description: Some(multiline_description.to_string()),
15440 detail: None,
15441 }),
15442 text_edit: gen_text_edit(¶ms, "new_text_2"),
15443 ..lsp::CompletionItem::default()
15444 },
15445 lsp::CompletionItem {
15446 label: multiline_label_2.to_string(),
15447 detail: Some(multiline_detail_2.to_string()),
15448 text_edit: gen_text_edit(¶ms, "new_text_3"),
15449 ..lsp::CompletionItem::default()
15450 },
15451 lsp::CompletionItem {
15452 label: "Label with many spaces and \t but without newlines".to_string(),
15453 detail: Some(
15454 "Details with many spaces and \t but without newlines".to_string(),
15455 ),
15456 text_edit: gen_text_edit(¶ms, "new_text_4"),
15457 ..lsp::CompletionItem::default()
15458 },
15459 ])))
15460 },
15461 );
15462
15463 editor.update_in(cx, |editor, window, cx| {
15464 cx.focus_self(window);
15465 editor.move_to_end(&MoveToEnd, window, cx);
15466 editor.handle_input(".", window, cx);
15467 });
15468 cx.run_until_parked();
15469 completion_handle.next().await.unwrap();
15470
15471 editor.update(cx, |editor, _| {
15472 assert!(editor.context_menu_visible());
15473 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15474 {
15475 let completion_labels = menu
15476 .completions
15477 .borrow()
15478 .iter()
15479 .map(|c| c.label.text.clone())
15480 .collect::<Vec<_>>();
15481 assert_eq!(
15482 completion_labels,
15483 &[
15484 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15485 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15486 "single line label 2 d e f ",
15487 "a b c g h i ",
15488 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15489 ],
15490 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15491 );
15492
15493 for completion in menu
15494 .completions
15495 .borrow()
15496 .iter() {
15497 assert_eq!(
15498 completion.label.filter_range,
15499 0..completion.label.text.len(),
15500 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15501 );
15502 }
15503 } else {
15504 panic!("expected completion menu to be open");
15505 }
15506 });
15507}
15508
15509#[gpui::test]
15510async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15511 init_test(cx, |_| {});
15512 let mut cx = EditorLspTestContext::new_rust(
15513 lsp::ServerCapabilities {
15514 completion_provider: Some(lsp::CompletionOptions {
15515 trigger_characters: Some(vec![".".to_string()]),
15516 ..Default::default()
15517 }),
15518 ..Default::default()
15519 },
15520 cx,
15521 )
15522 .await;
15523 cx.lsp
15524 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15525 Ok(Some(lsp::CompletionResponse::Array(vec![
15526 lsp::CompletionItem {
15527 label: "first".into(),
15528 ..Default::default()
15529 },
15530 lsp::CompletionItem {
15531 label: "last".into(),
15532 ..Default::default()
15533 },
15534 ])))
15535 });
15536 cx.set_state("variableˇ");
15537 cx.simulate_keystroke(".");
15538 cx.executor().run_until_parked();
15539
15540 cx.update_editor(|editor, _, _| {
15541 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15542 {
15543 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15544 } else {
15545 panic!("expected completion menu to be open");
15546 }
15547 });
15548
15549 cx.update_editor(|editor, window, cx| {
15550 editor.move_page_down(&MovePageDown::default(), window, cx);
15551 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15552 {
15553 assert!(
15554 menu.selected_item == 1,
15555 "expected PageDown to select the last item from the context menu"
15556 );
15557 } else {
15558 panic!("expected completion menu to stay open after PageDown");
15559 }
15560 });
15561
15562 cx.update_editor(|editor, window, cx| {
15563 editor.move_page_up(&MovePageUp::default(), window, cx);
15564 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15565 {
15566 assert!(
15567 menu.selected_item == 0,
15568 "expected PageUp to select the first item from the context menu"
15569 );
15570 } else {
15571 panic!("expected completion menu to stay open after PageUp");
15572 }
15573 });
15574}
15575
15576#[gpui::test]
15577async fn test_as_is_completions(cx: &mut TestAppContext) {
15578 init_test(cx, |_| {});
15579 let mut cx = EditorLspTestContext::new_rust(
15580 lsp::ServerCapabilities {
15581 completion_provider: Some(lsp::CompletionOptions {
15582 ..Default::default()
15583 }),
15584 ..Default::default()
15585 },
15586 cx,
15587 )
15588 .await;
15589 cx.lsp
15590 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15591 Ok(Some(lsp::CompletionResponse::Array(vec![
15592 lsp::CompletionItem {
15593 label: "unsafe".into(),
15594 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15595 range: lsp::Range {
15596 start: lsp::Position {
15597 line: 1,
15598 character: 2,
15599 },
15600 end: lsp::Position {
15601 line: 1,
15602 character: 3,
15603 },
15604 },
15605 new_text: "unsafe".to_string(),
15606 })),
15607 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15608 ..Default::default()
15609 },
15610 ])))
15611 });
15612 cx.set_state("fn a() {}\n nˇ");
15613 cx.executor().run_until_parked();
15614 cx.update_editor(|editor, window, cx| {
15615 editor.trigger_completion_on_input("n", true, window, cx)
15616 });
15617 cx.executor().run_until_parked();
15618
15619 cx.update_editor(|editor, window, cx| {
15620 editor.confirm_completion(&Default::default(), window, cx)
15621 });
15622 cx.executor().run_until_parked();
15623 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15624}
15625
15626#[gpui::test]
15627async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15628 init_test(cx, |_| {});
15629 let language =
15630 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15631 let mut cx = EditorLspTestContext::new(
15632 language,
15633 lsp::ServerCapabilities {
15634 completion_provider: Some(lsp::CompletionOptions {
15635 ..lsp::CompletionOptions::default()
15636 }),
15637 ..lsp::ServerCapabilities::default()
15638 },
15639 cx,
15640 )
15641 .await;
15642
15643 cx.set_state(
15644 "#ifndef BAR_H
15645#define BAR_H
15646
15647#include <stdbool.h>
15648
15649int fn_branch(bool do_branch1, bool do_branch2);
15650
15651#endif // BAR_H
15652ˇ",
15653 );
15654 cx.executor().run_until_parked();
15655 cx.update_editor(|editor, window, cx| {
15656 editor.handle_input("#", window, cx);
15657 });
15658 cx.executor().run_until_parked();
15659 cx.update_editor(|editor, window, cx| {
15660 editor.handle_input("i", window, cx);
15661 });
15662 cx.executor().run_until_parked();
15663 cx.update_editor(|editor, window, cx| {
15664 editor.handle_input("n", window, cx);
15665 });
15666 cx.executor().run_until_parked();
15667 cx.assert_editor_state(
15668 "#ifndef BAR_H
15669#define BAR_H
15670
15671#include <stdbool.h>
15672
15673int fn_branch(bool do_branch1, bool do_branch2);
15674
15675#endif // BAR_H
15676#inˇ",
15677 );
15678
15679 cx.lsp
15680 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15681 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15682 is_incomplete: false,
15683 item_defaults: None,
15684 items: vec![lsp::CompletionItem {
15685 kind: Some(lsp::CompletionItemKind::SNIPPET),
15686 label_details: Some(lsp::CompletionItemLabelDetails {
15687 detail: Some("header".to_string()),
15688 description: None,
15689 }),
15690 label: " include".to_string(),
15691 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15692 range: lsp::Range {
15693 start: lsp::Position {
15694 line: 8,
15695 character: 1,
15696 },
15697 end: lsp::Position {
15698 line: 8,
15699 character: 1,
15700 },
15701 },
15702 new_text: "include \"$0\"".to_string(),
15703 })),
15704 sort_text: Some("40b67681include".to_string()),
15705 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15706 filter_text: Some("include".to_string()),
15707 insert_text: Some("include \"$0\"".to_string()),
15708 ..lsp::CompletionItem::default()
15709 }],
15710 })))
15711 });
15712 cx.update_editor(|editor, window, cx| {
15713 editor.show_completions(&ShowCompletions, window, cx);
15714 });
15715 cx.executor().run_until_parked();
15716 cx.update_editor(|editor, window, cx| {
15717 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15718 });
15719 cx.executor().run_until_parked();
15720 cx.assert_editor_state(
15721 "#ifndef BAR_H
15722#define BAR_H
15723
15724#include <stdbool.h>
15725
15726int fn_branch(bool do_branch1, bool do_branch2);
15727
15728#endif // BAR_H
15729#include \"ˇ\"",
15730 );
15731
15732 cx.lsp
15733 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15734 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15735 is_incomplete: true,
15736 item_defaults: None,
15737 items: vec![lsp::CompletionItem {
15738 kind: Some(lsp::CompletionItemKind::FILE),
15739 label: "AGL/".to_string(),
15740 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15741 range: lsp::Range {
15742 start: lsp::Position {
15743 line: 8,
15744 character: 10,
15745 },
15746 end: lsp::Position {
15747 line: 8,
15748 character: 11,
15749 },
15750 },
15751 new_text: "AGL/".to_string(),
15752 })),
15753 sort_text: Some("40b67681AGL/".to_string()),
15754 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15755 filter_text: Some("AGL/".to_string()),
15756 insert_text: Some("AGL/".to_string()),
15757 ..lsp::CompletionItem::default()
15758 }],
15759 })))
15760 });
15761 cx.update_editor(|editor, window, cx| {
15762 editor.show_completions(&ShowCompletions, window, cx);
15763 });
15764 cx.executor().run_until_parked();
15765 cx.update_editor(|editor, window, cx| {
15766 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15767 });
15768 cx.executor().run_until_parked();
15769 cx.assert_editor_state(
15770 r##"#ifndef BAR_H
15771#define BAR_H
15772
15773#include <stdbool.h>
15774
15775int fn_branch(bool do_branch1, bool do_branch2);
15776
15777#endif // BAR_H
15778#include "AGL/ˇ"##,
15779 );
15780
15781 cx.update_editor(|editor, window, cx| {
15782 editor.handle_input("\"", window, cx);
15783 });
15784 cx.executor().run_until_parked();
15785 cx.assert_editor_state(
15786 r##"#ifndef BAR_H
15787#define BAR_H
15788
15789#include <stdbool.h>
15790
15791int fn_branch(bool do_branch1, bool do_branch2);
15792
15793#endif // BAR_H
15794#include "AGL/"ˇ"##,
15795 );
15796}
15797
15798#[gpui::test]
15799async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15800 init_test(cx, |_| {});
15801
15802 let mut cx = EditorLspTestContext::new_rust(
15803 lsp::ServerCapabilities {
15804 completion_provider: Some(lsp::CompletionOptions {
15805 trigger_characters: Some(vec![".".to_string()]),
15806 resolve_provider: Some(true),
15807 ..Default::default()
15808 }),
15809 ..Default::default()
15810 },
15811 cx,
15812 )
15813 .await;
15814
15815 cx.set_state("fn main() { let a = 2ˇ; }");
15816 cx.simulate_keystroke(".");
15817 let completion_item = lsp::CompletionItem {
15818 label: "Some".into(),
15819 kind: Some(lsp::CompletionItemKind::SNIPPET),
15820 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15821 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15822 kind: lsp::MarkupKind::Markdown,
15823 value: "```rust\nSome(2)\n```".to_string(),
15824 })),
15825 deprecated: Some(false),
15826 sort_text: Some("Some".to_string()),
15827 filter_text: Some("Some".to_string()),
15828 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15829 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15830 range: lsp::Range {
15831 start: lsp::Position {
15832 line: 0,
15833 character: 22,
15834 },
15835 end: lsp::Position {
15836 line: 0,
15837 character: 22,
15838 },
15839 },
15840 new_text: "Some(2)".to_string(),
15841 })),
15842 additional_text_edits: Some(vec![lsp::TextEdit {
15843 range: lsp::Range {
15844 start: lsp::Position {
15845 line: 0,
15846 character: 20,
15847 },
15848 end: lsp::Position {
15849 line: 0,
15850 character: 22,
15851 },
15852 },
15853 new_text: "".to_string(),
15854 }]),
15855 ..Default::default()
15856 };
15857
15858 let closure_completion_item = completion_item.clone();
15859 let counter = Arc::new(AtomicUsize::new(0));
15860 let counter_clone = counter.clone();
15861 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15862 let task_completion_item = closure_completion_item.clone();
15863 counter_clone.fetch_add(1, atomic::Ordering::Release);
15864 async move {
15865 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15866 is_incomplete: true,
15867 item_defaults: None,
15868 items: vec![task_completion_item],
15869 })))
15870 }
15871 });
15872
15873 cx.condition(|editor, _| editor.context_menu_visible())
15874 .await;
15875 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15876 assert!(request.next().await.is_some());
15877 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15878
15879 cx.simulate_keystrokes("S o m");
15880 cx.condition(|editor, _| editor.context_menu_visible())
15881 .await;
15882 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15883 assert!(request.next().await.is_some());
15884 assert!(request.next().await.is_some());
15885 assert!(request.next().await.is_some());
15886 request.close();
15887 assert!(request.next().await.is_none());
15888 assert_eq!(
15889 counter.load(atomic::Ordering::Acquire),
15890 4,
15891 "With the completions menu open, only one LSP request should happen per input"
15892 );
15893}
15894
15895#[gpui::test]
15896async fn test_toggle_comment(cx: &mut TestAppContext) {
15897 init_test(cx, |_| {});
15898 let mut cx = EditorTestContext::new(cx).await;
15899 let language = Arc::new(Language::new(
15900 LanguageConfig {
15901 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15902 ..Default::default()
15903 },
15904 Some(tree_sitter_rust::LANGUAGE.into()),
15905 ));
15906 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
15907
15908 // If multiple selections intersect a line, the line is only toggled once.
15909 cx.set_state(indoc! {"
15910 fn a() {
15911 «//b();
15912 ˇ»// «c();
15913 //ˇ» d();
15914 }
15915 "});
15916
15917 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15918
15919 cx.assert_editor_state(indoc! {"
15920 fn a() {
15921 «b();
15922 c();
15923 ˇ» d();
15924 }
15925 "});
15926
15927 // The comment prefix is inserted at the same column for every line in a
15928 // selection.
15929 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15930
15931 cx.assert_editor_state(indoc! {"
15932 fn a() {
15933 // «b();
15934 // c();
15935 ˇ»// d();
15936 }
15937 "});
15938
15939 // If a selection ends at the beginning of a line, that line is not toggled.
15940 cx.set_selections_state(indoc! {"
15941 fn a() {
15942 // b();
15943 «// c();
15944 ˇ» // d();
15945 }
15946 "});
15947
15948 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15949
15950 cx.assert_editor_state(indoc! {"
15951 fn a() {
15952 // b();
15953 «c();
15954 ˇ» // d();
15955 }
15956 "});
15957
15958 // If a selection span a single line and is empty, the line is toggled.
15959 cx.set_state(indoc! {"
15960 fn a() {
15961 a();
15962 b();
15963 ˇ
15964 }
15965 "});
15966
15967 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15968
15969 cx.assert_editor_state(indoc! {"
15970 fn a() {
15971 a();
15972 b();
15973 //•ˇ
15974 }
15975 "});
15976
15977 // If a selection span multiple lines, empty lines are not toggled.
15978 cx.set_state(indoc! {"
15979 fn a() {
15980 «a();
15981
15982 c();ˇ»
15983 }
15984 "});
15985
15986 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15987
15988 cx.assert_editor_state(indoc! {"
15989 fn a() {
15990 // «a();
15991
15992 // c();ˇ»
15993 }
15994 "});
15995
15996 // If a selection includes multiple comment prefixes, all lines are uncommented.
15997 cx.set_state(indoc! {"
15998 fn a() {
15999 «// a();
16000 /// b();
16001 //! c();ˇ»
16002 }
16003 "});
16004
16005 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16006
16007 cx.assert_editor_state(indoc! {"
16008 fn a() {
16009 «a();
16010 b();
16011 c();ˇ»
16012 }
16013 "});
16014}
16015
16016#[gpui::test]
16017async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16018 init_test(cx, |_| {});
16019 let mut cx = EditorTestContext::new(cx).await;
16020 let language = Arc::new(Language::new(
16021 LanguageConfig {
16022 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16023 ..Default::default()
16024 },
16025 Some(tree_sitter_rust::LANGUAGE.into()),
16026 ));
16027 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
16028
16029 let toggle_comments = &ToggleComments {
16030 advance_downwards: false,
16031 ignore_indent: true,
16032 };
16033
16034 // If multiple selections intersect a line, the line is only toggled once.
16035 cx.set_state(indoc! {"
16036 fn a() {
16037 // «b();
16038 // c();
16039 // ˇ» d();
16040 }
16041 "});
16042
16043 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16044
16045 cx.assert_editor_state(indoc! {"
16046 fn a() {
16047 «b();
16048 c();
16049 ˇ» d();
16050 }
16051 "});
16052
16053 // The comment prefix is inserted at the beginning of each line
16054 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16055
16056 cx.assert_editor_state(indoc! {"
16057 fn a() {
16058 // «b();
16059 // c();
16060 // ˇ» d();
16061 }
16062 "});
16063
16064 // If a selection ends at the beginning of a line, that line is not toggled.
16065 cx.set_selections_state(indoc! {"
16066 fn a() {
16067 // b();
16068 // «c();
16069 ˇ»// d();
16070 }
16071 "});
16072
16073 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16074
16075 cx.assert_editor_state(indoc! {"
16076 fn a() {
16077 // b();
16078 «c();
16079 ˇ»// d();
16080 }
16081 "});
16082
16083 // If a selection span a single line and is empty, the line is toggled.
16084 cx.set_state(indoc! {"
16085 fn a() {
16086 a();
16087 b();
16088 ˇ
16089 }
16090 "});
16091
16092 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16093
16094 cx.assert_editor_state(indoc! {"
16095 fn a() {
16096 a();
16097 b();
16098 //ˇ
16099 }
16100 "});
16101
16102 // If a selection span multiple lines, empty lines are not toggled.
16103 cx.set_state(indoc! {"
16104 fn a() {
16105 «a();
16106
16107 c();ˇ»
16108 }
16109 "});
16110
16111 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16112
16113 cx.assert_editor_state(indoc! {"
16114 fn a() {
16115 // «a();
16116
16117 // c();ˇ»
16118 }
16119 "});
16120
16121 // If a selection includes multiple comment prefixes, all lines are uncommented.
16122 cx.set_state(indoc! {"
16123 fn a() {
16124 // «a();
16125 /// b();
16126 //! c();ˇ»
16127 }
16128 "});
16129
16130 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16131
16132 cx.assert_editor_state(indoc! {"
16133 fn a() {
16134 «a();
16135 b();
16136 c();ˇ»
16137 }
16138 "});
16139}
16140
16141#[gpui::test]
16142async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16143 init_test(cx, |_| {});
16144
16145 let language = Arc::new(Language::new(
16146 LanguageConfig {
16147 line_comments: vec!["// ".into()],
16148 ..Default::default()
16149 },
16150 Some(tree_sitter_rust::LANGUAGE.into()),
16151 ));
16152
16153 let mut cx = EditorTestContext::new(cx).await;
16154
16155 cx.language_registry().add(language.clone());
16156 cx.update_buffer(|buffer, cx| {
16157 buffer.set_language_immediate(Some(language), cx);
16158 });
16159
16160 let toggle_comments = &ToggleComments {
16161 advance_downwards: true,
16162 ignore_indent: false,
16163 };
16164
16165 // Single cursor on one line -> advance
16166 // Cursor moves horizontally 3 characters as well on non-blank line
16167 cx.set_state(indoc!(
16168 "fn a() {
16169 ˇdog();
16170 cat();
16171 }"
16172 ));
16173 cx.update_editor(|editor, window, cx| {
16174 editor.toggle_comments(toggle_comments, window, cx);
16175 });
16176 cx.assert_editor_state(indoc!(
16177 "fn a() {
16178 // dog();
16179 catˇ();
16180 }"
16181 ));
16182
16183 // Single selection on one line -> don't advance
16184 cx.set_state(indoc!(
16185 "fn a() {
16186 «dog()ˇ»;
16187 cat();
16188 }"
16189 ));
16190 cx.update_editor(|editor, window, cx| {
16191 editor.toggle_comments(toggle_comments, window, cx);
16192 });
16193 cx.assert_editor_state(indoc!(
16194 "fn a() {
16195 // «dog()ˇ»;
16196 cat();
16197 }"
16198 ));
16199
16200 // Multiple cursors on one line -> advance
16201 cx.set_state(indoc!(
16202 "fn a() {
16203 ˇdˇog();
16204 cat();
16205 }"
16206 ));
16207 cx.update_editor(|editor, window, cx| {
16208 editor.toggle_comments(toggle_comments, window, cx);
16209 });
16210 cx.assert_editor_state(indoc!(
16211 "fn a() {
16212 // dog();
16213 catˇ(ˇ);
16214 }"
16215 ));
16216
16217 // Multiple cursors on one line, with selection -> don't advance
16218 cx.set_state(indoc!(
16219 "fn a() {
16220 ˇdˇog«()ˇ»;
16221 cat();
16222 }"
16223 ));
16224 cx.update_editor(|editor, window, cx| {
16225 editor.toggle_comments(toggle_comments, window, cx);
16226 });
16227 cx.assert_editor_state(indoc!(
16228 "fn a() {
16229 // ˇdˇog«()ˇ»;
16230 cat();
16231 }"
16232 ));
16233
16234 // Single cursor on one line -> advance
16235 // Cursor moves to column 0 on blank line
16236 cx.set_state(indoc!(
16237 "fn a() {
16238 ˇdog();
16239
16240 cat();
16241 }"
16242 ));
16243 cx.update_editor(|editor, window, cx| {
16244 editor.toggle_comments(toggle_comments, window, cx);
16245 });
16246 cx.assert_editor_state(indoc!(
16247 "fn a() {
16248 // dog();
16249 ˇ
16250 cat();
16251 }"
16252 ));
16253
16254 // Single cursor on one line -> advance
16255 // Cursor starts and ends at column 0
16256 cx.set_state(indoc!(
16257 "fn a() {
16258 ˇ dog();
16259 cat();
16260 }"
16261 ));
16262 cx.update_editor(|editor, window, cx| {
16263 editor.toggle_comments(toggle_comments, window, cx);
16264 });
16265 cx.assert_editor_state(indoc!(
16266 "fn a() {
16267 // dog();
16268 ˇ cat();
16269 }"
16270 ));
16271}
16272
16273#[gpui::test]
16274async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16275 init_test(cx, |_| {});
16276
16277 let mut cx = EditorTestContext::new(cx).await;
16278
16279 let html_language = Arc::new(
16280 Language::new(
16281 LanguageConfig {
16282 name: "HTML".into(),
16283 block_comment: Some(BlockCommentConfig {
16284 start: "<!-- ".into(),
16285 prefix: "".into(),
16286 end: " -->".into(),
16287 tab_size: 0,
16288 }),
16289 ..Default::default()
16290 },
16291 Some(tree_sitter_html::LANGUAGE.into()),
16292 )
16293 .with_injection_query(
16294 r#"
16295 (script_element
16296 (raw_text) @injection.content
16297 (#set! injection.language "javascript"))
16298 "#,
16299 )
16300 .unwrap(),
16301 );
16302
16303 let javascript_language = Arc::new(Language::new(
16304 LanguageConfig {
16305 name: "JavaScript".into(),
16306 line_comments: vec!["// ".into()],
16307 ..Default::default()
16308 },
16309 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16310 ));
16311
16312 cx.language_registry().add(html_language.clone());
16313 cx.language_registry().add(javascript_language);
16314 cx.update_buffer(|buffer, cx| {
16315 buffer.set_language_immediate(Some(html_language), cx);
16316 });
16317
16318 // Toggle comments for empty selections
16319 cx.set_state(
16320 &r#"
16321 <p>A</p>ˇ
16322 <p>B</p>ˇ
16323 <p>C</p>ˇ
16324 "#
16325 .unindent(),
16326 );
16327 cx.update_editor(|editor, window, cx| {
16328 editor.toggle_comments(&ToggleComments::default(), window, cx)
16329 });
16330 cx.assert_editor_state(
16331 &r#"
16332 <!-- <p>A</p>ˇ -->
16333 <!-- <p>B</p>ˇ -->
16334 <!-- <p>C</p>ˇ -->
16335 "#
16336 .unindent(),
16337 );
16338 cx.update_editor(|editor, window, cx| {
16339 editor.toggle_comments(&ToggleComments::default(), window, cx)
16340 });
16341 cx.assert_editor_state(
16342 &r#"
16343 <p>A</p>ˇ
16344 <p>B</p>ˇ
16345 <p>C</p>ˇ
16346 "#
16347 .unindent(),
16348 );
16349
16350 // Toggle comments for mixture of empty and non-empty selections, where
16351 // multiple selections occupy a given line.
16352 cx.set_state(
16353 &r#"
16354 <p>A«</p>
16355 <p>ˇ»B</p>ˇ
16356 <p>C«</p>
16357 <p>ˇ»D</p>ˇ
16358 "#
16359 .unindent(),
16360 );
16361
16362 cx.update_editor(|editor, window, cx| {
16363 editor.toggle_comments(&ToggleComments::default(), window, cx)
16364 });
16365 cx.assert_editor_state(
16366 &r#"
16367 <!-- <p>A«</p>
16368 <p>ˇ»B</p>ˇ -->
16369 <!-- <p>C«</p>
16370 <p>ˇ»D</p>ˇ -->
16371 "#
16372 .unindent(),
16373 );
16374 cx.update_editor(|editor, window, cx| {
16375 editor.toggle_comments(&ToggleComments::default(), window, cx)
16376 });
16377 cx.assert_editor_state(
16378 &r#"
16379 <p>A«</p>
16380 <p>ˇ»B</p>ˇ
16381 <p>C«</p>
16382 <p>ˇ»D</p>ˇ
16383 "#
16384 .unindent(),
16385 );
16386
16387 // Toggle comments when different languages are active for different
16388 // selections.
16389 cx.set_state(
16390 &r#"
16391 ˇ<script>
16392 ˇvar x = new Y();
16393 ˇ</script>
16394 "#
16395 .unindent(),
16396 );
16397 cx.executor().run_until_parked();
16398 cx.update_editor(|editor, window, cx| {
16399 editor.toggle_comments(&ToggleComments::default(), window, cx)
16400 });
16401 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16402 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16403 cx.assert_editor_state(
16404 &r#"
16405 <!-- ˇ<script> -->
16406 // ˇvar x = new Y();
16407 <!-- ˇ</script> -->
16408 "#
16409 .unindent(),
16410 );
16411}
16412
16413#[gpui::test]
16414fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16415 init_test(cx, |_| {});
16416
16417 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16418 let multibuffer = cx.new(|cx| {
16419 let mut multibuffer = MultiBuffer::new(ReadWrite);
16420 multibuffer.push_excerpts(
16421 buffer.clone(),
16422 [
16423 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16424 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16425 ],
16426 cx,
16427 );
16428 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16429 multibuffer
16430 });
16431
16432 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16433 editor.update_in(cx, |editor, window, cx| {
16434 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16435 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16436 s.select_ranges([
16437 Point::new(0, 0)..Point::new(0, 0),
16438 Point::new(1, 0)..Point::new(1, 0),
16439 ])
16440 });
16441
16442 editor.handle_input("X", window, cx);
16443 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16444 assert_eq!(
16445 editor.selections.ranges(&editor.display_snapshot(cx)),
16446 [
16447 Point::new(0, 1)..Point::new(0, 1),
16448 Point::new(1, 1)..Point::new(1, 1),
16449 ]
16450 );
16451
16452 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16453 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16454 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16455 });
16456 editor.backspace(&Default::default(), window, cx);
16457 assert_eq!(editor.text(cx), "Xa\nbbb");
16458 assert_eq!(
16459 editor.selections.ranges(&editor.display_snapshot(cx)),
16460 [Point::new(1, 0)..Point::new(1, 0)]
16461 );
16462
16463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16464 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16465 });
16466 editor.backspace(&Default::default(), window, cx);
16467 assert_eq!(editor.text(cx), "X\nbb");
16468 assert_eq!(
16469 editor.selections.ranges(&editor.display_snapshot(cx)),
16470 [Point::new(0, 1)..Point::new(0, 1)]
16471 );
16472 });
16473}
16474
16475#[gpui::test]
16476fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16477 init_test(cx, |_| {});
16478
16479 let markers = vec![('[', ']').into(), ('(', ')').into()];
16480 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16481 indoc! {"
16482 [aaaa
16483 (bbbb]
16484 cccc)",
16485 },
16486 markers.clone(),
16487 );
16488 let excerpt_ranges = markers.into_iter().map(|marker| {
16489 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16490 ExcerptRange::new(context)
16491 });
16492 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16493 let multibuffer = cx.new(|cx| {
16494 let mut multibuffer = MultiBuffer::new(ReadWrite);
16495 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16496 multibuffer
16497 });
16498
16499 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16500 editor.update_in(cx, |editor, window, cx| {
16501 let (expected_text, selection_ranges) = marked_text_ranges(
16502 indoc! {"
16503 aaaa
16504 bˇbbb
16505 bˇbbˇb
16506 cccc"
16507 },
16508 true,
16509 );
16510 assert_eq!(editor.text(cx), expected_text);
16511 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16512 s.select_ranges(
16513 selection_ranges
16514 .iter()
16515 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16516 )
16517 });
16518
16519 editor.handle_input("X", window, cx);
16520
16521 let (expected_text, expected_selections) = marked_text_ranges(
16522 indoc! {"
16523 aaaa
16524 bXˇbbXb
16525 bXˇbbXˇb
16526 cccc"
16527 },
16528 false,
16529 );
16530 assert_eq!(editor.text(cx), expected_text);
16531 assert_eq!(
16532 editor.selections.ranges(&editor.display_snapshot(cx)),
16533 expected_selections
16534 .iter()
16535 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16536 .collect::<Vec<_>>()
16537 );
16538
16539 editor.newline(&Newline, window, cx);
16540 let (expected_text, expected_selections) = marked_text_ranges(
16541 indoc! {"
16542 aaaa
16543 bX
16544 ˇbbX
16545 b
16546 bX
16547 ˇbbX
16548 ˇb
16549 cccc"
16550 },
16551 false,
16552 );
16553 assert_eq!(editor.text(cx), expected_text);
16554 assert_eq!(
16555 editor.selections.ranges(&editor.display_snapshot(cx)),
16556 expected_selections
16557 .iter()
16558 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16559 .collect::<Vec<_>>()
16560 );
16561 });
16562}
16563
16564#[gpui::test]
16565fn test_refresh_selections(cx: &mut TestAppContext) {
16566 init_test(cx, |_| {});
16567
16568 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16569 let mut excerpt1_id = None;
16570 let multibuffer = cx.new(|cx| {
16571 let mut multibuffer = MultiBuffer::new(ReadWrite);
16572 excerpt1_id = multibuffer
16573 .push_excerpts(
16574 buffer.clone(),
16575 [
16576 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16577 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16578 ],
16579 cx,
16580 )
16581 .into_iter()
16582 .next();
16583 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16584 multibuffer
16585 });
16586
16587 let editor = cx.add_window(|window, cx| {
16588 let mut editor = build_editor(multibuffer.clone(), window, cx);
16589 let snapshot = editor.snapshot(window, cx);
16590 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16591 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16592 });
16593 editor.begin_selection(
16594 Point::new(2, 1).to_display_point(&snapshot),
16595 true,
16596 1,
16597 window,
16598 cx,
16599 );
16600 assert_eq!(
16601 editor.selections.ranges(&editor.display_snapshot(cx)),
16602 [
16603 Point::new(1, 3)..Point::new(1, 3),
16604 Point::new(2, 1)..Point::new(2, 1),
16605 ]
16606 );
16607 editor
16608 });
16609
16610 // Refreshing selections is a no-op when excerpts haven't changed.
16611 _ = editor.update(cx, |editor, window, cx| {
16612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16613 assert_eq!(
16614 editor.selections.ranges(&editor.display_snapshot(cx)),
16615 [
16616 Point::new(1, 3)..Point::new(1, 3),
16617 Point::new(2, 1)..Point::new(2, 1),
16618 ]
16619 );
16620 });
16621
16622 multibuffer.update(cx, |multibuffer, cx| {
16623 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16624 });
16625 _ = editor.update(cx, |editor, window, cx| {
16626 // Removing an excerpt causes the first selection to become degenerate.
16627 assert_eq!(
16628 editor.selections.ranges(&editor.display_snapshot(cx)),
16629 [
16630 Point::new(0, 0)..Point::new(0, 0),
16631 Point::new(0, 1)..Point::new(0, 1)
16632 ]
16633 );
16634
16635 // Refreshing selections will relocate the first selection to the original buffer
16636 // location.
16637 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16638 assert_eq!(
16639 editor.selections.ranges(&editor.display_snapshot(cx)),
16640 [
16641 Point::new(0, 1)..Point::new(0, 1),
16642 Point::new(0, 3)..Point::new(0, 3)
16643 ]
16644 );
16645 assert!(editor.selections.pending_anchor().is_some());
16646 });
16647}
16648
16649#[gpui::test]
16650fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16651 init_test(cx, |_| {});
16652
16653 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16654 let mut excerpt1_id = None;
16655 let multibuffer = cx.new(|cx| {
16656 let mut multibuffer = MultiBuffer::new(ReadWrite);
16657 excerpt1_id = multibuffer
16658 .push_excerpts(
16659 buffer.clone(),
16660 [
16661 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16662 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16663 ],
16664 cx,
16665 )
16666 .into_iter()
16667 .next();
16668 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16669 multibuffer
16670 });
16671
16672 let editor = cx.add_window(|window, cx| {
16673 let mut editor = build_editor(multibuffer.clone(), window, cx);
16674 let snapshot = editor.snapshot(window, cx);
16675 editor.begin_selection(
16676 Point::new(1, 3).to_display_point(&snapshot),
16677 false,
16678 1,
16679 window,
16680 cx,
16681 );
16682 assert_eq!(
16683 editor.selections.ranges(&editor.display_snapshot(cx)),
16684 [Point::new(1, 3)..Point::new(1, 3)]
16685 );
16686 editor
16687 });
16688
16689 multibuffer.update(cx, |multibuffer, cx| {
16690 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16691 });
16692 _ = editor.update(cx, |editor, window, cx| {
16693 assert_eq!(
16694 editor.selections.ranges(&editor.display_snapshot(cx)),
16695 [Point::new(0, 0)..Point::new(0, 0)]
16696 );
16697
16698 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16699 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16700 assert_eq!(
16701 editor.selections.ranges(&editor.display_snapshot(cx)),
16702 [Point::new(0, 3)..Point::new(0, 3)]
16703 );
16704 assert!(editor.selections.pending_anchor().is_some());
16705 });
16706}
16707
16708#[gpui::test]
16709async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16710 init_test(cx, |_| {});
16711
16712 let language = Arc::new(
16713 Language::new(
16714 LanguageConfig {
16715 brackets: BracketPairConfig {
16716 pairs: vec![
16717 BracketPair {
16718 start: "{".to_string(),
16719 end: "}".to_string(),
16720 close: true,
16721 surround: true,
16722 newline: true,
16723 },
16724 BracketPair {
16725 start: "/* ".to_string(),
16726 end: " */".to_string(),
16727 close: true,
16728 surround: true,
16729 newline: true,
16730 },
16731 ],
16732 ..Default::default()
16733 },
16734 ..Default::default()
16735 },
16736 Some(tree_sitter_rust::LANGUAGE.into()),
16737 )
16738 .with_indents_query("")
16739 .unwrap(),
16740 );
16741
16742 let text = concat!(
16743 "{ }\n", //
16744 " x\n", //
16745 " /* */\n", //
16746 "x\n", //
16747 "{{} }\n", //
16748 );
16749
16750 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
16751 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16752 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16753 editor
16754 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16755 .await;
16756
16757 editor.update_in(cx, |editor, window, cx| {
16758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16759 s.select_display_ranges([
16760 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16761 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16762 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16763 ])
16764 });
16765 editor.newline(&Newline, window, cx);
16766
16767 assert_eq!(
16768 editor.buffer().read(cx).read(cx).text(),
16769 concat!(
16770 "{ \n", // Suppress rustfmt
16771 "\n", //
16772 "}\n", //
16773 " x\n", //
16774 " /* \n", //
16775 " \n", //
16776 " */\n", //
16777 "x\n", //
16778 "{{} \n", //
16779 "}\n", //
16780 )
16781 );
16782 });
16783}
16784
16785#[gpui::test]
16786fn test_highlighted_ranges(cx: &mut TestAppContext) {
16787 init_test(cx, |_| {});
16788
16789 let editor = cx.add_window(|window, cx| {
16790 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16791 build_editor(buffer, window, cx)
16792 });
16793
16794 _ = editor.update(cx, |editor, window, cx| {
16795 struct Type1;
16796 struct Type2;
16797
16798 let buffer = editor.buffer.read(cx).snapshot(cx);
16799
16800 let anchor_range =
16801 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16802
16803 editor.highlight_background::<Type1>(
16804 &[
16805 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16806 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16807 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16808 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16809 ],
16810 |_| Hsla::red(),
16811 cx,
16812 );
16813 editor.highlight_background::<Type2>(
16814 &[
16815 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16816 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16817 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16818 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16819 ],
16820 |_| Hsla::green(),
16821 cx,
16822 );
16823
16824 let snapshot = editor.snapshot(window, cx);
16825 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16826 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16827 &snapshot,
16828 cx.theme(),
16829 );
16830 assert_eq!(
16831 highlighted_ranges,
16832 &[
16833 (
16834 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16835 Hsla::green(),
16836 ),
16837 (
16838 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16839 Hsla::red(),
16840 ),
16841 (
16842 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16843 Hsla::green(),
16844 ),
16845 (
16846 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16847 Hsla::red(),
16848 ),
16849 ]
16850 );
16851 assert_eq!(
16852 editor.sorted_background_highlights_in_range(
16853 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16854 &snapshot,
16855 cx.theme(),
16856 ),
16857 &[(
16858 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16859 Hsla::red(),
16860 )]
16861 );
16862 });
16863}
16864
16865#[gpui::test]
16866async fn test_following(cx: &mut TestAppContext) {
16867 init_test(cx, |_| {});
16868
16869 let fs = FakeFs::new(cx.executor());
16870 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16871
16872 let buffer = project.update(cx, |project, cx| {
16873 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16874 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16875 });
16876 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16877 let follower = cx.update(|cx| {
16878 cx.open_window(
16879 WindowOptions {
16880 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16881 gpui::Point::new(px(0.), px(0.)),
16882 gpui::Point::new(px(10.), px(80.)),
16883 ))),
16884 ..Default::default()
16885 },
16886 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16887 )
16888 .unwrap()
16889 });
16890
16891 let is_still_following = Rc::new(RefCell::new(true));
16892 let follower_edit_event_count = Rc::new(RefCell::new(0));
16893 let pending_update = Rc::new(RefCell::new(None));
16894 let leader_entity = leader.root(cx).unwrap();
16895 let follower_entity = follower.root(cx).unwrap();
16896 _ = follower.update(cx, {
16897 let update = pending_update.clone();
16898 let is_still_following = is_still_following.clone();
16899 let follower_edit_event_count = follower_edit_event_count.clone();
16900 |_, window, cx| {
16901 cx.subscribe_in(
16902 &leader_entity,
16903 window,
16904 move |_, leader, event, window, cx| {
16905 leader.read(cx).add_event_to_update_proto(
16906 event,
16907 &mut update.borrow_mut(),
16908 window,
16909 cx,
16910 );
16911 },
16912 )
16913 .detach();
16914
16915 cx.subscribe_in(
16916 &follower_entity,
16917 window,
16918 move |_, _, event: &EditorEvent, _window, _cx| {
16919 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16920 *is_still_following.borrow_mut() = false;
16921 }
16922
16923 if let EditorEvent::BufferEdited = event {
16924 *follower_edit_event_count.borrow_mut() += 1;
16925 }
16926 },
16927 )
16928 .detach();
16929 }
16930 });
16931
16932 // Update the selections only
16933 _ = leader.update(cx, |leader, window, cx| {
16934 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16935 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
16936 });
16937 });
16938 follower
16939 .update(cx, |follower, window, cx| {
16940 follower.apply_update_proto(
16941 &project,
16942 pending_update.borrow_mut().take().unwrap(),
16943 window,
16944 cx,
16945 )
16946 })
16947 .unwrap()
16948 .await
16949 .unwrap();
16950 _ = follower.update(cx, |follower, _, cx| {
16951 assert_eq!(
16952 follower.selections.ranges(&follower.display_snapshot(cx)),
16953 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
16954 );
16955 });
16956 assert!(*is_still_following.borrow());
16957 assert_eq!(*follower_edit_event_count.borrow(), 0);
16958
16959 // Update the scroll position only
16960 _ = leader.update(cx, |leader, window, cx| {
16961 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16962 });
16963 follower
16964 .update(cx, |follower, window, cx| {
16965 follower.apply_update_proto(
16966 &project,
16967 pending_update.borrow_mut().take().unwrap(),
16968 window,
16969 cx,
16970 )
16971 })
16972 .unwrap()
16973 .await
16974 .unwrap();
16975 assert_eq!(
16976 follower
16977 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16978 .unwrap(),
16979 gpui::Point::new(1.5, 3.5)
16980 );
16981 assert!(*is_still_following.borrow());
16982 assert_eq!(*follower_edit_event_count.borrow(), 0);
16983
16984 // Update the selections and scroll position. The follower's scroll position is updated
16985 // via autoscroll, not via the leader's exact scroll position.
16986 _ = leader.update(cx, |leader, window, cx| {
16987 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16988 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
16989 });
16990 leader.request_autoscroll(Autoscroll::newest(), cx);
16991 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16992 });
16993 follower
16994 .update(cx, |follower, window, cx| {
16995 follower.apply_update_proto(
16996 &project,
16997 pending_update.borrow_mut().take().unwrap(),
16998 window,
16999 cx,
17000 )
17001 })
17002 .unwrap()
17003 .await
17004 .unwrap();
17005 _ = follower.update(cx, |follower, _, cx| {
17006 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17007 assert_eq!(
17008 follower.selections.ranges(&follower.display_snapshot(cx)),
17009 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17010 );
17011 });
17012 assert!(*is_still_following.borrow());
17013
17014 // Creating a pending selection that precedes another selection
17015 _ = leader.update(cx, |leader, window, cx| {
17016 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17017 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17018 });
17019 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17020 });
17021 follower
17022 .update(cx, |follower, window, cx| {
17023 follower.apply_update_proto(
17024 &project,
17025 pending_update.borrow_mut().take().unwrap(),
17026 window,
17027 cx,
17028 )
17029 })
17030 .unwrap()
17031 .await
17032 .unwrap();
17033 _ = follower.update(cx, |follower, _, cx| {
17034 assert_eq!(
17035 follower.selections.ranges(&follower.display_snapshot(cx)),
17036 vec![
17037 MultiBufferOffset(0)..MultiBufferOffset(0),
17038 MultiBufferOffset(1)..MultiBufferOffset(1)
17039 ]
17040 );
17041 });
17042 assert!(*is_still_following.borrow());
17043
17044 // Extend the pending selection so that it surrounds another selection
17045 _ = leader.update(cx, |leader, window, cx| {
17046 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17047 });
17048 follower
17049 .update(cx, |follower, window, cx| {
17050 follower.apply_update_proto(
17051 &project,
17052 pending_update.borrow_mut().take().unwrap(),
17053 window,
17054 cx,
17055 )
17056 })
17057 .unwrap()
17058 .await
17059 .unwrap();
17060 _ = follower.update(cx, |follower, _, cx| {
17061 assert_eq!(
17062 follower.selections.ranges(&follower.display_snapshot(cx)),
17063 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17064 );
17065 });
17066
17067 // Scrolling locally breaks the follow
17068 _ = follower.update(cx, |follower, window, cx| {
17069 let top_anchor = follower
17070 .buffer()
17071 .read(cx)
17072 .read(cx)
17073 .anchor_after(MultiBufferOffset(0));
17074 follower.set_scroll_anchor(
17075 ScrollAnchor {
17076 anchor: top_anchor,
17077 offset: gpui::Point::new(0.0, 0.5),
17078 },
17079 window,
17080 cx,
17081 );
17082 });
17083 assert!(!(*is_still_following.borrow()));
17084}
17085
17086#[gpui::test]
17087async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17088 init_test(cx, |_| {});
17089
17090 let fs = FakeFs::new(cx.executor());
17091 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17092 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17093 let pane = workspace
17094 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17095 .unwrap();
17096
17097 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17098
17099 let leader = pane.update_in(cx, |_, window, cx| {
17100 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17101 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17102 });
17103
17104 // Start following the editor when it has no excerpts.
17105 let mut state_message =
17106 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17107 let workspace_entity = workspace.root(cx).unwrap();
17108 let follower_1 = cx
17109 .update_window(*workspace.deref(), |_, window, cx| {
17110 Editor::from_state_proto(
17111 workspace_entity,
17112 ViewId {
17113 creator: CollaboratorId::PeerId(PeerId::default()),
17114 id: 0,
17115 },
17116 &mut state_message,
17117 window,
17118 cx,
17119 )
17120 })
17121 .unwrap()
17122 .unwrap()
17123 .await
17124 .unwrap();
17125
17126 let update_message = Rc::new(RefCell::new(None));
17127 follower_1.update_in(cx, {
17128 let update = update_message.clone();
17129 |_, window, cx| {
17130 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17131 leader.read(cx).add_event_to_update_proto(
17132 event,
17133 &mut update.borrow_mut(),
17134 window,
17135 cx,
17136 );
17137 })
17138 .detach();
17139 }
17140 });
17141
17142 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17143 (
17144 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17145 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17146 )
17147 });
17148
17149 // Insert some excerpts.
17150 leader.update(cx, |leader, cx| {
17151 leader.buffer.update(cx, |multibuffer, cx| {
17152 multibuffer.set_excerpts_for_path(
17153 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17154 buffer_1.clone(),
17155 vec![
17156 Point::row_range(0..3),
17157 Point::row_range(1..6),
17158 Point::row_range(12..15),
17159 ],
17160 0,
17161 cx,
17162 );
17163 multibuffer.set_excerpts_for_path(
17164 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17165 buffer_2.clone(),
17166 vec![Point::row_range(0..6), Point::row_range(8..12)],
17167 0,
17168 cx,
17169 );
17170 });
17171 });
17172
17173 // Apply the update of adding the excerpts.
17174 follower_1
17175 .update_in(cx, |follower, window, cx| {
17176 follower.apply_update_proto(
17177 &project,
17178 update_message.borrow().clone().unwrap(),
17179 window,
17180 cx,
17181 )
17182 })
17183 .await
17184 .unwrap();
17185 assert_eq!(
17186 follower_1.update(cx, |editor, cx| editor.text(cx)),
17187 leader.update(cx, |editor, cx| editor.text(cx))
17188 );
17189 update_message.borrow_mut().take();
17190
17191 // Start following separately after it already has excerpts.
17192 let mut state_message =
17193 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17194 let workspace_entity = workspace.root(cx).unwrap();
17195 let follower_2 = cx
17196 .update_window(*workspace.deref(), |_, window, cx| {
17197 Editor::from_state_proto(
17198 workspace_entity,
17199 ViewId {
17200 creator: CollaboratorId::PeerId(PeerId::default()),
17201 id: 0,
17202 },
17203 &mut state_message,
17204 window,
17205 cx,
17206 )
17207 })
17208 .unwrap()
17209 .unwrap()
17210 .await
17211 .unwrap();
17212 assert_eq!(
17213 follower_2.update(cx, |editor, cx| editor.text(cx)),
17214 leader.update(cx, |editor, cx| editor.text(cx))
17215 );
17216
17217 // Remove some excerpts.
17218 leader.update(cx, |leader, cx| {
17219 leader.buffer.update(cx, |multibuffer, cx| {
17220 let excerpt_ids = multibuffer.excerpt_ids();
17221 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17222 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17223 });
17224 });
17225
17226 // Apply the update of removing the excerpts.
17227 follower_1
17228 .update_in(cx, |follower, window, cx| {
17229 follower.apply_update_proto(
17230 &project,
17231 update_message.borrow().clone().unwrap(),
17232 window,
17233 cx,
17234 )
17235 })
17236 .await
17237 .unwrap();
17238 follower_2
17239 .update_in(cx, |follower, window, cx| {
17240 follower.apply_update_proto(
17241 &project,
17242 update_message.borrow().clone().unwrap(),
17243 window,
17244 cx,
17245 )
17246 })
17247 .await
17248 .unwrap();
17249 update_message.borrow_mut().take();
17250 assert_eq!(
17251 follower_1.update(cx, |editor, cx| editor.text(cx)),
17252 leader.update(cx, |editor, cx| editor.text(cx))
17253 );
17254}
17255
17256#[gpui::test]
17257async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17258 init_test(cx, |_| {});
17259
17260 let mut cx = EditorTestContext::new(cx).await;
17261 let lsp_store =
17262 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17263
17264 cx.set_state(indoc! {"
17265 ˇfn func(abc def: i32) -> u32 {
17266 }
17267 "});
17268
17269 cx.update(|_, cx| {
17270 lsp_store.update(cx, |lsp_store, cx| {
17271 lsp_store
17272 .update_diagnostics(
17273 LanguageServerId(0),
17274 lsp::PublishDiagnosticsParams {
17275 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17276 version: None,
17277 diagnostics: vec![
17278 lsp::Diagnostic {
17279 range: lsp::Range::new(
17280 lsp::Position::new(0, 11),
17281 lsp::Position::new(0, 12),
17282 ),
17283 severity: Some(lsp::DiagnosticSeverity::ERROR),
17284 ..Default::default()
17285 },
17286 lsp::Diagnostic {
17287 range: lsp::Range::new(
17288 lsp::Position::new(0, 12),
17289 lsp::Position::new(0, 15),
17290 ),
17291 severity: Some(lsp::DiagnosticSeverity::ERROR),
17292 ..Default::default()
17293 },
17294 lsp::Diagnostic {
17295 range: lsp::Range::new(
17296 lsp::Position::new(0, 25),
17297 lsp::Position::new(0, 28),
17298 ),
17299 severity: Some(lsp::DiagnosticSeverity::ERROR),
17300 ..Default::default()
17301 },
17302 ],
17303 },
17304 None,
17305 DiagnosticSourceKind::Pushed,
17306 &[],
17307 cx,
17308 )
17309 .unwrap()
17310 });
17311 });
17312
17313 executor.run_until_parked();
17314
17315 cx.update_editor(|editor, window, cx| {
17316 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17317 });
17318
17319 cx.assert_editor_state(indoc! {"
17320 fn func(abc def: i32) -> ˇu32 {
17321 }
17322 "});
17323
17324 cx.update_editor(|editor, window, cx| {
17325 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17326 });
17327
17328 cx.assert_editor_state(indoc! {"
17329 fn func(abc ˇdef: i32) -> u32 {
17330 }
17331 "});
17332
17333 cx.update_editor(|editor, window, cx| {
17334 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17335 });
17336
17337 cx.assert_editor_state(indoc! {"
17338 fn func(abcˇ def: i32) -> u32 {
17339 }
17340 "});
17341
17342 cx.update_editor(|editor, window, cx| {
17343 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17344 });
17345
17346 cx.assert_editor_state(indoc! {"
17347 fn func(abc def: i32) -> ˇu32 {
17348 }
17349 "});
17350}
17351
17352#[gpui::test]
17353async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17354 init_test(cx, |_| {});
17355
17356 let mut cx = EditorTestContext::new(cx).await;
17357
17358 let diff_base = r#"
17359 use some::mod;
17360
17361 const A: u32 = 42;
17362
17363 fn main() {
17364 println!("hello");
17365
17366 println!("world");
17367 }
17368 "#
17369 .unindent();
17370
17371 // Edits are modified, removed, modified, added
17372 cx.set_state(
17373 &r#"
17374 use some::modified;
17375
17376 ˇ
17377 fn main() {
17378 println!("hello there");
17379
17380 println!("around the");
17381 println!("world");
17382 }
17383 "#
17384 .unindent(),
17385 );
17386
17387 cx.set_head_text(&diff_base);
17388 executor.run_until_parked();
17389
17390 cx.update_editor(|editor, window, cx| {
17391 //Wrap around the bottom of the buffer
17392 for _ in 0..3 {
17393 editor.go_to_next_hunk(&GoToHunk, window, cx);
17394 }
17395 });
17396
17397 cx.assert_editor_state(
17398 &r#"
17399 ˇuse some::modified;
17400
17401
17402 fn main() {
17403 println!("hello there");
17404
17405 println!("around the");
17406 println!("world");
17407 }
17408 "#
17409 .unindent(),
17410 );
17411
17412 cx.update_editor(|editor, window, cx| {
17413 //Wrap around the top of the buffer
17414 for _ in 0..2 {
17415 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17416 }
17417 });
17418
17419 cx.assert_editor_state(
17420 &r#"
17421 use some::modified;
17422
17423
17424 fn main() {
17425 ˇ println!("hello there");
17426
17427 println!("around the");
17428 println!("world");
17429 }
17430 "#
17431 .unindent(),
17432 );
17433
17434 cx.update_editor(|editor, window, cx| {
17435 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17436 });
17437
17438 cx.assert_editor_state(
17439 &r#"
17440 use some::modified;
17441
17442 ˇ
17443 fn main() {
17444 println!("hello there");
17445
17446 println!("around the");
17447 println!("world");
17448 }
17449 "#
17450 .unindent(),
17451 );
17452
17453 cx.update_editor(|editor, window, cx| {
17454 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17455 });
17456
17457 cx.assert_editor_state(
17458 &r#"
17459 ˇuse some::modified;
17460
17461
17462 fn main() {
17463 println!("hello there");
17464
17465 println!("around the");
17466 println!("world");
17467 }
17468 "#
17469 .unindent(),
17470 );
17471
17472 cx.update_editor(|editor, window, cx| {
17473 for _ in 0..2 {
17474 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17475 }
17476 });
17477
17478 cx.assert_editor_state(
17479 &r#"
17480 use some::modified;
17481
17482
17483 fn main() {
17484 ˇ println!("hello there");
17485
17486 println!("around the");
17487 println!("world");
17488 }
17489 "#
17490 .unindent(),
17491 );
17492
17493 cx.update_editor(|editor, window, cx| {
17494 editor.fold(&Fold, window, cx);
17495 });
17496
17497 cx.update_editor(|editor, window, cx| {
17498 editor.go_to_next_hunk(&GoToHunk, window, cx);
17499 });
17500
17501 cx.assert_editor_state(
17502 &r#"
17503 ˇuse some::modified;
17504
17505
17506 fn main() {
17507 println!("hello there");
17508
17509 println!("around the");
17510 println!("world");
17511 }
17512 "#
17513 .unindent(),
17514 );
17515}
17516
17517#[test]
17518fn test_split_words() {
17519 fn split(text: &str) -> Vec<&str> {
17520 split_words(text).collect()
17521 }
17522
17523 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17524 assert_eq!(split("hello_world"), &["hello_", "world"]);
17525 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17526 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17527 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17528 assert_eq!(split("helloworld"), &["helloworld"]);
17529
17530 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17531}
17532
17533#[test]
17534fn test_split_words_for_snippet_prefix() {
17535 fn split(text: &str) -> Vec<&str> {
17536 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17537 }
17538
17539 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17540 assert_eq!(split("hello_world"), &["hello_world"]);
17541 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17542 assert_eq!(split("Hello_World"), &["Hello_World"]);
17543 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17544 assert_eq!(split("helloworld"), &["helloworld"]);
17545 assert_eq!(
17546 split("this@is!@#$^many . symbols"),
17547 &[
17548 "symbols",
17549 " symbols",
17550 ". symbols",
17551 " . symbols",
17552 " . symbols",
17553 " . symbols",
17554 "many . symbols",
17555 "^many . symbols",
17556 "$^many . symbols",
17557 "#$^many . symbols",
17558 "@#$^many . symbols",
17559 "!@#$^many . symbols",
17560 "is!@#$^many . symbols",
17561 "@is!@#$^many . symbols",
17562 "this@is!@#$^many . symbols",
17563 ],
17564 );
17565 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17566}
17567
17568#[gpui::test]
17569async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17570 init_test(cx, |_| {});
17571
17572 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17573
17574 #[track_caller]
17575 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17576 let _state_context = cx.set_state(before);
17577 cx.run_until_parked();
17578 cx.update_editor(|editor, window, cx| {
17579 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17580 });
17581 cx.run_until_parked();
17582 cx.assert_editor_state(after);
17583 }
17584
17585 // Outside bracket jumps to outside of matching bracket
17586 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17587 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17588
17589 // Inside bracket jumps to inside of matching bracket
17590 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17591 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17592
17593 // When outside a bracket and inside, favor jumping to the inside bracket
17594 assert(
17595 "console.log('foo', [1, 2, 3]ˇ);",
17596 "console.log('foo', ˇ[1, 2, 3]);",
17597 &mut cx,
17598 );
17599 assert(
17600 "console.log(ˇ'foo', [1, 2, 3]);",
17601 "console.log('foo'ˇ, [1, 2, 3]);",
17602 &mut cx,
17603 );
17604
17605 // Bias forward if two options are equally likely
17606 assert(
17607 "let result = curried_fun()ˇ();",
17608 "let result = curried_fun()()ˇ;",
17609 &mut cx,
17610 );
17611
17612 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17613 assert(
17614 indoc! {"
17615 function test() {
17616 console.log('test')ˇ
17617 }"},
17618 indoc! {"
17619 function test() {
17620 console.logˇ('test')
17621 }"},
17622 &mut cx,
17623 );
17624}
17625
17626#[gpui::test]
17627async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
17628 init_test(cx, |_| {});
17629 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
17630 language_registry.add(markdown_lang());
17631 language_registry.add(rust_lang());
17632 let buffer = cx.new(|cx| {
17633 let mut buffer = language::Buffer::local(
17634 indoc! {"
17635 ```rs
17636 impl Worktree {
17637 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17638 }
17639 }
17640 ```
17641 "},
17642 cx,
17643 );
17644 buffer.set_language_registry(language_registry.clone());
17645 buffer.set_language_immediate(Some(markdown_lang()), cx);
17646 buffer
17647 });
17648 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17649 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17650 cx.executor().run_until_parked();
17651 _ = editor.update(cx, |editor, window, cx| {
17652 // Case 1: Test outer enclosing brackets
17653 select_ranges(
17654 editor,
17655 &indoc! {"
17656 ```rs
17657 impl Worktree {
17658 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17659 }
17660 }ˇ
17661 ```
17662 "},
17663 window,
17664 cx,
17665 );
17666 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17667 assert_text_with_selections(
17668 editor,
17669 &indoc! {"
17670 ```rs
17671 impl Worktree ˇ{
17672 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17673 }
17674 }
17675 ```
17676 "},
17677 cx,
17678 );
17679 // Case 2: Test inner enclosing brackets
17680 select_ranges(
17681 editor,
17682 &indoc! {"
17683 ```rs
17684 impl Worktree {
17685 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17686 }ˇ
17687 }
17688 ```
17689 "},
17690 window,
17691 cx,
17692 );
17693 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17694 assert_text_with_selections(
17695 editor,
17696 &indoc! {"
17697 ```rs
17698 impl Worktree {
17699 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
17700 }
17701 }
17702 ```
17703 "},
17704 cx,
17705 );
17706 });
17707}
17708
17709#[gpui::test]
17710async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17711 init_test(cx, |_| {});
17712
17713 let fs = FakeFs::new(cx.executor());
17714 fs.insert_tree(
17715 path!("/a"),
17716 json!({
17717 "main.rs": "fn main() { let a = 5; }",
17718 "other.rs": "// Test file",
17719 }),
17720 )
17721 .await;
17722 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17723
17724 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17725 language_registry.add(Arc::new(Language::new(
17726 LanguageConfig {
17727 name: "Rust".into(),
17728 matcher: LanguageMatcher {
17729 path_suffixes: vec!["rs".to_string()],
17730 ..Default::default()
17731 },
17732 brackets: BracketPairConfig {
17733 pairs: vec![BracketPair {
17734 start: "{".to_string(),
17735 end: "}".to_string(),
17736 close: true,
17737 surround: true,
17738 newline: true,
17739 }],
17740 disabled_scopes_by_bracket_ix: Vec::new(),
17741 },
17742 ..Default::default()
17743 },
17744 Some(tree_sitter_rust::LANGUAGE.into()),
17745 )));
17746 let mut fake_servers = language_registry.register_fake_lsp(
17747 "Rust",
17748 FakeLspAdapter {
17749 capabilities: lsp::ServerCapabilities {
17750 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17751 first_trigger_character: "{".to_string(),
17752 more_trigger_character: None,
17753 }),
17754 ..Default::default()
17755 },
17756 ..Default::default()
17757 },
17758 );
17759
17760 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17761
17762 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17763
17764 let worktree_id = workspace
17765 .update(cx, |workspace, _, cx| {
17766 workspace.project().update(cx, |project, cx| {
17767 project.worktrees(cx).next().unwrap().read(cx).id()
17768 })
17769 })
17770 .unwrap();
17771
17772 let buffer = project
17773 .update(cx, |project, cx| {
17774 project.open_local_buffer(path!("/a/main.rs"), cx)
17775 })
17776 .await
17777 .unwrap();
17778 let editor_handle = workspace
17779 .update(cx, |workspace, window, cx| {
17780 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17781 })
17782 .unwrap()
17783 .await
17784 .unwrap()
17785 .downcast::<Editor>()
17786 .unwrap();
17787
17788 cx.executor().start_waiting();
17789 let fake_server = fake_servers.next().await.unwrap();
17790
17791 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17792 |params, _| async move {
17793 assert_eq!(
17794 params.text_document_position.text_document.uri,
17795 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17796 );
17797 assert_eq!(
17798 params.text_document_position.position,
17799 lsp::Position::new(0, 21),
17800 );
17801
17802 Ok(Some(vec![lsp::TextEdit {
17803 new_text: "]".to_string(),
17804 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17805 }]))
17806 },
17807 );
17808
17809 editor_handle.update_in(cx, |editor, window, cx| {
17810 window.focus(&editor.focus_handle(cx));
17811 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17812 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17813 });
17814 editor.handle_input("{", window, cx);
17815 });
17816
17817 cx.executor().run_until_parked();
17818
17819 buffer.update(cx, |buffer, _| {
17820 assert_eq!(
17821 buffer.text(),
17822 "fn main() { let a = {5}; }",
17823 "No extra braces from on type formatting should appear in the buffer"
17824 )
17825 });
17826}
17827
17828#[gpui::test(iterations = 20, seeds(31))]
17829async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17830 init_test(cx, |_| {});
17831
17832 let mut cx = EditorLspTestContext::new_rust(
17833 lsp::ServerCapabilities {
17834 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17835 first_trigger_character: ".".to_string(),
17836 more_trigger_character: None,
17837 }),
17838 ..Default::default()
17839 },
17840 cx,
17841 )
17842 .await;
17843
17844 cx.update_buffer(|buffer, _| {
17845 // This causes autoindent to be async.
17846 buffer.set_sync_parse_timeout(Duration::ZERO)
17847 });
17848
17849 cx.set_state("fn c() {\n d()ˇ\n}\n");
17850 cx.simulate_keystroke("\n");
17851 cx.run_until_parked();
17852
17853 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17854 let mut request =
17855 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17856 let buffer_cloned = buffer_cloned.clone();
17857 async move {
17858 buffer_cloned.update(&mut cx, |buffer, _| {
17859 assert_eq!(
17860 buffer.text(),
17861 "fn c() {\n d()\n .\n}\n",
17862 "OnTypeFormatting should triggered after autoindent applied"
17863 )
17864 })?;
17865
17866 Ok(Some(vec![]))
17867 }
17868 });
17869
17870 cx.simulate_keystroke(".");
17871 cx.run_until_parked();
17872
17873 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17874 assert!(request.next().await.is_some());
17875 request.close();
17876 assert!(request.next().await.is_none());
17877}
17878
17879#[gpui::test]
17880async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17881 init_test(cx, |_| {});
17882
17883 let fs = FakeFs::new(cx.executor());
17884 fs.insert_tree(
17885 path!("/a"),
17886 json!({
17887 "main.rs": "fn main() { let a = 5; }",
17888 "other.rs": "// Test file",
17889 }),
17890 )
17891 .await;
17892
17893 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17894
17895 let server_restarts = Arc::new(AtomicUsize::new(0));
17896 let closure_restarts = Arc::clone(&server_restarts);
17897 let language_server_name = "test language server";
17898 let language_name: LanguageName = "Rust".into();
17899
17900 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17901 language_registry.add(Arc::new(Language::new(
17902 LanguageConfig {
17903 name: language_name.clone(),
17904 matcher: LanguageMatcher {
17905 path_suffixes: vec!["rs".to_string()],
17906 ..Default::default()
17907 },
17908 ..Default::default()
17909 },
17910 Some(tree_sitter_rust::LANGUAGE.into()),
17911 )));
17912 let mut fake_servers = language_registry.register_fake_lsp(
17913 "Rust",
17914 FakeLspAdapter {
17915 name: language_server_name,
17916 initialization_options: Some(json!({
17917 "testOptionValue": true
17918 })),
17919 initializer: Some(Box::new(move |fake_server| {
17920 let task_restarts = Arc::clone(&closure_restarts);
17921 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17922 task_restarts.fetch_add(1, atomic::Ordering::Release);
17923 futures::future::ready(Ok(()))
17924 });
17925 })),
17926 ..Default::default()
17927 },
17928 );
17929
17930 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17931 let _buffer = project
17932 .update(cx, |project, cx| {
17933 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17934 })
17935 .await
17936 .unwrap();
17937 let _fake_server = fake_servers.next().await.unwrap();
17938 update_test_language_settings(cx, |language_settings| {
17939 language_settings.languages.0.insert(
17940 language_name.clone().0,
17941 LanguageSettingsContent {
17942 tab_size: NonZeroU32::new(8),
17943 ..Default::default()
17944 },
17945 );
17946 });
17947 cx.executor().run_until_parked();
17948 assert_eq!(
17949 server_restarts.load(atomic::Ordering::Acquire),
17950 0,
17951 "Should not restart LSP server on an unrelated change"
17952 );
17953
17954 update_test_project_settings(cx, |project_settings| {
17955 project_settings.lsp.insert(
17956 "Some other server name".into(),
17957 LspSettings {
17958 binary: None,
17959 settings: None,
17960 initialization_options: Some(json!({
17961 "some other init value": false
17962 })),
17963 enable_lsp_tasks: false,
17964 fetch: None,
17965 },
17966 );
17967 });
17968 cx.executor().run_until_parked();
17969 assert_eq!(
17970 server_restarts.load(atomic::Ordering::Acquire),
17971 0,
17972 "Should not restart LSP server on an unrelated LSP settings change"
17973 );
17974
17975 update_test_project_settings(cx, |project_settings| {
17976 project_settings.lsp.insert(
17977 language_server_name.into(),
17978 LspSettings {
17979 binary: None,
17980 settings: None,
17981 initialization_options: Some(json!({
17982 "anotherInitValue": false
17983 })),
17984 enable_lsp_tasks: false,
17985 fetch: None,
17986 },
17987 );
17988 });
17989 cx.executor().run_until_parked();
17990 assert_eq!(
17991 server_restarts.load(atomic::Ordering::Acquire),
17992 1,
17993 "Should restart LSP server on a related LSP settings change"
17994 );
17995
17996 update_test_project_settings(cx, |project_settings| {
17997 project_settings.lsp.insert(
17998 language_server_name.into(),
17999 LspSettings {
18000 binary: None,
18001 settings: None,
18002 initialization_options: Some(json!({
18003 "anotherInitValue": false
18004 })),
18005 enable_lsp_tasks: false,
18006 fetch: None,
18007 },
18008 );
18009 });
18010 cx.executor().run_until_parked();
18011 assert_eq!(
18012 server_restarts.load(atomic::Ordering::Acquire),
18013 1,
18014 "Should not restart LSP server on a related LSP settings change that is the same"
18015 );
18016
18017 update_test_project_settings(cx, |project_settings| {
18018 project_settings.lsp.insert(
18019 language_server_name.into(),
18020 LspSettings {
18021 binary: None,
18022 settings: None,
18023 initialization_options: None,
18024 enable_lsp_tasks: false,
18025 fetch: None,
18026 },
18027 );
18028 });
18029 cx.executor().run_until_parked();
18030 assert_eq!(
18031 server_restarts.load(atomic::Ordering::Acquire),
18032 2,
18033 "Should restart LSP server on another related LSP settings change"
18034 );
18035}
18036
18037#[gpui::test]
18038async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18039 init_test(cx, |_| {});
18040
18041 let mut cx = EditorLspTestContext::new_rust(
18042 lsp::ServerCapabilities {
18043 completion_provider: Some(lsp::CompletionOptions {
18044 trigger_characters: Some(vec![".".to_string()]),
18045 resolve_provider: Some(true),
18046 ..Default::default()
18047 }),
18048 ..Default::default()
18049 },
18050 cx,
18051 )
18052 .await;
18053
18054 cx.set_state("fn main() { let a = 2ˇ; }");
18055 cx.simulate_keystroke(".");
18056 let completion_item = lsp::CompletionItem {
18057 label: "some".into(),
18058 kind: Some(lsp::CompletionItemKind::SNIPPET),
18059 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18060 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18061 kind: lsp::MarkupKind::Markdown,
18062 value: "```rust\nSome(2)\n```".to_string(),
18063 })),
18064 deprecated: Some(false),
18065 sort_text: Some("fffffff2".to_string()),
18066 filter_text: Some("some".to_string()),
18067 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18068 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18069 range: lsp::Range {
18070 start: lsp::Position {
18071 line: 0,
18072 character: 22,
18073 },
18074 end: lsp::Position {
18075 line: 0,
18076 character: 22,
18077 },
18078 },
18079 new_text: "Some(2)".to_string(),
18080 })),
18081 additional_text_edits: Some(vec![lsp::TextEdit {
18082 range: lsp::Range {
18083 start: lsp::Position {
18084 line: 0,
18085 character: 20,
18086 },
18087 end: lsp::Position {
18088 line: 0,
18089 character: 22,
18090 },
18091 },
18092 new_text: "".to_string(),
18093 }]),
18094 ..Default::default()
18095 };
18096
18097 let closure_completion_item = completion_item.clone();
18098 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18099 let task_completion_item = closure_completion_item.clone();
18100 async move {
18101 Ok(Some(lsp::CompletionResponse::Array(vec![
18102 task_completion_item,
18103 ])))
18104 }
18105 });
18106
18107 request.next().await;
18108
18109 cx.condition(|editor, _| editor.context_menu_visible())
18110 .await;
18111 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18112 editor
18113 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18114 .unwrap()
18115 });
18116 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18117
18118 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18119 let task_completion_item = completion_item.clone();
18120 async move { Ok(task_completion_item) }
18121 })
18122 .next()
18123 .await
18124 .unwrap();
18125 apply_additional_edits.await.unwrap();
18126 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18127}
18128
18129#[gpui::test]
18130async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18131 init_test(cx, |_| {});
18132
18133 let mut cx = EditorLspTestContext::new_rust(
18134 lsp::ServerCapabilities {
18135 completion_provider: Some(lsp::CompletionOptions {
18136 trigger_characters: Some(vec![".".to_string()]),
18137 resolve_provider: Some(true),
18138 ..Default::default()
18139 }),
18140 ..Default::default()
18141 },
18142 cx,
18143 )
18144 .await;
18145
18146 cx.set_state("fn main() { let a = 2ˇ; }");
18147 cx.simulate_keystroke(".");
18148
18149 let item1 = lsp::CompletionItem {
18150 label: "method id()".to_string(),
18151 filter_text: Some("id".to_string()),
18152 detail: None,
18153 documentation: None,
18154 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18155 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18156 new_text: ".id".to_string(),
18157 })),
18158 ..lsp::CompletionItem::default()
18159 };
18160
18161 let item2 = lsp::CompletionItem {
18162 label: "other".to_string(),
18163 filter_text: Some("other".to_string()),
18164 detail: None,
18165 documentation: None,
18166 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18167 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18168 new_text: ".other".to_string(),
18169 })),
18170 ..lsp::CompletionItem::default()
18171 };
18172
18173 let item1 = item1.clone();
18174 cx.set_request_handler::<lsp::request::Completion, _, _>({
18175 let item1 = item1.clone();
18176 move |_, _, _| {
18177 let item1 = item1.clone();
18178 let item2 = item2.clone();
18179 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18180 }
18181 })
18182 .next()
18183 .await;
18184
18185 cx.condition(|editor, _| editor.context_menu_visible())
18186 .await;
18187 cx.update_editor(|editor, _, _| {
18188 let context_menu = editor.context_menu.borrow_mut();
18189 let context_menu = context_menu
18190 .as_ref()
18191 .expect("Should have the context menu deployed");
18192 match context_menu {
18193 CodeContextMenu::Completions(completions_menu) => {
18194 let completions = completions_menu.completions.borrow_mut();
18195 assert_eq!(
18196 completions
18197 .iter()
18198 .map(|completion| &completion.label.text)
18199 .collect::<Vec<_>>(),
18200 vec!["method id()", "other"]
18201 )
18202 }
18203 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18204 }
18205 });
18206
18207 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18208 let item1 = item1.clone();
18209 move |_, item_to_resolve, _| {
18210 let item1 = item1.clone();
18211 async move {
18212 if item1 == item_to_resolve {
18213 Ok(lsp::CompletionItem {
18214 label: "method id()".to_string(),
18215 filter_text: Some("id".to_string()),
18216 detail: Some("Now resolved!".to_string()),
18217 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18218 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18219 range: lsp::Range::new(
18220 lsp::Position::new(0, 22),
18221 lsp::Position::new(0, 22),
18222 ),
18223 new_text: ".id".to_string(),
18224 })),
18225 ..lsp::CompletionItem::default()
18226 })
18227 } else {
18228 Ok(item_to_resolve)
18229 }
18230 }
18231 }
18232 })
18233 .next()
18234 .await
18235 .unwrap();
18236 cx.run_until_parked();
18237
18238 cx.update_editor(|editor, window, cx| {
18239 editor.context_menu_next(&Default::default(), window, cx);
18240 });
18241
18242 cx.update_editor(|editor, _, _| {
18243 let context_menu = editor.context_menu.borrow_mut();
18244 let context_menu = context_menu
18245 .as_ref()
18246 .expect("Should have the context menu deployed");
18247 match context_menu {
18248 CodeContextMenu::Completions(completions_menu) => {
18249 let completions = completions_menu.completions.borrow_mut();
18250 assert_eq!(
18251 completions
18252 .iter()
18253 .map(|completion| &completion.label.text)
18254 .collect::<Vec<_>>(),
18255 vec!["method id() Now resolved!", "other"],
18256 "Should update first completion label, but not second as the filter text did not match."
18257 );
18258 }
18259 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18260 }
18261 });
18262}
18263
18264#[gpui::test]
18265async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18266 init_test(cx, |_| {});
18267 let mut cx = EditorLspTestContext::new_rust(
18268 lsp::ServerCapabilities {
18269 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18270 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18271 completion_provider: Some(lsp::CompletionOptions {
18272 resolve_provider: Some(true),
18273 ..Default::default()
18274 }),
18275 ..Default::default()
18276 },
18277 cx,
18278 )
18279 .await;
18280 cx.set_state(indoc! {"
18281 struct TestStruct {
18282 field: i32
18283 }
18284
18285 fn mainˇ() {
18286 let unused_var = 42;
18287 let test_struct = TestStruct { field: 42 };
18288 }
18289 "});
18290 let symbol_range = cx.lsp_range(indoc! {"
18291 struct TestStruct {
18292 field: i32
18293 }
18294
18295 «fn main»() {
18296 let unused_var = 42;
18297 let test_struct = TestStruct { field: 42 };
18298 }
18299 "});
18300 let mut hover_requests =
18301 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18302 Ok(Some(lsp::Hover {
18303 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18304 kind: lsp::MarkupKind::Markdown,
18305 value: "Function documentation".to_string(),
18306 }),
18307 range: Some(symbol_range),
18308 }))
18309 });
18310
18311 // Case 1: Test that code action menu hide hover popover
18312 cx.dispatch_action(Hover);
18313 hover_requests.next().await;
18314 cx.condition(|editor, _| editor.hover_state.visible()).await;
18315 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18316 move |_, _, _| async move {
18317 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18318 lsp::CodeAction {
18319 title: "Remove unused variable".to_string(),
18320 kind: Some(CodeActionKind::QUICKFIX),
18321 edit: Some(lsp::WorkspaceEdit {
18322 changes: Some(
18323 [(
18324 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18325 vec![lsp::TextEdit {
18326 range: lsp::Range::new(
18327 lsp::Position::new(5, 4),
18328 lsp::Position::new(5, 27),
18329 ),
18330 new_text: "".to_string(),
18331 }],
18332 )]
18333 .into_iter()
18334 .collect(),
18335 ),
18336 ..Default::default()
18337 }),
18338 ..Default::default()
18339 },
18340 )]))
18341 },
18342 );
18343 cx.update_editor(|editor, window, cx| {
18344 editor.toggle_code_actions(
18345 &ToggleCodeActions {
18346 deployed_from: None,
18347 quick_launch: false,
18348 },
18349 window,
18350 cx,
18351 );
18352 });
18353 code_action_requests.next().await;
18354 cx.run_until_parked();
18355 cx.condition(|editor, _| editor.context_menu_visible())
18356 .await;
18357 cx.update_editor(|editor, _, _| {
18358 assert!(
18359 !editor.hover_state.visible(),
18360 "Hover popover should be hidden when code action menu is shown"
18361 );
18362 // Hide code actions
18363 editor.context_menu.take();
18364 });
18365
18366 // Case 2: Test that code completions hide hover popover
18367 cx.dispatch_action(Hover);
18368 hover_requests.next().await;
18369 cx.condition(|editor, _| editor.hover_state.visible()).await;
18370 let counter = Arc::new(AtomicUsize::new(0));
18371 let mut completion_requests =
18372 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18373 let counter = counter.clone();
18374 async move {
18375 counter.fetch_add(1, atomic::Ordering::Release);
18376 Ok(Some(lsp::CompletionResponse::Array(vec![
18377 lsp::CompletionItem {
18378 label: "main".into(),
18379 kind: Some(lsp::CompletionItemKind::FUNCTION),
18380 detail: Some("() -> ()".to_string()),
18381 ..Default::default()
18382 },
18383 lsp::CompletionItem {
18384 label: "TestStruct".into(),
18385 kind: Some(lsp::CompletionItemKind::STRUCT),
18386 detail: Some("struct TestStruct".to_string()),
18387 ..Default::default()
18388 },
18389 ])))
18390 }
18391 });
18392 cx.update_editor(|editor, window, cx| {
18393 editor.show_completions(&ShowCompletions, window, cx);
18394 });
18395 completion_requests.next().await;
18396 cx.condition(|editor, _| editor.context_menu_visible())
18397 .await;
18398 cx.update_editor(|editor, _, _| {
18399 assert!(
18400 !editor.hover_state.visible(),
18401 "Hover popover should be hidden when completion menu is shown"
18402 );
18403 });
18404}
18405
18406#[gpui::test]
18407async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18408 init_test(cx, |_| {});
18409
18410 let mut cx = EditorLspTestContext::new_rust(
18411 lsp::ServerCapabilities {
18412 completion_provider: Some(lsp::CompletionOptions {
18413 trigger_characters: Some(vec![".".to_string()]),
18414 resolve_provider: Some(true),
18415 ..Default::default()
18416 }),
18417 ..Default::default()
18418 },
18419 cx,
18420 )
18421 .await;
18422
18423 cx.set_state("fn main() { let a = 2ˇ; }");
18424 cx.simulate_keystroke(".");
18425
18426 let unresolved_item_1 = lsp::CompletionItem {
18427 label: "id".to_string(),
18428 filter_text: Some("id".to_string()),
18429 detail: None,
18430 documentation: None,
18431 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18432 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18433 new_text: ".id".to_string(),
18434 })),
18435 ..lsp::CompletionItem::default()
18436 };
18437 let resolved_item_1 = lsp::CompletionItem {
18438 additional_text_edits: Some(vec![lsp::TextEdit {
18439 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18440 new_text: "!!".to_string(),
18441 }]),
18442 ..unresolved_item_1.clone()
18443 };
18444 let unresolved_item_2 = lsp::CompletionItem {
18445 label: "other".to_string(),
18446 filter_text: Some("other".to_string()),
18447 detail: None,
18448 documentation: None,
18449 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18450 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18451 new_text: ".other".to_string(),
18452 })),
18453 ..lsp::CompletionItem::default()
18454 };
18455 let resolved_item_2 = lsp::CompletionItem {
18456 additional_text_edits: Some(vec![lsp::TextEdit {
18457 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18458 new_text: "??".to_string(),
18459 }]),
18460 ..unresolved_item_2.clone()
18461 };
18462
18463 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18464 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18465 cx.lsp
18466 .server
18467 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18468 let unresolved_item_1 = unresolved_item_1.clone();
18469 let resolved_item_1 = resolved_item_1.clone();
18470 let unresolved_item_2 = unresolved_item_2.clone();
18471 let resolved_item_2 = resolved_item_2.clone();
18472 let resolve_requests_1 = resolve_requests_1.clone();
18473 let resolve_requests_2 = resolve_requests_2.clone();
18474 move |unresolved_request, _| {
18475 let unresolved_item_1 = unresolved_item_1.clone();
18476 let resolved_item_1 = resolved_item_1.clone();
18477 let unresolved_item_2 = unresolved_item_2.clone();
18478 let resolved_item_2 = resolved_item_2.clone();
18479 let resolve_requests_1 = resolve_requests_1.clone();
18480 let resolve_requests_2 = resolve_requests_2.clone();
18481 async move {
18482 if unresolved_request == unresolved_item_1 {
18483 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18484 Ok(resolved_item_1.clone())
18485 } else if unresolved_request == unresolved_item_2 {
18486 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18487 Ok(resolved_item_2.clone())
18488 } else {
18489 panic!("Unexpected completion item {unresolved_request:?}")
18490 }
18491 }
18492 }
18493 })
18494 .detach();
18495
18496 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18497 let unresolved_item_1 = unresolved_item_1.clone();
18498 let unresolved_item_2 = unresolved_item_2.clone();
18499 async move {
18500 Ok(Some(lsp::CompletionResponse::Array(vec![
18501 unresolved_item_1,
18502 unresolved_item_2,
18503 ])))
18504 }
18505 })
18506 .next()
18507 .await;
18508
18509 cx.condition(|editor, _| editor.context_menu_visible())
18510 .await;
18511 cx.update_editor(|editor, _, _| {
18512 let context_menu = editor.context_menu.borrow_mut();
18513 let context_menu = context_menu
18514 .as_ref()
18515 .expect("Should have the context menu deployed");
18516 match context_menu {
18517 CodeContextMenu::Completions(completions_menu) => {
18518 let completions = completions_menu.completions.borrow_mut();
18519 assert_eq!(
18520 completions
18521 .iter()
18522 .map(|completion| &completion.label.text)
18523 .collect::<Vec<_>>(),
18524 vec!["id", "other"]
18525 )
18526 }
18527 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18528 }
18529 });
18530 cx.run_until_parked();
18531
18532 cx.update_editor(|editor, window, cx| {
18533 editor.context_menu_next(&ContextMenuNext, window, cx);
18534 });
18535 cx.run_until_parked();
18536 cx.update_editor(|editor, window, cx| {
18537 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18538 });
18539 cx.run_until_parked();
18540 cx.update_editor(|editor, window, cx| {
18541 editor.context_menu_next(&ContextMenuNext, window, cx);
18542 });
18543 cx.run_until_parked();
18544 cx.update_editor(|editor, window, cx| {
18545 editor
18546 .compose_completion(&ComposeCompletion::default(), window, cx)
18547 .expect("No task returned")
18548 })
18549 .await
18550 .expect("Completion failed");
18551 cx.run_until_parked();
18552
18553 cx.update_editor(|editor, _, cx| {
18554 assert_eq!(
18555 resolve_requests_1.load(atomic::Ordering::Acquire),
18556 1,
18557 "Should always resolve once despite multiple selections"
18558 );
18559 assert_eq!(
18560 resolve_requests_2.load(atomic::Ordering::Acquire),
18561 1,
18562 "Should always resolve once after multiple selections and applying the completion"
18563 );
18564 assert_eq!(
18565 editor.text(cx),
18566 "fn main() { let a = ??.other; }",
18567 "Should use resolved data when applying the completion"
18568 );
18569 });
18570}
18571
18572#[gpui::test]
18573async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18574 init_test(cx, |_| {});
18575
18576 let item_0 = lsp::CompletionItem {
18577 label: "abs".into(),
18578 insert_text: Some("abs".into()),
18579 data: Some(json!({ "very": "special"})),
18580 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18581 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18582 lsp::InsertReplaceEdit {
18583 new_text: "abs".to_string(),
18584 insert: lsp::Range::default(),
18585 replace: lsp::Range::default(),
18586 },
18587 )),
18588 ..lsp::CompletionItem::default()
18589 };
18590 let items = iter::once(item_0.clone())
18591 .chain((11..51).map(|i| lsp::CompletionItem {
18592 label: format!("item_{}", i),
18593 insert_text: Some(format!("item_{}", i)),
18594 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18595 ..lsp::CompletionItem::default()
18596 }))
18597 .collect::<Vec<_>>();
18598
18599 let default_commit_characters = vec!["?".to_string()];
18600 let default_data = json!({ "default": "data"});
18601 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18602 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18603 let default_edit_range = lsp::Range {
18604 start: lsp::Position {
18605 line: 0,
18606 character: 5,
18607 },
18608 end: lsp::Position {
18609 line: 0,
18610 character: 5,
18611 },
18612 };
18613
18614 let mut cx = EditorLspTestContext::new_rust(
18615 lsp::ServerCapabilities {
18616 completion_provider: Some(lsp::CompletionOptions {
18617 trigger_characters: Some(vec![".".to_string()]),
18618 resolve_provider: Some(true),
18619 ..Default::default()
18620 }),
18621 ..Default::default()
18622 },
18623 cx,
18624 )
18625 .await;
18626
18627 cx.set_state("fn main() { let a = 2ˇ; }");
18628 cx.simulate_keystroke(".");
18629
18630 let completion_data = default_data.clone();
18631 let completion_characters = default_commit_characters.clone();
18632 let completion_items = items.clone();
18633 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18634 let default_data = completion_data.clone();
18635 let default_commit_characters = completion_characters.clone();
18636 let items = completion_items.clone();
18637 async move {
18638 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18639 items,
18640 item_defaults: Some(lsp::CompletionListItemDefaults {
18641 data: Some(default_data.clone()),
18642 commit_characters: Some(default_commit_characters.clone()),
18643 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18644 default_edit_range,
18645 )),
18646 insert_text_format: Some(default_insert_text_format),
18647 insert_text_mode: Some(default_insert_text_mode),
18648 }),
18649 ..lsp::CompletionList::default()
18650 })))
18651 }
18652 })
18653 .next()
18654 .await;
18655
18656 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18657 cx.lsp
18658 .server
18659 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18660 let closure_resolved_items = resolved_items.clone();
18661 move |item_to_resolve, _| {
18662 let closure_resolved_items = closure_resolved_items.clone();
18663 async move {
18664 closure_resolved_items.lock().push(item_to_resolve.clone());
18665 Ok(item_to_resolve)
18666 }
18667 }
18668 })
18669 .detach();
18670
18671 cx.condition(|editor, _| editor.context_menu_visible())
18672 .await;
18673 cx.run_until_parked();
18674 cx.update_editor(|editor, _, _| {
18675 let menu = editor.context_menu.borrow_mut();
18676 match menu.as_ref().expect("should have the completions menu") {
18677 CodeContextMenu::Completions(completions_menu) => {
18678 assert_eq!(
18679 completions_menu
18680 .entries
18681 .borrow()
18682 .iter()
18683 .map(|mat| mat.string.clone())
18684 .collect::<Vec<String>>(),
18685 items
18686 .iter()
18687 .map(|completion| completion.label.clone())
18688 .collect::<Vec<String>>()
18689 );
18690 }
18691 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18692 }
18693 });
18694 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18695 // with 4 from the end.
18696 assert_eq!(
18697 *resolved_items.lock(),
18698 [&items[0..16], &items[items.len() - 4..items.len()]]
18699 .concat()
18700 .iter()
18701 .cloned()
18702 .map(|mut item| {
18703 if item.data.is_none() {
18704 item.data = Some(default_data.clone());
18705 }
18706 item
18707 })
18708 .collect::<Vec<lsp::CompletionItem>>(),
18709 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18710 );
18711 resolved_items.lock().clear();
18712
18713 cx.update_editor(|editor, window, cx| {
18714 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18715 });
18716 cx.run_until_parked();
18717 // Completions that have already been resolved are skipped.
18718 assert_eq!(
18719 *resolved_items.lock(),
18720 items[items.len() - 17..items.len() - 4]
18721 .iter()
18722 .cloned()
18723 .map(|mut item| {
18724 if item.data.is_none() {
18725 item.data = Some(default_data.clone());
18726 }
18727 item
18728 })
18729 .collect::<Vec<lsp::CompletionItem>>()
18730 );
18731 resolved_items.lock().clear();
18732}
18733
18734#[gpui::test]
18735async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18736 init_test(cx, |_| {});
18737
18738 let mut cx = EditorLspTestContext::new(
18739 Language::new(
18740 LanguageConfig {
18741 matcher: LanguageMatcher {
18742 path_suffixes: vec!["jsx".into()],
18743 ..Default::default()
18744 },
18745 overrides: [(
18746 "element".into(),
18747 LanguageConfigOverride {
18748 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18749 ..Default::default()
18750 },
18751 )]
18752 .into_iter()
18753 .collect(),
18754 ..Default::default()
18755 },
18756 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18757 )
18758 .with_override_query("(jsx_self_closing_element) @element")
18759 .unwrap(),
18760 lsp::ServerCapabilities {
18761 completion_provider: Some(lsp::CompletionOptions {
18762 trigger_characters: Some(vec![":".to_string()]),
18763 ..Default::default()
18764 }),
18765 ..Default::default()
18766 },
18767 cx,
18768 )
18769 .await;
18770
18771 cx.lsp
18772 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18773 Ok(Some(lsp::CompletionResponse::Array(vec![
18774 lsp::CompletionItem {
18775 label: "bg-blue".into(),
18776 ..Default::default()
18777 },
18778 lsp::CompletionItem {
18779 label: "bg-red".into(),
18780 ..Default::default()
18781 },
18782 lsp::CompletionItem {
18783 label: "bg-yellow".into(),
18784 ..Default::default()
18785 },
18786 ])))
18787 });
18788
18789 cx.set_state(r#"<p class="bgˇ" />"#);
18790
18791 // Trigger completion when typing a dash, because the dash is an extra
18792 // word character in the 'element' scope, which contains the cursor.
18793 cx.simulate_keystroke("-");
18794 cx.executor().run_until_parked();
18795 cx.update_editor(|editor, _, _| {
18796 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18797 {
18798 assert_eq!(
18799 completion_menu_entries(menu),
18800 &["bg-blue", "bg-red", "bg-yellow"]
18801 );
18802 } else {
18803 panic!("expected completion menu to be open");
18804 }
18805 });
18806
18807 cx.simulate_keystroke("l");
18808 cx.executor().run_until_parked();
18809 cx.update_editor(|editor, _, _| {
18810 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18811 {
18812 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18813 } else {
18814 panic!("expected completion menu to be open");
18815 }
18816 });
18817
18818 // When filtering completions, consider the character after the '-' to
18819 // be the start of a subword.
18820 cx.set_state(r#"<p class="yelˇ" />"#);
18821 cx.simulate_keystroke("l");
18822 cx.executor().run_until_parked();
18823 cx.update_editor(|editor, _, _| {
18824 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18825 {
18826 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18827 } else {
18828 panic!("expected completion menu to be open");
18829 }
18830 });
18831}
18832
18833fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18834 let entries = menu.entries.borrow();
18835 entries.iter().map(|mat| mat.string.clone()).collect()
18836}
18837
18838#[gpui::test]
18839async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18840 init_test(cx, |settings| {
18841 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18842 });
18843
18844 let fs = FakeFs::new(cx.executor());
18845 fs.insert_file(path!("/file.ts"), Default::default()).await;
18846
18847 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18848 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18849
18850 language_registry.add(Arc::new(Language::new(
18851 LanguageConfig {
18852 name: "TypeScript".into(),
18853 matcher: LanguageMatcher {
18854 path_suffixes: vec!["ts".to_string()],
18855 ..Default::default()
18856 },
18857 ..Default::default()
18858 },
18859 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18860 )));
18861 update_test_language_settings(cx, |settings| {
18862 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18863 });
18864
18865 let test_plugin = "test_plugin";
18866 let _ = language_registry.register_fake_lsp(
18867 "TypeScript",
18868 FakeLspAdapter {
18869 prettier_plugins: vec![test_plugin],
18870 ..Default::default()
18871 },
18872 );
18873
18874 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18875 let buffer = project
18876 .update(cx, |project, cx| {
18877 project.open_local_buffer(path!("/file.ts"), cx)
18878 })
18879 .await
18880 .unwrap();
18881
18882 let buffer_text = "one\ntwo\nthree\n";
18883 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18884 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18885 editor.update_in(cx, |editor, window, cx| {
18886 editor.set_text(buffer_text, window, cx)
18887 });
18888
18889 editor
18890 .update_in(cx, |editor, window, cx| {
18891 editor.perform_format(
18892 project.clone(),
18893 FormatTrigger::Manual,
18894 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18895 window,
18896 cx,
18897 )
18898 })
18899 .unwrap()
18900 .await;
18901 assert_eq!(
18902 editor.update(cx, |editor, cx| editor.text(cx)),
18903 buffer_text.to_string() + prettier_format_suffix,
18904 "Test prettier formatting was not applied to the original buffer text",
18905 );
18906
18907 update_test_language_settings(cx, |settings| {
18908 settings.defaults.formatter = Some(FormatterList::default())
18909 });
18910 let format = editor.update_in(cx, |editor, window, cx| {
18911 editor.perform_format(
18912 project.clone(),
18913 FormatTrigger::Manual,
18914 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18915 window,
18916 cx,
18917 )
18918 });
18919 format.await.unwrap();
18920 assert_eq!(
18921 editor.update(cx, |editor, cx| editor.text(cx)),
18922 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18923 "Autoformatting (via test prettier) was not applied to the original buffer text",
18924 );
18925}
18926
18927#[gpui::test]
18928async fn test_addition_reverts(cx: &mut TestAppContext) {
18929 init_test(cx, |_| {});
18930 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18931 let base_text = indoc! {r#"
18932 struct Row;
18933 struct Row1;
18934 struct Row2;
18935
18936 struct Row4;
18937 struct Row5;
18938 struct Row6;
18939
18940 struct Row8;
18941 struct Row9;
18942 struct Row10;"#};
18943
18944 // When addition hunks are not adjacent to carets, no hunk revert is performed
18945 assert_hunk_revert(
18946 indoc! {r#"struct Row;
18947 struct Row1;
18948 struct Row1.1;
18949 struct Row1.2;
18950 struct Row2;ˇ
18951
18952 struct Row4;
18953 struct Row5;
18954 struct Row6;
18955
18956 struct Row8;
18957 ˇstruct Row9;
18958 struct Row9.1;
18959 struct Row9.2;
18960 struct Row9.3;
18961 struct Row10;"#},
18962 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18963 indoc! {r#"struct Row;
18964 struct Row1;
18965 struct Row1.1;
18966 struct Row1.2;
18967 struct Row2;ˇ
18968
18969 struct Row4;
18970 struct Row5;
18971 struct Row6;
18972
18973 struct Row8;
18974 ˇstruct Row9;
18975 struct Row9.1;
18976 struct Row9.2;
18977 struct Row9.3;
18978 struct Row10;"#},
18979 base_text,
18980 &mut cx,
18981 );
18982 // Same for selections
18983 assert_hunk_revert(
18984 indoc! {r#"struct Row;
18985 struct Row1;
18986 struct Row2;
18987 struct Row2.1;
18988 struct Row2.2;
18989 «ˇ
18990 struct Row4;
18991 struct» Row5;
18992 «struct Row6;
18993 ˇ»
18994 struct Row9.1;
18995 struct Row9.2;
18996 struct Row9.3;
18997 struct Row8;
18998 struct Row9;
18999 struct Row10;"#},
19000 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19001 indoc! {r#"struct Row;
19002 struct Row1;
19003 struct Row2;
19004 struct Row2.1;
19005 struct Row2.2;
19006 «ˇ
19007 struct Row4;
19008 struct» Row5;
19009 «struct Row6;
19010 ˇ»
19011 struct Row9.1;
19012 struct Row9.2;
19013 struct Row9.3;
19014 struct Row8;
19015 struct Row9;
19016 struct Row10;"#},
19017 base_text,
19018 &mut cx,
19019 );
19020
19021 // When carets and selections intersect the addition hunks, those are reverted.
19022 // Adjacent carets got merged.
19023 assert_hunk_revert(
19024 indoc! {r#"struct Row;
19025 ˇ// something on the top
19026 struct Row1;
19027 struct Row2;
19028 struct Roˇw3.1;
19029 struct Row2.2;
19030 struct Row2.3;ˇ
19031
19032 struct Row4;
19033 struct ˇRow5.1;
19034 struct Row5.2;
19035 struct «Rowˇ»5.3;
19036 struct Row5;
19037 struct Row6;
19038 ˇ
19039 struct Row9.1;
19040 struct «Rowˇ»9.2;
19041 struct «ˇRow»9.3;
19042 struct Row8;
19043 struct Row9;
19044 «ˇ// something on bottom»
19045 struct Row10;"#},
19046 vec![
19047 DiffHunkStatusKind::Added,
19048 DiffHunkStatusKind::Added,
19049 DiffHunkStatusKind::Added,
19050 DiffHunkStatusKind::Added,
19051 DiffHunkStatusKind::Added,
19052 ],
19053 indoc! {r#"struct Row;
19054 ˇstruct Row1;
19055 struct Row2;
19056 ˇ
19057 struct Row4;
19058 ˇstruct Row5;
19059 struct Row6;
19060 ˇ
19061 ˇstruct Row8;
19062 struct Row9;
19063 ˇstruct Row10;"#},
19064 base_text,
19065 &mut cx,
19066 );
19067}
19068
19069#[gpui::test]
19070async fn test_modification_reverts(cx: &mut TestAppContext) {
19071 init_test(cx, |_| {});
19072 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19073 let base_text = indoc! {r#"
19074 struct Row;
19075 struct Row1;
19076 struct Row2;
19077
19078 struct Row4;
19079 struct Row5;
19080 struct Row6;
19081
19082 struct Row8;
19083 struct Row9;
19084 struct Row10;"#};
19085
19086 // Modification hunks behave the same as the addition ones.
19087 assert_hunk_revert(
19088 indoc! {r#"struct Row;
19089 struct Row1;
19090 struct Row33;
19091 ˇ
19092 struct Row4;
19093 struct Row5;
19094 struct Row6;
19095 ˇ
19096 struct Row99;
19097 struct Row9;
19098 struct Row10;"#},
19099 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19100 indoc! {r#"struct Row;
19101 struct Row1;
19102 struct Row33;
19103 ˇ
19104 struct Row4;
19105 struct Row5;
19106 struct Row6;
19107 ˇ
19108 struct Row99;
19109 struct Row9;
19110 struct Row10;"#},
19111 base_text,
19112 &mut cx,
19113 );
19114 assert_hunk_revert(
19115 indoc! {r#"struct Row;
19116 struct Row1;
19117 struct Row33;
19118 «ˇ
19119 struct Row4;
19120 struct» Row5;
19121 «struct Row6;
19122 ˇ»
19123 struct Row99;
19124 struct Row9;
19125 struct Row10;"#},
19126 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19127 indoc! {r#"struct Row;
19128 struct Row1;
19129 struct Row33;
19130 «ˇ
19131 struct Row4;
19132 struct» Row5;
19133 «struct Row6;
19134 ˇ»
19135 struct Row99;
19136 struct Row9;
19137 struct Row10;"#},
19138 base_text,
19139 &mut cx,
19140 );
19141
19142 assert_hunk_revert(
19143 indoc! {r#"ˇstruct Row1.1;
19144 struct Row1;
19145 «ˇstr»uct Row22;
19146
19147 struct ˇRow44;
19148 struct Row5;
19149 struct «Rˇ»ow66;ˇ
19150
19151 «struˇ»ct Row88;
19152 struct Row9;
19153 struct Row1011;ˇ"#},
19154 vec![
19155 DiffHunkStatusKind::Modified,
19156 DiffHunkStatusKind::Modified,
19157 DiffHunkStatusKind::Modified,
19158 DiffHunkStatusKind::Modified,
19159 DiffHunkStatusKind::Modified,
19160 DiffHunkStatusKind::Modified,
19161 ],
19162 indoc! {r#"struct Row;
19163 ˇstruct Row1;
19164 struct Row2;
19165 ˇ
19166 struct Row4;
19167 ˇstruct Row5;
19168 struct Row6;
19169 ˇ
19170 struct Row8;
19171 ˇstruct Row9;
19172 struct Row10;ˇ"#},
19173 base_text,
19174 &mut cx,
19175 );
19176}
19177
19178#[gpui::test]
19179async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19180 init_test(cx, |_| {});
19181 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19182 let base_text = indoc! {r#"
19183 one
19184
19185 two
19186 three
19187 "#};
19188
19189 cx.set_head_text(base_text);
19190 cx.set_state("\nˇ\n");
19191 cx.executor().run_until_parked();
19192 cx.update_editor(|editor, _window, cx| {
19193 editor.expand_selected_diff_hunks(cx);
19194 });
19195 cx.executor().run_until_parked();
19196 cx.update_editor(|editor, window, cx| {
19197 editor.backspace(&Default::default(), window, cx);
19198 });
19199 cx.run_until_parked();
19200 cx.assert_state_with_diff(
19201 indoc! {r#"
19202
19203 - two
19204 - threeˇ
19205 +
19206 "#}
19207 .to_string(),
19208 );
19209}
19210
19211#[gpui::test]
19212async fn test_deletion_reverts(cx: &mut TestAppContext) {
19213 init_test(cx, |_| {});
19214 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19215 let base_text = indoc! {r#"struct Row;
19216struct Row1;
19217struct Row2;
19218
19219struct Row4;
19220struct Row5;
19221struct Row6;
19222
19223struct Row8;
19224struct Row9;
19225struct Row10;"#};
19226
19227 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19228 assert_hunk_revert(
19229 indoc! {r#"struct Row;
19230 struct Row2;
19231
19232 ˇstruct Row4;
19233 struct Row5;
19234 struct Row6;
19235 ˇ
19236 struct Row8;
19237 struct Row10;"#},
19238 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19239 indoc! {r#"struct Row;
19240 struct Row2;
19241
19242 ˇstruct Row4;
19243 struct Row5;
19244 struct Row6;
19245 ˇ
19246 struct Row8;
19247 struct Row10;"#},
19248 base_text,
19249 &mut cx,
19250 );
19251 assert_hunk_revert(
19252 indoc! {r#"struct Row;
19253 struct Row2;
19254
19255 «ˇstruct Row4;
19256 struct» Row5;
19257 «struct Row6;
19258 ˇ»
19259 struct Row8;
19260 struct Row10;"#},
19261 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19262 indoc! {r#"struct Row;
19263 struct Row2;
19264
19265 «ˇstruct Row4;
19266 struct» Row5;
19267 «struct Row6;
19268 ˇ»
19269 struct Row8;
19270 struct Row10;"#},
19271 base_text,
19272 &mut cx,
19273 );
19274
19275 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19276 assert_hunk_revert(
19277 indoc! {r#"struct Row;
19278 ˇstruct Row2;
19279
19280 struct Row4;
19281 struct Row5;
19282 struct Row6;
19283
19284 struct Row8;ˇ
19285 struct Row10;"#},
19286 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19287 indoc! {r#"struct Row;
19288 struct Row1;
19289 ˇstruct Row2;
19290
19291 struct Row4;
19292 struct Row5;
19293 struct Row6;
19294
19295 struct Row8;ˇ
19296 struct Row9;
19297 struct Row10;"#},
19298 base_text,
19299 &mut cx,
19300 );
19301 assert_hunk_revert(
19302 indoc! {r#"struct Row;
19303 struct Row2«ˇ;
19304 struct Row4;
19305 struct» Row5;
19306 «struct Row6;
19307
19308 struct Row8;ˇ»
19309 struct Row10;"#},
19310 vec![
19311 DiffHunkStatusKind::Deleted,
19312 DiffHunkStatusKind::Deleted,
19313 DiffHunkStatusKind::Deleted,
19314 ],
19315 indoc! {r#"struct Row;
19316 struct Row1;
19317 struct Row2«ˇ;
19318
19319 struct Row4;
19320 struct» Row5;
19321 «struct Row6;
19322
19323 struct Row8;ˇ»
19324 struct Row9;
19325 struct Row10;"#},
19326 base_text,
19327 &mut cx,
19328 );
19329}
19330
19331#[gpui::test]
19332async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19333 init_test(cx, |_| {});
19334
19335 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19336 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19337 let base_text_3 =
19338 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19339
19340 let text_1 = edit_first_char_of_every_line(base_text_1);
19341 let text_2 = edit_first_char_of_every_line(base_text_2);
19342 let text_3 = edit_first_char_of_every_line(base_text_3);
19343
19344 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19345 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19346 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19347
19348 let multibuffer = cx.new(|cx| {
19349 let mut multibuffer = MultiBuffer::new(ReadWrite);
19350 multibuffer.push_excerpts(
19351 buffer_1.clone(),
19352 [
19353 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19354 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19355 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19356 ],
19357 cx,
19358 );
19359 multibuffer.push_excerpts(
19360 buffer_2.clone(),
19361 [
19362 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19363 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19364 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19365 ],
19366 cx,
19367 );
19368 multibuffer.push_excerpts(
19369 buffer_3.clone(),
19370 [
19371 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19372 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19373 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19374 ],
19375 cx,
19376 );
19377 multibuffer
19378 });
19379
19380 let fs = FakeFs::new(cx.executor());
19381 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19382 let (editor, cx) = cx
19383 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19384 editor.update_in(cx, |editor, _window, cx| {
19385 for (buffer, diff_base) in [
19386 (buffer_1.clone(), base_text_1),
19387 (buffer_2.clone(), base_text_2),
19388 (buffer_3.clone(), base_text_3),
19389 ] {
19390 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19391 editor
19392 .buffer
19393 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19394 }
19395 });
19396 cx.executor().run_until_parked();
19397
19398 editor.update_in(cx, |editor, window, cx| {
19399 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}");
19400 editor.select_all(&SelectAll, window, cx);
19401 editor.git_restore(&Default::default(), window, cx);
19402 });
19403 cx.executor().run_until_parked();
19404
19405 // When all ranges are selected, all buffer hunks are reverted.
19406 editor.update(cx, |editor, cx| {
19407 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");
19408 });
19409 buffer_1.update(cx, |buffer, _| {
19410 assert_eq!(buffer.text(), base_text_1);
19411 });
19412 buffer_2.update(cx, |buffer, _| {
19413 assert_eq!(buffer.text(), base_text_2);
19414 });
19415 buffer_3.update(cx, |buffer, _| {
19416 assert_eq!(buffer.text(), base_text_3);
19417 });
19418
19419 editor.update_in(cx, |editor, window, cx| {
19420 editor.undo(&Default::default(), window, cx);
19421 });
19422
19423 editor.update_in(cx, |editor, window, cx| {
19424 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19425 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19426 });
19427 editor.git_restore(&Default::default(), window, cx);
19428 });
19429
19430 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19431 // but not affect buffer_2 and its related excerpts.
19432 editor.update(cx, |editor, cx| {
19433 assert_eq!(
19434 editor.text(cx),
19435 "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}"
19436 );
19437 });
19438 buffer_1.update(cx, |buffer, _| {
19439 assert_eq!(buffer.text(), base_text_1);
19440 });
19441 buffer_2.update(cx, |buffer, _| {
19442 assert_eq!(
19443 buffer.text(),
19444 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19445 );
19446 });
19447 buffer_3.update(cx, |buffer, _| {
19448 assert_eq!(
19449 buffer.text(),
19450 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19451 );
19452 });
19453
19454 fn edit_first_char_of_every_line(text: &str) -> String {
19455 text.split('\n')
19456 .map(|line| format!("X{}", &line[1..]))
19457 .collect::<Vec<_>>()
19458 .join("\n")
19459 }
19460}
19461
19462#[gpui::test]
19463async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19464 init_test(cx, |_| {});
19465
19466 let cols = 4;
19467 let rows = 10;
19468 let sample_text_1 = sample_text(rows, cols, 'a');
19469 assert_eq!(
19470 sample_text_1,
19471 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19472 );
19473 let sample_text_2 = sample_text(rows, cols, 'l');
19474 assert_eq!(
19475 sample_text_2,
19476 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19477 );
19478 let sample_text_3 = sample_text(rows, cols, 'v');
19479 assert_eq!(
19480 sample_text_3,
19481 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19482 );
19483
19484 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19485 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19486 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19487
19488 let multi_buffer = cx.new(|cx| {
19489 let mut multibuffer = MultiBuffer::new(ReadWrite);
19490 multibuffer.push_excerpts(
19491 buffer_1.clone(),
19492 [
19493 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19494 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19495 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19496 ],
19497 cx,
19498 );
19499 multibuffer.push_excerpts(
19500 buffer_2.clone(),
19501 [
19502 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19503 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19504 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19505 ],
19506 cx,
19507 );
19508 multibuffer.push_excerpts(
19509 buffer_3.clone(),
19510 [
19511 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19512 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19513 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19514 ],
19515 cx,
19516 );
19517 multibuffer
19518 });
19519
19520 let fs = FakeFs::new(cx.executor());
19521 fs.insert_tree(
19522 "/a",
19523 json!({
19524 "main.rs": sample_text_1,
19525 "other.rs": sample_text_2,
19526 "lib.rs": sample_text_3,
19527 }),
19528 )
19529 .await;
19530 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19531 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19532 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19533 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19534 Editor::new(
19535 EditorMode::full(),
19536 multi_buffer,
19537 Some(project.clone()),
19538 window,
19539 cx,
19540 )
19541 });
19542 let multibuffer_item_id = workspace
19543 .update(cx, |workspace, window, cx| {
19544 assert!(
19545 workspace.active_item(cx).is_none(),
19546 "active item should be None before the first item is added"
19547 );
19548 workspace.add_item_to_active_pane(
19549 Box::new(multi_buffer_editor.clone()),
19550 None,
19551 true,
19552 window,
19553 cx,
19554 );
19555 let active_item = workspace
19556 .active_item(cx)
19557 .expect("should have an active item after adding the multi buffer");
19558 assert_eq!(
19559 active_item.buffer_kind(cx),
19560 ItemBufferKind::Multibuffer,
19561 "A multi buffer was expected to active after adding"
19562 );
19563 active_item.item_id()
19564 })
19565 .unwrap();
19566 cx.executor().run_until_parked();
19567
19568 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19569 editor.change_selections(
19570 SelectionEffects::scroll(Autoscroll::Next),
19571 window,
19572 cx,
19573 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
19574 );
19575 editor.open_excerpts(&OpenExcerpts, window, cx);
19576 });
19577 cx.executor().run_until_parked();
19578 let first_item_id = workspace
19579 .update(cx, |workspace, window, cx| {
19580 let active_item = workspace
19581 .active_item(cx)
19582 .expect("should have an active item after navigating into the 1st buffer");
19583 let first_item_id = active_item.item_id();
19584 assert_ne!(
19585 first_item_id, multibuffer_item_id,
19586 "Should navigate into the 1st buffer and activate it"
19587 );
19588 assert_eq!(
19589 active_item.buffer_kind(cx),
19590 ItemBufferKind::Singleton,
19591 "New active item should be a singleton buffer"
19592 );
19593 assert_eq!(
19594 active_item
19595 .act_as::<Editor>(cx)
19596 .expect("should have navigated into an editor for the 1st buffer")
19597 .read(cx)
19598 .text(cx),
19599 sample_text_1
19600 );
19601
19602 workspace
19603 .go_back(workspace.active_pane().downgrade(), window, cx)
19604 .detach_and_log_err(cx);
19605
19606 first_item_id
19607 })
19608 .unwrap();
19609 cx.executor().run_until_parked();
19610 workspace
19611 .update(cx, |workspace, _, cx| {
19612 let active_item = workspace
19613 .active_item(cx)
19614 .expect("should have an active item after navigating back");
19615 assert_eq!(
19616 active_item.item_id(),
19617 multibuffer_item_id,
19618 "Should navigate back to the multi buffer"
19619 );
19620 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19621 })
19622 .unwrap();
19623
19624 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19625 editor.change_selections(
19626 SelectionEffects::scroll(Autoscroll::Next),
19627 window,
19628 cx,
19629 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
19630 );
19631 editor.open_excerpts(&OpenExcerpts, window, cx);
19632 });
19633 cx.executor().run_until_parked();
19634 let second_item_id = workspace
19635 .update(cx, |workspace, window, cx| {
19636 let active_item = workspace
19637 .active_item(cx)
19638 .expect("should have an active item after navigating into the 2nd buffer");
19639 let second_item_id = active_item.item_id();
19640 assert_ne!(
19641 second_item_id, multibuffer_item_id,
19642 "Should navigate away from the multibuffer"
19643 );
19644 assert_ne!(
19645 second_item_id, first_item_id,
19646 "Should navigate into the 2nd buffer and activate it"
19647 );
19648 assert_eq!(
19649 active_item.buffer_kind(cx),
19650 ItemBufferKind::Singleton,
19651 "New active item should be a singleton buffer"
19652 );
19653 assert_eq!(
19654 active_item
19655 .act_as::<Editor>(cx)
19656 .expect("should have navigated into an editor")
19657 .read(cx)
19658 .text(cx),
19659 sample_text_2
19660 );
19661
19662 workspace
19663 .go_back(workspace.active_pane().downgrade(), window, cx)
19664 .detach_and_log_err(cx);
19665
19666 second_item_id
19667 })
19668 .unwrap();
19669 cx.executor().run_until_parked();
19670 workspace
19671 .update(cx, |workspace, _, cx| {
19672 let active_item = workspace
19673 .active_item(cx)
19674 .expect("should have an active item after navigating back from the 2nd buffer");
19675 assert_eq!(
19676 active_item.item_id(),
19677 multibuffer_item_id,
19678 "Should navigate back from the 2nd buffer to the multi buffer"
19679 );
19680 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19681 })
19682 .unwrap();
19683
19684 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19685 editor.change_selections(
19686 SelectionEffects::scroll(Autoscroll::Next),
19687 window,
19688 cx,
19689 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
19690 );
19691 editor.open_excerpts(&OpenExcerpts, window, cx);
19692 });
19693 cx.executor().run_until_parked();
19694 workspace
19695 .update(cx, |workspace, window, cx| {
19696 let active_item = workspace
19697 .active_item(cx)
19698 .expect("should have an active item after navigating into the 3rd buffer");
19699 let third_item_id = active_item.item_id();
19700 assert_ne!(
19701 third_item_id, multibuffer_item_id,
19702 "Should navigate into the 3rd buffer and activate it"
19703 );
19704 assert_ne!(third_item_id, first_item_id);
19705 assert_ne!(third_item_id, second_item_id);
19706 assert_eq!(
19707 active_item.buffer_kind(cx),
19708 ItemBufferKind::Singleton,
19709 "New active item should be a singleton buffer"
19710 );
19711 assert_eq!(
19712 active_item
19713 .act_as::<Editor>(cx)
19714 .expect("should have navigated into an editor")
19715 .read(cx)
19716 .text(cx),
19717 sample_text_3
19718 );
19719
19720 workspace
19721 .go_back(workspace.active_pane().downgrade(), window, cx)
19722 .detach_and_log_err(cx);
19723 })
19724 .unwrap();
19725 cx.executor().run_until_parked();
19726 workspace
19727 .update(cx, |workspace, _, cx| {
19728 let active_item = workspace
19729 .active_item(cx)
19730 .expect("should have an active item after navigating back from the 3rd buffer");
19731 assert_eq!(
19732 active_item.item_id(),
19733 multibuffer_item_id,
19734 "Should navigate back from the 3rd buffer to the multi buffer"
19735 );
19736 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19737 })
19738 .unwrap();
19739}
19740
19741#[gpui::test]
19742async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19743 init_test(cx, |_| {});
19744
19745 let mut cx = EditorTestContext::new(cx).await;
19746
19747 let diff_base = r#"
19748 use some::mod;
19749
19750 const A: u32 = 42;
19751
19752 fn main() {
19753 println!("hello");
19754
19755 println!("world");
19756 }
19757 "#
19758 .unindent();
19759
19760 cx.set_state(
19761 &r#"
19762 use some::modified;
19763
19764 ˇ
19765 fn main() {
19766 println!("hello there");
19767
19768 println!("around the");
19769 println!("world");
19770 }
19771 "#
19772 .unindent(),
19773 );
19774
19775 cx.set_head_text(&diff_base);
19776 executor.run_until_parked();
19777
19778 cx.update_editor(|editor, window, cx| {
19779 editor.go_to_next_hunk(&GoToHunk, window, cx);
19780 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19781 });
19782 executor.run_until_parked();
19783 cx.assert_state_with_diff(
19784 r#"
19785 use some::modified;
19786
19787
19788 fn main() {
19789 - println!("hello");
19790 + ˇ println!("hello there");
19791
19792 println!("around the");
19793 println!("world");
19794 }
19795 "#
19796 .unindent(),
19797 );
19798
19799 cx.update_editor(|editor, window, cx| {
19800 for _ in 0..2 {
19801 editor.go_to_next_hunk(&GoToHunk, window, cx);
19802 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19803 }
19804 });
19805 executor.run_until_parked();
19806 cx.assert_state_with_diff(
19807 r#"
19808 - use some::mod;
19809 + ˇuse some::modified;
19810
19811
19812 fn main() {
19813 - println!("hello");
19814 + println!("hello there");
19815
19816 + println!("around the");
19817 println!("world");
19818 }
19819 "#
19820 .unindent(),
19821 );
19822
19823 cx.update_editor(|editor, window, cx| {
19824 editor.go_to_next_hunk(&GoToHunk, window, cx);
19825 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19826 });
19827 executor.run_until_parked();
19828 cx.assert_state_with_diff(
19829 r#"
19830 - use some::mod;
19831 + use some::modified;
19832
19833 - const A: u32 = 42;
19834 ˇ
19835 fn main() {
19836 - println!("hello");
19837 + println!("hello there");
19838
19839 + println!("around the");
19840 println!("world");
19841 }
19842 "#
19843 .unindent(),
19844 );
19845
19846 cx.update_editor(|editor, window, cx| {
19847 editor.cancel(&Cancel, window, cx);
19848 });
19849
19850 cx.assert_state_with_diff(
19851 r#"
19852 use some::modified;
19853
19854 ˇ
19855 fn main() {
19856 println!("hello there");
19857
19858 println!("around the");
19859 println!("world");
19860 }
19861 "#
19862 .unindent(),
19863 );
19864}
19865
19866#[gpui::test]
19867async fn test_diff_base_change_with_expanded_diff_hunks(
19868 executor: BackgroundExecutor,
19869 cx: &mut TestAppContext,
19870) {
19871 init_test(cx, |_| {});
19872
19873 let mut cx = EditorTestContext::new(cx).await;
19874
19875 let diff_base = r#"
19876 use some::mod1;
19877 use some::mod2;
19878
19879 const A: u32 = 42;
19880 const B: u32 = 42;
19881 const C: u32 = 42;
19882
19883 fn main() {
19884 println!("hello");
19885
19886 println!("world");
19887 }
19888 "#
19889 .unindent();
19890
19891 cx.set_state(
19892 &r#"
19893 use some::mod2;
19894
19895 const A: u32 = 42;
19896 const C: u32 = 42;
19897
19898 fn main(ˇ) {
19899 //println!("hello");
19900
19901 println!("world");
19902 //
19903 //
19904 }
19905 "#
19906 .unindent(),
19907 );
19908
19909 cx.set_head_text(&diff_base);
19910 executor.run_until_parked();
19911
19912 cx.update_editor(|editor, window, cx| {
19913 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19914 });
19915 executor.run_until_parked();
19916 cx.assert_state_with_diff(
19917 r#"
19918 - use some::mod1;
19919 use some::mod2;
19920
19921 const A: u32 = 42;
19922 - const B: u32 = 42;
19923 const C: u32 = 42;
19924
19925 fn main(ˇ) {
19926 - println!("hello");
19927 + //println!("hello");
19928
19929 println!("world");
19930 + //
19931 + //
19932 }
19933 "#
19934 .unindent(),
19935 );
19936
19937 cx.set_head_text("new diff base!");
19938 executor.run_until_parked();
19939 cx.assert_state_with_diff(
19940 r#"
19941 - new diff base!
19942 + use some::mod2;
19943 +
19944 + const A: u32 = 42;
19945 + const C: u32 = 42;
19946 +
19947 + fn main(ˇ) {
19948 + //println!("hello");
19949 +
19950 + println!("world");
19951 + //
19952 + //
19953 + }
19954 "#
19955 .unindent(),
19956 );
19957}
19958
19959#[gpui::test]
19960async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19961 init_test(cx, |_| {});
19962
19963 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19964 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19965 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19966 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19967 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19968 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19969
19970 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19971 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19972 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19973
19974 let multi_buffer = cx.new(|cx| {
19975 let mut multibuffer = MultiBuffer::new(ReadWrite);
19976 multibuffer.push_excerpts(
19977 buffer_1.clone(),
19978 [
19979 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19980 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19981 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19982 ],
19983 cx,
19984 );
19985 multibuffer.push_excerpts(
19986 buffer_2.clone(),
19987 [
19988 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19989 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19990 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19991 ],
19992 cx,
19993 );
19994 multibuffer.push_excerpts(
19995 buffer_3.clone(),
19996 [
19997 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19998 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19999 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20000 ],
20001 cx,
20002 );
20003 multibuffer
20004 });
20005
20006 let editor =
20007 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20008 editor
20009 .update(cx, |editor, _window, cx| {
20010 for (buffer, diff_base) in [
20011 (buffer_1.clone(), file_1_old),
20012 (buffer_2.clone(), file_2_old),
20013 (buffer_3.clone(), file_3_old),
20014 ] {
20015 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20016 editor
20017 .buffer
20018 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20019 }
20020 })
20021 .unwrap();
20022
20023 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20024 cx.run_until_parked();
20025
20026 cx.assert_editor_state(
20027 &"
20028 ˇaaa
20029 ccc
20030 ddd
20031
20032 ggg
20033 hhh
20034
20035
20036 lll
20037 mmm
20038 NNN
20039
20040 qqq
20041 rrr
20042
20043 uuu
20044 111
20045 222
20046 333
20047
20048 666
20049 777
20050
20051 000
20052 !!!"
20053 .unindent(),
20054 );
20055
20056 cx.update_editor(|editor, window, cx| {
20057 editor.select_all(&SelectAll, window, cx);
20058 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20059 });
20060 cx.executor().run_until_parked();
20061
20062 cx.assert_state_with_diff(
20063 "
20064 «aaa
20065 - bbb
20066 ccc
20067 ddd
20068
20069 ggg
20070 hhh
20071
20072
20073 lll
20074 mmm
20075 - nnn
20076 + NNN
20077
20078 qqq
20079 rrr
20080
20081 uuu
20082 111
20083 222
20084 333
20085
20086 + 666
20087 777
20088
20089 000
20090 !!!ˇ»"
20091 .unindent(),
20092 );
20093}
20094
20095#[gpui::test]
20096async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20097 init_test(cx, |_| {});
20098
20099 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20100 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20101
20102 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20103 let multi_buffer = cx.new(|cx| {
20104 let mut multibuffer = MultiBuffer::new(ReadWrite);
20105 multibuffer.push_excerpts(
20106 buffer.clone(),
20107 [
20108 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20109 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20110 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20111 ],
20112 cx,
20113 );
20114 multibuffer
20115 });
20116
20117 let editor =
20118 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20119 editor
20120 .update(cx, |editor, _window, cx| {
20121 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20122 editor
20123 .buffer
20124 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20125 })
20126 .unwrap();
20127
20128 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20129 cx.run_until_parked();
20130
20131 cx.update_editor(|editor, window, cx| {
20132 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20133 });
20134 cx.executor().run_until_parked();
20135
20136 // When the start of a hunk coincides with the start of its excerpt,
20137 // the hunk is expanded. When the start of a hunk is earlier than
20138 // the start of its excerpt, the hunk is not expanded.
20139 cx.assert_state_with_diff(
20140 "
20141 ˇaaa
20142 - bbb
20143 + BBB
20144
20145 - ddd
20146 - eee
20147 + DDD
20148 + EEE
20149 fff
20150
20151 iii
20152 "
20153 .unindent(),
20154 );
20155}
20156
20157#[gpui::test]
20158async fn test_edits_around_expanded_insertion_hunks(
20159 executor: BackgroundExecutor,
20160 cx: &mut TestAppContext,
20161) {
20162 init_test(cx, |_| {});
20163
20164 let mut cx = EditorTestContext::new(cx).await;
20165
20166 let diff_base = r#"
20167 use some::mod1;
20168 use some::mod2;
20169
20170 const A: u32 = 42;
20171
20172 fn main() {
20173 println!("hello");
20174
20175 println!("world");
20176 }
20177 "#
20178 .unindent();
20179 executor.run_until_parked();
20180 cx.set_state(
20181 &r#"
20182 use some::mod1;
20183 use some::mod2;
20184
20185 const A: u32 = 42;
20186 const B: u32 = 42;
20187 const C: u32 = 42;
20188 ˇ
20189
20190 fn main() {
20191 println!("hello");
20192
20193 println!("world");
20194 }
20195 "#
20196 .unindent(),
20197 );
20198
20199 cx.set_head_text(&diff_base);
20200 executor.run_until_parked();
20201
20202 cx.update_editor(|editor, window, cx| {
20203 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20204 });
20205 executor.run_until_parked();
20206
20207 cx.assert_state_with_diff(
20208 r#"
20209 use some::mod1;
20210 use some::mod2;
20211
20212 const A: u32 = 42;
20213 + const B: u32 = 42;
20214 + const C: u32 = 42;
20215 + ˇ
20216
20217 fn main() {
20218 println!("hello");
20219
20220 println!("world");
20221 }
20222 "#
20223 .unindent(),
20224 );
20225
20226 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20227 executor.run_until_parked();
20228
20229 cx.assert_state_with_diff(
20230 r#"
20231 use some::mod1;
20232 use some::mod2;
20233
20234 const A: u32 = 42;
20235 + const B: u32 = 42;
20236 + const C: u32 = 42;
20237 + const D: u32 = 42;
20238 + ˇ
20239
20240 fn main() {
20241 println!("hello");
20242
20243 println!("world");
20244 }
20245 "#
20246 .unindent(),
20247 );
20248
20249 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20250 executor.run_until_parked();
20251
20252 cx.assert_state_with_diff(
20253 r#"
20254 use some::mod1;
20255 use some::mod2;
20256
20257 const A: u32 = 42;
20258 + const B: u32 = 42;
20259 + const C: u32 = 42;
20260 + const D: u32 = 42;
20261 + const E: u32 = 42;
20262 + ˇ
20263
20264 fn main() {
20265 println!("hello");
20266
20267 println!("world");
20268 }
20269 "#
20270 .unindent(),
20271 );
20272
20273 cx.update_editor(|editor, window, cx| {
20274 editor.delete_line(&DeleteLine, window, cx);
20275 });
20276 executor.run_until_parked();
20277
20278 cx.assert_state_with_diff(
20279 r#"
20280 use some::mod1;
20281 use some::mod2;
20282
20283 const A: u32 = 42;
20284 + const B: u32 = 42;
20285 + const C: u32 = 42;
20286 + const D: u32 = 42;
20287 + const E: u32 = 42;
20288 ˇ
20289 fn main() {
20290 println!("hello");
20291
20292 println!("world");
20293 }
20294 "#
20295 .unindent(),
20296 );
20297
20298 cx.update_editor(|editor, window, cx| {
20299 editor.move_up(&MoveUp, window, cx);
20300 editor.delete_line(&DeleteLine, window, cx);
20301 editor.move_up(&MoveUp, window, cx);
20302 editor.delete_line(&DeleteLine, window, cx);
20303 editor.move_up(&MoveUp, window, cx);
20304 editor.delete_line(&DeleteLine, window, cx);
20305 });
20306 executor.run_until_parked();
20307 cx.assert_state_with_diff(
20308 r#"
20309 use some::mod1;
20310 use some::mod2;
20311
20312 const A: u32 = 42;
20313 + const B: u32 = 42;
20314 ˇ
20315 fn main() {
20316 println!("hello");
20317
20318 println!("world");
20319 }
20320 "#
20321 .unindent(),
20322 );
20323
20324 cx.update_editor(|editor, window, cx| {
20325 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20326 editor.delete_line(&DeleteLine, window, cx);
20327 });
20328 executor.run_until_parked();
20329 cx.assert_state_with_diff(
20330 r#"
20331 ˇ
20332 fn main() {
20333 println!("hello");
20334
20335 println!("world");
20336 }
20337 "#
20338 .unindent(),
20339 );
20340}
20341
20342#[gpui::test]
20343async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20344 init_test(cx, |_| {});
20345
20346 let mut cx = EditorTestContext::new(cx).await;
20347 cx.set_head_text(indoc! { "
20348 one
20349 two
20350 three
20351 four
20352 five
20353 "
20354 });
20355 cx.set_state(indoc! { "
20356 one
20357 ˇthree
20358 five
20359 "});
20360 cx.run_until_parked();
20361 cx.update_editor(|editor, window, cx| {
20362 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20363 });
20364 cx.assert_state_with_diff(
20365 indoc! { "
20366 one
20367 - two
20368 ˇthree
20369 - four
20370 five
20371 "}
20372 .to_string(),
20373 );
20374 cx.update_editor(|editor, window, cx| {
20375 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20376 });
20377
20378 cx.assert_state_with_diff(
20379 indoc! { "
20380 one
20381 ˇthree
20382 five
20383 "}
20384 .to_string(),
20385 );
20386
20387 cx.set_state(indoc! { "
20388 one
20389 ˇTWO
20390 three
20391 four
20392 five
20393 "});
20394 cx.run_until_parked();
20395 cx.update_editor(|editor, window, cx| {
20396 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20397 });
20398
20399 cx.assert_state_with_diff(
20400 indoc! { "
20401 one
20402 - two
20403 + ˇTWO
20404 three
20405 four
20406 five
20407 "}
20408 .to_string(),
20409 );
20410 cx.update_editor(|editor, window, cx| {
20411 editor.move_up(&Default::default(), window, cx);
20412 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20413 });
20414 cx.assert_state_with_diff(
20415 indoc! { "
20416 one
20417 ˇTWO
20418 three
20419 four
20420 five
20421 "}
20422 .to_string(),
20423 );
20424}
20425
20426#[gpui::test]
20427async fn test_edits_around_expanded_deletion_hunks(
20428 executor: BackgroundExecutor,
20429 cx: &mut TestAppContext,
20430) {
20431 init_test(cx, |_| {});
20432
20433 let mut cx = EditorTestContext::new(cx).await;
20434
20435 let diff_base = r#"
20436 use some::mod1;
20437 use some::mod2;
20438
20439 const A: u32 = 42;
20440 const B: u32 = 42;
20441 const C: u32 = 42;
20442
20443
20444 fn main() {
20445 println!("hello");
20446
20447 println!("world");
20448 }
20449 "#
20450 .unindent();
20451 executor.run_until_parked();
20452 cx.set_state(
20453 &r#"
20454 use some::mod1;
20455 use some::mod2;
20456
20457 ˇconst B: u32 = 42;
20458 const C: u32 = 42;
20459
20460
20461 fn main() {
20462 println!("hello");
20463
20464 println!("world");
20465 }
20466 "#
20467 .unindent(),
20468 );
20469
20470 cx.set_head_text(&diff_base);
20471 executor.run_until_parked();
20472
20473 cx.update_editor(|editor, window, cx| {
20474 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20475 });
20476 executor.run_until_parked();
20477
20478 cx.assert_state_with_diff(
20479 r#"
20480 use some::mod1;
20481 use some::mod2;
20482
20483 - const A: u32 = 42;
20484 ˇconst B: u32 = 42;
20485 const C: u32 = 42;
20486
20487
20488 fn main() {
20489 println!("hello");
20490
20491 println!("world");
20492 }
20493 "#
20494 .unindent(),
20495 );
20496
20497 cx.update_editor(|editor, window, cx| {
20498 editor.delete_line(&DeleteLine, window, cx);
20499 });
20500 executor.run_until_parked();
20501 cx.assert_state_with_diff(
20502 r#"
20503 use some::mod1;
20504 use some::mod2;
20505
20506 - const A: u32 = 42;
20507 - const B: u32 = 42;
20508 ˇconst C: u32 = 42;
20509
20510
20511 fn main() {
20512 println!("hello");
20513
20514 println!("world");
20515 }
20516 "#
20517 .unindent(),
20518 );
20519
20520 cx.update_editor(|editor, window, cx| {
20521 editor.delete_line(&DeleteLine, window, cx);
20522 });
20523 executor.run_until_parked();
20524 cx.assert_state_with_diff(
20525 r#"
20526 use some::mod1;
20527 use some::mod2;
20528
20529 - const A: u32 = 42;
20530 - const B: u32 = 42;
20531 - const C: u32 = 42;
20532 ˇ
20533
20534 fn main() {
20535 println!("hello");
20536
20537 println!("world");
20538 }
20539 "#
20540 .unindent(),
20541 );
20542
20543 cx.update_editor(|editor, window, cx| {
20544 editor.handle_input("replacement", window, cx);
20545 });
20546 executor.run_until_parked();
20547 cx.assert_state_with_diff(
20548 r#"
20549 use some::mod1;
20550 use some::mod2;
20551
20552 - const A: u32 = 42;
20553 - const B: u32 = 42;
20554 - const C: u32 = 42;
20555 -
20556 + replacementˇ
20557
20558 fn main() {
20559 println!("hello");
20560
20561 println!("world");
20562 }
20563 "#
20564 .unindent(),
20565 );
20566}
20567
20568#[gpui::test]
20569async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20570 init_test(cx, |_| {});
20571
20572 let mut cx = EditorTestContext::new(cx).await;
20573
20574 let base_text = r#"
20575 one
20576 two
20577 three
20578 four
20579 five
20580 "#
20581 .unindent();
20582 executor.run_until_parked();
20583 cx.set_state(
20584 &r#"
20585 one
20586 two
20587 fˇour
20588 five
20589 "#
20590 .unindent(),
20591 );
20592
20593 cx.set_head_text(&base_text);
20594 executor.run_until_parked();
20595
20596 cx.update_editor(|editor, window, cx| {
20597 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20598 });
20599 executor.run_until_parked();
20600
20601 cx.assert_state_with_diff(
20602 r#"
20603 one
20604 two
20605 - three
20606 fˇour
20607 five
20608 "#
20609 .unindent(),
20610 );
20611
20612 cx.update_editor(|editor, window, cx| {
20613 editor.backspace(&Backspace, window, cx);
20614 editor.backspace(&Backspace, window, cx);
20615 });
20616 executor.run_until_parked();
20617 cx.assert_state_with_diff(
20618 r#"
20619 one
20620 two
20621 - threeˇ
20622 - four
20623 + our
20624 five
20625 "#
20626 .unindent(),
20627 );
20628}
20629
20630#[gpui::test]
20631async fn test_edit_after_expanded_modification_hunk(
20632 executor: BackgroundExecutor,
20633 cx: &mut TestAppContext,
20634) {
20635 init_test(cx, |_| {});
20636
20637 let mut cx = EditorTestContext::new(cx).await;
20638
20639 let diff_base = r#"
20640 use some::mod1;
20641 use some::mod2;
20642
20643 const A: u32 = 42;
20644 const B: u32 = 42;
20645 const C: u32 = 42;
20646 const D: u32 = 42;
20647
20648
20649 fn main() {
20650 println!("hello");
20651
20652 println!("world");
20653 }"#
20654 .unindent();
20655
20656 cx.set_state(
20657 &r#"
20658 use some::mod1;
20659 use some::mod2;
20660
20661 const A: u32 = 42;
20662 const B: u32 = 42;
20663 const C: u32 = 43ˇ
20664 const D: u32 = 42;
20665
20666
20667 fn main() {
20668 println!("hello");
20669
20670 println!("world");
20671 }"#
20672 .unindent(),
20673 );
20674
20675 cx.set_head_text(&diff_base);
20676 executor.run_until_parked();
20677 cx.update_editor(|editor, window, cx| {
20678 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20679 });
20680 executor.run_until_parked();
20681
20682 cx.assert_state_with_diff(
20683 r#"
20684 use some::mod1;
20685 use some::mod2;
20686
20687 const A: u32 = 42;
20688 const B: u32 = 42;
20689 - const C: u32 = 42;
20690 + const C: u32 = 43ˇ
20691 const D: u32 = 42;
20692
20693
20694 fn main() {
20695 println!("hello");
20696
20697 println!("world");
20698 }"#
20699 .unindent(),
20700 );
20701
20702 cx.update_editor(|editor, window, cx| {
20703 editor.handle_input("\nnew_line\n", window, cx);
20704 });
20705 executor.run_until_parked();
20706
20707 cx.assert_state_with_diff(
20708 r#"
20709 use some::mod1;
20710 use some::mod2;
20711
20712 const A: u32 = 42;
20713 const B: u32 = 42;
20714 - const C: u32 = 42;
20715 + const C: u32 = 43
20716 + new_line
20717 + ˇ
20718 const D: u32 = 42;
20719
20720
20721 fn main() {
20722 println!("hello");
20723
20724 println!("world");
20725 }"#
20726 .unindent(),
20727 );
20728}
20729
20730#[gpui::test]
20731async fn test_stage_and_unstage_added_file_hunk(
20732 executor: BackgroundExecutor,
20733 cx: &mut TestAppContext,
20734) {
20735 init_test(cx, |_| {});
20736
20737 let mut cx = EditorTestContext::new(cx).await;
20738 cx.update_editor(|editor, _, cx| {
20739 editor.set_expand_all_diff_hunks(cx);
20740 });
20741
20742 let working_copy = r#"
20743 ˇfn main() {
20744 println!("hello, world!");
20745 }
20746 "#
20747 .unindent();
20748
20749 cx.set_state(&working_copy);
20750 executor.run_until_parked();
20751
20752 cx.assert_state_with_diff(
20753 r#"
20754 + ˇfn main() {
20755 + println!("hello, world!");
20756 + }
20757 "#
20758 .unindent(),
20759 );
20760 cx.assert_index_text(None);
20761
20762 cx.update_editor(|editor, window, cx| {
20763 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20764 });
20765 executor.run_until_parked();
20766 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20767 cx.assert_state_with_diff(
20768 r#"
20769 + ˇfn main() {
20770 + println!("hello, world!");
20771 + }
20772 "#
20773 .unindent(),
20774 );
20775
20776 cx.update_editor(|editor, window, cx| {
20777 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20778 });
20779 executor.run_until_parked();
20780 cx.assert_index_text(None);
20781}
20782
20783async fn setup_indent_guides_editor(
20784 text: &str,
20785 cx: &mut TestAppContext,
20786) -> (BufferId, EditorTestContext) {
20787 init_test(cx, |_| {});
20788
20789 let mut cx = EditorTestContext::new(cx).await;
20790
20791 let buffer_id = cx.update_editor(|editor, window, cx| {
20792 editor.set_text(text, window, cx);
20793 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20794
20795 buffer_ids[0]
20796 });
20797
20798 (buffer_id, cx)
20799}
20800
20801fn assert_indent_guides(
20802 range: Range<u32>,
20803 expected: Vec<IndentGuide>,
20804 active_indices: Option<Vec<usize>>,
20805 cx: &mut EditorTestContext,
20806) {
20807 let indent_guides = cx.update_editor(|editor, window, cx| {
20808 let snapshot = editor.snapshot(window, cx).display_snapshot;
20809 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20810 editor,
20811 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20812 true,
20813 &snapshot,
20814 cx,
20815 );
20816
20817 indent_guides.sort_by(|a, b| {
20818 a.depth.cmp(&b.depth).then(
20819 a.start_row
20820 .cmp(&b.start_row)
20821 .then(a.end_row.cmp(&b.end_row)),
20822 )
20823 });
20824 indent_guides
20825 });
20826
20827 if let Some(expected) = active_indices {
20828 let active_indices = cx.update_editor(|editor, window, cx| {
20829 let snapshot = editor.snapshot(window, cx).display_snapshot;
20830 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20831 });
20832
20833 assert_eq!(
20834 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20835 expected,
20836 "Active indent guide indices do not match"
20837 );
20838 }
20839
20840 assert_eq!(indent_guides, expected, "Indent guides do not match");
20841}
20842
20843fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20844 IndentGuide {
20845 buffer_id,
20846 start_row: MultiBufferRow(start_row),
20847 end_row: MultiBufferRow(end_row),
20848 depth,
20849 tab_size: 4,
20850 settings: IndentGuideSettings {
20851 enabled: true,
20852 line_width: 1,
20853 active_line_width: 1,
20854 coloring: IndentGuideColoring::default(),
20855 background_coloring: IndentGuideBackgroundColoring::default(),
20856 },
20857 }
20858}
20859
20860#[gpui::test]
20861async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20862 let (buffer_id, mut cx) = setup_indent_guides_editor(
20863 &"
20864 fn main() {
20865 let a = 1;
20866 }"
20867 .unindent(),
20868 cx,
20869 )
20870 .await;
20871
20872 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20873}
20874
20875#[gpui::test]
20876async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20877 let (buffer_id, mut cx) = setup_indent_guides_editor(
20878 &"
20879 fn main() {
20880 let a = 1;
20881 let b = 2;
20882 }"
20883 .unindent(),
20884 cx,
20885 )
20886 .await;
20887
20888 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20889}
20890
20891#[gpui::test]
20892async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20893 let (buffer_id, mut cx) = setup_indent_guides_editor(
20894 &"
20895 fn main() {
20896 let a = 1;
20897 if a == 3 {
20898 let b = 2;
20899 } else {
20900 let c = 3;
20901 }
20902 }"
20903 .unindent(),
20904 cx,
20905 )
20906 .await;
20907
20908 assert_indent_guides(
20909 0..8,
20910 vec![
20911 indent_guide(buffer_id, 1, 6, 0),
20912 indent_guide(buffer_id, 3, 3, 1),
20913 indent_guide(buffer_id, 5, 5, 1),
20914 ],
20915 None,
20916 &mut cx,
20917 );
20918}
20919
20920#[gpui::test]
20921async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20922 let (buffer_id, mut cx) = setup_indent_guides_editor(
20923 &"
20924 fn main() {
20925 let a = 1;
20926 let b = 2;
20927 let c = 3;
20928 }"
20929 .unindent(),
20930 cx,
20931 )
20932 .await;
20933
20934 assert_indent_guides(
20935 0..5,
20936 vec![
20937 indent_guide(buffer_id, 1, 3, 0),
20938 indent_guide(buffer_id, 2, 2, 1),
20939 ],
20940 None,
20941 &mut cx,
20942 );
20943}
20944
20945#[gpui::test]
20946async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20947 let (buffer_id, mut cx) = setup_indent_guides_editor(
20948 &"
20949 fn main() {
20950 let a = 1;
20951
20952 let c = 3;
20953 }"
20954 .unindent(),
20955 cx,
20956 )
20957 .await;
20958
20959 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20960}
20961
20962#[gpui::test]
20963async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20964 let (buffer_id, mut cx) = setup_indent_guides_editor(
20965 &"
20966 fn main() {
20967 let a = 1;
20968
20969 let c = 3;
20970
20971 if a == 3 {
20972 let b = 2;
20973 } else {
20974 let c = 3;
20975 }
20976 }"
20977 .unindent(),
20978 cx,
20979 )
20980 .await;
20981
20982 assert_indent_guides(
20983 0..11,
20984 vec![
20985 indent_guide(buffer_id, 1, 9, 0),
20986 indent_guide(buffer_id, 6, 6, 1),
20987 indent_guide(buffer_id, 8, 8, 1),
20988 ],
20989 None,
20990 &mut cx,
20991 );
20992}
20993
20994#[gpui::test]
20995async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20996 let (buffer_id, mut cx) = setup_indent_guides_editor(
20997 &"
20998 fn main() {
20999 let a = 1;
21000
21001 let c = 3;
21002
21003 if a == 3 {
21004 let b = 2;
21005 } else {
21006 let c = 3;
21007 }
21008 }"
21009 .unindent(),
21010 cx,
21011 )
21012 .await;
21013
21014 assert_indent_guides(
21015 1..11,
21016 vec![
21017 indent_guide(buffer_id, 1, 9, 0),
21018 indent_guide(buffer_id, 6, 6, 1),
21019 indent_guide(buffer_id, 8, 8, 1),
21020 ],
21021 None,
21022 &mut cx,
21023 );
21024}
21025
21026#[gpui::test]
21027async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21028 let (buffer_id, mut cx) = setup_indent_guides_editor(
21029 &"
21030 fn main() {
21031 let a = 1;
21032
21033 let c = 3;
21034
21035 if a == 3 {
21036 let b = 2;
21037 } else {
21038 let c = 3;
21039 }
21040 }"
21041 .unindent(),
21042 cx,
21043 )
21044 .await;
21045
21046 assert_indent_guides(
21047 1..10,
21048 vec![
21049 indent_guide(buffer_id, 1, 9, 0),
21050 indent_guide(buffer_id, 6, 6, 1),
21051 indent_guide(buffer_id, 8, 8, 1),
21052 ],
21053 None,
21054 &mut cx,
21055 );
21056}
21057
21058#[gpui::test]
21059async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21060 let (buffer_id, mut cx) = setup_indent_guides_editor(
21061 &"
21062 fn main() {
21063 if a {
21064 b(
21065 c,
21066 d,
21067 )
21068 } else {
21069 e(
21070 f
21071 )
21072 }
21073 }"
21074 .unindent(),
21075 cx,
21076 )
21077 .await;
21078
21079 assert_indent_guides(
21080 0..11,
21081 vec![
21082 indent_guide(buffer_id, 1, 10, 0),
21083 indent_guide(buffer_id, 2, 5, 1),
21084 indent_guide(buffer_id, 7, 9, 1),
21085 indent_guide(buffer_id, 3, 4, 2),
21086 indent_guide(buffer_id, 8, 8, 2),
21087 ],
21088 None,
21089 &mut cx,
21090 );
21091
21092 cx.update_editor(|editor, window, cx| {
21093 editor.fold_at(MultiBufferRow(2), window, cx);
21094 assert_eq!(
21095 editor.display_text(cx),
21096 "
21097 fn main() {
21098 if a {
21099 b(⋯
21100 )
21101 } else {
21102 e(
21103 f
21104 )
21105 }
21106 }"
21107 .unindent()
21108 );
21109 });
21110
21111 assert_indent_guides(
21112 0..11,
21113 vec![
21114 indent_guide(buffer_id, 1, 10, 0),
21115 indent_guide(buffer_id, 2, 5, 1),
21116 indent_guide(buffer_id, 7, 9, 1),
21117 indent_guide(buffer_id, 8, 8, 2),
21118 ],
21119 None,
21120 &mut cx,
21121 );
21122}
21123
21124#[gpui::test]
21125async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21126 let (buffer_id, mut cx) = setup_indent_guides_editor(
21127 &"
21128 block1
21129 block2
21130 block3
21131 block4
21132 block2
21133 block1
21134 block1"
21135 .unindent(),
21136 cx,
21137 )
21138 .await;
21139
21140 assert_indent_guides(
21141 1..10,
21142 vec![
21143 indent_guide(buffer_id, 1, 4, 0),
21144 indent_guide(buffer_id, 2, 3, 1),
21145 indent_guide(buffer_id, 3, 3, 2),
21146 ],
21147 None,
21148 &mut cx,
21149 );
21150}
21151
21152#[gpui::test]
21153async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21154 let (buffer_id, mut cx) = setup_indent_guides_editor(
21155 &"
21156 block1
21157 block2
21158 block3
21159
21160 block1
21161 block1"
21162 .unindent(),
21163 cx,
21164 )
21165 .await;
21166
21167 assert_indent_guides(
21168 0..6,
21169 vec![
21170 indent_guide(buffer_id, 1, 2, 0),
21171 indent_guide(buffer_id, 2, 2, 1),
21172 ],
21173 None,
21174 &mut cx,
21175 );
21176}
21177
21178#[gpui::test]
21179async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21180 let (buffer_id, mut cx) = setup_indent_guides_editor(
21181 &"
21182 function component() {
21183 \treturn (
21184 \t\t\t
21185 \t\t<div>
21186 \t\t\t<abc></abc>
21187 \t\t</div>
21188 \t)
21189 }"
21190 .unindent(),
21191 cx,
21192 )
21193 .await;
21194
21195 assert_indent_guides(
21196 0..8,
21197 vec![
21198 indent_guide(buffer_id, 1, 6, 0),
21199 indent_guide(buffer_id, 2, 5, 1),
21200 indent_guide(buffer_id, 4, 4, 2),
21201 ],
21202 None,
21203 &mut cx,
21204 );
21205}
21206
21207#[gpui::test]
21208async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21209 let (buffer_id, mut cx) = setup_indent_guides_editor(
21210 &"
21211 function component() {
21212 \treturn (
21213 \t
21214 \t\t<div>
21215 \t\t\t<abc></abc>
21216 \t\t</div>
21217 \t)
21218 }"
21219 .unindent(),
21220 cx,
21221 )
21222 .await;
21223
21224 assert_indent_guides(
21225 0..8,
21226 vec![
21227 indent_guide(buffer_id, 1, 6, 0),
21228 indent_guide(buffer_id, 2, 5, 1),
21229 indent_guide(buffer_id, 4, 4, 2),
21230 ],
21231 None,
21232 &mut cx,
21233 );
21234}
21235
21236#[gpui::test]
21237async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21238 let (buffer_id, mut cx) = setup_indent_guides_editor(
21239 &"
21240 block1
21241
21242
21243
21244 block2
21245 "
21246 .unindent(),
21247 cx,
21248 )
21249 .await;
21250
21251 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21252}
21253
21254#[gpui::test]
21255async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21256 let (buffer_id, mut cx) = setup_indent_guides_editor(
21257 &"
21258 def a:
21259 \tb = 3
21260 \tif True:
21261 \t\tc = 4
21262 \t\td = 5
21263 \tprint(b)
21264 "
21265 .unindent(),
21266 cx,
21267 )
21268 .await;
21269
21270 assert_indent_guides(
21271 0..6,
21272 vec![
21273 indent_guide(buffer_id, 1, 5, 0),
21274 indent_guide(buffer_id, 3, 4, 1),
21275 ],
21276 None,
21277 &mut cx,
21278 );
21279}
21280
21281#[gpui::test]
21282async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21283 let (buffer_id, mut cx) = setup_indent_guides_editor(
21284 &"
21285 fn main() {
21286 let a = 1;
21287 }"
21288 .unindent(),
21289 cx,
21290 )
21291 .await;
21292
21293 cx.update_editor(|editor, window, cx| {
21294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21295 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21296 });
21297 });
21298
21299 assert_indent_guides(
21300 0..3,
21301 vec![indent_guide(buffer_id, 1, 1, 0)],
21302 Some(vec![0]),
21303 &mut cx,
21304 );
21305}
21306
21307#[gpui::test]
21308async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21309 let (buffer_id, mut cx) = setup_indent_guides_editor(
21310 &"
21311 fn main() {
21312 if 1 == 2 {
21313 let a = 1;
21314 }
21315 }"
21316 .unindent(),
21317 cx,
21318 )
21319 .await;
21320
21321 cx.update_editor(|editor, window, cx| {
21322 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21323 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21324 });
21325 });
21326
21327 assert_indent_guides(
21328 0..4,
21329 vec![
21330 indent_guide(buffer_id, 1, 3, 0),
21331 indent_guide(buffer_id, 2, 2, 1),
21332 ],
21333 Some(vec![1]),
21334 &mut cx,
21335 );
21336
21337 cx.update_editor(|editor, window, cx| {
21338 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21339 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21340 });
21341 });
21342
21343 assert_indent_guides(
21344 0..4,
21345 vec![
21346 indent_guide(buffer_id, 1, 3, 0),
21347 indent_guide(buffer_id, 2, 2, 1),
21348 ],
21349 Some(vec![1]),
21350 &mut cx,
21351 );
21352
21353 cx.update_editor(|editor, window, cx| {
21354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21355 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21356 });
21357 });
21358
21359 assert_indent_guides(
21360 0..4,
21361 vec![
21362 indent_guide(buffer_id, 1, 3, 0),
21363 indent_guide(buffer_id, 2, 2, 1),
21364 ],
21365 Some(vec![0]),
21366 &mut cx,
21367 );
21368}
21369
21370#[gpui::test]
21371async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21372 let (buffer_id, mut cx) = setup_indent_guides_editor(
21373 &"
21374 fn main() {
21375 let a = 1;
21376
21377 let b = 2;
21378 }"
21379 .unindent(),
21380 cx,
21381 )
21382 .await;
21383
21384 cx.update_editor(|editor, window, cx| {
21385 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21386 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21387 });
21388 });
21389
21390 assert_indent_guides(
21391 0..5,
21392 vec![indent_guide(buffer_id, 1, 3, 0)],
21393 Some(vec![0]),
21394 &mut cx,
21395 );
21396}
21397
21398#[gpui::test]
21399async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21400 let (buffer_id, mut cx) = setup_indent_guides_editor(
21401 &"
21402 def m:
21403 a = 1
21404 pass"
21405 .unindent(),
21406 cx,
21407 )
21408 .await;
21409
21410 cx.update_editor(|editor, window, cx| {
21411 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21412 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21413 });
21414 });
21415
21416 assert_indent_guides(
21417 0..3,
21418 vec![indent_guide(buffer_id, 1, 2, 0)],
21419 Some(vec![0]),
21420 &mut cx,
21421 );
21422}
21423
21424#[gpui::test]
21425async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21426 init_test(cx, |_| {});
21427 let mut cx = EditorTestContext::new(cx).await;
21428 let text = indoc! {
21429 "
21430 impl A {
21431 fn b() {
21432 0;
21433 3;
21434 5;
21435 6;
21436 7;
21437 }
21438 }
21439 "
21440 };
21441 let base_text = indoc! {
21442 "
21443 impl A {
21444 fn b() {
21445 0;
21446 1;
21447 2;
21448 3;
21449 4;
21450 }
21451 fn c() {
21452 5;
21453 6;
21454 7;
21455 }
21456 }
21457 "
21458 };
21459
21460 cx.update_editor(|editor, window, cx| {
21461 editor.set_text(text, window, cx);
21462
21463 editor.buffer().update(cx, |multibuffer, cx| {
21464 let buffer = multibuffer.as_singleton().unwrap();
21465 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21466
21467 multibuffer.set_all_diff_hunks_expanded(cx);
21468 multibuffer.add_diff(diff, cx);
21469
21470 buffer.read(cx).remote_id()
21471 })
21472 });
21473 cx.run_until_parked();
21474
21475 cx.assert_state_with_diff(
21476 indoc! { "
21477 impl A {
21478 fn b() {
21479 0;
21480 - 1;
21481 - 2;
21482 3;
21483 - 4;
21484 - }
21485 - fn c() {
21486 5;
21487 6;
21488 7;
21489 }
21490 }
21491 ˇ"
21492 }
21493 .to_string(),
21494 );
21495
21496 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21497 editor
21498 .snapshot(window, cx)
21499 .buffer_snapshot()
21500 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21501 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21502 .collect::<Vec<_>>()
21503 });
21504 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21505 assert_eq!(
21506 actual_guides,
21507 vec![
21508 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21509 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21510 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21511 ]
21512 );
21513}
21514
21515#[gpui::test]
21516async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21517 init_test(cx, |_| {});
21518 let mut cx = EditorTestContext::new(cx).await;
21519
21520 let diff_base = r#"
21521 a
21522 b
21523 c
21524 "#
21525 .unindent();
21526
21527 cx.set_state(
21528 &r#"
21529 ˇA
21530 b
21531 C
21532 "#
21533 .unindent(),
21534 );
21535 cx.set_head_text(&diff_base);
21536 cx.update_editor(|editor, window, cx| {
21537 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21538 });
21539 executor.run_until_parked();
21540
21541 let both_hunks_expanded = r#"
21542 - a
21543 + ˇA
21544 b
21545 - c
21546 + C
21547 "#
21548 .unindent();
21549
21550 cx.assert_state_with_diff(both_hunks_expanded.clone());
21551
21552 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21553 let snapshot = editor.snapshot(window, cx);
21554 let hunks = editor
21555 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21556 .collect::<Vec<_>>();
21557 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21558 hunks
21559 .into_iter()
21560 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21561 .collect::<Vec<_>>()
21562 });
21563 assert_eq!(hunk_ranges.len(), 2);
21564
21565 cx.update_editor(|editor, _, cx| {
21566 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21567 });
21568 executor.run_until_parked();
21569
21570 let second_hunk_expanded = r#"
21571 ˇA
21572 b
21573 - c
21574 + C
21575 "#
21576 .unindent();
21577
21578 cx.assert_state_with_diff(second_hunk_expanded);
21579
21580 cx.update_editor(|editor, _, cx| {
21581 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21582 });
21583 executor.run_until_parked();
21584
21585 cx.assert_state_with_diff(both_hunks_expanded.clone());
21586
21587 cx.update_editor(|editor, _, cx| {
21588 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21589 });
21590 executor.run_until_parked();
21591
21592 let first_hunk_expanded = r#"
21593 - a
21594 + ˇA
21595 b
21596 C
21597 "#
21598 .unindent();
21599
21600 cx.assert_state_with_diff(first_hunk_expanded);
21601
21602 cx.update_editor(|editor, _, cx| {
21603 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21604 });
21605 executor.run_until_parked();
21606
21607 cx.assert_state_with_diff(both_hunks_expanded);
21608
21609 cx.set_state(
21610 &r#"
21611 ˇA
21612 b
21613 "#
21614 .unindent(),
21615 );
21616 cx.run_until_parked();
21617
21618 // TODO this cursor position seems bad
21619 cx.assert_state_with_diff(
21620 r#"
21621 - ˇa
21622 + A
21623 b
21624 "#
21625 .unindent(),
21626 );
21627
21628 cx.update_editor(|editor, window, cx| {
21629 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21630 });
21631
21632 cx.assert_state_with_diff(
21633 r#"
21634 - ˇa
21635 + A
21636 b
21637 - c
21638 "#
21639 .unindent(),
21640 );
21641
21642 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21643 let snapshot = editor.snapshot(window, cx);
21644 let hunks = editor
21645 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21646 .collect::<Vec<_>>();
21647 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21648 hunks
21649 .into_iter()
21650 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21651 .collect::<Vec<_>>()
21652 });
21653 assert_eq!(hunk_ranges.len(), 2);
21654
21655 cx.update_editor(|editor, _, cx| {
21656 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21657 });
21658 executor.run_until_parked();
21659
21660 cx.assert_state_with_diff(
21661 r#"
21662 - ˇa
21663 + A
21664 b
21665 "#
21666 .unindent(),
21667 );
21668}
21669
21670#[gpui::test]
21671async fn test_toggle_deletion_hunk_at_start_of_file(
21672 executor: BackgroundExecutor,
21673 cx: &mut TestAppContext,
21674) {
21675 init_test(cx, |_| {});
21676 let mut cx = EditorTestContext::new(cx).await;
21677
21678 let diff_base = r#"
21679 a
21680 b
21681 c
21682 "#
21683 .unindent();
21684
21685 cx.set_state(
21686 &r#"
21687 ˇb
21688 c
21689 "#
21690 .unindent(),
21691 );
21692 cx.set_head_text(&diff_base);
21693 cx.update_editor(|editor, window, cx| {
21694 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21695 });
21696 executor.run_until_parked();
21697
21698 let hunk_expanded = r#"
21699 - a
21700 ˇb
21701 c
21702 "#
21703 .unindent();
21704
21705 cx.assert_state_with_diff(hunk_expanded.clone());
21706
21707 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21708 let snapshot = editor.snapshot(window, cx);
21709 let hunks = editor
21710 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21711 .collect::<Vec<_>>();
21712 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21713 hunks
21714 .into_iter()
21715 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21716 .collect::<Vec<_>>()
21717 });
21718 assert_eq!(hunk_ranges.len(), 1);
21719
21720 cx.update_editor(|editor, _, cx| {
21721 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21722 });
21723 executor.run_until_parked();
21724
21725 let hunk_collapsed = r#"
21726 ˇb
21727 c
21728 "#
21729 .unindent();
21730
21731 cx.assert_state_with_diff(hunk_collapsed);
21732
21733 cx.update_editor(|editor, _, cx| {
21734 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21735 });
21736 executor.run_until_parked();
21737
21738 cx.assert_state_with_diff(hunk_expanded);
21739}
21740
21741#[gpui::test]
21742async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21743 init_test(cx, |_| {});
21744
21745 let fs = FakeFs::new(cx.executor());
21746 fs.insert_tree(
21747 path!("/test"),
21748 json!({
21749 ".git": {},
21750 "file-1": "ONE\n",
21751 "file-2": "TWO\n",
21752 "file-3": "THREE\n",
21753 }),
21754 )
21755 .await;
21756
21757 fs.set_head_for_repo(
21758 path!("/test/.git").as_ref(),
21759 &[
21760 ("file-1", "one\n".into()),
21761 ("file-2", "two\n".into()),
21762 ("file-3", "three\n".into()),
21763 ],
21764 "deadbeef",
21765 );
21766
21767 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21768 let mut buffers = vec![];
21769 for i in 1..=3 {
21770 let buffer = project
21771 .update(cx, |project, cx| {
21772 let path = format!(path!("/test/file-{}"), i);
21773 project.open_local_buffer(path, cx)
21774 })
21775 .await
21776 .unwrap();
21777 buffers.push(buffer);
21778 }
21779
21780 let multibuffer = cx.new(|cx| {
21781 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21782 multibuffer.set_all_diff_hunks_expanded(cx);
21783 for buffer in &buffers {
21784 let snapshot = buffer.read(cx).snapshot();
21785 multibuffer.set_excerpts_for_path(
21786 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21787 buffer.clone(),
21788 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21789 2,
21790 cx,
21791 );
21792 }
21793 multibuffer
21794 });
21795
21796 let editor = cx.add_window(|window, cx| {
21797 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21798 });
21799 cx.run_until_parked();
21800
21801 let snapshot = editor
21802 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21803 .unwrap();
21804 let hunks = snapshot
21805 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21806 .map(|hunk| match hunk {
21807 DisplayDiffHunk::Unfolded {
21808 display_row_range, ..
21809 } => display_row_range,
21810 DisplayDiffHunk::Folded { .. } => unreachable!(),
21811 })
21812 .collect::<Vec<_>>();
21813 assert_eq!(
21814 hunks,
21815 [
21816 DisplayRow(2)..DisplayRow(4),
21817 DisplayRow(7)..DisplayRow(9),
21818 DisplayRow(12)..DisplayRow(14),
21819 ]
21820 );
21821}
21822
21823#[gpui::test]
21824async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21825 init_test(cx, |_| {});
21826
21827 let mut cx = EditorTestContext::new(cx).await;
21828 cx.set_head_text(indoc! { "
21829 one
21830 two
21831 three
21832 four
21833 five
21834 "
21835 });
21836 cx.set_index_text(indoc! { "
21837 one
21838 two
21839 three
21840 four
21841 five
21842 "
21843 });
21844 cx.set_state(indoc! {"
21845 one
21846 TWO
21847 ˇTHREE
21848 FOUR
21849 five
21850 "});
21851 cx.run_until_parked();
21852 cx.update_editor(|editor, window, cx| {
21853 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21854 });
21855 cx.run_until_parked();
21856 cx.assert_index_text(Some(indoc! {"
21857 one
21858 TWO
21859 THREE
21860 FOUR
21861 five
21862 "}));
21863 cx.set_state(indoc! { "
21864 one
21865 TWO
21866 ˇTHREE-HUNDRED
21867 FOUR
21868 five
21869 "});
21870 cx.run_until_parked();
21871 cx.update_editor(|editor, window, cx| {
21872 let snapshot = editor.snapshot(window, cx);
21873 let hunks = editor
21874 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21875 .collect::<Vec<_>>();
21876 assert_eq!(hunks.len(), 1);
21877 assert_eq!(
21878 hunks[0].status(),
21879 DiffHunkStatus {
21880 kind: DiffHunkStatusKind::Modified,
21881 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21882 }
21883 );
21884
21885 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21886 });
21887 cx.run_until_parked();
21888 cx.assert_index_text(Some(indoc! {"
21889 one
21890 TWO
21891 THREE-HUNDRED
21892 FOUR
21893 five
21894 "}));
21895}
21896
21897#[gpui::test]
21898fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21899 init_test(cx, |_| {});
21900
21901 let editor = cx.add_window(|window, cx| {
21902 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21903 build_editor(buffer, window, cx)
21904 });
21905
21906 let render_args = Arc::new(Mutex::new(None));
21907 let snapshot = editor
21908 .update(cx, |editor, window, cx| {
21909 let snapshot = editor.buffer().read(cx).snapshot(cx);
21910 let range =
21911 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21912
21913 struct RenderArgs {
21914 row: MultiBufferRow,
21915 folded: bool,
21916 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21917 }
21918
21919 let crease = Crease::inline(
21920 range,
21921 FoldPlaceholder::test(),
21922 {
21923 let toggle_callback = render_args.clone();
21924 move |row, folded, callback, _window, _cx| {
21925 *toggle_callback.lock() = Some(RenderArgs {
21926 row,
21927 folded,
21928 callback,
21929 });
21930 div()
21931 }
21932 },
21933 |_row, _folded, _window, _cx| div(),
21934 );
21935
21936 editor.insert_creases(Some(crease), cx);
21937 let snapshot = editor.snapshot(window, cx);
21938 let _div =
21939 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21940 snapshot
21941 })
21942 .unwrap();
21943
21944 let render_args = render_args.lock().take().unwrap();
21945 assert_eq!(render_args.row, MultiBufferRow(1));
21946 assert!(!render_args.folded);
21947 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21948
21949 cx.update_window(*editor, |_, window, cx| {
21950 (render_args.callback)(true, window, cx)
21951 })
21952 .unwrap();
21953 let snapshot = editor
21954 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21955 .unwrap();
21956 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21957
21958 cx.update_window(*editor, |_, window, cx| {
21959 (render_args.callback)(false, window, cx)
21960 })
21961 .unwrap();
21962 let snapshot = editor
21963 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21964 .unwrap();
21965 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21966}
21967
21968#[gpui::test]
21969async fn test_input_text(cx: &mut TestAppContext) {
21970 init_test(cx, |_| {});
21971 let mut cx = EditorTestContext::new(cx).await;
21972
21973 cx.set_state(
21974 &r#"ˇone
21975 two
21976
21977 three
21978 fourˇ
21979 five
21980
21981 siˇx"#
21982 .unindent(),
21983 );
21984
21985 cx.dispatch_action(HandleInput(String::new()));
21986 cx.assert_editor_state(
21987 &r#"ˇone
21988 two
21989
21990 three
21991 fourˇ
21992 five
21993
21994 siˇx"#
21995 .unindent(),
21996 );
21997
21998 cx.dispatch_action(HandleInput("AAAA".to_string()));
21999 cx.assert_editor_state(
22000 &r#"AAAAˇone
22001 two
22002
22003 three
22004 fourAAAAˇ
22005 five
22006
22007 siAAAAˇx"#
22008 .unindent(),
22009 );
22010}
22011
22012#[gpui::test]
22013async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22014 init_test(cx, |_| {});
22015
22016 let mut cx = EditorTestContext::new(cx).await;
22017 cx.set_state(
22018 r#"let foo = 1;
22019let foo = 2;
22020let foo = 3;
22021let fooˇ = 4;
22022let foo = 5;
22023let foo = 6;
22024let foo = 7;
22025let foo = 8;
22026let foo = 9;
22027let foo = 10;
22028let foo = 11;
22029let foo = 12;
22030let foo = 13;
22031let foo = 14;
22032let foo = 15;"#,
22033 );
22034
22035 cx.update_editor(|e, window, cx| {
22036 assert_eq!(
22037 e.next_scroll_position,
22038 NextScrollCursorCenterTopBottom::Center,
22039 "Default next scroll direction is center",
22040 );
22041
22042 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22043 assert_eq!(
22044 e.next_scroll_position,
22045 NextScrollCursorCenterTopBottom::Top,
22046 "After center, next scroll direction should be top",
22047 );
22048
22049 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22050 assert_eq!(
22051 e.next_scroll_position,
22052 NextScrollCursorCenterTopBottom::Bottom,
22053 "After top, next scroll direction should be bottom",
22054 );
22055
22056 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22057 assert_eq!(
22058 e.next_scroll_position,
22059 NextScrollCursorCenterTopBottom::Center,
22060 "After bottom, scrolling should start over",
22061 );
22062
22063 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22064 assert_eq!(
22065 e.next_scroll_position,
22066 NextScrollCursorCenterTopBottom::Top,
22067 "Scrolling continues if retriggered fast enough"
22068 );
22069 });
22070
22071 cx.executor()
22072 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22073 cx.executor().run_until_parked();
22074 cx.update_editor(|e, _, _| {
22075 assert_eq!(
22076 e.next_scroll_position,
22077 NextScrollCursorCenterTopBottom::Center,
22078 "If scrolling is not triggered fast enough, it should reset"
22079 );
22080 });
22081}
22082
22083#[gpui::test]
22084async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22085 init_test(cx, |_| {});
22086 let mut cx = EditorLspTestContext::new_rust(
22087 lsp::ServerCapabilities {
22088 definition_provider: Some(lsp::OneOf::Left(true)),
22089 references_provider: Some(lsp::OneOf::Left(true)),
22090 ..lsp::ServerCapabilities::default()
22091 },
22092 cx,
22093 )
22094 .await;
22095
22096 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22097 let go_to_definition = cx
22098 .lsp
22099 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22100 move |params, _| async move {
22101 if empty_go_to_definition {
22102 Ok(None)
22103 } else {
22104 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22105 uri: params.text_document_position_params.text_document.uri,
22106 range: lsp::Range::new(
22107 lsp::Position::new(4, 3),
22108 lsp::Position::new(4, 6),
22109 ),
22110 })))
22111 }
22112 },
22113 );
22114 let references = cx
22115 .lsp
22116 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22117 Ok(Some(vec![lsp::Location {
22118 uri: params.text_document_position.text_document.uri,
22119 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22120 }]))
22121 });
22122 (go_to_definition, references)
22123 };
22124
22125 cx.set_state(
22126 &r#"fn one() {
22127 let mut a = ˇtwo();
22128 }
22129
22130 fn two() {}"#
22131 .unindent(),
22132 );
22133 set_up_lsp_handlers(false, &mut cx);
22134 let navigated = cx
22135 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22136 .await
22137 .expect("Failed to navigate to definition");
22138 assert_eq!(
22139 navigated,
22140 Navigated::Yes,
22141 "Should have navigated to definition from the GetDefinition response"
22142 );
22143 cx.assert_editor_state(
22144 &r#"fn one() {
22145 let mut a = two();
22146 }
22147
22148 fn «twoˇ»() {}"#
22149 .unindent(),
22150 );
22151
22152 let editors = cx.update_workspace(|workspace, _, cx| {
22153 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22154 });
22155 cx.update_editor(|_, _, test_editor_cx| {
22156 assert_eq!(
22157 editors.len(),
22158 1,
22159 "Initially, only one, test, editor should be open in the workspace"
22160 );
22161 assert_eq!(
22162 test_editor_cx.entity(),
22163 editors.last().expect("Asserted len is 1").clone()
22164 );
22165 });
22166
22167 set_up_lsp_handlers(true, &mut cx);
22168 let navigated = cx
22169 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22170 .await
22171 .expect("Failed to navigate to lookup references");
22172 assert_eq!(
22173 navigated,
22174 Navigated::Yes,
22175 "Should have navigated to references as a fallback after empty GoToDefinition response"
22176 );
22177 // We should not change the selections in the existing file,
22178 // if opening another milti buffer with the references
22179 cx.assert_editor_state(
22180 &r#"fn one() {
22181 let mut a = two();
22182 }
22183
22184 fn «twoˇ»() {}"#
22185 .unindent(),
22186 );
22187 let editors = cx.update_workspace(|workspace, _, cx| {
22188 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22189 });
22190 cx.update_editor(|_, _, test_editor_cx| {
22191 assert_eq!(
22192 editors.len(),
22193 2,
22194 "After falling back to references search, we open a new editor with the results"
22195 );
22196 let references_fallback_text = editors
22197 .into_iter()
22198 .find(|new_editor| *new_editor != test_editor_cx.entity())
22199 .expect("Should have one non-test editor now")
22200 .read(test_editor_cx)
22201 .text(test_editor_cx);
22202 assert_eq!(
22203 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22204 "Should use the range from the references response and not the GoToDefinition one"
22205 );
22206 });
22207}
22208
22209#[gpui::test]
22210async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22211 init_test(cx, |_| {});
22212 cx.update(|cx| {
22213 let mut editor_settings = EditorSettings::get_global(cx).clone();
22214 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22215 EditorSettings::override_global(editor_settings, cx);
22216 });
22217 let mut cx = EditorLspTestContext::new_rust(
22218 lsp::ServerCapabilities {
22219 definition_provider: Some(lsp::OneOf::Left(true)),
22220 references_provider: Some(lsp::OneOf::Left(true)),
22221 ..lsp::ServerCapabilities::default()
22222 },
22223 cx,
22224 )
22225 .await;
22226 let original_state = r#"fn one() {
22227 let mut a = ˇtwo();
22228 }
22229
22230 fn two() {}"#
22231 .unindent();
22232 cx.set_state(&original_state);
22233
22234 let mut go_to_definition = cx
22235 .lsp
22236 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22237 move |_, _| async move { Ok(None) },
22238 );
22239 let _references = cx
22240 .lsp
22241 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22242 panic!("Should not call for references with no go to definition fallback")
22243 });
22244
22245 let navigated = cx
22246 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22247 .await
22248 .expect("Failed to navigate to lookup references");
22249 go_to_definition
22250 .next()
22251 .await
22252 .expect("Should have called the go_to_definition handler");
22253
22254 assert_eq!(
22255 navigated,
22256 Navigated::No,
22257 "Should have navigated to references as a fallback after empty GoToDefinition response"
22258 );
22259 cx.assert_editor_state(&original_state);
22260 let editors = cx.update_workspace(|workspace, _, cx| {
22261 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22262 });
22263 cx.update_editor(|_, _, _| {
22264 assert_eq!(
22265 editors.len(),
22266 1,
22267 "After unsuccessful fallback, no other editor should have been opened"
22268 );
22269 });
22270}
22271
22272#[gpui::test]
22273async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22274 init_test(cx, |_| {});
22275 let mut cx = EditorLspTestContext::new_rust(
22276 lsp::ServerCapabilities {
22277 references_provider: Some(lsp::OneOf::Left(true)),
22278 ..lsp::ServerCapabilities::default()
22279 },
22280 cx,
22281 )
22282 .await;
22283
22284 cx.set_state(
22285 &r#"
22286 fn one() {
22287 let mut a = two();
22288 }
22289
22290 fn ˇtwo() {}"#
22291 .unindent(),
22292 );
22293 cx.lsp
22294 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22295 Ok(Some(vec![
22296 lsp::Location {
22297 uri: params.text_document_position.text_document.uri.clone(),
22298 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22299 },
22300 lsp::Location {
22301 uri: params.text_document_position.text_document.uri,
22302 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22303 },
22304 ]))
22305 });
22306 let navigated = cx
22307 .update_editor(|editor, window, cx| {
22308 editor.find_all_references(&FindAllReferences, window, cx)
22309 })
22310 .unwrap()
22311 .await
22312 .expect("Failed to navigate to references");
22313 assert_eq!(
22314 navigated,
22315 Navigated::Yes,
22316 "Should have navigated to references from the FindAllReferences response"
22317 );
22318 cx.assert_editor_state(
22319 &r#"fn one() {
22320 let mut a = two();
22321 }
22322
22323 fn ˇtwo() {}"#
22324 .unindent(),
22325 );
22326
22327 let editors = cx.update_workspace(|workspace, _, cx| {
22328 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22329 });
22330 cx.update_editor(|_, _, _| {
22331 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22332 });
22333
22334 cx.set_state(
22335 &r#"fn one() {
22336 let mut a = ˇtwo();
22337 }
22338
22339 fn two() {}"#
22340 .unindent(),
22341 );
22342 let navigated = cx
22343 .update_editor(|editor, window, cx| {
22344 editor.find_all_references(&FindAllReferences, window, cx)
22345 })
22346 .unwrap()
22347 .await
22348 .expect("Failed to navigate to references");
22349 assert_eq!(
22350 navigated,
22351 Navigated::Yes,
22352 "Should have navigated to references from the FindAllReferences response"
22353 );
22354 cx.assert_editor_state(
22355 &r#"fn one() {
22356 let mut a = ˇtwo();
22357 }
22358
22359 fn two() {}"#
22360 .unindent(),
22361 );
22362 let editors = cx.update_workspace(|workspace, _, cx| {
22363 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22364 });
22365 cx.update_editor(|_, _, _| {
22366 assert_eq!(
22367 editors.len(),
22368 2,
22369 "should have re-used the previous multibuffer"
22370 );
22371 });
22372
22373 cx.set_state(
22374 &r#"fn one() {
22375 let mut a = ˇtwo();
22376 }
22377 fn three() {}
22378 fn two() {}"#
22379 .unindent(),
22380 );
22381 cx.lsp
22382 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22383 Ok(Some(vec![
22384 lsp::Location {
22385 uri: params.text_document_position.text_document.uri.clone(),
22386 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22387 },
22388 lsp::Location {
22389 uri: params.text_document_position.text_document.uri,
22390 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22391 },
22392 ]))
22393 });
22394 let navigated = cx
22395 .update_editor(|editor, window, cx| {
22396 editor.find_all_references(&FindAllReferences, window, cx)
22397 })
22398 .unwrap()
22399 .await
22400 .expect("Failed to navigate to references");
22401 assert_eq!(
22402 navigated,
22403 Navigated::Yes,
22404 "Should have navigated to references from the FindAllReferences response"
22405 );
22406 cx.assert_editor_state(
22407 &r#"fn one() {
22408 let mut a = ˇtwo();
22409 }
22410 fn three() {}
22411 fn two() {}"#
22412 .unindent(),
22413 );
22414 let editors = cx.update_workspace(|workspace, _, cx| {
22415 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22416 });
22417 cx.update_editor(|_, _, _| {
22418 assert_eq!(
22419 editors.len(),
22420 3,
22421 "should have used a new multibuffer as offsets changed"
22422 );
22423 });
22424}
22425#[gpui::test]
22426async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22427 init_test(cx, |_| {});
22428
22429 let language = Arc::new(Language::new(
22430 LanguageConfig::default(),
22431 Some(tree_sitter_rust::LANGUAGE.into()),
22432 ));
22433
22434 let text = r#"
22435 #[cfg(test)]
22436 mod tests() {
22437 #[test]
22438 fn runnable_1() {
22439 let a = 1;
22440 }
22441
22442 #[test]
22443 fn runnable_2() {
22444 let a = 1;
22445 let b = 2;
22446 }
22447 }
22448 "#
22449 .unindent();
22450
22451 let fs = FakeFs::new(cx.executor());
22452 fs.insert_file("/file.rs", Default::default()).await;
22453
22454 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22455 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22456 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22457 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
22458 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22459
22460 let editor = cx.new_window_entity(|window, cx| {
22461 Editor::new(
22462 EditorMode::full(),
22463 multi_buffer,
22464 Some(project.clone()),
22465 window,
22466 cx,
22467 )
22468 });
22469
22470 editor.update_in(cx, |editor, window, cx| {
22471 let snapshot = editor.buffer().read(cx).snapshot(cx);
22472 editor.tasks.insert(
22473 (buffer.read(cx).remote_id(), 3),
22474 RunnableTasks {
22475 templates: vec![],
22476 offset: snapshot.anchor_before(MultiBufferOffset(43)),
22477 column: 0,
22478 extra_variables: HashMap::default(),
22479 context_range: BufferOffset(43)..BufferOffset(85),
22480 },
22481 );
22482 editor.tasks.insert(
22483 (buffer.read(cx).remote_id(), 8),
22484 RunnableTasks {
22485 templates: vec![],
22486 offset: snapshot.anchor_before(MultiBufferOffset(86)),
22487 column: 0,
22488 extra_variables: HashMap::default(),
22489 context_range: BufferOffset(86)..BufferOffset(191),
22490 },
22491 );
22492
22493 // Test finding task when cursor is inside function body
22494 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22495 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22496 });
22497 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22498 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22499
22500 // Test finding task when cursor is on function name
22501 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22502 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22503 });
22504 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22505 assert_eq!(row, 8, "Should find task when cursor is on function name");
22506 });
22507}
22508
22509#[gpui::test]
22510async fn test_folding_buffers(cx: &mut TestAppContext) {
22511 init_test(cx, |_| {});
22512
22513 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22514 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22515 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22516
22517 let fs = FakeFs::new(cx.executor());
22518 fs.insert_tree(
22519 path!("/a"),
22520 json!({
22521 "first.rs": sample_text_1,
22522 "second.rs": sample_text_2,
22523 "third.rs": sample_text_3,
22524 }),
22525 )
22526 .await;
22527 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22528 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22529 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22530 let worktree = project.update(cx, |project, cx| {
22531 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22532 assert_eq!(worktrees.len(), 1);
22533 worktrees.pop().unwrap()
22534 });
22535 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22536
22537 let buffer_1 = project
22538 .update(cx, |project, cx| {
22539 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22540 })
22541 .await
22542 .unwrap();
22543 let buffer_2 = project
22544 .update(cx, |project, cx| {
22545 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22546 })
22547 .await
22548 .unwrap();
22549 let buffer_3 = project
22550 .update(cx, |project, cx| {
22551 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22552 })
22553 .await
22554 .unwrap();
22555
22556 let multi_buffer = cx.new(|cx| {
22557 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22558 multi_buffer.push_excerpts(
22559 buffer_1.clone(),
22560 [
22561 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22562 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22563 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22564 ],
22565 cx,
22566 );
22567 multi_buffer.push_excerpts(
22568 buffer_2.clone(),
22569 [
22570 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22571 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22572 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22573 ],
22574 cx,
22575 );
22576 multi_buffer.push_excerpts(
22577 buffer_3.clone(),
22578 [
22579 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22580 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22581 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22582 ],
22583 cx,
22584 );
22585 multi_buffer
22586 });
22587 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22588 Editor::new(
22589 EditorMode::full(),
22590 multi_buffer.clone(),
22591 Some(project.clone()),
22592 window,
22593 cx,
22594 )
22595 });
22596
22597 assert_eq!(
22598 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22599 "\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",
22600 );
22601
22602 multi_buffer_editor.update(cx, |editor, cx| {
22603 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22604 });
22605 assert_eq!(
22606 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22607 "\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",
22608 "After folding the first buffer, its text should not be displayed"
22609 );
22610
22611 multi_buffer_editor.update(cx, |editor, cx| {
22612 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22613 });
22614 assert_eq!(
22615 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22616 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22617 "After folding the second buffer, its text should not be displayed"
22618 );
22619
22620 multi_buffer_editor.update(cx, |editor, cx| {
22621 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22622 });
22623 assert_eq!(
22624 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22625 "\n\n\n\n\n",
22626 "After folding the third buffer, its text should not be displayed"
22627 );
22628
22629 // Emulate selection inside the fold logic, that should work
22630 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22631 editor
22632 .snapshot(window, cx)
22633 .next_line_boundary(Point::new(0, 4));
22634 });
22635
22636 multi_buffer_editor.update(cx, |editor, cx| {
22637 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22638 });
22639 assert_eq!(
22640 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22641 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22642 "After unfolding the second buffer, its text should be displayed"
22643 );
22644
22645 // Typing inside of buffer 1 causes that buffer to be unfolded.
22646 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22647 assert_eq!(
22648 multi_buffer
22649 .read(cx)
22650 .snapshot(cx)
22651 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22652 .collect::<String>(),
22653 "bbbb"
22654 );
22655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22656 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22657 });
22658 editor.handle_input("B", window, cx);
22659 });
22660
22661 assert_eq!(
22662 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22663 "\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",
22664 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22665 );
22666
22667 multi_buffer_editor.update(cx, |editor, cx| {
22668 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22669 });
22670 assert_eq!(
22671 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22672 "\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",
22673 "After unfolding the all buffers, all original text should be displayed"
22674 );
22675}
22676
22677#[gpui::test]
22678async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22679 init_test(cx, |_| {});
22680
22681 let sample_text_1 = "1111\n2222\n3333".to_string();
22682 let sample_text_2 = "4444\n5555\n6666".to_string();
22683 let sample_text_3 = "7777\n8888\n9999".to_string();
22684
22685 let fs = FakeFs::new(cx.executor());
22686 fs.insert_tree(
22687 path!("/a"),
22688 json!({
22689 "first.rs": sample_text_1,
22690 "second.rs": sample_text_2,
22691 "third.rs": sample_text_3,
22692 }),
22693 )
22694 .await;
22695 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22696 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22697 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22698 let worktree = project.update(cx, |project, cx| {
22699 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22700 assert_eq!(worktrees.len(), 1);
22701 worktrees.pop().unwrap()
22702 });
22703 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22704
22705 let buffer_1 = project
22706 .update(cx, |project, cx| {
22707 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22708 })
22709 .await
22710 .unwrap();
22711 let buffer_2 = project
22712 .update(cx, |project, cx| {
22713 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22714 })
22715 .await
22716 .unwrap();
22717 let buffer_3 = project
22718 .update(cx, |project, cx| {
22719 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22720 })
22721 .await
22722 .unwrap();
22723
22724 let multi_buffer = cx.new(|cx| {
22725 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22726 multi_buffer.push_excerpts(
22727 buffer_1.clone(),
22728 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22729 cx,
22730 );
22731 multi_buffer.push_excerpts(
22732 buffer_2.clone(),
22733 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22734 cx,
22735 );
22736 multi_buffer.push_excerpts(
22737 buffer_3.clone(),
22738 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22739 cx,
22740 );
22741 multi_buffer
22742 });
22743
22744 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22745 Editor::new(
22746 EditorMode::full(),
22747 multi_buffer,
22748 Some(project.clone()),
22749 window,
22750 cx,
22751 )
22752 });
22753
22754 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22755 assert_eq!(
22756 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22757 full_text,
22758 );
22759
22760 multi_buffer_editor.update(cx, |editor, cx| {
22761 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22762 });
22763 assert_eq!(
22764 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22765 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22766 "After folding the first buffer, its text should not be displayed"
22767 );
22768
22769 multi_buffer_editor.update(cx, |editor, cx| {
22770 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22771 });
22772
22773 assert_eq!(
22774 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22775 "\n\n\n\n\n\n7777\n8888\n9999",
22776 "After folding the second buffer, its text should not be displayed"
22777 );
22778
22779 multi_buffer_editor.update(cx, |editor, cx| {
22780 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22781 });
22782 assert_eq!(
22783 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22784 "\n\n\n\n\n",
22785 "After folding the third buffer, its text should not be displayed"
22786 );
22787
22788 multi_buffer_editor.update(cx, |editor, cx| {
22789 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22790 });
22791 assert_eq!(
22792 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22793 "\n\n\n\n4444\n5555\n6666\n\n",
22794 "After unfolding the second buffer, its text should be displayed"
22795 );
22796
22797 multi_buffer_editor.update(cx, |editor, cx| {
22798 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22799 });
22800 assert_eq!(
22801 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22802 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22803 "After unfolding the first buffer, its text should be displayed"
22804 );
22805
22806 multi_buffer_editor.update(cx, |editor, cx| {
22807 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22808 });
22809 assert_eq!(
22810 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22811 full_text,
22812 "After unfolding all buffers, all original text should be displayed"
22813 );
22814}
22815
22816#[gpui::test]
22817async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22818 init_test(cx, |_| {});
22819
22820 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22821
22822 let fs = FakeFs::new(cx.executor());
22823 fs.insert_tree(
22824 path!("/a"),
22825 json!({
22826 "main.rs": sample_text,
22827 }),
22828 )
22829 .await;
22830 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22831 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22832 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22833 let worktree = project.update(cx, |project, cx| {
22834 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22835 assert_eq!(worktrees.len(), 1);
22836 worktrees.pop().unwrap()
22837 });
22838 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22839
22840 let buffer_1 = project
22841 .update(cx, |project, cx| {
22842 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22843 })
22844 .await
22845 .unwrap();
22846
22847 let multi_buffer = cx.new(|cx| {
22848 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22849 multi_buffer.push_excerpts(
22850 buffer_1.clone(),
22851 [ExcerptRange::new(
22852 Point::new(0, 0)
22853 ..Point::new(
22854 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22855 0,
22856 ),
22857 )],
22858 cx,
22859 );
22860 multi_buffer
22861 });
22862 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22863 Editor::new(
22864 EditorMode::full(),
22865 multi_buffer,
22866 Some(project.clone()),
22867 window,
22868 cx,
22869 )
22870 });
22871
22872 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22873 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22874 enum TestHighlight {}
22875 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22876 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22877 editor.highlight_text::<TestHighlight>(
22878 vec![highlight_range.clone()],
22879 HighlightStyle::color(Hsla::green()),
22880 cx,
22881 );
22882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22883 s.select_ranges(Some(highlight_range))
22884 });
22885 });
22886
22887 let full_text = format!("\n\n{sample_text}");
22888 assert_eq!(
22889 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22890 full_text,
22891 );
22892}
22893
22894#[gpui::test]
22895async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22896 init_test(cx, |_| {});
22897 cx.update(|cx| {
22898 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22899 "keymaps/default-linux.json",
22900 cx,
22901 )
22902 .unwrap();
22903 cx.bind_keys(default_key_bindings);
22904 });
22905
22906 let (editor, cx) = cx.add_window_view(|window, cx| {
22907 let multi_buffer = MultiBuffer::build_multi(
22908 [
22909 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22910 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22911 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22912 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22913 ],
22914 cx,
22915 );
22916 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22917
22918 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22919 // fold all but the second buffer, so that we test navigating between two
22920 // adjacent folded buffers, as well as folded buffers at the start and
22921 // end the multibuffer
22922 editor.fold_buffer(buffer_ids[0], cx);
22923 editor.fold_buffer(buffer_ids[2], cx);
22924 editor.fold_buffer(buffer_ids[3], cx);
22925
22926 editor
22927 });
22928 cx.simulate_resize(size(px(1000.), px(1000.)));
22929
22930 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22931 cx.assert_excerpts_with_selections(indoc! {"
22932 [EXCERPT]
22933 ˇ[FOLDED]
22934 [EXCERPT]
22935 a1
22936 b1
22937 [EXCERPT]
22938 [FOLDED]
22939 [EXCERPT]
22940 [FOLDED]
22941 "
22942 });
22943 cx.simulate_keystroke("down");
22944 cx.assert_excerpts_with_selections(indoc! {"
22945 [EXCERPT]
22946 [FOLDED]
22947 [EXCERPT]
22948 ˇa1
22949 b1
22950 [EXCERPT]
22951 [FOLDED]
22952 [EXCERPT]
22953 [FOLDED]
22954 "
22955 });
22956 cx.simulate_keystroke("down");
22957 cx.assert_excerpts_with_selections(indoc! {"
22958 [EXCERPT]
22959 [FOLDED]
22960 [EXCERPT]
22961 a1
22962 ˇb1
22963 [EXCERPT]
22964 [FOLDED]
22965 [EXCERPT]
22966 [FOLDED]
22967 "
22968 });
22969 cx.simulate_keystroke("down");
22970 cx.assert_excerpts_with_selections(indoc! {"
22971 [EXCERPT]
22972 [FOLDED]
22973 [EXCERPT]
22974 a1
22975 b1
22976 ˇ[EXCERPT]
22977 [FOLDED]
22978 [EXCERPT]
22979 [FOLDED]
22980 "
22981 });
22982 cx.simulate_keystroke("down");
22983 cx.assert_excerpts_with_selections(indoc! {"
22984 [EXCERPT]
22985 [FOLDED]
22986 [EXCERPT]
22987 a1
22988 b1
22989 [EXCERPT]
22990 ˇ[FOLDED]
22991 [EXCERPT]
22992 [FOLDED]
22993 "
22994 });
22995 for _ in 0..5 {
22996 cx.simulate_keystroke("down");
22997 cx.assert_excerpts_with_selections(indoc! {"
22998 [EXCERPT]
22999 [FOLDED]
23000 [EXCERPT]
23001 a1
23002 b1
23003 [EXCERPT]
23004 [FOLDED]
23005 [EXCERPT]
23006 ˇ[FOLDED]
23007 "
23008 });
23009 }
23010
23011 cx.simulate_keystroke("up");
23012 cx.assert_excerpts_with_selections(indoc! {"
23013 [EXCERPT]
23014 [FOLDED]
23015 [EXCERPT]
23016 a1
23017 b1
23018 [EXCERPT]
23019 ˇ[FOLDED]
23020 [EXCERPT]
23021 [FOLDED]
23022 "
23023 });
23024 cx.simulate_keystroke("up");
23025 cx.assert_excerpts_with_selections(indoc! {"
23026 [EXCERPT]
23027 [FOLDED]
23028 [EXCERPT]
23029 a1
23030 b1
23031 ˇ[EXCERPT]
23032 [FOLDED]
23033 [EXCERPT]
23034 [FOLDED]
23035 "
23036 });
23037 cx.simulate_keystroke("up");
23038 cx.assert_excerpts_with_selections(indoc! {"
23039 [EXCERPT]
23040 [FOLDED]
23041 [EXCERPT]
23042 a1
23043 ˇb1
23044 [EXCERPT]
23045 [FOLDED]
23046 [EXCERPT]
23047 [FOLDED]
23048 "
23049 });
23050 cx.simulate_keystroke("up");
23051 cx.assert_excerpts_with_selections(indoc! {"
23052 [EXCERPT]
23053 [FOLDED]
23054 [EXCERPT]
23055 ˇa1
23056 b1
23057 [EXCERPT]
23058 [FOLDED]
23059 [EXCERPT]
23060 [FOLDED]
23061 "
23062 });
23063 for _ in 0..5 {
23064 cx.simulate_keystroke("up");
23065 cx.assert_excerpts_with_selections(indoc! {"
23066 [EXCERPT]
23067 ˇ[FOLDED]
23068 [EXCERPT]
23069 a1
23070 b1
23071 [EXCERPT]
23072 [FOLDED]
23073 [EXCERPT]
23074 [FOLDED]
23075 "
23076 });
23077 }
23078}
23079
23080#[gpui::test]
23081async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23082 init_test(cx, |_| {});
23083
23084 // Simple insertion
23085 assert_highlighted_edits(
23086 "Hello, world!",
23087 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23088 true,
23089 cx,
23090 |highlighted_edits, cx| {
23091 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23092 assert_eq!(highlighted_edits.highlights.len(), 1);
23093 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23094 assert_eq!(
23095 highlighted_edits.highlights[0].1.background_color,
23096 Some(cx.theme().status().created_background)
23097 );
23098 },
23099 )
23100 .await;
23101
23102 // Replacement
23103 assert_highlighted_edits(
23104 "This is a test.",
23105 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23106 false,
23107 cx,
23108 |highlighted_edits, cx| {
23109 assert_eq!(highlighted_edits.text, "That is a test.");
23110 assert_eq!(highlighted_edits.highlights.len(), 1);
23111 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23112 assert_eq!(
23113 highlighted_edits.highlights[0].1.background_color,
23114 Some(cx.theme().status().created_background)
23115 );
23116 },
23117 )
23118 .await;
23119
23120 // Multiple edits
23121 assert_highlighted_edits(
23122 "Hello, world!",
23123 vec![
23124 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23125 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23126 ],
23127 false,
23128 cx,
23129 |highlighted_edits, cx| {
23130 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23131 assert_eq!(highlighted_edits.highlights.len(), 2);
23132 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23133 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23134 assert_eq!(
23135 highlighted_edits.highlights[0].1.background_color,
23136 Some(cx.theme().status().created_background)
23137 );
23138 assert_eq!(
23139 highlighted_edits.highlights[1].1.background_color,
23140 Some(cx.theme().status().created_background)
23141 );
23142 },
23143 )
23144 .await;
23145
23146 // Multiple lines with edits
23147 assert_highlighted_edits(
23148 "First line\nSecond line\nThird line\nFourth line",
23149 vec![
23150 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23151 (
23152 Point::new(2, 0)..Point::new(2, 10),
23153 "New third line".to_string(),
23154 ),
23155 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23156 ],
23157 false,
23158 cx,
23159 |highlighted_edits, cx| {
23160 assert_eq!(
23161 highlighted_edits.text,
23162 "Second modified\nNew third line\nFourth updated line"
23163 );
23164 assert_eq!(highlighted_edits.highlights.len(), 3);
23165 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23166 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23167 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23168 for highlight in &highlighted_edits.highlights {
23169 assert_eq!(
23170 highlight.1.background_color,
23171 Some(cx.theme().status().created_background)
23172 );
23173 }
23174 },
23175 )
23176 .await;
23177}
23178
23179#[gpui::test]
23180async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23181 init_test(cx, |_| {});
23182
23183 // Deletion
23184 assert_highlighted_edits(
23185 "Hello, world!",
23186 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23187 true,
23188 cx,
23189 |highlighted_edits, cx| {
23190 assert_eq!(highlighted_edits.text, "Hello, world!");
23191 assert_eq!(highlighted_edits.highlights.len(), 1);
23192 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23193 assert_eq!(
23194 highlighted_edits.highlights[0].1.background_color,
23195 Some(cx.theme().status().deleted_background)
23196 );
23197 },
23198 )
23199 .await;
23200
23201 // Insertion
23202 assert_highlighted_edits(
23203 "Hello, world!",
23204 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23205 true,
23206 cx,
23207 |highlighted_edits, cx| {
23208 assert_eq!(highlighted_edits.highlights.len(), 1);
23209 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23210 assert_eq!(
23211 highlighted_edits.highlights[0].1.background_color,
23212 Some(cx.theme().status().created_background)
23213 );
23214 },
23215 )
23216 .await;
23217}
23218
23219async fn assert_highlighted_edits(
23220 text: &str,
23221 edits: Vec<(Range<Point>, String)>,
23222 include_deletions: bool,
23223 cx: &mut TestAppContext,
23224 assertion_fn: impl Fn(HighlightedText, &App),
23225) {
23226 let window = cx.add_window(|window, cx| {
23227 let buffer = MultiBuffer::build_simple(text, cx);
23228 Editor::new(EditorMode::full(), buffer, None, window, cx)
23229 });
23230 let cx = &mut VisualTestContext::from_window(*window, cx);
23231
23232 let (buffer, snapshot) = window
23233 .update(cx, |editor, _window, cx| {
23234 (
23235 editor.buffer().clone(),
23236 editor.buffer().read(cx).snapshot(cx),
23237 )
23238 })
23239 .unwrap();
23240
23241 let edits = edits
23242 .into_iter()
23243 .map(|(range, edit)| {
23244 (
23245 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23246 edit,
23247 )
23248 })
23249 .collect::<Vec<_>>();
23250
23251 let text_anchor_edits = edits
23252 .clone()
23253 .into_iter()
23254 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23255 .collect::<Vec<_>>();
23256
23257 let edit_preview = window
23258 .update(cx, |_, _window, cx| {
23259 buffer
23260 .read(cx)
23261 .as_singleton()
23262 .unwrap()
23263 .read(cx)
23264 .preview_edits(text_anchor_edits.into(), cx)
23265 })
23266 .unwrap()
23267 .await;
23268
23269 cx.update(|_window, cx| {
23270 let highlighted_edits = edit_prediction_edit_text(
23271 snapshot.as_singleton().unwrap().2,
23272 &edits,
23273 &edit_preview,
23274 include_deletions,
23275 cx,
23276 );
23277 assertion_fn(highlighted_edits, cx)
23278 });
23279}
23280
23281#[track_caller]
23282fn assert_breakpoint(
23283 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23284 path: &Arc<Path>,
23285 expected: Vec<(u32, Breakpoint)>,
23286) {
23287 if expected.is_empty() {
23288 assert!(!breakpoints.contains_key(path), "{}", path.display());
23289 } else {
23290 let mut breakpoint = breakpoints
23291 .get(path)
23292 .unwrap()
23293 .iter()
23294 .map(|breakpoint| {
23295 (
23296 breakpoint.row,
23297 Breakpoint {
23298 message: breakpoint.message.clone(),
23299 state: breakpoint.state,
23300 condition: breakpoint.condition.clone(),
23301 hit_condition: breakpoint.hit_condition.clone(),
23302 },
23303 )
23304 })
23305 .collect::<Vec<_>>();
23306
23307 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23308
23309 assert_eq!(expected, breakpoint);
23310 }
23311}
23312
23313fn add_log_breakpoint_at_cursor(
23314 editor: &mut Editor,
23315 log_message: &str,
23316 window: &mut Window,
23317 cx: &mut Context<Editor>,
23318) {
23319 let (anchor, bp) = editor
23320 .breakpoints_at_cursors(window, cx)
23321 .first()
23322 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23323 .unwrap_or_else(|| {
23324 let snapshot = editor.snapshot(window, cx);
23325 let cursor_position: Point =
23326 editor.selections.newest(&snapshot.display_snapshot).head();
23327
23328 let breakpoint_position = snapshot
23329 .buffer_snapshot()
23330 .anchor_before(Point::new(cursor_position.row, 0));
23331
23332 (breakpoint_position, Breakpoint::new_log(log_message))
23333 });
23334
23335 editor.edit_breakpoint_at_anchor(
23336 anchor,
23337 bp,
23338 BreakpointEditAction::EditLogMessage(log_message.into()),
23339 cx,
23340 );
23341}
23342
23343#[gpui::test]
23344async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23345 init_test(cx, |_| {});
23346
23347 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23348 let fs = FakeFs::new(cx.executor());
23349 fs.insert_tree(
23350 path!("/a"),
23351 json!({
23352 "main.rs": sample_text,
23353 }),
23354 )
23355 .await;
23356 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23357 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23358 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23359
23360 let fs = FakeFs::new(cx.executor());
23361 fs.insert_tree(
23362 path!("/a"),
23363 json!({
23364 "main.rs": sample_text,
23365 }),
23366 )
23367 .await;
23368 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23369 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23370 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23371 let worktree_id = workspace
23372 .update(cx, |workspace, _window, cx| {
23373 workspace.project().update(cx, |project, cx| {
23374 project.worktrees(cx).next().unwrap().read(cx).id()
23375 })
23376 })
23377 .unwrap();
23378
23379 let buffer = project
23380 .update(cx, |project, cx| {
23381 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23382 })
23383 .await
23384 .unwrap();
23385
23386 let (editor, cx) = cx.add_window_view(|window, cx| {
23387 Editor::new(
23388 EditorMode::full(),
23389 MultiBuffer::build_from_buffer(buffer, cx),
23390 Some(project.clone()),
23391 window,
23392 cx,
23393 )
23394 });
23395
23396 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23397 let abs_path = project.read_with(cx, |project, cx| {
23398 project
23399 .absolute_path(&project_path, cx)
23400 .map(Arc::from)
23401 .unwrap()
23402 });
23403
23404 // assert we can add breakpoint on the first line
23405 editor.update_in(cx, |editor, window, cx| {
23406 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23407 editor.move_to_end(&MoveToEnd, window, cx);
23408 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23409 });
23410
23411 let breakpoints = editor.update(cx, |editor, cx| {
23412 editor
23413 .breakpoint_store()
23414 .as_ref()
23415 .unwrap()
23416 .read(cx)
23417 .all_source_breakpoints(cx)
23418 });
23419
23420 assert_eq!(1, breakpoints.len());
23421 assert_breakpoint(
23422 &breakpoints,
23423 &abs_path,
23424 vec![
23425 (0, Breakpoint::new_standard()),
23426 (3, Breakpoint::new_standard()),
23427 ],
23428 );
23429
23430 editor.update_in(cx, |editor, window, cx| {
23431 editor.move_to_beginning(&MoveToBeginning, window, cx);
23432 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23433 });
23434
23435 let breakpoints = editor.update(cx, |editor, cx| {
23436 editor
23437 .breakpoint_store()
23438 .as_ref()
23439 .unwrap()
23440 .read(cx)
23441 .all_source_breakpoints(cx)
23442 });
23443
23444 assert_eq!(1, breakpoints.len());
23445 assert_breakpoint(
23446 &breakpoints,
23447 &abs_path,
23448 vec![(3, Breakpoint::new_standard())],
23449 );
23450
23451 editor.update_in(cx, |editor, window, cx| {
23452 editor.move_to_end(&MoveToEnd, window, cx);
23453 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23454 });
23455
23456 let breakpoints = editor.update(cx, |editor, cx| {
23457 editor
23458 .breakpoint_store()
23459 .as_ref()
23460 .unwrap()
23461 .read(cx)
23462 .all_source_breakpoints(cx)
23463 });
23464
23465 assert_eq!(0, breakpoints.len());
23466 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23467}
23468
23469#[gpui::test]
23470async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23471 init_test(cx, |_| {});
23472
23473 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23474
23475 let fs = FakeFs::new(cx.executor());
23476 fs.insert_tree(
23477 path!("/a"),
23478 json!({
23479 "main.rs": sample_text,
23480 }),
23481 )
23482 .await;
23483 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23484 let (workspace, cx) =
23485 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23486
23487 let worktree_id = workspace.update(cx, |workspace, cx| {
23488 workspace.project().update(cx, |project, cx| {
23489 project.worktrees(cx).next().unwrap().read(cx).id()
23490 })
23491 });
23492
23493 let buffer = project
23494 .update(cx, |project, cx| {
23495 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23496 })
23497 .await
23498 .unwrap();
23499
23500 let (editor, cx) = cx.add_window_view(|window, cx| {
23501 Editor::new(
23502 EditorMode::full(),
23503 MultiBuffer::build_from_buffer(buffer, cx),
23504 Some(project.clone()),
23505 window,
23506 cx,
23507 )
23508 });
23509
23510 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23511 let abs_path = project.read_with(cx, |project, cx| {
23512 project
23513 .absolute_path(&project_path, cx)
23514 .map(Arc::from)
23515 .unwrap()
23516 });
23517
23518 editor.update_in(cx, |editor, window, cx| {
23519 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23520 });
23521
23522 let breakpoints = editor.update(cx, |editor, cx| {
23523 editor
23524 .breakpoint_store()
23525 .as_ref()
23526 .unwrap()
23527 .read(cx)
23528 .all_source_breakpoints(cx)
23529 });
23530
23531 assert_breakpoint(
23532 &breakpoints,
23533 &abs_path,
23534 vec![(0, Breakpoint::new_log("hello world"))],
23535 );
23536
23537 // Removing a log message from a log breakpoint should remove it
23538 editor.update_in(cx, |editor, window, cx| {
23539 add_log_breakpoint_at_cursor(editor, "", window, cx);
23540 });
23541
23542 let breakpoints = editor.update(cx, |editor, cx| {
23543 editor
23544 .breakpoint_store()
23545 .as_ref()
23546 .unwrap()
23547 .read(cx)
23548 .all_source_breakpoints(cx)
23549 });
23550
23551 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23552
23553 editor.update_in(cx, |editor, window, cx| {
23554 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23555 editor.move_to_end(&MoveToEnd, window, cx);
23556 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23557 // Not adding a log message to a standard breakpoint shouldn't remove it
23558 add_log_breakpoint_at_cursor(editor, "", window, cx);
23559 });
23560
23561 let breakpoints = editor.update(cx, |editor, cx| {
23562 editor
23563 .breakpoint_store()
23564 .as_ref()
23565 .unwrap()
23566 .read(cx)
23567 .all_source_breakpoints(cx)
23568 });
23569
23570 assert_breakpoint(
23571 &breakpoints,
23572 &abs_path,
23573 vec![
23574 (0, Breakpoint::new_standard()),
23575 (3, Breakpoint::new_standard()),
23576 ],
23577 );
23578
23579 editor.update_in(cx, |editor, window, cx| {
23580 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23581 });
23582
23583 let breakpoints = editor.update(cx, |editor, cx| {
23584 editor
23585 .breakpoint_store()
23586 .as_ref()
23587 .unwrap()
23588 .read(cx)
23589 .all_source_breakpoints(cx)
23590 });
23591
23592 assert_breakpoint(
23593 &breakpoints,
23594 &abs_path,
23595 vec![
23596 (0, Breakpoint::new_standard()),
23597 (3, Breakpoint::new_log("hello world")),
23598 ],
23599 );
23600
23601 editor.update_in(cx, |editor, window, cx| {
23602 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23603 });
23604
23605 let breakpoints = editor.update(cx, |editor, cx| {
23606 editor
23607 .breakpoint_store()
23608 .as_ref()
23609 .unwrap()
23610 .read(cx)
23611 .all_source_breakpoints(cx)
23612 });
23613
23614 assert_breakpoint(
23615 &breakpoints,
23616 &abs_path,
23617 vec![
23618 (0, Breakpoint::new_standard()),
23619 (3, Breakpoint::new_log("hello Earth!!")),
23620 ],
23621 );
23622}
23623
23624/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23625/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23626/// or when breakpoints were placed out of order. This tests for a regression too
23627#[gpui::test]
23628async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23629 init_test(cx, |_| {});
23630
23631 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23632 let fs = FakeFs::new(cx.executor());
23633 fs.insert_tree(
23634 path!("/a"),
23635 json!({
23636 "main.rs": sample_text,
23637 }),
23638 )
23639 .await;
23640 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23641 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23642 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23643
23644 let fs = FakeFs::new(cx.executor());
23645 fs.insert_tree(
23646 path!("/a"),
23647 json!({
23648 "main.rs": sample_text,
23649 }),
23650 )
23651 .await;
23652 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23653 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23654 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23655 let worktree_id = workspace
23656 .update(cx, |workspace, _window, cx| {
23657 workspace.project().update(cx, |project, cx| {
23658 project.worktrees(cx).next().unwrap().read(cx).id()
23659 })
23660 })
23661 .unwrap();
23662
23663 let buffer = project
23664 .update(cx, |project, cx| {
23665 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23666 })
23667 .await
23668 .unwrap();
23669
23670 let (editor, cx) = cx.add_window_view(|window, cx| {
23671 Editor::new(
23672 EditorMode::full(),
23673 MultiBuffer::build_from_buffer(buffer, cx),
23674 Some(project.clone()),
23675 window,
23676 cx,
23677 )
23678 });
23679
23680 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23681 let abs_path = project.read_with(cx, |project, cx| {
23682 project
23683 .absolute_path(&project_path, cx)
23684 .map(Arc::from)
23685 .unwrap()
23686 });
23687
23688 // assert we can add breakpoint on the first line
23689 editor.update_in(cx, |editor, window, cx| {
23690 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23691 editor.move_to_end(&MoveToEnd, window, cx);
23692 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23693 editor.move_up(&MoveUp, window, cx);
23694 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23695 });
23696
23697 let breakpoints = editor.update(cx, |editor, cx| {
23698 editor
23699 .breakpoint_store()
23700 .as_ref()
23701 .unwrap()
23702 .read(cx)
23703 .all_source_breakpoints(cx)
23704 });
23705
23706 assert_eq!(1, breakpoints.len());
23707 assert_breakpoint(
23708 &breakpoints,
23709 &abs_path,
23710 vec![
23711 (0, Breakpoint::new_standard()),
23712 (2, Breakpoint::new_standard()),
23713 (3, Breakpoint::new_standard()),
23714 ],
23715 );
23716
23717 editor.update_in(cx, |editor, window, cx| {
23718 editor.move_to_beginning(&MoveToBeginning, window, cx);
23719 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23720 editor.move_to_end(&MoveToEnd, window, cx);
23721 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23722 // Disabling a breakpoint that doesn't exist should do nothing
23723 editor.move_up(&MoveUp, window, cx);
23724 editor.move_up(&MoveUp, window, cx);
23725 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23726 });
23727
23728 let breakpoints = editor.update(cx, |editor, cx| {
23729 editor
23730 .breakpoint_store()
23731 .as_ref()
23732 .unwrap()
23733 .read(cx)
23734 .all_source_breakpoints(cx)
23735 });
23736
23737 let disable_breakpoint = {
23738 let mut bp = Breakpoint::new_standard();
23739 bp.state = BreakpointState::Disabled;
23740 bp
23741 };
23742
23743 assert_eq!(1, breakpoints.len());
23744 assert_breakpoint(
23745 &breakpoints,
23746 &abs_path,
23747 vec![
23748 (0, disable_breakpoint.clone()),
23749 (2, Breakpoint::new_standard()),
23750 (3, disable_breakpoint.clone()),
23751 ],
23752 );
23753
23754 editor.update_in(cx, |editor, window, cx| {
23755 editor.move_to_beginning(&MoveToBeginning, window, cx);
23756 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23757 editor.move_to_end(&MoveToEnd, window, cx);
23758 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23759 editor.move_up(&MoveUp, window, cx);
23760 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23761 });
23762
23763 let breakpoints = editor.update(cx, |editor, cx| {
23764 editor
23765 .breakpoint_store()
23766 .as_ref()
23767 .unwrap()
23768 .read(cx)
23769 .all_source_breakpoints(cx)
23770 });
23771
23772 assert_eq!(1, breakpoints.len());
23773 assert_breakpoint(
23774 &breakpoints,
23775 &abs_path,
23776 vec![
23777 (0, Breakpoint::new_standard()),
23778 (2, disable_breakpoint),
23779 (3, Breakpoint::new_standard()),
23780 ],
23781 );
23782}
23783
23784#[gpui::test]
23785async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23786 init_test(cx, |_| {});
23787 let capabilities = lsp::ServerCapabilities {
23788 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23789 prepare_provider: Some(true),
23790 work_done_progress_options: Default::default(),
23791 })),
23792 ..Default::default()
23793 };
23794 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23795
23796 cx.set_state(indoc! {"
23797 struct Fˇoo {}
23798 "});
23799
23800 cx.update_editor(|editor, _, cx| {
23801 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23802 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23803 editor.highlight_background::<DocumentHighlightRead>(
23804 &[highlight_range],
23805 |theme| theme.colors().editor_document_highlight_read_background,
23806 cx,
23807 );
23808 });
23809
23810 let mut prepare_rename_handler = cx
23811 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23812 move |_, _, _| async move {
23813 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23814 start: lsp::Position {
23815 line: 0,
23816 character: 7,
23817 },
23818 end: lsp::Position {
23819 line: 0,
23820 character: 10,
23821 },
23822 })))
23823 },
23824 );
23825 let prepare_rename_task = cx
23826 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23827 .expect("Prepare rename was not started");
23828 prepare_rename_handler.next().await.unwrap();
23829 prepare_rename_task.await.expect("Prepare rename failed");
23830
23831 let mut rename_handler =
23832 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23833 let edit = lsp::TextEdit {
23834 range: lsp::Range {
23835 start: lsp::Position {
23836 line: 0,
23837 character: 7,
23838 },
23839 end: lsp::Position {
23840 line: 0,
23841 character: 10,
23842 },
23843 },
23844 new_text: "FooRenamed".to_string(),
23845 };
23846 Ok(Some(lsp::WorkspaceEdit::new(
23847 // Specify the same edit twice
23848 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23849 )))
23850 });
23851 let rename_task = cx
23852 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23853 .expect("Confirm rename was not started");
23854 rename_handler.next().await.unwrap();
23855 rename_task.await.expect("Confirm rename failed");
23856 cx.run_until_parked();
23857
23858 // Despite two edits, only one is actually applied as those are identical
23859 cx.assert_editor_state(indoc! {"
23860 struct FooRenamedˇ {}
23861 "});
23862}
23863
23864#[gpui::test]
23865async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23866 init_test(cx, |_| {});
23867 // These capabilities indicate that the server does not support prepare rename.
23868 let capabilities = lsp::ServerCapabilities {
23869 rename_provider: Some(lsp::OneOf::Left(true)),
23870 ..Default::default()
23871 };
23872 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23873
23874 cx.set_state(indoc! {"
23875 struct Fˇoo {}
23876 "});
23877
23878 cx.update_editor(|editor, _window, cx| {
23879 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23880 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23881 editor.highlight_background::<DocumentHighlightRead>(
23882 &[highlight_range],
23883 |theme| theme.colors().editor_document_highlight_read_background,
23884 cx,
23885 );
23886 });
23887
23888 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23889 .expect("Prepare rename was not started")
23890 .await
23891 .expect("Prepare rename failed");
23892
23893 let mut rename_handler =
23894 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23895 let edit = lsp::TextEdit {
23896 range: lsp::Range {
23897 start: lsp::Position {
23898 line: 0,
23899 character: 7,
23900 },
23901 end: lsp::Position {
23902 line: 0,
23903 character: 10,
23904 },
23905 },
23906 new_text: "FooRenamed".to_string(),
23907 };
23908 Ok(Some(lsp::WorkspaceEdit::new(
23909 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23910 )))
23911 });
23912 let rename_task = cx
23913 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23914 .expect("Confirm rename was not started");
23915 rename_handler.next().await.unwrap();
23916 rename_task.await.expect("Confirm rename failed");
23917 cx.run_until_parked();
23918
23919 // Correct range is renamed, as `surrounding_word` is used to find it.
23920 cx.assert_editor_state(indoc! {"
23921 struct FooRenamedˇ {}
23922 "});
23923}
23924
23925#[gpui::test]
23926async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23927 init_test(cx, |_| {});
23928 let mut cx = EditorTestContext::new(cx).await;
23929
23930 let language = Arc::new(
23931 Language::new(
23932 LanguageConfig::default(),
23933 Some(tree_sitter_html::LANGUAGE.into()),
23934 )
23935 .with_brackets_query(
23936 r#"
23937 ("<" @open "/>" @close)
23938 ("</" @open ">" @close)
23939 ("<" @open ">" @close)
23940 ("\"" @open "\"" @close)
23941 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23942 "#,
23943 )
23944 .unwrap(),
23945 );
23946 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
23947
23948 cx.set_state(indoc! {"
23949 <span>ˇ</span>
23950 "});
23951 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23952 cx.assert_editor_state(indoc! {"
23953 <span>
23954 ˇ
23955 </span>
23956 "});
23957
23958 cx.set_state(indoc! {"
23959 <span><span></span>ˇ</span>
23960 "});
23961 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23962 cx.assert_editor_state(indoc! {"
23963 <span><span></span>
23964 ˇ</span>
23965 "});
23966
23967 cx.set_state(indoc! {"
23968 <span>ˇ
23969 </span>
23970 "});
23971 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23972 cx.assert_editor_state(indoc! {"
23973 <span>
23974 ˇ
23975 </span>
23976 "});
23977}
23978
23979#[gpui::test(iterations = 10)]
23980async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23981 init_test(cx, |_| {});
23982
23983 let fs = FakeFs::new(cx.executor());
23984 fs.insert_tree(
23985 path!("/dir"),
23986 json!({
23987 "a.ts": "a",
23988 }),
23989 )
23990 .await;
23991
23992 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23993 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23994 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23995
23996 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23997 language_registry.add(Arc::new(Language::new(
23998 LanguageConfig {
23999 name: "TypeScript".into(),
24000 matcher: LanguageMatcher {
24001 path_suffixes: vec!["ts".to_string()],
24002 ..Default::default()
24003 },
24004 ..Default::default()
24005 },
24006 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24007 )));
24008 let mut fake_language_servers = language_registry.register_fake_lsp(
24009 "TypeScript",
24010 FakeLspAdapter {
24011 capabilities: lsp::ServerCapabilities {
24012 code_lens_provider: Some(lsp::CodeLensOptions {
24013 resolve_provider: Some(true),
24014 }),
24015 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24016 commands: vec!["_the/command".to_string()],
24017 ..lsp::ExecuteCommandOptions::default()
24018 }),
24019 ..lsp::ServerCapabilities::default()
24020 },
24021 ..FakeLspAdapter::default()
24022 },
24023 );
24024
24025 let editor = workspace
24026 .update(cx, |workspace, window, cx| {
24027 workspace.open_abs_path(
24028 PathBuf::from(path!("/dir/a.ts")),
24029 OpenOptions::default(),
24030 window,
24031 cx,
24032 )
24033 })
24034 .unwrap()
24035 .await
24036 .unwrap()
24037 .downcast::<Editor>()
24038 .unwrap();
24039 cx.executor().run_until_parked();
24040
24041 let fake_server = fake_language_servers.next().await.unwrap();
24042
24043 let buffer = editor.update(cx, |editor, cx| {
24044 editor
24045 .buffer()
24046 .read(cx)
24047 .as_singleton()
24048 .expect("have opened a single file by path")
24049 });
24050
24051 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24052 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24053 drop(buffer_snapshot);
24054 let actions = cx
24055 .update_window(*workspace, |_, window, cx| {
24056 project.code_actions(&buffer, anchor..anchor, window, cx)
24057 })
24058 .unwrap();
24059
24060 fake_server
24061 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24062 Ok(Some(vec![
24063 lsp::CodeLens {
24064 range: lsp::Range::default(),
24065 command: Some(lsp::Command {
24066 title: "Code lens command".to_owned(),
24067 command: "_the/command".to_owned(),
24068 arguments: None,
24069 }),
24070 data: None,
24071 },
24072 lsp::CodeLens {
24073 range: lsp::Range::default(),
24074 command: Some(lsp::Command {
24075 title: "Command not in capabilities".to_owned(),
24076 command: "not in capabilities".to_owned(),
24077 arguments: None,
24078 }),
24079 data: None,
24080 },
24081 lsp::CodeLens {
24082 range: lsp::Range {
24083 start: lsp::Position {
24084 line: 1,
24085 character: 1,
24086 },
24087 end: lsp::Position {
24088 line: 1,
24089 character: 1,
24090 },
24091 },
24092 command: Some(lsp::Command {
24093 title: "Command not in range".to_owned(),
24094 command: "_the/command".to_owned(),
24095 arguments: None,
24096 }),
24097 data: None,
24098 },
24099 ]))
24100 })
24101 .next()
24102 .await;
24103
24104 let actions = actions.await.unwrap();
24105 assert_eq!(
24106 actions.len(),
24107 1,
24108 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24109 );
24110 let action = actions[0].clone();
24111 let apply = project.update(cx, |project, cx| {
24112 project.apply_code_action(buffer.clone(), action, true, cx)
24113 });
24114
24115 // Resolving the code action does not populate its edits. In absence of
24116 // edits, we must execute the given command.
24117 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24118 |mut lens, _| async move {
24119 let lens_command = lens.command.as_mut().expect("should have a command");
24120 assert_eq!(lens_command.title, "Code lens command");
24121 lens_command.arguments = Some(vec![json!("the-argument")]);
24122 Ok(lens)
24123 },
24124 );
24125
24126 // While executing the command, the language server sends the editor
24127 // a `workspaceEdit` request.
24128 fake_server
24129 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24130 let fake = fake_server.clone();
24131 move |params, _| {
24132 assert_eq!(params.command, "_the/command");
24133 let fake = fake.clone();
24134 async move {
24135 fake.server
24136 .request::<lsp::request::ApplyWorkspaceEdit>(
24137 lsp::ApplyWorkspaceEditParams {
24138 label: None,
24139 edit: lsp::WorkspaceEdit {
24140 changes: Some(
24141 [(
24142 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24143 vec![lsp::TextEdit {
24144 range: lsp::Range::new(
24145 lsp::Position::new(0, 0),
24146 lsp::Position::new(0, 0),
24147 ),
24148 new_text: "X".into(),
24149 }],
24150 )]
24151 .into_iter()
24152 .collect(),
24153 ),
24154 ..lsp::WorkspaceEdit::default()
24155 },
24156 },
24157 )
24158 .await
24159 .into_response()
24160 .unwrap();
24161 Ok(Some(json!(null)))
24162 }
24163 }
24164 })
24165 .next()
24166 .await;
24167
24168 // Applying the code lens command returns a project transaction containing the edits
24169 // sent by the language server in its `workspaceEdit` request.
24170 let transaction = apply.await.unwrap();
24171 assert!(transaction.0.contains_key(&buffer));
24172 buffer.update(cx, |buffer, cx| {
24173 assert_eq!(buffer.text(), "Xa");
24174 buffer.undo(cx);
24175 assert_eq!(buffer.text(), "a");
24176 });
24177
24178 let actions_after_edits = cx
24179 .update_window(*workspace, |_, window, cx| {
24180 project.code_actions(&buffer, anchor..anchor, window, cx)
24181 })
24182 .unwrap()
24183 .await
24184 .unwrap();
24185 assert_eq!(
24186 actions, actions_after_edits,
24187 "For the same selection, same code lens actions should be returned"
24188 );
24189
24190 let _responses =
24191 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24192 panic!("No more code lens requests are expected");
24193 });
24194 editor.update_in(cx, |editor, window, cx| {
24195 editor.select_all(&SelectAll, window, cx);
24196 });
24197 cx.executor().run_until_parked();
24198 let new_actions = cx
24199 .update_window(*workspace, |_, window, cx| {
24200 project.code_actions(&buffer, anchor..anchor, window, cx)
24201 })
24202 .unwrap()
24203 .await
24204 .unwrap();
24205 assert_eq!(
24206 actions, new_actions,
24207 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24208 );
24209}
24210
24211#[gpui::test]
24212async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24213 init_test(cx, |_| {});
24214
24215 let fs = FakeFs::new(cx.executor());
24216 let main_text = r#"fn main() {
24217println!("1");
24218println!("2");
24219println!("3");
24220println!("4");
24221println!("5");
24222}"#;
24223 let lib_text = "mod foo {}";
24224 fs.insert_tree(
24225 path!("/a"),
24226 json!({
24227 "lib.rs": lib_text,
24228 "main.rs": main_text,
24229 }),
24230 )
24231 .await;
24232
24233 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24234 let (workspace, cx) =
24235 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24236 let worktree_id = workspace.update(cx, |workspace, cx| {
24237 workspace.project().update(cx, |project, cx| {
24238 project.worktrees(cx).next().unwrap().read(cx).id()
24239 })
24240 });
24241
24242 let expected_ranges = vec![
24243 Point::new(0, 0)..Point::new(0, 0),
24244 Point::new(1, 0)..Point::new(1, 1),
24245 Point::new(2, 0)..Point::new(2, 2),
24246 Point::new(3, 0)..Point::new(3, 3),
24247 ];
24248
24249 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24250 let editor_1 = workspace
24251 .update_in(cx, |workspace, window, cx| {
24252 workspace.open_path(
24253 (worktree_id, rel_path("main.rs")),
24254 Some(pane_1.downgrade()),
24255 true,
24256 window,
24257 cx,
24258 )
24259 })
24260 .unwrap()
24261 .await
24262 .downcast::<Editor>()
24263 .unwrap();
24264 pane_1.update(cx, |pane, cx| {
24265 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24266 open_editor.update(cx, |editor, cx| {
24267 assert_eq!(
24268 editor.display_text(cx),
24269 main_text,
24270 "Original main.rs text on initial open",
24271 );
24272 assert_eq!(
24273 editor
24274 .selections
24275 .all::<Point>(&editor.display_snapshot(cx))
24276 .into_iter()
24277 .map(|s| s.range())
24278 .collect::<Vec<_>>(),
24279 vec![Point::zero()..Point::zero()],
24280 "Default selections on initial open",
24281 );
24282 })
24283 });
24284 editor_1.update_in(cx, |editor, window, cx| {
24285 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24286 s.select_ranges(expected_ranges.clone());
24287 });
24288 });
24289
24290 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24291 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24292 });
24293 let editor_2 = workspace
24294 .update_in(cx, |workspace, window, cx| {
24295 workspace.open_path(
24296 (worktree_id, rel_path("main.rs")),
24297 Some(pane_2.downgrade()),
24298 true,
24299 window,
24300 cx,
24301 )
24302 })
24303 .unwrap()
24304 .await
24305 .downcast::<Editor>()
24306 .unwrap();
24307 pane_2.update(cx, |pane, cx| {
24308 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24309 open_editor.update(cx, |editor, cx| {
24310 assert_eq!(
24311 editor.display_text(cx),
24312 main_text,
24313 "Original main.rs text on initial open in another panel",
24314 );
24315 assert_eq!(
24316 editor
24317 .selections
24318 .all::<Point>(&editor.display_snapshot(cx))
24319 .into_iter()
24320 .map(|s| s.range())
24321 .collect::<Vec<_>>(),
24322 vec![Point::zero()..Point::zero()],
24323 "Default selections on initial open in another panel",
24324 );
24325 })
24326 });
24327
24328 editor_2.update_in(cx, |editor, window, cx| {
24329 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24330 });
24331
24332 let _other_editor_1 = workspace
24333 .update_in(cx, |workspace, window, cx| {
24334 workspace.open_path(
24335 (worktree_id, rel_path("lib.rs")),
24336 Some(pane_1.downgrade()),
24337 true,
24338 window,
24339 cx,
24340 )
24341 })
24342 .unwrap()
24343 .await
24344 .downcast::<Editor>()
24345 .unwrap();
24346 pane_1
24347 .update_in(cx, |pane, window, cx| {
24348 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24349 })
24350 .await
24351 .unwrap();
24352 drop(editor_1);
24353 pane_1.update(cx, |pane, cx| {
24354 pane.active_item()
24355 .unwrap()
24356 .downcast::<Editor>()
24357 .unwrap()
24358 .update(cx, |editor, cx| {
24359 assert_eq!(
24360 editor.display_text(cx),
24361 lib_text,
24362 "Other file should be open and active",
24363 );
24364 });
24365 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24366 });
24367
24368 let _other_editor_2 = workspace
24369 .update_in(cx, |workspace, window, cx| {
24370 workspace.open_path(
24371 (worktree_id, rel_path("lib.rs")),
24372 Some(pane_2.downgrade()),
24373 true,
24374 window,
24375 cx,
24376 )
24377 })
24378 .unwrap()
24379 .await
24380 .downcast::<Editor>()
24381 .unwrap();
24382 pane_2
24383 .update_in(cx, |pane, window, cx| {
24384 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24385 })
24386 .await
24387 .unwrap();
24388 drop(editor_2);
24389 pane_2.update(cx, |pane, cx| {
24390 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24391 open_editor.update(cx, |editor, cx| {
24392 assert_eq!(
24393 editor.display_text(cx),
24394 lib_text,
24395 "Other file should be open and active in another panel too",
24396 );
24397 });
24398 assert_eq!(
24399 pane.items().count(),
24400 1,
24401 "No other editors should be open in another pane",
24402 );
24403 });
24404
24405 let _editor_1_reopened = workspace
24406 .update_in(cx, |workspace, window, cx| {
24407 workspace.open_path(
24408 (worktree_id, rel_path("main.rs")),
24409 Some(pane_1.downgrade()),
24410 true,
24411 window,
24412 cx,
24413 )
24414 })
24415 .unwrap()
24416 .await
24417 .downcast::<Editor>()
24418 .unwrap();
24419 let _editor_2_reopened = workspace
24420 .update_in(cx, |workspace, window, cx| {
24421 workspace.open_path(
24422 (worktree_id, rel_path("main.rs")),
24423 Some(pane_2.downgrade()),
24424 true,
24425 window,
24426 cx,
24427 )
24428 })
24429 .unwrap()
24430 .await
24431 .downcast::<Editor>()
24432 .unwrap();
24433 pane_1.update(cx, |pane, cx| {
24434 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24435 open_editor.update(cx, |editor, cx| {
24436 assert_eq!(
24437 editor.display_text(cx),
24438 main_text,
24439 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24440 );
24441 assert_eq!(
24442 editor
24443 .selections
24444 .all::<Point>(&editor.display_snapshot(cx))
24445 .into_iter()
24446 .map(|s| s.range())
24447 .collect::<Vec<_>>(),
24448 expected_ranges,
24449 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24450 );
24451 })
24452 });
24453 pane_2.update(cx, |pane, cx| {
24454 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24455 open_editor.update(cx, |editor, cx| {
24456 assert_eq!(
24457 editor.display_text(cx),
24458 r#"fn main() {
24459⋯rintln!("1");
24460⋯intln!("2");
24461⋯ntln!("3");
24462println!("4");
24463println!("5");
24464}"#,
24465 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24466 );
24467 assert_eq!(
24468 editor
24469 .selections
24470 .all::<Point>(&editor.display_snapshot(cx))
24471 .into_iter()
24472 .map(|s| s.range())
24473 .collect::<Vec<_>>(),
24474 vec![Point::zero()..Point::zero()],
24475 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24476 );
24477 })
24478 });
24479}
24480
24481#[gpui::test]
24482async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24483 init_test(cx, |_| {});
24484
24485 let fs = FakeFs::new(cx.executor());
24486 let main_text = r#"fn main() {
24487println!("1");
24488println!("2");
24489println!("3");
24490println!("4");
24491println!("5");
24492}"#;
24493 let lib_text = "mod foo {}";
24494 fs.insert_tree(
24495 path!("/a"),
24496 json!({
24497 "lib.rs": lib_text,
24498 "main.rs": main_text,
24499 }),
24500 )
24501 .await;
24502
24503 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24504 let (workspace, cx) =
24505 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24506 let worktree_id = workspace.update(cx, |workspace, cx| {
24507 workspace.project().update(cx, |project, cx| {
24508 project.worktrees(cx).next().unwrap().read(cx).id()
24509 })
24510 });
24511
24512 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24513 let editor = workspace
24514 .update_in(cx, |workspace, window, cx| {
24515 workspace.open_path(
24516 (worktree_id, rel_path("main.rs")),
24517 Some(pane.downgrade()),
24518 true,
24519 window,
24520 cx,
24521 )
24522 })
24523 .unwrap()
24524 .await
24525 .downcast::<Editor>()
24526 .unwrap();
24527 pane.update(cx, |pane, cx| {
24528 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24529 open_editor.update(cx, |editor, cx| {
24530 assert_eq!(
24531 editor.display_text(cx),
24532 main_text,
24533 "Original main.rs text on initial open",
24534 );
24535 })
24536 });
24537 editor.update_in(cx, |editor, window, cx| {
24538 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24539 });
24540
24541 cx.update_global(|store: &mut SettingsStore, cx| {
24542 store.update_user_settings(cx, |s| {
24543 s.workspace.restore_on_file_reopen = Some(false);
24544 });
24545 });
24546 editor.update_in(cx, |editor, window, cx| {
24547 editor.fold_ranges(
24548 vec![
24549 Point::new(1, 0)..Point::new(1, 1),
24550 Point::new(2, 0)..Point::new(2, 2),
24551 Point::new(3, 0)..Point::new(3, 3),
24552 ],
24553 false,
24554 window,
24555 cx,
24556 );
24557 });
24558 pane.update_in(cx, |pane, window, cx| {
24559 pane.close_all_items(&CloseAllItems::default(), window, cx)
24560 })
24561 .await
24562 .unwrap();
24563 pane.update(cx, |pane, _| {
24564 assert!(pane.active_item().is_none());
24565 });
24566 cx.update_global(|store: &mut SettingsStore, cx| {
24567 store.update_user_settings(cx, |s| {
24568 s.workspace.restore_on_file_reopen = Some(true);
24569 });
24570 });
24571
24572 let _editor_reopened = workspace
24573 .update_in(cx, |workspace, window, cx| {
24574 workspace.open_path(
24575 (worktree_id, rel_path("main.rs")),
24576 Some(pane.downgrade()),
24577 true,
24578 window,
24579 cx,
24580 )
24581 })
24582 .unwrap()
24583 .await
24584 .downcast::<Editor>()
24585 .unwrap();
24586 pane.update(cx, |pane, cx| {
24587 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24588 open_editor.update(cx, |editor, cx| {
24589 assert_eq!(
24590 editor.display_text(cx),
24591 main_text,
24592 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24593 );
24594 })
24595 });
24596}
24597
24598#[gpui::test]
24599async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24600 struct EmptyModalView {
24601 focus_handle: gpui::FocusHandle,
24602 }
24603 impl EventEmitter<DismissEvent> for EmptyModalView {}
24604 impl Render for EmptyModalView {
24605 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24606 div()
24607 }
24608 }
24609 impl Focusable for EmptyModalView {
24610 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24611 self.focus_handle.clone()
24612 }
24613 }
24614 impl workspace::ModalView for EmptyModalView {}
24615 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24616 EmptyModalView {
24617 focus_handle: cx.focus_handle(),
24618 }
24619 }
24620
24621 init_test(cx, |_| {});
24622
24623 let fs = FakeFs::new(cx.executor());
24624 let project = Project::test(fs, [], cx).await;
24625 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24626 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24627 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24628 let editor = cx.new_window_entity(|window, cx| {
24629 Editor::new(
24630 EditorMode::full(),
24631 buffer,
24632 Some(project.clone()),
24633 window,
24634 cx,
24635 )
24636 });
24637 workspace
24638 .update(cx, |workspace, window, cx| {
24639 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24640 })
24641 .unwrap();
24642 editor.update_in(cx, |editor, window, cx| {
24643 editor.open_context_menu(&OpenContextMenu, window, cx);
24644 assert!(editor.mouse_context_menu.is_some());
24645 });
24646 workspace
24647 .update(cx, |workspace, window, cx| {
24648 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24649 })
24650 .unwrap();
24651 cx.read(|cx| {
24652 assert!(editor.read(cx).mouse_context_menu.is_none());
24653 });
24654}
24655
24656fn set_linked_edit_ranges(
24657 opening: (Point, Point),
24658 closing: (Point, Point),
24659 editor: &mut Editor,
24660 cx: &mut Context<Editor>,
24661) {
24662 let Some((buffer, _)) = editor
24663 .buffer
24664 .read(cx)
24665 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24666 else {
24667 panic!("Failed to get buffer for selection position");
24668 };
24669 let buffer = buffer.read(cx);
24670 let buffer_id = buffer.remote_id();
24671 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24672 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24673 let mut linked_ranges = HashMap::default();
24674 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24675 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24676}
24677
24678#[gpui::test]
24679async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24680 init_test(cx, |_| {});
24681
24682 let fs = FakeFs::new(cx.executor());
24683 fs.insert_file(path!("/file.html"), Default::default())
24684 .await;
24685
24686 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24687
24688 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24689 let html_language = Arc::new(Language::new(
24690 LanguageConfig {
24691 name: "HTML".into(),
24692 matcher: LanguageMatcher {
24693 path_suffixes: vec!["html".to_string()],
24694 ..LanguageMatcher::default()
24695 },
24696 brackets: BracketPairConfig {
24697 pairs: vec![BracketPair {
24698 start: "<".into(),
24699 end: ">".into(),
24700 close: true,
24701 ..Default::default()
24702 }],
24703 ..Default::default()
24704 },
24705 ..Default::default()
24706 },
24707 Some(tree_sitter_html::LANGUAGE.into()),
24708 ));
24709 language_registry.add(html_language);
24710 let mut fake_servers = language_registry.register_fake_lsp(
24711 "HTML",
24712 FakeLspAdapter {
24713 capabilities: lsp::ServerCapabilities {
24714 completion_provider: Some(lsp::CompletionOptions {
24715 resolve_provider: Some(true),
24716 ..Default::default()
24717 }),
24718 ..Default::default()
24719 },
24720 ..Default::default()
24721 },
24722 );
24723
24724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24725 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24726
24727 let worktree_id = workspace
24728 .update(cx, |workspace, _window, cx| {
24729 workspace.project().update(cx, |project, cx| {
24730 project.worktrees(cx).next().unwrap().read(cx).id()
24731 })
24732 })
24733 .unwrap();
24734 project
24735 .update(cx, |project, cx| {
24736 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24737 })
24738 .await
24739 .unwrap();
24740 let editor = workspace
24741 .update(cx, |workspace, window, cx| {
24742 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24743 })
24744 .unwrap()
24745 .await
24746 .unwrap()
24747 .downcast::<Editor>()
24748 .unwrap();
24749
24750 let fake_server = fake_servers.next().await.unwrap();
24751 editor.update_in(cx, |editor, window, cx| {
24752 editor.set_text("<ad></ad>", window, cx);
24753 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24754 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24755 });
24756 set_linked_edit_ranges(
24757 (Point::new(0, 1), Point::new(0, 3)),
24758 (Point::new(0, 6), Point::new(0, 8)),
24759 editor,
24760 cx,
24761 );
24762 });
24763 let mut completion_handle =
24764 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24765 Ok(Some(lsp::CompletionResponse::Array(vec![
24766 lsp::CompletionItem {
24767 label: "head".to_string(),
24768 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24769 lsp::InsertReplaceEdit {
24770 new_text: "head".to_string(),
24771 insert: lsp::Range::new(
24772 lsp::Position::new(0, 1),
24773 lsp::Position::new(0, 3),
24774 ),
24775 replace: lsp::Range::new(
24776 lsp::Position::new(0, 1),
24777 lsp::Position::new(0, 3),
24778 ),
24779 },
24780 )),
24781 ..Default::default()
24782 },
24783 ])))
24784 });
24785 editor.update_in(cx, |editor, window, cx| {
24786 editor.show_completions(&ShowCompletions, window, cx);
24787 });
24788 cx.run_until_parked();
24789 completion_handle.next().await.unwrap();
24790 editor.update(cx, |editor, _| {
24791 assert!(
24792 editor.context_menu_visible(),
24793 "Completion menu should be visible"
24794 );
24795 });
24796 editor.update_in(cx, |editor, window, cx| {
24797 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24798 });
24799 cx.executor().run_until_parked();
24800 editor.update(cx, |editor, cx| {
24801 assert_eq!(editor.text(cx), "<head></head>");
24802 });
24803}
24804
24805#[gpui::test]
24806async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24807 init_test(cx, |_| {});
24808
24809 let mut cx = EditorTestContext::new(cx).await;
24810 let language = Arc::new(Language::new(
24811 LanguageConfig {
24812 name: "TSX".into(),
24813 matcher: LanguageMatcher {
24814 path_suffixes: vec!["tsx".to_string()],
24815 ..LanguageMatcher::default()
24816 },
24817 brackets: BracketPairConfig {
24818 pairs: vec![BracketPair {
24819 start: "<".into(),
24820 end: ">".into(),
24821 close: true,
24822 ..Default::default()
24823 }],
24824 ..Default::default()
24825 },
24826 linked_edit_characters: HashSet::from_iter(['.']),
24827 ..Default::default()
24828 },
24829 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24830 ));
24831 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
24832
24833 // Test typing > does not extend linked pair
24834 cx.set_state("<divˇ<div></div>");
24835 cx.update_editor(|editor, _, cx| {
24836 set_linked_edit_ranges(
24837 (Point::new(0, 1), Point::new(0, 4)),
24838 (Point::new(0, 11), Point::new(0, 14)),
24839 editor,
24840 cx,
24841 );
24842 });
24843 cx.update_editor(|editor, window, cx| {
24844 editor.handle_input(">", window, cx);
24845 });
24846 cx.assert_editor_state("<div>ˇ<div></div>");
24847
24848 // Test typing . do extend linked pair
24849 cx.set_state("<Animatedˇ></Animated>");
24850 cx.update_editor(|editor, _, cx| {
24851 set_linked_edit_ranges(
24852 (Point::new(0, 1), Point::new(0, 9)),
24853 (Point::new(0, 12), Point::new(0, 20)),
24854 editor,
24855 cx,
24856 );
24857 });
24858 cx.update_editor(|editor, window, cx| {
24859 editor.handle_input(".", window, cx);
24860 });
24861 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24862 cx.update_editor(|editor, _, cx| {
24863 set_linked_edit_ranges(
24864 (Point::new(0, 1), Point::new(0, 10)),
24865 (Point::new(0, 13), Point::new(0, 21)),
24866 editor,
24867 cx,
24868 );
24869 });
24870 cx.update_editor(|editor, window, cx| {
24871 editor.handle_input("V", window, cx);
24872 });
24873 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24874}
24875
24876#[gpui::test]
24877async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24878 init_test(cx, |_| {});
24879
24880 let fs = FakeFs::new(cx.executor());
24881 fs.insert_tree(
24882 path!("/root"),
24883 json!({
24884 "a": {
24885 "main.rs": "fn main() {}",
24886 },
24887 "foo": {
24888 "bar": {
24889 "external_file.rs": "pub mod external {}",
24890 }
24891 }
24892 }),
24893 )
24894 .await;
24895
24896 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24897 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24898 language_registry.add(rust_lang());
24899 let _fake_servers = language_registry.register_fake_lsp(
24900 "Rust",
24901 FakeLspAdapter {
24902 ..FakeLspAdapter::default()
24903 },
24904 );
24905 let (workspace, cx) =
24906 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24907 let worktree_id = workspace.update(cx, |workspace, cx| {
24908 workspace.project().update(cx, |project, cx| {
24909 project.worktrees(cx).next().unwrap().read(cx).id()
24910 })
24911 });
24912
24913 let assert_language_servers_count =
24914 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24915 project.update(cx, |project, cx| {
24916 let current = project
24917 .lsp_store()
24918 .read(cx)
24919 .as_local()
24920 .unwrap()
24921 .language_servers
24922 .len();
24923 assert_eq!(expected, current, "{context}");
24924 });
24925 };
24926
24927 assert_language_servers_count(
24928 0,
24929 "No servers should be running before any file is open",
24930 cx,
24931 );
24932 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24933 let main_editor = workspace
24934 .update_in(cx, |workspace, window, cx| {
24935 workspace.open_path(
24936 (worktree_id, rel_path("main.rs")),
24937 Some(pane.downgrade()),
24938 true,
24939 window,
24940 cx,
24941 )
24942 })
24943 .unwrap()
24944 .await
24945 .downcast::<Editor>()
24946 .unwrap();
24947 pane.update(cx, |pane, cx| {
24948 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24949 open_editor.update(cx, |editor, cx| {
24950 assert_eq!(
24951 editor.display_text(cx),
24952 "fn main() {}",
24953 "Original main.rs text on initial open",
24954 );
24955 });
24956 assert_eq!(open_editor, main_editor);
24957 });
24958 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24959
24960 let external_editor = workspace
24961 .update_in(cx, |workspace, window, cx| {
24962 workspace.open_abs_path(
24963 PathBuf::from("/root/foo/bar/external_file.rs"),
24964 OpenOptions::default(),
24965 window,
24966 cx,
24967 )
24968 })
24969 .await
24970 .expect("opening external file")
24971 .downcast::<Editor>()
24972 .expect("downcasted external file's open element to editor");
24973 pane.update(cx, |pane, cx| {
24974 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24975 open_editor.update(cx, |editor, cx| {
24976 assert_eq!(
24977 editor.display_text(cx),
24978 "pub mod external {}",
24979 "External file is open now",
24980 );
24981 });
24982 assert_eq!(open_editor, external_editor);
24983 });
24984 assert_language_servers_count(
24985 1,
24986 "Second, external, *.rs file should join the existing server",
24987 cx,
24988 );
24989
24990 pane.update_in(cx, |pane, window, cx| {
24991 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24992 })
24993 .await
24994 .unwrap();
24995 pane.update_in(cx, |pane, window, cx| {
24996 pane.navigate_backward(&Default::default(), window, cx);
24997 });
24998 cx.run_until_parked();
24999 pane.update(cx, |pane, cx| {
25000 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25001 open_editor.update(cx, |editor, cx| {
25002 assert_eq!(
25003 editor.display_text(cx),
25004 "pub mod external {}",
25005 "External file is open now",
25006 );
25007 });
25008 });
25009 assert_language_servers_count(
25010 1,
25011 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25012 cx,
25013 );
25014
25015 cx.update(|_, cx| {
25016 workspace::reload(cx);
25017 });
25018 assert_language_servers_count(
25019 1,
25020 "After reloading the worktree with local and external files opened, only one project should be started",
25021 cx,
25022 );
25023}
25024
25025#[gpui::test]
25026async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25027 init_test(cx, |_| {});
25028
25029 let mut cx = EditorTestContext::new(cx).await;
25030 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25031 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
25032
25033 // test cursor move to start of each line on tab
25034 // for `if`, `elif`, `else`, `while`, `with` and `for`
25035 cx.set_state(indoc! {"
25036 def main():
25037 ˇ for item in items:
25038 ˇ while item.active:
25039 ˇ if item.value > 10:
25040 ˇ continue
25041 ˇ elif item.value < 0:
25042 ˇ break
25043 ˇ else:
25044 ˇ with item.context() as ctx:
25045 ˇ yield count
25046 ˇ else:
25047 ˇ log('while else')
25048 ˇ else:
25049 ˇ log('for else')
25050 "});
25051 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25052 cx.assert_editor_state(indoc! {"
25053 def main():
25054 ˇfor item in items:
25055 ˇwhile item.active:
25056 ˇif item.value > 10:
25057 ˇcontinue
25058 ˇelif item.value < 0:
25059 ˇbreak
25060 ˇelse:
25061 ˇwith item.context() as ctx:
25062 ˇyield count
25063 ˇelse:
25064 ˇlog('while else')
25065 ˇelse:
25066 ˇlog('for else')
25067 "});
25068 // test relative indent is preserved when tab
25069 // for `if`, `elif`, `else`, `while`, `with` and `for`
25070 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25071 cx.assert_editor_state(indoc! {"
25072 def main():
25073 ˇfor item in items:
25074 ˇwhile item.active:
25075 ˇif item.value > 10:
25076 ˇcontinue
25077 ˇelif item.value < 0:
25078 ˇbreak
25079 ˇelse:
25080 ˇwith item.context() as ctx:
25081 ˇyield count
25082 ˇelse:
25083 ˇlog('while else')
25084 ˇelse:
25085 ˇlog('for else')
25086 "});
25087
25088 // test cursor move to start of each line on tab
25089 // for `try`, `except`, `else`, `finally`, `match` and `def`
25090 cx.set_state(indoc! {"
25091 def main():
25092 ˇ try:
25093 ˇ fetch()
25094 ˇ except ValueError:
25095 ˇ handle_error()
25096 ˇ else:
25097 ˇ match value:
25098 ˇ case _:
25099 ˇ finally:
25100 ˇ def status():
25101 ˇ return 0
25102 "});
25103 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25104 cx.assert_editor_state(indoc! {"
25105 def main():
25106 ˇtry:
25107 ˇfetch()
25108 ˇexcept ValueError:
25109 ˇhandle_error()
25110 ˇelse:
25111 ˇmatch value:
25112 ˇcase _:
25113 ˇfinally:
25114 ˇdef status():
25115 ˇreturn 0
25116 "});
25117 // test relative indent is preserved when tab
25118 // for `try`, `except`, `else`, `finally`, `match` and `def`
25119 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25120 cx.assert_editor_state(indoc! {"
25121 def main():
25122 ˇtry:
25123 ˇfetch()
25124 ˇexcept ValueError:
25125 ˇhandle_error()
25126 ˇelse:
25127 ˇmatch value:
25128 ˇcase _:
25129 ˇfinally:
25130 ˇdef status():
25131 ˇreturn 0
25132 "});
25133}
25134
25135#[gpui::test]
25136async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25137 init_test(cx, |_| {});
25138
25139 let mut cx = EditorTestContext::new(cx).await;
25140 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25141 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
25142
25143 // test `else` auto outdents when typed inside `if` block
25144 cx.set_state(indoc! {"
25145 def main():
25146 if i == 2:
25147 return
25148 ˇ
25149 "});
25150 cx.update_editor(|editor, window, cx| {
25151 editor.handle_input("else:", window, cx);
25152 });
25153 cx.assert_editor_state(indoc! {"
25154 def main():
25155 if i == 2:
25156 return
25157 else:ˇ
25158 "});
25159
25160 // test `except` auto outdents when typed inside `try` block
25161 cx.set_state(indoc! {"
25162 def main():
25163 try:
25164 i = 2
25165 ˇ
25166 "});
25167 cx.update_editor(|editor, window, cx| {
25168 editor.handle_input("except:", window, cx);
25169 });
25170 cx.assert_editor_state(indoc! {"
25171 def main():
25172 try:
25173 i = 2
25174 except:ˇ
25175 "});
25176
25177 // test `else` auto outdents when typed inside `except` block
25178 cx.set_state(indoc! {"
25179 def main():
25180 try:
25181 i = 2
25182 except:
25183 j = 2
25184 ˇ
25185 "});
25186 cx.update_editor(|editor, window, cx| {
25187 editor.handle_input("else:", window, cx);
25188 });
25189 cx.assert_editor_state(indoc! {"
25190 def main():
25191 try:
25192 i = 2
25193 except:
25194 j = 2
25195 else:ˇ
25196 "});
25197
25198 // test `finally` auto outdents when typed inside `else` block
25199 cx.set_state(indoc! {"
25200 def main():
25201 try:
25202 i = 2
25203 except:
25204 j = 2
25205 else:
25206 k = 2
25207 ˇ
25208 "});
25209 cx.update_editor(|editor, window, cx| {
25210 editor.handle_input("finally:", window, cx);
25211 });
25212 cx.assert_editor_state(indoc! {"
25213 def main():
25214 try:
25215 i = 2
25216 except:
25217 j = 2
25218 else:
25219 k = 2
25220 finally:ˇ
25221 "});
25222
25223 // test `else` does not outdents when typed inside `except` block right after for block
25224 cx.set_state(indoc! {"
25225 def main():
25226 try:
25227 i = 2
25228 except:
25229 for i in range(n):
25230 pass
25231 ˇ
25232 "});
25233 cx.update_editor(|editor, window, cx| {
25234 editor.handle_input("else:", window, cx);
25235 });
25236 cx.assert_editor_state(indoc! {"
25237 def main():
25238 try:
25239 i = 2
25240 except:
25241 for i in range(n):
25242 pass
25243 else:ˇ
25244 "});
25245
25246 // test `finally` auto outdents when typed inside `else` block right after for block
25247 cx.set_state(indoc! {"
25248 def main():
25249 try:
25250 i = 2
25251 except:
25252 j = 2
25253 else:
25254 for i in range(n):
25255 pass
25256 ˇ
25257 "});
25258 cx.update_editor(|editor, window, cx| {
25259 editor.handle_input("finally:", window, cx);
25260 });
25261 cx.assert_editor_state(indoc! {"
25262 def main():
25263 try:
25264 i = 2
25265 except:
25266 j = 2
25267 else:
25268 for i in range(n):
25269 pass
25270 finally:ˇ
25271 "});
25272
25273 // test `except` outdents to inner "try" block
25274 cx.set_state(indoc! {"
25275 def main():
25276 try:
25277 i = 2
25278 if i == 2:
25279 try:
25280 i = 3
25281 ˇ
25282 "});
25283 cx.update_editor(|editor, window, cx| {
25284 editor.handle_input("except:", window, cx);
25285 });
25286 cx.assert_editor_state(indoc! {"
25287 def main():
25288 try:
25289 i = 2
25290 if i == 2:
25291 try:
25292 i = 3
25293 except:ˇ
25294 "});
25295
25296 // test `except` outdents to outer "try" block
25297 cx.set_state(indoc! {"
25298 def main():
25299 try:
25300 i = 2
25301 if i == 2:
25302 try:
25303 i = 3
25304 ˇ
25305 "});
25306 cx.update_editor(|editor, window, cx| {
25307 editor.handle_input("except:", window, cx);
25308 });
25309 cx.assert_editor_state(indoc! {"
25310 def main():
25311 try:
25312 i = 2
25313 if i == 2:
25314 try:
25315 i = 3
25316 except:ˇ
25317 "});
25318
25319 // test `else` stays at correct indent when typed after `for` block
25320 cx.set_state(indoc! {"
25321 def main():
25322 for i in range(10):
25323 if i == 3:
25324 break
25325 ˇ
25326 "});
25327 cx.update_editor(|editor, window, cx| {
25328 editor.handle_input("else:", window, cx);
25329 });
25330 cx.assert_editor_state(indoc! {"
25331 def main():
25332 for i in range(10):
25333 if i == 3:
25334 break
25335 else:ˇ
25336 "});
25337
25338 // test does not outdent on typing after line with square brackets
25339 cx.set_state(indoc! {"
25340 def f() -> list[str]:
25341 ˇ
25342 "});
25343 cx.update_editor(|editor, window, cx| {
25344 editor.handle_input("a", window, cx);
25345 });
25346 cx.assert_editor_state(indoc! {"
25347 def f() -> list[str]:
25348 aˇ
25349 "});
25350
25351 // test does not outdent on typing : after case keyword
25352 cx.set_state(indoc! {"
25353 match 1:
25354 caseˇ
25355 "});
25356 cx.update_editor(|editor, window, cx| {
25357 editor.handle_input(":", window, cx);
25358 });
25359 cx.assert_editor_state(indoc! {"
25360 match 1:
25361 case:ˇ
25362 "});
25363}
25364
25365#[gpui::test]
25366async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25367 init_test(cx, |_| {});
25368 update_test_language_settings(cx, |settings| {
25369 settings.defaults.extend_comment_on_newline = Some(false);
25370 });
25371 let mut cx = EditorTestContext::new(cx).await;
25372 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25373 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
25374
25375 // test correct indent after newline on comment
25376 cx.set_state(indoc! {"
25377 # COMMENT:ˇ
25378 "});
25379 cx.update_editor(|editor, window, cx| {
25380 editor.newline(&Newline, window, cx);
25381 });
25382 cx.assert_editor_state(indoc! {"
25383 # COMMENT:
25384 ˇ
25385 "});
25386
25387 // test correct indent after newline in brackets
25388 cx.set_state(indoc! {"
25389 {ˇ}
25390 "});
25391 cx.update_editor(|editor, window, cx| {
25392 editor.newline(&Newline, window, cx);
25393 });
25394 cx.run_until_parked();
25395 cx.assert_editor_state(indoc! {"
25396 {
25397 ˇ
25398 }
25399 "});
25400
25401 cx.set_state(indoc! {"
25402 (ˇ)
25403 "});
25404 cx.update_editor(|editor, window, cx| {
25405 editor.newline(&Newline, window, cx);
25406 });
25407 cx.run_until_parked();
25408 cx.assert_editor_state(indoc! {"
25409 (
25410 ˇ
25411 )
25412 "});
25413
25414 // do not indent after empty lists or dictionaries
25415 cx.set_state(indoc! {"
25416 a = []ˇ
25417 "});
25418 cx.update_editor(|editor, window, cx| {
25419 editor.newline(&Newline, window, cx);
25420 });
25421 cx.run_until_parked();
25422 cx.assert_editor_state(indoc! {"
25423 a = []
25424 ˇ
25425 "});
25426}
25427
25428#[gpui::test]
25429async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25430 init_test(cx, |_| {});
25431
25432 let mut cx = EditorTestContext::new(cx).await;
25433 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25434 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
25435
25436 // test cursor move to start of each line on tab
25437 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25438 cx.set_state(indoc! {"
25439 function main() {
25440 ˇ for item in $items; do
25441 ˇ while [ -n \"$item\" ]; do
25442 ˇ if [ \"$value\" -gt 10 ]; then
25443 ˇ continue
25444 ˇ elif [ \"$value\" -lt 0 ]; then
25445 ˇ break
25446 ˇ else
25447 ˇ echo \"$item\"
25448 ˇ fi
25449 ˇ done
25450 ˇ done
25451 ˇ}
25452 "});
25453 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25454 cx.assert_editor_state(indoc! {"
25455 function main() {
25456 ˇfor item in $items; do
25457 ˇwhile [ -n \"$item\" ]; do
25458 ˇif [ \"$value\" -gt 10 ]; then
25459 ˇcontinue
25460 ˇelif [ \"$value\" -lt 0 ]; then
25461 ˇbreak
25462 ˇelse
25463 ˇecho \"$item\"
25464 ˇfi
25465 ˇdone
25466 ˇdone
25467 ˇ}
25468 "});
25469 // test relative indent is preserved when tab
25470 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25471 cx.assert_editor_state(indoc! {"
25472 function main() {
25473 ˇfor item in $items; do
25474 ˇwhile [ -n \"$item\" ]; do
25475 ˇif [ \"$value\" -gt 10 ]; then
25476 ˇcontinue
25477 ˇelif [ \"$value\" -lt 0 ]; then
25478 ˇbreak
25479 ˇelse
25480 ˇecho \"$item\"
25481 ˇfi
25482 ˇdone
25483 ˇdone
25484 ˇ}
25485 "});
25486
25487 // test cursor move to start of each line on tab
25488 // for `case` statement with patterns
25489 cx.set_state(indoc! {"
25490 function handle() {
25491 ˇ case \"$1\" in
25492 ˇ start)
25493 ˇ echo \"a\"
25494 ˇ ;;
25495 ˇ stop)
25496 ˇ echo \"b\"
25497 ˇ ;;
25498 ˇ *)
25499 ˇ echo \"c\"
25500 ˇ ;;
25501 ˇ esac
25502 ˇ}
25503 "});
25504 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25505 cx.assert_editor_state(indoc! {"
25506 function handle() {
25507 ˇcase \"$1\" in
25508 ˇstart)
25509 ˇecho \"a\"
25510 ˇ;;
25511 ˇstop)
25512 ˇecho \"b\"
25513 ˇ;;
25514 ˇ*)
25515 ˇecho \"c\"
25516 ˇ;;
25517 ˇesac
25518 ˇ}
25519 "});
25520}
25521
25522#[gpui::test]
25523async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25524 init_test(cx, |_| {});
25525
25526 let mut cx = EditorTestContext::new(cx).await;
25527 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25528 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
25529
25530 // test indents on comment insert
25531 cx.set_state(indoc! {"
25532 function main() {
25533 ˇ for item in $items; do
25534 ˇ while [ -n \"$item\" ]; do
25535 ˇ if [ \"$value\" -gt 10 ]; then
25536 ˇ continue
25537 ˇ elif [ \"$value\" -lt 0 ]; then
25538 ˇ break
25539 ˇ else
25540 ˇ echo \"$item\"
25541 ˇ fi
25542 ˇ done
25543 ˇ done
25544 ˇ}
25545 "});
25546 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25547 cx.assert_editor_state(indoc! {"
25548 function main() {
25549 #ˇ for item in $items; do
25550 #ˇ while [ -n \"$item\" ]; do
25551 #ˇ if [ \"$value\" -gt 10 ]; then
25552 #ˇ continue
25553 #ˇ elif [ \"$value\" -lt 0 ]; then
25554 #ˇ break
25555 #ˇ else
25556 #ˇ echo \"$item\"
25557 #ˇ fi
25558 #ˇ done
25559 #ˇ done
25560 #ˇ}
25561 "});
25562}
25563
25564#[gpui::test]
25565async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25566 init_test(cx, |_| {});
25567
25568 let mut cx = EditorTestContext::new(cx).await;
25569 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25570 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
25571
25572 // test `else` auto outdents when typed inside `if` block
25573 cx.set_state(indoc! {"
25574 if [ \"$1\" = \"test\" ]; then
25575 echo \"foo bar\"
25576 ˇ
25577 "});
25578 cx.update_editor(|editor, window, cx| {
25579 editor.handle_input("else", window, cx);
25580 });
25581 cx.assert_editor_state(indoc! {"
25582 if [ \"$1\" = \"test\" ]; then
25583 echo \"foo bar\"
25584 elseˇ
25585 "});
25586
25587 // test `elif` auto outdents when typed inside `if` block
25588 cx.set_state(indoc! {"
25589 if [ \"$1\" = \"test\" ]; then
25590 echo \"foo bar\"
25591 ˇ
25592 "});
25593 cx.update_editor(|editor, window, cx| {
25594 editor.handle_input("elif", window, cx);
25595 });
25596 cx.assert_editor_state(indoc! {"
25597 if [ \"$1\" = \"test\" ]; then
25598 echo \"foo bar\"
25599 elifˇ
25600 "});
25601
25602 // test `fi` auto outdents when typed inside `else` block
25603 cx.set_state(indoc! {"
25604 if [ \"$1\" = \"test\" ]; then
25605 echo \"foo bar\"
25606 else
25607 echo \"bar baz\"
25608 ˇ
25609 "});
25610 cx.update_editor(|editor, window, cx| {
25611 editor.handle_input("fi", window, cx);
25612 });
25613 cx.assert_editor_state(indoc! {"
25614 if [ \"$1\" = \"test\" ]; then
25615 echo \"foo bar\"
25616 else
25617 echo \"bar baz\"
25618 fiˇ
25619 "});
25620
25621 // test `done` auto outdents when typed inside `while` block
25622 cx.set_state(indoc! {"
25623 while read line; do
25624 echo \"$line\"
25625 ˇ
25626 "});
25627 cx.update_editor(|editor, window, cx| {
25628 editor.handle_input("done", window, cx);
25629 });
25630 cx.assert_editor_state(indoc! {"
25631 while read line; do
25632 echo \"$line\"
25633 doneˇ
25634 "});
25635
25636 // test `done` auto outdents when typed inside `for` block
25637 cx.set_state(indoc! {"
25638 for file in *.txt; do
25639 cat \"$file\"
25640 ˇ
25641 "});
25642 cx.update_editor(|editor, window, cx| {
25643 editor.handle_input("done", window, cx);
25644 });
25645 cx.assert_editor_state(indoc! {"
25646 for file in *.txt; do
25647 cat \"$file\"
25648 doneˇ
25649 "});
25650
25651 // test `esac` auto outdents when typed inside `case` block
25652 cx.set_state(indoc! {"
25653 case \"$1\" in
25654 start)
25655 echo \"foo bar\"
25656 ;;
25657 stop)
25658 echo \"bar baz\"
25659 ;;
25660 ˇ
25661 "});
25662 cx.update_editor(|editor, window, cx| {
25663 editor.handle_input("esac", window, cx);
25664 });
25665 cx.assert_editor_state(indoc! {"
25666 case \"$1\" in
25667 start)
25668 echo \"foo bar\"
25669 ;;
25670 stop)
25671 echo \"bar baz\"
25672 ;;
25673 esacˇ
25674 "});
25675
25676 // test `*)` auto outdents when typed inside `case` block
25677 cx.set_state(indoc! {"
25678 case \"$1\" in
25679 start)
25680 echo \"foo bar\"
25681 ;;
25682 ˇ
25683 "});
25684 cx.update_editor(|editor, window, cx| {
25685 editor.handle_input("*)", window, cx);
25686 });
25687 cx.assert_editor_state(indoc! {"
25688 case \"$1\" in
25689 start)
25690 echo \"foo bar\"
25691 ;;
25692 *)ˇ
25693 "});
25694
25695 // test `fi` outdents to correct level with nested if blocks
25696 cx.set_state(indoc! {"
25697 if [ \"$1\" = \"test\" ]; then
25698 echo \"outer if\"
25699 if [ \"$2\" = \"debug\" ]; then
25700 echo \"inner if\"
25701 ˇ
25702 "});
25703 cx.update_editor(|editor, window, cx| {
25704 editor.handle_input("fi", window, cx);
25705 });
25706 cx.assert_editor_state(indoc! {"
25707 if [ \"$1\" = \"test\" ]; then
25708 echo \"outer if\"
25709 if [ \"$2\" = \"debug\" ]; then
25710 echo \"inner if\"
25711 fiˇ
25712 "});
25713}
25714
25715#[gpui::test]
25716async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25717 init_test(cx, |_| {});
25718 update_test_language_settings(cx, |settings| {
25719 settings.defaults.extend_comment_on_newline = Some(false);
25720 });
25721 let mut cx = EditorTestContext::new(cx).await;
25722 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25723 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(language), cx));
25724
25725 // test correct indent after newline on comment
25726 cx.set_state(indoc! {"
25727 # COMMENT:ˇ
25728 "});
25729 cx.update_editor(|editor, window, cx| {
25730 editor.newline(&Newline, window, cx);
25731 });
25732 cx.assert_editor_state(indoc! {"
25733 # COMMENT:
25734 ˇ
25735 "});
25736
25737 // test correct indent after newline after `then`
25738 cx.set_state(indoc! {"
25739
25740 if [ \"$1\" = \"test\" ]; thenˇ
25741 "});
25742 cx.update_editor(|editor, window, cx| {
25743 editor.newline(&Newline, window, cx);
25744 });
25745 cx.run_until_parked();
25746 cx.assert_editor_state(indoc! {"
25747
25748 if [ \"$1\" = \"test\" ]; then
25749 ˇ
25750 "});
25751
25752 // test correct indent after newline after `else`
25753 cx.set_state(indoc! {"
25754 if [ \"$1\" = \"test\" ]; then
25755 elseˇ
25756 "});
25757 cx.update_editor(|editor, window, cx| {
25758 editor.newline(&Newline, window, cx);
25759 });
25760 cx.run_until_parked();
25761 cx.assert_editor_state(indoc! {"
25762 if [ \"$1\" = \"test\" ]; then
25763 else
25764 ˇ
25765 "});
25766
25767 // test correct indent after newline after `elif`
25768 cx.set_state(indoc! {"
25769 if [ \"$1\" = \"test\" ]; then
25770 elifˇ
25771 "});
25772 cx.update_editor(|editor, window, cx| {
25773 editor.newline(&Newline, window, cx);
25774 });
25775 cx.run_until_parked();
25776 cx.assert_editor_state(indoc! {"
25777 if [ \"$1\" = \"test\" ]; then
25778 elif
25779 ˇ
25780 "});
25781
25782 // test correct indent after newline after `do`
25783 cx.set_state(indoc! {"
25784 for file in *.txt; doˇ
25785 "});
25786 cx.update_editor(|editor, window, cx| {
25787 editor.newline(&Newline, window, cx);
25788 });
25789 cx.run_until_parked();
25790 cx.assert_editor_state(indoc! {"
25791 for file in *.txt; do
25792 ˇ
25793 "});
25794
25795 // test correct indent after newline after case pattern
25796 cx.set_state(indoc! {"
25797 case \"$1\" in
25798 start)ˇ
25799 "});
25800 cx.update_editor(|editor, window, cx| {
25801 editor.newline(&Newline, window, cx);
25802 });
25803 cx.run_until_parked();
25804 cx.assert_editor_state(indoc! {"
25805 case \"$1\" in
25806 start)
25807 ˇ
25808 "});
25809
25810 // test correct indent after newline after case pattern
25811 cx.set_state(indoc! {"
25812 case \"$1\" in
25813 start)
25814 ;;
25815 *)ˇ
25816 "});
25817 cx.update_editor(|editor, window, cx| {
25818 editor.newline(&Newline, window, cx);
25819 });
25820 cx.run_until_parked();
25821 cx.assert_editor_state(indoc! {"
25822 case \"$1\" in
25823 start)
25824 ;;
25825 *)
25826 ˇ
25827 "});
25828
25829 // test correct indent after newline after function opening brace
25830 cx.set_state(indoc! {"
25831 function test() {ˇ}
25832 "});
25833 cx.update_editor(|editor, window, cx| {
25834 editor.newline(&Newline, window, cx);
25835 });
25836 cx.run_until_parked();
25837 cx.assert_editor_state(indoc! {"
25838 function test() {
25839 ˇ
25840 }
25841 "});
25842
25843 // test no extra indent after semicolon on same line
25844 cx.set_state(indoc! {"
25845 echo \"test\";ˇ
25846 "});
25847 cx.update_editor(|editor, window, cx| {
25848 editor.newline(&Newline, window, cx);
25849 });
25850 cx.run_until_parked();
25851 cx.assert_editor_state(indoc! {"
25852 echo \"test\";
25853 ˇ
25854 "});
25855}
25856
25857fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25858 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25859 point..point
25860}
25861
25862#[track_caller]
25863fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25864 let (text, ranges) = marked_text_ranges(marked_text, true);
25865 assert_eq!(editor.text(cx), text);
25866 assert_eq!(
25867 editor.selections.ranges(&editor.display_snapshot(cx)),
25868 ranges
25869 .iter()
25870 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
25871 .collect::<Vec<_>>(),
25872 "Assert selections are {}",
25873 marked_text
25874 );
25875}
25876
25877pub fn handle_signature_help_request(
25878 cx: &mut EditorLspTestContext,
25879 mocked_response: lsp::SignatureHelp,
25880) -> impl Future<Output = ()> + use<> {
25881 let mut request =
25882 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25883 let mocked_response = mocked_response.clone();
25884 async move { Ok(Some(mocked_response)) }
25885 });
25886
25887 async move {
25888 request.next().await;
25889 }
25890}
25891
25892#[track_caller]
25893pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25894 cx.update_editor(|editor, _, _| {
25895 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25896 let entries = menu.entries.borrow();
25897 let entries = entries
25898 .iter()
25899 .map(|entry| entry.string.as_str())
25900 .collect::<Vec<_>>();
25901 assert_eq!(entries, expected);
25902 } else {
25903 panic!("Expected completions menu");
25904 }
25905 });
25906}
25907
25908#[gpui::test]
25909async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
25910 init_test(cx, |_| {});
25911 let mut cx = EditorLspTestContext::new_rust(
25912 lsp::ServerCapabilities {
25913 completion_provider: Some(lsp::CompletionOptions {
25914 ..Default::default()
25915 }),
25916 ..Default::default()
25917 },
25918 cx,
25919 )
25920 .await;
25921 cx.lsp
25922 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25923 Ok(Some(lsp::CompletionResponse::Array(vec![
25924 lsp::CompletionItem {
25925 label: "unsafe".into(),
25926 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25927 range: lsp::Range {
25928 start: lsp::Position {
25929 line: 0,
25930 character: 9,
25931 },
25932 end: lsp::Position {
25933 line: 0,
25934 character: 11,
25935 },
25936 },
25937 new_text: "unsafe".to_string(),
25938 })),
25939 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
25940 ..Default::default()
25941 },
25942 ])))
25943 });
25944
25945 cx.update_editor(|editor, _, cx| {
25946 editor.project().unwrap().update(cx, |project, cx| {
25947 project.snippets().update(cx, |snippets, _cx| {
25948 snippets.add_snippet_for_test(
25949 None,
25950 PathBuf::from("test_snippets.json"),
25951 vec![
25952 Arc::new(project::snippet_provider::Snippet {
25953 prefix: vec![
25954 "unlimited word count".to_string(),
25955 "unlimit word count".to_string(),
25956 "unlimited unknown".to_string(),
25957 ],
25958 body: "this is many words".to_string(),
25959 description: Some("description".to_string()),
25960 name: "multi-word snippet test".to_string(),
25961 }),
25962 Arc::new(project::snippet_provider::Snippet {
25963 prefix: vec!["unsnip".to_string(), "@few".to_string()],
25964 body: "fewer words".to_string(),
25965 description: Some("alt description".to_string()),
25966 name: "other name".to_string(),
25967 }),
25968 Arc::new(project::snippet_provider::Snippet {
25969 prefix: vec!["ab aa".to_string()],
25970 body: "abcd".to_string(),
25971 description: None,
25972 name: "alphabet".to_string(),
25973 }),
25974 ],
25975 );
25976 });
25977 })
25978 });
25979
25980 let get_completions = |cx: &mut EditorLspTestContext| {
25981 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
25982 Some(CodeContextMenu::Completions(context_menu)) => {
25983 let entries = context_menu.entries.borrow();
25984 entries
25985 .iter()
25986 .map(|entry| entry.string.clone())
25987 .collect_vec()
25988 }
25989 _ => vec![],
25990 })
25991 };
25992
25993 // snippets:
25994 // @foo
25995 // foo bar
25996 //
25997 // when typing:
25998 //
25999 // when typing:
26000 // - if I type a symbol "open the completions with snippets only"
26001 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26002 //
26003 // stuff we need:
26004 // - filtering logic change?
26005 // - remember how far back the completion started.
26006
26007 let test_cases: &[(&str, &[&str])] = &[
26008 (
26009 "un",
26010 &[
26011 "unsafe",
26012 "unlimit word count",
26013 "unlimited unknown",
26014 "unlimited word count",
26015 "unsnip",
26016 ],
26017 ),
26018 (
26019 "u ",
26020 &[
26021 "unlimit word count",
26022 "unlimited unknown",
26023 "unlimited word count",
26024 ],
26025 ),
26026 ("u a", &["ab aa", "unsafe"]), // unsAfe
26027 (
26028 "u u",
26029 &[
26030 "unsafe",
26031 "unlimit word count",
26032 "unlimited unknown", // ranked highest among snippets
26033 "unlimited word count",
26034 "unsnip",
26035 ],
26036 ),
26037 ("uw c", &["unlimit word count", "unlimited word count"]),
26038 (
26039 "u w",
26040 &[
26041 "unlimit word count",
26042 "unlimited word count",
26043 "unlimited unknown",
26044 ],
26045 ),
26046 ("u w ", &["unlimit word count", "unlimited word count"]),
26047 (
26048 "u ",
26049 &[
26050 "unlimit word count",
26051 "unlimited unknown",
26052 "unlimited word count",
26053 ],
26054 ),
26055 ("wor", &[]),
26056 ("uf", &["unsafe"]),
26057 ("af", &["unsafe"]),
26058 ("afu", &[]),
26059 (
26060 "ue",
26061 &["unsafe", "unlimited unknown", "unlimited word count"],
26062 ),
26063 ("@", &["@few"]),
26064 ("@few", &["@few"]),
26065 ("@ ", &[]),
26066 ("a@", &["@few"]),
26067 ("a@f", &["@few", "unsafe"]),
26068 ("a@fw", &["@few"]),
26069 ("a", &["ab aa", "unsafe"]),
26070 ("aa", &["ab aa"]),
26071 ("aaa", &["ab aa"]),
26072 ("ab", &["ab aa"]),
26073 ("ab ", &["ab aa"]),
26074 ("ab a", &["ab aa", "unsafe"]),
26075 ("ab ab", &["ab aa"]),
26076 ("ab ab aa", &["ab aa"]),
26077 ];
26078
26079 for &(input_to_simulate, expected_completions) in test_cases {
26080 cx.set_state("fn a() { ˇ }\n");
26081 for c in input_to_simulate.split("") {
26082 cx.simulate_input(c);
26083 cx.run_until_parked();
26084 }
26085 let expected_completions = expected_completions
26086 .iter()
26087 .map(|s| s.to_string())
26088 .collect_vec();
26089 assert_eq!(
26090 get_completions(&mut cx),
26091 expected_completions,
26092 "< actual / expected >, input = {input_to_simulate:?}",
26093 );
26094 }
26095}
26096
26097/// Handle completion request passing a marked string specifying where the completion
26098/// should be triggered from using '|' character, what range should be replaced, and what completions
26099/// should be returned using '<' and '>' to delimit the range.
26100///
26101/// Also see `handle_completion_request_with_insert_and_replace`.
26102#[track_caller]
26103pub fn handle_completion_request(
26104 marked_string: &str,
26105 completions: Vec<&'static str>,
26106 is_incomplete: bool,
26107 counter: Arc<AtomicUsize>,
26108 cx: &mut EditorLspTestContext,
26109) -> impl Future<Output = ()> {
26110 let complete_from_marker: TextRangeMarker = '|'.into();
26111 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26112 let (_, mut marked_ranges) = marked_text_ranges_by(
26113 marked_string,
26114 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26115 );
26116
26117 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26118 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26119 ));
26120 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26121 let replace_range =
26122 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26123
26124 let mut request =
26125 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26126 let completions = completions.clone();
26127 counter.fetch_add(1, atomic::Ordering::Release);
26128 async move {
26129 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26130 assert_eq!(
26131 params.text_document_position.position,
26132 complete_from_position
26133 );
26134 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26135 is_incomplete,
26136 item_defaults: None,
26137 items: completions
26138 .iter()
26139 .map(|completion_text| lsp::CompletionItem {
26140 label: completion_text.to_string(),
26141 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26142 range: replace_range,
26143 new_text: completion_text.to_string(),
26144 })),
26145 ..Default::default()
26146 })
26147 .collect(),
26148 })))
26149 }
26150 });
26151
26152 async move {
26153 request.next().await;
26154 }
26155}
26156
26157/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26158/// given instead, which also contains an `insert` range.
26159///
26160/// This function uses markers to define ranges:
26161/// - `|` marks the cursor position
26162/// - `<>` marks the replace range
26163/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26164pub fn handle_completion_request_with_insert_and_replace(
26165 cx: &mut EditorLspTestContext,
26166 marked_string: &str,
26167 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26168 counter: Arc<AtomicUsize>,
26169) -> impl Future<Output = ()> {
26170 let complete_from_marker: TextRangeMarker = '|'.into();
26171 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26172 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26173
26174 let (_, mut marked_ranges) = marked_text_ranges_by(
26175 marked_string,
26176 vec![
26177 complete_from_marker.clone(),
26178 replace_range_marker.clone(),
26179 insert_range_marker.clone(),
26180 ],
26181 );
26182
26183 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26184 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26185 ));
26186 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26187 let replace_range =
26188 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26189
26190 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26191 Some(ranges) if !ranges.is_empty() => {
26192 let range1 = ranges[0].clone();
26193 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26194 }
26195 _ => lsp::Range {
26196 start: replace_range.start,
26197 end: complete_from_position,
26198 },
26199 };
26200
26201 let mut request =
26202 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26203 let completions = completions.clone();
26204 counter.fetch_add(1, atomic::Ordering::Release);
26205 async move {
26206 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26207 assert_eq!(
26208 params.text_document_position.position, complete_from_position,
26209 "marker `|` position doesn't match",
26210 );
26211 Ok(Some(lsp::CompletionResponse::Array(
26212 completions
26213 .iter()
26214 .map(|(label, new_text)| lsp::CompletionItem {
26215 label: label.to_string(),
26216 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26217 lsp::InsertReplaceEdit {
26218 insert: insert_range,
26219 replace: replace_range,
26220 new_text: new_text.to_string(),
26221 },
26222 )),
26223 ..Default::default()
26224 })
26225 .collect(),
26226 )))
26227 }
26228 });
26229
26230 async move {
26231 request.next().await;
26232 }
26233}
26234
26235fn handle_resolve_completion_request(
26236 cx: &mut EditorLspTestContext,
26237 edits: Option<Vec<(&'static str, &'static str)>>,
26238) -> impl Future<Output = ()> {
26239 let edits = edits.map(|edits| {
26240 edits
26241 .iter()
26242 .map(|(marked_string, new_text)| {
26243 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26244 let replace_range = cx.to_lsp_range(
26245 MultiBufferOffset(marked_ranges[0].start)
26246 ..MultiBufferOffset(marked_ranges[0].end),
26247 );
26248 lsp::TextEdit::new(replace_range, new_text.to_string())
26249 })
26250 .collect::<Vec<_>>()
26251 });
26252
26253 let mut request =
26254 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26255 let edits = edits.clone();
26256 async move {
26257 Ok(lsp::CompletionItem {
26258 additional_text_edits: edits,
26259 ..Default::default()
26260 })
26261 }
26262 });
26263
26264 async move {
26265 request.next().await;
26266 }
26267}
26268
26269pub(crate) fn update_test_language_settings(
26270 cx: &mut TestAppContext,
26271 f: impl Fn(&mut AllLanguageSettingsContent),
26272) {
26273 cx.update(|cx| {
26274 SettingsStore::update_global(cx, |store, cx| {
26275 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26276 });
26277 });
26278}
26279
26280pub(crate) fn update_test_project_settings(
26281 cx: &mut TestAppContext,
26282 f: impl Fn(&mut ProjectSettingsContent),
26283) {
26284 cx.update(|cx| {
26285 SettingsStore::update_global(cx, |store, cx| {
26286 store.update_user_settings(cx, |settings| f(&mut settings.project));
26287 });
26288 });
26289}
26290
26291pub(crate) fn update_test_editor_settings(
26292 cx: &mut TestAppContext,
26293 f: impl Fn(&mut EditorSettingsContent),
26294) {
26295 cx.update(|cx| {
26296 SettingsStore::update_global(cx, |store, cx| {
26297 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26298 })
26299 })
26300}
26301
26302pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26303 cx.update(|cx| {
26304 assets::Assets.load_test_fonts(cx);
26305 let store = SettingsStore::test(cx);
26306 cx.set_global(store);
26307 theme::init(theme::LoadThemes::JustBase, cx);
26308 release_channel::init(semver::Version::new(0, 0, 0), cx);
26309 crate::init(cx);
26310 });
26311 zlog::init_test();
26312 update_test_language_settings(cx, f);
26313}
26314
26315#[track_caller]
26316fn assert_hunk_revert(
26317 not_reverted_text_with_selections: &str,
26318 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26319 expected_reverted_text_with_selections: &str,
26320 base_text: &str,
26321 cx: &mut EditorLspTestContext,
26322) {
26323 cx.set_state(not_reverted_text_with_selections);
26324 cx.set_head_text(base_text);
26325 cx.executor().run_until_parked();
26326
26327 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26328 let snapshot = editor.snapshot(window, cx);
26329 let reverted_hunk_statuses = snapshot
26330 .buffer_snapshot()
26331 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26332 .map(|hunk| hunk.status().kind)
26333 .collect::<Vec<_>>();
26334
26335 editor.git_restore(&Default::default(), window, cx);
26336 reverted_hunk_statuses
26337 });
26338 cx.executor().run_until_parked();
26339 cx.assert_editor_state(expected_reverted_text_with_selections);
26340 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26341}
26342
26343#[gpui::test(iterations = 10)]
26344async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26345 init_test(cx, |_| {});
26346
26347 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26348 let counter = diagnostic_requests.clone();
26349
26350 let fs = FakeFs::new(cx.executor());
26351 fs.insert_tree(
26352 path!("/a"),
26353 json!({
26354 "first.rs": "fn main() { let a = 5; }",
26355 "second.rs": "// Test file",
26356 }),
26357 )
26358 .await;
26359
26360 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26361 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26362 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26363
26364 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26365 language_registry.add(rust_lang());
26366 let mut fake_servers = language_registry.register_fake_lsp(
26367 "Rust",
26368 FakeLspAdapter {
26369 capabilities: lsp::ServerCapabilities {
26370 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26371 lsp::DiagnosticOptions {
26372 identifier: None,
26373 inter_file_dependencies: true,
26374 workspace_diagnostics: true,
26375 work_done_progress_options: Default::default(),
26376 },
26377 )),
26378 ..Default::default()
26379 },
26380 ..Default::default()
26381 },
26382 );
26383
26384 let editor = workspace
26385 .update(cx, |workspace, window, cx| {
26386 workspace.open_abs_path(
26387 PathBuf::from(path!("/a/first.rs")),
26388 OpenOptions::default(),
26389 window,
26390 cx,
26391 )
26392 })
26393 .unwrap()
26394 .await
26395 .unwrap()
26396 .downcast::<Editor>()
26397 .unwrap();
26398 let fake_server = fake_servers.next().await.unwrap();
26399 let server_id = fake_server.server.server_id();
26400 let mut first_request = fake_server
26401 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26402 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26403 let result_id = Some(new_result_id.to_string());
26404 assert_eq!(
26405 params.text_document.uri,
26406 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26407 );
26408 async move {
26409 Ok(lsp::DocumentDiagnosticReportResult::Report(
26410 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26411 related_documents: None,
26412 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26413 items: Vec::new(),
26414 result_id,
26415 },
26416 }),
26417 ))
26418 }
26419 });
26420
26421 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
26422 project.update(cx, |project, cx| {
26423 let buffer_id = editor
26424 .read(cx)
26425 .buffer()
26426 .read(cx)
26427 .as_singleton()
26428 .expect("created a singleton buffer")
26429 .read(cx)
26430 .remote_id();
26431 let buffer_result_id = project
26432 .lsp_store()
26433 .read(cx)
26434 .result_id(server_id, buffer_id, cx);
26435 assert_eq!(expected, buffer_result_id);
26436 });
26437 };
26438
26439 ensure_result_id(None, cx);
26440 cx.executor().advance_clock(Duration::from_millis(60));
26441 cx.executor().run_until_parked();
26442 assert_eq!(
26443 diagnostic_requests.load(atomic::Ordering::Acquire),
26444 1,
26445 "Opening file should trigger diagnostic request"
26446 );
26447 first_request
26448 .next()
26449 .await
26450 .expect("should have sent the first diagnostics pull request");
26451 ensure_result_id(Some("1".to_string()), cx);
26452
26453 // Editing should trigger diagnostics
26454 editor.update_in(cx, |editor, window, cx| {
26455 editor.handle_input("2", window, cx)
26456 });
26457 cx.executor().advance_clock(Duration::from_millis(60));
26458 cx.executor().run_until_parked();
26459 assert_eq!(
26460 diagnostic_requests.load(atomic::Ordering::Acquire),
26461 2,
26462 "Editing should trigger diagnostic request"
26463 );
26464 ensure_result_id(Some("2".to_string()), cx);
26465
26466 // Moving cursor should not trigger diagnostic request
26467 editor.update_in(cx, |editor, window, cx| {
26468 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26469 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26470 });
26471 });
26472 cx.executor().advance_clock(Duration::from_millis(60));
26473 cx.executor().run_until_parked();
26474 assert_eq!(
26475 diagnostic_requests.load(atomic::Ordering::Acquire),
26476 2,
26477 "Cursor movement should not trigger diagnostic request"
26478 );
26479 ensure_result_id(Some("2".to_string()), cx);
26480 // Multiple rapid edits should be debounced
26481 for _ in 0..5 {
26482 editor.update_in(cx, |editor, window, cx| {
26483 editor.handle_input("x", window, cx)
26484 });
26485 }
26486 cx.executor().advance_clock(Duration::from_millis(60));
26487 cx.executor().run_until_parked();
26488
26489 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26490 assert!(
26491 final_requests <= 4,
26492 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26493 );
26494 ensure_result_id(Some(final_requests.to_string()), cx);
26495}
26496
26497#[gpui::test]
26498async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26499 // Regression test for issue #11671
26500 // Previously, adding a cursor after moving multiple cursors would reset
26501 // the cursor count instead of adding to the existing cursors.
26502 init_test(cx, |_| {});
26503 let mut cx = EditorTestContext::new(cx).await;
26504
26505 // Create a simple buffer with cursor at start
26506 cx.set_state(indoc! {"
26507 ˇaaaa
26508 bbbb
26509 cccc
26510 dddd
26511 eeee
26512 ffff
26513 gggg
26514 hhhh"});
26515
26516 // Add 2 cursors below (so we have 3 total)
26517 cx.update_editor(|editor, window, cx| {
26518 editor.add_selection_below(&Default::default(), window, cx);
26519 editor.add_selection_below(&Default::default(), window, cx);
26520 });
26521
26522 // Verify we have 3 cursors
26523 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26524 assert_eq!(
26525 initial_count, 3,
26526 "Should have 3 cursors after adding 2 below"
26527 );
26528
26529 // Move down one line
26530 cx.update_editor(|editor, window, cx| {
26531 editor.move_down(&MoveDown, window, cx);
26532 });
26533
26534 // Add another cursor below
26535 cx.update_editor(|editor, window, cx| {
26536 editor.add_selection_below(&Default::default(), window, cx);
26537 });
26538
26539 // Should now have 4 cursors (3 original + 1 new)
26540 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26541 assert_eq!(
26542 final_count, 4,
26543 "Should have 4 cursors after moving and adding another"
26544 );
26545}
26546
26547#[gpui::test]
26548async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26549 init_test(cx, |_| {});
26550
26551 let mut cx = EditorTestContext::new(cx).await;
26552
26553 cx.set_state(indoc!(
26554 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26555 Second line here"#
26556 ));
26557
26558 cx.update_editor(|editor, window, cx| {
26559 // Enable soft wrapping with a narrow width to force soft wrapping and
26560 // confirm that more than 2 rows are being displayed.
26561 editor.set_wrap_width(Some(100.0.into()), cx);
26562 assert!(editor.display_text(cx).lines().count() > 2);
26563
26564 editor.add_selection_below(
26565 &AddSelectionBelow {
26566 skip_soft_wrap: true,
26567 },
26568 window,
26569 cx,
26570 );
26571
26572 assert_eq!(
26573 display_ranges(editor, cx),
26574 &[
26575 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26576 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26577 ]
26578 );
26579
26580 editor.add_selection_above(
26581 &AddSelectionAbove {
26582 skip_soft_wrap: true,
26583 },
26584 window,
26585 cx,
26586 );
26587
26588 assert_eq!(
26589 display_ranges(editor, cx),
26590 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26591 );
26592
26593 editor.add_selection_below(
26594 &AddSelectionBelow {
26595 skip_soft_wrap: false,
26596 },
26597 window,
26598 cx,
26599 );
26600
26601 assert_eq!(
26602 display_ranges(editor, cx),
26603 &[
26604 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26605 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26606 ]
26607 );
26608
26609 editor.add_selection_above(
26610 &AddSelectionAbove {
26611 skip_soft_wrap: false,
26612 },
26613 window,
26614 cx,
26615 );
26616
26617 assert_eq!(
26618 display_ranges(editor, cx),
26619 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26620 );
26621 });
26622}
26623
26624#[gpui::test(iterations = 10)]
26625async fn test_document_colors(cx: &mut TestAppContext) {
26626 let expected_color = Rgba {
26627 r: 0.33,
26628 g: 0.33,
26629 b: 0.33,
26630 a: 0.33,
26631 };
26632
26633 init_test(cx, |_| {});
26634
26635 let fs = FakeFs::new(cx.executor());
26636 fs.insert_tree(
26637 path!("/a"),
26638 json!({
26639 "first.rs": "fn main() { let a = 5; }",
26640 }),
26641 )
26642 .await;
26643
26644 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26645 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26646 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26647
26648 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26649 language_registry.add(rust_lang());
26650 let mut fake_servers = language_registry.register_fake_lsp(
26651 "Rust",
26652 FakeLspAdapter {
26653 capabilities: lsp::ServerCapabilities {
26654 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26655 ..lsp::ServerCapabilities::default()
26656 },
26657 name: "rust-analyzer",
26658 ..FakeLspAdapter::default()
26659 },
26660 );
26661 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26662 "Rust",
26663 FakeLspAdapter {
26664 capabilities: lsp::ServerCapabilities {
26665 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26666 ..lsp::ServerCapabilities::default()
26667 },
26668 name: "not-rust-analyzer",
26669 ..FakeLspAdapter::default()
26670 },
26671 );
26672
26673 let editor = workspace
26674 .update(cx, |workspace, window, cx| {
26675 workspace.open_abs_path(
26676 PathBuf::from(path!("/a/first.rs")),
26677 OpenOptions::default(),
26678 window,
26679 cx,
26680 )
26681 })
26682 .unwrap()
26683 .await
26684 .unwrap()
26685 .downcast::<Editor>()
26686 .unwrap();
26687 let fake_language_server = fake_servers.next().await.unwrap();
26688 let fake_language_server_without_capabilities =
26689 fake_servers_without_capabilities.next().await.unwrap();
26690 let requests_made = Arc::new(AtomicUsize::new(0));
26691 let closure_requests_made = Arc::clone(&requests_made);
26692 let mut color_request_handle = fake_language_server
26693 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26694 let requests_made = Arc::clone(&closure_requests_made);
26695 async move {
26696 assert_eq!(
26697 params.text_document.uri,
26698 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26699 );
26700 requests_made.fetch_add(1, atomic::Ordering::Release);
26701 Ok(vec![
26702 lsp::ColorInformation {
26703 range: lsp::Range {
26704 start: lsp::Position {
26705 line: 0,
26706 character: 0,
26707 },
26708 end: lsp::Position {
26709 line: 0,
26710 character: 1,
26711 },
26712 },
26713 color: lsp::Color {
26714 red: 0.33,
26715 green: 0.33,
26716 blue: 0.33,
26717 alpha: 0.33,
26718 },
26719 },
26720 lsp::ColorInformation {
26721 range: lsp::Range {
26722 start: lsp::Position {
26723 line: 0,
26724 character: 0,
26725 },
26726 end: lsp::Position {
26727 line: 0,
26728 character: 1,
26729 },
26730 },
26731 color: lsp::Color {
26732 red: 0.33,
26733 green: 0.33,
26734 blue: 0.33,
26735 alpha: 0.33,
26736 },
26737 },
26738 ])
26739 }
26740 });
26741
26742 let _handle = fake_language_server_without_capabilities
26743 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26744 panic!("Should not be called");
26745 });
26746 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26747 color_request_handle.next().await.unwrap();
26748 cx.run_until_parked();
26749 assert_eq!(
26750 1,
26751 requests_made.load(atomic::Ordering::Acquire),
26752 "Should query for colors once per editor open"
26753 );
26754 editor.update_in(cx, |editor, _, cx| {
26755 assert_eq!(
26756 vec![expected_color],
26757 extract_color_inlays(editor, cx),
26758 "Should have an initial inlay"
26759 );
26760 });
26761
26762 // opening another file in a split should not influence the LSP query counter
26763 workspace
26764 .update(cx, |workspace, window, cx| {
26765 assert_eq!(
26766 workspace.panes().len(),
26767 1,
26768 "Should have one pane with one editor"
26769 );
26770 workspace.move_item_to_pane_in_direction(
26771 &MoveItemToPaneInDirection {
26772 direction: SplitDirection::Right,
26773 focus: false,
26774 clone: true,
26775 },
26776 window,
26777 cx,
26778 );
26779 })
26780 .unwrap();
26781 cx.run_until_parked();
26782 workspace
26783 .update(cx, |workspace, _, cx| {
26784 let panes = workspace.panes();
26785 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26786 for pane in panes {
26787 let editor = pane
26788 .read(cx)
26789 .active_item()
26790 .and_then(|item| item.downcast::<Editor>())
26791 .expect("Should have opened an editor in each split");
26792 let editor_file = editor
26793 .read(cx)
26794 .buffer()
26795 .read(cx)
26796 .as_singleton()
26797 .expect("test deals with singleton buffers")
26798 .read(cx)
26799 .file()
26800 .expect("test buffese should have a file")
26801 .path();
26802 assert_eq!(
26803 editor_file.as_ref(),
26804 rel_path("first.rs"),
26805 "Both editors should be opened for the same file"
26806 )
26807 }
26808 })
26809 .unwrap();
26810
26811 cx.executor().advance_clock(Duration::from_millis(500));
26812 let save = editor.update_in(cx, |editor, window, cx| {
26813 editor.move_to_end(&MoveToEnd, window, cx);
26814 editor.handle_input("dirty", window, cx);
26815 editor.save(
26816 SaveOptions {
26817 format: true,
26818 autosave: true,
26819 },
26820 project.clone(),
26821 window,
26822 cx,
26823 )
26824 });
26825 save.await.unwrap();
26826
26827 color_request_handle.next().await.unwrap();
26828 cx.run_until_parked();
26829 assert_eq!(
26830 2,
26831 requests_made.load(atomic::Ordering::Acquire),
26832 "Should query for colors once per save (deduplicated) and once per formatting after save"
26833 );
26834
26835 drop(editor);
26836 let close = workspace
26837 .update(cx, |workspace, window, cx| {
26838 workspace.active_pane().update(cx, |pane, cx| {
26839 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26840 })
26841 })
26842 .unwrap();
26843 close.await.unwrap();
26844 let close = workspace
26845 .update(cx, |workspace, window, cx| {
26846 workspace.active_pane().update(cx, |pane, cx| {
26847 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26848 })
26849 })
26850 .unwrap();
26851 close.await.unwrap();
26852 assert_eq!(
26853 2,
26854 requests_made.load(atomic::Ordering::Acquire),
26855 "After saving and closing all editors, no extra requests should be made"
26856 );
26857 workspace
26858 .update(cx, |workspace, _, cx| {
26859 assert!(
26860 workspace.active_item(cx).is_none(),
26861 "Should close all editors"
26862 )
26863 })
26864 .unwrap();
26865
26866 workspace
26867 .update(cx, |workspace, window, cx| {
26868 workspace.active_pane().update(cx, |pane, cx| {
26869 pane.navigate_backward(&workspace::GoBack, window, cx);
26870 })
26871 })
26872 .unwrap();
26873 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26874 cx.run_until_parked();
26875 let editor = workspace
26876 .update(cx, |workspace, _, cx| {
26877 workspace
26878 .active_item(cx)
26879 .expect("Should have reopened the editor again after navigating back")
26880 .downcast::<Editor>()
26881 .expect("Should be an editor")
26882 })
26883 .unwrap();
26884
26885 assert_eq!(
26886 2,
26887 requests_made.load(atomic::Ordering::Acquire),
26888 "Cache should be reused on buffer close and reopen"
26889 );
26890 editor.update(cx, |editor, cx| {
26891 assert_eq!(
26892 vec![expected_color],
26893 extract_color_inlays(editor, cx),
26894 "Should have an initial inlay"
26895 );
26896 });
26897
26898 drop(color_request_handle);
26899 let closure_requests_made = Arc::clone(&requests_made);
26900 let mut empty_color_request_handle = fake_language_server
26901 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26902 let requests_made = Arc::clone(&closure_requests_made);
26903 async move {
26904 assert_eq!(
26905 params.text_document.uri,
26906 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26907 );
26908 requests_made.fetch_add(1, atomic::Ordering::Release);
26909 Ok(Vec::new())
26910 }
26911 });
26912 let save = editor.update_in(cx, |editor, window, cx| {
26913 editor.move_to_end(&MoveToEnd, window, cx);
26914 editor.handle_input("dirty_again", window, cx);
26915 editor.save(
26916 SaveOptions {
26917 format: false,
26918 autosave: true,
26919 },
26920 project.clone(),
26921 window,
26922 cx,
26923 )
26924 });
26925 save.await.unwrap();
26926
26927 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26928 empty_color_request_handle.next().await.unwrap();
26929 cx.run_until_parked();
26930 assert_eq!(
26931 3,
26932 requests_made.load(atomic::Ordering::Acquire),
26933 "Should query for colors once per save only, as formatting was not requested"
26934 );
26935 editor.update(cx, |editor, cx| {
26936 assert_eq!(
26937 Vec::<Rgba>::new(),
26938 extract_color_inlays(editor, cx),
26939 "Should clear all colors when the server returns an empty response"
26940 );
26941 });
26942}
26943
26944#[gpui::test]
26945async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26946 init_test(cx, |_| {});
26947 let (editor, cx) = cx.add_window_view(Editor::single_line);
26948 editor.update_in(cx, |editor, window, cx| {
26949 editor.set_text("oops\n\nwow\n", window, cx)
26950 });
26951 cx.run_until_parked();
26952 editor.update(cx, |editor, cx| {
26953 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26954 });
26955 editor.update(cx, |editor, cx| {
26956 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
26957 });
26958 cx.run_until_parked();
26959 editor.update(cx, |editor, cx| {
26960 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26961 });
26962}
26963
26964#[gpui::test]
26965async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26966 init_test(cx, |_| {});
26967
26968 cx.update(|cx| {
26969 register_project_item::<Editor>(cx);
26970 });
26971
26972 let fs = FakeFs::new(cx.executor());
26973 fs.insert_tree("/root1", json!({})).await;
26974 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26975 .await;
26976
26977 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26978 let (workspace, cx) =
26979 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26980
26981 let worktree_id = project.update(cx, |project, cx| {
26982 project.worktrees(cx).next().unwrap().read(cx).id()
26983 });
26984
26985 let handle = workspace
26986 .update_in(cx, |workspace, window, cx| {
26987 let project_path = (worktree_id, rel_path("one.pdf"));
26988 workspace.open_path(project_path, None, true, window, cx)
26989 })
26990 .await
26991 .unwrap();
26992
26993 assert_eq!(
26994 handle.to_any_view().entity_type(),
26995 TypeId::of::<InvalidItemView>()
26996 );
26997}
26998
26999#[gpui::test]
27000async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27001 init_test(cx, |_| {});
27002
27003 let language = Arc::new(Language::new(
27004 LanguageConfig::default(),
27005 Some(tree_sitter_rust::LANGUAGE.into()),
27006 ));
27007
27008 // Test hierarchical sibling navigation
27009 let text = r#"
27010 fn outer() {
27011 if condition {
27012 let a = 1;
27013 }
27014 let b = 2;
27015 }
27016
27017 fn another() {
27018 let c = 3;
27019 }
27020 "#;
27021
27022 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language_immediate(language, cx));
27023 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27024 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27025
27026 // Wait for parsing to complete
27027 editor
27028 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27029 .await;
27030
27031 editor.update_in(cx, |editor, window, cx| {
27032 // Start by selecting "let a = 1;" inside the if block
27033 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27034 s.select_display_ranges([
27035 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27036 ]);
27037 });
27038
27039 let initial_selection = editor
27040 .selections
27041 .display_ranges(&editor.display_snapshot(cx));
27042 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27043
27044 // Test select next sibling - should move up levels to find the next sibling
27045 // Since "let a = 1;" has no siblings in the if block, it should move up
27046 // to find "let b = 2;" which is a sibling of the if block
27047 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27048 let next_selection = editor
27049 .selections
27050 .display_ranges(&editor.display_snapshot(cx));
27051
27052 // Should have a selection and it should be different from the initial
27053 assert_eq!(
27054 next_selection.len(),
27055 1,
27056 "Should have one selection after next"
27057 );
27058 assert_ne!(
27059 next_selection[0], initial_selection[0],
27060 "Next sibling selection should be different"
27061 );
27062
27063 // Test hierarchical navigation by going to the end of the current function
27064 // and trying to navigate to the next function
27065 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27066 s.select_display_ranges([
27067 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27068 ]);
27069 });
27070
27071 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27072 let function_next_selection = editor
27073 .selections
27074 .display_ranges(&editor.display_snapshot(cx));
27075
27076 // Should move to the next function
27077 assert_eq!(
27078 function_next_selection.len(),
27079 1,
27080 "Should have one selection after function next"
27081 );
27082
27083 // Test select previous sibling navigation
27084 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27085 let prev_selection = editor
27086 .selections
27087 .display_ranges(&editor.display_snapshot(cx));
27088
27089 // Should have a selection and it should be different
27090 assert_eq!(
27091 prev_selection.len(),
27092 1,
27093 "Should have one selection after prev"
27094 );
27095 assert_ne!(
27096 prev_selection[0], function_next_selection[0],
27097 "Previous sibling selection should be different from next"
27098 );
27099 });
27100}
27101
27102#[gpui::test]
27103async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27104 init_test(cx, |_| {});
27105
27106 let mut cx = EditorTestContext::new(cx).await;
27107 cx.set_state(
27108 "let ˇvariable = 42;
27109let another = variable + 1;
27110let result = variable * 2;",
27111 );
27112
27113 // Set up document highlights manually (simulating LSP response)
27114 cx.update_editor(|editor, _window, cx| {
27115 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27116
27117 // Create highlights for "variable" occurrences
27118 let highlight_ranges = [
27119 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27120 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27121 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27122 ];
27123
27124 let anchor_ranges: Vec<_> = highlight_ranges
27125 .iter()
27126 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27127 .collect();
27128
27129 editor.highlight_background::<DocumentHighlightRead>(
27130 &anchor_ranges,
27131 |theme| theme.colors().editor_document_highlight_read_background,
27132 cx,
27133 );
27134 });
27135
27136 // Go to next highlight - should move to second "variable"
27137 cx.update_editor(|editor, window, cx| {
27138 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27139 });
27140 cx.assert_editor_state(
27141 "let variable = 42;
27142let another = ˇvariable + 1;
27143let result = variable * 2;",
27144 );
27145
27146 // Go to next highlight - should move to third "variable"
27147 cx.update_editor(|editor, window, cx| {
27148 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27149 });
27150 cx.assert_editor_state(
27151 "let variable = 42;
27152let another = variable + 1;
27153let result = ˇvariable * 2;",
27154 );
27155
27156 // Go to next highlight - should stay at third "variable" (no wrap-around)
27157 cx.update_editor(|editor, window, cx| {
27158 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27159 });
27160 cx.assert_editor_state(
27161 "let variable = 42;
27162let another = variable + 1;
27163let result = ˇvariable * 2;",
27164 );
27165
27166 // Now test going backwards from third position
27167 cx.update_editor(|editor, window, cx| {
27168 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27169 });
27170 cx.assert_editor_state(
27171 "let variable = 42;
27172let another = ˇvariable + 1;
27173let result = variable * 2;",
27174 );
27175
27176 // Go to previous highlight - should move to first "variable"
27177 cx.update_editor(|editor, window, cx| {
27178 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27179 });
27180 cx.assert_editor_state(
27181 "let ˇvariable = 42;
27182let another = variable + 1;
27183let result = variable * 2;",
27184 );
27185
27186 // Go to previous highlight - should stay on first "variable"
27187 cx.update_editor(|editor, window, cx| {
27188 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27189 });
27190 cx.assert_editor_state(
27191 "let ˇvariable = 42;
27192let another = variable + 1;
27193let result = variable * 2;",
27194 );
27195}
27196
27197#[gpui::test]
27198async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27199 cx: &mut gpui::TestAppContext,
27200) {
27201 init_test(cx, |_| {});
27202
27203 let url = "https://zed.dev";
27204
27205 let markdown_language = Arc::new(Language::new(
27206 LanguageConfig {
27207 name: "Markdown".into(),
27208 ..LanguageConfig::default()
27209 },
27210 None,
27211 ));
27212
27213 let mut cx = EditorTestContext::new(cx).await;
27214 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(markdown_language), cx));
27215 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27216
27217 cx.update_editor(|editor, window, cx| {
27218 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27219 editor.paste(&Paste, window, cx);
27220 });
27221
27222 cx.assert_editor_state(&format!(
27223 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27224 ));
27225}
27226
27227#[gpui::test]
27228async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
27229 cx: &mut gpui::TestAppContext,
27230) {
27231 init_test(cx, |_| {});
27232
27233 let url = "https://zed.dev";
27234
27235 let markdown_language = Arc::new(Language::new(
27236 LanguageConfig {
27237 name: "Markdown".into(),
27238 ..LanguageConfig::default()
27239 },
27240 None,
27241 ));
27242
27243 let mut cx = EditorTestContext::new(cx).await;
27244 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(markdown_language), cx));
27245 cx.set_state(&format!(
27246 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
27247 ));
27248
27249 cx.update_editor(|editor, window, cx| {
27250 editor.copy(&Copy, window, cx);
27251 });
27252
27253 cx.set_state(&format!(
27254 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
27255 ));
27256
27257 cx.update_editor(|editor, window, cx| {
27258 editor.paste(&Paste, window, cx);
27259 });
27260
27261 cx.assert_editor_state(&format!(
27262 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
27263 ));
27264}
27265
27266#[gpui::test]
27267async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
27268 cx: &mut gpui::TestAppContext,
27269) {
27270 init_test(cx, |_| {});
27271
27272 let url = "https://zed.dev";
27273
27274 let markdown_language = Arc::new(Language::new(
27275 LanguageConfig {
27276 name: "Markdown".into(),
27277 ..LanguageConfig::default()
27278 },
27279 None,
27280 ));
27281
27282 let mut cx = EditorTestContext::new(cx).await;
27283 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(markdown_language), cx));
27284 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
27285
27286 cx.update_editor(|editor, window, cx| {
27287 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27288 editor.paste(&Paste, window, cx);
27289 });
27290
27291 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
27292}
27293
27294#[gpui::test]
27295async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
27296 cx: &mut gpui::TestAppContext,
27297) {
27298 init_test(cx, |_| {});
27299
27300 let text = "Awesome";
27301
27302 let markdown_language = Arc::new(Language::new(
27303 LanguageConfig {
27304 name: "Markdown".into(),
27305 ..LanguageConfig::default()
27306 },
27307 None,
27308 ));
27309
27310 let mut cx = EditorTestContext::new(cx).await;
27311 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(markdown_language), cx));
27312 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
27313
27314 cx.update_editor(|editor, window, cx| {
27315 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
27316 editor.paste(&Paste, window, cx);
27317 });
27318
27319 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
27320}
27321
27322#[gpui::test]
27323async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
27324 cx: &mut gpui::TestAppContext,
27325) {
27326 init_test(cx, |_| {});
27327
27328 let url = "https://zed.dev";
27329
27330 let markdown_language = Arc::new(Language::new(
27331 LanguageConfig {
27332 name: "Rust".into(),
27333 ..LanguageConfig::default()
27334 },
27335 None,
27336 ));
27337
27338 let mut cx = EditorTestContext::new(cx).await;
27339 cx.update_buffer(|buffer, cx| buffer.set_language_immediate(Some(markdown_language), cx));
27340 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
27341
27342 cx.update_editor(|editor, window, cx| {
27343 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27344 editor.paste(&Paste, window, cx);
27345 });
27346
27347 cx.assert_editor_state(&format!(
27348 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
27349 ));
27350}
27351
27352#[gpui::test]
27353async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
27354 cx: &mut TestAppContext,
27355) {
27356 init_test(cx, |_| {});
27357
27358 let url = "https://zed.dev";
27359
27360 let markdown_language = Arc::new(Language::new(
27361 LanguageConfig {
27362 name: "Markdown".into(),
27363 ..LanguageConfig::default()
27364 },
27365 None,
27366 ));
27367
27368 let (editor, cx) = cx.add_window_view(|window, cx| {
27369 let multi_buffer = MultiBuffer::build_multi(
27370 [
27371 ("this will embed -> link", vec![Point::row_range(0..1)]),
27372 ("this will replace -> link", vec![Point::row_range(0..1)]),
27373 ],
27374 cx,
27375 );
27376 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
27377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27378 s.select_ranges(vec![
27379 Point::new(0, 19)..Point::new(0, 23),
27380 Point::new(1, 21)..Point::new(1, 25),
27381 ])
27382 });
27383 let first_buffer_id = multi_buffer
27384 .read(cx)
27385 .excerpt_buffer_ids()
27386 .into_iter()
27387 .next()
27388 .unwrap();
27389 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
27390 first_buffer.update(cx, |buffer, cx| {
27391 buffer.set_language_immediate(Some(markdown_language.clone()), cx);
27392 });
27393
27394 editor
27395 });
27396 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27397
27398 cx.update_editor(|editor, window, cx| {
27399 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27400 editor.paste(&Paste, window, cx);
27401 });
27402
27403 cx.assert_editor_state(&format!(
27404 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
27405 ));
27406}
27407
27408#[gpui::test]
27409async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
27410 init_test(cx, |_| {});
27411
27412 let fs = FakeFs::new(cx.executor());
27413 fs.insert_tree(
27414 path!("/project"),
27415 json!({
27416 "first.rs": "# First Document\nSome content here.",
27417 "second.rs": "Plain text content for second file.",
27418 }),
27419 )
27420 .await;
27421
27422 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
27423 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27424 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27425
27426 let language = rust_lang();
27427 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27428 language_registry.add(language.clone());
27429 let mut fake_servers = language_registry.register_fake_lsp(
27430 "Rust",
27431 FakeLspAdapter {
27432 ..FakeLspAdapter::default()
27433 },
27434 );
27435
27436 let buffer1 = project
27437 .update(cx, |project, cx| {
27438 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
27439 })
27440 .await
27441 .unwrap();
27442 let buffer2 = project
27443 .update(cx, |project, cx| {
27444 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
27445 })
27446 .await
27447 .unwrap();
27448
27449 let multi_buffer = cx.new(|cx| {
27450 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
27451 multi_buffer.set_excerpts_for_path(
27452 PathKey::for_buffer(&buffer1, cx),
27453 buffer1.clone(),
27454 [Point::zero()..buffer1.read(cx).max_point()],
27455 3,
27456 cx,
27457 );
27458 multi_buffer.set_excerpts_for_path(
27459 PathKey::for_buffer(&buffer2, cx),
27460 buffer2.clone(),
27461 [Point::zero()..buffer1.read(cx).max_point()],
27462 3,
27463 cx,
27464 );
27465 multi_buffer
27466 });
27467
27468 let (editor, cx) = cx.add_window_view(|window, cx| {
27469 Editor::new(
27470 EditorMode::full(),
27471 multi_buffer,
27472 Some(project.clone()),
27473 window,
27474 cx,
27475 )
27476 });
27477
27478 let fake_language_server = fake_servers.next().await.unwrap();
27479
27480 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
27481
27482 let save = editor.update_in(cx, |editor, window, cx| {
27483 assert!(editor.is_dirty(cx));
27484
27485 editor.save(
27486 SaveOptions {
27487 format: true,
27488 autosave: true,
27489 },
27490 project,
27491 window,
27492 cx,
27493 )
27494 });
27495 let (start_edit_tx, start_edit_rx) = oneshot::channel();
27496 let (done_edit_tx, done_edit_rx) = oneshot::channel();
27497 let mut done_edit_rx = Some(done_edit_rx);
27498 let mut start_edit_tx = Some(start_edit_tx);
27499
27500 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
27501 start_edit_tx.take().unwrap().send(()).unwrap();
27502 let done_edit_rx = done_edit_rx.take().unwrap();
27503 async move {
27504 done_edit_rx.await.unwrap();
27505 Ok(None)
27506 }
27507 });
27508
27509 start_edit_rx.await.unwrap();
27510 buffer2
27511 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
27512 .unwrap();
27513
27514 done_edit_tx.send(()).unwrap();
27515
27516 save.await.unwrap();
27517 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
27518}
27519
27520#[track_caller]
27521fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
27522 editor
27523 .all_inlays(cx)
27524 .into_iter()
27525 .filter_map(|inlay| inlay.get_color())
27526 .map(Rgba::from)
27527 .collect()
27528}
27529
27530#[gpui::test]
27531fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
27532 init_test(cx, |_| {});
27533
27534 let editor = cx.add_window(|window, cx| {
27535 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
27536 build_editor(buffer, window, cx)
27537 });
27538
27539 editor
27540 .update(cx, |editor, window, cx| {
27541 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27542 s.select_display_ranges([
27543 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
27544 ])
27545 });
27546
27547 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
27548
27549 assert_eq!(
27550 editor.display_text(cx),
27551 "line1\nline2\nline2",
27552 "Duplicating last line upward should create duplicate above, not on same line"
27553 );
27554
27555 assert_eq!(
27556 editor
27557 .selections
27558 .display_ranges(&editor.display_snapshot(cx)),
27559 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27560 "Selection should move to the duplicated line"
27561 );
27562 })
27563 .unwrap();
27564}
27565
27566#[gpui::test]
27567async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27568 init_test(cx, |_| {});
27569
27570 let mut cx = EditorTestContext::new(cx).await;
27571
27572 cx.set_state("line1\nline2ˇ");
27573
27574 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27575
27576 let clipboard_text = cx
27577 .read_from_clipboard()
27578 .and_then(|item| item.text().as_deref().map(str::to_string));
27579
27580 assert_eq!(
27581 clipboard_text,
27582 Some("line2\n".to_string()),
27583 "Copying a line without trailing newline should include a newline"
27584 );
27585
27586 cx.set_state("line1\nˇ");
27587
27588 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27589
27590 cx.assert_editor_state("line1\nline2\nˇ");
27591}
27592
27593#[gpui::test]
27594async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27595 init_test(cx, |_| {});
27596
27597 let mut cx = EditorTestContext::new(cx).await;
27598
27599 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27600
27601 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27602
27603 let clipboard_text = cx
27604 .read_from_clipboard()
27605 .and_then(|item| item.text().as_deref().map(str::to_string));
27606
27607 assert_eq!(
27608 clipboard_text,
27609 Some("line1\nline2\nline3\n".to_string()),
27610 "Copying multiple lines should include a single newline between lines"
27611 );
27612
27613 cx.set_state("lineA\nˇ");
27614
27615 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27616
27617 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27618}
27619
27620#[gpui::test]
27621async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27622 init_test(cx, |_| {});
27623
27624 let mut cx = EditorTestContext::new(cx).await;
27625
27626 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27627
27628 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
27629
27630 let clipboard_text = cx
27631 .read_from_clipboard()
27632 .and_then(|item| item.text().as_deref().map(str::to_string));
27633
27634 assert_eq!(
27635 clipboard_text,
27636 Some("line1\nline2\nline3\n".to_string()),
27637 "Copying multiple lines should include a single newline between lines"
27638 );
27639
27640 cx.set_state("lineA\nˇ");
27641
27642 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27643
27644 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27645}
27646
27647#[gpui::test]
27648async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27649 init_test(cx, |_| {});
27650
27651 let mut cx = EditorTestContext::new(cx).await;
27652
27653 cx.set_state("line1\nline2ˇ");
27654 cx.update_editor(|e, window, cx| {
27655 e.set_mode(EditorMode::SingleLine);
27656 assert!(e.key_context(window, cx).contains("end_of_input"));
27657 });
27658 cx.set_state("ˇline1\nline2");
27659 cx.update_editor(|e, window, cx| {
27660 assert!(!e.key_context(window, cx).contains("end_of_input"));
27661 });
27662 cx.set_state("line1ˇ\nline2");
27663 cx.update_editor(|e, window, cx| {
27664 assert!(!e.key_context(window, cx).contains("end_of_input"));
27665 });
27666}
27667
27668#[gpui::test]
27669async fn test_sticky_scroll(cx: &mut TestAppContext) {
27670 init_test(cx, |_| {});
27671 let mut cx = EditorTestContext::new(cx).await;
27672
27673 let buffer = indoc! {"
27674 ˇfn foo() {
27675 let abc = 123;
27676 }
27677 struct Bar;
27678 impl Bar {
27679 fn new() -> Self {
27680 Self
27681 }
27682 }
27683 fn baz() {
27684 }
27685 "};
27686 cx.set_state(&buffer);
27687
27688 cx.update_editor(|e, _, cx| {
27689 e.buffer()
27690 .read(cx)
27691 .as_singleton()
27692 .unwrap()
27693 .update(cx, |buffer, cx| {
27694 buffer.set_language_immediate(Some(rust_lang()), cx);
27695 })
27696 });
27697
27698 let mut sticky_headers = |offset: ScrollOffset| {
27699 cx.update_editor(|e, window, cx| {
27700 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27701 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27702 .into_iter()
27703 .map(
27704 |StickyHeader {
27705 start_point,
27706 offset,
27707 ..
27708 }| { (start_point, offset) },
27709 )
27710 .collect::<Vec<_>>()
27711 })
27712 };
27713
27714 let fn_foo = Point { row: 0, column: 0 };
27715 let impl_bar = Point { row: 4, column: 0 };
27716 let fn_new = Point { row: 5, column: 4 };
27717
27718 assert_eq!(sticky_headers(0.0), vec![]);
27719 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27720 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27721 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27722 assert_eq!(sticky_headers(2.0), vec![]);
27723 assert_eq!(sticky_headers(2.5), vec![]);
27724 assert_eq!(sticky_headers(3.0), vec![]);
27725 assert_eq!(sticky_headers(3.5), vec![]);
27726 assert_eq!(sticky_headers(4.0), vec![]);
27727 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27728 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27729 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27730 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27731 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27732 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27733 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27734 assert_eq!(sticky_headers(8.0), vec![]);
27735 assert_eq!(sticky_headers(8.5), vec![]);
27736 assert_eq!(sticky_headers(9.0), vec![]);
27737 assert_eq!(sticky_headers(9.5), vec![]);
27738 assert_eq!(sticky_headers(10.0), vec![]);
27739}
27740
27741#[gpui::test]
27742async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27743 init_test(cx, |_| {});
27744 cx.update(|cx| {
27745 SettingsStore::update_global(cx, |store, cx| {
27746 store.update_user_settings(cx, |settings| {
27747 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27748 enabled: Some(true),
27749 })
27750 });
27751 });
27752 });
27753 let mut cx = EditorTestContext::new(cx).await;
27754
27755 let line_height = cx.editor(|editor, window, _cx| {
27756 editor
27757 .style()
27758 .unwrap()
27759 .text
27760 .line_height_in_pixels(window.rem_size())
27761 });
27762
27763 let buffer = indoc! {"
27764 ˇfn foo() {
27765 let abc = 123;
27766 }
27767 struct Bar;
27768 impl Bar {
27769 fn new() -> Self {
27770 Self
27771 }
27772 }
27773 fn baz() {
27774 }
27775 "};
27776 cx.set_state(&buffer);
27777
27778 cx.update_editor(|e, _, cx| {
27779 e.buffer()
27780 .read(cx)
27781 .as_singleton()
27782 .unwrap()
27783 .update(cx, |buffer, cx| {
27784 buffer.set_language_immediate(Some(rust_lang()), cx);
27785 })
27786 });
27787
27788 let fn_foo = || empty_range(0, 0);
27789 let impl_bar = || empty_range(4, 0);
27790 let fn_new = || empty_range(5, 4);
27791
27792 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27793 cx.update_editor(|e, window, cx| {
27794 e.scroll(
27795 gpui::Point {
27796 x: 0.,
27797 y: scroll_offset,
27798 },
27799 None,
27800 window,
27801 cx,
27802 );
27803 });
27804 cx.simulate_click(
27805 gpui::Point {
27806 x: px(0.),
27807 y: click_offset as f32 * line_height,
27808 },
27809 Modifiers::none(),
27810 );
27811 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27812 };
27813
27814 assert_eq!(
27815 scroll_and_click(
27816 4.5, // impl Bar is halfway off the screen
27817 0.0 // click top of screen
27818 ),
27819 // scrolled to impl Bar
27820 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27821 );
27822
27823 assert_eq!(
27824 scroll_and_click(
27825 4.5, // impl Bar is halfway off the screen
27826 0.25 // click middle of impl Bar
27827 ),
27828 // scrolled to impl Bar
27829 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27830 );
27831
27832 assert_eq!(
27833 scroll_and_click(
27834 4.5, // impl Bar is halfway off the screen
27835 1.5 // click below impl Bar (e.g. fn new())
27836 ),
27837 // scrolled to fn new() - this is below the impl Bar header which has persisted
27838 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27839 );
27840
27841 assert_eq!(
27842 scroll_and_click(
27843 5.5, // fn new is halfway underneath impl Bar
27844 0.75 // click on the overlap of impl Bar and fn new()
27845 ),
27846 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27847 );
27848
27849 assert_eq!(
27850 scroll_and_click(
27851 5.5, // fn new is halfway underneath impl Bar
27852 1.25 // click on the visible part of fn new()
27853 ),
27854 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27855 );
27856
27857 assert_eq!(
27858 scroll_and_click(
27859 1.5, // fn foo is halfway off the screen
27860 0.0 // click top of screen
27861 ),
27862 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
27863 );
27864
27865 assert_eq!(
27866 scroll_and_click(
27867 1.5, // fn foo is halfway off the screen
27868 0.75 // click visible part of let abc...
27869 )
27870 .0,
27871 // no change in scroll
27872 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
27873 (gpui::Point { x: 0., y: 1.5 })
27874 );
27875}
27876
27877#[gpui::test]
27878async fn test_next_prev_reference(cx: &mut TestAppContext) {
27879 const CYCLE_POSITIONS: &[&'static str] = &[
27880 indoc! {"
27881 fn foo() {
27882 let ˇabc = 123;
27883 let x = abc + 1;
27884 let y = abc + 2;
27885 let z = abc + 2;
27886 }
27887 "},
27888 indoc! {"
27889 fn foo() {
27890 let abc = 123;
27891 let x = ˇabc + 1;
27892 let y = abc + 2;
27893 let z = abc + 2;
27894 }
27895 "},
27896 indoc! {"
27897 fn foo() {
27898 let abc = 123;
27899 let x = abc + 1;
27900 let y = ˇabc + 2;
27901 let z = abc + 2;
27902 }
27903 "},
27904 indoc! {"
27905 fn foo() {
27906 let abc = 123;
27907 let x = abc + 1;
27908 let y = abc + 2;
27909 let z = ˇabc + 2;
27910 }
27911 "},
27912 ];
27913
27914 init_test(cx, |_| {});
27915
27916 let mut cx = EditorLspTestContext::new_rust(
27917 lsp::ServerCapabilities {
27918 references_provider: Some(lsp::OneOf::Left(true)),
27919 ..Default::default()
27920 },
27921 cx,
27922 )
27923 .await;
27924
27925 // importantly, the cursor is in the middle
27926 cx.set_state(indoc! {"
27927 fn foo() {
27928 let aˇbc = 123;
27929 let x = abc + 1;
27930 let y = abc + 2;
27931 let z = abc + 2;
27932 }
27933 "});
27934
27935 let reference_ranges = [
27936 lsp::Position::new(1, 8),
27937 lsp::Position::new(2, 12),
27938 lsp::Position::new(3, 12),
27939 lsp::Position::new(4, 12),
27940 ]
27941 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27942
27943 cx.lsp
27944 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27945 Ok(Some(
27946 reference_ranges
27947 .map(|range| lsp::Location {
27948 uri: params.text_document_position.text_document.uri.clone(),
27949 range,
27950 })
27951 .to_vec(),
27952 ))
27953 });
27954
27955 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27956 cx.update_editor(|editor, window, cx| {
27957 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27958 })
27959 .unwrap()
27960 .await
27961 .unwrap()
27962 };
27963
27964 _move(Direction::Next, 1, &mut cx).await;
27965 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27966
27967 _move(Direction::Next, 1, &mut cx).await;
27968 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27969
27970 _move(Direction::Next, 1, &mut cx).await;
27971 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27972
27973 // loops back to the start
27974 _move(Direction::Next, 1, &mut cx).await;
27975 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27976
27977 // loops back to the end
27978 _move(Direction::Prev, 1, &mut cx).await;
27979 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27980
27981 _move(Direction::Prev, 1, &mut cx).await;
27982 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27983
27984 _move(Direction::Prev, 1, &mut cx).await;
27985 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27986
27987 _move(Direction::Prev, 1, &mut cx).await;
27988 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27989
27990 _move(Direction::Next, 3, &mut cx).await;
27991 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27992
27993 _move(Direction::Prev, 2, &mut cx).await;
27994 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27995}
27996
27997#[gpui::test]
27998async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
27999 init_test(cx, |_| {});
28000
28001 let (editor, cx) = cx.add_window_view(|window, cx| {
28002 let multi_buffer = MultiBuffer::build_multi(
28003 [
28004 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28005 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28006 ],
28007 cx,
28008 );
28009 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28010 });
28011
28012 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28013 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28014
28015 cx.assert_excerpts_with_selections(indoc! {"
28016 [EXCERPT]
28017 ˇ1
28018 2
28019 3
28020 [EXCERPT]
28021 1
28022 2
28023 3
28024 "});
28025
28026 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28027 cx.update_editor(|editor, window, cx| {
28028 editor.change_selections(None.into(), window, cx, |s| {
28029 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28030 });
28031 });
28032 cx.assert_excerpts_with_selections(indoc! {"
28033 [EXCERPT]
28034 1
28035 2ˇ
28036 3
28037 [EXCERPT]
28038 1
28039 2
28040 3
28041 "});
28042
28043 cx.update_editor(|editor, window, cx| {
28044 editor
28045 .select_all_matches(&SelectAllMatches, window, cx)
28046 .unwrap();
28047 });
28048 cx.assert_excerpts_with_selections(indoc! {"
28049 [EXCERPT]
28050 1
28051 2ˇ
28052 3
28053 [EXCERPT]
28054 1
28055 2ˇ
28056 3
28057 "});
28058
28059 cx.update_editor(|editor, window, cx| {
28060 editor.handle_input("X", window, cx);
28061 });
28062 cx.assert_excerpts_with_selections(indoc! {"
28063 [EXCERPT]
28064 1
28065 Xˇ
28066 3
28067 [EXCERPT]
28068 1
28069 Xˇ
28070 3
28071 "});
28072
28073 // Scenario 2: Select "2", then fold second buffer before insertion
28074 cx.update_multibuffer(|mb, cx| {
28075 for buffer_id in buffer_ids.iter() {
28076 let buffer = mb.buffer(*buffer_id).unwrap();
28077 buffer.update(cx, |buffer, cx| {
28078 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28079 });
28080 }
28081 });
28082
28083 // Select "2" and select all matches
28084 cx.update_editor(|editor, window, cx| {
28085 editor.change_selections(None.into(), window, cx, |s| {
28086 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28087 });
28088 editor
28089 .select_all_matches(&SelectAllMatches, window, cx)
28090 .unwrap();
28091 });
28092
28093 // Fold second buffer - should remove selections from folded buffer
28094 cx.update_editor(|editor, _, cx| {
28095 editor.fold_buffer(buffer_ids[1], cx);
28096 });
28097 cx.assert_excerpts_with_selections(indoc! {"
28098 [EXCERPT]
28099 1
28100 2ˇ
28101 3
28102 [EXCERPT]
28103 [FOLDED]
28104 "});
28105
28106 // Insert text - should only affect first buffer
28107 cx.update_editor(|editor, window, cx| {
28108 editor.handle_input("Y", window, cx);
28109 });
28110 cx.update_editor(|editor, _, cx| {
28111 editor.unfold_buffer(buffer_ids[1], cx);
28112 });
28113 cx.assert_excerpts_with_selections(indoc! {"
28114 [EXCERPT]
28115 1
28116 Yˇ
28117 3
28118 [EXCERPT]
28119 1
28120 2
28121 3
28122 "});
28123
28124 // Scenario 3: Select "2", then fold first buffer before insertion
28125 cx.update_multibuffer(|mb, cx| {
28126 for buffer_id in buffer_ids.iter() {
28127 let buffer = mb.buffer(*buffer_id).unwrap();
28128 buffer.update(cx, |buffer, cx| {
28129 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28130 });
28131 }
28132 });
28133
28134 // Select "2" and select all matches
28135 cx.update_editor(|editor, window, cx| {
28136 editor.change_selections(None.into(), window, cx, |s| {
28137 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28138 });
28139 editor
28140 .select_all_matches(&SelectAllMatches, window, cx)
28141 .unwrap();
28142 });
28143
28144 // Fold first buffer - should remove selections from folded buffer
28145 cx.update_editor(|editor, _, cx| {
28146 editor.fold_buffer(buffer_ids[0], cx);
28147 });
28148 cx.assert_excerpts_with_selections(indoc! {"
28149 [EXCERPT]
28150 [FOLDED]
28151 [EXCERPT]
28152 1
28153 2ˇ
28154 3
28155 "});
28156
28157 // Insert text - should only affect second buffer
28158 cx.update_editor(|editor, window, cx| {
28159 editor.handle_input("Z", window, cx);
28160 });
28161 cx.update_editor(|editor, _, cx| {
28162 editor.unfold_buffer(buffer_ids[0], cx);
28163 });
28164 cx.assert_excerpts_with_selections(indoc! {"
28165 [EXCERPT]
28166 1
28167 2
28168 3
28169 [EXCERPT]
28170 1
28171 Zˇ
28172 3
28173 "});
28174
28175 // Edge case scenario: fold all buffers, then try to insert
28176 cx.update_editor(|editor, _, cx| {
28177 editor.fold_buffer(buffer_ids[0], cx);
28178 editor.fold_buffer(buffer_ids[1], cx);
28179 });
28180 cx.assert_excerpts_with_selections(indoc! {"
28181 [EXCERPT]
28182 ˇ[FOLDED]
28183 [EXCERPT]
28184 [FOLDED]
28185 "});
28186
28187 // Insert should work via default selection
28188 cx.update_editor(|editor, window, cx| {
28189 editor.handle_input("W", window, cx);
28190 });
28191 cx.update_editor(|editor, _, cx| {
28192 editor.unfold_buffer(buffer_ids[0], cx);
28193 editor.unfold_buffer(buffer_ids[1], cx);
28194 });
28195 cx.assert_excerpts_with_selections(indoc! {"
28196 [EXCERPT]
28197 Wˇ1
28198 2
28199 3
28200 [EXCERPT]
28201 1
28202 Z
28203 3
28204 "});
28205}
28206
28207#[gpui::test]
28208async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
28209 init_test(cx, |_| {});
28210 let mut leader_cx = EditorTestContext::new(cx).await;
28211
28212 let diff_base = indoc!(
28213 r#"
28214 one
28215 two
28216 three
28217 four
28218 five
28219 six
28220 "#
28221 );
28222
28223 let initial_state = indoc!(
28224 r#"
28225 ˇone
28226 two
28227 THREE
28228 four
28229 five
28230 six
28231 "#
28232 );
28233
28234 leader_cx.set_state(initial_state);
28235
28236 leader_cx.set_head_text(&diff_base);
28237 leader_cx.run_until_parked();
28238
28239 let follower = leader_cx.update_multibuffer(|leader, cx| {
28240 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28241 leader.set_all_diff_hunks_expanded(cx);
28242 leader.get_or_create_follower(cx)
28243 });
28244 follower.update(cx, |follower, cx| {
28245 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28246 follower.set_all_diff_hunks_expanded(cx);
28247 });
28248
28249 let follower_editor =
28250 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
28251 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
28252
28253 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
28254 cx.run_until_parked();
28255
28256 leader_cx.assert_editor_state(initial_state);
28257 follower_cx.assert_editor_state(indoc! {
28258 r#"
28259 ˇone
28260 two
28261 three
28262 four
28263 five
28264 six
28265 "#
28266 });
28267
28268 follower_cx.editor(|editor, _window, cx| {
28269 assert!(editor.read_only(cx));
28270 });
28271
28272 leader_cx.update_editor(|editor, _window, cx| {
28273 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
28274 });
28275 cx.run_until_parked();
28276
28277 leader_cx.assert_editor_state(indoc! {
28278 r#"
28279 ˇone
28280 two
28281 THREE
28282 four
28283 FIVE
28284 six
28285 "#
28286 });
28287
28288 follower_cx.assert_editor_state(indoc! {
28289 r#"
28290 ˇone
28291 two
28292 three
28293 four
28294 five
28295 six
28296 "#
28297 });
28298
28299 leader_cx.update_editor(|editor, _window, cx| {
28300 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
28301 });
28302 cx.run_until_parked();
28303
28304 leader_cx.assert_editor_state(indoc! {
28305 r#"
28306 ˇone
28307 two
28308 THREE
28309 four
28310 FIVE
28311 six
28312 SEVEN"#
28313 });
28314
28315 follower_cx.assert_editor_state(indoc! {
28316 r#"
28317 ˇone
28318 two
28319 three
28320 four
28321 five
28322 six
28323 "#
28324 });
28325
28326 leader_cx.update_editor(|editor, window, cx| {
28327 editor.move_down(&MoveDown, window, cx);
28328 editor.refresh_selected_text_highlights(true, window, cx);
28329 });
28330 leader_cx.run_until_parked();
28331}
28332
28333#[gpui::test]
28334async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
28335 init_test(cx, |_| {});
28336 let base_text = "base\n";
28337 let buffer_text = "buffer\n";
28338
28339 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
28340 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
28341
28342 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
28343 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
28344 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
28345 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
28346
28347 let leader = cx.new(|cx| {
28348 let mut leader = MultiBuffer::new(Capability::ReadWrite);
28349 leader.set_all_diff_hunks_expanded(cx);
28350 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28351 leader
28352 });
28353 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
28354 follower.update(cx, |follower, _| {
28355 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28356 });
28357
28358 leader.update(cx, |leader, cx| {
28359 leader.insert_excerpts_after(
28360 ExcerptId::min(),
28361 extra_buffer_2.clone(),
28362 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28363 cx,
28364 );
28365 leader.add_diff(extra_diff_2.clone(), cx);
28366
28367 leader.insert_excerpts_after(
28368 ExcerptId::min(),
28369 extra_buffer_1.clone(),
28370 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28371 cx,
28372 );
28373 leader.add_diff(extra_diff_1.clone(), cx);
28374
28375 leader.insert_excerpts_after(
28376 ExcerptId::min(),
28377 buffer1.clone(),
28378 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28379 cx,
28380 );
28381 leader.add_diff(diff1.clone(), cx);
28382 });
28383
28384 cx.run_until_parked();
28385 let mut cx = cx.add_empty_window();
28386
28387 let leader_editor = cx
28388 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
28389 let follower_editor = cx.new_window_entity(|window, cx| {
28390 Editor::for_multibuffer(follower.clone(), None, window, cx)
28391 });
28392
28393 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
28394 leader_cx.assert_editor_state(indoc! {"
28395 ˇbuffer
28396
28397 dummy text 1
28398
28399 dummy text 2
28400 "});
28401 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
28402 follower_cx.assert_editor_state(indoc! {"
28403 ˇbase
28404
28405
28406 "});
28407}
28408
28409#[gpui::test]
28410async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
28411 init_test(cx, |_| {});
28412
28413 let (editor, cx) = cx.add_window_view(|window, cx| {
28414 let multi_buffer = MultiBuffer::build_multi(
28415 [
28416 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28417 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
28418 ],
28419 cx,
28420 );
28421 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28422 });
28423
28424 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28425
28426 cx.assert_excerpts_with_selections(indoc! {"
28427 [EXCERPT]
28428 ˇ1
28429 2
28430 3
28431 [EXCERPT]
28432 1
28433 2
28434 3
28435 4
28436 5
28437 6
28438 7
28439 8
28440 9
28441 "});
28442
28443 cx.update_editor(|editor, window, cx| {
28444 editor.change_selections(None.into(), window, cx, |s| {
28445 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
28446 });
28447 });
28448
28449 cx.assert_excerpts_with_selections(indoc! {"
28450 [EXCERPT]
28451 1
28452 2
28453 3
28454 [EXCERPT]
28455 1
28456 2
28457 3
28458 4
28459 5
28460 6
28461 ˇ7
28462 8
28463 9
28464 "});
28465
28466 cx.update_editor(|editor, _window, cx| {
28467 editor.set_vertical_scroll_margin(0, cx);
28468 });
28469
28470 cx.update_editor(|editor, window, cx| {
28471 assert_eq!(editor.vertical_scroll_margin(), 0);
28472 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28473 assert_eq!(
28474 editor.snapshot(window, cx).scroll_position(),
28475 gpui::Point::new(0., 12.0)
28476 );
28477 });
28478
28479 cx.update_editor(|editor, _window, cx| {
28480 editor.set_vertical_scroll_margin(3, cx);
28481 });
28482
28483 cx.update_editor(|editor, window, cx| {
28484 assert_eq!(editor.vertical_scroll_margin(), 3);
28485 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28486 assert_eq!(
28487 editor.snapshot(window, cx).scroll_position(),
28488 gpui::Point::new(0., 9.0)
28489 );
28490 });
28491}