1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionDelegate,
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.update_editor(|editor, window, cx| {
2222 editor
2223 .style(cx)
2224 .text
2225 .line_height_in_pixels(window.rem_size())
2226 });
2227 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2228
2229 cx.set_state(
2230 &r#"ˇone
2231 two
2232
2233 three
2234 fourˇ
2235 five
2236
2237 six"#
2238 .unindent(),
2239 );
2240
2241 cx.update_editor(|editor, window, cx| {
2242 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2243 });
2244 cx.assert_editor_state(
2245 &r#"one
2246 two
2247 ˇ
2248 three
2249 four
2250 five
2251 ˇ
2252 six"#
2253 .unindent(),
2254 );
2255
2256 cx.update_editor(|editor, window, cx| {
2257 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2258 });
2259 cx.assert_editor_state(
2260 &r#"one
2261 two
2262
2263 three
2264 four
2265 five
2266 ˇ
2267 sixˇ"#
2268 .unindent(),
2269 );
2270
2271 cx.update_editor(|editor, window, cx| {
2272 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2273 });
2274 cx.assert_editor_state(
2275 &r#"one
2276 two
2277
2278 three
2279 four
2280 five
2281
2282 sixˇ"#
2283 .unindent(),
2284 );
2285
2286 cx.update_editor(|editor, window, cx| {
2287 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2288 });
2289 cx.assert_editor_state(
2290 &r#"one
2291 two
2292
2293 three
2294 four
2295 five
2296 ˇ
2297 six"#
2298 .unindent(),
2299 );
2300
2301 cx.update_editor(|editor, window, cx| {
2302 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2303 });
2304 cx.assert_editor_state(
2305 &r#"one
2306 two
2307 ˇ
2308 three
2309 four
2310 five
2311
2312 six"#
2313 .unindent(),
2314 );
2315
2316 cx.update_editor(|editor, window, cx| {
2317 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2318 });
2319 cx.assert_editor_state(
2320 &r#"ˇone
2321 two
2322
2323 three
2324 four
2325 five
2326
2327 six"#
2328 .unindent(),
2329 );
2330}
2331
2332#[gpui::test]
2333async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2334 init_test(cx, |_| {});
2335 let mut cx = EditorTestContext::new(cx).await;
2336 let line_height = cx.update_editor(|editor, window, cx| {
2337 editor
2338 .style(cx)
2339 .text
2340 .line_height_in_pixels(window.rem_size())
2341 });
2342 let window = cx.window;
2343 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2344
2345 cx.set_state(
2346 r#"ˇone
2347 two
2348 three
2349 four
2350 five
2351 six
2352 seven
2353 eight
2354 nine
2355 ten
2356 "#,
2357 );
2358
2359 cx.update_editor(|editor, window, cx| {
2360 assert_eq!(
2361 editor.snapshot(window, cx).scroll_position(),
2362 gpui::Point::new(0., 0.)
2363 );
2364 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2365 assert_eq!(
2366 editor.snapshot(window, cx).scroll_position(),
2367 gpui::Point::new(0., 3.)
2368 );
2369 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2370 assert_eq!(
2371 editor.snapshot(window, cx).scroll_position(),
2372 gpui::Point::new(0., 6.)
2373 );
2374 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2375 assert_eq!(
2376 editor.snapshot(window, cx).scroll_position(),
2377 gpui::Point::new(0., 3.)
2378 );
2379
2380 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2381 assert_eq!(
2382 editor.snapshot(window, cx).scroll_position(),
2383 gpui::Point::new(0., 1.)
2384 );
2385 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2386 assert_eq!(
2387 editor.snapshot(window, cx).scroll_position(),
2388 gpui::Point::new(0., 3.)
2389 );
2390 });
2391}
2392
2393#[gpui::test]
2394async fn test_autoscroll(cx: &mut TestAppContext) {
2395 init_test(cx, |_| {});
2396 let mut cx = EditorTestContext::new(cx).await;
2397
2398 let line_height = cx.update_editor(|editor, window, cx| {
2399 editor.set_vertical_scroll_margin(2, cx);
2400 editor
2401 .style(cx)
2402 .text
2403 .line_height_in_pixels(window.rem_size())
2404 });
2405 let window = cx.window;
2406 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2407
2408 cx.set_state(
2409 r#"ˇone
2410 two
2411 three
2412 four
2413 five
2414 six
2415 seven
2416 eight
2417 nine
2418 ten
2419 "#,
2420 );
2421 cx.update_editor(|editor, window, cx| {
2422 assert_eq!(
2423 editor.snapshot(window, cx).scroll_position(),
2424 gpui::Point::new(0., 0.0)
2425 );
2426 });
2427
2428 // Add a cursor below the visible area. Since both cursors cannot fit
2429 // on screen, the editor autoscrolls to reveal the newest cursor, and
2430 // allows the vertical scroll margin below that cursor.
2431 cx.update_editor(|editor, window, cx| {
2432 editor.change_selections(Default::default(), window, cx, |selections| {
2433 selections.select_ranges([
2434 Point::new(0, 0)..Point::new(0, 0),
2435 Point::new(6, 0)..Point::new(6, 0),
2436 ]);
2437 })
2438 });
2439 cx.update_editor(|editor, window, cx| {
2440 assert_eq!(
2441 editor.snapshot(window, cx).scroll_position(),
2442 gpui::Point::new(0., 3.0)
2443 );
2444 });
2445
2446 // Move down. The editor cursor scrolls down to track the newest cursor.
2447 cx.update_editor(|editor, window, cx| {
2448 editor.move_down(&Default::default(), window, cx);
2449 });
2450 cx.update_editor(|editor, window, cx| {
2451 assert_eq!(
2452 editor.snapshot(window, cx).scroll_position(),
2453 gpui::Point::new(0., 4.0)
2454 );
2455 });
2456
2457 // Add a cursor above the visible area. Since both cursors fit on screen,
2458 // the editor scrolls to show both.
2459 cx.update_editor(|editor, window, cx| {
2460 editor.change_selections(Default::default(), window, cx, |selections| {
2461 selections.select_ranges([
2462 Point::new(1, 0)..Point::new(1, 0),
2463 Point::new(6, 0)..Point::new(6, 0),
2464 ]);
2465 })
2466 });
2467 cx.update_editor(|editor, window, cx| {
2468 assert_eq!(
2469 editor.snapshot(window, cx).scroll_position(),
2470 gpui::Point::new(0., 1.0)
2471 );
2472 });
2473}
2474
2475#[gpui::test]
2476async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2477 init_test(cx, |_| {});
2478 let mut cx = EditorTestContext::new(cx).await;
2479
2480 let line_height = cx.update_editor(|editor, window, cx| {
2481 editor
2482 .style(cx)
2483 .text
2484 .line_height_in_pixels(window.rem_size())
2485 });
2486 let window = cx.window;
2487 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2488 cx.set_state(
2489 &r#"
2490 ˇone
2491 two
2492 threeˇ
2493 four
2494 five
2495 six
2496 seven
2497 eight
2498 nine
2499 ten
2500 "#
2501 .unindent(),
2502 );
2503
2504 cx.update_editor(|editor, window, cx| {
2505 editor.move_page_down(&MovePageDown::default(), window, cx)
2506 });
2507 cx.assert_editor_state(
2508 &r#"
2509 one
2510 two
2511 three
2512 ˇfour
2513 five
2514 sixˇ
2515 seven
2516 eight
2517 nine
2518 ten
2519 "#
2520 .unindent(),
2521 );
2522
2523 cx.update_editor(|editor, window, cx| {
2524 editor.move_page_down(&MovePageDown::default(), window, cx)
2525 });
2526 cx.assert_editor_state(
2527 &r#"
2528 one
2529 two
2530 three
2531 four
2532 five
2533 six
2534 ˇseven
2535 eight
2536 nineˇ
2537 ten
2538 "#
2539 .unindent(),
2540 );
2541
2542 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2543 cx.assert_editor_state(
2544 &r#"
2545 one
2546 two
2547 three
2548 ˇfour
2549 five
2550 sixˇ
2551 seven
2552 eight
2553 nine
2554 ten
2555 "#
2556 .unindent(),
2557 );
2558
2559 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2560 cx.assert_editor_state(
2561 &r#"
2562 ˇone
2563 two
2564 threeˇ
2565 four
2566 five
2567 six
2568 seven
2569 eight
2570 nine
2571 ten
2572 "#
2573 .unindent(),
2574 );
2575
2576 // Test select collapsing
2577 cx.update_editor(|editor, window, cx| {
2578 editor.move_page_down(&MovePageDown::default(), window, cx);
2579 editor.move_page_down(&MovePageDown::default(), window, cx);
2580 editor.move_page_down(&MovePageDown::default(), window, cx);
2581 });
2582 cx.assert_editor_state(
2583 &r#"
2584 one
2585 two
2586 three
2587 four
2588 five
2589 six
2590 seven
2591 eight
2592 nine
2593 ˇten
2594 ˇ"#
2595 .unindent(),
2596 );
2597}
2598
2599#[gpui::test]
2600async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2601 init_test(cx, |_| {});
2602 let mut cx = EditorTestContext::new(cx).await;
2603 cx.set_state("one «two threeˇ» four");
2604 cx.update_editor(|editor, window, cx| {
2605 editor.delete_to_beginning_of_line(
2606 &DeleteToBeginningOfLine {
2607 stop_at_indent: false,
2608 },
2609 window,
2610 cx,
2611 );
2612 assert_eq!(editor.text(cx), " four");
2613 });
2614}
2615
2616#[gpui::test]
2617async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2618 init_test(cx, |_| {});
2619
2620 let mut cx = EditorTestContext::new(cx).await;
2621
2622 // For an empty selection, the preceding word fragment is deleted.
2623 // For non-empty selections, only selected characters are deleted.
2624 cx.set_state("onˇe two t«hreˇ»e four");
2625 cx.update_editor(|editor, window, cx| {
2626 editor.delete_to_previous_word_start(
2627 &DeleteToPreviousWordStart {
2628 ignore_newlines: false,
2629 ignore_brackets: false,
2630 },
2631 window,
2632 cx,
2633 );
2634 });
2635 cx.assert_editor_state("ˇe two tˇe four");
2636
2637 cx.set_state("e tˇwo te «fˇ»our");
2638 cx.update_editor(|editor, window, cx| {
2639 editor.delete_to_next_word_end(
2640 &DeleteToNextWordEnd {
2641 ignore_newlines: false,
2642 ignore_brackets: false,
2643 },
2644 window,
2645 cx,
2646 );
2647 });
2648 cx.assert_editor_state("e tˇ te ˇour");
2649}
2650
2651#[gpui::test]
2652async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2653 init_test(cx, |_| {});
2654
2655 let mut cx = EditorTestContext::new(cx).await;
2656
2657 cx.set_state("here is some text ˇwith a space");
2658 cx.update_editor(|editor, window, cx| {
2659 editor.delete_to_previous_word_start(
2660 &DeleteToPreviousWordStart {
2661 ignore_newlines: false,
2662 ignore_brackets: true,
2663 },
2664 window,
2665 cx,
2666 );
2667 });
2668 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2669 cx.assert_editor_state("here is some textˇwith a space");
2670
2671 cx.set_state("here is some text ˇwith a space");
2672 cx.update_editor(|editor, window, cx| {
2673 editor.delete_to_previous_word_start(
2674 &DeleteToPreviousWordStart {
2675 ignore_newlines: false,
2676 ignore_brackets: false,
2677 },
2678 window,
2679 cx,
2680 );
2681 });
2682 cx.assert_editor_state("here is some textˇwith a space");
2683
2684 cx.set_state("here is some textˇ with a space");
2685 cx.update_editor(|editor, window, cx| {
2686 editor.delete_to_next_word_end(
2687 &DeleteToNextWordEnd {
2688 ignore_newlines: false,
2689 ignore_brackets: true,
2690 },
2691 window,
2692 cx,
2693 );
2694 });
2695 // Same happens in the other direction.
2696 cx.assert_editor_state("here is some textˇwith a space");
2697
2698 cx.set_state("here is some textˇ with a space");
2699 cx.update_editor(|editor, window, cx| {
2700 editor.delete_to_next_word_end(
2701 &DeleteToNextWordEnd {
2702 ignore_newlines: false,
2703 ignore_brackets: false,
2704 },
2705 window,
2706 cx,
2707 );
2708 });
2709 cx.assert_editor_state("here is some textˇwith a space");
2710
2711 cx.set_state("here is some textˇ with a space");
2712 cx.update_editor(|editor, window, cx| {
2713 editor.delete_to_next_word_end(
2714 &DeleteToNextWordEnd {
2715 ignore_newlines: true,
2716 ignore_brackets: false,
2717 },
2718 window,
2719 cx,
2720 );
2721 });
2722 cx.assert_editor_state("here is some textˇwith a space");
2723 cx.update_editor(|editor, window, cx| {
2724 editor.delete_to_previous_word_start(
2725 &DeleteToPreviousWordStart {
2726 ignore_newlines: true,
2727 ignore_brackets: false,
2728 },
2729 window,
2730 cx,
2731 );
2732 });
2733 cx.assert_editor_state("here is some ˇwith a space");
2734 cx.update_editor(|editor, window, cx| {
2735 editor.delete_to_previous_word_start(
2736 &DeleteToPreviousWordStart {
2737 ignore_newlines: true,
2738 ignore_brackets: false,
2739 },
2740 window,
2741 cx,
2742 );
2743 });
2744 // Single whitespaces are removed with the word behind them.
2745 cx.assert_editor_state("here is ˇwith a space");
2746 cx.update_editor(|editor, window, cx| {
2747 editor.delete_to_previous_word_start(
2748 &DeleteToPreviousWordStart {
2749 ignore_newlines: true,
2750 ignore_brackets: false,
2751 },
2752 window,
2753 cx,
2754 );
2755 });
2756 cx.assert_editor_state("here ˇwith a space");
2757 cx.update_editor(|editor, window, cx| {
2758 editor.delete_to_previous_word_start(
2759 &DeleteToPreviousWordStart {
2760 ignore_newlines: true,
2761 ignore_brackets: false,
2762 },
2763 window,
2764 cx,
2765 );
2766 });
2767 cx.assert_editor_state("ˇwith a space");
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_previous_word_start(
2770 &DeleteToPreviousWordStart {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state("ˇwith a space");
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_next_word_end(
2781 &DeleteToNextWordEnd {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 // Same happens in the other direction.
2790 cx.assert_editor_state("ˇ a space");
2791 cx.update_editor(|editor, window, cx| {
2792 editor.delete_to_next_word_end(
2793 &DeleteToNextWordEnd {
2794 ignore_newlines: true,
2795 ignore_brackets: false,
2796 },
2797 window,
2798 cx,
2799 );
2800 });
2801 cx.assert_editor_state("ˇ space");
2802 cx.update_editor(|editor, window, cx| {
2803 editor.delete_to_next_word_end(
2804 &DeleteToNextWordEnd {
2805 ignore_newlines: true,
2806 ignore_brackets: false,
2807 },
2808 window,
2809 cx,
2810 );
2811 });
2812 cx.assert_editor_state("ˇ");
2813 cx.update_editor(|editor, window, cx| {
2814 editor.delete_to_next_word_end(
2815 &DeleteToNextWordEnd {
2816 ignore_newlines: true,
2817 ignore_brackets: false,
2818 },
2819 window,
2820 cx,
2821 );
2822 });
2823 cx.assert_editor_state("ˇ");
2824 cx.update_editor(|editor, window, cx| {
2825 editor.delete_to_previous_word_start(
2826 &DeleteToPreviousWordStart {
2827 ignore_newlines: true,
2828 ignore_brackets: false,
2829 },
2830 window,
2831 cx,
2832 );
2833 });
2834 cx.assert_editor_state("ˇ");
2835}
2836
2837#[gpui::test]
2838async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2839 init_test(cx, |_| {});
2840
2841 let language = Arc::new(
2842 Language::new(
2843 LanguageConfig {
2844 brackets: BracketPairConfig {
2845 pairs: vec![
2846 BracketPair {
2847 start: "\"".to_string(),
2848 end: "\"".to_string(),
2849 close: true,
2850 surround: true,
2851 newline: false,
2852 },
2853 BracketPair {
2854 start: "(".to_string(),
2855 end: ")".to_string(),
2856 close: true,
2857 surround: true,
2858 newline: true,
2859 },
2860 ],
2861 ..BracketPairConfig::default()
2862 },
2863 ..LanguageConfig::default()
2864 },
2865 Some(tree_sitter_rust::LANGUAGE.into()),
2866 )
2867 .with_brackets_query(
2868 r#"
2869 ("(" @open ")" @close)
2870 ("\"" @open "\"" @close)
2871 "#,
2872 )
2873 .unwrap(),
2874 );
2875
2876 let mut cx = EditorTestContext::new(cx).await;
2877 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2878
2879 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2880 cx.update_editor(|editor, window, cx| {
2881 editor.delete_to_previous_word_start(
2882 &DeleteToPreviousWordStart {
2883 ignore_newlines: true,
2884 ignore_brackets: false,
2885 },
2886 window,
2887 cx,
2888 );
2889 });
2890 // Deletion stops before brackets if asked to not ignore them.
2891 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2892 cx.update_editor(|editor, window, cx| {
2893 editor.delete_to_previous_word_start(
2894 &DeleteToPreviousWordStart {
2895 ignore_newlines: true,
2896 ignore_brackets: false,
2897 },
2898 window,
2899 cx,
2900 );
2901 });
2902 // Deletion has to remove a single bracket and then stop again.
2903 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2904
2905 cx.update_editor(|editor, window, cx| {
2906 editor.delete_to_previous_word_start(
2907 &DeleteToPreviousWordStart {
2908 ignore_newlines: true,
2909 ignore_brackets: false,
2910 },
2911 window,
2912 cx,
2913 );
2914 });
2915 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2916
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_previous_word_start(
2919 &DeleteToPreviousWordStart {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2928
2929 cx.update_editor(|editor, window, cx| {
2930 editor.delete_to_previous_word_start(
2931 &DeleteToPreviousWordStart {
2932 ignore_newlines: true,
2933 ignore_brackets: false,
2934 },
2935 window,
2936 cx,
2937 );
2938 });
2939 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2940
2941 cx.update_editor(|editor, window, cx| {
2942 editor.delete_to_next_word_end(
2943 &DeleteToNextWordEnd {
2944 ignore_newlines: true,
2945 ignore_brackets: false,
2946 },
2947 window,
2948 cx,
2949 );
2950 });
2951 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2952 cx.assert_editor_state(r#"ˇ");"#);
2953
2954 cx.update_editor(|editor, window, cx| {
2955 editor.delete_to_next_word_end(
2956 &DeleteToNextWordEnd {
2957 ignore_newlines: true,
2958 ignore_brackets: false,
2959 },
2960 window,
2961 cx,
2962 );
2963 });
2964 cx.assert_editor_state(r#"ˇ"#);
2965
2966 cx.update_editor(|editor, window, cx| {
2967 editor.delete_to_next_word_end(
2968 &DeleteToNextWordEnd {
2969 ignore_newlines: true,
2970 ignore_brackets: false,
2971 },
2972 window,
2973 cx,
2974 );
2975 });
2976 cx.assert_editor_state(r#"ˇ"#);
2977
2978 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2979 cx.update_editor(|editor, window, cx| {
2980 editor.delete_to_previous_word_start(
2981 &DeleteToPreviousWordStart {
2982 ignore_newlines: true,
2983 ignore_brackets: true,
2984 },
2985 window,
2986 cx,
2987 );
2988 });
2989 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2990}
2991
2992#[gpui::test]
2993fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2994 init_test(cx, |_| {});
2995
2996 let editor = cx.add_window(|window, cx| {
2997 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2998 build_editor(buffer, window, cx)
2999 });
3000 let del_to_prev_word_start = DeleteToPreviousWordStart {
3001 ignore_newlines: false,
3002 ignore_brackets: false,
3003 };
3004 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3005 ignore_newlines: true,
3006 ignore_brackets: false,
3007 };
3008
3009 _ = editor.update(cx, |editor, window, cx| {
3010 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3011 s.select_display_ranges([
3012 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3013 ])
3014 });
3015 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3016 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3017 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3018 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
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\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");
3023 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3024 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3025 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3026 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3027 });
3028}
3029
3030#[gpui::test]
3031fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3032 init_test(cx, |_| {});
3033
3034 let editor = cx.add_window(|window, cx| {
3035 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3036 build_editor(buffer, window, cx)
3037 });
3038 let del_to_next_word_end = DeleteToNextWordEnd {
3039 ignore_newlines: false,
3040 ignore_brackets: false,
3041 };
3042 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3043 ignore_newlines: true,
3044 ignore_brackets: false,
3045 };
3046
3047 _ = editor.update(cx, |editor, window, cx| {
3048 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3049 s.select_display_ranges([
3050 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3051 ])
3052 });
3053 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3054 assert_eq!(
3055 editor.buffer.read(cx).read(cx).text(),
3056 "one\n two\nthree\n four"
3057 );
3058 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3059 assert_eq!(
3060 editor.buffer.read(cx).read(cx).text(),
3061 "\n two\nthree\n four"
3062 );
3063 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3064 assert_eq!(
3065 editor.buffer.read(cx).read(cx).text(),
3066 "two\nthree\n four"
3067 );
3068 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3069 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3070 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3071 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3072 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3073 assert_eq!(editor.buffer.read(cx).read(cx).text(), "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(), "");
3076 });
3077}
3078
3079#[gpui::test]
3080fn test_newline(cx: &mut TestAppContext) {
3081 init_test(cx, |_| {});
3082
3083 let editor = cx.add_window(|window, cx| {
3084 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3085 build_editor(buffer, window, cx)
3086 });
3087
3088 _ = editor.update(cx, |editor, window, cx| {
3089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3090 s.select_display_ranges([
3091 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3092 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3093 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3094 ])
3095 });
3096
3097 editor.newline(&Newline, window, cx);
3098 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3099 });
3100}
3101
3102#[gpui::test]
3103async fn test_newline_yaml(cx: &mut TestAppContext) {
3104 init_test(cx, |_| {});
3105
3106 let mut cx = EditorTestContext::new(cx).await;
3107 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3108 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3109
3110 // Object (between 2 fields)
3111 cx.set_state(indoc! {"
3112 test:ˇ
3113 hello: bye"});
3114 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3115 cx.assert_editor_state(indoc! {"
3116 test:
3117 ˇ
3118 hello: bye"});
3119
3120 // Object (first and single line)
3121 cx.set_state(indoc! {"
3122 test:ˇ"});
3123 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3124 cx.assert_editor_state(indoc! {"
3125 test:
3126 ˇ"});
3127
3128 // Array with objects (after first element)
3129 cx.set_state(indoc! {"
3130 test:
3131 - foo: barˇ"});
3132 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3133 cx.assert_editor_state(indoc! {"
3134 test:
3135 - foo: bar
3136 ˇ"});
3137
3138 // Array with objects and comment
3139 cx.set_state(indoc! {"
3140 test:
3141 - foo: bar
3142 - bar: # testˇ"});
3143 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3144 cx.assert_editor_state(indoc! {"
3145 test:
3146 - foo: bar
3147 - bar: # test
3148 ˇ"});
3149
3150 // Array with objects (after second element)
3151 cx.set_state(indoc! {"
3152 test:
3153 - foo: bar
3154 - bar: fooˇ"});
3155 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3156 cx.assert_editor_state(indoc! {"
3157 test:
3158 - foo: bar
3159 - bar: foo
3160 ˇ"});
3161
3162 // Array with strings (after first element)
3163 cx.set_state(indoc! {"
3164 test:
3165 - fooˇ"});
3166 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3167 cx.assert_editor_state(indoc! {"
3168 test:
3169 - foo
3170 ˇ"});
3171}
3172
3173#[gpui::test]
3174fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3175 init_test(cx, |_| {});
3176
3177 let editor = cx.add_window(|window, cx| {
3178 let buffer = MultiBuffer::build_simple(
3179 "
3180 a
3181 b(
3182 X
3183 )
3184 c(
3185 X
3186 )
3187 "
3188 .unindent()
3189 .as_str(),
3190 cx,
3191 );
3192 let mut editor = build_editor(buffer, window, cx);
3193 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3194 s.select_ranges([
3195 Point::new(2, 4)..Point::new(2, 5),
3196 Point::new(5, 4)..Point::new(5, 5),
3197 ])
3198 });
3199 editor
3200 });
3201
3202 _ = editor.update(cx, |editor, window, cx| {
3203 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3204 editor.buffer.update(cx, |buffer, cx| {
3205 buffer.edit(
3206 [
3207 (Point::new(1, 2)..Point::new(3, 0), ""),
3208 (Point::new(4, 2)..Point::new(6, 0), ""),
3209 ],
3210 None,
3211 cx,
3212 );
3213 assert_eq!(
3214 buffer.read(cx).text(),
3215 "
3216 a
3217 b()
3218 c()
3219 "
3220 .unindent()
3221 );
3222 });
3223 assert_eq!(
3224 editor.selections.ranges(&editor.display_snapshot(cx)),
3225 &[
3226 Point::new(1, 2)..Point::new(1, 2),
3227 Point::new(2, 2)..Point::new(2, 2),
3228 ],
3229 );
3230
3231 editor.newline(&Newline, window, cx);
3232 assert_eq!(
3233 editor.text(cx),
3234 "
3235 a
3236 b(
3237 )
3238 c(
3239 )
3240 "
3241 .unindent()
3242 );
3243
3244 // The selections are moved after the inserted newlines
3245 assert_eq!(
3246 editor.selections.ranges(&editor.display_snapshot(cx)),
3247 &[
3248 Point::new(2, 0)..Point::new(2, 0),
3249 Point::new(4, 0)..Point::new(4, 0),
3250 ],
3251 );
3252 });
3253}
3254
3255#[gpui::test]
3256async fn test_newline_above(cx: &mut TestAppContext) {
3257 init_test(cx, |settings| {
3258 settings.defaults.tab_size = NonZeroU32::new(4)
3259 });
3260
3261 let language = Arc::new(
3262 Language::new(
3263 LanguageConfig::default(),
3264 Some(tree_sitter_rust::LANGUAGE.into()),
3265 )
3266 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3267 .unwrap(),
3268 );
3269
3270 let mut cx = EditorTestContext::new(cx).await;
3271 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3272 cx.set_state(indoc! {"
3273 const a: ˇA = (
3274 (ˇ
3275 «const_functionˇ»(ˇ),
3276 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3277 )ˇ
3278 ˇ);ˇ
3279 "});
3280
3281 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3282 cx.assert_editor_state(indoc! {"
3283 ˇ
3284 const a: A = (
3285 ˇ
3286 (
3287 ˇ
3288 ˇ
3289 const_function(),
3290 ˇ
3291 ˇ
3292 ˇ
3293 ˇ
3294 something_else,
3295 ˇ
3296 )
3297 ˇ
3298 ˇ
3299 );
3300 "});
3301}
3302
3303#[gpui::test]
3304async fn test_newline_below(cx: &mut TestAppContext) {
3305 init_test(cx, |settings| {
3306 settings.defaults.tab_size = NonZeroU32::new(4)
3307 });
3308
3309 let language = Arc::new(
3310 Language::new(
3311 LanguageConfig::default(),
3312 Some(tree_sitter_rust::LANGUAGE.into()),
3313 )
3314 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3315 .unwrap(),
3316 );
3317
3318 let mut cx = EditorTestContext::new(cx).await;
3319 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3320 cx.set_state(indoc! {"
3321 const a: ˇA = (
3322 (ˇ
3323 «const_functionˇ»(ˇ),
3324 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3325 )ˇ
3326 ˇ);ˇ
3327 "});
3328
3329 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 const a: A = (
3332 ˇ
3333 (
3334 ˇ
3335 const_function(),
3336 ˇ
3337 ˇ
3338 something_else,
3339 ˇ
3340 ˇ
3341 ˇ
3342 ˇ
3343 )
3344 ˇ
3345 );
3346 ˇ
3347 ˇ
3348 "});
3349}
3350
3351#[gpui::test]
3352async fn test_newline_comments(cx: &mut TestAppContext) {
3353 init_test(cx, |settings| {
3354 settings.defaults.tab_size = NonZeroU32::new(4)
3355 });
3356
3357 let language = Arc::new(Language::new(
3358 LanguageConfig {
3359 line_comments: vec!["// ".into()],
3360 ..LanguageConfig::default()
3361 },
3362 None,
3363 ));
3364 {
3365 let mut cx = EditorTestContext::new(cx).await;
3366 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3367 cx.set_state(indoc! {"
3368 // Fooˇ
3369 "});
3370
3371 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3372 cx.assert_editor_state(indoc! {"
3373 // Foo
3374 // ˇ
3375 "});
3376 // Ensure that we add comment prefix when existing line contains space
3377 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3378 cx.assert_editor_state(
3379 indoc! {"
3380 // Foo
3381 //s
3382 // ˇ
3383 "}
3384 .replace("s", " ") // s is used as space placeholder to prevent format on save
3385 .as_str(),
3386 );
3387 // Ensure that we add comment prefix when existing line does not contain space
3388 cx.set_state(indoc! {"
3389 // Foo
3390 //ˇ
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 // Foo
3395 //
3396 // ˇ
3397 "});
3398 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3399 cx.set_state(indoc! {"
3400 ˇ// Foo
3401 "});
3402 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404
3405 ˇ// Foo
3406 "});
3407 }
3408 // Ensure that comment continuations can be disabled.
3409 update_test_language_settings(cx, |settings| {
3410 settings.defaults.extend_comment_on_newline = Some(false);
3411 });
3412 let mut cx = EditorTestContext::new(cx).await;
3413 cx.set_state(indoc! {"
3414 // Fooˇ
3415 "});
3416 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3417 cx.assert_editor_state(indoc! {"
3418 // Foo
3419 ˇ
3420 "});
3421}
3422
3423#[gpui::test]
3424async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3425 init_test(cx, |settings| {
3426 settings.defaults.tab_size = NonZeroU32::new(4)
3427 });
3428
3429 let language = Arc::new(Language::new(
3430 LanguageConfig {
3431 line_comments: vec!["// ".into(), "/// ".into()],
3432 ..LanguageConfig::default()
3433 },
3434 None,
3435 ));
3436 {
3437 let mut cx = EditorTestContext::new(cx).await;
3438 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3439 cx.set_state(indoc! {"
3440 //ˇ
3441 "});
3442 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 //
3445 // ˇ
3446 "});
3447
3448 cx.set_state(indoc! {"
3449 ///ˇ
3450 "});
3451 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3452 cx.assert_editor_state(indoc! {"
3453 ///
3454 /// ˇ
3455 "});
3456 }
3457}
3458
3459#[gpui::test]
3460async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3461 init_test(cx, |settings| {
3462 settings.defaults.tab_size = NonZeroU32::new(4)
3463 });
3464
3465 let language = Arc::new(
3466 Language::new(
3467 LanguageConfig {
3468 documentation_comment: Some(language::BlockCommentConfig {
3469 start: "/**".into(),
3470 end: "*/".into(),
3471 prefix: "* ".into(),
3472 tab_size: 1,
3473 }),
3474
3475 ..LanguageConfig::default()
3476 },
3477 Some(tree_sitter_rust::LANGUAGE.into()),
3478 )
3479 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3480 .unwrap(),
3481 );
3482
3483 {
3484 let mut cx = EditorTestContext::new(cx).await;
3485 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3486 cx.set_state(indoc! {"
3487 /**ˇ
3488 "});
3489
3490 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3491 cx.assert_editor_state(indoc! {"
3492 /**
3493 * ˇ
3494 "});
3495 // Ensure that if cursor is before the comment start,
3496 // we do not actually insert a comment prefix.
3497 cx.set_state(indoc! {"
3498 ˇ/**
3499 "});
3500 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3501 cx.assert_editor_state(indoc! {"
3502
3503 ˇ/**
3504 "});
3505 // Ensure that if cursor is between it doesn't add comment prefix.
3506 cx.set_state(indoc! {"
3507 /*ˇ*
3508 "});
3509 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3510 cx.assert_editor_state(indoc! {"
3511 /*
3512 ˇ*
3513 "});
3514 // Ensure that if suffix exists on same line after cursor it adds new line.
3515 cx.set_state(indoc! {"
3516 /**ˇ*/
3517 "});
3518 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3519 cx.assert_editor_state(indoc! {"
3520 /**
3521 * ˇ
3522 */
3523 "});
3524 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3525 cx.set_state(indoc! {"
3526 /**ˇ */
3527 "});
3528 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3529 cx.assert_editor_state(indoc! {"
3530 /**
3531 * ˇ
3532 */
3533 "});
3534 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3535 cx.set_state(indoc! {"
3536 /** ˇ*/
3537 "});
3538 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3539 cx.assert_editor_state(
3540 indoc! {"
3541 /**s
3542 * ˇ
3543 */
3544 "}
3545 .replace("s", " ") // s is used as space placeholder to prevent format on save
3546 .as_str(),
3547 );
3548 // Ensure that delimiter space is preserved when newline on already
3549 // spaced delimiter.
3550 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3551 cx.assert_editor_state(
3552 indoc! {"
3553 /**s
3554 *s
3555 * ˇ
3556 */
3557 "}
3558 .replace("s", " ") // s is used as space placeholder to prevent format on save
3559 .as_str(),
3560 );
3561 // Ensure that delimiter space is preserved when space is not
3562 // on existing delimiter.
3563 cx.set_state(indoc! {"
3564 /**
3565 *ˇ
3566 */
3567 "});
3568 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3569 cx.assert_editor_state(indoc! {"
3570 /**
3571 *
3572 * ˇ
3573 */
3574 "});
3575 // Ensure that if suffix exists on same line after cursor it
3576 // doesn't add extra new line if prefix is not on same line.
3577 cx.set_state(indoc! {"
3578 /**
3579 ˇ*/
3580 "});
3581 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3582 cx.assert_editor_state(indoc! {"
3583 /**
3584
3585 ˇ*/
3586 "});
3587 // Ensure that it detects suffix after existing prefix.
3588 cx.set_state(indoc! {"
3589 /**ˇ/
3590 "});
3591 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3592 cx.assert_editor_state(indoc! {"
3593 /**
3594 ˇ/
3595 "});
3596 // Ensure that if suffix exists on same line before
3597 // cursor it does not add comment prefix.
3598 cx.set_state(indoc! {"
3599 /** */ˇ
3600 "});
3601 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3602 cx.assert_editor_state(indoc! {"
3603 /** */
3604 ˇ
3605 "});
3606 // Ensure that if suffix exists on same line before
3607 // cursor it does not add comment prefix.
3608 cx.set_state(indoc! {"
3609 /**
3610 *
3611 */ˇ
3612 "});
3613 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3614 cx.assert_editor_state(indoc! {"
3615 /**
3616 *
3617 */
3618 ˇ
3619 "});
3620
3621 // Ensure that inline comment followed by code
3622 // doesn't add comment prefix on newline
3623 cx.set_state(indoc! {"
3624 /** */ textˇ
3625 "});
3626 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3627 cx.assert_editor_state(indoc! {"
3628 /** */ text
3629 ˇ
3630 "});
3631
3632 // Ensure that text after comment end tag
3633 // doesn't add comment prefix on newline
3634 cx.set_state(indoc! {"
3635 /**
3636 *
3637 */ˇtext
3638 "});
3639 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3640 cx.assert_editor_state(indoc! {"
3641 /**
3642 *
3643 */
3644 ˇtext
3645 "});
3646
3647 // Ensure if not comment block it doesn't
3648 // add comment prefix on newline
3649 cx.set_state(indoc! {"
3650 * textˇ
3651 "});
3652 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3653 cx.assert_editor_state(indoc! {"
3654 * text
3655 ˇ
3656 "});
3657 }
3658 // Ensure that comment continuations can be disabled.
3659 update_test_language_settings(cx, |settings| {
3660 settings.defaults.extend_comment_on_newline = Some(false);
3661 });
3662 let mut cx = EditorTestContext::new(cx).await;
3663 cx.set_state(indoc! {"
3664 /**ˇ
3665 "});
3666 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3667 cx.assert_editor_state(indoc! {"
3668 /**
3669 ˇ
3670 "});
3671}
3672
3673#[gpui::test]
3674async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3675 init_test(cx, |settings| {
3676 settings.defaults.tab_size = NonZeroU32::new(4)
3677 });
3678
3679 let lua_language = Arc::new(Language::new(
3680 LanguageConfig {
3681 line_comments: vec!["--".into()],
3682 block_comment: Some(language::BlockCommentConfig {
3683 start: "--[[".into(),
3684 prefix: "".into(),
3685 end: "]]".into(),
3686 tab_size: 0,
3687 }),
3688 ..LanguageConfig::default()
3689 },
3690 None,
3691 ));
3692
3693 let mut cx = EditorTestContext::new(cx).await;
3694 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3695
3696 // Line with line comment should extend
3697 cx.set_state(indoc! {"
3698 --ˇ
3699 "});
3700 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 --
3703 --ˇ
3704 "});
3705
3706 // Line with block comment that matches line comment should not extend
3707 cx.set_state(indoc! {"
3708 --[[ˇ
3709 "});
3710 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3711 cx.assert_editor_state(indoc! {"
3712 --[[
3713 ˇ
3714 "});
3715}
3716
3717#[gpui::test]
3718fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3719 init_test(cx, |_| {});
3720
3721 let editor = cx.add_window(|window, cx| {
3722 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3723 let mut editor = build_editor(buffer, window, cx);
3724 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3725 s.select_ranges([
3726 MultiBufferOffset(3)..MultiBufferOffset(4),
3727 MultiBufferOffset(11)..MultiBufferOffset(12),
3728 MultiBufferOffset(19)..MultiBufferOffset(20),
3729 ])
3730 });
3731 editor
3732 });
3733
3734 _ = editor.update(cx, |editor, window, cx| {
3735 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3736 editor.buffer.update(cx, |buffer, cx| {
3737 buffer.edit(
3738 [
3739 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3740 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3741 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3742 ],
3743 None,
3744 cx,
3745 );
3746 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3747 });
3748 assert_eq!(
3749 editor.selections.ranges(&editor.display_snapshot(cx)),
3750 &[
3751 MultiBufferOffset(2)..MultiBufferOffset(2),
3752 MultiBufferOffset(7)..MultiBufferOffset(7),
3753 MultiBufferOffset(12)..MultiBufferOffset(12)
3754 ],
3755 );
3756
3757 editor.insert("Z", window, cx);
3758 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3759
3760 // The selections are moved after the inserted characters
3761 assert_eq!(
3762 editor.selections.ranges(&editor.display_snapshot(cx)),
3763 &[
3764 MultiBufferOffset(3)..MultiBufferOffset(3),
3765 MultiBufferOffset(9)..MultiBufferOffset(9),
3766 MultiBufferOffset(15)..MultiBufferOffset(15)
3767 ],
3768 );
3769 });
3770}
3771
3772#[gpui::test]
3773async fn test_tab(cx: &mut TestAppContext) {
3774 init_test(cx, |settings| {
3775 settings.defaults.tab_size = NonZeroU32::new(3)
3776 });
3777
3778 let mut cx = EditorTestContext::new(cx).await;
3779 cx.set_state(indoc! {"
3780 ˇabˇc
3781 ˇ🏀ˇ🏀ˇefg
3782 dˇ
3783 "});
3784 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3785 cx.assert_editor_state(indoc! {"
3786 ˇab ˇc
3787 ˇ🏀 ˇ🏀 ˇefg
3788 d ˇ
3789 "});
3790
3791 cx.set_state(indoc! {"
3792 a
3793 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3794 "});
3795 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 a
3798 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3799 "});
3800}
3801
3802#[gpui::test]
3803async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3804 init_test(cx, |_| {});
3805
3806 let mut cx = EditorTestContext::new(cx).await;
3807 let language = Arc::new(
3808 Language::new(
3809 LanguageConfig::default(),
3810 Some(tree_sitter_rust::LANGUAGE.into()),
3811 )
3812 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3813 .unwrap(),
3814 );
3815 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3816
3817 // test when all cursors are not at suggested indent
3818 // then simply move to their suggested indent location
3819 cx.set_state(indoc! {"
3820 const a: B = (
3821 c(
3822 ˇ
3823 ˇ )
3824 );
3825 "});
3826 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3827 cx.assert_editor_state(indoc! {"
3828 const a: B = (
3829 c(
3830 ˇ
3831 ˇ)
3832 );
3833 "});
3834
3835 // test cursor already at suggested indent not moving when
3836 // other cursors are yet to reach their suggested indents
3837 cx.set_state(indoc! {"
3838 ˇ
3839 const a: B = (
3840 c(
3841 d(
3842 ˇ
3843 )
3844 ˇ
3845 ˇ )
3846 );
3847 "});
3848 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3849 cx.assert_editor_state(indoc! {"
3850 ˇ
3851 const a: B = (
3852 c(
3853 d(
3854 ˇ
3855 )
3856 ˇ
3857 ˇ)
3858 );
3859 "});
3860 // test when all cursors are at suggested indent then tab is inserted
3861 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3862 cx.assert_editor_state(indoc! {"
3863 ˇ
3864 const a: B = (
3865 c(
3866 d(
3867 ˇ
3868 )
3869 ˇ
3870 ˇ)
3871 );
3872 "});
3873
3874 // test when current indent is less than suggested indent,
3875 // we adjust line to match suggested indent and move cursor to it
3876 //
3877 // when no other cursor is at word boundary, all of them should move
3878 cx.set_state(indoc! {"
3879 const a: B = (
3880 c(
3881 d(
3882 ˇ
3883 ˇ )
3884 ˇ )
3885 );
3886 "});
3887 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3888 cx.assert_editor_state(indoc! {"
3889 const a: B = (
3890 c(
3891 d(
3892 ˇ
3893 ˇ)
3894 ˇ)
3895 );
3896 "});
3897
3898 // test when current indent is less than suggested indent,
3899 // we adjust line to match suggested indent and move cursor to it
3900 //
3901 // when some other cursor is at word boundary, it should not move
3902 cx.set_state(indoc! {"
3903 const a: B = (
3904 c(
3905 d(
3906 ˇ
3907 ˇ )
3908 ˇ)
3909 );
3910 "});
3911 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3912 cx.assert_editor_state(indoc! {"
3913 const a: B = (
3914 c(
3915 d(
3916 ˇ
3917 ˇ)
3918 ˇ)
3919 );
3920 "});
3921
3922 // test when current indent is more than suggested indent,
3923 // we just move cursor to current indent instead of suggested indent
3924 //
3925 // when no other cursor is at word boundary, all of them should move
3926 cx.set_state(indoc! {"
3927 const a: B = (
3928 c(
3929 d(
3930 ˇ
3931 ˇ )
3932 ˇ )
3933 );
3934 "});
3935 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3936 cx.assert_editor_state(indoc! {"
3937 const a: B = (
3938 c(
3939 d(
3940 ˇ
3941 ˇ)
3942 ˇ)
3943 );
3944 "});
3945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3946 cx.assert_editor_state(indoc! {"
3947 const a: B = (
3948 c(
3949 d(
3950 ˇ
3951 ˇ)
3952 ˇ)
3953 );
3954 "});
3955
3956 // test when current indent is more than suggested indent,
3957 // we just move cursor to current indent instead of suggested indent
3958 //
3959 // when some other cursor is at word boundary, it doesn't move
3960 cx.set_state(indoc! {"
3961 const a: B = (
3962 c(
3963 d(
3964 ˇ
3965 ˇ )
3966 ˇ)
3967 );
3968 "});
3969 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3970 cx.assert_editor_state(indoc! {"
3971 const a: B = (
3972 c(
3973 d(
3974 ˇ
3975 ˇ)
3976 ˇ)
3977 );
3978 "});
3979
3980 // handle auto-indent when there are multiple cursors on the same line
3981 cx.set_state(indoc! {"
3982 const a: B = (
3983 c(
3984 ˇ ˇ
3985 ˇ )
3986 );
3987 "});
3988 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3989 cx.assert_editor_state(indoc! {"
3990 const a: B = (
3991 c(
3992 ˇ
3993 ˇ)
3994 );
3995 "});
3996}
3997
3998#[gpui::test]
3999async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4000 init_test(cx, |settings| {
4001 settings.defaults.tab_size = NonZeroU32::new(3)
4002 });
4003
4004 let mut cx = EditorTestContext::new(cx).await;
4005 cx.set_state(indoc! {"
4006 ˇ
4007 \t ˇ
4008 \t ˇ
4009 \t ˇ
4010 \t \t\t \t \t\t \t\t \t \t ˇ
4011 "});
4012
4013 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 ˇ
4016 \t ˇ
4017 \t ˇ
4018 \t ˇ
4019 \t \t\t \t \t\t \t\t \t \t ˇ
4020 "});
4021}
4022
4023#[gpui::test]
4024async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4025 init_test(cx, |settings| {
4026 settings.defaults.tab_size = NonZeroU32::new(4)
4027 });
4028
4029 let language = Arc::new(
4030 Language::new(
4031 LanguageConfig::default(),
4032 Some(tree_sitter_rust::LANGUAGE.into()),
4033 )
4034 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4035 .unwrap(),
4036 );
4037
4038 let mut cx = EditorTestContext::new(cx).await;
4039 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4040 cx.set_state(indoc! {"
4041 fn a() {
4042 if b {
4043 \t ˇc
4044 }
4045 }
4046 "});
4047
4048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 fn a() {
4051 if b {
4052 ˇc
4053 }
4054 }
4055 "});
4056}
4057
4058#[gpui::test]
4059async fn test_indent_outdent(cx: &mut TestAppContext) {
4060 init_test(cx, |settings| {
4061 settings.defaults.tab_size = NonZeroU32::new(4);
4062 });
4063
4064 let mut cx = EditorTestContext::new(cx).await;
4065
4066 cx.set_state(indoc! {"
4067 «oneˇ» «twoˇ»
4068 three
4069 four
4070 "});
4071 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4072 cx.assert_editor_state(indoc! {"
4073 «oneˇ» «twoˇ»
4074 three
4075 four
4076 "});
4077
4078 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4079 cx.assert_editor_state(indoc! {"
4080 «oneˇ» «twoˇ»
4081 three
4082 four
4083 "});
4084
4085 // select across line ending
4086 cx.set_state(indoc! {"
4087 one two
4088 t«hree
4089 ˇ» four
4090 "});
4091 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4092 cx.assert_editor_state(indoc! {"
4093 one two
4094 t«hree
4095 ˇ» four
4096 "});
4097
4098 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4099 cx.assert_editor_state(indoc! {"
4100 one two
4101 t«hree
4102 ˇ» four
4103 "});
4104
4105 // Ensure that indenting/outdenting works when the cursor is at column 0.
4106 cx.set_state(indoc! {"
4107 one two
4108 ˇthree
4109 four
4110 "});
4111 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4112 cx.assert_editor_state(indoc! {"
4113 one two
4114 ˇthree
4115 four
4116 "});
4117
4118 cx.set_state(indoc! {"
4119 one two
4120 ˇ three
4121 four
4122 "});
4123 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4124 cx.assert_editor_state(indoc! {"
4125 one two
4126 ˇthree
4127 four
4128 "});
4129}
4130
4131#[gpui::test]
4132async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4133 // This is a regression test for issue #33761
4134 init_test(cx, |_| {});
4135
4136 let mut cx = EditorTestContext::new(cx).await;
4137 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4138 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4139
4140 cx.set_state(
4141 r#"ˇ# ingress:
4142ˇ# api:
4143ˇ# enabled: false
4144ˇ# pathType: Prefix
4145ˇ# console:
4146ˇ# enabled: false
4147ˇ# pathType: Prefix
4148"#,
4149 );
4150
4151 // Press tab to indent all lines
4152 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4153
4154 cx.assert_editor_state(
4155 r#" ˇ# ingress:
4156 ˇ# api:
4157 ˇ# enabled: false
4158 ˇ# pathType: Prefix
4159 ˇ# console:
4160 ˇ# enabled: false
4161 ˇ# pathType: Prefix
4162"#,
4163 );
4164}
4165
4166#[gpui::test]
4167async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4168 // This is a test to make sure our fix for issue #33761 didn't break anything
4169 init_test(cx, |_| {});
4170
4171 let mut cx = EditorTestContext::new(cx).await;
4172 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4173 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4174
4175 cx.set_state(
4176 r#"ˇingress:
4177ˇ api:
4178ˇ enabled: false
4179ˇ pathType: Prefix
4180"#,
4181 );
4182
4183 // Press tab to indent all lines
4184 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4185
4186 cx.assert_editor_state(
4187 r#"ˇingress:
4188 ˇapi:
4189 ˇenabled: false
4190 ˇpathType: Prefix
4191"#,
4192 );
4193}
4194
4195#[gpui::test]
4196async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4197 init_test(cx, |settings| {
4198 settings.defaults.hard_tabs = Some(true);
4199 });
4200
4201 let mut cx = EditorTestContext::new(cx).await;
4202
4203 // select two ranges on one line
4204 cx.set_state(indoc! {"
4205 «oneˇ» «twoˇ»
4206 three
4207 four
4208 "});
4209 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4210 cx.assert_editor_state(indoc! {"
4211 \t«oneˇ» «twoˇ»
4212 three
4213 four
4214 "});
4215 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4216 cx.assert_editor_state(indoc! {"
4217 \t\t«oneˇ» «twoˇ»
4218 three
4219 four
4220 "});
4221 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4222 cx.assert_editor_state(indoc! {"
4223 \t«oneˇ» «twoˇ»
4224 three
4225 four
4226 "});
4227 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4228 cx.assert_editor_state(indoc! {"
4229 «oneˇ» «twoˇ»
4230 three
4231 four
4232 "});
4233
4234 // select across a line ending
4235 cx.set_state(indoc! {"
4236 one two
4237 t«hree
4238 ˇ»four
4239 "});
4240 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4241 cx.assert_editor_state(indoc! {"
4242 one two
4243 \tt«hree
4244 ˇ»four
4245 "});
4246 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4247 cx.assert_editor_state(indoc! {"
4248 one two
4249 \t\tt«hree
4250 ˇ»four
4251 "});
4252 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4253 cx.assert_editor_state(indoc! {"
4254 one two
4255 \tt«hree
4256 ˇ»four
4257 "});
4258 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4259 cx.assert_editor_state(indoc! {"
4260 one two
4261 t«hree
4262 ˇ»four
4263 "});
4264
4265 // Ensure that indenting/outdenting works when the cursor is at column 0.
4266 cx.set_state(indoc! {"
4267 one two
4268 ˇthree
4269 four
4270 "});
4271 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4272 cx.assert_editor_state(indoc! {"
4273 one two
4274 ˇthree
4275 four
4276 "});
4277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4278 cx.assert_editor_state(indoc! {"
4279 one two
4280 \tˇthree
4281 four
4282 "});
4283 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4284 cx.assert_editor_state(indoc! {"
4285 one two
4286 ˇthree
4287 four
4288 "});
4289}
4290
4291#[gpui::test]
4292fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4293 init_test(cx, |settings| {
4294 settings.languages.0.extend([
4295 (
4296 "TOML".into(),
4297 LanguageSettingsContent {
4298 tab_size: NonZeroU32::new(2),
4299 ..Default::default()
4300 },
4301 ),
4302 (
4303 "Rust".into(),
4304 LanguageSettingsContent {
4305 tab_size: NonZeroU32::new(4),
4306 ..Default::default()
4307 },
4308 ),
4309 ]);
4310 });
4311
4312 let toml_language = Arc::new(Language::new(
4313 LanguageConfig {
4314 name: "TOML".into(),
4315 ..Default::default()
4316 },
4317 None,
4318 ));
4319 let rust_language = Arc::new(Language::new(
4320 LanguageConfig {
4321 name: "Rust".into(),
4322 ..Default::default()
4323 },
4324 None,
4325 ));
4326
4327 let toml_buffer =
4328 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4329 let rust_buffer =
4330 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4331 let multibuffer = cx.new(|cx| {
4332 let mut multibuffer = MultiBuffer::new(ReadWrite);
4333 multibuffer.push_excerpts(
4334 toml_buffer.clone(),
4335 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4336 cx,
4337 );
4338 multibuffer.push_excerpts(
4339 rust_buffer.clone(),
4340 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4341 cx,
4342 );
4343 multibuffer
4344 });
4345
4346 cx.add_window(|window, cx| {
4347 let mut editor = build_editor(multibuffer, window, cx);
4348
4349 assert_eq!(
4350 editor.text(cx),
4351 indoc! {"
4352 a = 1
4353 b = 2
4354
4355 const c: usize = 3;
4356 "}
4357 );
4358
4359 select_ranges(
4360 &mut editor,
4361 indoc! {"
4362 «aˇ» = 1
4363 b = 2
4364
4365 «const c:ˇ» usize = 3;
4366 "},
4367 window,
4368 cx,
4369 );
4370
4371 editor.tab(&Tab, window, cx);
4372 assert_text_with_selections(
4373 &mut editor,
4374 indoc! {"
4375 «aˇ» = 1
4376 b = 2
4377
4378 «const c:ˇ» usize = 3;
4379 "},
4380 cx,
4381 );
4382 editor.backtab(&Backtab, window, cx);
4383 assert_text_with_selections(
4384 &mut editor,
4385 indoc! {"
4386 «aˇ» = 1
4387 b = 2
4388
4389 «const c:ˇ» usize = 3;
4390 "},
4391 cx,
4392 );
4393
4394 editor
4395 });
4396}
4397
4398#[gpui::test]
4399async fn test_backspace(cx: &mut TestAppContext) {
4400 init_test(cx, |_| {});
4401
4402 let mut cx = EditorTestContext::new(cx).await;
4403
4404 // Basic backspace
4405 cx.set_state(indoc! {"
4406 onˇe two three
4407 fou«rˇ» five six
4408 seven «ˇeight nine
4409 »ten
4410 "});
4411 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4412 cx.assert_editor_state(indoc! {"
4413 oˇe two three
4414 fouˇ five six
4415 seven ˇten
4416 "});
4417
4418 // Test backspace inside and around indents
4419 cx.set_state(indoc! {"
4420 zero
4421 ˇone
4422 ˇtwo
4423 ˇ ˇ ˇ three
4424 ˇ ˇ four
4425 "});
4426 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4427 cx.assert_editor_state(indoc! {"
4428 zero
4429 ˇone
4430 ˇtwo
4431 ˇ threeˇ four
4432 "});
4433}
4434
4435#[gpui::test]
4436async fn test_delete(cx: &mut TestAppContext) {
4437 init_test(cx, |_| {});
4438
4439 let mut cx = EditorTestContext::new(cx).await;
4440 cx.set_state(indoc! {"
4441 onˇe two three
4442 fou«rˇ» five six
4443 seven «ˇeight nine
4444 »ten
4445 "});
4446 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4447 cx.assert_editor_state(indoc! {"
4448 onˇ two three
4449 fouˇ five six
4450 seven ˇten
4451 "});
4452}
4453
4454#[gpui::test]
4455fn test_delete_line(cx: &mut TestAppContext) {
4456 init_test(cx, |_| {});
4457
4458 let editor = cx.add_window(|window, cx| {
4459 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4460 build_editor(buffer, window, cx)
4461 });
4462 _ = editor.update(cx, |editor, window, cx| {
4463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4464 s.select_display_ranges([
4465 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4466 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4467 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4468 ])
4469 });
4470 editor.delete_line(&DeleteLine, window, cx);
4471 assert_eq!(editor.display_text(cx), "ghi");
4472 assert_eq!(
4473 display_ranges(editor, cx),
4474 vec![
4475 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4476 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4477 ]
4478 );
4479 });
4480
4481 let editor = cx.add_window(|window, cx| {
4482 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4483 build_editor(buffer, window, cx)
4484 });
4485 _ = editor.update(cx, |editor, window, cx| {
4486 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4487 s.select_display_ranges([
4488 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4489 ])
4490 });
4491 editor.delete_line(&DeleteLine, window, cx);
4492 assert_eq!(editor.display_text(cx), "ghi\n");
4493 assert_eq!(
4494 display_ranges(editor, cx),
4495 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4496 );
4497 });
4498
4499 let editor = cx.add_window(|window, cx| {
4500 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4501 build_editor(buffer, window, cx)
4502 });
4503 _ = editor.update(cx, |editor, window, cx| {
4504 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4505 s.select_display_ranges([
4506 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4507 ])
4508 });
4509 editor.delete_line(&DeleteLine, window, cx);
4510 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4511 assert_eq!(
4512 display_ranges(editor, cx),
4513 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4514 );
4515 });
4516}
4517
4518#[gpui::test]
4519fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4520 init_test(cx, |_| {});
4521
4522 cx.add_window(|window, cx| {
4523 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4524 let mut editor = build_editor(buffer.clone(), window, cx);
4525 let buffer = buffer.read(cx).as_singleton().unwrap();
4526
4527 assert_eq!(
4528 editor
4529 .selections
4530 .ranges::<Point>(&editor.display_snapshot(cx)),
4531 &[Point::new(0, 0)..Point::new(0, 0)]
4532 );
4533
4534 // When on single line, replace newline at end by space
4535 editor.join_lines(&JoinLines, window, cx);
4536 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4537 assert_eq!(
4538 editor
4539 .selections
4540 .ranges::<Point>(&editor.display_snapshot(cx)),
4541 &[Point::new(0, 3)..Point::new(0, 3)]
4542 );
4543
4544 // When multiple lines are selected, remove newlines that are spanned by the selection
4545 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4546 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4547 });
4548 editor.join_lines(&JoinLines, window, cx);
4549 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4550 assert_eq!(
4551 editor
4552 .selections
4553 .ranges::<Point>(&editor.display_snapshot(cx)),
4554 &[Point::new(0, 11)..Point::new(0, 11)]
4555 );
4556
4557 // Undo should be transactional
4558 editor.undo(&Undo, window, cx);
4559 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4560 assert_eq!(
4561 editor
4562 .selections
4563 .ranges::<Point>(&editor.display_snapshot(cx)),
4564 &[Point::new(0, 5)..Point::new(2, 2)]
4565 );
4566
4567 // When joining an empty line don't insert a space
4568 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4569 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4570 });
4571 editor.join_lines(&JoinLines, window, cx);
4572 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4573 assert_eq!(
4574 editor
4575 .selections
4576 .ranges::<Point>(&editor.display_snapshot(cx)),
4577 [Point::new(2, 3)..Point::new(2, 3)]
4578 );
4579
4580 // We can remove trailing newlines
4581 editor.join_lines(&JoinLines, window, cx);
4582 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4583 assert_eq!(
4584 editor
4585 .selections
4586 .ranges::<Point>(&editor.display_snapshot(cx)),
4587 [Point::new(2, 3)..Point::new(2, 3)]
4588 );
4589
4590 // We don't blow up on the last line
4591 editor.join_lines(&JoinLines, window, cx);
4592 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4593 assert_eq!(
4594 editor
4595 .selections
4596 .ranges::<Point>(&editor.display_snapshot(cx)),
4597 [Point::new(2, 3)..Point::new(2, 3)]
4598 );
4599
4600 // reset to test indentation
4601 editor.buffer.update(cx, |buffer, cx| {
4602 buffer.edit(
4603 [
4604 (Point::new(1, 0)..Point::new(1, 2), " "),
4605 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4606 ],
4607 None,
4608 cx,
4609 )
4610 });
4611
4612 // We remove any leading spaces
4613 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4614 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4615 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4616 });
4617 editor.join_lines(&JoinLines, window, cx);
4618 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4619
4620 // We don't insert a space for a line containing only spaces
4621 editor.join_lines(&JoinLines, window, cx);
4622 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4623
4624 // We ignore any leading tabs
4625 editor.join_lines(&JoinLines, window, cx);
4626 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4627
4628 editor
4629 });
4630}
4631
4632#[gpui::test]
4633fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4634 init_test(cx, |_| {});
4635
4636 cx.add_window(|window, cx| {
4637 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4638 let mut editor = build_editor(buffer.clone(), window, cx);
4639 let buffer = buffer.read(cx).as_singleton().unwrap();
4640
4641 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4642 s.select_ranges([
4643 Point::new(0, 2)..Point::new(1, 1),
4644 Point::new(1, 2)..Point::new(1, 2),
4645 Point::new(3, 1)..Point::new(3, 2),
4646 ])
4647 });
4648
4649 editor.join_lines(&JoinLines, window, cx);
4650 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4651
4652 assert_eq!(
4653 editor
4654 .selections
4655 .ranges::<Point>(&editor.display_snapshot(cx)),
4656 [
4657 Point::new(0, 7)..Point::new(0, 7),
4658 Point::new(1, 3)..Point::new(1, 3)
4659 ]
4660 );
4661 editor
4662 });
4663}
4664
4665#[gpui::test]
4666async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4667 init_test(cx, |_| {});
4668
4669 let mut cx = EditorTestContext::new(cx).await;
4670
4671 let diff_base = r#"
4672 Line 0
4673 Line 1
4674 Line 2
4675 Line 3
4676 "#
4677 .unindent();
4678
4679 cx.set_state(
4680 &r#"
4681 ˇLine 0
4682 Line 1
4683 Line 2
4684 Line 3
4685 "#
4686 .unindent(),
4687 );
4688
4689 cx.set_head_text(&diff_base);
4690 executor.run_until_parked();
4691
4692 // Join lines
4693 cx.update_editor(|editor, window, cx| {
4694 editor.join_lines(&JoinLines, window, cx);
4695 });
4696 executor.run_until_parked();
4697
4698 cx.assert_editor_state(
4699 &r#"
4700 Line 0ˇ Line 1
4701 Line 2
4702 Line 3
4703 "#
4704 .unindent(),
4705 );
4706 // Join again
4707 cx.update_editor(|editor, window, cx| {
4708 editor.join_lines(&JoinLines, window, cx);
4709 });
4710 executor.run_until_parked();
4711
4712 cx.assert_editor_state(
4713 &r#"
4714 Line 0 Line 1ˇ Line 2
4715 Line 3
4716 "#
4717 .unindent(),
4718 );
4719}
4720
4721#[gpui::test]
4722async fn test_custom_newlines_cause_no_false_positive_diffs(
4723 executor: BackgroundExecutor,
4724 cx: &mut TestAppContext,
4725) {
4726 init_test(cx, |_| {});
4727 let mut cx = EditorTestContext::new(cx).await;
4728 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4729 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4730 executor.run_until_parked();
4731
4732 cx.update_editor(|editor, window, cx| {
4733 let snapshot = editor.snapshot(window, cx);
4734 assert_eq!(
4735 snapshot
4736 .buffer_snapshot()
4737 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4738 .collect::<Vec<_>>(),
4739 Vec::new(),
4740 "Should not have any diffs for files with custom newlines"
4741 );
4742 });
4743}
4744
4745#[gpui::test]
4746async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4747 init_test(cx, |_| {});
4748
4749 let mut cx = EditorTestContext::new(cx).await;
4750
4751 // Test sort_lines_case_insensitive()
4752 cx.set_state(indoc! {"
4753 «z
4754 y
4755 x
4756 Z
4757 Y
4758 Xˇ»
4759 "});
4760 cx.update_editor(|e, window, cx| {
4761 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4762 });
4763 cx.assert_editor_state(indoc! {"
4764 «x
4765 X
4766 y
4767 Y
4768 z
4769 Zˇ»
4770 "});
4771
4772 // Test sort_lines_by_length()
4773 //
4774 // Demonstrates:
4775 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4776 // - sort is stable
4777 cx.set_state(indoc! {"
4778 «123
4779 æ
4780 12
4781 ∞
4782 1
4783 æˇ»
4784 "});
4785 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4786 cx.assert_editor_state(indoc! {"
4787 «æ
4788 ∞
4789 1
4790 æ
4791 12
4792 123ˇ»
4793 "});
4794
4795 // Test reverse_lines()
4796 cx.set_state(indoc! {"
4797 «5
4798 4
4799 3
4800 2
4801 1ˇ»
4802 "});
4803 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4804 cx.assert_editor_state(indoc! {"
4805 «1
4806 2
4807 3
4808 4
4809 5ˇ»
4810 "});
4811
4812 // Skip testing shuffle_line()
4813
4814 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4815 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4816
4817 // Don't manipulate when cursor is on single line, but expand the selection
4818 cx.set_state(indoc! {"
4819 ddˇdd
4820 ccc
4821 bb
4822 a
4823 "});
4824 cx.update_editor(|e, window, cx| {
4825 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4826 });
4827 cx.assert_editor_state(indoc! {"
4828 «ddddˇ»
4829 ccc
4830 bb
4831 a
4832 "});
4833
4834 // Basic manipulate case
4835 // Start selection moves to column 0
4836 // End of selection shrinks to fit shorter line
4837 cx.set_state(indoc! {"
4838 dd«d
4839 ccc
4840 bb
4841 aaaaaˇ»
4842 "});
4843 cx.update_editor(|e, window, cx| {
4844 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4845 });
4846 cx.assert_editor_state(indoc! {"
4847 «aaaaa
4848 bb
4849 ccc
4850 dddˇ»
4851 "});
4852
4853 // Manipulate case with newlines
4854 cx.set_state(indoc! {"
4855 dd«d
4856 ccc
4857
4858 bb
4859 aaaaa
4860
4861 ˇ»
4862 "});
4863 cx.update_editor(|e, window, cx| {
4864 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4865 });
4866 cx.assert_editor_state(indoc! {"
4867 «
4868
4869 aaaaa
4870 bb
4871 ccc
4872 dddˇ»
4873
4874 "});
4875
4876 // Adding new line
4877 cx.set_state(indoc! {"
4878 aa«a
4879 bbˇ»b
4880 "});
4881 cx.update_editor(|e, window, cx| {
4882 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4883 });
4884 cx.assert_editor_state(indoc! {"
4885 «aaa
4886 bbb
4887 added_lineˇ»
4888 "});
4889
4890 // Removing line
4891 cx.set_state(indoc! {"
4892 aa«a
4893 bbbˇ»
4894 "});
4895 cx.update_editor(|e, window, cx| {
4896 e.manipulate_immutable_lines(window, cx, |lines| {
4897 lines.pop();
4898 })
4899 });
4900 cx.assert_editor_state(indoc! {"
4901 «aaaˇ»
4902 "});
4903
4904 // Removing all lines
4905 cx.set_state(indoc! {"
4906 aa«a
4907 bbbˇ»
4908 "});
4909 cx.update_editor(|e, window, cx| {
4910 e.manipulate_immutable_lines(window, cx, |lines| {
4911 lines.drain(..);
4912 })
4913 });
4914 cx.assert_editor_state(indoc! {"
4915 ˇ
4916 "});
4917}
4918
4919#[gpui::test]
4920async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4921 init_test(cx, |_| {});
4922
4923 let mut cx = EditorTestContext::new(cx).await;
4924
4925 // Consider continuous selection as single selection
4926 cx.set_state(indoc! {"
4927 Aaa«aa
4928 cˇ»c«c
4929 bb
4930 aaaˇ»aa
4931 "});
4932 cx.update_editor(|e, window, cx| {
4933 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4934 });
4935 cx.assert_editor_state(indoc! {"
4936 «Aaaaa
4937 ccc
4938 bb
4939 aaaaaˇ»
4940 "});
4941
4942 cx.set_state(indoc! {"
4943 Aaa«aa
4944 cˇ»c«c
4945 bb
4946 aaaˇ»aa
4947 "});
4948 cx.update_editor(|e, window, cx| {
4949 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4950 });
4951 cx.assert_editor_state(indoc! {"
4952 «Aaaaa
4953 ccc
4954 bbˇ»
4955 "});
4956
4957 // Consider non continuous selection as distinct dedup operations
4958 cx.set_state(indoc! {"
4959 «aaaaa
4960 bb
4961 aaaaa
4962 aaaaaˇ»
4963
4964 aaa«aaˇ»
4965 "});
4966 cx.update_editor(|e, window, cx| {
4967 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4968 });
4969 cx.assert_editor_state(indoc! {"
4970 «aaaaa
4971 bbˇ»
4972
4973 «aaaaaˇ»
4974 "});
4975}
4976
4977#[gpui::test]
4978async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4979 init_test(cx, |_| {});
4980
4981 let mut cx = EditorTestContext::new(cx).await;
4982
4983 cx.set_state(indoc! {"
4984 «Aaa
4985 aAa
4986 Aaaˇ»
4987 "});
4988 cx.update_editor(|e, window, cx| {
4989 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4990 });
4991 cx.assert_editor_state(indoc! {"
4992 «Aaa
4993 aAaˇ»
4994 "});
4995
4996 cx.set_state(indoc! {"
4997 «Aaa
4998 aAa
4999 aaAˇ»
5000 "});
5001 cx.update_editor(|e, window, cx| {
5002 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5003 });
5004 cx.assert_editor_state(indoc! {"
5005 «Aaaˇ»
5006 "});
5007}
5008
5009#[gpui::test]
5010async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5011 init_test(cx, |_| {});
5012
5013 let mut cx = EditorTestContext::new(cx).await;
5014
5015 let js_language = Arc::new(Language::new(
5016 LanguageConfig {
5017 name: "JavaScript".into(),
5018 wrap_characters: Some(language::WrapCharactersConfig {
5019 start_prefix: "<".into(),
5020 start_suffix: ">".into(),
5021 end_prefix: "</".into(),
5022 end_suffix: ">".into(),
5023 }),
5024 ..LanguageConfig::default()
5025 },
5026 None,
5027 ));
5028
5029 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5030
5031 cx.set_state(indoc! {"
5032 «testˇ»
5033 "});
5034 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5035 cx.assert_editor_state(indoc! {"
5036 <«ˇ»>test</«ˇ»>
5037 "});
5038
5039 cx.set_state(indoc! {"
5040 «test
5041 testˇ»
5042 "});
5043 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5044 cx.assert_editor_state(indoc! {"
5045 <«ˇ»>test
5046 test</«ˇ»>
5047 "});
5048
5049 cx.set_state(indoc! {"
5050 teˇst
5051 "});
5052 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5053 cx.assert_editor_state(indoc! {"
5054 te<«ˇ»></«ˇ»>st
5055 "});
5056}
5057
5058#[gpui::test]
5059async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5060 init_test(cx, |_| {});
5061
5062 let mut cx = EditorTestContext::new(cx).await;
5063
5064 let js_language = Arc::new(Language::new(
5065 LanguageConfig {
5066 name: "JavaScript".into(),
5067 wrap_characters: Some(language::WrapCharactersConfig {
5068 start_prefix: "<".into(),
5069 start_suffix: ">".into(),
5070 end_prefix: "</".into(),
5071 end_suffix: ">".into(),
5072 }),
5073 ..LanguageConfig::default()
5074 },
5075 None,
5076 ));
5077
5078 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5079
5080 cx.set_state(indoc! {"
5081 «testˇ»
5082 «testˇ» «testˇ»
5083 «testˇ»
5084 "});
5085 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5086 cx.assert_editor_state(indoc! {"
5087 <«ˇ»>test</«ˇ»>
5088 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5089 <«ˇ»>test</«ˇ»>
5090 "});
5091
5092 cx.set_state(indoc! {"
5093 «test
5094 testˇ»
5095 «test
5096 testˇ»
5097 "});
5098 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5099 cx.assert_editor_state(indoc! {"
5100 <«ˇ»>test
5101 test</«ˇ»>
5102 <«ˇ»>test
5103 test</«ˇ»>
5104 "});
5105}
5106
5107#[gpui::test]
5108async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5109 init_test(cx, |_| {});
5110
5111 let mut cx = EditorTestContext::new(cx).await;
5112
5113 let plaintext_language = Arc::new(Language::new(
5114 LanguageConfig {
5115 name: "Plain Text".into(),
5116 ..LanguageConfig::default()
5117 },
5118 None,
5119 ));
5120
5121 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5122
5123 cx.set_state(indoc! {"
5124 «testˇ»
5125 "});
5126 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5127 cx.assert_editor_state(indoc! {"
5128 «testˇ»
5129 "});
5130}
5131
5132#[gpui::test]
5133async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5134 init_test(cx, |_| {});
5135
5136 let mut cx = EditorTestContext::new(cx).await;
5137
5138 // Manipulate with multiple selections on a single line
5139 cx.set_state(indoc! {"
5140 dd«dd
5141 cˇ»c«c
5142 bb
5143 aaaˇ»aa
5144 "});
5145 cx.update_editor(|e, window, cx| {
5146 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5147 });
5148 cx.assert_editor_state(indoc! {"
5149 «aaaaa
5150 bb
5151 ccc
5152 ddddˇ»
5153 "});
5154
5155 // Manipulate with multiple disjoin selections
5156 cx.set_state(indoc! {"
5157 5«
5158 4
5159 3
5160 2
5161 1ˇ»
5162
5163 dd«dd
5164 ccc
5165 bb
5166 aaaˇ»aa
5167 "});
5168 cx.update_editor(|e, window, cx| {
5169 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5170 });
5171 cx.assert_editor_state(indoc! {"
5172 «1
5173 2
5174 3
5175 4
5176 5ˇ»
5177
5178 «aaaaa
5179 bb
5180 ccc
5181 ddddˇ»
5182 "});
5183
5184 // Adding lines on each selection
5185 cx.set_state(indoc! {"
5186 2«
5187 1ˇ»
5188
5189 bb«bb
5190 aaaˇ»aa
5191 "});
5192 cx.update_editor(|e, window, cx| {
5193 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5194 });
5195 cx.assert_editor_state(indoc! {"
5196 «2
5197 1
5198 added lineˇ»
5199
5200 «bbbb
5201 aaaaa
5202 added lineˇ»
5203 "});
5204
5205 // Removing lines on each selection
5206 cx.set_state(indoc! {"
5207 2«
5208 1ˇ»
5209
5210 bb«bb
5211 aaaˇ»aa
5212 "});
5213 cx.update_editor(|e, window, cx| {
5214 e.manipulate_immutable_lines(window, cx, |lines| {
5215 lines.pop();
5216 })
5217 });
5218 cx.assert_editor_state(indoc! {"
5219 «2ˇ»
5220
5221 «bbbbˇ»
5222 "});
5223}
5224
5225#[gpui::test]
5226async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5227 init_test(cx, |settings| {
5228 settings.defaults.tab_size = NonZeroU32::new(3)
5229 });
5230
5231 let mut cx = EditorTestContext::new(cx).await;
5232
5233 // MULTI SELECTION
5234 // Ln.1 "«" tests empty lines
5235 // Ln.9 tests just leading whitespace
5236 cx.set_state(indoc! {"
5237 «
5238 abc // No indentationˇ»
5239 «\tabc // 1 tabˇ»
5240 \t\tabc « ˇ» // 2 tabs
5241 \t ab«c // Tab followed by space
5242 \tabc // Space followed by tab (3 spaces should be the result)
5243 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5244 abˇ»ˇc ˇ ˇ // Already space indented«
5245 \t
5246 \tabc\tdef // Only the leading tab is manipulatedˇ»
5247 "});
5248 cx.update_editor(|e, window, cx| {
5249 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5250 });
5251 cx.assert_editor_state(
5252 indoc! {"
5253 «
5254 abc // No indentation
5255 abc // 1 tab
5256 abc // 2 tabs
5257 abc // Tab followed by space
5258 abc // Space followed by tab (3 spaces should be the result)
5259 abc // Mixed indentation (tab conversion depends on the column)
5260 abc // Already space indented
5261 ·
5262 abc\tdef // Only the leading tab is manipulatedˇ»
5263 "}
5264 .replace("·", "")
5265 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5266 );
5267
5268 // Test on just a few lines, the others should remain unchanged
5269 // Only lines (3, 5, 10, 11) should change
5270 cx.set_state(
5271 indoc! {"
5272 ·
5273 abc // No indentation
5274 \tabcˇ // 1 tab
5275 \t\tabc // 2 tabs
5276 \t abcˇ // Tab followed by space
5277 \tabc // Space followed by tab (3 spaces should be the result)
5278 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5279 abc // Already space indented
5280 «\t
5281 \tabc\tdef // Only the leading tab is manipulatedˇ»
5282 "}
5283 .replace("·", "")
5284 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5285 );
5286 cx.update_editor(|e, window, cx| {
5287 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5288 });
5289 cx.assert_editor_state(
5290 indoc! {"
5291 ·
5292 abc // No indentation
5293 « abc // 1 tabˇ»
5294 \t\tabc // 2 tabs
5295 « abc // Tab followed by spaceˇ»
5296 \tabc // Space followed by tab (3 spaces should be the result)
5297 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5298 abc // Already space indented
5299 « ·
5300 abc\tdef // Only the leading tab is manipulatedˇ»
5301 "}
5302 .replace("·", "")
5303 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5304 );
5305
5306 // SINGLE SELECTION
5307 // Ln.1 "«" tests empty lines
5308 // Ln.9 tests just leading whitespace
5309 cx.set_state(indoc! {"
5310 «
5311 abc // No indentation
5312 \tabc // 1 tab
5313 \t\tabc // 2 tabs
5314 \t abc // Tab followed by space
5315 \tabc // Space followed by tab (3 spaces should be the result)
5316 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5317 abc // Already space indented
5318 \t
5319 \tabc\tdef // Only the leading tab is manipulatedˇ»
5320 "});
5321 cx.update_editor(|e, window, cx| {
5322 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5323 });
5324 cx.assert_editor_state(
5325 indoc! {"
5326 «
5327 abc // No indentation
5328 abc // 1 tab
5329 abc // 2 tabs
5330 abc // Tab followed by space
5331 abc // Space followed by tab (3 spaces should be the result)
5332 abc // Mixed indentation (tab conversion depends on the column)
5333 abc // Already space indented
5334 ·
5335 abc\tdef // Only the leading tab is manipulatedˇ»
5336 "}
5337 .replace("·", "")
5338 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5339 );
5340}
5341
5342#[gpui::test]
5343async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5344 init_test(cx, |settings| {
5345 settings.defaults.tab_size = NonZeroU32::new(3)
5346 });
5347
5348 let mut cx = EditorTestContext::new(cx).await;
5349
5350 // MULTI SELECTION
5351 // Ln.1 "«" tests empty lines
5352 // Ln.11 tests just leading whitespace
5353 cx.set_state(indoc! {"
5354 «
5355 abˇ»ˇc // No indentation
5356 abc ˇ ˇ // 1 space (< 3 so dont convert)
5357 abc « // 2 spaces (< 3 so dont convert)
5358 abc // 3 spaces (convert)
5359 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5360 «\tˇ»\t«\tˇ»abc // Already tab indented
5361 «\t abc // Tab followed by space
5362 \tabc // Space followed by tab (should be consumed due to tab)
5363 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5364 \tˇ» «\t
5365 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5366 "});
5367 cx.update_editor(|e, window, cx| {
5368 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5369 });
5370 cx.assert_editor_state(indoc! {"
5371 «
5372 abc // No indentation
5373 abc // 1 space (< 3 so dont convert)
5374 abc // 2 spaces (< 3 so dont convert)
5375 \tabc // 3 spaces (convert)
5376 \t abc // 5 spaces (1 tab + 2 spaces)
5377 \t\t\tabc // Already tab indented
5378 \t abc // Tab followed by space
5379 \tabc // Space followed by tab (should be consumed due to tab)
5380 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5381 \t\t\t
5382 \tabc \t // Only the leading spaces should be convertedˇ»
5383 "});
5384
5385 // Test on just a few lines, the other should remain unchanged
5386 // Only lines (4, 8, 11, 12) should change
5387 cx.set_state(
5388 indoc! {"
5389 ·
5390 abc // No indentation
5391 abc // 1 space (< 3 so dont convert)
5392 abc // 2 spaces (< 3 so dont convert)
5393 « abc // 3 spaces (convert)ˇ»
5394 abc // 5 spaces (1 tab + 2 spaces)
5395 \t\t\tabc // Already tab indented
5396 \t abc // Tab followed by space
5397 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5398 \t\t \tabc // Mixed indentation
5399 \t \t \t \tabc // Mixed indentation
5400 \t \tˇ
5401 « abc \t // Only the leading spaces should be convertedˇ»
5402 "}
5403 .replace("·", "")
5404 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5405 );
5406 cx.update_editor(|e, window, cx| {
5407 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5408 });
5409 cx.assert_editor_state(
5410 indoc! {"
5411 ·
5412 abc // No indentation
5413 abc // 1 space (< 3 so dont convert)
5414 abc // 2 spaces (< 3 so dont convert)
5415 «\tabc // 3 spaces (convert)ˇ»
5416 abc // 5 spaces (1 tab + 2 spaces)
5417 \t\t\tabc // Already tab indented
5418 \t abc // Tab followed by space
5419 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5420 \t\t \tabc // Mixed indentation
5421 \t \t \t \tabc // Mixed indentation
5422 «\t\t\t
5423 \tabc \t // Only the leading spaces should be convertedˇ»
5424 "}
5425 .replace("·", "")
5426 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5427 );
5428
5429 // SINGLE SELECTION
5430 // Ln.1 "«" tests empty lines
5431 // Ln.11 tests just leading whitespace
5432 cx.set_state(indoc! {"
5433 «
5434 abc // No indentation
5435 abc // 1 space (< 3 so dont convert)
5436 abc // 2 spaces (< 3 so dont convert)
5437 abc // 3 spaces (convert)
5438 abc // 5 spaces (1 tab + 2 spaces)
5439 \t\t\tabc // Already tab indented
5440 \t abc // Tab followed by space
5441 \tabc // Space followed by tab (should be consumed due to tab)
5442 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5443 \t \t
5444 abc \t // Only the leading spaces should be convertedˇ»
5445 "});
5446 cx.update_editor(|e, window, cx| {
5447 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5448 });
5449 cx.assert_editor_state(indoc! {"
5450 «
5451 abc // No indentation
5452 abc // 1 space (< 3 so dont convert)
5453 abc // 2 spaces (< 3 so dont convert)
5454 \tabc // 3 spaces (convert)
5455 \t abc // 5 spaces (1 tab + 2 spaces)
5456 \t\t\tabc // Already tab indented
5457 \t abc // Tab followed by space
5458 \tabc // Space followed by tab (should be consumed due to tab)
5459 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5460 \t\t\t
5461 \tabc \t // Only the leading spaces should be convertedˇ»
5462 "});
5463}
5464
5465#[gpui::test]
5466async fn test_toggle_case(cx: &mut TestAppContext) {
5467 init_test(cx, |_| {});
5468
5469 let mut cx = EditorTestContext::new(cx).await;
5470
5471 // If all lower case -> upper case
5472 cx.set_state(indoc! {"
5473 «hello worldˇ»
5474 "});
5475 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5476 cx.assert_editor_state(indoc! {"
5477 «HELLO WORLDˇ»
5478 "});
5479
5480 // If all upper case -> lower case
5481 cx.set_state(indoc! {"
5482 «HELLO WORLDˇ»
5483 "});
5484 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5485 cx.assert_editor_state(indoc! {"
5486 «hello worldˇ»
5487 "});
5488
5489 // If any upper case characters are identified -> lower case
5490 // This matches JetBrains IDEs
5491 cx.set_state(indoc! {"
5492 «hEllo worldˇ»
5493 "});
5494 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5495 cx.assert_editor_state(indoc! {"
5496 «hello worldˇ»
5497 "});
5498}
5499
5500#[gpui::test]
5501async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5502 init_test(cx, |_| {});
5503
5504 let mut cx = EditorTestContext::new(cx).await;
5505
5506 cx.set_state(indoc! {"
5507 «implement-windows-supportˇ»
5508 "});
5509 cx.update_editor(|e, window, cx| {
5510 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5511 });
5512 cx.assert_editor_state(indoc! {"
5513 «Implement windows supportˇ»
5514 "});
5515}
5516
5517#[gpui::test]
5518async fn test_manipulate_text(cx: &mut TestAppContext) {
5519 init_test(cx, |_| {});
5520
5521 let mut cx = EditorTestContext::new(cx).await;
5522
5523 // Test convert_to_upper_case()
5524 cx.set_state(indoc! {"
5525 «hello worldˇ»
5526 "});
5527 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5528 cx.assert_editor_state(indoc! {"
5529 «HELLO WORLDˇ»
5530 "});
5531
5532 // Test convert_to_lower_case()
5533 cx.set_state(indoc! {"
5534 «HELLO WORLDˇ»
5535 "});
5536 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5537 cx.assert_editor_state(indoc! {"
5538 «hello worldˇ»
5539 "});
5540
5541 // Test multiple line, single selection case
5542 cx.set_state(indoc! {"
5543 «The quick brown
5544 fox jumps over
5545 the lazy dogˇ»
5546 "});
5547 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5548 cx.assert_editor_state(indoc! {"
5549 «The Quick Brown
5550 Fox Jumps Over
5551 The Lazy Dogˇ»
5552 "});
5553
5554 // Test multiple line, single selection case
5555 cx.set_state(indoc! {"
5556 «The quick brown
5557 fox jumps over
5558 the lazy dogˇ»
5559 "});
5560 cx.update_editor(|e, window, cx| {
5561 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5562 });
5563 cx.assert_editor_state(indoc! {"
5564 «TheQuickBrown
5565 FoxJumpsOver
5566 TheLazyDogˇ»
5567 "});
5568
5569 // From here on out, test more complex cases of manipulate_text()
5570
5571 // Test no selection case - should affect words cursors are in
5572 // Cursor at beginning, middle, and end of word
5573 cx.set_state(indoc! {"
5574 ˇhello big beauˇtiful worldˇ
5575 "});
5576 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5577 cx.assert_editor_state(indoc! {"
5578 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5579 "});
5580
5581 // Test multiple selections on a single line and across multiple lines
5582 cx.set_state(indoc! {"
5583 «Theˇ» quick «brown
5584 foxˇ» jumps «overˇ»
5585 the «lazyˇ» dog
5586 "});
5587 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5588 cx.assert_editor_state(indoc! {"
5589 «THEˇ» quick «BROWN
5590 FOXˇ» jumps «OVERˇ»
5591 the «LAZYˇ» dog
5592 "});
5593
5594 // Test case where text length grows
5595 cx.set_state(indoc! {"
5596 «tschüߡ»
5597 "});
5598 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5599 cx.assert_editor_state(indoc! {"
5600 «TSCHÜSSˇ»
5601 "});
5602
5603 // Test to make sure we don't crash when text shrinks
5604 cx.set_state(indoc! {"
5605 aaa_bbbˇ
5606 "});
5607 cx.update_editor(|e, window, cx| {
5608 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5609 });
5610 cx.assert_editor_state(indoc! {"
5611 «aaaBbbˇ»
5612 "});
5613
5614 // Test to make sure we all aware of the fact that each word can grow and shrink
5615 // Final selections should be aware of this fact
5616 cx.set_state(indoc! {"
5617 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5618 "});
5619 cx.update_editor(|e, window, cx| {
5620 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5621 });
5622 cx.assert_editor_state(indoc! {"
5623 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5624 "});
5625
5626 cx.set_state(indoc! {"
5627 «hElLo, WoRld!ˇ»
5628 "});
5629 cx.update_editor(|e, window, cx| {
5630 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5631 });
5632 cx.assert_editor_state(indoc! {"
5633 «HeLlO, wOrLD!ˇ»
5634 "});
5635
5636 // Test selections with `line_mode() = true`.
5637 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5638 cx.set_state(indoc! {"
5639 «The quick brown
5640 fox jumps over
5641 tˇ»he lazy dog
5642 "});
5643 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5644 cx.assert_editor_state(indoc! {"
5645 «THE QUICK BROWN
5646 FOX JUMPS OVER
5647 THE LAZY DOGˇ»
5648 "});
5649}
5650
5651#[gpui::test]
5652fn test_duplicate_line(cx: &mut TestAppContext) {
5653 init_test(cx, |_| {});
5654
5655 let editor = cx.add_window(|window, cx| {
5656 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5657 build_editor(buffer, window, cx)
5658 });
5659 _ = editor.update(cx, |editor, window, cx| {
5660 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5661 s.select_display_ranges([
5662 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5663 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5664 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5665 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5666 ])
5667 });
5668 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5669 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5670 assert_eq!(
5671 display_ranges(editor, cx),
5672 vec![
5673 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5674 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5675 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5676 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5677 ]
5678 );
5679 });
5680
5681 let editor = cx.add_window(|window, cx| {
5682 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5683 build_editor(buffer, window, cx)
5684 });
5685 _ = editor.update(cx, |editor, window, cx| {
5686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5687 s.select_display_ranges([
5688 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5689 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5690 ])
5691 });
5692 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5693 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5694 assert_eq!(
5695 display_ranges(editor, cx),
5696 vec![
5697 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5698 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5699 ]
5700 );
5701 });
5702
5703 // With `duplicate_line_up` the selections move to the duplicated lines,
5704 // which are inserted above the original lines
5705 let editor = cx.add_window(|window, cx| {
5706 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5707 build_editor(buffer, window, cx)
5708 });
5709 _ = editor.update(cx, |editor, window, cx| {
5710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5711 s.select_display_ranges([
5712 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5713 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5714 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5715 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5716 ])
5717 });
5718 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5719 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5720 assert_eq!(
5721 display_ranges(editor, cx),
5722 vec![
5723 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5724 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5725 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5726 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5727 ]
5728 );
5729 });
5730
5731 let editor = cx.add_window(|window, cx| {
5732 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5733 build_editor(buffer, window, cx)
5734 });
5735 _ = editor.update(cx, |editor, window, cx| {
5736 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5737 s.select_display_ranges([
5738 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5739 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5740 ])
5741 });
5742 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5743 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5744 assert_eq!(
5745 display_ranges(editor, cx),
5746 vec![
5747 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5748 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5749 ]
5750 );
5751 });
5752
5753 let editor = cx.add_window(|window, cx| {
5754 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5755 build_editor(buffer, window, cx)
5756 });
5757 _ = editor.update(cx, |editor, window, cx| {
5758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5759 s.select_display_ranges([
5760 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5761 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5762 ])
5763 });
5764 editor.duplicate_selection(&DuplicateSelection, window, cx);
5765 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5766 assert_eq!(
5767 display_ranges(editor, cx),
5768 vec![
5769 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5770 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5771 ]
5772 );
5773 });
5774}
5775
5776#[gpui::test]
5777async fn test_rotate_selections(cx: &mut TestAppContext) {
5778 init_test(cx, |_| {});
5779
5780 let mut cx = EditorTestContext::new(cx).await;
5781
5782 // Rotate text selections (horizontal)
5783 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5784 cx.update_editor(|e, window, cx| {
5785 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5786 });
5787 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
5788 cx.update_editor(|e, window, cx| {
5789 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5790 });
5791 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5792
5793 // Rotate text selections (vertical)
5794 cx.set_state(indoc! {"
5795 x=«1ˇ»
5796 y=«2ˇ»
5797 z=«3ˇ»
5798 "});
5799 cx.update_editor(|e, window, cx| {
5800 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5801 });
5802 cx.assert_editor_state(indoc! {"
5803 x=«3ˇ»
5804 y=«1ˇ»
5805 z=«2ˇ»
5806 "});
5807 cx.update_editor(|e, window, cx| {
5808 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5809 });
5810 cx.assert_editor_state(indoc! {"
5811 x=«1ˇ»
5812 y=«2ˇ»
5813 z=«3ˇ»
5814 "});
5815
5816 // Rotate text selections (vertical, different lengths)
5817 cx.set_state(indoc! {"
5818 x=\"«ˇ»\"
5819 y=\"«aˇ»\"
5820 z=\"«aaˇ»\"
5821 "});
5822 cx.update_editor(|e, window, cx| {
5823 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5824 });
5825 cx.assert_editor_state(indoc! {"
5826 x=\"«aaˇ»\"
5827 y=\"«ˇ»\"
5828 z=\"«aˇ»\"
5829 "});
5830 cx.update_editor(|e, window, cx| {
5831 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5832 });
5833 cx.assert_editor_state(indoc! {"
5834 x=\"«ˇ»\"
5835 y=\"«aˇ»\"
5836 z=\"«aaˇ»\"
5837 "});
5838
5839 // Rotate whole lines (cursor positions preserved)
5840 cx.set_state(indoc! {"
5841 ˇline123
5842 liˇne23
5843 line3ˇ
5844 "});
5845 cx.update_editor(|e, window, cx| {
5846 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5847 });
5848 cx.assert_editor_state(indoc! {"
5849 line3ˇ
5850 ˇline123
5851 liˇne23
5852 "});
5853 cx.update_editor(|e, window, cx| {
5854 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5855 });
5856 cx.assert_editor_state(indoc! {"
5857 ˇline123
5858 liˇne23
5859 line3ˇ
5860 "});
5861
5862 // Rotate whole lines, multiple cursors per line (positions preserved)
5863 cx.set_state(indoc! {"
5864 ˇliˇne123
5865 ˇline23
5866 ˇline3
5867 "});
5868 cx.update_editor(|e, window, cx| {
5869 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5870 });
5871 cx.assert_editor_state(indoc! {"
5872 ˇline3
5873 ˇliˇne123
5874 ˇline23
5875 "});
5876 cx.update_editor(|e, window, cx| {
5877 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5878 });
5879 cx.assert_editor_state(indoc! {"
5880 ˇliˇne123
5881 ˇline23
5882 ˇline3
5883 "});
5884}
5885
5886#[gpui::test]
5887fn test_move_line_up_down(cx: &mut TestAppContext) {
5888 init_test(cx, |_| {});
5889
5890 let editor = cx.add_window(|window, cx| {
5891 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5892 build_editor(buffer, window, cx)
5893 });
5894 _ = editor.update(cx, |editor, window, cx| {
5895 editor.fold_creases(
5896 vec![
5897 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5898 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5899 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5900 ],
5901 true,
5902 window,
5903 cx,
5904 );
5905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5906 s.select_display_ranges([
5907 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5908 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5909 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5910 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5911 ])
5912 });
5913 assert_eq!(
5914 editor.display_text(cx),
5915 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5916 );
5917
5918 editor.move_line_up(&MoveLineUp, window, cx);
5919 assert_eq!(
5920 editor.display_text(cx),
5921 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5922 );
5923 assert_eq!(
5924 display_ranges(editor, cx),
5925 vec![
5926 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5927 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5928 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5929 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5930 ]
5931 );
5932 });
5933
5934 _ = editor.update(cx, |editor, window, cx| {
5935 editor.move_line_down(&MoveLineDown, window, cx);
5936 assert_eq!(
5937 editor.display_text(cx),
5938 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5939 );
5940 assert_eq!(
5941 display_ranges(editor, cx),
5942 vec![
5943 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5944 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5945 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5946 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5947 ]
5948 );
5949 });
5950
5951 _ = editor.update(cx, |editor, window, cx| {
5952 editor.move_line_down(&MoveLineDown, window, cx);
5953 assert_eq!(
5954 editor.display_text(cx),
5955 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5956 );
5957 assert_eq!(
5958 display_ranges(editor, cx),
5959 vec![
5960 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5961 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5962 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5963 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5964 ]
5965 );
5966 });
5967
5968 _ = editor.update(cx, |editor, window, cx| {
5969 editor.move_line_up(&MoveLineUp, window, cx);
5970 assert_eq!(
5971 editor.display_text(cx),
5972 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5973 );
5974 assert_eq!(
5975 display_ranges(editor, cx),
5976 vec![
5977 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5978 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5979 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5980 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5981 ]
5982 );
5983 });
5984}
5985
5986#[gpui::test]
5987fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5988 init_test(cx, |_| {});
5989 let editor = cx.add_window(|window, cx| {
5990 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5991 build_editor(buffer, window, cx)
5992 });
5993 _ = editor.update(cx, |editor, window, cx| {
5994 editor.fold_creases(
5995 vec![Crease::simple(
5996 Point::new(6, 4)..Point::new(7, 4),
5997 FoldPlaceholder::test(),
5998 )],
5999 true,
6000 window,
6001 cx,
6002 );
6003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6004 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6005 });
6006 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6007 editor.move_line_up(&MoveLineUp, window, cx);
6008 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6009 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6010 });
6011}
6012
6013#[gpui::test]
6014fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6015 init_test(cx, |_| {});
6016
6017 let editor = cx.add_window(|window, cx| {
6018 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6019 build_editor(buffer, window, cx)
6020 });
6021 _ = editor.update(cx, |editor, window, cx| {
6022 let snapshot = editor.buffer.read(cx).snapshot(cx);
6023 editor.insert_blocks(
6024 [BlockProperties {
6025 style: BlockStyle::Fixed,
6026 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6027 height: Some(1),
6028 render: Arc::new(|_| div().into_any()),
6029 priority: 0,
6030 }],
6031 Some(Autoscroll::fit()),
6032 cx,
6033 );
6034 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6035 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6036 });
6037 editor.move_line_down(&MoveLineDown, window, cx);
6038 });
6039}
6040
6041#[gpui::test]
6042async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6043 init_test(cx, |_| {});
6044
6045 let mut cx = EditorTestContext::new(cx).await;
6046 cx.set_state(
6047 &"
6048 ˇzero
6049 one
6050 two
6051 three
6052 four
6053 five
6054 "
6055 .unindent(),
6056 );
6057
6058 // Create a four-line block that replaces three lines of text.
6059 cx.update_editor(|editor, window, cx| {
6060 let snapshot = editor.snapshot(window, cx);
6061 let snapshot = &snapshot.buffer_snapshot();
6062 let placement = BlockPlacement::Replace(
6063 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6064 );
6065 editor.insert_blocks(
6066 [BlockProperties {
6067 placement,
6068 height: Some(4),
6069 style: BlockStyle::Sticky,
6070 render: Arc::new(|_| gpui::div().into_any_element()),
6071 priority: 0,
6072 }],
6073 None,
6074 cx,
6075 );
6076 });
6077
6078 // Move down so that the cursor touches the block.
6079 cx.update_editor(|editor, window, cx| {
6080 editor.move_down(&Default::default(), window, cx);
6081 });
6082 cx.assert_editor_state(
6083 &"
6084 zero
6085 «one
6086 two
6087 threeˇ»
6088 four
6089 five
6090 "
6091 .unindent(),
6092 );
6093
6094 // Move down past the block.
6095 cx.update_editor(|editor, window, cx| {
6096 editor.move_down(&Default::default(), window, cx);
6097 });
6098 cx.assert_editor_state(
6099 &"
6100 zero
6101 one
6102 two
6103 three
6104 ˇfour
6105 five
6106 "
6107 .unindent(),
6108 );
6109}
6110
6111#[gpui::test]
6112fn test_transpose(cx: &mut TestAppContext) {
6113 init_test(cx, |_| {});
6114
6115 _ = cx.add_window(|window, cx| {
6116 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6117 editor.set_style(EditorStyle::default(), window, cx);
6118 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6119 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6120 });
6121 editor.transpose(&Default::default(), window, cx);
6122 assert_eq!(editor.text(cx), "bac");
6123 assert_eq!(
6124 editor.selections.ranges(&editor.display_snapshot(cx)),
6125 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6126 );
6127
6128 editor.transpose(&Default::default(), window, cx);
6129 assert_eq!(editor.text(cx), "bca");
6130 assert_eq!(
6131 editor.selections.ranges(&editor.display_snapshot(cx)),
6132 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6133 );
6134
6135 editor.transpose(&Default::default(), window, cx);
6136 assert_eq!(editor.text(cx), "bac");
6137 assert_eq!(
6138 editor.selections.ranges(&editor.display_snapshot(cx)),
6139 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6140 );
6141
6142 editor
6143 });
6144
6145 _ = cx.add_window(|window, cx| {
6146 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", 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(3)..MultiBufferOffset(3)])
6150 });
6151 editor.transpose(&Default::default(), window, cx);
6152 assert_eq!(editor.text(cx), "acb\nde");
6153 assert_eq!(
6154 editor.selections.ranges(&editor.display_snapshot(cx)),
6155 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6156 );
6157
6158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6159 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6160 });
6161 editor.transpose(&Default::default(), window, cx);
6162 assert_eq!(editor.text(cx), "acbd\ne");
6163 assert_eq!(
6164 editor.selections.ranges(&editor.display_snapshot(cx)),
6165 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6166 );
6167
6168 editor.transpose(&Default::default(), window, cx);
6169 assert_eq!(editor.text(cx), "acbde\n");
6170 assert_eq!(
6171 editor.selections.ranges(&editor.display_snapshot(cx)),
6172 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6173 );
6174
6175 editor.transpose(&Default::default(), window, cx);
6176 assert_eq!(editor.text(cx), "acbd\ne");
6177 assert_eq!(
6178 editor.selections.ranges(&editor.display_snapshot(cx)),
6179 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6180 );
6181
6182 editor
6183 });
6184
6185 _ = cx.add_window(|window, cx| {
6186 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6187 editor.set_style(EditorStyle::default(), window, cx);
6188 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6189 s.select_ranges([
6190 MultiBufferOffset(1)..MultiBufferOffset(1),
6191 MultiBufferOffset(2)..MultiBufferOffset(2),
6192 MultiBufferOffset(4)..MultiBufferOffset(4),
6193 ])
6194 });
6195 editor.transpose(&Default::default(), window, cx);
6196 assert_eq!(editor.text(cx), "bacd\ne");
6197 assert_eq!(
6198 editor.selections.ranges(&editor.display_snapshot(cx)),
6199 [
6200 MultiBufferOffset(2)..MultiBufferOffset(2),
6201 MultiBufferOffset(3)..MultiBufferOffset(3),
6202 MultiBufferOffset(5)..MultiBufferOffset(5)
6203 ]
6204 );
6205
6206 editor.transpose(&Default::default(), window, cx);
6207 assert_eq!(editor.text(cx), "bcade\n");
6208 assert_eq!(
6209 editor.selections.ranges(&editor.display_snapshot(cx)),
6210 [
6211 MultiBufferOffset(3)..MultiBufferOffset(3),
6212 MultiBufferOffset(4)..MultiBufferOffset(4),
6213 MultiBufferOffset(6)..MultiBufferOffset(6)
6214 ]
6215 );
6216
6217 editor.transpose(&Default::default(), window, cx);
6218 assert_eq!(editor.text(cx), "bcda\ne");
6219 assert_eq!(
6220 editor.selections.ranges(&editor.display_snapshot(cx)),
6221 [
6222 MultiBufferOffset(4)..MultiBufferOffset(4),
6223 MultiBufferOffset(6)..MultiBufferOffset(6)
6224 ]
6225 );
6226
6227 editor.transpose(&Default::default(), window, cx);
6228 assert_eq!(editor.text(cx), "bcade\n");
6229 assert_eq!(
6230 editor.selections.ranges(&editor.display_snapshot(cx)),
6231 [
6232 MultiBufferOffset(4)..MultiBufferOffset(4),
6233 MultiBufferOffset(6)..MultiBufferOffset(6)
6234 ]
6235 );
6236
6237 editor.transpose(&Default::default(), window, cx);
6238 assert_eq!(editor.text(cx), "bcaed\n");
6239 assert_eq!(
6240 editor.selections.ranges(&editor.display_snapshot(cx)),
6241 [
6242 MultiBufferOffset(5)..MultiBufferOffset(5),
6243 MultiBufferOffset(6)..MultiBufferOffset(6)
6244 ]
6245 );
6246
6247 editor
6248 });
6249
6250 _ = cx.add_window(|window, cx| {
6251 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6252 editor.set_style(EditorStyle::default(), window, cx);
6253 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6254 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6255 });
6256 editor.transpose(&Default::default(), window, cx);
6257 assert_eq!(editor.text(cx), "🏀🍐✋");
6258 assert_eq!(
6259 editor.selections.ranges(&editor.display_snapshot(cx)),
6260 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6261 );
6262
6263 editor.transpose(&Default::default(), window, cx);
6264 assert_eq!(editor.text(cx), "🏀✋🍐");
6265 assert_eq!(
6266 editor.selections.ranges(&editor.display_snapshot(cx)),
6267 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6268 );
6269
6270 editor.transpose(&Default::default(), window, cx);
6271 assert_eq!(editor.text(cx), "🏀🍐✋");
6272 assert_eq!(
6273 editor.selections.ranges(&editor.display_snapshot(cx)),
6274 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6275 );
6276
6277 editor
6278 });
6279}
6280
6281#[gpui::test]
6282async fn test_rewrap(cx: &mut TestAppContext) {
6283 init_test(cx, |settings| {
6284 settings.languages.0.extend([
6285 (
6286 "Markdown".into(),
6287 LanguageSettingsContent {
6288 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6289 preferred_line_length: Some(40),
6290 ..Default::default()
6291 },
6292 ),
6293 (
6294 "Plain Text".into(),
6295 LanguageSettingsContent {
6296 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6297 preferred_line_length: Some(40),
6298 ..Default::default()
6299 },
6300 ),
6301 (
6302 "C++".into(),
6303 LanguageSettingsContent {
6304 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6305 preferred_line_length: Some(40),
6306 ..Default::default()
6307 },
6308 ),
6309 (
6310 "Python".into(),
6311 LanguageSettingsContent {
6312 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6313 preferred_line_length: Some(40),
6314 ..Default::default()
6315 },
6316 ),
6317 (
6318 "Rust".into(),
6319 LanguageSettingsContent {
6320 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6321 preferred_line_length: Some(40),
6322 ..Default::default()
6323 },
6324 ),
6325 ])
6326 });
6327
6328 let mut cx = EditorTestContext::new(cx).await;
6329
6330 let cpp_language = Arc::new(Language::new(
6331 LanguageConfig {
6332 name: "C++".into(),
6333 line_comments: vec!["// ".into()],
6334 ..LanguageConfig::default()
6335 },
6336 None,
6337 ));
6338 let python_language = Arc::new(Language::new(
6339 LanguageConfig {
6340 name: "Python".into(),
6341 line_comments: vec!["# ".into()],
6342 ..LanguageConfig::default()
6343 },
6344 None,
6345 ));
6346 let markdown_language = Arc::new(Language::new(
6347 LanguageConfig {
6348 name: "Markdown".into(),
6349 rewrap_prefixes: vec![
6350 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6351 regex::Regex::new("[-*+]\\s+").unwrap(),
6352 ],
6353 ..LanguageConfig::default()
6354 },
6355 None,
6356 ));
6357 let rust_language = Arc::new(
6358 Language::new(
6359 LanguageConfig {
6360 name: "Rust".into(),
6361 line_comments: vec!["// ".into(), "/// ".into()],
6362 ..LanguageConfig::default()
6363 },
6364 Some(tree_sitter_rust::LANGUAGE.into()),
6365 )
6366 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6367 .unwrap(),
6368 );
6369
6370 let plaintext_language = Arc::new(Language::new(
6371 LanguageConfig {
6372 name: "Plain Text".into(),
6373 ..LanguageConfig::default()
6374 },
6375 None,
6376 ));
6377
6378 // Test basic rewrapping of a long line with a cursor
6379 assert_rewrap(
6380 indoc! {"
6381 // ˇThis is a long comment that needs to be wrapped.
6382 "},
6383 indoc! {"
6384 // ˇThis is a long comment that needs to
6385 // be wrapped.
6386 "},
6387 cpp_language.clone(),
6388 &mut cx,
6389 );
6390
6391 // Test rewrapping a full selection
6392 assert_rewrap(
6393 indoc! {"
6394 «// This selected long comment needs to be wrapped.ˇ»"
6395 },
6396 indoc! {"
6397 «// This selected long comment needs to
6398 // be wrapped.ˇ»"
6399 },
6400 cpp_language.clone(),
6401 &mut cx,
6402 );
6403
6404 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6405 assert_rewrap(
6406 indoc! {"
6407 // ˇThis is the first line.
6408 // Thisˇ is the second line.
6409 // This is the thirdˇ line, all part of one paragraph.
6410 "},
6411 indoc! {"
6412 // ˇThis is the first line. Thisˇ is the
6413 // second line. This is the thirdˇ line,
6414 // all part of one paragraph.
6415 "},
6416 cpp_language.clone(),
6417 &mut cx,
6418 );
6419
6420 // Test multiple cursors in different paragraphs trigger separate rewraps
6421 assert_rewrap(
6422 indoc! {"
6423 // ˇThis is the first paragraph, first line.
6424 // ˇThis is the first paragraph, second line.
6425
6426 // ˇThis is the second paragraph, first line.
6427 // ˇThis is the second paragraph, second line.
6428 "},
6429 indoc! {"
6430 // ˇThis is the first paragraph, first
6431 // line. ˇThis is the first paragraph,
6432 // second line.
6433
6434 // ˇThis is the second paragraph, first
6435 // line. ˇThis is the second paragraph,
6436 // second line.
6437 "},
6438 cpp_language.clone(),
6439 &mut cx,
6440 );
6441
6442 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6443 assert_rewrap(
6444 indoc! {"
6445 «// A regular long long comment to be wrapped.
6446 /// A documentation long comment to be wrapped.ˇ»
6447 "},
6448 indoc! {"
6449 «// A regular long long comment to be
6450 // wrapped.
6451 /// A documentation long comment to be
6452 /// wrapped.ˇ»
6453 "},
6454 rust_language.clone(),
6455 &mut cx,
6456 );
6457
6458 // Test that change in indentation level trigger seperate rewraps
6459 assert_rewrap(
6460 indoc! {"
6461 fn foo() {
6462 «// This is a long comment at the base indent.
6463 // This is a long comment at the next indent.ˇ»
6464 }
6465 "},
6466 indoc! {"
6467 fn foo() {
6468 «// This is a long comment at the
6469 // base indent.
6470 // This is a long comment at the
6471 // next indent.ˇ»
6472 }
6473 "},
6474 rust_language.clone(),
6475 &mut cx,
6476 );
6477
6478 // Test that different comment prefix characters (e.g., '#') are handled correctly
6479 assert_rewrap(
6480 indoc! {"
6481 # ˇThis is a long comment using a pound sign.
6482 "},
6483 indoc! {"
6484 # ˇThis is a long comment using a pound
6485 # sign.
6486 "},
6487 python_language,
6488 &mut cx,
6489 );
6490
6491 // Test rewrapping only affects comments, not code even when selected
6492 assert_rewrap(
6493 indoc! {"
6494 «/// This doc comment is long and should be wrapped.
6495 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6496 "},
6497 indoc! {"
6498 «/// This doc comment is long and should
6499 /// be wrapped.
6500 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6501 "},
6502 rust_language.clone(),
6503 &mut cx,
6504 );
6505
6506 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6507 assert_rewrap(
6508 indoc! {"
6509 # Header
6510
6511 A long long long line of markdown text to wrap.ˇ
6512 "},
6513 indoc! {"
6514 # Header
6515
6516 A long long long line of markdown text
6517 to wrap.ˇ
6518 "},
6519 markdown_language.clone(),
6520 &mut cx,
6521 );
6522
6523 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6524 assert_rewrap(
6525 indoc! {"
6526 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6527 2. This is a numbered list item that is very long and needs to be wrapped properly.
6528 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6529 "},
6530 indoc! {"
6531 «1. This is a numbered list item that is
6532 very long and needs to be wrapped
6533 properly.
6534 2. This is a numbered list item that is
6535 very long and needs to be wrapped
6536 properly.
6537 - This is an unordered list item that is
6538 also very long and should not merge
6539 with the numbered item.ˇ»
6540 "},
6541 markdown_language.clone(),
6542 &mut cx,
6543 );
6544
6545 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6546 assert_rewrap(
6547 indoc! {"
6548 «1. This is a numbered list item that is
6549 very long and needs to be wrapped
6550 properly.
6551 2. This is a numbered list item that is
6552 very long and needs to be wrapped
6553 properly.
6554 - This is an unordered list item that is
6555 also very long and should not merge with
6556 the numbered item.ˇ»
6557 "},
6558 indoc! {"
6559 «1. This is a numbered list item that is
6560 very long and needs to be wrapped
6561 properly.
6562 2. This is a numbered list item that is
6563 very long and needs to be wrapped
6564 properly.
6565 - This is an unordered list item that is
6566 also very long and should not merge
6567 with the numbered item.ˇ»
6568 "},
6569 markdown_language.clone(),
6570 &mut cx,
6571 );
6572
6573 // Test that rewrapping maintain indents even when they already exists.
6574 assert_rewrap(
6575 indoc! {"
6576 «1. This is a numbered list
6577 item that is very long and needs to be wrapped properly.
6578 2. This is a numbered list
6579 item that is very long and needs to be wrapped properly.
6580 - This is an unordered list item that is also very long and
6581 should not merge with the numbered item.ˇ»
6582 "},
6583 indoc! {"
6584 «1. This is a numbered list item that is
6585 very long and needs to be wrapped
6586 properly.
6587 2. This is a numbered list item that is
6588 very long and needs to be wrapped
6589 properly.
6590 - This is an unordered list item that is
6591 also very long and should not merge
6592 with the numbered item.ˇ»
6593 "},
6594 markdown_language,
6595 &mut cx,
6596 );
6597
6598 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6599 assert_rewrap(
6600 indoc! {"
6601 ˇThis is a very long line of plain text that will be wrapped.
6602 "},
6603 indoc! {"
6604 ˇThis is a very long line of plain text
6605 that will be wrapped.
6606 "},
6607 plaintext_language.clone(),
6608 &mut cx,
6609 );
6610
6611 // Test that non-commented code acts as a paragraph boundary within a selection
6612 assert_rewrap(
6613 indoc! {"
6614 «// This is the first long comment block to be wrapped.
6615 fn my_func(a: u32);
6616 // This is the second long comment block to be wrapped.ˇ»
6617 "},
6618 indoc! {"
6619 «// This is the first long comment block
6620 // to be wrapped.
6621 fn my_func(a: u32);
6622 // This is the second long comment block
6623 // to be wrapped.ˇ»
6624 "},
6625 rust_language,
6626 &mut cx,
6627 );
6628
6629 // Test rewrapping multiple selections, including ones with blank lines or tabs
6630 assert_rewrap(
6631 indoc! {"
6632 «ˇThis is a very long line that will be wrapped.
6633
6634 This is another paragraph in the same selection.»
6635
6636 «\tThis is a very long indented line that will be wrapped.ˇ»
6637 "},
6638 indoc! {"
6639 «ˇThis is a very long line that will be
6640 wrapped.
6641
6642 This is another paragraph in the same
6643 selection.»
6644
6645 «\tThis is a very long indented line
6646 \tthat will be wrapped.ˇ»
6647 "},
6648 plaintext_language,
6649 &mut cx,
6650 );
6651
6652 // Test that an empty comment line acts as a paragraph boundary
6653 assert_rewrap(
6654 indoc! {"
6655 // ˇThis is a long comment that will be wrapped.
6656 //
6657 // And this is another long comment that will also be wrapped.ˇ
6658 "},
6659 indoc! {"
6660 // ˇThis is a long comment that will be
6661 // wrapped.
6662 //
6663 // And this is another long comment that
6664 // will also be wrapped.ˇ
6665 "},
6666 cpp_language,
6667 &mut cx,
6668 );
6669
6670 #[track_caller]
6671 fn assert_rewrap(
6672 unwrapped_text: &str,
6673 wrapped_text: &str,
6674 language: Arc<Language>,
6675 cx: &mut EditorTestContext,
6676 ) {
6677 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6678 cx.set_state(unwrapped_text);
6679 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6680 cx.assert_editor_state(wrapped_text);
6681 }
6682}
6683
6684#[gpui::test]
6685async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6686 init_test(cx, |settings| {
6687 settings.languages.0.extend([(
6688 "Rust".into(),
6689 LanguageSettingsContent {
6690 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6691 preferred_line_length: Some(40),
6692 ..Default::default()
6693 },
6694 )])
6695 });
6696
6697 let mut cx = EditorTestContext::new(cx).await;
6698
6699 let rust_lang = Arc::new(
6700 Language::new(
6701 LanguageConfig {
6702 name: "Rust".into(),
6703 line_comments: vec!["// ".into()],
6704 block_comment: Some(BlockCommentConfig {
6705 start: "/*".into(),
6706 end: "*/".into(),
6707 prefix: "* ".into(),
6708 tab_size: 1,
6709 }),
6710 documentation_comment: Some(BlockCommentConfig {
6711 start: "/**".into(),
6712 end: "*/".into(),
6713 prefix: "* ".into(),
6714 tab_size: 1,
6715 }),
6716
6717 ..LanguageConfig::default()
6718 },
6719 Some(tree_sitter_rust::LANGUAGE.into()),
6720 )
6721 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6722 .unwrap(),
6723 );
6724
6725 // regular block comment
6726 assert_rewrap(
6727 indoc! {"
6728 /*
6729 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6730 */
6731 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6732 "},
6733 indoc! {"
6734 /*
6735 *ˇ Lorem ipsum dolor sit amet,
6736 * consectetur adipiscing elit.
6737 */
6738 /*
6739 *ˇ Lorem ipsum dolor sit amet,
6740 * consectetur adipiscing elit.
6741 */
6742 "},
6743 rust_lang.clone(),
6744 &mut cx,
6745 );
6746
6747 // indent is respected
6748 assert_rewrap(
6749 indoc! {"
6750 {}
6751 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6752 "},
6753 indoc! {"
6754 {}
6755 /*
6756 *ˇ Lorem ipsum dolor sit amet,
6757 * consectetur adipiscing elit.
6758 */
6759 "},
6760 rust_lang.clone(),
6761 &mut cx,
6762 );
6763
6764 // short block comments with inline delimiters
6765 assert_rewrap(
6766 indoc! {"
6767 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6768 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6769 */
6770 /*
6771 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6772 "},
6773 indoc! {"
6774 /*
6775 *ˇ Lorem ipsum dolor sit amet,
6776 * consectetur adipiscing elit.
6777 */
6778 /*
6779 *ˇ Lorem ipsum dolor sit amet,
6780 * consectetur adipiscing elit.
6781 */
6782 /*
6783 *ˇ Lorem ipsum dolor sit amet,
6784 * consectetur adipiscing elit.
6785 */
6786 "},
6787 rust_lang.clone(),
6788 &mut cx,
6789 );
6790
6791 // multiline block comment with inline start/end delimiters
6792 assert_rewrap(
6793 indoc! {"
6794 /*ˇ Lorem ipsum dolor sit amet,
6795 * consectetur adipiscing elit. */
6796 "},
6797 indoc! {"
6798 /*
6799 *ˇ Lorem ipsum dolor sit amet,
6800 * consectetur adipiscing elit.
6801 */
6802 "},
6803 rust_lang.clone(),
6804 &mut cx,
6805 );
6806
6807 // block comment rewrap still respects paragraph bounds
6808 assert_rewrap(
6809 indoc! {"
6810 /*
6811 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6812 *
6813 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6814 */
6815 "},
6816 indoc! {"
6817 /*
6818 *ˇ Lorem ipsum dolor sit amet,
6819 * consectetur adipiscing elit.
6820 *
6821 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6822 */
6823 "},
6824 rust_lang.clone(),
6825 &mut cx,
6826 );
6827
6828 // documentation comments
6829 assert_rewrap(
6830 indoc! {"
6831 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6832 /**
6833 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6834 */
6835 "},
6836 indoc! {"
6837 /**
6838 *ˇ Lorem ipsum dolor sit amet,
6839 * consectetur adipiscing elit.
6840 */
6841 /**
6842 *ˇ Lorem ipsum dolor sit amet,
6843 * consectetur adipiscing elit.
6844 */
6845 "},
6846 rust_lang.clone(),
6847 &mut cx,
6848 );
6849
6850 // different, adjacent comments
6851 assert_rewrap(
6852 indoc! {"
6853 /**
6854 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6855 */
6856 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6857 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6858 "},
6859 indoc! {"
6860 /**
6861 *ˇ Lorem ipsum dolor sit amet,
6862 * consectetur adipiscing elit.
6863 */
6864 /*
6865 *ˇ Lorem ipsum dolor sit amet,
6866 * consectetur adipiscing elit.
6867 */
6868 //ˇ Lorem ipsum dolor sit amet,
6869 // consectetur adipiscing elit.
6870 "},
6871 rust_lang.clone(),
6872 &mut cx,
6873 );
6874
6875 // selection w/ single short block comment
6876 assert_rewrap(
6877 indoc! {"
6878 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6879 "},
6880 indoc! {"
6881 «/*
6882 * Lorem ipsum dolor sit amet,
6883 * consectetur adipiscing elit.
6884 */ˇ»
6885 "},
6886 rust_lang.clone(),
6887 &mut cx,
6888 );
6889
6890 // rewrapping a single comment w/ abutting comments
6891 assert_rewrap(
6892 indoc! {"
6893 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6894 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6895 "},
6896 indoc! {"
6897 /*
6898 * ˇLorem ipsum dolor sit amet,
6899 * consectetur adipiscing elit.
6900 */
6901 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6902 "},
6903 rust_lang.clone(),
6904 &mut cx,
6905 );
6906
6907 // selection w/ non-abutting short block comments
6908 assert_rewrap(
6909 indoc! {"
6910 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6911
6912 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6913 "},
6914 indoc! {"
6915 «/*
6916 * Lorem ipsum dolor sit amet,
6917 * consectetur adipiscing elit.
6918 */
6919
6920 /*
6921 * Lorem ipsum dolor sit amet,
6922 * consectetur adipiscing elit.
6923 */ˇ»
6924 "},
6925 rust_lang.clone(),
6926 &mut cx,
6927 );
6928
6929 // selection of multiline block comments
6930 assert_rewrap(
6931 indoc! {"
6932 «/* Lorem ipsum dolor sit amet,
6933 * consectetur adipiscing elit. */ˇ»
6934 "},
6935 indoc! {"
6936 «/*
6937 * Lorem ipsum dolor sit amet,
6938 * consectetur adipiscing elit.
6939 */ˇ»
6940 "},
6941 rust_lang.clone(),
6942 &mut cx,
6943 );
6944
6945 // partial selection of multiline block comments
6946 assert_rewrap(
6947 indoc! {"
6948 «/* Lorem ipsum dolor sit amet,ˇ»
6949 * consectetur adipiscing elit. */
6950 /* Lorem ipsum dolor sit amet,
6951 «* consectetur adipiscing elit. */ˇ»
6952 "},
6953 indoc! {"
6954 «/*
6955 * Lorem ipsum dolor sit amet,ˇ»
6956 * consectetur adipiscing elit. */
6957 /* Lorem ipsum dolor sit amet,
6958 «* consectetur adipiscing elit.
6959 */ˇ»
6960 "},
6961 rust_lang.clone(),
6962 &mut cx,
6963 );
6964
6965 // selection w/ abutting short block comments
6966 // TODO: should not be combined; should rewrap as 2 comments
6967 assert_rewrap(
6968 indoc! {"
6969 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6970 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6971 "},
6972 // desired behavior:
6973 // indoc! {"
6974 // «/*
6975 // * Lorem ipsum dolor sit amet,
6976 // * consectetur adipiscing elit.
6977 // */
6978 // /*
6979 // * Lorem ipsum dolor sit amet,
6980 // * consectetur adipiscing elit.
6981 // */ˇ»
6982 // "},
6983 // actual behaviour:
6984 indoc! {"
6985 «/*
6986 * Lorem ipsum dolor sit amet,
6987 * consectetur adipiscing elit. Lorem
6988 * ipsum dolor sit amet, consectetur
6989 * adipiscing elit.
6990 */ˇ»
6991 "},
6992 rust_lang.clone(),
6993 &mut cx,
6994 );
6995
6996 // TODO: same as above, but with delimiters on separate line
6997 // assert_rewrap(
6998 // indoc! {"
6999 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7000 // */
7001 // /*
7002 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7003 // "},
7004 // // desired:
7005 // // indoc! {"
7006 // // «/*
7007 // // * Lorem ipsum dolor sit amet,
7008 // // * consectetur adipiscing elit.
7009 // // */
7010 // // /*
7011 // // * Lorem ipsum dolor sit amet,
7012 // // * consectetur adipiscing elit.
7013 // // */ˇ»
7014 // // "},
7015 // // actual: (but with trailing w/s on the empty lines)
7016 // indoc! {"
7017 // «/*
7018 // * Lorem ipsum dolor sit amet,
7019 // * consectetur adipiscing elit.
7020 // *
7021 // */
7022 // /*
7023 // *
7024 // * Lorem ipsum dolor sit amet,
7025 // * consectetur adipiscing elit.
7026 // */ˇ»
7027 // "},
7028 // rust_lang.clone(),
7029 // &mut cx,
7030 // );
7031
7032 // TODO these are unhandled edge cases; not correct, just documenting known issues
7033 assert_rewrap(
7034 indoc! {"
7035 /*
7036 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7037 */
7038 /*
7039 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7040 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7041 "},
7042 // desired:
7043 // indoc! {"
7044 // /*
7045 // *ˇ Lorem ipsum dolor sit amet,
7046 // * consectetur adipiscing elit.
7047 // */
7048 // /*
7049 // *ˇ Lorem ipsum dolor sit amet,
7050 // * consectetur adipiscing elit.
7051 // */
7052 // /*
7053 // *ˇ Lorem ipsum dolor sit amet
7054 // */ /* consectetur adipiscing elit. */
7055 // "},
7056 // actual:
7057 indoc! {"
7058 /*
7059 //ˇ Lorem ipsum dolor sit amet,
7060 // consectetur adipiscing elit.
7061 */
7062 /*
7063 * //ˇ Lorem ipsum dolor sit amet,
7064 * consectetur adipiscing elit.
7065 */
7066 /*
7067 *ˇ Lorem ipsum dolor sit amet */ /*
7068 * consectetur adipiscing elit.
7069 */
7070 "},
7071 rust_lang,
7072 &mut cx,
7073 );
7074
7075 #[track_caller]
7076 fn assert_rewrap(
7077 unwrapped_text: &str,
7078 wrapped_text: &str,
7079 language: Arc<Language>,
7080 cx: &mut EditorTestContext,
7081 ) {
7082 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7083 cx.set_state(unwrapped_text);
7084 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7085 cx.assert_editor_state(wrapped_text);
7086 }
7087}
7088
7089#[gpui::test]
7090async fn test_hard_wrap(cx: &mut TestAppContext) {
7091 init_test(cx, |_| {});
7092 let mut cx = EditorTestContext::new(cx).await;
7093
7094 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7095 cx.update_editor(|editor, _, cx| {
7096 editor.set_hard_wrap(Some(14), cx);
7097 });
7098
7099 cx.set_state(indoc!(
7100 "
7101 one two three ˇ
7102 "
7103 ));
7104 cx.simulate_input("four");
7105 cx.run_until_parked();
7106
7107 cx.assert_editor_state(indoc!(
7108 "
7109 one two three
7110 fourˇ
7111 "
7112 ));
7113
7114 cx.update_editor(|editor, window, cx| {
7115 editor.newline(&Default::default(), window, cx);
7116 });
7117 cx.run_until_parked();
7118 cx.assert_editor_state(indoc!(
7119 "
7120 one two three
7121 four
7122 ˇ
7123 "
7124 ));
7125
7126 cx.simulate_input("five");
7127 cx.run_until_parked();
7128 cx.assert_editor_state(indoc!(
7129 "
7130 one two three
7131 four
7132 fiveˇ
7133 "
7134 ));
7135
7136 cx.update_editor(|editor, window, cx| {
7137 editor.newline(&Default::default(), window, cx);
7138 });
7139 cx.run_until_parked();
7140 cx.simulate_input("# ");
7141 cx.run_until_parked();
7142 cx.assert_editor_state(indoc!(
7143 "
7144 one two three
7145 four
7146 five
7147 # ˇ
7148 "
7149 ));
7150
7151 cx.update_editor(|editor, window, cx| {
7152 editor.newline(&Default::default(), window, cx);
7153 });
7154 cx.run_until_parked();
7155 cx.assert_editor_state(indoc!(
7156 "
7157 one two three
7158 four
7159 five
7160 #\x20
7161 #ˇ
7162 "
7163 ));
7164
7165 cx.simulate_input(" 6");
7166 cx.run_until_parked();
7167 cx.assert_editor_state(indoc!(
7168 "
7169 one two three
7170 four
7171 five
7172 #
7173 # 6ˇ
7174 "
7175 ));
7176}
7177
7178#[gpui::test]
7179async fn test_cut_line_ends(cx: &mut TestAppContext) {
7180 init_test(cx, |_| {});
7181
7182 let mut cx = EditorTestContext::new(cx).await;
7183
7184 cx.set_state(indoc! {"The quick brownˇ"});
7185 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7186 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7187
7188 cx.set_state(indoc! {"The emacs foxˇ"});
7189 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7190 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7191
7192 cx.set_state(indoc! {"
7193 The quick« brownˇ»
7194 fox jumps overˇ
7195 the lazy dog"});
7196 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7197 cx.assert_editor_state(indoc! {"
7198 The quickˇ
7199 ˇthe lazy dog"});
7200
7201 cx.set_state(indoc! {"
7202 The quick« brownˇ»
7203 fox jumps overˇ
7204 the lazy dog"});
7205 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7206 cx.assert_editor_state(indoc! {"
7207 The quickˇ
7208 fox jumps overˇthe lazy dog"});
7209
7210 cx.set_state(indoc! {"
7211 The quick« brownˇ»
7212 fox jumps overˇ
7213 the lazy dog"});
7214 cx.update_editor(|e, window, cx| {
7215 e.cut_to_end_of_line(
7216 &CutToEndOfLine {
7217 stop_at_newlines: true,
7218 },
7219 window,
7220 cx,
7221 )
7222 });
7223 cx.assert_editor_state(indoc! {"
7224 The quickˇ
7225 fox jumps overˇ
7226 the lazy dog"});
7227
7228 cx.set_state(indoc! {"
7229 The quick« brownˇ»
7230 fox jumps overˇ
7231 the lazy dog"});
7232 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7233 cx.assert_editor_state(indoc! {"
7234 The quickˇ
7235 fox jumps overˇthe lazy dog"});
7236}
7237
7238#[gpui::test]
7239async fn test_clipboard(cx: &mut TestAppContext) {
7240 init_test(cx, |_| {});
7241
7242 let mut cx = EditorTestContext::new(cx).await;
7243
7244 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7245 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7246 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7247
7248 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7249 cx.set_state("two ˇfour ˇsix ˇ");
7250 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7251 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7252
7253 // Paste again but with only two cursors. Since the number of cursors doesn't
7254 // match the number of slices in the clipboard, the entire clipboard text
7255 // is pasted at each cursor.
7256 cx.set_state("ˇtwo one✅ four three six five ˇ");
7257 cx.update_editor(|e, window, cx| {
7258 e.handle_input("( ", window, cx);
7259 e.paste(&Paste, window, cx);
7260 e.handle_input(") ", window, cx);
7261 });
7262 cx.assert_editor_state(
7263 &([
7264 "( one✅ ",
7265 "three ",
7266 "five ) ˇtwo one✅ four three six five ( one✅ ",
7267 "three ",
7268 "five ) ˇ",
7269 ]
7270 .join("\n")),
7271 );
7272
7273 // Cut with three selections, one of which is full-line.
7274 cx.set_state(indoc! {"
7275 1«2ˇ»3
7276 4ˇ567
7277 «8ˇ»9"});
7278 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7279 cx.assert_editor_state(indoc! {"
7280 1ˇ3
7281 ˇ9"});
7282
7283 // Paste with three selections, noticing how the copied selection that was full-line
7284 // gets inserted before the second cursor.
7285 cx.set_state(indoc! {"
7286 1ˇ3
7287 9ˇ
7288 «oˇ»ne"});
7289 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7290 cx.assert_editor_state(indoc! {"
7291 12ˇ3
7292 4567
7293 9ˇ
7294 8ˇne"});
7295
7296 // Copy with a single cursor only, which writes the whole line into the clipboard.
7297 cx.set_state(indoc! {"
7298 The quick brown
7299 fox juˇmps over
7300 the lazy dog"});
7301 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7302 assert_eq!(
7303 cx.read_from_clipboard()
7304 .and_then(|item| item.text().as_deref().map(str::to_string)),
7305 Some("fox jumps over\n".to_string())
7306 );
7307
7308 // Paste with three selections, noticing how the copied full-line selection is inserted
7309 // before the empty selections but replaces the selection that is non-empty.
7310 cx.set_state(indoc! {"
7311 Tˇhe quick brown
7312 «foˇ»x jumps over
7313 tˇhe lazy dog"});
7314 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7315 cx.assert_editor_state(indoc! {"
7316 fox jumps over
7317 Tˇhe quick brown
7318 fox jumps over
7319 ˇx jumps over
7320 fox jumps over
7321 tˇhe lazy dog"});
7322}
7323
7324#[gpui::test]
7325async fn test_copy_trim(cx: &mut TestAppContext) {
7326 init_test(cx, |_| {});
7327
7328 let mut cx = EditorTestContext::new(cx).await;
7329 cx.set_state(
7330 r#" «for selection in selections.iter() {
7331 let mut start = selection.start;
7332 let mut end = selection.end;
7333 let is_entire_line = selection.is_empty();
7334 if is_entire_line {
7335 start = Point::new(start.row, 0);ˇ»
7336 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7337 }
7338 "#,
7339 );
7340 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7341 assert_eq!(
7342 cx.read_from_clipboard()
7343 .and_then(|item| item.text().as_deref().map(str::to_string)),
7344 Some(
7345 "for selection in selections.iter() {
7346 let mut start = selection.start;
7347 let mut end = selection.end;
7348 let is_entire_line = selection.is_empty();
7349 if is_entire_line {
7350 start = Point::new(start.row, 0);"
7351 .to_string()
7352 ),
7353 "Regular copying preserves all indentation selected",
7354 );
7355 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7356 assert_eq!(
7357 cx.read_from_clipboard()
7358 .and_then(|item| item.text().as_deref().map(str::to_string)),
7359 Some(
7360 "for selection in selections.iter() {
7361let mut start = selection.start;
7362let mut end = selection.end;
7363let is_entire_line = selection.is_empty();
7364if is_entire_line {
7365 start = Point::new(start.row, 0);"
7366 .to_string()
7367 ),
7368 "Copying with stripping should strip all leading whitespaces"
7369 );
7370
7371 cx.set_state(
7372 r#" « for selection in selections.iter() {
7373 let mut start = selection.start;
7374 let mut end = selection.end;
7375 let is_entire_line = selection.is_empty();
7376 if is_entire_line {
7377 start = Point::new(start.row, 0);ˇ»
7378 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7379 }
7380 "#,
7381 );
7382 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7383 assert_eq!(
7384 cx.read_from_clipboard()
7385 .and_then(|item| item.text().as_deref().map(str::to_string)),
7386 Some(
7387 " for selection in selections.iter() {
7388 let mut start = selection.start;
7389 let mut end = selection.end;
7390 let is_entire_line = selection.is_empty();
7391 if is_entire_line {
7392 start = Point::new(start.row, 0);"
7393 .to_string()
7394 ),
7395 "Regular copying preserves all indentation selected",
7396 );
7397 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7398 assert_eq!(
7399 cx.read_from_clipboard()
7400 .and_then(|item| item.text().as_deref().map(str::to_string)),
7401 Some(
7402 "for selection in selections.iter() {
7403let mut start = selection.start;
7404let mut end = selection.end;
7405let is_entire_line = selection.is_empty();
7406if is_entire_line {
7407 start = Point::new(start.row, 0);"
7408 .to_string()
7409 ),
7410 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7411 );
7412
7413 cx.set_state(
7414 r#" «ˇ for selection in selections.iter() {
7415 let mut start = selection.start;
7416 let mut end = selection.end;
7417 let is_entire_line = selection.is_empty();
7418 if is_entire_line {
7419 start = Point::new(start.row, 0);»
7420 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7421 }
7422 "#,
7423 );
7424 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7425 assert_eq!(
7426 cx.read_from_clipboard()
7427 .and_then(|item| item.text().as_deref().map(str::to_string)),
7428 Some(
7429 " for selection in selections.iter() {
7430 let mut start = selection.start;
7431 let mut end = selection.end;
7432 let is_entire_line = selection.is_empty();
7433 if is_entire_line {
7434 start = Point::new(start.row, 0);"
7435 .to_string()
7436 ),
7437 "Regular copying for reverse selection works the same",
7438 );
7439 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7440 assert_eq!(
7441 cx.read_from_clipboard()
7442 .and_then(|item| item.text().as_deref().map(str::to_string)),
7443 Some(
7444 "for selection in selections.iter() {
7445let mut start = selection.start;
7446let mut end = selection.end;
7447let is_entire_line = selection.is_empty();
7448if is_entire_line {
7449 start = Point::new(start.row, 0);"
7450 .to_string()
7451 ),
7452 "Copying with stripping for reverse selection works the same"
7453 );
7454
7455 cx.set_state(
7456 r#" for selection «in selections.iter() {
7457 let mut start = selection.start;
7458 let mut end = selection.end;
7459 let is_entire_line = selection.is_empty();
7460 if is_entire_line {
7461 start = Point::new(start.row, 0);ˇ»
7462 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7463 }
7464 "#,
7465 );
7466 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7467 assert_eq!(
7468 cx.read_from_clipboard()
7469 .and_then(|item| item.text().as_deref().map(str::to_string)),
7470 Some(
7471 "in selections.iter() {
7472 let mut start = selection.start;
7473 let mut end = selection.end;
7474 let is_entire_line = selection.is_empty();
7475 if is_entire_line {
7476 start = Point::new(start.row, 0);"
7477 .to_string()
7478 ),
7479 "When selecting past the indent, the copying works as usual",
7480 );
7481 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7482 assert_eq!(
7483 cx.read_from_clipboard()
7484 .and_then(|item| item.text().as_deref().map(str::to_string)),
7485 Some(
7486 "in selections.iter() {
7487 let mut start = selection.start;
7488 let mut end = selection.end;
7489 let is_entire_line = selection.is_empty();
7490 if is_entire_line {
7491 start = Point::new(start.row, 0);"
7492 .to_string()
7493 ),
7494 "When selecting past the indent, nothing is trimmed"
7495 );
7496
7497 cx.set_state(
7498 r#" «for selection in selections.iter() {
7499 let mut start = selection.start;
7500
7501 let mut end = selection.end;
7502 let is_entire_line = selection.is_empty();
7503 if is_entire_line {
7504 start = Point::new(start.row, 0);
7505ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7506 }
7507 "#,
7508 );
7509 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7510 assert_eq!(
7511 cx.read_from_clipboard()
7512 .and_then(|item| item.text().as_deref().map(str::to_string)),
7513 Some(
7514 "for selection in selections.iter() {
7515let mut start = selection.start;
7516
7517let mut end = selection.end;
7518let is_entire_line = selection.is_empty();
7519if is_entire_line {
7520 start = Point::new(start.row, 0);
7521"
7522 .to_string()
7523 ),
7524 "Copying with stripping should ignore empty lines"
7525 );
7526}
7527
7528#[gpui::test]
7529async fn test_paste_multiline(cx: &mut TestAppContext) {
7530 init_test(cx, |_| {});
7531
7532 let mut cx = EditorTestContext::new(cx).await;
7533 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7534
7535 // Cut an indented block, without the leading whitespace.
7536 cx.set_state(indoc! {"
7537 const a: B = (
7538 c(),
7539 «d(
7540 e,
7541 f
7542 )ˇ»
7543 );
7544 "});
7545 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7546 cx.assert_editor_state(indoc! {"
7547 const a: B = (
7548 c(),
7549 ˇ
7550 );
7551 "});
7552
7553 // Paste it at the same position.
7554 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7555 cx.assert_editor_state(indoc! {"
7556 const a: B = (
7557 c(),
7558 d(
7559 e,
7560 f
7561 )ˇ
7562 );
7563 "});
7564
7565 // Paste it at a line with a lower indent level.
7566 cx.set_state(indoc! {"
7567 ˇ
7568 const a: B = (
7569 c(),
7570 );
7571 "});
7572 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7573 cx.assert_editor_state(indoc! {"
7574 d(
7575 e,
7576 f
7577 )ˇ
7578 const a: B = (
7579 c(),
7580 );
7581 "});
7582
7583 // Cut an indented block, with the leading whitespace.
7584 cx.set_state(indoc! {"
7585 const a: B = (
7586 c(),
7587 « d(
7588 e,
7589 f
7590 )
7591 ˇ»);
7592 "});
7593 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7594 cx.assert_editor_state(indoc! {"
7595 const a: B = (
7596 c(),
7597 ˇ);
7598 "});
7599
7600 // Paste it at the same position.
7601 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7602 cx.assert_editor_state(indoc! {"
7603 const a: B = (
7604 c(),
7605 d(
7606 e,
7607 f
7608 )
7609 ˇ);
7610 "});
7611
7612 // Paste it at a line with a higher indent level.
7613 cx.set_state(indoc! {"
7614 const a: B = (
7615 c(),
7616 d(
7617 e,
7618 fˇ
7619 )
7620 );
7621 "});
7622 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7623 cx.assert_editor_state(indoc! {"
7624 const a: B = (
7625 c(),
7626 d(
7627 e,
7628 f d(
7629 e,
7630 f
7631 )
7632 ˇ
7633 )
7634 );
7635 "});
7636
7637 // Copy an indented block, starting mid-line
7638 cx.set_state(indoc! {"
7639 const a: B = (
7640 c(),
7641 somethin«g(
7642 e,
7643 f
7644 )ˇ»
7645 );
7646 "});
7647 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7648
7649 // Paste it on a line with a lower indent level
7650 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7651 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7652 cx.assert_editor_state(indoc! {"
7653 const a: B = (
7654 c(),
7655 something(
7656 e,
7657 f
7658 )
7659 );
7660 g(
7661 e,
7662 f
7663 )ˇ"});
7664}
7665
7666#[gpui::test]
7667async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7668 init_test(cx, |_| {});
7669
7670 cx.write_to_clipboard(ClipboardItem::new_string(
7671 " d(\n e\n );\n".into(),
7672 ));
7673
7674 let mut cx = EditorTestContext::new(cx).await;
7675 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7676
7677 cx.set_state(indoc! {"
7678 fn a() {
7679 b();
7680 if c() {
7681 ˇ
7682 }
7683 }
7684 "});
7685
7686 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7687 cx.assert_editor_state(indoc! {"
7688 fn a() {
7689 b();
7690 if c() {
7691 d(
7692 e
7693 );
7694 ˇ
7695 }
7696 }
7697 "});
7698
7699 cx.set_state(indoc! {"
7700 fn a() {
7701 b();
7702 ˇ
7703 }
7704 "});
7705
7706 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7707 cx.assert_editor_state(indoc! {"
7708 fn a() {
7709 b();
7710 d(
7711 e
7712 );
7713 ˇ
7714 }
7715 "});
7716}
7717
7718#[gpui::test]
7719fn test_select_all(cx: &mut TestAppContext) {
7720 init_test(cx, |_| {});
7721
7722 let editor = cx.add_window(|window, cx| {
7723 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7724 build_editor(buffer, window, cx)
7725 });
7726 _ = editor.update(cx, |editor, window, cx| {
7727 editor.select_all(&SelectAll, window, cx);
7728 assert_eq!(
7729 display_ranges(editor, cx),
7730 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7731 );
7732 });
7733}
7734
7735#[gpui::test]
7736fn test_select_line(cx: &mut TestAppContext) {
7737 init_test(cx, |_| {});
7738
7739 let editor = cx.add_window(|window, cx| {
7740 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7741 build_editor(buffer, window, cx)
7742 });
7743 _ = editor.update(cx, |editor, window, cx| {
7744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7745 s.select_display_ranges([
7746 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7747 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7748 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7749 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7750 ])
7751 });
7752 editor.select_line(&SelectLine, window, cx);
7753 // Adjacent line selections should NOT merge (only overlapping ones do)
7754 assert_eq!(
7755 display_ranges(editor, cx),
7756 vec![
7757 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
7758 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
7759 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7760 ]
7761 );
7762 });
7763
7764 _ = editor.update(cx, |editor, window, cx| {
7765 editor.select_line(&SelectLine, window, cx);
7766 assert_eq!(
7767 display_ranges(editor, cx),
7768 vec![
7769 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7770 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7771 ]
7772 );
7773 });
7774
7775 _ = editor.update(cx, |editor, window, cx| {
7776 editor.select_line(&SelectLine, window, cx);
7777 // Adjacent but not overlapping, so they stay separate
7778 assert_eq!(
7779 display_ranges(editor, cx),
7780 vec![
7781 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
7782 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7783 ]
7784 );
7785 });
7786}
7787
7788#[gpui::test]
7789async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7790 init_test(cx, |_| {});
7791 let mut cx = EditorTestContext::new(cx).await;
7792
7793 #[track_caller]
7794 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7795 cx.set_state(initial_state);
7796 cx.update_editor(|e, window, cx| {
7797 e.split_selection_into_lines(&Default::default(), window, cx)
7798 });
7799 cx.assert_editor_state(expected_state);
7800 }
7801
7802 // Selection starts and ends at the middle of lines, left-to-right
7803 test(
7804 &mut cx,
7805 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7806 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7807 );
7808 // Same thing, right-to-left
7809 test(
7810 &mut cx,
7811 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7812 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7813 );
7814
7815 // Whole buffer, left-to-right, last line *doesn't* end with newline
7816 test(
7817 &mut cx,
7818 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7819 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7820 );
7821 // Same thing, right-to-left
7822 test(
7823 &mut cx,
7824 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7825 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7826 );
7827
7828 // Whole buffer, left-to-right, last line ends with newline
7829 test(
7830 &mut cx,
7831 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7832 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7833 );
7834 // Same thing, right-to-left
7835 test(
7836 &mut cx,
7837 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7838 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7839 );
7840
7841 // Starts at the end of a line, ends at the start of another
7842 test(
7843 &mut cx,
7844 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7845 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7846 );
7847}
7848
7849#[gpui::test]
7850async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7851 init_test(cx, |_| {});
7852
7853 let editor = cx.add_window(|window, cx| {
7854 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7855 build_editor(buffer, window, cx)
7856 });
7857
7858 // setup
7859 _ = editor.update(cx, |editor, window, cx| {
7860 editor.fold_creases(
7861 vec![
7862 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7863 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7864 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7865 ],
7866 true,
7867 window,
7868 cx,
7869 );
7870 assert_eq!(
7871 editor.display_text(cx),
7872 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7873 );
7874 });
7875
7876 _ = editor.update(cx, |editor, window, cx| {
7877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7878 s.select_display_ranges([
7879 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7880 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7881 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7882 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7883 ])
7884 });
7885 editor.split_selection_into_lines(&Default::default(), window, cx);
7886 assert_eq!(
7887 editor.display_text(cx),
7888 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7889 );
7890 });
7891 EditorTestContext::for_editor(editor, cx)
7892 .await
7893 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7894
7895 _ = editor.update(cx, |editor, window, cx| {
7896 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7897 s.select_display_ranges([
7898 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7899 ])
7900 });
7901 editor.split_selection_into_lines(&Default::default(), window, cx);
7902 assert_eq!(
7903 editor.display_text(cx),
7904 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7905 );
7906 assert_eq!(
7907 display_ranges(editor, cx),
7908 [
7909 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7910 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7911 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7912 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7913 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7914 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7915 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7916 ]
7917 );
7918 });
7919 EditorTestContext::for_editor(editor, cx)
7920 .await
7921 .assert_editor_state(
7922 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7923 );
7924}
7925
7926#[gpui::test]
7927async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7928 init_test(cx, |_| {});
7929
7930 let mut cx = EditorTestContext::new(cx).await;
7931
7932 cx.set_state(indoc!(
7933 r#"abc
7934 defˇghi
7935
7936 jk
7937 nlmo
7938 "#
7939 ));
7940
7941 cx.update_editor(|editor, window, cx| {
7942 editor.add_selection_above(&Default::default(), window, cx);
7943 });
7944
7945 cx.assert_editor_state(indoc!(
7946 r#"abcˇ
7947 defˇghi
7948
7949 jk
7950 nlmo
7951 "#
7952 ));
7953
7954 cx.update_editor(|editor, window, cx| {
7955 editor.add_selection_above(&Default::default(), window, cx);
7956 });
7957
7958 cx.assert_editor_state(indoc!(
7959 r#"abcˇ
7960 defˇghi
7961
7962 jk
7963 nlmo
7964 "#
7965 ));
7966
7967 cx.update_editor(|editor, window, cx| {
7968 editor.add_selection_below(&Default::default(), window, cx);
7969 });
7970
7971 cx.assert_editor_state(indoc!(
7972 r#"abc
7973 defˇghi
7974
7975 jk
7976 nlmo
7977 "#
7978 ));
7979
7980 cx.update_editor(|editor, window, cx| {
7981 editor.undo_selection(&Default::default(), window, cx);
7982 });
7983
7984 cx.assert_editor_state(indoc!(
7985 r#"abcˇ
7986 defˇghi
7987
7988 jk
7989 nlmo
7990 "#
7991 ));
7992
7993 cx.update_editor(|editor, window, cx| {
7994 editor.redo_selection(&Default::default(), window, cx);
7995 });
7996
7997 cx.assert_editor_state(indoc!(
7998 r#"abc
7999 defˇghi
8000
8001 jk
8002 nlmo
8003 "#
8004 ));
8005
8006 cx.update_editor(|editor, window, cx| {
8007 editor.add_selection_below(&Default::default(), window, cx);
8008 });
8009
8010 cx.assert_editor_state(indoc!(
8011 r#"abc
8012 defˇghi
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#"abc
8025 defˇghi
8026 ˇ
8027 jkˇ
8028 nlmo
8029 "#
8030 ));
8031
8032 cx.update_editor(|editor, window, cx| {
8033 editor.add_selection_below(&Default::default(), window, cx);
8034 });
8035
8036 cx.assert_editor_state(indoc!(
8037 r#"abc
8038 defˇghi
8039 ˇ
8040 jkˇ
8041 nlmˇo
8042 "#
8043 ));
8044
8045 cx.update_editor(|editor, window, cx| {
8046 editor.add_selection_below(&Default::default(), window, cx);
8047 });
8048
8049 cx.assert_editor_state(indoc!(
8050 r#"abc
8051 defˇghi
8052 ˇ
8053 jkˇ
8054 nlmˇo
8055 ˇ"#
8056 ));
8057
8058 // change selections
8059 cx.set_state(indoc!(
8060 r#"abc
8061 def«ˇg»hi
8062
8063 jk
8064 nlmo
8065 "#
8066 ));
8067
8068 cx.update_editor(|editor, window, cx| {
8069 editor.add_selection_below(&Default::default(), window, cx);
8070 });
8071
8072 cx.assert_editor_state(indoc!(
8073 r#"abc
8074 def«ˇg»hi
8075
8076 jk
8077 nlm«ˇo»
8078 "#
8079 ));
8080
8081 cx.update_editor(|editor, window, cx| {
8082 editor.add_selection_below(&Default::default(), window, cx);
8083 });
8084
8085 cx.assert_editor_state(indoc!(
8086 r#"abc
8087 def«ˇg»hi
8088
8089 jk
8090 nlm«ˇo»
8091 "#
8092 ));
8093
8094 cx.update_editor(|editor, window, cx| {
8095 editor.add_selection_above(&Default::default(), window, cx);
8096 });
8097
8098 cx.assert_editor_state(indoc!(
8099 r#"abc
8100 def«ˇg»hi
8101
8102 jk
8103 nlmo
8104 "#
8105 ));
8106
8107 cx.update_editor(|editor, window, cx| {
8108 editor.add_selection_above(&Default::default(), window, cx);
8109 });
8110
8111 cx.assert_editor_state(indoc!(
8112 r#"abc
8113 def«ˇg»hi
8114
8115 jk
8116 nlmo
8117 "#
8118 ));
8119
8120 // Change selections again
8121 cx.set_state(indoc!(
8122 r#"a«bc
8123 defgˇ»hi
8124
8125 jk
8126 nlmo
8127 "#
8128 ));
8129
8130 cx.update_editor(|editor, window, cx| {
8131 editor.add_selection_below(&Default::default(), window, cx);
8132 });
8133
8134 cx.assert_editor_state(indoc!(
8135 r#"a«bcˇ»
8136 d«efgˇ»hi
8137
8138 j«kˇ»
8139 nlmo
8140 "#
8141 ));
8142
8143 cx.update_editor(|editor, window, cx| {
8144 editor.add_selection_below(&Default::default(), window, cx);
8145 });
8146 cx.assert_editor_state(indoc!(
8147 r#"a«bcˇ»
8148 d«efgˇ»hi
8149
8150 j«kˇ»
8151 n«lmoˇ»
8152 "#
8153 ));
8154 cx.update_editor(|editor, window, cx| {
8155 editor.add_selection_above(&Default::default(), window, cx);
8156 });
8157
8158 cx.assert_editor_state(indoc!(
8159 r#"a«bcˇ»
8160 d«efgˇ»hi
8161
8162 j«kˇ»
8163 nlmo
8164 "#
8165 ));
8166
8167 // Change selections again
8168 cx.set_state(indoc!(
8169 r#"abc
8170 d«ˇefghi
8171
8172 jk
8173 nlm»o
8174 "#
8175 ));
8176
8177 cx.update_editor(|editor, window, cx| {
8178 editor.add_selection_above(&Default::default(), window, cx);
8179 });
8180
8181 cx.assert_editor_state(indoc!(
8182 r#"a«ˇbc»
8183 d«ˇef»ghi
8184
8185 j«ˇk»
8186 n«ˇlm»o
8187 "#
8188 ));
8189
8190 cx.update_editor(|editor, window, cx| {
8191 editor.add_selection_below(&Default::default(), window, cx);
8192 });
8193
8194 cx.assert_editor_state(indoc!(
8195 r#"abc
8196 d«ˇef»ghi
8197
8198 j«ˇk»
8199 n«ˇlm»o
8200 "#
8201 ));
8202}
8203
8204#[gpui::test]
8205async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8206 init_test(cx, |_| {});
8207 let mut cx = EditorTestContext::new(cx).await;
8208
8209 cx.set_state(indoc!(
8210 r#"line onˇe
8211 liˇne two
8212 line three
8213 line four"#
8214 ));
8215
8216 cx.update_editor(|editor, window, cx| {
8217 editor.add_selection_below(&Default::default(), window, cx);
8218 });
8219
8220 // test multiple cursors expand in the same direction
8221 cx.assert_editor_state(indoc!(
8222 r#"line onˇe
8223 liˇne twˇo
8224 liˇne three
8225 line four"#
8226 ));
8227
8228 cx.update_editor(|editor, window, cx| {
8229 editor.add_selection_below(&Default::default(), window, cx);
8230 });
8231
8232 cx.update_editor(|editor, window, cx| {
8233 editor.add_selection_below(&Default::default(), window, cx);
8234 });
8235
8236 // test multiple cursors expand below overflow
8237 cx.assert_editor_state(indoc!(
8238 r#"line onˇe
8239 liˇne twˇo
8240 liˇne thˇree
8241 liˇne foˇur"#
8242 ));
8243
8244 cx.update_editor(|editor, window, cx| {
8245 editor.add_selection_above(&Default::default(), window, cx);
8246 });
8247
8248 // test multiple cursors retrieves back correctly
8249 cx.assert_editor_state(indoc!(
8250 r#"line onˇe
8251 liˇne twˇo
8252 liˇne thˇree
8253 line four"#
8254 ));
8255
8256 cx.update_editor(|editor, window, cx| {
8257 editor.add_selection_above(&Default::default(), window, cx);
8258 });
8259
8260 cx.update_editor(|editor, window, cx| {
8261 editor.add_selection_above(&Default::default(), window, cx);
8262 });
8263
8264 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8265 cx.assert_editor_state(indoc!(
8266 r#"liˇne onˇe
8267 liˇne two
8268 line three
8269 line four"#
8270 ));
8271
8272 cx.update_editor(|editor, window, cx| {
8273 editor.undo_selection(&Default::default(), window, cx);
8274 });
8275
8276 // test undo
8277 cx.assert_editor_state(indoc!(
8278 r#"line onˇe
8279 liˇne twˇo
8280 line three
8281 line four"#
8282 ));
8283
8284 cx.update_editor(|editor, window, cx| {
8285 editor.redo_selection(&Default::default(), window, cx);
8286 });
8287
8288 // test redo
8289 cx.assert_editor_state(indoc!(
8290 r#"liˇne onˇe
8291 liˇne two
8292 line three
8293 line four"#
8294 ));
8295
8296 cx.set_state(indoc!(
8297 r#"abcd
8298 ef«ghˇ»
8299 ijkl
8300 «mˇ»nop"#
8301 ));
8302
8303 cx.update_editor(|editor, window, cx| {
8304 editor.add_selection_above(&Default::default(), window, cx);
8305 });
8306
8307 // test multiple selections expand in the same direction
8308 cx.assert_editor_state(indoc!(
8309 r#"ab«cdˇ»
8310 ef«ghˇ»
8311 «iˇ»jkl
8312 «mˇ»nop"#
8313 ));
8314
8315 cx.update_editor(|editor, window, cx| {
8316 editor.add_selection_above(&Default::default(), window, cx);
8317 });
8318
8319 // test multiple selection upward overflow
8320 cx.assert_editor_state(indoc!(
8321 r#"ab«cdˇ»
8322 «eˇ»f«ghˇ»
8323 «iˇ»jkl
8324 «mˇ»nop"#
8325 ));
8326
8327 cx.update_editor(|editor, window, cx| {
8328 editor.add_selection_below(&Default::default(), window, cx);
8329 });
8330
8331 // test multiple selection retrieves back correctly
8332 cx.assert_editor_state(indoc!(
8333 r#"abcd
8334 ef«ghˇ»
8335 «iˇ»jkl
8336 «mˇ»nop"#
8337 ));
8338
8339 cx.update_editor(|editor, window, cx| {
8340 editor.add_selection_below(&Default::default(), window, cx);
8341 });
8342
8343 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8344 cx.assert_editor_state(indoc!(
8345 r#"abcd
8346 ef«ghˇ»
8347 ij«klˇ»
8348 «mˇ»nop"#
8349 ));
8350
8351 cx.update_editor(|editor, window, cx| {
8352 editor.undo_selection(&Default::default(), window, cx);
8353 });
8354
8355 // test undo
8356 cx.assert_editor_state(indoc!(
8357 r#"abcd
8358 ef«ghˇ»
8359 «iˇ»jkl
8360 «mˇ»nop"#
8361 ));
8362
8363 cx.update_editor(|editor, window, cx| {
8364 editor.redo_selection(&Default::default(), window, cx);
8365 });
8366
8367 // test redo
8368 cx.assert_editor_state(indoc!(
8369 r#"abcd
8370 ef«ghˇ»
8371 ij«klˇ»
8372 «mˇ»nop"#
8373 ));
8374}
8375
8376#[gpui::test]
8377async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8378 init_test(cx, |_| {});
8379 let mut cx = EditorTestContext::new(cx).await;
8380
8381 cx.set_state(indoc!(
8382 r#"line onˇe
8383 liˇne two
8384 line three
8385 line four"#
8386 ));
8387
8388 cx.update_editor(|editor, window, cx| {
8389 editor.add_selection_below(&Default::default(), window, cx);
8390 editor.add_selection_below(&Default::default(), window, cx);
8391 editor.add_selection_below(&Default::default(), window, cx);
8392 });
8393
8394 // initial state with two multi cursor groups
8395 cx.assert_editor_state(indoc!(
8396 r#"line onˇe
8397 liˇne twˇo
8398 liˇne thˇree
8399 liˇne foˇur"#
8400 ));
8401
8402 // add single cursor in middle - simulate opt click
8403 cx.update_editor(|editor, window, cx| {
8404 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8405 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8406 editor.end_selection(window, cx);
8407 });
8408
8409 cx.assert_editor_state(indoc!(
8410 r#"line onˇe
8411 liˇne twˇo
8412 liˇneˇ thˇree
8413 liˇne foˇur"#
8414 ));
8415
8416 cx.update_editor(|editor, window, cx| {
8417 editor.add_selection_above(&Default::default(), window, cx);
8418 });
8419
8420 // test new added selection expands above and existing selection shrinks
8421 cx.assert_editor_state(indoc!(
8422 r#"line onˇe
8423 liˇneˇ twˇo
8424 liˇneˇ thˇree
8425 line four"#
8426 ));
8427
8428 cx.update_editor(|editor, window, cx| {
8429 editor.add_selection_above(&Default::default(), window, cx);
8430 });
8431
8432 // test new added selection expands above and existing selection shrinks
8433 cx.assert_editor_state(indoc!(
8434 r#"lineˇ onˇe
8435 liˇneˇ twˇo
8436 lineˇ three
8437 line four"#
8438 ));
8439
8440 // intial state with two selection groups
8441 cx.set_state(indoc!(
8442 r#"abcd
8443 ef«ghˇ»
8444 ijkl
8445 «mˇ»nop"#
8446 ));
8447
8448 cx.update_editor(|editor, window, cx| {
8449 editor.add_selection_above(&Default::default(), window, cx);
8450 editor.add_selection_above(&Default::default(), window, cx);
8451 });
8452
8453 cx.assert_editor_state(indoc!(
8454 r#"ab«cdˇ»
8455 «eˇ»f«ghˇ»
8456 «iˇ»jkl
8457 «mˇ»nop"#
8458 ));
8459
8460 // add single selection in middle - simulate opt drag
8461 cx.update_editor(|editor, window, cx| {
8462 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8463 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8464 editor.update_selection(
8465 DisplayPoint::new(DisplayRow(2), 4),
8466 0,
8467 gpui::Point::<f32>::default(),
8468 window,
8469 cx,
8470 );
8471 editor.end_selection(window, cx);
8472 });
8473
8474 cx.assert_editor_state(indoc!(
8475 r#"ab«cdˇ»
8476 «eˇ»f«ghˇ»
8477 «iˇ»jk«lˇ»
8478 «mˇ»nop"#
8479 ));
8480
8481 cx.update_editor(|editor, window, cx| {
8482 editor.add_selection_below(&Default::default(), window, cx);
8483 });
8484
8485 // test new added selection expands below, others shrinks from above
8486 cx.assert_editor_state(indoc!(
8487 r#"abcd
8488 ef«ghˇ»
8489 «iˇ»jk«lˇ»
8490 «mˇ»no«pˇ»"#
8491 ));
8492}
8493
8494#[gpui::test]
8495async fn test_select_next(cx: &mut TestAppContext) {
8496 init_test(cx, |_| {});
8497 let mut cx = EditorTestContext::new(cx).await;
8498
8499 // Enable case sensitive search.
8500 update_test_editor_settings(&mut cx, |settings| {
8501 let mut search_settings = SearchSettingsContent::default();
8502 search_settings.case_sensitive = Some(true);
8503 settings.search = Some(search_settings);
8504 });
8505
8506 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8507
8508 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8509 .unwrap();
8510 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8511
8512 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8513 .unwrap();
8514 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8515
8516 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8517 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8518
8519 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8520 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8521
8522 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8523 .unwrap();
8524 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8525
8526 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8527 .unwrap();
8528 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8529
8530 // Test selection direction should be preserved
8531 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8532
8533 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8534 .unwrap();
8535 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8536
8537 // Test case sensitivity
8538 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8539 cx.update_editor(|e, window, cx| {
8540 e.select_next(&SelectNext::default(), window, cx).unwrap();
8541 });
8542 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8543
8544 // Disable case sensitive search.
8545 update_test_editor_settings(&mut cx, |settings| {
8546 let mut search_settings = SearchSettingsContent::default();
8547 search_settings.case_sensitive = Some(false);
8548 settings.search = Some(search_settings);
8549 });
8550
8551 cx.set_state("«ˇfoo»\nFOO\nFoo");
8552 cx.update_editor(|e, window, cx| {
8553 e.select_next(&SelectNext::default(), window, cx).unwrap();
8554 e.select_next(&SelectNext::default(), window, cx).unwrap();
8555 });
8556 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8557}
8558
8559#[gpui::test]
8560async fn test_select_all_matches(cx: &mut TestAppContext) {
8561 init_test(cx, |_| {});
8562 let mut cx = EditorTestContext::new(cx).await;
8563
8564 // Enable case sensitive search.
8565 update_test_editor_settings(&mut cx, |settings| {
8566 let mut search_settings = SearchSettingsContent::default();
8567 search_settings.case_sensitive = Some(true);
8568 settings.search = Some(search_settings);
8569 });
8570
8571 // Test caret-only selections
8572 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8573 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8574 .unwrap();
8575 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8576
8577 // Test left-to-right selections
8578 cx.set_state("abc\n«abcˇ»\nabc");
8579 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8580 .unwrap();
8581 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8582
8583 // Test right-to-left selections
8584 cx.set_state("abc\n«ˇabc»\nabc");
8585 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8586 .unwrap();
8587 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8588
8589 // Test selecting whitespace with caret selection
8590 cx.set_state("abc\nˇ abc\nabc");
8591 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8592 .unwrap();
8593 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8594
8595 // Test selecting whitespace with left-to-right selection
8596 cx.set_state("abc\n«ˇ »abc\nabc");
8597 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8598 .unwrap();
8599 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8600
8601 // Test no matches with right-to-left selection
8602 cx.set_state("abc\n« ˇ»abc\nabc");
8603 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8604 .unwrap();
8605 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8606
8607 // Test with a single word and clip_at_line_ends=true (#29823)
8608 cx.set_state("aˇbc");
8609 cx.update_editor(|e, window, cx| {
8610 e.set_clip_at_line_ends(true, cx);
8611 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8612 e.set_clip_at_line_ends(false, cx);
8613 });
8614 cx.assert_editor_state("«abcˇ»");
8615
8616 // Test case sensitivity
8617 cx.set_state("fˇoo\nFOO\nFoo");
8618 cx.update_editor(|e, window, cx| {
8619 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8620 });
8621 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8622
8623 // Disable case sensitive search.
8624 update_test_editor_settings(&mut cx, |settings| {
8625 let mut search_settings = SearchSettingsContent::default();
8626 search_settings.case_sensitive = Some(false);
8627 settings.search = Some(search_settings);
8628 });
8629
8630 cx.set_state("fˇoo\nFOO\nFoo");
8631 cx.update_editor(|e, window, cx| {
8632 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8633 });
8634 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8635}
8636
8637#[gpui::test]
8638async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8639 init_test(cx, |_| {});
8640
8641 let mut cx = EditorTestContext::new(cx).await;
8642
8643 let large_body_1 = "\nd".repeat(200);
8644 let large_body_2 = "\ne".repeat(200);
8645
8646 cx.set_state(&format!(
8647 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8648 ));
8649 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8650 let scroll_position = editor.scroll_position(cx);
8651 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8652 scroll_position
8653 });
8654
8655 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8656 .unwrap();
8657 cx.assert_editor_state(&format!(
8658 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8659 ));
8660 let scroll_position_after_selection =
8661 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8662 assert_eq!(
8663 initial_scroll_position, scroll_position_after_selection,
8664 "Scroll position should not change after selecting all matches"
8665 );
8666}
8667
8668#[gpui::test]
8669async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8670 init_test(cx, |_| {});
8671
8672 let mut cx = EditorLspTestContext::new_rust(
8673 lsp::ServerCapabilities {
8674 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8675 ..Default::default()
8676 },
8677 cx,
8678 )
8679 .await;
8680
8681 cx.set_state(indoc! {"
8682 line 1
8683 line 2
8684 linˇe 3
8685 line 4
8686 line 5
8687 "});
8688
8689 // Make an edit
8690 cx.update_editor(|editor, window, cx| {
8691 editor.handle_input("X", window, cx);
8692 });
8693
8694 // Move cursor to a different position
8695 cx.update_editor(|editor, window, cx| {
8696 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8697 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8698 });
8699 });
8700
8701 cx.assert_editor_state(indoc! {"
8702 line 1
8703 line 2
8704 linXe 3
8705 line 4
8706 liˇne 5
8707 "});
8708
8709 cx.lsp
8710 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8711 Ok(Some(vec![lsp::TextEdit::new(
8712 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8713 "PREFIX ".to_string(),
8714 )]))
8715 });
8716
8717 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8718 .unwrap()
8719 .await
8720 .unwrap();
8721
8722 cx.assert_editor_state(indoc! {"
8723 PREFIX line 1
8724 line 2
8725 linXe 3
8726 line 4
8727 liˇne 5
8728 "});
8729
8730 // Undo formatting
8731 cx.update_editor(|editor, window, cx| {
8732 editor.undo(&Default::default(), window, cx);
8733 });
8734
8735 // Verify cursor moved back to position after edit
8736 cx.assert_editor_state(indoc! {"
8737 line 1
8738 line 2
8739 linXˇe 3
8740 line 4
8741 line 5
8742 "});
8743}
8744
8745#[gpui::test]
8746async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8747 init_test(cx, |_| {});
8748
8749 let mut cx = EditorTestContext::new(cx).await;
8750
8751 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
8752 cx.update_editor(|editor, window, cx| {
8753 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8754 });
8755
8756 cx.set_state(indoc! {"
8757 line 1
8758 line 2
8759 linˇe 3
8760 line 4
8761 line 5
8762 line 6
8763 line 7
8764 line 8
8765 line 9
8766 line 10
8767 "});
8768
8769 let snapshot = cx.buffer_snapshot();
8770 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8771
8772 cx.update(|_, cx| {
8773 provider.update(cx, |provider, _| {
8774 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
8775 id: None,
8776 edits: vec![(edit_position..edit_position, "X".into())],
8777 edit_preview: None,
8778 }))
8779 })
8780 });
8781
8782 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8783 cx.update_editor(|editor, window, cx| {
8784 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8785 });
8786
8787 cx.assert_editor_state(indoc! {"
8788 line 1
8789 line 2
8790 lineXˇ 3
8791 line 4
8792 line 5
8793 line 6
8794 line 7
8795 line 8
8796 line 9
8797 line 10
8798 "});
8799
8800 cx.update_editor(|editor, window, cx| {
8801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8802 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8803 });
8804 });
8805
8806 cx.assert_editor_state(indoc! {"
8807 line 1
8808 line 2
8809 lineX 3
8810 line 4
8811 line 5
8812 line 6
8813 line 7
8814 line 8
8815 line 9
8816 liˇne 10
8817 "});
8818
8819 cx.update_editor(|editor, window, cx| {
8820 editor.undo(&Default::default(), window, cx);
8821 });
8822
8823 cx.assert_editor_state(indoc! {"
8824 line 1
8825 line 2
8826 lineˇ 3
8827 line 4
8828 line 5
8829 line 6
8830 line 7
8831 line 8
8832 line 9
8833 line 10
8834 "});
8835}
8836
8837#[gpui::test]
8838async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8839 init_test(cx, |_| {});
8840
8841 let mut cx = EditorTestContext::new(cx).await;
8842 cx.set_state(
8843 r#"let foo = 2;
8844lˇet foo = 2;
8845let fooˇ = 2;
8846let foo = 2;
8847let foo = ˇ2;"#,
8848 );
8849
8850 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8851 .unwrap();
8852 cx.assert_editor_state(
8853 r#"let foo = 2;
8854«letˇ» foo = 2;
8855let «fooˇ» = 2;
8856let foo = 2;
8857let foo = «2ˇ»;"#,
8858 );
8859
8860 // noop for multiple selections with different contents
8861 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8862 .unwrap();
8863 cx.assert_editor_state(
8864 r#"let foo = 2;
8865«letˇ» foo = 2;
8866let «fooˇ» = 2;
8867let foo = 2;
8868let foo = «2ˇ»;"#,
8869 );
8870
8871 // Test last selection direction should be preserved
8872 cx.set_state(
8873 r#"let foo = 2;
8874let foo = 2;
8875let «fooˇ» = 2;
8876let «ˇfoo» = 2;
8877let foo = 2;"#,
8878 );
8879
8880 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8881 .unwrap();
8882 cx.assert_editor_state(
8883 r#"let foo = 2;
8884let foo = 2;
8885let «fooˇ» = 2;
8886let «ˇfoo» = 2;
8887let «ˇfoo» = 2;"#,
8888 );
8889}
8890
8891#[gpui::test]
8892async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8893 init_test(cx, |_| {});
8894
8895 let mut cx =
8896 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8897
8898 cx.assert_editor_state(indoc! {"
8899 ˇbbb
8900 ccc
8901
8902 bbb
8903 ccc
8904 "});
8905 cx.dispatch_action(SelectPrevious::default());
8906 cx.assert_editor_state(indoc! {"
8907 «bbbˇ»
8908 ccc
8909
8910 bbb
8911 ccc
8912 "});
8913 cx.dispatch_action(SelectPrevious::default());
8914 cx.assert_editor_state(indoc! {"
8915 «bbbˇ»
8916 ccc
8917
8918 «bbbˇ»
8919 ccc
8920 "});
8921}
8922
8923#[gpui::test]
8924async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8925 init_test(cx, |_| {});
8926
8927 let mut cx = EditorTestContext::new(cx).await;
8928 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8929
8930 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8931 .unwrap();
8932 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8933
8934 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8935 .unwrap();
8936 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8937
8938 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8939 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8940
8941 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8942 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8943
8944 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8945 .unwrap();
8946 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8947
8948 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8949 .unwrap();
8950 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8951}
8952
8953#[gpui::test]
8954async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8955 init_test(cx, |_| {});
8956
8957 let mut cx = EditorTestContext::new(cx).await;
8958 cx.set_state("aˇ");
8959
8960 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8961 .unwrap();
8962 cx.assert_editor_state("«aˇ»");
8963 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8964 .unwrap();
8965 cx.assert_editor_state("«aˇ»");
8966}
8967
8968#[gpui::test]
8969async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8970 init_test(cx, |_| {});
8971
8972 let mut cx = EditorTestContext::new(cx).await;
8973 cx.set_state(
8974 r#"let foo = 2;
8975lˇet foo = 2;
8976let fooˇ = 2;
8977let foo = 2;
8978let foo = ˇ2;"#,
8979 );
8980
8981 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8982 .unwrap();
8983 cx.assert_editor_state(
8984 r#"let foo = 2;
8985«letˇ» foo = 2;
8986let «fooˇ» = 2;
8987let foo = 2;
8988let foo = «2ˇ»;"#,
8989 );
8990
8991 // noop for multiple selections with different contents
8992 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8993 .unwrap();
8994 cx.assert_editor_state(
8995 r#"let foo = 2;
8996«letˇ» foo = 2;
8997let «fooˇ» = 2;
8998let foo = 2;
8999let foo = «2ˇ»;"#,
9000 );
9001}
9002
9003#[gpui::test]
9004async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9005 init_test(cx, |_| {});
9006 let mut cx = EditorTestContext::new(cx).await;
9007
9008 // Enable case sensitive search.
9009 update_test_editor_settings(&mut cx, |settings| {
9010 let mut search_settings = SearchSettingsContent::default();
9011 search_settings.case_sensitive = Some(true);
9012 settings.search = Some(search_settings);
9013 });
9014
9015 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9016
9017 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9018 .unwrap();
9019 // selection direction is preserved
9020 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9021
9022 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9023 .unwrap();
9024 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9025
9026 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9027 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9028
9029 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9030 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9031
9032 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9033 .unwrap();
9034 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9035
9036 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9037 .unwrap();
9038 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9039
9040 // Test case sensitivity
9041 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9042 cx.update_editor(|e, window, cx| {
9043 e.select_previous(&SelectPrevious::default(), window, cx)
9044 .unwrap();
9045 e.select_previous(&SelectPrevious::default(), window, cx)
9046 .unwrap();
9047 });
9048 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9049
9050 // Disable case sensitive search.
9051 update_test_editor_settings(&mut cx, |settings| {
9052 let mut search_settings = SearchSettingsContent::default();
9053 search_settings.case_sensitive = Some(false);
9054 settings.search = Some(search_settings);
9055 });
9056
9057 cx.set_state("foo\nFOO\n«ˇFoo»");
9058 cx.update_editor(|e, window, cx| {
9059 e.select_previous(&SelectPrevious::default(), window, cx)
9060 .unwrap();
9061 e.select_previous(&SelectPrevious::default(), window, cx)
9062 .unwrap();
9063 });
9064 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9065}
9066
9067#[gpui::test]
9068async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9069 init_test(cx, |_| {});
9070
9071 let language = Arc::new(Language::new(
9072 LanguageConfig::default(),
9073 Some(tree_sitter_rust::LANGUAGE.into()),
9074 ));
9075
9076 let text = r#"
9077 use mod1::mod2::{mod3, mod4};
9078
9079 fn fn_1(param1: bool, param2: &str) {
9080 let var1 = "text";
9081 }
9082 "#
9083 .unindent();
9084
9085 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9086 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9087 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9088
9089 editor
9090 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9091 .await;
9092
9093 editor.update_in(cx, |editor, window, cx| {
9094 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9095 s.select_display_ranges([
9096 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9097 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9098 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9099 ]);
9100 });
9101 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9102 });
9103 editor.update(cx, |editor, cx| {
9104 assert_text_with_selections(
9105 editor,
9106 indoc! {r#"
9107 use mod1::mod2::{mod3, «mod4ˇ»};
9108
9109 fn fn_1«ˇ(param1: bool, param2: &str)» {
9110 let var1 = "«ˇtext»";
9111 }
9112 "#},
9113 cx,
9114 );
9115 });
9116
9117 editor.update_in(cx, |editor, window, cx| {
9118 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9119 });
9120 editor.update(cx, |editor, cx| {
9121 assert_text_with_selections(
9122 editor,
9123 indoc! {r#"
9124 use mod1::mod2::«{mod3, mod4}ˇ»;
9125
9126 «ˇfn fn_1(param1: bool, param2: &str) {
9127 let var1 = "text";
9128 }»
9129 "#},
9130 cx,
9131 );
9132 });
9133
9134 editor.update_in(cx, |editor, window, cx| {
9135 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9136 });
9137 assert_eq!(
9138 editor.update(cx, |editor, cx| editor
9139 .selections
9140 .display_ranges(&editor.display_snapshot(cx))),
9141 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9142 );
9143
9144 // Trying to expand the selected syntax node one more time has no effect.
9145 editor.update_in(cx, |editor, window, cx| {
9146 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9147 });
9148 assert_eq!(
9149 editor.update(cx, |editor, cx| editor
9150 .selections
9151 .display_ranges(&editor.display_snapshot(cx))),
9152 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9153 );
9154
9155 editor.update_in(cx, |editor, window, cx| {
9156 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9157 });
9158 editor.update(cx, |editor, cx| {
9159 assert_text_with_selections(
9160 editor,
9161 indoc! {r#"
9162 use mod1::mod2::«{mod3, mod4}ˇ»;
9163
9164 «ˇfn fn_1(param1: bool, param2: &str) {
9165 let var1 = "text";
9166 }»
9167 "#},
9168 cx,
9169 );
9170 });
9171
9172 editor.update_in(cx, |editor, window, cx| {
9173 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9174 });
9175 editor.update(cx, |editor, cx| {
9176 assert_text_with_selections(
9177 editor,
9178 indoc! {r#"
9179 use mod1::mod2::{mod3, «mod4ˇ»};
9180
9181 fn fn_1«ˇ(param1: bool, param2: &str)» {
9182 let var1 = "«ˇtext»";
9183 }
9184 "#},
9185 cx,
9186 );
9187 });
9188
9189 editor.update_in(cx, |editor, window, cx| {
9190 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9191 });
9192 editor.update(cx, |editor, cx| {
9193 assert_text_with_selections(
9194 editor,
9195 indoc! {r#"
9196 use mod1::mod2::{mod3, moˇd4};
9197
9198 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9199 let var1 = "teˇxt";
9200 }
9201 "#},
9202 cx,
9203 );
9204 });
9205
9206 // Trying to shrink the selected syntax node one more time has no effect.
9207 editor.update_in(cx, |editor, window, cx| {
9208 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9209 });
9210 editor.update_in(cx, |editor, _, cx| {
9211 assert_text_with_selections(
9212 editor,
9213 indoc! {r#"
9214 use mod1::mod2::{mod3, moˇd4};
9215
9216 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9217 let var1 = "teˇxt";
9218 }
9219 "#},
9220 cx,
9221 );
9222 });
9223
9224 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9225 // a fold.
9226 editor.update_in(cx, |editor, window, cx| {
9227 editor.fold_creases(
9228 vec![
9229 Crease::simple(
9230 Point::new(0, 21)..Point::new(0, 24),
9231 FoldPlaceholder::test(),
9232 ),
9233 Crease::simple(
9234 Point::new(3, 20)..Point::new(3, 22),
9235 FoldPlaceholder::test(),
9236 ),
9237 ],
9238 true,
9239 window,
9240 cx,
9241 );
9242 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9243 });
9244 editor.update(cx, |editor, cx| {
9245 assert_text_with_selections(
9246 editor,
9247 indoc! {r#"
9248 use mod1::mod2::«{mod3, mod4}ˇ»;
9249
9250 fn fn_1«ˇ(param1: bool, param2: &str)» {
9251 let var1 = "«ˇtext»";
9252 }
9253 "#},
9254 cx,
9255 );
9256 });
9257}
9258
9259#[gpui::test]
9260async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9261 init_test(cx, |_| {});
9262
9263 let language = Arc::new(Language::new(
9264 LanguageConfig::default(),
9265 Some(tree_sitter_rust::LANGUAGE.into()),
9266 ));
9267
9268 let text = "let a = 2;";
9269
9270 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9272 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9273
9274 editor
9275 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9276 .await;
9277
9278 // Test case 1: Cursor at end of word
9279 editor.update_in(cx, |editor, window, cx| {
9280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9281 s.select_display_ranges([
9282 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9283 ]);
9284 });
9285 });
9286 editor.update(cx, |editor, cx| {
9287 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9288 });
9289 editor.update_in(cx, |editor, window, cx| {
9290 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9291 });
9292 editor.update(cx, |editor, cx| {
9293 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9294 });
9295 editor.update_in(cx, |editor, window, cx| {
9296 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9297 });
9298 editor.update(cx, |editor, cx| {
9299 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9300 });
9301
9302 // Test case 2: Cursor at end of statement
9303 editor.update_in(cx, |editor, window, cx| {
9304 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9305 s.select_display_ranges([
9306 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9307 ]);
9308 });
9309 });
9310 editor.update(cx, |editor, cx| {
9311 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9312 });
9313 editor.update_in(cx, |editor, window, cx| {
9314 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9315 });
9316 editor.update(cx, |editor, cx| {
9317 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9318 });
9319}
9320
9321#[gpui::test]
9322async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9323 init_test(cx, |_| {});
9324
9325 let language = Arc::new(Language::new(
9326 LanguageConfig {
9327 name: "JavaScript".into(),
9328 ..Default::default()
9329 },
9330 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9331 ));
9332
9333 let text = r#"
9334 let a = {
9335 key: "value",
9336 };
9337 "#
9338 .unindent();
9339
9340 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9341 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9342 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9343
9344 editor
9345 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9346 .await;
9347
9348 // Test case 1: Cursor after '{'
9349 editor.update_in(cx, |editor, window, cx| {
9350 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9351 s.select_display_ranges([
9352 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9353 ]);
9354 });
9355 });
9356 editor.update(cx, |editor, cx| {
9357 assert_text_with_selections(
9358 editor,
9359 indoc! {r#"
9360 let a = {ˇ
9361 key: "value",
9362 };
9363 "#},
9364 cx,
9365 );
9366 });
9367 editor.update_in(cx, |editor, window, cx| {
9368 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9369 });
9370 editor.update(cx, |editor, cx| {
9371 assert_text_with_selections(
9372 editor,
9373 indoc! {r#"
9374 let a = «ˇ{
9375 key: "value",
9376 }»;
9377 "#},
9378 cx,
9379 );
9380 });
9381
9382 // Test case 2: Cursor after ':'
9383 editor.update_in(cx, |editor, window, cx| {
9384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9385 s.select_display_ranges([
9386 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9387 ]);
9388 });
9389 });
9390 editor.update(cx, |editor, cx| {
9391 assert_text_with_selections(
9392 editor,
9393 indoc! {r#"
9394 let a = {
9395 key:ˇ "value",
9396 };
9397 "#},
9398 cx,
9399 );
9400 });
9401 editor.update_in(cx, |editor, window, cx| {
9402 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9403 });
9404 editor.update(cx, |editor, cx| {
9405 assert_text_with_selections(
9406 editor,
9407 indoc! {r#"
9408 let a = {
9409 «ˇkey: "value"»,
9410 };
9411 "#},
9412 cx,
9413 );
9414 });
9415 editor.update_in(cx, |editor, window, cx| {
9416 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9417 });
9418 editor.update(cx, |editor, cx| {
9419 assert_text_with_selections(
9420 editor,
9421 indoc! {r#"
9422 let a = «ˇ{
9423 key: "value",
9424 }»;
9425 "#},
9426 cx,
9427 );
9428 });
9429
9430 // Test case 3: Cursor after ','
9431 editor.update_in(cx, |editor, window, cx| {
9432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9433 s.select_display_ranges([
9434 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9435 ]);
9436 });
9437 });
9438 editor.update(cx, |editor, cx| {
9439 assert_text_with_selections(
9440 editor,
9441 indoc! {r#"
9442 let a = {
9443 key: "value",ˇ
9444 };
9445 "#},
9446 cx,
9447 );
9448 });
9449 editor.update_in(cx, |editor, window, cx| {
9450 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9451 });
9452 editor.update(cx, |editor, cx| {
9453 assert_text_with_selections(
9454 editor,
9455 indoc! {r#"
9456 let a = «ˇ{
9457 key: "value",
9458 }»;
9459 "#},
9460 cx,
9461 );
9462 });
9463
9464 // Test case 4: Cursor after ';'
9465 editor.update_in(cx, |editor, window, cx| {
9466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9467 s.select_display_ranges([
9468 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9469 ]);
9470 });
9471 });
9472 editor.update(cx, |editor, cx| {
9473 assert_text_with_selections(
9474 editor,
9475 indoc! {r#"
9476 let a = {
9477 key: "value",
9478 };ˇ
9479 "#},
9480 cx,
9481 );
9482 });
9483 editor.update_in(cx, |editor, window, cx| {
9484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9485 });
9486 editor.update(cx, |editor, cx| {
9487 assert_text_with_selections(
9488 editor,
9489 indoc! {r#"
9490 «ˇlet a = {
9491 key: "value",
9492 };
9493 »"#},
9494 cx,
9495 );
9496 });
9497}
9498
9499#[gpui::test]
9500async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9501 init_test(cx, |_| {});
9502
9503 let language = Arc::new(Language::new(
9504 LanguageConfig::default(),
9505 Some(tree_sitter_rust::LANGUAGE.into()),
9506 ));
9507
9508 let text = r#"
9509 use mod1::mod2::{mod3, mod4};
9510
9511 fn fn_1(param1: bool, param2: &str) {
9512 let var1 = "hello world";
9513 }
9514 "#
9515 .unindent();
9516
9517 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9518 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9519 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9520
9521 editor
9522 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9523 .await;
9524
9525 // Test 1: Cursor on a letter of a string word
9526 editor.update_in(cx, |editor, window, cx| {
9527 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9528 s.select_display_ranges([
9529 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9530 ]);
9531 });
9532 });
9533 editor.update_in(cx, |editor, window, cx| {
9534 assert_text_with_selections(
9535 editor,
9536 indoc! {r#"
9537 use mod1::mod2::{mod3, mod4};
9538
9539 fn fn_1(param1: bool, param2: &str) {
9540 let var1 = "hˇello world";
9541 }
9542 "#},
9543 cx,
9544 );
9545 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9546 assert_text_with_selections(
9547 editor,
9548 indoc! {r#"
9549 use mod1::mod2::{mod3, mod4};
9550
9551 fn fn_1(param1: bool, param2: &str) {
9552 let var1 = "«ˇhello» world";
9553 }
9554 "#},
9555 cx,
9556 );
9557 });
9558
9559 // Test 2: Partial selection within a word
9560 editor.update_in(cx, |editor, window, cx| {
9561 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9562 s.select_display_ranges([
9563 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9564 ]);
9565 });
9566 });
9567 editor.update_in(cx, |editor, window, cx| {
9568 assert_text_with_selections(
9569 editor,
9570 indoc! {r#"
9571 use mod1::mod2::{mod3, mod4};
9572
9573 fn fn_1(param1: bool, param2: &str) {
9574 let var1 = "h«elˇ»lo world";
9575 }
9576 "#},
9577 cx,
9578 );
9579 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9580 assert_text_with_selections(
9581 editor,
9582 indoc! {r#"
9583 use mod1::mod2::{mod3, mod4};
9584
9585 fn fn_1(param1: bool, param2: &str) {
9586 let var1 = "«ˇhello» world";
9587 }
9588 "#},
9589 cx,
9590 );
9591 });
9592
9593 // Test 3: Complete word already selected
9594 editor.update_in(cx, |editor, window, cx| {
9595 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9596 s.select_display_ranges([
9597 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9598 ]);
9599 });
9600 });
9601 editor.update_in(cx, |editor, window, cx| {
9602 assert_text_with_selections(
9603 editor,
9604 indoc! {r#"
9605 use mod1::mod2::{mod3, mod4};
9606
9607 fn fn_1(param1: bool, param2: &str) {
9608 let var1 = "«helloˇ» world";
9609 }
9610 "#},
9611 cx,
9612 );
9613 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9614 assert_text_with_selections(
9615 editor,
9616 indoc! {r#"
9617 use mod1::mod2::{mod3, mod4};
9618
9619 fn fn_1(param1: bool, param2: &str) {
9620 let var1 = "«hello worldˇ»";
9621 }
9622 "#},
9623 cx,
9624 );
9625 });
9626
9627 // Test 4: Selection spanning across words
9628 editor.update_in(cx, |editor, window, cx| {
9629 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9630 s.select_display_ranges([
9631 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9632 ]);
9633 });
9634 });
9635 editor.update_in(cx, |editor, window, cx| {
9636 assert_text_with_selections(
9637 editor,
9638 indoc! {r#"
9639 use mod1::mod2::{mod3, mod4};
9640
9641 fn fn_1(param1: bool, param2: &str) {
9642 let var1 = "hel«lo woˇ»rld";
9643 }
9644 "#},
9645 cx,
9646 );
9647 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9648 assert_text_with_selections(
9649 editor,
9650 indoc! {r#"
9651 use mod1::mod2::{mod3, mod4};
9652
9653 fn fn_1(param1: bool, param2: &str) {
9654 let var1 = "«ˇhello world»";
9655 }
9656 "#},
9657 cx,
9658 );
9659 });
9660
9661 // Test 5: Expansion beyond string
9662 editor.update_in(cx, |editor, window, cx| {
9663 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9664 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9665 assert_text_with_selections(
9666 editor,
9667 indoc! {r#"
9668 use mod1::mod2::{mod3, mod4};
9669
9670 fn fn_1(param1: bool, param2: &str) {
9671 «ˇlet var1 = "hello world";»
9672 }
9673 "#},
9674 cx,
9675 );
9676 });
9677}
9678
9679#[gpui::test]
9680async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9681 init_test(cx, |_| {});
9682
9683 let mut cx = EditorTestContext::new(cx).await;
9684
9685 let language = Arc::new(Language::new(
9686 LanguageConfig::default(),
9687 Some(tree_sitter_rust::LANGUAGE.into()),
9688 ));
9689
9690 cx.update_buffer(|buffer, cx| {
9691 buffer.set_language(Some(language), cx);
9692 });
9693
9694 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9695 cx.update_editor(|editor, window, cx| {
9696 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9697 });
9698
9699 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9700
9701 cx.set_state(indoc! { r#"fn a() {
9702 // what
9703 // a
9704 // ˇlong
9705 // method
9706 // I
9707 // sure
9708 // hope
9709 // it
9710 // works
9711 }"# });
9712
9713 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9714 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9715 cx.update(|_, cx| {
9716 multi_buffer.update(cx, |multi_buffer, cx| {
9717 multi_buffer.set_excerpts_for_path(
9718 PathKey::for_buffer(&buffer, cx),
9719 buffer,
9720 [Point::new(1, 0)..Point::new(1, 0)],
9721 3,
9722 cx,
9723 );
9724 });
9725 });
9726
9727 let editor2 = cx.new_window_entity(|window, cx| {
9728 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9729 });
9730
9731 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9732 cx.update_editor(|editor, window, cx| {
9733 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9734 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9735 })
9736 });
9737
9738 cx.assert_editor_state(indoc! { "
9739 fn a() {
9740 // what
9741 // a
9742 ˇ // long
9743 // method"});
9744
9745 cx.update_editor(|editor, window, cx| {
9746 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9747 });
9748
9749 // Although we could potentially make the action work when the syntax node
9750 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9751 // did. Maybe we could also expand the excerpt to contain the range?
9752 cx.assert_editor_state(indoc! { "
9753 fn a() {
9754 // what
9755 // a
9756 ˇ // long
9757 // method"});
9758}
9759
9760#[gpui::test]
9761async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9762 init_test(cx, |_| {});
9763
9764 let base_text = r#"
9765 impl A {
9766 // this is an uncommitted comment
9767
9768 fn b() {
9769 c();
9770 }
9771
9772 // this is another uncommitted comment
9773
9774 fn d() {
9775 // e
9776 // f
9777 }
9778 }
9779
9780 fn g() {
9781 // h
9782 }
9783 "#
9784 .unindent();
9785
9786 let text = r#"
9787 ˇimpl A {
9788
9789 fn b() {
9790 c();
9791 }
9792
9793 fn d() {
9794 // e
9795 // f
9796 }
9797 }
9798
9799 fn g() {
9800 // h
9801 }
9802 "#
9803 .unindent();
9804
9805 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9806 cx.set_state(&text);
9807 cx.set_head_text(&base_text);
9808 cx.update_editor(|editor, window, cx| {
9809 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9810 });
9811
9812 cx.assert_state_with_diff(
9813 "
9814 ˇimpl A {
9815 - // this is an uncommitted comment
9816
9817 fn b() {
9818 c();
9819 }
9820
9821 - // this is another uncommitted comment
9822 -
9823 fn d() {
9824 // e
9825 // f
9826 }
9827 }
9828
9829 fn g() {
9830 // h
9831 }
9832 "
9833 .unindent(),
9834 );
9835
9836 let expected_display_text = "
9837 impl A {
9838 // this is an uncommitted comment
9839
9840 fn b() {
9841 ⋯
9842 }
9843
9844 // this is another uncommitted comment
9845
9846 fn d() {
9847 ⋯
9848 }
9849 }
9850
9851 fn g() {
9852 ⋯
9853 }
9854 "
9855 .unindent();
9856
9857 cx.update_editor(|editor, window, cx| {
9858 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9859 assert_eq!(editor.display_text(cx), expected_display_text);
9860 });
9861}
9862
9863#[gpui::test]
9864async fn test_autoindent(cx: &mut TestAppContext) {
9865 init_test(cx, |_| {});
9866
9867 let language = Arc::new(
9868 Language::new(
9869 LanguageConfig {
9870 brackets: BracketPairConfig {
9871 pairs: vec![
9872 BracketPair {
9873 start: "{".to_string(),
9874 end: "}".to_string(),
9875 close: false,
9876 surround: false,
9877 newline: true,
9878 },
9879 BracketPair {
9880 start: "(".to_string(),
9881 end: ")".to_string(),
9882 close: false,
9883 surround: false,
9884 newline: true,
9885 },
9886 ],
9887 ..Default::default()
9888 },
9889 ..Default::default()
9890 },
9891 Some(tree_sitter_rust::LANGUAGE.into()),
9892 )
9893 .with_indents_query(
9894 r#"
9895 (_ "(" ")" @end) @indent
9896 (_ "{" "}" @end) @indent
9897 "#,
9898 )
9899 .unwrap(),
9900 );
9901
9902 let text = "fn a() {}";
9903
9904 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9905 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9906 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9907 editor
9908 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9909 .await;
9910
9911 editor.update_in(cx, |editor, window, cx| {
9912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9913 s.select_ranges([
9914 MultiBufferOffset(5)..MultiBufferOffset(5),
9915 MultiBufferOffset(8)..MultiBufferOffset(8),
9916 MultiBufferOffset(9)..MultiBufferOffset(9),
9917 ])
9918 });
9919 editor.newline(&Newline, window, cx);
9920 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9921 assert_eq!(
9922 editor.selections.ranges(&editor.display_snapshot(cx)),
9923 &[
9924 Point::new(1, 4)..Point::new(1, 4),
9925 Point::new(3, 4)..Point::new(3, 4),
9926 Point::new(5, 0)..Point::new(5, 0)
9927 ]
9928 );
9929 });
9930}
9931
9932#[gpui::test]
9933async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9934 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9935
9936 let language = Arc::new(
9937 Language::new(
9938 LanguageConfig {
9939 brackets: BracketPairConfig {
9940 pairs: vec![
9941 BracketPair {
9942 start: "{".to_string(),
9943 end: "}".to_string(),
9944 close: false,
9945 surround: false,
9946 newline: true,
9947 },
9948 BracketPair {
9949 start: "(".to_string(),
9950 end: ")".to_string(),
9951 close: false,
9952 surround: false,
9953 newline: true,
9954 },
9955 ],
9956 ..Default::default()
9957 },
9958 ..Default::default()
9959 },
9960 Some(tree_sitter_rust::LANGUAGE.into()),
9961 )
9962 .with_indents_query(
9963 r#"
9964 (_ "(" ")" @end) @indent
9965 (_ "{" "}" @end) @indent
9966 "#,
9967 )
9968 .unwrap(),
9969 );
9970
9971 let text = "fn a() {}";
9972
9973 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9974 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9975 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9976 editor
9977 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9978 .await;
9979
9980 editor.update_in(cx, |editor, window, cx| {
9981 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9982 s.select_ranges([
9983 MultiBufferOffset(5)..MultiBufferOffset(5),
9984 MultiBufferOffset(8)..MultiBufferOffset(8),
9985 MultiBufferOffset(9)..MultiBufferOffset(9),
9986 ])
9987 });
9988 editor.newline(&Newline, window, cx);
9989 assert_eq!(
9990 editor.text(cx),
9991 indoc!(
9992 "
9993 fn a(
9994
9995 ) {
9996
9997 }
9998 "
9999 )
10000 );
10001 assert_eq!(
10002 editor.selections.ranges(&editor.display_snapshot(cx)),
10003 &[
10004 Point::new(1, 0)..Point::new(1, 0),
10005 Point::new(3, 0)..Point::new(3, 0),
10006 Point::new(5, 0)..Point::new(5, 0)
10007 ]
10008 );
10009 });
10010}
10011
10012#[gpui::test]
10013async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10014 init_test(cx, |settings| {
10015 settings.defaults.auto_indent = Some(true);
10016 settings.languages.0.insert(
10017 "python".into(),
10018 LanguageSettingsContent {
10019 auto_indent: Some(false),
10020 ..Default::default()
10021 },
10022 );
10023 });
10024
10025 let mut cx = EditorTestContext::new(cx).await;
10026
10027 let injected_language = Arc::new(
10028 Language::new(
10029 LanguageConfig {
10030 brackets: BracketPairConfig {
10031 pairs: vec![
10032 BracketPair {
10033 start: "{".to_string(),
10034 end: "}".to_string(),
10035 close: false,
10036 surround: false,
10037 newline: true,
10038 },
10039 BracketPair {
10040 start: "(".to_string(),
10041 end: ")".to_string(),
10042 close: true,
10043 surround: false,
10044 newline: true,
10045 },
10046 ],
10047 ..Default::default()
10048 },
10049 name: "python".into(),
10050 ..Default::default()
10051 },
10052 Some(tree_sitter_python::LANGUAGE.into()),
10053 )
10054 .with_indents_query(
10055 r#"
10056 (_ "(" ")" @end) @indent
10057 (_ "{" "}" @end) @indent
10058 "#,
10059 )
10060 .unwrap(),
10061 );
10062
10063 let language = Arc::new(
10064 Language::new(
10065 LanguageConfig {
10066 brackets: BracketPairConfig {
10067 pairs: vec![
10068 BracketPair {
10069 start: "{".to_string(),
10070 end: "}".to_string(),
10071 close: false,
10072 surround: false,
10073 newline: true,
10074 },
10075 BracketPair {
10076 start: "(".to_string(),
10077 end: ")".to_string(),
10078 close: true,
10079 surround: false,
10080 newline: true,
10081 },
10082 ],
10083 ..Default::default()
10084 },
10085 name: LanguageName::new_static("rust"),
10086 ..Default::default()
10087 },
10088 Some(tree_sitter_rust::LANGUAGE.into()),
10089 )
10090 .with_indents_query(
10091 r#"
10092 (_ "(" ")" @end) @indent
10093 (_ "{" "}" @end) @indent
10094 "#,
10095 )
10096 .unwrap()
10097 .with_injection_query(
10098 r#"
10099 (macro_invocation
10100 macro: (identifier) @_macro_name
10101 (token_tree) @injection.content
10102 (#set! injection.language "python"))
10103 "#,
10104 )
10105 .unwrap(),
10106 );
10107
10108 cx.language_registry().add(injected_language);
10109 cx.language_registry().add(language.clone());
10110
10111 cx.update_buffer(|buffer, cx| {
10112 buffer.set_language(Some(language), cx);
10113 });
10114
10115 cx.set_state(r#"struct A {ˇ}"#);
10116
10117 cx.update_editor(|editor, window, cx| {
10118 editor.newline(&Default::default(), window, cx);
10119 });
10120
10121 cx.assert_editor_state(indoc!(
10122 "struct A {
10123 ˇ
10124 }"
10125 ));
10126
10127 cx.set_state(r#"select_biased!(ˇ)"#);
10128
10129 cx.update_editor(|editor, window, cx| {
10130 editor.newline(&Default::default(), window, cx);
10131 editor.handle_input("def ", window, cx);
10132 editor.handle_input("(", window, cx);
10133 editor.newline(&Default::default(), window, cx);
10134 editor.handle_input("a", window, cx);
10135 });
10136
10137 cx.assert_editor_state(indoc!(
10138 "select_biased!(
10139 def (
10140 aˇ
10141 )
10142 )"
10143 ));
10144}
10145
10146#[gpui::test]
10147async fn test_autoindent_selections(cx: &mut TestAppContext) {
10148 init_test(cx, |_| {});
10149
10150 {
10151 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10152 cx.set_state(indoc! {"
10153 impl A {
10154
10155 fn b() {}
10156
10157 «fn c() {
10158
10159 }ˇ»
10160 }
10161 "});
10162
10163 cx.update_editor(|editor, window, cx| {
10164 editor.autoindent(&Default::default(), window, cx);
10165 });
10166
10167 cx.assert_editor_state(indoc! {"
10168 impl A {
10169
10170 fn b() {}
10171
10172 «fn c() {
10173
10174 }ˇ»
10175 }
10176 "});
10177 }
10178
10179 {
10180 let mut cx = EditorTestContext::new_multibuffer(
10181 cx,
10182 [indoc! { "
10183 impl A {
10184 «
10185 // a
10186 fn b(){}
10187 »
10188 «
10189 }
10190 fn c(){}
10191 »
10192 "}],
10193 );
10194
10195 let buffer = cx.update_editor(|editor, _, cx| {
10196 let buffer = editor.buffer().update(cx, |buffer, _| {
10197 buffer.all_buffers().iter().next().unwrap().clone()
10198 });
10199 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10200 buffer
10201 });
10202
10203 cx.run_until_parked();
10204 cx.update_editor(|editor, window, cx| {
10205 editor.select_all(&Default::default(), window, cx);
10206 editor.autoindent(&Default::default(), window, cx)
10207 });
10208 cx.run_until_parked();
10209
10210 cx.update(|_, cx| {
10211 assert_eq!(
10212 buffer.read(cx).text(),
10213 indoc! { "
10214 impl A {
10215
10216 // a
10217 fn b(){}
10218
10219
10220 }
10221 fn c(){}
10222
10223 " }
10224 )
10225 });
10226 }
10227}
10228
10229#[gpui::test]
10230async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10231 init_test(cx, |_| {});
10232
10233 let mut cx = EditorTestContext::new(cx).await;
10234
10235 let language = Arc::new(Language::new(
10236 LanguageConfig {
10237 brackets: BracketPairConfig {
10238 pairs: vec![
10239 BracketPair {
10240 start: "{".to_string(),
10241 end: "}".to_string(),
10242 close: true,
10243 surround: true,
10244 newline: true,
10245 },
10246 BracketPair {
10247 start: "(".to_string(),
10248 end: ")".to_string(),
10249 close: true,
10250 surround: true,
10251 newline: true,
10252 },
10253 BracketPair {
10254 start: "/*".to_string(),
10255 end: " */".to_string(),
10256 close: true,
10257 surround: true,
10258 newline: true,
10259 },
10260 BracketPair {
10261 start: "[".to_string(),
10262 end: "]".to_string(),
10263 close: false,
10264 surround: false,
10265 newline: true,
10266 },
10267 BracketPair {
10268 start: "\"".to_string(),
10269 end: "\"".to_string(),
10270 close: true,
10271 surround: true,
10272 newline: false,
10273 },
10274 BracketPair {
10275 start: "<".to_string(),
10276 end: ">".to_string(),
10277 close: false,
10278 surround: true,
10279 newline: true,
10280 },
10281 ],
10282 ..Default::default()
10283 },
10284 autoclose_before: "})]".to_string(),
10285 ..Default::default()
10286 },
10287 Some(tree_sitter_rust::LANGUAGE.into()),
10288 ));
10289
10290 cx.language_registry().add(language.clone());
10291 cx.update_buffer(|buffer, cx| {
10292 buffer.set_language(Some(language), cx);
10293 });
10294
10295 cx.set_state(
10296 &r#"
10297 🏀ˇ
10298 εˇ
10299 ❤️ˇ
10300 "#
10301 .unindent(),
10302 );
10303
10304 // autoclose multiple nested brackets at multiple cursors
10305 cx.update_editor(|editor, window, cx| {
10306 editor.handle_input("{", window, cx);
10307 editor.handle_input("{", window, cx);
10308 editor.handle_input("{", window, cx);
10309 });
10310 cx.assert_editor_state(
10311 &"
10312 🏀{{{ˇ}}}
10313 ε{{{ˇ}}}
10314 ❤️{{{ˇ}}}
10315 "
10316 .unindent(),
10317 );
10318
10319 // insert a different closing bracket
10320 cx.update_editor(|editor, window, cx| {
10321 editor.handle_input(")", window, cx);
10322 });
10323 cx.assert_editor_state(
10324 &"
10325 🏀{{{)ˇ}}}
10326 ε{{{)ˇ}}}
10327 ❤️{{{)ˇ}}}
10328 "
10329 .unindent(),
10330 );
10331
10332 // skip over the auto-closed brackets when typing a closing bracket
10333 cx.update_editor(|editor, window, cx| {
10334 editor.move_right(&MoveRight, window, cx);
10335 editor.handle_input("}", window, cx);
10336 editor.handle_input("}", window, cx);
10337 editor.handle_input("}", window, cx);
10338 });
10339 cx.assert_editor_state(
10340 &"
10341 🏀{{{)}}}}ˇ
10342 ε{{{)}}}}ˇ
10343 ❤️{{{)}}}}ˇ
10344 "
10345 .unindent(),
10346 );
10347
10348 // autoclose multi-character pairs
10349 cx.set_state(
10350 &"
10351 ˇ
10352 ˇ
10353 "
10354 .unindent(),
10355 );
10356 cx.update_editor(|editor, window, cx| {
10357 editor.handle_input("/", window, cx);
10358 editor.handle_input("*", window, cx);
10359 });
10360 cx.assert_editor_state(
10361 &"
10362 /*ˇ */
10363 /*ˇ */
10364 "
10365 .unindent(),
10366 );
10367
10368 // one cursor autocloses a multi-character pair, one cursor
10369 // does not autoclose.
10370 cx.set_state(
10371 &"
10372 /ˇ
10373 ˇ
10374 "
10375 .unindent(),
10376 );
10377 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10378 cx.assert_editor_state(
10379 &"
10380 /*ˇ */
10381 *ˇ
10382 "
10383 .unindent(),
10384 );
10385
10386 // Don't autoclose if the next character isn't whitespace and isn't
10387 // listed in the language's "autoclose_before" section.
10388 cx.set_state("ˇa b");
10389 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10390 cx.assert_editor_state("{ˇa b");
10391
10392 // Don't autoclose if `close` is false for the bracket pair
10393 cx.set_state("ˇ");
10394 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10395 cx.assert_editor_state("[ˇ");
10396
10397 // Surround with brackets if text is selected
10398 cx.set_state("«aˇ» b");
10399 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10400 cx.assert_editor_state("{«aˇ»} b");
10401
10402 // Autoclose when not immediately after a word character
10403 cx.set_state("a ˇ");
10404 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10405 cx.assert_editor_state("a \"ˇ\"");
10406
10407 // Autoclose pair where the start and end characters are the same
10408 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10409 cx.assert_editor_state("a \"\"ˇ");
10410
10411 // Don't autoclose when immediately after a word character
10412 cx.set_state("aˇ");
10413 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10414 cx.assert_editor_state("a\"ˇ");
10415
10416 // Do autoclose when after a non-word character
10417 cx.set_state("{ˇ");
10418 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10419 cx.assert_editor_state("{\"ˇ\"");
10420
10421 // Non identical pairs autoclose regardless of preceding character
10422 cx.set_state("aˇ");
10423 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10424 cx.assert_editor_state("a{ˇ}");
10425
10426 // Don't autoclose pair if autoclose is disabled
10427 cx.set_state("ˇ");
10428 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10429 cx.assert_editor_state("<ˇ");
10430
10431 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10432 cx.set_state("«aˇ» b");
10433 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10434 cx.assert_editor_state("<«aˇ»> b");
10435}
10436
10437#[gpui::test]
10438async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10439 init_test(cx, |settings| {
10440 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10441 });
10442
10443 let mut cx = EditorTestContext::new(cx).await;
10444
10445 let language = Arc::new(Language::new(
10446 LanguageConfig {
10447 brackets: BracketPairConfig {
10448 pairs: vec![
10449 BracketPair {
10450 start: "{".to_string(),
10451 end: "}".to_string(),
10452 close: true,
10453 surround: true,
10454 newline: true,
10455 },
10456 BracketPair {
10457 start: "(".to_string(),
10458 end: ")".to_string(),
10459 close: true,
10460 surround: true,
10461 newline: true,
10462 },
10463 BracketPair {
10464 start: "[".to_string(),
10465 end: "]".to_string(),
10466 close: false,
10467 surround: false,
10468 newline: true,
10469 },
10470 ],
10471 ..Default::default()
10472 },
10473 autoclose_before: "})]".to_string(),
10474 ..Default::default()
10475 },
10476 Some(tree_sitter_rust::LANGUAGE.into()),
10477 ));
10478
10479 cx.language_registry().add(language.clone());
10480 cx.update_buffer(|buffer, cx| {
10481 buffer.set_language(Some(language), cx);
10482 });
10483
10484 cx.set_state(
10485 &"
10486 ˇ
10487 ˇ
10488 ˇ
10489 "
10490 .unindent(),
10491 );
10492
10493 // ensure only matching closing brackets are skipped over
10494 cx.update_editor(|editor, window, cx| {
10495 editor.handle_input("}", window, cx);
10496 editor.move_left(&MoveLeft, window, cx);
10497 editor.handle_input(")", window, cx);
10498 editor.move_left(&MoveLeft, window, cx);
10499 });
10500 cx.assert_editor_state(
10501 &"
10502 ˇ)}
10503 ˇ)}
10504 ˇ)}
10505 "
10506 .unindent(),
10507 );
10508
10509 // skip-over closing brackets at multiple cursors
10510 cx.update_editor(|editor, window, cx| {
10511 editor.handle_input(")", window, cx);
10512 editor.handle_input("}", window, cx);
10513 });
10514 cx.assert_editor_state(
10515 &"
10516 )}ˇ
10517 )}ˇ
10518 )}ˇ
10519 "
10520 .unindent(),
10521 );
10522
10523 // ignore non-close brackets
10524 cx.update_editor(|editor, window, cx| {
10525 editor.handle_input("]", window, cx);
10526 editor.move_left(&MoveLeft, window, cx);
10527 editor.handle_input("]", window, cx);
10528 });
10529 cx.assert_editor_state(
10530 &"
10531 )}]ˇ]
10532 )}]ˇ]
10533 )}]ˇ]
10534 "
10535 .unindent(),
10536 );
10537}
10538
10539#[gpui::test]
10540async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10541 init_test(cx, |_| {});
10542
10543 let mut cx = EditorTestContext::new(cx).await;
10544
10545 let html_language = Arc::new(
10546 Language::new(
10547 LanguageConfig {
10548 name: "HTML".into(),
10549 brackets: BracketPairConfig {
10550 pairs: vec![
10551 BracketPair {
10552 start: "<".into(),
10553 end: ">".into(),
10554 close: true,
10555 ..Default::default()
10556 },
10557 BracketPair {
10558 start: "{".into(),
10559 end: "}".into(),
10560 close: true,
10561 ..Default::default()
10562 },
10563 BracketPair {
10564 start: "(".into(),
10565 end: ")".into(),
10566 close: true,
10567 ..Default::default()
10568 },
10569 ],
10570 ..Default::default()
10571 },
10572 autoclose_before: "})]>".into(),
10573 ..Default::default()
10574 },
10575 Some(tree_sitter_html::LANGUAGE.into()),
10576 )
10577 .with_injection_query(
10578 r#"
10579 (script_element
10580 (raw_text) @injection.content
10581 (#set! injection.language "javascript"))
10582 "#,
10583 )
10584 .unwrap(),
10585 );
10586
10587 let javascript_language = Arc::new(Language::new(
10588 LanguageConfig {
10589 name: "JavaScript".into(),
10590 brackets: BracketPairConfig {
10591 pairs: vec![
10592 BracketPair {
10593 start: "/*".into(),
10594 end: " */".into(),
10595 close: true,
10596 ..Default::default()
10597 },
10598 BracketPair {
10599 start: "{".into(),
10600 end: "}".into(),
10601 close: true,
10602 ..Default::default()
10603 },
10604 BracketPair {
10605 start: "(".into(),
10606 end: ")".into(),
10607 close: true,
10608 ..Default::default()
10609 },
10610 ],
10611 ..Default::default()
10612 },
10613 autoclose_before: "})]>".into(),
10614 ..Default::default()
10615 },
10616 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10617 ));
10618
10619 cx.language_registry().add(html_language.clone());
10620 cx.language_registry().add(javascript_language);
10621 cx.executor().run_until_parked();
10622
10623 cx.update_buffer(|buffer, cx| {
10624 buffer.set_language(Some(html_language), cx);
10625 });
10626
10627 cx.set_state(
10628 &r#"
10629 <body>ˇ
10630 <script>
10631 var x = 1;ˇ
10632 </script>
10633 </body>ˇ
10634 "#
10635 .unindent(),
10636 );
10637
10638 // Precondition: different languages are active at different locations.
10639 cx.update_editor(|editor, window, cx| {
10640 let snapshot = editor.snapshot(window, cx);
10641 let cursors = editor
10642 .selections
10643 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10644 let languages = cursors
10645 .iter()
10646 .map(|c| snapshot.language_at(c.start).unwrap().name())
10647 .collect::<Vec<_>>();
10648 assert_eq!(
10649 languages,
10650 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10651 );
10652 });
10653
10654 // Angle brackets autoclose in HTML, but not JavaScript.
10655 cx.update_editor(|editor, window, cx| {
10656 editor.handle_input("<", window, cx);
10657 editor.handle_input("a", window, cx);
10658 });
10659 cx.assert_editor_state(
10660 &r#"
10661 <body><aˇ>
10662 <script>
10663 var x = 1;<aˇ
10664 </script>
10665 </body><aˇ>
10666 "#
10667 .unindent(),
10668 );
10669
10670 // Curly braces and parens autoclose in both HTML and JavaScript.
10671 cx.update_editor(|editor, window, cx| {
10672 editor.handle_input(" b=", window, cx);
10673 editor.handle_input("{", window, cx);
10674 editor.handle_input("c", window, cx);
10675 editor.handle_input("(", window, cx);
10676 });
10677 cx.assert_editor_state(
10678 &r#"
10679 <body><a b={c(ˇ)}>
10680 <script>
10681 var x = 1;<a b={c(ˇ)}
10682 </script>
10683 </body><a b={c(ˇ)}>
10684 "#
10685 .unindent(),
10686 );
10687
10688 // Brackets that were already autoclosed are skipped.
10689 cx.update_editor(|editor, window, cx| {
10690 editor.handle_input(")", window, cx);
10691 editor.handle_input("d", window, cx);
10692 editor.handle_input("}", window, cx);
10693 });
10694 cx.assert_editor_state(
10695 &r#"
10696 <body><a b={c()d}ˇ>
10697 <script>
10698 var x = 1;<a b={c()d}ˇ
10699 </script>
10700 </body><a b={c()d}ˇ>
10701 "#
10702 .unindent(),
10703 );
10704 cx.update_editor(|editor, window, cx| {
10705 editor.handle_input(">", window, cx);
10706 });
10707 cx.assert_editor_state(
10708 &r#"
10709 <body><a b={c()d}>ˇ
10710 <script>
10711 var x = 1;<a b={c()d}>ˇ
10712 </script>
10713 </body><a b={c()d}>ˇ
10714 "#
10715 .unindent(),
10716 );
10717
10718 // Reset
10719 cx.set_state(
10720 &r#"
10721 <body>ˇ
10722 <script>
10723 var x = 1;ˇ
10724 </script>
10725 </body>ˇ
10726 "#
10727 .unindent(),
10728 );
10729
10730 cx.update_editor(|editor, window, cx| {
10731 editor.handle_input("<", window, cx);
10732 });
10733 cx.assert_editor_state(
10734 &r#"
10735 <body><ˇ>
10736 <script>
10737 var x = 1;<ˇ
10738 </script>
10739 </body><ˇ>
10740 "#
10741 .unindent(),
10742 );
10743
10744 // When backspacing, the closing angle brackets are removed.
10745 cx.update_editor(|editor, window, cx| {
10746 editor.backspace(&Backspace, window, cx);
10747 });
10748 cx.assert_editor_state(
10749 &r#"
10750 <body>ˇ
10751 <script>
10752 var x = 1;ˇ
10753 </script>
10754 </body>ˇ
10755 "#
10756 .unindent(),
10757 );
10758
10759 // Block comments autoclose in JavaScript, but not HTML.
10760 cx.update_editor(|editor, window, cx| {
10761 editor.handle_input("/", window, cx);
10762 editor.handle_input("*", window, cx);
10763 });
10764 cx.assert_editor_state(
10765 &r#"
10766 <body>/*ˇ
10767 <script>
10768 var x = 1;/*ˇ */
10769 </script>
10770 </body>/*ˇ
10771 "#
10772 .unindent(),
10773 );
10774}
10775
10776#[gpui::test]
10777async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10778 init_test(cx, |_| {});
10779
10780 let mut cx = EditorTestContext::new(cx).await;
10781
10782 let rust_language = Arc::new(
10783 Language::new(
10784 LanguageConfig {
10785 name: "Rust".into(),
10786 brackets: serde_json::from_value(json!([
10787 { "start": "{", "end": "}", "close": true, "newline": true },
10788 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10789 ]))
10790 .unwrap(),
10791 autoclose_before: "})]>".into(),
10792 ..Default::default()
10793 },
10794 Some(tree_sitter_rust::LANGUAGE.into()),
10795 )
10796 .with_override_query("(string_literal) @string")
10797 .unwrap(),
10798 );
10799
10800 cx.language_registry().add(rust_language.clone());
10801 cx.update_buffer(|buffer, cx| {
10802 buffer.set_language(Some(rust_language), cx);
10803 });
10804
10805 cx.set_state(
10806 &r#"
10807 let x = ˇ
10808 "#
10809 .unindent(),
10810 );
10811
10812 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10813 cx.update_editor(|editor, window, cx| {
10814 editor.handle_input("\"", window, cx);
10815 });
10816 cx.assert_editor_state(
10817 &r#"
10818 let x = "ˇ"
10819 "#
10820 .unindent(),
10821 );
10822
10823 // Inserting another quotation mark. The cursor moves across the existing
10824 // automatically-inserted quotation mark.
10825 cx.update_editor(|editor, window, cx| {
10826 editor.handle_input("\"", window, cx);
10827 });
10828 cx.assert_editor_state(
10829 &r#"
10830 let x = ""ˇ
10831 "#
10832 .unindent(),
10833 );
10834
10835 // Reset
10836 cx.set_state(
10837 &r#"
10838 let x = ˇ
10839 "#
10840 .unindent(),
10841 );
10842
10843 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10844 cx.update_editor(|editor, window, cx| {
10845 editor.handle_input("\"", window, cx);
10846 editor.handle_input(" ", window, cx);
10847 editor.move_left(&Default::default(), window, cx);
10848 editor.handle_input("\\", window, cx);
10849 editor.handle_input("\"", window, cx);
10850 });
10851 cx.assert_editor_state(
10852 &r#"
10853 let x = "\"ˇ "
10854 "#
10855 .unindent(),
10856 );
10857
10858 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10859 // mark. Nothing is inserted.
10860 cx.update_editor(|editor, window, cx| {
10861 editor.move_right(&Default::default(), window, cx);
10862 editor.handle_input("\"", window, cx);
10863 });
10864 cx.assert_editor_state(
10865 &r#"
10866 let x = "\" "ˇ
10867 "#
10868 .unindent(),
10869 );
10870}
10871
10872#[gpui::test]
10873async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
10874 init_test(cx, |_| {});
10875
10876 let mut cx = EditorTestContext::new(cx).await;
10877 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
10878
10879 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10880
10881 // Double quote inside single-quoted string
10882 cx.set_state(indoc! {r#"
10883 def main():
10884 items = ['"', ˇ]
10885 "#});
10886 cx.update_editor(|editor, window, cx| {
10887 editor.handle_input("\"", window, cx);
10888 });
10889 cx.assert_editor_state(indoc! {r#"
10890 def main():
10891 items = ['"', "ˇ"]
10892 "#});
10893
10894 // Two double quotes inside single-quoted string
10895 cx.set_state(indoc! {r#"
10896 def main():
10897 items = ['""', ˇ]
10898 "#});
10899 cx.update_editor(|editor, window, cx| {
10900 editor.handle_input("\"", window, cx);
10901 });
10902 cx.assert_editor_state(indoc! {r#"
10903 def main():
10904 items = ['""', "ˇ"]
10905 "#});
10906
10907 // Single quote inside double-quoted string
10908 cx.set_state(indoc! {r#"
10909 def main():
10910 items = ["'", ˇ]
10911 "#});
10912 cx.update_editor(|editor, window, cx| {
10913 editor.handle_input("'", window, cx);
10914 });
10915 cx.assert_editor_state(indoc! {r#"
10916 def main():
10917 items = ["'", 'ˇ']
10918 "#});
10919
10920 // Two single quotes inside double-quoted string
10921 cx.set_state(indoc! {r#"
10922 def main():
10923 items = ["''", ˇ]
10924 "#});
10925 cx.update_editor(|editor, window, cx| {
10926 editor.handle_input("'", window, cx);
10927 });
10928 cx.assert_editor_state(indoc! {r#"
10929 def main():
10930 items = ["''", 'ˇ']
10931 "#});
10932
10933 // Mixed quotes on same line
10934 cx.set_state(indoc! {r#"
10935 def main():
10936 items = ['"""', "'''''", ˇ]
10937 "#});
10938 cx.update_editor(|editor, window, cx| {
10939 editor.handle_input("\"", window, cx);
10940 });
10941 cx.assert_editor_state(indoc! {r#"
10942 def main():
10943 items = ['"""', "'''''", "ˇ"]
10944 "#});
10945 cx.update_editor(|editor, window, cx| {
10946 editor.move_right(&MoveRight, window, cx);
10947 });
10948 cx.update_editor(|editor, window, cx| {
10949 editor.handle_input(", ", window, cx);
10950 });
10951 cx.update_editor(|editor, window, cx| {
10952 editor.handle_input("'", window, cx);
10953 });
10954 cx.assert_editor_state(indoc! {r#"
10955 def main():
10956 items = ['"""', "'''''", "", 'ˇ']
10957 "#});
10958}
10959
10960#[gpui::test]
10961async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
10962 init_test(cx, |_| {});
10963
10964 let mut cx = EditorTestContext::new(cx).await;
10965 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
10966 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10967
10968 cx.set_state(indoc! {r#"
10969 def main():
10970 items = ["🎉", ˇ]
10971 "#});
10972 cx.update_editor(|editor, window, cx| {
10973 editor.handle_input("\"", window, cx);
10974 });
10975 cx.assert_editor_state(indoc! {r#"
10976 def main():
10977 items = ["🎉", "ˇ"]
10978 "#});
10979}
10980
10981#[gpui::test]
10982async fn test_surround_with_pair(cx: &mut TestAppContext) {
10983 init_test(cx, |_| {});
10984
10985 let language = Arc::new(Language::new(
10986 LanguageConfig {
10987 brackets: BracketPairConfig {
10988 pairs: vec![
10989 BracketPair {
10990 start: "{".to_string(),
10991 end: "}".to_string(),
10992 close: true,
10993 surround: true,
10994 newline: true,
10995 },
10996 BracketPair {
10997 start: "/* ".to_string(),
10998 end: "*/".to_string(),
10999 close: true,
11000 surround: true,
11001 ..Default::default()
11002 },
11003 ],
11004 ..Default::default()
11005 },
11006 ..Default::default()
11007 },
11008 Some(tree_sitter_rust::LANGUAGE.into()),
11009 ));
11010
11011 let text = r#"
11012 a
11013 b
11014 c
11015 "#
11016 .unindent();
11017
11018 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11019 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11020 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11021 editor
11022 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11023 .await;
11024
11025 editor.update_in(cx, |editor, window, cx| {
11026 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11027 s.select_display_ranges([
11028 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11029 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11030 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11031 ])
11032 });
11033
11034 editor.handle_input("{", window, cx);
11035 editor.handle_input("{", window, cx);
11036 editor.handle_input("{", window, cx);
11037 assert_eq!(
11038 editor.text(cx),
11039 "
11040 {{{a}}}
11041 {{{b}}}
11042 {{{c}}}
11043 "
11044 .unindent()
11045 );
11046 assert_eq!(
11047 display_ranges(editor, cx),
11048 [
11049 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11050 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11051 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11052 ]
11053 );
11054
11055 editor.undo(&Undo, window, cx);
11056 editor.undo(&Undo, window, cx);
11057 editor.undo(&Undo, window, cx);
11058 assert_eq!(
11059 editor.text(cx),
11060 "
11061 a
11062 b
11063 c
11064 "
11065 .unindent()
11066 );
11067 assert_eq!(
11068 display_ranges(editor, cx),
11069 [
11070 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11071 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11072 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11073 ]
11074 );
11075
11076 // Ensure inserting the first character of a multi-byte bracket pair
11077 // doesn't surround the selections with the bracket.
11078 editor.handle_input("/", window, cx);
11079 assert_eq!(
11080 editor.text(cx),
11081 "
11082 /
11083 /
11084 /
11085 "
11086 .unindent()
11087 );
11088 assert_eq!(
11089 display_ranges(editor, cx),
11090 [
11091 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11092 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11093 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11094 ]
11095 );
11096
11097 editor.undo(&Undo, window, cx);
11098 assert_eq!(
11099 editor.text(cx),
11100 "
11101 a
11102 b
11103 c
11104 "
11105 .unindent()
11106 );
11107 assert_eq!(
11108 display_ranges(editor, cx),
11109 [
11110 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11111 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11112 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11113 ]
11114 );
11115
11116 // Ensure inserting the last character of a multi-byte bracket pair
11117 // doesn't surround the selections with the bracket.
11118 editor.handle_input("*", window, cx);
11119 assert_eq!(
11120 editor.text(cx),
11121 "
11122 *
11123 *
11124 *
11125 "
11126 .unindent()
11127 );
11128 assert_eq!(
11129 display_ranges(editor, cx),
11130 [
11131 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11132 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11133 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11134 ]
11135 );
11136 });
11137}
11138
11139#[gpui::test]
11140async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11141 init_test(cx, |_| {});
11142
11143 let language = Arc::new(Language::new(
11144 LanguageConfig {
11145 brackets: BracketPairConfig {
11146 pairs: vec![BracketPair {
11147 start: "{".to_string(),
11148 end: "}".to_string(),
11149 close: true,
11150 surround: true,
11151 newline: true,
11152 }],
11153 ..Default::default()
11154 },
11155 autoclose_before: "}".to_string(),
11156 ..Default::default()
11157 },
11158 Some(tree_sitter_rust::LANGUAGE.into()),
11159 ));
11160
11161 let text = r#"
11162 a
11163 b
11164 c
11165 "#
11166 .unindent();
11167
11168 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11169 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11170 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11171 editor
11172 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11173 .await;
11174
11175 editor.update_in(cx, |editor, window, cx| {
11176 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11177 s.select_ranges([
11178 Point::new(0, 1)..Point::new(0, 1),
11179 Point::new(1, 1)..Point::new(1, 1),
11180 Point::new(2, 1)..Point::new(2, 1),
11181 ])
11182 });
11183
11184 editor.handle_input("{", window, cx);
11185 editor.handle_input("{", window, cx);
11186 editor.handle_input("_", window, cx);
11187 assert_eq!(
11188 editor.text(cx),
11189 "
11190 a{{_}}
11191 b{{_}}
11192 c{{_}}
11193 "
11194 .unindent()
11195 );
11196 assert_eq!(
11197 editor
11198 .selections
11199 .ranges::<Point>(&editor.display_snapshot(cx)),
11200 [
11201 Point::new(0, 4)..Point::new(0, 4),
11202 Point::new(1, 4)..Point::new(1, 4),
11203 Point::new(2, 4)..Point::new(2, 4)
11204 ]
11205 );
11206
11207 editor.backspace(&Default::default(), window, cx);
11208 editor.backspace(&Default::default(), window, cx);
11209 assert_eq!(
11210 editor.text(cx),
11211 "
11212 a{}
11213 b{}
11214 c{}
11215 "
11216 .unindent()
11217 );
11218 assert_eq!(
11219 editor
11220 .selections
11221 .ranges::<Point>(&editor.display_snapshot(cx)),
11222 [
11223 Point::new(0, 2)..Point::new(0, 2),
11224 Point::new(1, 2)..Point::new(1, 2),
11225 Point::new(2, 2)..Point::new(2, 2)
11226 ]
11227 );
11228
11229 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11230 assert_eq!(
11231 editor.text(cx),
11232 "
11233 a
11234 b
11235 c
11236 "
11237 .unindent()
11238 );
11239 assert_eq!(
11240 editor
11241 .selections
11242 .ranges::<Point>(&editor.display_snapshot(cx)),
11243 [
11244 Point::new(0, 1)..Point::new(0, 1),
11245 Point::new(1, 1)..Point::new(1, 1),
11246 Point::new(2, 1)..Point::new(2, 1)
11247 ]
11248 );
11249 });
11250}
11251
11252#[gpui::test]
11253async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11254 init_test(cx, |settings| {
11255 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11256 });
11257
11258 let mut cx = EditorTestContext::new(cx).await;
11259
11260 let language = Arc::new(Language::new(
11261 LanguageConfig {
11262 brackets: BracketPairConfig {
11263 pairs: vec![
11264 BracketPair {
11265 start: "{".to_string(),
11266 end: "}".to_string(),
11267 close: true,
11268 surround: true,
11269 newline: true,
11270 },
11271 BracketPair {
11272 start: "(".to_string(),
11273 end: ")".to_string(),
11274 close: true,
11275 surround: true,
11276 newline: true,
11277 },
11278 BracketPair {
11279 start: "[".to_string(),
11280 end: "]".to_string(),
11281 close: false,
11282 surround: true,
11283 newline: true,
11284 },
11285 ],
11286 ..Default::default()
11287 },
11288 autoclose_before: "})]".to_string(),
11289 ..Default::default()
11290 },
11291 Some(tree_sitter_rust::LANGUAGE.into()),
11292 ));
11293
11294 cx.language_registry().add(language.clone());
11295 cx.update_buffer(|buffer, cx| {
11296 buffer.set_language(Some(language), cx);
11297 });
11298
11299 cx.set_state(
11300 &"
11301 {(ˇ)}
11302 [[ˇ]]
11303 {(ˇ)}
11304 "
11305 .unindent(),
11306 );
11307
11308 cx.update_editor(|editor, window, cx| {
11309 editor.backspace(&Default::default(), window, cx);
11310 editor.backspace(&Default::default(), window, cx);
11311 });
11312
11313 cx.assert_editor_state(
11314 &"
11315 ˇ
11316 ˇ]]
11317 ˇ
11318 "
11319 .unindent(),
11320 );
11321
11322 cx.update_editor(|editor, window, cx| {
11323 editor.handle_input("{", window, cx);
11324 editor.handle_input("{", window, cx);
11325 editor.move_right(&MoveRight, window, cx);
11326 editor.move_right(&MoveRight, window, cx);
11327 editor.move_left(&MoveLeft, window, cx);
11328 editor.move_left(&MoveLeft, window, cx);
11329 editor.backspace(&Default::default(), window, cx);
11330 });
11331
11332 cx.assert_editor_state(
11333 &"
11334 {ˇ}
11335 {ˇ}]]
11336 {ˇ}
11337 "
11338 .unindent(),
11339 );
11340
11341 cx.update_editor(|editor, window, cx| {
11342 editor.backspace(&Default::default(), window, cx);
11343 });
11344
11345 cx.assert_editor_state(
11346 &"
11347 ˇ
11348 ˇ]]
11349 ˇ
11350 "
11351 .unindent(),
11352 );
11353}
11354
11355#[gpui::test]
11356async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11357 init_test(cx, |_| {});
11358
11359 let language = Arc::new(Language::new(
11360 LanguageConfig::default(),
11361 Some(tree_sitter_rust::LANGUAGE.into()),
11362 ));
11363
11364 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11365 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11366 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11367 editor
11368 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11369 .await;
11370
11371 editor.update_in(cx, |editor, window, cx| {
11372 editor.set_auto_replace_emoji_shortcode(true);
11373
11374 editor.handle_input("Hello ", window, cx);
11375 editor.handle_input(":wave", window, cx);
11376 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11377
11378 editor.handle_input(":", window, cx);
11379 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11380
11381 editor.handle_input(" :smile", window, cx);
11382 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11383
11384 editor.handle_input(":", window, cx);
11385 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11386
11387 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11388 editor.handle_input(":wave", window, cx);
11389 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11390
11391 editor.handle_input(":", window, cx);
11392 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11393
11394 editor.handle_input(":1", window, cx);
11395 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11396
11397 editor.handle_input(":", window, cx);
11398 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11399
11400 // Ensure shortcode does not get replaced when it is part of a word
11401 editor.handle_input(" Test:wave", window, cx);
11402 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11403
11404 editor.handle_input(":", window, cx);
11405 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11406
11407 editor.set_auto_replace_emoji_shortcode(false);
11408
11409 // Ensure shortcode does not get replaced when auto replace is off
11410 editor.handle_input(" :wave", window, cx);
11411 assert_eq!(
11412 editor.text(cx),
11413 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11414 );
11415
11416 editor.handle_input(":", window, cx);
11417 assert_eq!(
11418 editor.text(cx),
11419 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11420 );
11421 });
11422}
11423
11424#[gpui::test]
11425async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11426 init_test(cx, |_| {});
11427
11428 let (text, insertion_ranges) = marked_text_ranges(
11429 indoc! {"
11430 ˇ
11431 "},
11432 false,
11433 );
11434
11435 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11436 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11437
11438 _ = editor.update_in(cx, |editor, window, cx| {
11439 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11440
11441 editor
11442 .insert_snippet(
11443 &insertion_ranges
11444 .iter()
11445 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11446 .collect::<Vec<_>>(),
11447 snippet,
11448 window,
11449 cx,
11450 )
11451 .unwrap();
11452
11453 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11454 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11455 assert_eq!(editor.text(cx), expected_text);
11456 assert_eq!(
11457 editor.selections.ranges(&editor.display_snapshot(cx)),
11458 selection_ranges
11459 .iter()
11460 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11461 .collect::<Vec<_>>()
11462 );
11463 }
11464
11465 assert(
11466 editor,
11467 cx,
11468 indoc! {"
11469 type «» =•
11470 "},
11471 );
11472
11473 assert!(editor.context_menu_visible(), "There should be a matches");
11474 });
11475}
11476
11477#[gpui::test]
11478async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11479 init_test(cx, |_| {});
11480
11481 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11482 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11483 assert_eq!(editor.text(cx), expected_text);
11484 assert_eq!(
11485 editor.selections.ranges(&editor.display_snapshot(cx)),
11486 selection_ranges
11487 .iter()
11488 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11489 .collect::<Vec<_>>()
11490 );
11491 }
11492
11493 let (text, insertion_ranges) = marked_text_ranges(
11494 indoc! {"
11495 ˇ
11496 "},
11497 false,
11498 );
11499
11500 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11501 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11502
11503 _ = editor.update_in(cx, |editor, window, cx| {
11504 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11505
11506 editor
11507 .insert_snippet(
11508 &insertion_ranges
11509 .iter()
11510 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11511 .collect::<Vec<_>>(),
11512 snippet,
11513 window,
11514 cx,
11515 )
11516 .unwrap();
11517
11518 assert_state(
11519 editor,
11520 cx,
11521 indoc! {"
11522 type «» = ;•
11523 "},
11524 );
11525
11526 assert!(
11527 editor.context_menu_visible(),
11528 "Context menu should be visible for placeholder choices"
11529 );
11530
11531 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11532
11533 assert_state(
11534 editor,
11535 cx,
11536 indoc! {"
11537 type = «»;•
11538 "},
11539 );
11540
11541 assert!(
11542 !editor.context_menu_visible(),
11543 "Context menu should be hidden after moving to next tabstop"
11544 );
11545
11546 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11547
11548 assert_state(
11549 editor,
11550 cx,
11551 indoc! {"
11552 type = ; ˇ
11553 "},
11554 );
11555
11556 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11557
11558 assert_state(
11559 editor,
11560 cx,
11561 indoc! {"
11562 type = ; ˇ
11563 "},
11564 );
11565 });
11566
11567 _ = editor.update_in(cx, |editor, window, cx| {
11568 editor.select_all(&SelectAll, window, cx);
11569 editor.backspace(&Backspace, window, cx);
11570
11571 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11572 let insertion_ranges = editor
11573 .selections
11574 .all(&editor.display_snapshot(cx))
11575 .iter()
11576 .map(|s| s.range())
11577 .collect::<Vec<_>>();
11578
11579 editor
11580 .insert_snippet(&insertion_ranges, snippet, window, cx)
11581 .unwrap();
11582
11583 assert_state(editor, cx, "fn «» = value;•");
11584
11585 assert!(
11586 editor.context_menu_visible(),
11587 "Context menu should be visible for placeholder choices"
11588 );
11589
11590 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11591
11592 assert_state(editor, cx, "fn = «valueˇ»;•");
11593
11594 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11595
11596 assert_state(editor, cx, "fn «» = value;•");
11597
11598 assert!(
11599 editor.context_menu_visible(),
11600 "Context menu should be visible again after returning to first tabstop"
11601 );
11602
11603 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11604
11605 assert_state(editor, cx, "fn «» = value;•");
11606 });
11607}
11608
11609#[gpui::test]
11610async fn test_snippets(cx: &mut TestAppContext) {
11611 init_test(cx, |_| {});
11612
11613 let mut cx = EditorTestContext::new(cx).await;
11614
11615 cx.set_state(indoc! {"
11616 a.ˇ b
11617 a.ˇ b
11618 a.ˇ b
11619 "});
11620
11621 cx.update_editor(|editor, window, cx| {
11622 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11623 let insertion_ranges = editor
11624 .selections
11625 .all(&editor.display_snapshot(cx))
11626 .iter()
11627 .map(|s| s.range())
11628 .collect::<Vec<_>>();
11629 editor
11630 .insert_snippet(&insertion_ranges, snippet, window, cx)
11631 .unwrap();
11632 });
11633
11634 cx.assert_editor_state(indoc! {"
11635 a.f(«oneˇ», two, «threeˇ») b
11636 a.f(«oneˇ», two, «threeˇ») b
11637 a.f(«oneˇ», two, «threeˇ») b
11638 "});
11639
11640 // Can't move earlier than the first tab stop
11641 cx.update_editor(|editor, window, cx| {
11642 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11643 });
11644 cx.assert_editor_state(indoc! {"
11645 a.f(«oneˇ», two, «threeˇ») b
11646 a.f(«oneˇ», two, «threeˇ») b
11647 a.f(«oneˇ», two, «threeˇ») b
11648 "});
11649
11650 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11651 cx.assert_editor_state(indoc! {"
11652 a.f(one, «twoˇ», three) b
11653 a.f(one, «twoˇ», three) b
11654 a.f(one, «twoˇ», three) b
11655 "});
11656
11657 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11658 cx.assert_editor_state(indoc! {"
11659 a.f(«oneˇ», two, «threeˇ») b
11660 a.f(«oneˇ», two, «threeˇ») b
11661 a.f(«oneˇ», two, «threeˇ») b
11662 "});
11663
11664 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11665 cx.assert_editor_state(indoc! {"
11666 a.f(one, «twoˇ», three) b
11667 a.f(one, «twoˇ», three) b
11668 a.f(one, «twoˇ», three) b
11669 "});
11670 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11671 cx.assert_editor_state(indoc! {"
11672 a.f(one, two, three)ˇ b
11673 a.f(one, two, three)ˇ b
11674 a.f(one, two, three)ˇ b
11675 "});
11676
11677 // As soon as the last tab stop is reached, snippet state is gone
11678 cx.update_editor(|editor, window, cx| {
11679 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11680 });
11681 cx.assert_editor_state(indoc! {"
11682 a.f(one, two, three)ˇ b
11683 a.f(one, two, three)ˇ b
11684 a.f(one, two, three)ˇ b
11685 "});
11686}
11687
11688#[gpui::test]
11689async fn test_snippet_indentation(cx: &mut TestAppContext) {
11690 init_test(cx, |_| {});
11691
11692 let mut cx = EditorTestContext::new(cx).await;
11693
11694 cx.update_editor(|editor, window, cx| {
11695 let snippet = Snippet::parse(indoc! {"
11696 /*
11697 * Multiline comment with leading indentation
11698 *
11699 * $1
11700 */
11701 $0"})
11702 .unwrap();
11703 let insertion_ranges = editor
11704 .selections
11705 .all(&editor.display_snapshot(cx))
11706 .iter()
11707 .map(|s| s.range())
11708 .collect::<Vec<_>>();
11709 editor
11710 .insert_snippet(&insertion_ranges, snippet, window, cx)
11711 .unwrap();
11712 });
11713
11714 cx.assert_editor_state(indoc! {"
11715 /*
11716 * Multiline comment with leading indentation
11717 *
11718 * ˇ
11719 */
11720 "});
11721
11722 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11723 cx.assert_editor_state(indoc! {"
11724 /*
11725 * Multiline comment with leading indentation
11726 *
11727 *•
11728 */
11729 ˇ"});
11730}
11731
11732#[gpui::test]
11733async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11734 init_test(cx, |_| {});
11735
11736 let mut cx = EditorTestContext::new(cx).await;
11737 cx.update_editor(|editor, _, cx| {
11738 editor.project().unwrap().update(cx, |project, cx| {
11739 project.snippets().update(cx, |snippets, _cx| {
11740 let snippet = project::snippet_provider::Snippet {
11741 prefix: vec!["multi word".to_string()],
11742 body: "this is many words".to_string(),
11743 description: Some("description".to_string()),
11744 name: "multi-word snippet test".to_string(),
11745 };
11746 snippets.add_snippet_for_test(
11747 None,
11748 PathBuf::from("test_snippets.json"),
11749 vec![Arc::new(snippet)],
11750 );
11751 });
11752 })
11753 });
11754
11755 for (input_to_simulate, should_match_snippet) in [
11756 ("m", true),
11757 ("m ", true),
11758 ("m w", true),
11759 ("aa m w", true),
11760 ("aa m g", false),
11761 ] {
11762 cx.set_state("ˇ");
11763 cx.simulate_input(input_to_simulate); // fails correctly
11764
11765 cx.update_editor(|editor, _, _| {
11766 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11767 else {
11768 assert!(!should_match_snippet); // no completions! don't even show the menu
11769 return;
11770 };
11771 assert!(context_menu.visible());
11772 let completions = context_menu.completions.borrow();
11773
11774 assert_eq!(!completions.is_empty(), should_match_snippet);
11775 });
11776 }
11777}
11778
11779#[gpui::test]
11780async fn test_document_format_during_save(cx: &mut TestAppContext) {
11781 init_test(cx, |_| {});
11782
11783 let fs = FakeFs::new(cx.executor());
11784 fs.insert_file(path!("/file.rs"), Default::default()).await;
11785
11786 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11787
11788 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11789 language_registry.add(rust_lang());
11790 let mut fake_servers = language_registry.register_fake_lsp(
11791 "Rust",
11792 FakeLspAdapter {
11793 capabilities: lsp::ServerCapabilities {
11794 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11795 ..Default::default()
11796 },
11797 ..Default::default()
11798 },
11799 );
11800
11801 let buffer = project
11802 .update(cx, |project, cx| {
11803 project.open_local_buffer(path!("/file.rs"), cx)
11804 })
11805 .await
11806 .unwrap();
11807
11808 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11809 let (editor, cx) = cx.add_window_view(|window, cx| {
11810 build_editor_with_project(project.clone(), buffer, window, cx)
11811 });
11812 editor.update_in(cx, |editor, window, cx| {
11813 editor.set_text("one\ntwo\nthree\n", window, cx)
11814 });
11815 assert!(cx.read(|cx| editor.is_dirty(cx)));
11816
11817 cx.executor().start_waiting();
11818 let fake_server = fake_servers.next().await.unwrap();
11819
11820 {
11821 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11822 move |params, _| async move {
11823 assert_eq!(
11824 params.text_document.uri,
11825 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11826 );
11827 assert_eq!(params.options.tab_size, 4);
11828 Ok(Some(vec![lsp::TextEdit::new(
11829 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11830 ", ".to_string(),
11831 )]))
11832 },
11833 );
11834 let save = editor
11835 .update_in(cx, |editor, window, cx| {
11836 editor.save(
11837 SaveOptions {
11838 format: true,
11839 autosave: false,
11840 },
11841 project.clone(),
11842 window,
11843 cx,
11844 )
11845 })
11846 .unwrap();
11847 cx.executor().start_waiting();
11848 save.await;
11849
11850 assert_eq!(
11851 editor.update(cx, |editor, cx| editor.text(cx)),
11852 "one, two\nthree\n"
11853 );
11854 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11855 }
11856
11857 {
11858 editor.update_in(cx, |editor, window, cx| {
11859 editor.set_text("one\ntwo\nthree\n", window, cx)
11860 });
11861 assert!(cx.read(|cx| editor.is_dirty(cx)));
11862
11863 // Ensure we can still save even if formatting hangs.
11864 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11865 move |params, _| async move {
11866 assert_eq!(
11867 params.text_document.uri,
11868 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11869 );
11870 futures::future::pending::<()>().await;
11871 unreachable!()
11872 },
11873 );
11874 let save = editor
11875 .update_in(cx, |editor, window, cx| {
11876 editor.save(
11877 SaveOptions {
11878 format: true,
11879 autosave: false,
11880 },
11881 project.clone(),
11882 window,
11883 cx,
11884 )
11885 })
11886 .unwrap();
11887 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11888 cx.executor().start_waiting();
11889 save.await;
11890 assert_eq!(
11891 editor.update(cx, |editor, cx| editor.text(cx)),
11892 "one\ntwo\nthree\n"
11893 );
11894 }
11895
11896 // Set rust language override and assert overridden tabsize is sent to language server
11897 update_test_language_settings(cx, |settings| {
11898 settings.languages.0.insert(
11899 "Rust".into(),
11900 LanguageSettingsContent {
11901 tab_size: NonZeroU32::new(8),
11902 ..Default::default()
11903 },
11904 );
11905 });
11906
11907 {
11908 editor.update_in(cx, |editor, window, cx| {
11909 editor.set_text("somehting_new\n", window, cx)
11910 });
11911 assert!(cx.read(|cx| editor.is_dirty(cx)));
11912 let _formatting_request_signal = fake_server
11913 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11914 assert_eq!(
11915 params.text_document.uri,
11916 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11917 );
11918 assert_eq!(params.options.tab_size, 8);
11919 Ok(Some(vec![]))
11920 });
11921 let save = editor
11922 .update_in(cx, |editor, window, cx| {
11923 editor.save(
11924 SaveOptions {
11925 format: true,
11926 autosave: false,
11927 },
11928 project.clone(),
11929 window,
11930 cx,
11931 )
11932 })
11933 .unwrap();
11934 cx.executor().start_waiting();
11935 save.await;
11936 }
11937}
11938
11939#[gpui::test]
11940async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11941 init_test(cx, |settings| {
11942 settings.defaults.ensure_final_newline_on_save = Some(false);
11943 });
11944
11945 let fs = FakeFs::new(cx.executor());
11946 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11947
11948 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11949
11950 let buffer = project
11951 .update(cx, |project, cx| {
11952 project.open_local_buffer(path!("/file.txt"), cx)
11953 })
11954 .await
11955 .unwrap();
11956
11957 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11958 let (editor, cx) = cx.add_window_view(|window, cx| {
11959 build_editor_with_project(project.clone(), buffer, window, cx)
11960 });
11961 editor.update_in(cx, |editor, window, cx| {
11962 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11963 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11964 });
11965 });
11966 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11967
11968 editor.update_in(cx, |editor, window, cx| {
11969 editor.handle_input("\n", window, cx)
11970 });
11971 cx.run_until_parked();
11972 save(&editor, &project, cx).await;
11973 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11974
11975 editor.update_in(cx, |editor, window, cx| {
11976 editor.undo(&Default::default(), window, cx);
11977 });
11978 save(&editor, &project, cx).await;
11979 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11980
11981 editor.update_in(cx, |editor, window, cx| {
11982 editor.redo(&Default::default(), window, cx);
11983 });
11984 cx.run_until_parked();
11985 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11986
11987 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11988 let save = editor
11989 .update_in(cx, |editor, window, cx| {
11990 editor.save(
11991 SaveOptions {
11992 format: true,
11993 autosave: false,
11994 },
11995 project.clone(),
11996 window,
11997 cx,
11998 )
11999 })
12000 .unwrap();
12001 cx.executor().start_waiting();
12002 save.await;
12003 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12004 }
12005}
12006
12007#[gpui::test]
12008async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12009 init_test(cx, |_| {});
12010
12011 let cols = 4;
12012 let rows = 10;
12013 let sample_text_1 = sample_text(rows, cols, 'a');
12014 assert_eq!(
12015 sample_text_1,
12016 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12017 );
12018 let sample_text_2 = sample_text(rows, cols, 'l');
12019 assert_eq!(
12020 sample_text_2,
12021 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12022 );
12023 let sample_text_3 = sample_text(rows, cols, 'v');
12024 assert_eq!(
12025 sample_text_3,
12026 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12027 );
12028
12029 let fs = FakeFs::new(cx.executor());
12030 fs.insert_tree(
12031 path!("/a"),
12032 json!({
12033 "main.rs": sample_text_1,
12034 "other.rs": sample_text_2,
12035 "lib.rs": sample_text_3,
12036 }),
12037 )
12038 .await;
12039
12040 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12041 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12042 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12043
12044 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12045 language_registry.add(rust_lang());
12046 let mut fake_servers = language_registry.register_fake_lsp(
12047 "Rust",
12048 FakeLspAdapter {
12049 capabilities: lsp::ServerCapabilities {
12050 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12051 ..Default::default()
12052 },
12053 ..Default::default()
12054 },
12055 );
12056
12057 let worktree = project.update(cx, |project, cx| {
12058 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12059 assert_eq!(worktrees.len(), 1);
12060 worktrees.pop().unwrap()
12061 });
12062 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12063
12064 let buffer_1 = project
12065 .update(cx, |project, cx| {
12066 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12067 })
12068 .await
12069 .unwrap();
12070 let buffer_2 = project
12071 .update(cx, |project, cx| {
12072 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12073 })
12074 .await
12075 .unwrap();
12076 let buffer_3 = project
12077 .update(cx, |project, cx| {
12078 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12079 })
12080 .await
12081 .unwrap();
12082
12083 let multi_buffer = cx.new(|cx| {
12084 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12085 multi_buffer.push_excerpts(
12086 buffer_1.clone(),
12087 [
12088 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12089 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12090 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12091 ],
12092 cx,
12093 );
12094 multi_buffer.push_excerpts(
12095 buffer_2.clone(),
12096 [
12097 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12098 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12099 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12100 ],
12101 cx,
12102 );
12103 multi_buffer.push_excerpts(
12104 buffer_3.clone(),
12105 [
12106 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12107 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12108 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12109 ],
12110 cx,
12111 );
12112 multi_buffer
12113 });
12114 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12115 Editor::new(
12116 EditorMode::full(),
12117 multi_buffer,
12118 Some(project.clone()),
12119 window,
12120 cx,
12121 )
12122 });
12123
12124 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12125 editor.change_selections(
12126 SelectionEffects::scroll(Autoscroll::Next),
12127 window,
12128 cx,
12129 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12130 );
12131 editor.insert("|one|two|three|", window, cx);
12132 });
12133 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12134 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12135 editor.change_selections(
12136 SelectionEffects::scroll(Autoscroll::Next),
12137 window,
12138 cx,
12139 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12140 );
12141 editor.insert("|four|five|six|", window, cx);
12142 });
12143 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12144
12145 // First two buffers should be edited, but not the third one.
12146 assert_eq!(
12147 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12148 "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}",
12149 );
12150 buffer_1.update(cx, |buffer, _| {
12151 assert!(buffer.is_dirty());
12152 assert_eq!(
12153 buffer.text(),
12154 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12155 )
12156 });
12157 buffer_2.update(cx, |buffer, _| {
12158 assert!(buffer.is_dirty());
12159 assert_eq!(
12160 buffer.text(),
12161 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12162 )
12163 });
12164 buffer_3.update(cx, |buffer, _| {
12165 assert!(!buffer.is_dirty());
12166 assert_eq!(buffer.text(), sample_text_3,)
12167 });
12168 cx.executor().run_until_parked();
12169
12170 cx.executor().start_waiting();
12171 let save = multi_buffer_editor
12172 .update_in(cx, |editor, window, cx| {
12173 editor.save(
12174 SaveOptions {
12175 format: true,
12176 autosave: false,
12177 },
12178 project.clone(),
12179 window,
12180 cx,
12181 )
12182 })
12183 .unwrap();
12184
12185 let fake_server = fake_servers.next().await.unwrap();
12186 fake_server
12187 .server
12188 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12189 Ok(Some(vec![lsp::TextEdit::new(
12190 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12191 format!("[{} formatted]", params.text_document.uri),
12192 )]))
12193 })
12194 .detach();
12195 save.await;
12196
12197 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12198 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12199 assert_eq!(
12200 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12201 uri!(
12202 "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}"
12203 ),
12204 );
12205 buffer_1.update(cx, |buffer, _| {
12206 assert!(!buffer.is_dirty());
12207 assert_eq!(
12208 buffer.text(),
12209 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12210 )
12211 });
12212 buffer_2.update(cx, |buffer, _| {
12213 assert!(!buffer.is_dirty());
12214 assert_eq!(
12215 buffer.text(),
12216 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12217 )
12218 });
12219 buffer_3.update(cx, |buffer, _| {
12220 assert!(!buffer.is_dirty());
12221 assert_eq!(buffer.text(), sample_text_3,)
12222 });
12223}
12224
12225#[gpui::test]
12226async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12227 init_test(cx, |_| {});
12228
12229 let fs = FakeFs::new(cx.executor());
12230 fs.insert_tree(
12231 path!("/dir"),
12232 json!({
12233 "file1.rs": "fn main() { println!(\"hello\"); }",
12234 "file2.rs": "fn test() { println!(\"test\"); }",
12235 "file3.rs": "fn other() { println!(\"other\"); }\n",
12236 }),
12237 )
12238 .await;
12239
12240 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12241 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12242 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12243
12244 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12245 language_registry.add(rust_lang());
12246
12247 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12248 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12249
12250 // Open three buffers
12251 let buffer_1 = project
12252 .update(cx, |project, cx| {
12253 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12254 })
12255 .await
12256 .unwrap();
12257 let buffer_2 = project
12258 .update(cx, |project, cx| {
12259 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12260 })
12261 .await
12262 .unwrap();
12263 let buffer_3 = project
12264 .update(cx, |project, cx| {
12265 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12266 })
12267 .await
12268 .unwrap();
12269
12270 // Create a multi-buffer with all three buffers
12271 let multi_buffer = cx.new(|cx| {
12272 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12273 multi_buffer.push_excerpts(
12274 buffer_1.clone(),
12275 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12276 cx,
12277 );
12278 multi_buffer.push_excerpts(
12279 buffer_2.clone(),
12280 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12281 cx,
12282 );
12283 multi_buffer.push_excerpts(
12284 buffer_3.clone(),
12285 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12286 cx,
12287 );
12288 multi_buffer
12289 });
12290
12291 let editor = cx.new_window_entity(|window, cx| {
12292 Editor::new(
12293 EditorMode::full(),
12294 multi_buffer,
12295 Some(project.clone()),
12296 window,
12297 cx,
12298 )
12299 });
12300
12301 // Edit only the first buffer
12302 editor.update_in(cx, |editor, window, cx| {
12303 editor.change_selections(
12304 SelectionEffects::scroll(Autoscroll::Next),
12305 window,
12306 cx,
12307 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12308 );
12309 editor.insert("// edited", window, cx);
12310 });
12311
12312 // Verify that only buffer 1 is dirty
12313 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12314 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12315 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12316
12317 // Get write counts after file creation (files were created with initial content)
12318 // We expect each file to have been written once during creation
12319 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12320 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12321 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12322
12323 // Perform autosave
12324 let save_task = editor.update_in(cx, |editor, window, cx| {
12325 editor.save(
12326 SaveOptions {
12327 format: true,
12328 autosave: true,
12329 },
12330 project.clone(),
12331 window,
12332 cx,
12333 )
12334 });
12335 save_task.await.unwrap();
12336
12337 // Only the dirty buffer should have been saved
12338 assert_eq!(
12339 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12340 1,
12341 "Buffer 1 was dirty, so it should have been written once during autosave"
12342 );
12343 assert_eq!(
12344 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12345 0,
12346 "Buffer 2 was clean, so it should not have been written during autosave"
12347 );
12348 assert_eq!(
12349 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12350 0,
12351 "Buffer 3 was clean, so it should not have been written during autosave"
12352 );
12353
12354 // Verify buffer states after autosave
12355 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12356 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12357 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12358
12359 // Now perform a manual save (format = true)
12360 let save_task = editor.update_in(cx, |editor, window, cx| {
12361 editor.save(
12362 SaveOptions {
12363 format: true,
12364 autosave: false,
12365 },
12366 project.clone(),
12367 window,
12368 cx,
12369 )
12370 });
12371 save_task.await.unwrap();
12372
12373 // During manual save, clean buffers don't get written to disk
12374 // They just get did_save called for language server notifications
12375 assert_eq!(
12376 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12377 1,
12378 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12379 );
12380 assert_eq!(
12381 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12382 0,
12383 "Buffer 2 should not have been written at all"
12384 );
12385 assert_eq!(
12386 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12387 0,
12388 "Buffer 3 should not have been written at all"
12389 );
12390}
12391
12392async fn setup_range_format_test(
12393 cx: &mut TestAppContext,
12394) -> (
12395 Entity<Project>,
12396 Entity<Editor>,
12397 &mut gpui::VisualTestContext,
12398 lsp::FakeLanguageServer,
12399) {
12400 init_test(cx, |_| {});
12401
12402 let fs = FakeFs::new(cx.executor());
12403 fs.insert_file(path!("/file.rs"), Default::default()).await;
12404
12405 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12406
12407 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12408 language_registry.add(rust_lang());
12409 let mut fake_servers = language_registry.register_fake_lsp(
12410 "Rust",
12411 FakeLspAdapter {
12412 capabilities: lsp::ServerCapabilities {
12413 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12414 ..lsp::ServerCapabilities::default()
12415 },
12416 ..FakeLspAdapter::default()
12417 },
12418 );
12419
12420 let buffer = project
12421 .update(cx, |project, cx| {
12422 project.open_local_buffer(path!("/file.rs"), cx)
12423 })
12424 .await
12425 .unwrap();
12426
12427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12428 let (editor, cx) = cx.add_window_view(|window, cx| {
12429 build_editor_with_project(project.clone(), buffer, window, cx)
12430 });
12431
12432 cx.executor().start_waiting();
12433 let fake_server = fake_servers.next().await.unwrap();
12434
12435 (project, editor, cx, fake_server)
12436}
12437
12438#[gpui::test]
12439async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12440 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12441
12442 editor.update_in(cx, |editor, window, cx| {
12443 editor.set_text("one\ntwo\nthree\n", window, cx)
12444 });
12445 assert!(cx.read(|cx| editor.is_dirty(cx)));
12446
12447 let save = editor
12448 .update_in(cx, |editor, window, cx| {
12449 editor.save(
12450 SaveOptions {
12451 format: true,
12452 autosave: false,
12453 },
12454 project.clone(),
12455 window,
12456 cx,
12457 )
12458 })
12459 .unwrap();
12460 fake_server
12461 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12462 assert_eq!(
12463 params.text_document.uri,
12464 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12465 );
12466 assert_eq!(params.options.tab_size, 4);
12467 Ok(Some(vec![lsp::TextEdit::new(
12468 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12469 ", ".to_string(),
12470 )]))
12471 })
12472 .next()
12473 .await;
12474 cx.executor().start_waiting();
12475 save.await;
12476 assert_eq!(
12477 editor.update(cx, |editor, cx| editor.text(cx)),
12478 "one, two\nthree\n"
12479 );
12480 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12481}
12482
12483#[gpui::test]
12484async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12485 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12486
12487 editor.update_in(cx, |editor, window, cx| {
12488 editor.set_text("one\ntwo\nthree\n", window, cx)
12489 });
12490 assert!(cx.read(|cx| editor.is_dirty(cx)));
12491
12492 // Test that save still works when formatting hangs
12493 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12494 move |params, _| async move {
12495 assert_eq!(
12496 params.text_document.uri,
12497 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12498 );
12499 futures::future::pending::<()>().await;
12500 unreachable!()
12501 },
12502 );
12503 let save = editor
12504 .update_in(cx, |editor, window, cx| {
12505 editor.save(
12506 SaveOptions {
12507 format: true,
12508 autosave: false,
12509 },
12510 project.clone(),
12511 window,
12512 cx,
12513 )
12514 })
12515 .unwrap();
12516 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12517 cx.executor().start_waiting();
12518 save.await;
12519 assert_eq!(
12520 editor.update(cx, |editor, cx| editor.text(cx)),
12521 "one\ntwo\nthree\n"
12522 );
12523 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12524}
12525
12526#[gpui::test]
12527async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12528 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12529
12530 // Buffer starts clean, no formatting should be requested
12531 let save = editor
12532 .update_in(cx, |editor, window, cx| {
12533 editor.save(
12534 SaveOptions {
12535 format: false,
12536 autosave: false,
12537 },
12538 project.clone(),
12539 window,
12540 cx,
12541 )
12542 })
12543 .unwrap();
12544 let _pending_format_request = fake_server
12545 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12546 panic!("Should not be invoked");
12547 })
12548 .next();
12549 cx.executor().start_waiting();
12550 save.await;
12551 cx.run_until_parked();
12552}
12553
12554#[gpui::test]
12555async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12556 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12557
12558 // Set Rust language override and assert overridden tabsize is sent to language server
12559 update_test_language_settings(cx, |settings| {
12560 settings.languages.0.insert(
12561 "Rust".into(),
12562 LanguageSettingsContent {
12563 tab_size: NonZeroU32::new(8),
12564 ..Default::default()
12565 },
12566 );
12567 });
12568
12569 editor.update_in(cx, |editor, window, cx| {
12570 editor.set_text("something_new\n", window, cx)
12571 });
12572 assert!(cx.read(|cx| editor.is_dirty(cx)));
12573 let save = editor
12574 .update_in(cx, |editor, window, cx| {
12575 editor.save(
12576 SaveOptions {
12577 format: true,
12578 autosave: false,
12579 },
12580 project.clone(),
12581 window,
12582 cx,
12583 )
12584 })
12585 .unwrap();
12586 fake_server
12587 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12588 assert_eq!(
12589 params.text_document.uri,
12590 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12591 );
12592 assert_eq!(params.options.tab_size, 8);
12593 Ok(Some(Vec::new()))
12594 })
12595 .next()
12596 .await;
12597 save.await;
12598}
12599
12600#[gpui::test]
12601async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12602 init_test(cx, |settings| {
12603 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12604 settings::LanguageServerFormatterSpecifier::Current,
12605 )))
12606 });
12607
12608 let fs = FakeFs::new(cx.executor());
12609 fs.insert_file(path!("/file.rs"), Default::default()).await;
12610
12611 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12612
12613 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12614 language_registry.add(Arc::new(Language::new(
12615 LanguageConfig {
12616 name: "Rust".into(),
12617 matcher: LanguageMatcher {
12618 path_suffixes: vec!["rs".to_string()],
12619 ..Default::default()
12620 },
12621 ..LanguageConfig::default()
12622 },
12623 Some(tree_sitter_rust::LANGUAGE.into()),
12624 )));
12625 update_test_language_settings(cx, |settings| {
12626 // Enable Prettier formatting for the same buffer, and ensure
12627 // LSP is called instead of Prettier.
12628 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12629 });
12630 let mut fake_servers = language_registry.register_fake_lsp(
12631 "Rust",
12632 FakeLspAdapter {
12633 capabilities: lsp::ServerCapabilities {
12634 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12635 ..Default::default()
12636 },
12637 ..Default::default()
12638 },
12639 );
12640
12641 let buffer = project
12642 .update(cx, |project, cx| {
12643 project.open_local_buffer(path!("/file.rs"), cx)
12644 })
12645 .await
12646 .unwrap();
12647
12648 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12649 let (editor, cx) = cx.add_window_view(|window, cx| {
12650 build_editor_with_project(project.clone(), buffer, window, cx)
12651 });
12652 editor.update_in(cx, |editor, window, cx| {
12653 editor.set_text("one\ntwo\nthree\n", window, cx)
12654 });
12655
12656 cx.executor().start_waiting();
12657 let fake_server = fake_servers.next().await.unwrap();
12658
12659 let format = editor
12660 .update_in(cx, |editor, window, cx| {
12661 editor.perform_format(
12662 project.clone(),
12663 FormatTrigger::Manual,
12664 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12665 window,
12666 cx,
12667 )
12668 })
12669 .unwrap();
12670 fake_server
12671 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12672 assert_eq!(
12673 params.text_document.uri,
12674 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12675 );
12676 assert_eq!(params.options.tab_size, 4);
12677 Ok(Some(vec![lsp::TextEdit::new(
12678 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12679 ", ".to_string(),
12680 )]))
12681 })
12682 .next()
12683 .await;
12684 cx.executor().start_waiting();
12685 format.await;
12686 assert_eq!(
12687 editor.update(cx, |editor, cx| editor.text(cx)),
12688 "one, two\nthree\n"
12689 );
12690
12691 editor.update_in(cx, |editor, window, cx| {
12692 editor.set_text("one\ntwo\nthree\n", window, cx)
12693 });
12694 // Ensure we don't lock if formatting hangs.
12695 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12696 move |params, _| async move {
12697 assert_eq!(
12698 params.text_document.uri,
12699 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12700 );
12701 futures::future::pending::<()>().await;
12702 unreachable!()
12703 },
12704 );
12705 let format = editor
12706 .update_in(cx, |editor, window, cx| {
12707 editor.perform_format(
12708 project,
12709 FormatTrigger::Manual,
12710 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12711 window,
12712 cx,
12713 )
12714 })
12715 .unwrap();
12716 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12717 cx.executor().start_waiting();
12718 format.await;
12719 assert_eq!(
12720 editor.update(cx, |editor, cx| editor.text(cx)),
12721 "one\ntwo\nthree\n"
12722 );
12723}
12724
12725#[gpui::test]
12726async fn test_multiple_formatters(cx: &mut TestAppContext) {
12727 init_test(cx, |settings| {
12728 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12729 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12730 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12731 Formatter::CodeAction("code-action-1".into()),
12732 Formatter::CodeAction("code-action-2".into()),
12733 ]))
12734 });
12735
12736 let fs = FakeFs::new(cx.executor());
12737 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12738 .await;
12739
12740 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12741 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12742 language_registry.add(rust_lang());
12743
12744 let mut fake_servers = language_registry.register_fake_lsp(
12745 "Rust",
12746 FakeLspAdapter {
12747 capabilities: lsp::ServerCapabilities {
12748 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12749 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12750 commands: vec!["the-command-for-code-action-1".into()],
12751 ..Default::default()
12752 }),
12753 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12754 ..Default::default()
12755 },
12756 ..Default::default()
12757 },
12758 );
12759
12760 let buffer = project
12761 .update(cx, |project, cx| {
12762 project.open_local_buffer(path!("/file.rs"), cx)
12763 })
12764 .await
12765 .unwrap();
12766
12767 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12768 let (editor, cx) = cx.add_window_view(|window, cx| {
12769 build_editor_with_project(project.clone(), buffer, window, cx)
12770 });
12771
12772 cx.executor().start_waiting();
12773
12774 let fake_server = fake_servers.next().await.unwrap();
12775 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12776 move |_params, _| async move {
12777 Ok(Some(vec![lsp::TextEdit::new(
12778 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12779 "applied-formatting\n".to_string(),
12780 )]))
12781 },
12782 );
12783 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12784 move |params, _| async move {
12785 let requested_code_actions = params.context.only.expect("Expected code action request");
12786 assert_eq!(requested_code_actions.len(), 1);
12787
12788 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12789 let code_action = match requested_code_actions[0].as_str() {
12790 "code-action-1" => lsp::CodeAction {
12791 kind: Some("code-action-1".into()),
12792 edit: Some(lsp::WorkspaceEdit::new(
12793 [(
12794 uri,
12795 vec![lsp::TextEdit::new(
12796 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12797 "applied-code-action-1-edit\n".to_string(),
12798 )],
12799 )]
12800 .into_iter()
12801 .collect(),
12802 )),
12803 command: Some(lsp::Command {
12804 command: "the-command-for-code-action-1".into(),
12805 ..Default::default()
12806 }),
12807 ..Default::default()
12808 },
12809 "code-action-2" => lsp::CodeAction {
12810 kind: Some("code-action-2".into()),
12811 edit: Some(lsp::WorkspaceEdit::new(
12812 [(
12813 uri,
12814 vec![lsp::TextEdit::new(
12815 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12816 "applied-code-action-2-edit\n".to_string(),
12817 )],
12818 )]
12819 .into_iter()
12820 .collect(),
12821 )),
12822 ..Default::default()
12823 },
12824 req => panic!("Unexpected code action request: {:?}", req),
12825 };
12826 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12827 code_action,
12828 )]))
12829 },
12830 );
12831
12832 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12833 move |params, _| async move { Ok(params) }
12834 });
12835
12836 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12837 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12838 let fake = fake_server.clone();
12839 let lock = command_lock.clone();
12840 move |params, _| {
12841 assert_eq!(params.command, "the-command-for-code-action-1");
12842 let fake = fake.clone();
12843 let lock = lock.clone();
12844 async move {
12845 lock.lock().await;
12846 fake.server
12847 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12848 label: None,
12849 edit: lsp::WorkspaceEdit {
12850 changes: Some(
12851 [(
12852 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12853 vec![lsp::TextEdit {
12854 range: lsp::Range::new(
12855 lsp::Position::new(0, 0),
12856 lsp::Position::new(0, 0),
12857 ),
12858 new_text: "applied-code-action-1-command\n".into(),
12859 }],
12860 )]
12861 .into_iter()
12862 .collect(),
12863 ),
12864 ..Default::default()
12865 },
12866 })
12867 .await
12868 .into_response()
12869 .unwrap();
12870 Ok(Some(json!(null)))
12871 }
12872 }
12873 });
12874
12875 cx.executor().start_waiting();
12876 editor
12877 .update_in(cx, |editor, window, cx| {
12878 editor.perform_format(
12879 project.clone(),
12880 FormatTrigger::Manual,
12881 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12882 window,
12883 cx,
12884 )
12885 })
12886 .unwrap()
12887 .await;
12888 editor.update(cx, |editor, cx| {
12889 assert_eq!(
12890 editor.text(cx),
12891 r#"
12892 applied-code-action-2-edit
12893 applied-code-action-1-command
12894 applied-code-action-1-edit
12895 applied-formatting
12896 one
12897 two
12898 three
12899 "#
12900 .unindent()
12901 );
12902 });
12903
12904 editor.update_in(cx, |editor, window, cx| {
12905 editor.undo(&Default::default(), window, cx);
12906 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12907 });
12908
12909 // Perform a manual edit while waiting for an LSP command
12910 // that's being run as part of a formatting code action.
12911 let lock_guard = command_lock.lock().await;
12912 let format = editor
12913 .update_in(cx, |editor, window, cx| {
12914 editor.perform_format(
12915 project.clone(),
12916 FormatTrigger::Manual,
12917 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12918 window,
12919 cx,
12920 )
12921 })
12922 .unwrap();
12923 cx.run_until_parked();
12924 editor.update(cx, |editor, cx| {
12925 assert_eq!(
12926 editor.text(cx),
12927 r#"
12928 applied-code-action-1-edit
12929 applied-formatting
12930 one
12931 two
12932 three
12933 "#
12934 .unindent()
12935 );
12936
12937 editor.buffer.update(cx, |buffer, cx| {
12938 let ix = buffer.len(cx);
12939 buffer.edit([(ix..ix, "edited\n")], None, cx);
12940 });
12941 });
12942
12943 // Allow the LSP command to proceed. Because the buffer was edited,
12944 // the second code action will not be run.
12945 drop(lock_guard);
12946 format.await;
12947 editor.update_in(cx, |editor, window, cx| {
12948 assert_eq!(
12949 editor.text(cx),
12950 r#"
12951 applied-code-action-1-command
12952 applied-code-action-1-edit
12953 applied-formatting
12954 one
12955 two
12956 three
12957 edited
12958 "#
12959 .unindent()
12960 );
12961
12962 // The manual edit is undone first, because it is the last thing the user did
12963 // (even though the command completed afterwards).
12964 editor.undo(&Default::default(), window, cx);
12965 assert_eq!(
12966 editor.text(cx),
12967 r#"
12968 applied-code-action-1-command
12969 applied-code-action-1-edit
12970 applied-formatting
12971 one
12972 two
12973 three
12974 "#
12975 .unindent()
12976 );
12977
12978 // All the formatting (including the command, which completed after the manual edit)
12979 // is undone together.
12980 editor.undo(&Default::default(), window, cx);
12981 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12982 });
12983}
12984
12985#[gpui::test]
12986async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12987 init_test(cx, |settings| {
12988 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12989 settings::LanguageServerFormatterSpecifier::Current,
12990 )]))
12991 });
12992
12993 let fs = FakeFs::new(cx.executor());
12994 fs.insert_file(path!("/file.ts"), Default::default()).await;
12995
12996 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12997
12998 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12999 language_registry.add(Arc::new(Language::new(
13000 LanguageConfig {
13001 name: "TypeScript".into(),
13002 matcher: LanguageMatcher {
13003 path_suffixes: vec!["ts".to_string()],
13004 ..Default::default()
13005 },
13006 ..LanguageConfig::default()
13007 },
13008 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13009 )));
13010 update_test_language_settings(cx, |settings| {
13011 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13012 });
13013 let mut fake_servers = language_registry.register_fake_lsp(
13014 "TypeScript",
13015 FakeLspAdapter {
13016 capabilities: lsp::ServerCapabilities {
13017 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13018 ..Default::default()
13019 },
13020 ..Default::default()
13021 },
13022 );
13023
13024 let buffer = project
13025 .update(cx, |project, cx| {
13026 project.open_local_buffer(path!("/file.ts"), cx)
13027 })
13028 .await
13029 .unwrap();
13030
13031 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13032 let (editor, cx) = cx.add_window_view(|window, cx| {
13033 build_editor_with_project(project.clone(), buffer, window, cx)
13034 });
13035 editor.update_in(cx, |editor, window, cx| {
13036 editor.set_text(
13037 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13038 window,
13039 cx,
13040 )
13041 });
13042
13043 cx.executor().start_waiting();
13044 let fake_server = fake_servers.next().await.unwrap();
13045
13046 let format = editor
13047 .update_in(cx, |editor, window, cx| {
13048 editor.perform_code_action_kind(
13049 project.clone(),
13050 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13051 window,
13052 cx,
13053 )
13054 })
13055 .unwrap();
13056 fake_server
13057 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13058 assert_eq!(
13059 params.text_document.uri,
13060 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13061 );
13062 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13063 lsp::CodeAction {
13064 title: "Organize Imports".to_string(),
13065 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13066 edit: Some(lsp::WorkspaceEdit {
13067 changes: Some(
13068 [(
13069 params.text_document.uri.clone(),
13070 vec![lsp::TextEdit::new(
13071 lsp::Range::new(
13072 lsp::Position::new(1, 0),
13073 lsp::Position::new(2, 0),
13074 ),
13075 "".to_string(),
13076 )],
13077 )]
13078 .into_iter()
13079 .collect(),
13080 ),
13081 ..Default::default()
13082 }),
13083 ..Default::default()
13084 },
13085 )]))
13086 })
13087 .next()
13088 .await;
13089 cx.executor().start_waiting();
13090 format.await;
13091 assert_eq!(
13092 editor.update(cx, |editor, cx| editor.text(cx)),
13093 "import { a } from 'module';\n\nconst x = a;\n"
13094 );
13095
13096 editor.update_in(cx, |editor, window, cx| {
13097 editor.set_text(
13098 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13099 window,
13100 cx,
13101 )
13102 });
13103 // Ensure we don't lock if code action hangs.
13104 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13105 move |params, _| async move {
13106 assert_eq!(
13107 params.text_document.uri,
13108 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13109 );
13110 futures::future::pending::<()>().await;
13111 unreachable!()
13112 },
13113 );
13114 let format = editor
13115 .update_in(cx, |editor, window, cx| {
13116 editor.perform_code_action_kind(
13117 project,
13118 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13119 window,
13120 cx,
13121 )
13122 })
13123 .unwrap();
13124 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13125 cx.executor().start_waiting();
13126 format.await;
13127 assert_eq!(
13128 editor.update(cx, |editor, cx| editor.text(cx)),
13129 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13130 );
13131}
13132
13133#[gpui::test]
13134async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13135 init_test(cx, |_| {});
13136
13137 let mut cx = EditorLspTestContext::new_rust(
13138 lsp::ServerCapabilities {
13139 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13140 ..Default::default()
13141 },
13142 cx,
13143 )
13144 .await;
13145
13146 cx.set_state(indoc! {"
13147 one.twoˇ
13148 "});
13149
13150 // The format request takes a long time. When it completes, it inserts
13151 // a newline and an indent before the `.`
13152 cx.lsp
13153 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13154 let executor = cx.background_executor().clone();
13155 async move {
13156 executor.timer(Duration::from_millis(100)).await;
13157 Ok(Some(vec![lsp::TextEdit {
13158 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13159 new_text: "\n ".into(),
13160 }]))
13161 }
13162 });
13163
13164 // Submit a format request.
13165 let format_1 = cx
13166 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13167 .unwrap();
13168 cx.executor().run_until_parked();
13169
13170 // Submit a second format request.
13171 let format_2 = cx
13172 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13173 .unwrap();
13174 cx.executor().run_until_parked();
13175
13176 // Wait for both format requests to complete
13177 cx.executor().advance_clock(Duration::from_millis(200));
13178 cx.executor().start_waiting();
13179 format_1.await.unwrap();
13180 cx.executor().start_waiting();
13181 format_2.await.unwrap();
13182
13183 // The formatting edits only happens once.
13184 cx.assert_editor_state(indoc! {"
13185 one
13186 .twoˇ
13187 "});
13188}
13189
13190#[gpui::test]
13191async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13192 init_test(cx, |settings| {
13193 settings.defaults.formatter = Some(FormatterList::default())
13194 });
13195
13196 let mut cx = EditorLspTestContext::new_rust(
13197 lsp::ServerCapabilities {
13198 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13199 ..Default::default()
13200 },
13201 cx,
13202 )
13203 .await;
13204
13205 // Record which buffer changes have been sent to the language server
13206 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13207 cx.lsp
13208 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13209 let buffer_changes = buffer_changes.clone();
13210 move |params, _| {
13211 buffer_changes.lock().extend(
13212 params
13213 .content_changes
13214 .into_iter()
13215 .map(|e| (e.range.unwrap(), e.text)),
13216 );
13217 }
13218 });
13219 // Handle formatting requests to the language server.
13220 cx.lsp
13221 .set_request_handler::<lsp::request::Formatting, _, _>({
13222 let buffer_changes = buffer_changes.clone();
13223 move |_, _| {
13224 let buffer_changes = buffer_changes.clone();
13225 // Insert blank lines between each line of the buffer.
13226 async move {
13227 // When formatting is requested, trailing whitespace has already been stripped,
13228 // and the trailing newline has already been added.
13229 assert_eq!(
13230 &buffer_changes.lock()[1..],
13231 &[
13232 (
13233 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13234 "".into()
13235 ),
13236 (
13237 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13238 "".into()
13239 ),
13240 (
13241 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13242 "\n".into()
13243 ),
13244 ]
13245 );
13246
13247 Ok(Some(vec![
13248 lsp::TextEdit {
13249 range: lsp::Range::new(
13250 lsp::Position::new(1, 0),
13251 lsp::Position::new(1, 0),
13252 ),
13253 new_text: "\n".into(),
13254 },
13255 lsp::TextEdit {
13256 range: lsp::Range::new(
13257 lsp::Position::new(2, 0),
13258 lsp::Position::new(2, 0),
13259 ),
13260 new_text: "\n".into(),
13261 },
13262 ]))
13263 }
13264 }
13265 });
13266
13267 // Set up a buffer white some trailing whitespace and no trailing newline.
13268 cx.set_state(
13269 &[
13270 "one ", //
13271 "twoˇ", //
13272 "three ", //
13273 "four", //
13274 ]
13275 .join("\n"),
13276 );
13277 cx.run_until_parked();
13278
13279 // Submit a format request.
13280 let format = cx
13281 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13282 .unwrap();
13283
13284 cx.run_until_parked();
13285 // After formatting the buffer, the trailing whitespace is stripped,
13286 // a newline is appended, and the edits provided by the language server
13287 // have been applied.
13288 format.await.unwrap();
13289
13290 cx.assert_editor_state(
13291 &[
13292 "one", //
13293 "", //
13294 "twoˇ", //
13295 "", //
13296 "three", //
13297 "four", //
13298 "", //
13299 ]
13300 .join("\n"),
13301 );
13302
13303 // Undoing the formatting undoes the trailing whitespace removal, the
13304 // trailing newline, and the LSP edits.
13305 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13306 cx.assert_editor_state(
13307 &[
13308 "one ", //
13309 "twoˇ", //
13310 "three ", //
13311 "four", //
13312 ]
13313 .join("\n"),
13314 );
13315}
13316
13317#[gpui::test]
13318async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13319 cx: &mut TestAppContext,
13320) {
13321 init_test(cx, |_| {});
13322
13323 cx.update(|cx| {
13324 cx.update_global::<SettingsStore, _>(|settings, cx| {
13325 settings.update_user_settings(cx, |settings| {
13326 settings.editor.auto_signature_help = Some(true);
13327 });
13328 });
13329 });
13330
13331 let mut cx = EditorLspTestContext::new_rust(
13332 lsp::ServerCapabilities {
13333 signature_help_provider: Some(lsp::SignatureHelpOptions {
13334 ..Default::default()
13335 }),
13336 ..Default::default()
13337 },
13338 cx,
13339 )
13340 .await;
13341
13342 let language = Language::new(
13343 LanguageConfig {
13344 name: "Rust".into(),
13345 brackets: BracketPairConfig {
13346 pairs: vec![
13347 BracketPair {
13348 start: "{".to_string(),
13349 end: "}".to_string(),
13350 close: true,
13351 surround: true,
13352 newline: true,
13353 },
13354 BracketPair {
13355 start: "(".to_string(),
13356 end: ")".to_string(),
13357 close: true,
13358 surround: true,
13359 newline: true,
13360 },
13361 BracketPair {
13362 start: "/*".to_string(),
13363 end: " */".to_string(),
13364 close: true,
13365 surround: true,
13366 newline: true,
13367 },
13368 BracketPair {
13369 start: "[".to_string(),
13370 end: "]".to_string(),
13371 close: false,
13372 surround: false,
13373 newline: true,
13374 },
13375 BracketPair {
13376 start: "\"".to_string(),
13377 end: "\"".to_string(),
13378 close: true,
13379 surround: true,
13380 newline: false,
13381 },
13382 BracketPair {
13383 start: "<".to_string(),
13384 end: ">".to_string(),
13385 close: false,
13386 surround: true,
13387 newline: true,
13388 },
13389 ],
13390 ..Default::default()
13391 },
13392 autoclose_before: "})]".to_string(),
13393 ..Default::default()
13394 },
13395 Some(tree_sitter_rust::LANGUAGE.into()),
13396 );
13397 let language = Arc::new(language);
13398
13399 cx.language_registry().add(language.clone());
13400 cx.update_buffer(|buffer, cx| {
13401 buffer.set_language(Some(language), cx);
13402 });
13403
13404 cx.set_state(
13405 &r#"
13406 fn main() {
13407 sampleˇ
13408 }
13409 "#
13410 .unindent(),
13411 );
13412
13413 cx.update_editor(|editor, window, cx| {
13414 editor.handle_input("(", window, cx);
13415 });
13416 cx.assert_editor_state(
13417 &"
13418 fn main() {
13419 sample(ˇ)
13420 }
13421 "
13422 .unindent(),
13423 );
13424
13425 let mocked_response = lsp::SignatureHelp {
13426 signatures: vec![lsp::SignatureInformation {
13427 label: "fn sample(param1: u8, param2: u8)".to_string(),
13428 documentation: None,
13429 parameters: Some(vec![
13430 lsp::ParameterInformation {
13431 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13432 documentation: None,
13433 },
13434 lsp::ParameterInformation {
13435 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13436 documentation: None,
13437 },
13438 ]),
13439 active_parameter: None,
13440 }],
13441 active_signature: Some(0),
13442 active_parameter: Some(0),
13443 };
13444 handle_signature_help_request(&mut cx, mocked_response).await;
13445
13446 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13447 .await;
13448
13449 cx.editor(|editor, _, _| {
13450 let signature_help_state = editor.signature_help_state.popover().cloned();
13451 let signature = signature_help_state.unwrap();
13452 assert_eq!(
13453 signature.signatures[signature.current_signature].label,
13454 "fn sample(param1: u8, param2: u8)"
13455 );
13456 });
13457}
13458
13459#[gpui::test]
13460async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13461 init_test(cx, |_| {});
13462
13463 cx.update(|cx| {
13464 cx.update_global::<SettingsStore, _>(|settings, cx| {
13465 settings.update_user_settings(cx, |settings| {
13466 settings.editor.auto_signature_help = Some(false);
13467 settings.editor.show_signature_help_after_edits = Some(false);
13468 });
13469 });
13470 });
13471
13472 let mut cx = EditorLspTestContext::new_rust(
13473 lsp::ServerCapabilities {
13474 signature_help_provider: Some(lsp::SignatureHelpOptions {
13475 ..Default::default()
13476 }),
13477 ..Default::default()
13478 },
13479 cx,
13480 )
13481 .await;
13482
13483 let language = Language::new(
13484 LanguageConfig {
13485 name: "Rust".into(),
13486 brackets: BracketPairConfig {
13487 pairs: vec![
13488 BracketPair {
13489 start: "{".to_string(),
13490 end: "}".to_string(),
13491 close: true,
13492 surround: true,
13493 newline: true,
13494 },
13495 BracketPair {
13496 start: "(".to_string(),
13497 end: ")".to_string(),
13498 close: true,
13499 surround: true,
13500 newline: true,
13501 },
13502 BracketPair {
13503 start: "/*".to_string(),
13504 end: " */".to_string(),
13505 close: true,
13506 surround: true,
13507 newline: true,
13508 },
13509 BracketPair {
13510 start: "[".to_string(),
13511 end: "]".to_string(),
13512 close: false,
13513 surround: false,
13514 newline: true,
13515 },
13516 BracketPair {
13517 start: "\"".to_string(),
13518 end: "\"".to_string(),
13519 close: true,
13520 surround: true,
13521 newline: false,
13522 },
13523 BracketPair {
13524 start: "<".to_string(),
13525 end: ">".to_string(),
13526 close: false,
13527 surround: true,
13528 newline: true,
13529 },
13530 ],
13531 ..Default::default()
13532 },
13533 autoclose_before: "})]".to_string(),
13534 ..Default::default()
13535 },
13536 Some(tree_sitter_rust::LANGUAGE.into()),
13537 );
13538 let language = Arc::new(language);
13539
13540 cx.language_registry().add(language.clone());
13541 cx.update_buffer(|buffer, cx| {
13542 buffer.set_language(Some(language), cx);
13543 });
13544
13545 // Ensure that signature_help is not called when no signature help is enabled.
13546 cx.set_state(
13547 &r#"
13548 fn main() {
13549 sampleˇ
13550 }
13551 "#
13552 .unindent(),
13553 );
13554 cx.update_editor(|editor, window, cx| {
13555 editor.handle_input("(", window, cx);
13556 });
13557 cx.assert_editor_state(
13558 &"
13559 fn main() {
13560 sample(ˇ)
13561 }
13562 "
13563 .unindent(),
13564 );
13565 cx.editor(|editor, _, _| {
13566 assert!(editor.signature_help_state.task().is_none());
13567 });
13568
13569 let mocked_response = lsp::SignatureHelp {
13570 signatures: vec![lsp::SignatureInformation {
13571 label: "fn sample(param1: u8, param2: u8)".to_string(),
13572 documentation: None,
13573 parameters: Some(vec![
13574 lsp::ParameterInformation {
13575 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13576 documentation: None,
13577 },
13578 lsp::ParameterInformation {
13579 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13580 documentation: None,
13581 },
13582 ]),
13583 active_parameter: None,
13584 }],
13585 active_signature: Some(0),
13586 active_parameter: Some(0),
13587 };
13588
13589 // Ensure that signature_help is called when enabled afte edits
13590 cx.update(|_, cx| {
13591 cx.update_global::<SettingsStore, _>(|settings, cx| {
13592 settings.update_user_settings(cx, |settings| {
13593 settings.editor.auto_signature_help = Some(false);
13594 settings.editor.show_signature_help_after_edits = Some(true);
13595 });
13596 });
13597 });
13598 cx.set_state(
13599 &r#"
13600 fn main() {
13601 sampleˇ
13602 }
13603 "#
13604 .unindent(),
13605 );
13606 cx.update_editor(|editor, window, cx| {
13607 editor.handle_input("(", window, cx);
13608 });
13609 cx.assert_editor_state(
13610 &"
13611 fn main() {
13612 sample(ˇ)
13613 }
13614 "
13615 .unindent(),
13616 );
13617 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13618 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13619 .await;
13620 cx.update_editor(|editor, _, _| {
13621 let signature_help_state = editor.signature_help_state.popover().cloned();
13622 assert!(signature_help_state.is_some());
13623 let signature = signature_help_state.unwrap();
13624 assert_eq!(
13625 signature.signatures[signature.current_signature].label,
13626 "fn sample(param1: u8, param2: u8)"
13627 );
13628 editor.signature_help_state = SignatureHelpState::default();
13629 });
13630
13631 // Ensure that signature_help is called when auto signature help override is enabled
13632 cx.update(|_, cx| {
13633 cx.update_global::<SettingsStore, _>(|settings, cx| {
13634 settings.update_user_settings(cx, |settings| {
13635 settings.editor.auto_signature_help = Some(true);
13636 settings.editor.show_signature_help_after_edits = Some(false);
13637 });
13638 });
13639 });
13640 cx.set_state(
13641 &r#"
13642 fn main() {
13643 sampleˇ
13644 }
13645 "#
13646 .unindent(),
13647 );
13648 cx.update_editor(|editor, window, cx| {
13649 editor.handle_input("(", window, cx);
13650 });
13651 cx.assert_editor_state(
13652 &"
13653 fn main() {
13654 sample(ˇ)
13655 }
13656 "
13657 .unindent(),
13658 );
13659 handle_signature_help_request(&mut cx, mocked_response).await;
13660 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13661 .await;
13662 cx.editor(|editor, _, _| {
13663 let signature_help_state = editor.signature_help_state.popover().cloned();
13664 assert!(signature_help_state.is_some());
13665 let signature = signature_help_state.unwrap();
13666 assert_eq!(
13667 signature.signatures[signature.current_signature].label,
13668 "fn sample(param1: u8, param2: u8)"
13669 );
13670 });
13671}
13672
13673#[gpui::test]
13674async fn test_signature_help(cx: &mut TestAppContext) {
13675 init_test(cx, |_| {});
13676 cx.update(|cx| {
13677 cx.update_global::<SettingsStore, _>(|settings, cx| {
13678 settings.update_user_settings(cx, |settings| {
13679 settings.editor.auto_signature_help = Some(true);
13680 });
13681 });
13682 });
13683
13684 let mut cx = EditorLspTestContext::new_rust(
13685 lsp::ServerCapabilities {
13686 signature_help_provider: Some(lsp::SignatureHelpOptions {
13687 ..Default::default()
13688 }),
13689 ..Default::default()
13690 },
13691 cx,
13692 )
13693 .await;
13694
13695 // A test that directly calls `show_signature_help`
13696 cx.update_editor(|editor, window, cx| {
13697 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13698 });
13699
13700 let mocked_response = lsp::SignatureHelp {
13701 signatures: vec![lsp::SignatureInformation {
13702 label: "fn sample(param1: u8, param2: u8)".to_string(),
13703 documentation: None,
13704 parameters: Some(vec![
13705 lsp::ParameterInformation {
13706 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13707 documentation: None,
13708 },
13709 lsp::ParameterInformation {
13710 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13711 documentation: None,
13712 },
13713 ]),
13714 active_parameter: None,
13715 }],
13716 active_signature: Some(0),
13717 active_parameter: Some(0),
13718 };
13719 handle_signature_help_request(&mut cx, mocked_response).await;
13720
13721 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13722 .await;
13723
13724 cx.editor(|editor, _, _| {
13725 let signature_help_state = editor.signature_help_state.popover().cloned();
13726 assert!(signature_help_state.is_some());
13727 let signature = signature_help_state.unwrap();
13728 assert_eq!(
13729 signature.signatures[signature.current_signature].label,
13730 "fn sample(param1: u8, param2: u8)"
13731 );
13732 });
13733
13734 // When exiting outside from inside the brackets, `signature_help` is closed.
13735 cx.set_state(indoc! {"
13736 fn main() {
13737 sample(ˇ);
13738 }
13739
13740 fn sample(param1: u8, param2: u8) {}
13741 "});
13742
13743 cx.update_editor(|editor, window, cx| {
13744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13745 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13746 });
13747 });
13748
13749 let mocked_response = lsp::SignatureHelp {
13750 signatures: Vec::new(),
13751 active_signature: None,
13752 active_parameter: None,
13753 };
13754 handle_signature_help_request(&mut cx, mocked_response).await;
13755
13756 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13757 .await;
13758
13759 cx.editor(|editor, _, _| {
13760 assert!(!editor.signature_help_state.is_shown());
13761 });
13762
13763 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13764 cx.set_state(indoc! {"
13765 fn main() {
13766 sample(ˇ);
13767 }
13768
13769 fn sample(param1: u8, param2: u8) {}
13770 "});
13771
13772 let mocked_response = lsp::SignatureHelp {
13773 signatures: vec![lsp::SignatureInformation {
13774 label: "fn sample(param1: u8, param2: u8)".to_string(),
13775 documentation: None,
13776 parameters: Some(vec![
13777 lsp::ParameterInformation {
13778 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13779 documentation: None,
13780 },
13781 lsp::ParameterInformation {
13782 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13783 documentation: None,
13784 },
13785 ]),
13786 active_parameter: None,
13787 }],
13788 active_signature: Some(0),
13789 active_parameter: Some(0),
13790 };
13791 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13792 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13793 .await;
13794 cx.editor(|editor, _, _| {
13795 assert!(editor.signature_help_state.is_shown());
13796 });
13797
13798 // Restore the popover with more parameter input
13799 cx.set_state(indoc! {"
13800 fn main() {
13801 sample(param1, param2ˇ);
13802 }
13803
13804 fn sample(param1: u8, param2: u8) {}
13805 "});
13806
13807 let mocked_response = lsp::SignatureHelp {
13808 signatures: vec![lsp::SignatureInformation {
13809 label: "fn sample(param1: u8, param2: u8)".to_string(),
13810 documentation: None,
13811 parameters: Some(vec![
13812 lsp::ParameterInformation {
13813 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13814 documentation: None,
13815 },
13816 lsp::ParameterInformation {
13817 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13818 documentation: None,
13819 },
13820 ]),
13821 active_parameter: None,
13822 }],
13823 active_signature: Some(0),
13824 active_parameter: Some(1),
13825 };
13826 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13827 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13828 .await;
13829
13830 // When selecting a range, the popover is gone.
13831 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13832 cx.update_editor(|editor, window, cx| {
13833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13834 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13835 })
13836 });
13837 cx.assert_editor_state(indoc! {"
13838 fn main() {
13839 sample(param1, «ˇparam2»);
13840 }
13841
13842 fn sample(param1: u8, param2: u8) {}
13843 "});
13844 cx.editor(|editor, _, _| {
13845 assert!(!editor.signature_help_state.is_shown());
13846 });
13847
13848 // When unselecting again, the popover is back if within the brackets.
13849 cx.update_editor(|editor, window, cx| {
13850 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13851 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13852 })
13853 });
13854 cx.assert_editor_state(indoc! {"
13855 fn main() {
13856 sample(param1, ˇparam2);
13857 }
13858
13859 fn sample(param1: u8, param2: u8) {}
13860 "});
13861 handle_signature_help_request(&mut cx, mocked_response).await;
13862 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13863 .await;
13864 cx.editor(|editor, _, _| {
13865 assert!(editor.signature_help_state.is_shown());
13866 });
13867
13868 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13869 cx.update_editor(|editor, window, cx| {
13870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13871 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13872 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13873 })
13874 });
13875 cx.assert_editor_state(indoc! {"
13876 fn main() {
13877 sample(param1, ˇparam2);
13878 }
13879
13880 fn sample(param1: u8, param2: u8) {}
13881 "});
13882
13883 let mocked_response = lsp::SignatureHelp {
13884 signatures: vec![lsp::SignatureInformation {
13885 label: "fn sample(param1: u8, param2: u8)".to_string(),
13886 documentation: None,
13887 parameters: Some(vec![
13888 lsp::ParameterInformation {
13889 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13890 documentation: None,
13891 },
13892 lsp::ParameterInformation {
13893 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13894 documentation: None,
13895 },
13896 ]),
13897 active_parameter: None,
13898 }],
13899 active_signature: Some(0),
13900 active_parameter: Some(1),
13901 };
13902 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13903 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13904 .await;
13905 cx.update_editor(|editor, _, cx| {
13906 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13907 });
13908 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13909 .await;
13910 cx.update_editor(|editor, window, cx| {
13911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13912 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13913 })
13914 });
13915 cx.assert_editor_state(indoc! {"
13916 fn main() {
13917 sample(param1, «ˇparam2»);
13918 }
13919
13920 fn sample(param1: u8, param2: u8) {}
13921 "});
13922 cx.update_editor(|editor, window, cx| {
13923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13924 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13925 })
13926 });
13927 cx.assert_editor_state(indoc! {"
13928 fn main() {
13929 sample(param1, ˇparam2);
13930 }
13931
13932 fn sample(param1: u8, param2: u8) {}
13933 "});
13934 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13935 .await;
13936}
13937
13938#[gpui::test]
13939async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13940 init_test(cx, |_| {});
13941
13942 let mut cx = EditorLspTestContext::new_rust(
13943 lsp::ServerCapabilities {
13944 signature_help_provider: Some(lsp::SignatureHelpOptions {
13945 ..Default::default()
13946 }),
13947 ..Default::default()
13948 },
13949 cx,
13950 )
13951 .await;
13952
13953 cx.set_state(indoc! {"
13954 fn main() {
13955 overloadedˇ
13956 }
13957 "});
13958
13959 cx.update_editor(|editor, window, cx| {
13960 editor.handle_input("(", window, cx);
13961 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13962 });
13963
13964 // Mock response with 3 signatures
13965 let mocked_response = lsp::SignatureHelp {
13966 signatures: vec![
13967 lsp::SignatureInformation {
13968 label: "fn overloaded(x: i32)".to_string(),
13969 documentation: None,
13970 parameters: Some(vec![lsp::ParameterInformation {
13971 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13972 documentation: None,
13973 }]),
13974 active_parameter: None,
13975 },
13976 lsp::SignatureInformation {
13977 label: "fn overloaded(x: i32, y: i32)".to_string(),
13978 documentation: None,
13979 parameters: Some(vec![
13980 lsp::ParameterInformation {
13981 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13982 documentation: None,
13983 },
13984 lsp::ParameterInformation {
13985 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13986 documentation: None,
13987 },
13988 ]),
13989 active_parameter: None,
13990 },
13991 lsp::SignatureInformation {
13992 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13993 documentation: None,
13994 parameters: Some(vec![
13995 lsp::ParameterInformation {
13996 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13997 documentation: None,
13998 },
13999 lsp::ParameterInformation {
14000 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14001 documentation: None,
14002 },
14003 lsp::ParameterInformation {
14004 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14005 documentation: None,
14006 },
14007 ]),
14008 active_parameter: None,
14009 },
14010 ],
14011 active_signature: Some(1),
14012 active_parameter: Some(0),
14013 };
14014 handle_signature_help_request(&mut cx, mocked_response).await;
14015
14016 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14017 .await;
14018
14019 // Verify we have multiple signatures and the right one is selected
14020 cx.editor(|editor, _, _| {
14021 let popover = editor.signature_help_state.popover().cloned().unwrap();
14022 assert_eq!(popover.signatures.len(), 3);
14023 // active_signature was 1, so that should be the current
14024 assert_eq!(popover.current_signature, 1);
14025 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14026 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14027 assert_eq!(
14028 popover.signatures[2].label,
14029 "fn overloaded(x: i32, y: i32, z: i32)"
14030 );
14031 });
14032
14033 // Test navigation functionality
14034 cx.update_editor(|editor, window, cx| {
14035 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14036 });
14037
14038 cx.editor(|editor, _, _| {
14039 let popover = editor.signature_help_state.popover().cloned().unwrap();
14040 assert_eq!(popover.current_signature, 2);
14041 });
14042
14043 // Test wrap around
14044 cx.update_editor(|editor, window, cx| {
14045 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14046 });
14047
14048 cx.editor(|editor, _, _| {
14049 let popover = editor.signature_help_state.popover().cloned().unwrap();
14050 assert_eq!(popover.current_signature, 0);
14051 });
14052
14053 // Test previous navigation
14054 cx.update_editor(|editor, window, cx| {
14055 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14056 });
14057
14058 cx.editor(|editor, _, _| {
14059 let popover = editor.signature_help_state.popover().cloned().unwrap();
14060 assert_eq!(popover.current_signature, 2);
14061 });
14062}
14063
14064#[gpui::test]
14065async fn test_completion_mode(cx: &mut TestAppContext) {
14066 init_test(cx, |_| {});
14067 let mut cx = EditorLspTestContext::new_rust(
14068 lsp::ServerCapabilities {
14069 completion_provider: Some(lsp::CompletionOptions {
14070 resolve_provider: Some(true),
14071 ..Default::default()
14072 }),
14073 ..Default::default()
14074 },
14075 cx,
14076 )
14077 .await;
14078
14079 struct Run {
14080 run_description: &'static str,
14081 initial_state: String,
14082 buffer_marked_text: String,
14083 completion_label: &'static str,
14084 completion_text: &'static str,
14085 expected_with_insert_mode: String,
14086 expected_with_replace_mode: String,
14087 expected_with_replace_subsequence_mode: String,
14088 expected_with_replace_suffix_mode: String,
14089 }
14090
14091 let runs = [
14092 Run {
14093 run_description: "Start of word matches completion text",
14094 initial_state: "before ediˇ after".into(),
14095 buffer_marked_text: "before <edi|> after".into(),
14096 completion_label: "editor",
14097 completion_text: "editor",
14098 expected_with_insert_mode: "before editorˇ after".into(),
14099 expected_with_replace_mode: "before editorˇ after".into(),
14100 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14101 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14102 },
14103 Run {
14104 run_description: "Accept same text at the middle of the word",
14105 initial_state: "before ediˇtor after".into(),
14106 buffer_marked_text: "before <edi|tor> after".into(),
14107 completion_label: "editor",
14108 completion_text: "editor",
14109 expected_with_insert_mode: "before editorˇtor after".into(),
14110 expected_with_replace_mode: "before editorˇ after".into(),
14111 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14112 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14113 },
14114 Run {
14115 run_description: "End of word matches completion text -- cursor at end",
14116 initial_state: "before torˇ after".into(),
14117 buffer_marked_text: "before <tor|> after".into(),
14118 completion_label: "editor",
14119 completion_text: "editor",
14120 expected_with_insert_mode: "before editorˇ after".into(),
14121 expected_with_replace_mode: "before editorˇ after".into(),
14122 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14123 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14124 },
14125 Run {
14126 run_description: "End of word matches completion text -- cursor at start",
14127 initial_state: "before ˇtor after".into(),
14128 buffer_marked_text: "before <|tor> after".into(),
14129 completion_label: "editor",
14130 completion_text: "editor",
14131 expected_with_insert_mode: "before editorˇtor after".into(),
14132 expected_with_replace_mode: "before editorˇ after".into(),
14133 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14134 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14135 },
14136 Run {
14137 run_description: "Prepend text containing whitespace",
14138 initial_state: "pˇfield: bool".into(),
14139 buffer_marked_text: "<p|field>: bool".into(),
14140 completion_label: "pub ",
14141 completion_text: "pub ",
14142 expected_with_insert_mode: "pub ˇfield: bool".into(),
14143 expected_with_replace_mode: "pub ˇ: bool".into(),
14144 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14145 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14146 },
14147 Run {
14148 run_description: "Add element to start of list",
14149 initial_state: "[element_ˇelement_2]".into(),
14150 buffer_marked_text: "[<element_|element_2>]".into(),
14151 completion_label: "element_1",
14152 completion_text: "element_1",
14153 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14154 expected_with_replace_mode: "[element_1ˇ]".into(),
14155 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14156 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14157 },
14158 Run {
14159 run_description: "Add element to start of list -- first and second elements are equal",
14160 initial_state: "[elˇelement]".into(),
14161 buffer_marked_text: "[<el|element>]".into(),
14162 completion_label: "element",
14163 completion_text: "element",
14164 expected_with_insert_mode: "[elementˇelement]".into(),
14165 expected_with_replace_mode: "[elementˇ]".into(),
14166 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14167 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14168 },
14169 Run {
14170 run_description: "Ends with matching suffix",
14171 initial_state: "SubˇError".into(),
14172 buffer_marked_text: "<Sub|Error>".into(),
14173 completion_label: "SubscriptionError",
14174 completion_text: "SubscriptionError",
14175 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14176 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14177 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14178 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14179 },
14180 Run {
14181 run_description: "Suffix is a subsequence -- contiguous",
14182 initial_state: "SubˇErr".into(),
14183 buffer_marked_text: "<Sub|Err>".into(),
14184 completion_label: "SubscriptionError",
14185 completion_text: "SubscriptionError",
14186 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14187 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14188 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14189 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14190 },
14191 Run {
14192 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14193 initial_state: "Suˇscrirr".into(),
14194 buffer_marked_text: "<Su|scrirr>".into(),
14195 completion_label: "SubscriptionError",
14196 completion_text: "SubscriptionError",
14197 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14198 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14199 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14200 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14201 },
14202 Run {
14203 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14204 initial_state: "foo(indˇix)".into(),
14205 buffer_marked_text: "foo(<ind|ix>)".into(),
14206 completion_label: "node_index",
14207 completion_text: "node_index",
14208 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14209 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14210 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14211 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14212 },
14213 Run {
14214 run_description: "Replace range ends before cursor - should extend to cursor",
14215 initial_state: "before editˇo after".into(),
14216 buffer_marked_text: "before <{ed}>it|o after".into(),
14217 completion_label: "editor",
14218 completion_text: "editor",
14219 expected_with_insert_mode: "before editorˇo after".into(),
14220 expected_with_replace_mode: "before editorˇo after".into(),
14221 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14222 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14223 },
14224 Run {
14225 run_description: "Uses label for suffix matching",
14226 initial_state: "before ediˇtor after".into(),
14227 buffer_marked_text: "before <edi|tor> after".into(),
14228 completion_label: "editor",
14229 completion_text: "editor()",
14230 expected_with_insert_mode: "before editor()ˇtor after".into(),
14231 expected_with_replace_mode: "before editor()ˇ after".into(),
14232 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14233 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14234 },
14235 Run {
14236 run_description: "Case insensitive subsequence and suffix matching",
14237 initial_state: "before EDiˇtoR after".into(),
14238 buffer_marked_text: "before <EDi|toR> after".into(),
14239 completion_label: "editor",
14240 completion_text: "editor",
14241 expected_with_insert_mode: "before editorˇtoR after".into(),
14242 expected_with_replace_mode: "before editorˇ after".into(),
14243 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14244 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14245 },
14246 ];
14247
14248 for run in runs {
14249 let run_variations = [
14250 (LspInsertMode::Insert, run.expected_with_insert_mode),
14251 (LspInsertMode::Replace, run.expected_with_replace_mode),
14252 (
14253 LspInsertMode::ReplaceSubsequence,
14254 run.expected_with_replace_subsequence_mode,
14255 ),
14256 (
14257 LspInsertMode::ReplaceSuffix,
14258 run.expected_with_replace_suffix_mode,
14259 ),
14260 ];
14261
14262 for (lsp_insert_mode, expected_text) in run_variations {
14263 eprintln!(
14264 "run = {:?}, mode = {lsp_insert_mode:.?}",
14265 run.run_description,
14266 );
14267
14268 update_test_language_settings(&mut cx, |settings| {
14269 settings.defaults.completions = Some(CompletionSettingsContent {
14270 lsp_insert_mode: Some(lsp_insert_mode),
14271 words: Some(WordsCompletionMode::Disabled),
14272 words_min_length: Some(0),
14273 ..Default::default()
14274 });
14275 });
14276
14277 cx.set_state(&run.initial_state);
14278 cx.update_editor(|editor, window, cx| {
14279 editor.show_completions(&ShowCompletions, window, cx);
14280 });
14281
14282 let counter = Arc::new(AtomicUsize::new(0));
14283 handle_completion_request_with_insert_and_replace(
14284 &mut cx,
14285 &run.buffer_marked_text,
14286 vec![(run.completion_label, run.completion_text)],
14287 counter.clone(),
14288 )
14289 .await;
14290 cx.condition(|editor, _| editor.context_menu_visible())
14291 .await;
14292 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14293
14294 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14295 editor
14296 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14297 .unwrap()
14298 });
14299 cx.assert_editor_state(&expected_text);
14300 handle_resolve_completion_request(&mut cx, None).await;
14301 apply_additional_edits.await.unwrap();
14302 }
14303 }
14304}
14305
14306#[gpui::test]
14307async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14308 init_test(cx, |_| {});
14309 let mut cx = EditorLspTestContext::new_rust(
14310 lsp::ServerCapabilities {
14311 completion_provider: Some(lsp::CompletionOptions {
14312 resolve_provider: Some(true),
14313 ..Default::default()
14314 }),
14315 ..Default::default()
14316 },
14317 cx,
14318 )
14319 .await;
14320
14321 let initial_state = "SubˇError";
14322 let buffer_marked_text = "<Sub|Error>";
14323 let completion_text = "SubscriptionError";
14324 let expected_with_insert_mode = "SubscriptionErrorˇError";
14325 let expected_with_replace_mode = "SubscriptionErrorˇ";
14326
14327 update_test_language_settings(&mut cx, |settings| {
14328 settings.defaults.completions = Some(CompletionSettingsContent {
14329 words: Some(WordsCompletionMode::Disabled),
14330 words_min_length: Some(0),
14331 // set the opposite here to ensure that the action is overriding the default behavior
14332 lsp_insert_mode: Some(LspInsertMode::Insert),
14333 ..Default::default()
14334 });
14335 });
14336
14337 cx.set_state(initial_state);
14338 cx.update_editor(|editor, window, cx| {
14339 editor.show_completions(&ShowCompletions, window, cx);
14340 });
14341
14342 let counter = Arc::new(AtomicUsize::new(0));
14343 handle_completion_request_with_insert_and_replace(
14344 &mut cx,
14345 buffer_marked_text,
14346 vec![(completion_text, completion_text)],
14347 counter.clone(),
14348 )
14349 .await;
14350 cx.condition(|editor, _| editor.context_menu_visible())
14351 .await;
14352 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14353
14354 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14355 editor
14356 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14357 .unwrap()
14358 });
14359 cx.assert_editor_state(expected_with_replace_mode);
14360 handle_resolve_completion_request(&mut cx, None).await;
14361 apply_additional_edits.await.unwrap();
14362
14363 update_test_language_settings(&mut cx, |settings| {
14364 settings.defaults.completions = Some(CompletionSettingsContent {
14365 words: Some(WordsCompletionMode::Disabled),
14366 words_min_length: Some(0),
14367 // set the opposite here to ensure that the action is overriding the default behavior
14368 lsp_insert_mode: Some(LspInsertMode::Replace),
14369 ..Default::default()
14370 });
14371 });
14372
14373 cx.set_state(initial_state);
14374 cx.update_editor(|editor, window, cx| {
14375 editor.show_completions(&ShowCompletions, window, cx);
14376 });
14377 handle_completion_request_with_insert_and_replace(
14378 &mut cx,
14379 buffer_marked_text,
14380 vec![(completion_text, completion_text)],
14381 counter.clone(),
14382 )
14383 .await;
14384 cx.condition(|editor, _| editor.context_menu_visible())
14385 .await;
14386 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14387
14388 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14389 editor
14390 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14391 .unwrap()
14392 });
14393 cx.assert_editor_state(expected_with_insert_mode);
14394 handle_resolve_completion_request(&mut cx, None).await;
14395 apply_additional_edits.await.unwrap();
14396}
14397
14398#[gpui::test]
14399async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14400 init_test(cx, |_| {});
14401 let mut cx = EditorLspTestContext::new_rust(
14402 lsp::ServerCapabilities {
14403 completion_provider: Some(lsp::CompletionOptions {
14404 resolve_provider: Some(true),
14405 ..Default::default()
14406 }),
14407 ..Default::default()
14408 },
14409 cx,
14410 )
14411 .await;
14412
14413 // scenario: surrounding text matches completion text
14414 let completion_text = "to_offset";
14415 let initial_state = indoc! {"
14416 1. buf.to_offˇsuffix
14417 2. buf.to_offˇsuf
14418 3. buf.to_offˇfix
14419 4. buf.to_offˇ
14420 5. into_offˇensive
14421 6. ˇsuffix
14422 7. let ˇ //
14423 8. aaˇzz
14424 9. buf.to_off«zzzzzˇ»suffix
14425 10. buf.«ˇzzzzz»suffix
14426 11. to_off«ˇzzzzz»
14427
14428 buf.to_offˇsuffix // newest cursor
14429 "};
14430 let completion_marked_buffer = indoc! {"
14431 1. buf.to_offsuffix
14432 2. buf.to_offsuf
14433 3. buf.to_offfix
14434 4. buf.to_off
14435 5. into_offensive
14436 6. suffix
14437 7. let //
14438 8. aazz
14439 9. buf.to_offzzzzzsuffix
14440 10. buf.zzzzzsuffix
14441 11. to_offzzzzz
14442
14443 buf.<to_off|suffix> // newest cursor
14444 "};
14445 let expected = indoc! {"
14446 1. buf.to_offsetˇ
14447 2. buf.to_offsetˇsuf
14448 3. buf.to_offsetˇfix
14449 4. buf.to_offsetˇ
14450 5. into_offsetˇensive
14451 6. to_offsetˇsuffix
14452 7. let to_offsetˇ //
14453 8. aato_offsetˇzz
14454 9. buf.to_offsetˇ
14455 10. buf.to_offsetˇsuffix
14456 11. to_offsetˇ
14457
14458 buf.to_offsetˇ // newest cursor
14459 "};
14460 cx.set_state(initial_state);
14461 cx.update_editor(|editor, window, cx| {
14462 editor.show_completions(&ShowCompletions, window, cx);
14463 });
14464 handle_completion_request_with_insert_and_replace(
14465 &mut cx,
14466 completion_marked_buffer,
14467 vec![(completion_text, completion_text)],
14468 Arc::new(AtomicUsize::new(0)),
14469 )
14470 .await;
14471 cx.condition(|editor, _| editor.context_menu_visible())
14472 .await;
14473 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14474 editor
14475 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14476 .unwrap()
14477 });
14478 cx.assert_editor_state(expected);
14479 handle_resolve_completion_request(&mut cx, None).await;
14480 apply_additional_edits.await.unwrap();
14481
14482 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14483 let completion_text = "foo_and_bar";
14484 let initial_state = indoc! {"
14485 1. ooanbˇ
14486 2. zooanbˇ
14487 3. ooanbˇz
14488 4. zooanbˇz
14489 5. ooanˇ
14490 6. oanbˇ
14491
14492 ooanbˇ
14493 "};
14494 let completion_marked_buffer = indoc! {"
14495 1. ooanb
14496 2. zooanb
14497 3. ooanbz
14498 4. zooanbz
14499 5. ooan
14500 6. oanb
14501
14502 <ooanb|>
14503 "};
14504 let expected = indoc! {"
14505 1. foo_and_barˇ
14506 2. zfoo_and_barˇ
14507 3. foo_and_barˇz
14508 4. zfoo_and_barˇz
14509 5. ooanfoo_and_barˇ
14510 6. oanbfoo_and_barˇ
14511
14512 foo_and_barˇ
14513 "};
14514 cx.set_state(initial_state);
14515 cx.update_editor(|editor, window, cx| {
14516 editor.show_completions(&ShowCompletions, window, cx);
14517 });
14518 handle_completion_request_with_insert_and_replace(
14519 &mut cx,
14520 completion_marked_buffer,
14521 vec![(completion_text, completion_text)],
14522 Arc::new(AtomicUsize::new(0)),
14523 )
14524 .await;
14525 cx.condition(|editor, _| editor.context_menu_visible())
14526 .await;
14527 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14528 editor
14529 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14530 .unwrap()
14531 });
14532 cx.assert_editor_state(expected);
14533 handle_resolve_completion_request(&mut cx, None).await;
14534 apply_additional_edits.await.unwrap();
14535
14536 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14537 // (expects the same as if it was inserted at the end)
14538 let completion_text = "foo_and_bar";
14539 let initial_state = indoc! {"
14540 1. ooˇanb
14541 2. zooˇanb
14542 3. ooˇanbz
14543 4. zooˇanbz
14544
14545 ooˇanb
14546 "};
14547 let completion_marked_buffer = indoc! {"
14548 1. ooanb
14549 2. zooanb
14550 3. ooanbz
14551 4. zooanbz
14552
14553 <oo|anb>
14554 "};
14555 let expected = indoc! {"
14556 1. foo_and_barˇ
14557 2. zfoo_and_barˇ
14558 3. foo_and_barˇz
14559 4. zfoo_and_barˇz
14560
14561 foo_and_barˇ
14562 "};
14563 cx.set_state(initial_state);
14564 cx.update_editor(|editor, window, cx| {
14565 editor.show_completions(&ShowCompletions, window, cx);
14566 });
14567 handle_completion_request_with_insert_and_replace(
14568 &mut cx,
14569 completion_marked_buffer,
14570 vec![(completion_text, completion_text)],
14571 Arc::new(AtomicUsize::new(0)),
14572 )
14573 .await;
14574 cx.condition(|editor, _| editor.context_menu_visible())
14575 .await;
14576 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14577 editor
14578 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14579 .unwrap()
14580 });
14581 cx.assert_editor_state(expected);
14582 handle_resolve_completion_request(&mut cx, None).await;
14583 apply_additional_edits.await.unwrap();
14584}
14585
14586// This used to crash
14587#[gpui::test]
14588async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14589 init_test(cx, |_| {});
14590
14591 let buffer_text = indoc! {"
14592 fn main() {
14593 10.satu;
14594
14595 //
14596 // separate cursors so they open in different excerpts (manually reproducible)
14597 //
14598
14599 10.satu20;
14600 }
14601 "};
14602 let multibuffer_text_with_selections = indoc! {"
14603 fn main() {
14604 10.satuˇ;
14605
14606 //
14607
14608 //
14609
14610 10.satuˇ20;
14611 }
14612 "};
14613 let expected_multibuffer = indoc! {"
14614 fn main() {
14615 10.saturating_sub()ˇ;
14616
14617 //
14618
14619 //
14620
14621 10.saturating_sub()ˇ;
14622 }
14623 "};
14624
14625 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14626 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14627
14628 let fs = FakeFs::new(cx.executor());
14629 fs.insert_tree(
14630 path!("/a"),
14631 json!({
14632 "main.rs": buffer_text,
14633 }),
14634 )
14635 .await;
14636
14637 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14638 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14639 language_registry.add(rust_lang());
14640 let mut fake_servers = language_registry.register_fake_lsp(
14641 "Rust",
14642 FakeLspAdapter {
14643 capabilities: lsp::ServerCapabilities {
14644 completion_provider: Some(lsp::CompletionOptions {
14645 resolve_provider: None,
14646 ..lsp::CompletionOptions::default()
14647 }),
14648 ..lsp::ServerCapabilities::default()
14649 },
14650 ..FakeLspAdapter::default()
14651 },
14652 );
14653 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14654 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14655 let buffer = project
14656 .update(cx, |project, cx| {
14657 project.open_local_buffer(path!("/a/main.rs"), cx)
14658 })
14659 .await
14660 .unwrap();
14661
14662 let multi_buffer = cx.new(|cx| {
14663 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14664 multi_buffer.push_excerpts(
14665 buffer.clone(),
14666 [ExcerptRange::new(0..first_excerpt_end)],
14667 cx,
14668 );
14669 multi_buffer.push_excerpts(
14670 buffer.clone(),
14671 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14672 cx,
14673 );
14674 multi_buffer
14675 });
14676
14677 let editor = workspace
14678 .update(cx, |_, window, cx| {
14679 cx.new(|cx| {
14680 Editor::new(
14681 EditorMode::Full {
14682 scale_ui_elements_with_buffer_font_size: false,
14683 show_active_line_background: false,
14684 sizing_behavior: SizingBehavior::Default,
14685 },
14686 multi_buffer.clone(),
14687 Some(project.clone()),
14688 window,
14689 cx,
14690 )
14691 })
14692 })
14693 .unwrap();
14694
14695 let pane = workspace
14696 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14697 .unwrap();
14698 pane.update_in(cx, |pane, window, cx| {
14699 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14700 });
14701
14702 let fake_server = fake_servers.next().await.unwrap();
14703
14704 editor.update_in(cx, |editor, window, cx| {
14705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14706 s.select_ranges([
14707 Point::new(1, 11)..Point::new(1, 11),
14708 Point::new(7, 11)..Point::new(7, 11),
14709 ])
14710 });
14711
14712 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14713 });
14714
14715 editor.update_in(cx, |editor, window, cx| {
14716 editor.show_completions(&ShowCompletions, window, cx);
14717 });
14718
14719 fake_server
14720 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14721 let completion_item = lsp::CompletionItem {
14722 label: "saturating_sub()".into(),
14723 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14724 lsp::InsertReplaceEdit {
14725 new_text: "saturating_sub()".to_owned(),
14726 insert: lsp::Range::new(
14727 lsp::Position::new(7, 7),
14728 lsp::Position::new(7, 11),
14729 ),
14730 replace: lsp::Range::new(
14731 lsp::Position::new(7, 7),
14732 lsp::Position::new(7, 13),
14733 ),
14734 },
14735 )),
14736 ..lsp::CompletionItem::default()
14737 };
14738
14739 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14740 })
14741 .next()
14742 .await
14743 .unwrap();
14744
14745 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14746 .await;
14747
14748 editor
14749 .update_in(cx, |editor, window, cx| {
14750 editor
14751 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14752 .unwrap()
14753 })
14754 .await
14755 .unwrap();
14756
14757 editor.update(cx, |editor, cx| {
14758 assert_text_with_selections(editor, expected_multibuffer, cx);
14759 })
14760}
14761
14762#[gpui::test]
14763async fn test_completion(cx: &mut TestAppContext) {
14764 init_test(cx, |_| {});
14765
14766 let mut cx = EditorLspTestContext::new_rust(
14767 lsp::ServerCapabilities {
14768 completion_provider: Some(lsp::CompletionOptions {
14769 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14770 resolve_provider: Some(true),
14771 ..Default::default()
14772 }),
14773 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14774 ..Default::default()
14775 },
14776 cx,
14777 )
14778 .await;
14779 let counter = Arc::new(AtomicUsize::new(0));
14780
14781 cx.set_state(indoc! {"
14782 oneˇ
14783 two
14784 three
14785 "});
14786 cx.simulate_keystroke(".");
14787 handle_completion_request(
14788 indoc! {"
14789 one.|<>
14790 two
14791 three
14792 "},
14793 vec!["first_completion", "second_completion"],
14794 true,
14795 counter.clone(),
14796 &mut cx,
14797 )
14798 .await;
14799 cx.condition(|editor, _| editor.context_menu_visible())
14800 .await;
14801 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14802
14803 let _handler = handle_signature_help_request(
14804 &mut cx,
14805 lsp::SignatureHelp {
14806 signatures: vec![lsp::SignatureInformation {
14807 label: "test signature".to_string(),
14808 documentation: None,
14809 parameters: Some(vec![lsp::ParameterInformation {
14810 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14811 documentation: None,
14812 }]),
14813 active_parameter: None,
14814 }],
14815 active_signature: None,
14816 active_parameter: None,
14817 },
14818 );
14819 cx.update_editor(|editor, window, cx| {
14820 assert!(
14821 !editor.signature_help_state.is_shown(),
14822 "No signature help was called for"
14823 );
14824 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14825 });
14826 cx.run_until_parked();
14827 cx.update_editor(|editor, _, _| {
14828 assert!(
14829 !editor.signature_help_state.is_shown(),
14830 "No signature help should be shown when completions menu is open"
14831 );
14832 });
14833
14834 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14835 editor.context_menu_next(&Default::default(), window, cx);
14836 editor
14837 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14838 .unwrap()
14839 });
14840 cx.assert_editor_state(indoc! {"
14841 one.second_completionˇ
14842 two
14843 three
14844 "});
14845
14846 handle_resolve_completion_request(
14847 &mut cx,
14848 Some(vec![
14849 (
14850 //This overlaps with the primary completion edit which is
14851 //misbehavior from the LSP spec, test that we filter it out
14852 indoc! {"
14853 one.second_ˇcompletion
14854 two
14855 threeˇ
14856 "},
14857 "overlapping additional edit",
14858 ),
14859 (
14860 indoc! {"
14861 one.second_completion
14862 two
14863 threeˇ
14864 "},
14865 "\nadditional edit",
14866 ),
14867 ]),
14868 )
14869 .await;
14870 apply_additional_edits.await.unwrap();
14871 cx.assert_editor_state(indoc! {"
14872 one.second_completionˇ
14873 two
14874 three
14875 additional edit
14876 "});
14877
14878 cx.set_state(indoc! {"
14879 one.second_completion
14880 twoˇ
14881 threeˇ
14882 additional edit
14883 "});
14884 cx.simulate_keystroke(" ");
14885 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14886 cx.simulate_keystroke("s");
14887 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14888
14889 cx.assert_editor_state(indoc! {"
14890 one.second_completion
14891 two sˇ
14892 three sˇ
14893 additional edit
14894 "});
14895 handle_completion_request(
14896 indoc! {"
14897 one.second_completion
14898 two s
14899 three <s|>
14900 additional edit
14901 "},
14902 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14903 true,
14904 counter.clone(),
14905 &mut cx,
14906 )
14907 .await;
14908 cx.condition(|editor, _| editor.context_menu_visible())
14909 .await;
14910 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14911
14912 cx.simulate_keystroke("i");
14913
14914 handle_completion_request(
14915 indoc! {"
14916 one.second_completion
14917 two si
14918 three <si|>
14919 additional edit
14920 "},
14921 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14922 true,
14923 counter.clone(),
14924 &mut cx,
14925 )
14926 .await;
14927 cx.condition(|editor, _| editor.context_menu_visible())
14928 .await;
14929 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14930
14931 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14932 editor
14933 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14934 .unwrap()
14935 });
14936 cx.assert_editor_state(indoc! {"
14937 one.second_completion
14938 two sixth_completionˇ
14939 three sixth_completionˇ
14940 additional edit
14941 "});
14942
14943 apply_additional_edits.await.unwrap();
14944
14945 update_test_language_settings(&mut cx, |settings| {
14946 settings.defaults.show_completions_on_input = Some(false);
14947 });
14948 cx.set_state("editorˇ");
14949 cx.simulate_keystroke(".");
14950 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14951 cx.simulate_keystrokes("c l o");
14952 cx.assert_editor_state("editor.cloˇ");
14953 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14954 cx.update_editor(|editor, window, cx| {
14955 editor.show_completions(&ShowCompletions, window, cx);
14956 });
14957 handle_completion_request(
14958 "editor.<clo|>",
14959 vec!["close", "clobber"],
14960 true,
14961 counter.clone(),
14962 &mut cx,
14963 )
14964 .await;
14965 cx.condition(|editor, _| editor.context_menu_visible())
14966 .await;
14967 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14968
14969 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14970 editor
14971 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14972 .unwrap()
14973 });
14974 cx.assert_editor_state("editor.clobberˇ");
14975 handle_resolve_completion_request(&mut cx, None).await;
14976 apply_additional_edits.await.unwrap();
14977}
14978
14979#[gpui::test]
14980async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
14981 init_test(cx, |_| {});
14982
14983 let fs = FakeFs::new(cx.executor());
14984 fs.insert_tree(
14985 path!("/a"),
14986 json!({
14987 "main.rs": "",
14988 }),
14989 )
14990 .await;
14991
14992 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14993 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14994 language_registry.add(rust_lang());
14995 let command_calls = Arc::new(AtomicUsize::new(0));
14996 let registered_command = "_the/command";
14997
14998 let closure_command_calls = command_calls.clone();
14999 let mut fake_servers = language_registry.register_fake_lsp(
15000 "Rust",
15001 FakeLspAdapter {
15002 capabilities: lsp::ServerCapabilities {
15003 completion_provider: Some(lsp::CompletionOptions {
15004 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15005 ..lsp::CompletionOptions::default()
15006 }),
15007 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15008 commands: vec![registered_command.to_owned()],
15009 ..lsp::ExecuteCommandOptions::default()
15010 }),
15011 ..lsp::ServerCapabilities::default()
15012 },
15013 initializer: Some(Box::new(move |fake_server| {
15014 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15015 move |params, _| async move {
15016 Ok(Some(lsp::CompletionResponse::Array(vec![
15017 lsp::CompletionItem {
15018 label: "registered_command".to_owned(),
15019 text_edit: gen_text_edit(¶ms, ""),
15020 command: Some(lsp::Command {
15021 title: registered_command.to_owned(),
15022 command: "_the/command".to_owned(),
15023 arguments: Some(vec![serde_json::Value::Bool(true)]),
15024 }),
15025 ..lsp::CompletionItem::default()
15026 },
15027 lsp::CompletionItem {
15028 label: "unregistered_command".to_owned(),
15029 text_edit: gen_text_edit(¶ms, ""),
15030 command: Some(lsp::Command {
15031 title: "????????????".to_owned(),
15032 command: "????????????".to_owned(),
15033 arguments: Some(vec![serde_json::Value::Null]),
15034 }),
15035 ..lsp::CompletionItem::default()
15036 },
15037 ])))
15038 },
15039 );
15040 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15041 let command_calls = closure_command_calls.clone();
15042 move |params, _| {
15043 assert_eq!(params.command, registered_command);
15044 let command_calls = command_calls.clone();
15045 async move {
15046 command_calls.fetch_add(1, atomic::Ordering::Release);
15047 Ok(Some(json!(null)))
15048 }
15049 }
15050 });
15051 })),
15052 ..FakeLspAdapter::default()
15053 },
15054 );
15055 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15056 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15057 let editor = workspace
15058 .update(cx, |workspace, window, cx| {
15059 workspace.open_abs_path(
15060 PathBuf::from(path!("/a/main.rs")),
15061 OpenOptions::default(),
15062 window,
15063 cx,
15064 )
15065 })
15066 .unwrap()
15067 .await
15068 .unwrap()
15069 .downcast::<Editor>()
15070 .unwrap();
15071 let _fake_server = fake_servers.next().await.unwrap();
15072
15073 editor.update_in(cx, |editor, window, cx| {
15074 cx.focus_self(window);
15075 editor.move_to_end(&MoveToEnd, window, cx);
15076 editor.handle_input(".", window, cx);
15077 });
15078 cx.run_until_parked();
15079 editor.update(cx, |editor, _| {
15080 assert!(editor.context_menu_visible());
15081 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15082 {
15083 let completion_labels = menu
15084 .completions
15085 .borrow()
15086 .iter()
15087 .map(|c| c.label.text.clone())
15088 .collect::<Vec<_>>();
15089 assert_eq!(
15090 completion_labels,
15091 &["registered_command", "unregistered_command",],
15092 );
15093 } else {
15094 panic!("expected completion menu to be open");
15095 }
15096 });
15097
15098 editor
15099 .update_in(cx, |editor, window, cx| {
15100 editor
15101 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15102 .unwrap()
15103 })
15104 .await
15105 .unwrap();
15106 cx.run_until_parked();
15107 assert_eq!(
15108 command_calls.load(atomic::Ordering::Acquire),
15109 1,
15110 "For completion with a registered command, Zed should send a command execution request",
15111 );
15112
15113 editor.update_in(cx, |editor, window, cx| {
15114 cx.focus_self(window);
15115 editor.handle_input(".", window, cx);
15116 });
15117 cx.run_until_parked();
15118 editor.update(cx, |editor, _| {
15119 assert!(editor.context_menu_visible());
15120 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15121 {
15122 let completion_labels = menu
15123 .completions
15124 .borrow()
15125 .iter()
15126 .map(|c| c.label.text.clone())
15127 .collect::<Vec<_>>();
15128 assert_eq!(
15129 completion_labels,
15130 &["registered_command", "unregistered_command",],
15131 );
15132 } else {
15133 panic!("expected completion menu to be open");
15134 }
15135 });
15136 editor
15137 .update_in(cx, |editor, window, cx| {
15138 editor.context_menu_next(&Default::default(), window, cx);
15139 editor
15140 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15141 .unwrap()
15142 })
15143 .await
15144 .unwrap();
15145 cx.run_until_parked();
15146 assert_eq!(
15147 command_calls.load(atomic::Ordering::Acquire),
15148 1,
15149 "For completion with an unregistered command, Zed should not send a command execution request",
15150 );
15151}
15152
15153#[gpui::test]
15154async fn test_completion_reuse(cx: &mut TestAppContext) {
15155 init_test(cx, |_| {});
15156
15157 let mut cx = EditorLspTestContext::new_rust(
15158 lsp::ServerCapabilities {
15159 completion_provider: Some(lsp::CompletionOptions {
15160 trigger_characters: Some(vec![".".to_string()]),
15161 ..Default::default()
15162 }),
15163 ..Default::default()
15164 },
15165 cx,
15166 )
15167 .await;
15168
15169 let counter = Arc::new(AtomicUsize::new(0));
15170 cx.set_state("objˇ");
15171 cx.simulate_keystroke(".");
15172
15173 // Initial completion request returns complete results
15174 let is_incomplete = false;
15175 handle_completion_request(
15176 "obj.|<>",
15177 vec!["a", "ab", "abc"],
15178 is_incomplete,
15179 counter.clone(),
15180 &mut cx,
15181 )
15182 .await;
15183 cx.run_until_parked();
15184 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15185 cx.assert_editor_state("obj.ˇ");
15186 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15187
15188 // Type "a" - filters existing completions
15189 cx.simulate_keystroke("a");
15190 cx.run_until_parked();
15191 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15192 cx.assert_editor_state("obj.aˇ");
15193 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15194
15195 // Type "b" - filters existing completions
15196 cx.simulate_keystroke("b");
15197 cx.run_until_parked();
15198 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15199 cx.assert_editor_state("obj.abˇ");
15200 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15201
15202 // Type "c" - filters existing completions
15203 cx.simulate_keystroke("c");
15204 cx.run_until_parked();
15205 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15206 cx.assert_editor_state("obj.abcˇ");
15207 check_displayed_completions(vec!["abc"], &mut cx);
15208
15209 // Backspace to delete "c" - filters existing completions
15210 cx.update_editor(|editor, window, cx| {
15211 editor.backspace(&Backspace, window, cx);
15212 });
15213 cx.run_until_parked();
15214 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15215 cx.assert_editor_state("obj.abˇ");
15216 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15217
15218 // Moving cursor to the left dismisses menu.
15219 cx.update_editor(|editor, window, cx| {
15220 editor.move_left(&MoveLeft, window, cx);
15221 });
15222 cx.run_until_parked();
15223 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15224 cx.assert_editor_state("obj.aˇb");
15225 cx.update_editor(|editor, _, _| {
15226 assert_eq!(editor.context_menu_visible(), false);
15227 });
15228
15229 // Type "b" - new request
15230 cx.simulate_keystroke("b");
15231 let is_incomplete = false;
15232 handle_completion_request(
15233 "obj.<ab|>a",
15234 vec!["ab", "abc"],
15235 is_incomplete,
15236 counter.clone(),
15237 &mut cx,
15238 )
15239 .await;
15240 cx.run_until_parked();
15241 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15242 cx.assert_editor_state("obj.abˇb");
15243 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15244
15245 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15246 cx.update_editor(|editor, window, cx| {
15247 editor.backspace(&Backspace, window, cx);
15248 });
15249 let is_incomplete = false;
15250 handle_completion_request(
15251 "obj.<a|>b",
15252 vec!["a", "ab", "abc"],
15253 is_incomplete,
15254 counter.clone(),
15255 &mut cx,
15256 )
15257 .await;
15258 cx.run_until_parked();
15259 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15260 cx.assert_editor_state("obj.aˇb");
15261 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15262
15263 // Backspace to delete "a" - dismisses menu.
15264 cx.update_editor(|editor, window, cx| {
15265 editor.backspace(&Backspace, window, cx);
15266 });
15267 cx.run_until_parked();
15268 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15269 cx.assert_editor_state("obj.ˇb");
15270 cx.update_editor(|editor, _, _| {
15271 assert_eq!(editor.context_menu_visible(), false);
15272 });
15273}
15274
15275#[gpui::test]
15276async fn test_word_completion(cx: &mut TestAppContext) {
15277 let lsp_fetch_timeout_ms = 10;
15278 init_test(cx, |language_settings| {
15279 language_settings.defaults.completions = Some(CompletionSettingsContent {
15280 words_min_length: Some(0),
15281 lsp_fetch_timeout_ms: Some(10),
15282 lsp_insert_mode: Some(LspInsertMode::Insert),
15283 ..Default::default()
15284 });
15285 });
15286
15287 let mut cx = EditorLspTestContext::new_rust(
15288 lsp::ServerCapabilities {
15289 completion_provider: Some(lsp::CompletionOptions {
15290 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15291 ..lsp::CompletionOptions::default()
15292 }),
15293 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15294 ..lsp::ServerCapabilities::default()
15295 },
15296 cx,
15297 )
15298 .await;
15299
15300 let throttle_completions = Arc::new(AtomicBool::new(false));
15301
15302 let lsp_throttle_completions = throttle_completions.clone();
15303 let _completion_requests_handler =
15304 cx.lsp
15305 .server
15306 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15307 let lsp_throttle_completions = lsp_throttle_completions.clone();
15308 let cx = cx.clone();
15309 async move {
15310 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15311 cx.background_executor()
15312 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15313 .await;
15314 }
15315 Ok(Some(lsp::CompletionResponse::Array(vec![
15316 lsp::CompletionItem {
15317 label: "first".into(),
15318 ..lsp::CompletionItem::default()
15319 },
15320 lsp::CompletionItem {
15321 label: "last".into(),
15322 ..lsp::CompletionItem::default()
15323 },
15324 ])))
15325 }
15326 });
15327
15328 cx.set_state(indoc! {"
15329 oneˇ
15330 two
15331 three
15332 "});
15333 cx.simulate_keystroke(".");
15334 cx.executor().run_until_parked();
15335 cx.condition(|editor, _| editor.context_menu_visible())
15336 .await;
15337 cx.update_editor(|editor, window, cx| {
15338 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15339 {
15340 assert_eq!(
15341 completion_menu_entries(menu),
15342 &["first", "last"],
15343 "When LSP server is fast to reply, no fallback word completions are used"
15344 );
15345 } else {
15346 panic!("expected completion menu to be open");
15347 }
15348 editor.cancel(&Cancel, window, cx);
15349 });
15350 cx.executor().run_until_parked();
15351 cx.condition(|editor, _| !editor.context_menu_visible())
15352 .await;
15353
15354 throttle_completions.store(true, atomic::Ordering::Release);
15355 cx.simulate_keystroke(".");
15356 cx.executor()
15357 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15358 cx.executor().run_until_parked();
15359 cx.condition(|editor, _| editor.context_menu_visible())
15360 .await;
15361 cx.update_editor(|editor, _, _| {
15362 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15363 {
15364 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15365 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15366 } else {
15367 panic!("expected completion menu to be open");
15368 }
15369 });
15370}
15371
15372#[gpui::test]
15373async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15374 init_test(cx, |language_settings| {
15375 language_settings.defaults.completions = Some(CompletionSettingsContent {
15376 words: Some(WordsCompletionMode::Enabled),
15377 words_min_length: Some(0),
15378 lsp_insert_mode: Some(LspInsertMode::Insert),
15379 ..Default::default()
15380 });
15381 });
15382
15383 let mut cx = EditorLspTestContext::new_rust(
15384 lsp::ServerCapabilities {
15385 completion_provider: Some(lsp::CompletionOptions {
15386 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15387 ..lsp::CompletionOptions::default()
15388 }),
15389 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15390 ..lsp::ServerCapabilities::default()
15391 },
15392 cx,
15393 )
15394 .await;
15395
15396 let _completion_requests_handler =
15397 cx.lsp
15398 .server
15399 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15400 Ok(Some(lsp::CompletionResponse::Array(vec![
15401 lsp::CompletionItem {
15402 label: "first".into(),
15403 ..lsp::CompletionItem::default()
15404 },
15405 lsp::CompletionItem {
15406 label: "last".into(),
15407 ..lsp::CompletionItem::default()
15408 },
15409 ])))
15410 });
15411
15412 cx.set_state(indoc! {"ˇ
15413 first
15414 last
15415 second
15416 "});
15417 cx.simulate_keystroke(".");
15418 cx.executor().run_until_parked();
15419 cx.condition(|editor, _| editor.context_menu_visible())
15420 .await;
15421 cx.update_editor(|editor, _, _| {
15422 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15423 {
15424 assert_eq!(
15425 completion_menu_entries(menu),
15426 &["first", "last", "second"],
15427 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15428 );
15429 } else {
15430 panic!("expected completion menu to be open");
15431 }
15432 });
15433}
15434
15435#[gpui::test]
15436async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15437 init_test(cx, |language_settings| {
15438 language_settings.defaults.completions = Some(CompletionSettingsContent {
15439 words: Some(WordsCompletionMode::Disabled),
15440 words_min_length: Some(0),
15441 lsp_insert_mode: Some(LspInsertMode::Insert),
15442 ..Default::default()
15443 });
15444 });
15445
15446 let mut cx = EditorLspTestContext::new_rust(
15447 lsp::ServerCapabilities {
15448 completion_provider: Some(lsp::CompletionOptions {
15449 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15450 ..lsp::CompletionOptions::default()
15451 }),
15452 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15453 ..lsp::ServerCapabilities::default()
15454 },
15455 cx,
15456 )
15457 .await;
15458
15459 let _completion_requests_handler =
15460 cx.lsp
15461 .server
15462 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15463 panic!("LSP completions should not be queried when dealing with word completions")
15464 });
15465
15466 cx.set_state(indoc! {"ˇ
15467 first
15468 last
15469 second
15470 "});
15471 cx.update_editor(|editor, window, cx| {
15472 editor.show_word_completions(&ShowWordCompletions, window, cx);
15473 });
15474 cx.executor().run_until_parked();
15475 cx.condition(|editor, _| editor.context_menu_visible())
15476 .await;
15477 cx.update_editor(|editor, _, _| {
15478 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15479 {
15480 assert_eq!(
15481 completion_menu_entries(menu),
15482 &["first", "last", "second"],
15483 "`ShowWordCompletions` action should show word completions"
15484 );
15485 } else {
15486 panic!("expected completion menu to be open");
15487 }
15488 });
15489
15490 cx.simulate_keystroke("l");
15491 cx.executor().run_until_parked();
15492 cx.condition(|editor, _| editor.context_menu_visible())
15493 .await;
15494 cx.update_editor(|editor, _, _| {
15495 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15496 {
15497 assert_eq!(
15498 completion_menu_entries(menu),
15499 &["last"],
15500 "After showing word completions, further editing should filter them and not query the LSP"
15501 );
15502 } else {
15503 panic!("expected completion menu to be open");
15504 }
15505 });
15506}
15507
15508#[gpui::test]
15509async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15510 init_test(cx, |language_settings| {
15511 language_settings.defaults.completions = Some(CompletionSettingsContent {
15512 words_min_length: Some(0),
15513 lsp: Some(false),
15514 lsp_insert_mode: Some(LspInsertMode::Insert),
15515 ..Default::default()
15516 });
15517 });
15518
15519 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15520
15521 cx.set_state(indoc! {"ˇ
15522 0_usize
15523 let
15524 33
15525 4.5f32
15526 "});
15527 cx.update_editor(|editor, window, cx| {
15528 editor.show_completions(&ShowCompletions, window, cx);
15529 });
15530 cx.executor().run_until_parked();
15531 cx.condition(|editor, _| editor.context_menu_visible())
15532 .await;
15533 cx.update_editor(|editor, window, cx| {
15534 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15535 {
15536 assert_eq!(
15537 completion_menu_entries(menu),
15538 &["let"],
15539 "With no digits in the completion query, no digits should be in the word completions"
15540 );
15541 } else {
15542 panic!("expected completion menu to be open");
15543 }
15544 editor.cancel(&Cancel, window, cx);
15545 });
15546
15547 cx.set_state(indoc! {"3ˇ
15548 0_usize
15549 let
15550 3
15551 33.35f32
15552 "});
15553 cx.update_editor(|editor, window, cx| {
15554 editor.show_completions(&ShowCompletions, window, cx);
15555 });
15556 cx.executor().run_until_parked();
15557 cx.condition(|editor, _| editor.context_menu_visible())
15558 .await;
15559 cx.update_editor(|editor, _, _| {
15560 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15561 {
15562 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15563 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15564 } else {
15565 panic!("expected completion menu to be open");
15566 }
15567 });
15568}
15569
15570#[gpui::test]
15571async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15572 init_test(cx, |language_settings| {
15573 language_settings.defaults.completions = Some(CompletionSettingsContent {
15574 words: Some(WordsCompletionMode::Enabled),
15575 words_min_length: Some(3),
15576 lsp_insert_mode: Some(LspInsertMode::Insert),
15577 ..Default::default()
15578 });
15579 });
15580
15581 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15582 cx.set_state(indoc! {"ˇ
15583 wow
15584 wowen
15585 wowser
15586 "});
15587 cx.simulate_keystroke("w");
15588 cx.executor().run_until_parked();
15589 cx.update_editor(|editor, _, _| {
15590 if editor.context_menu.borrow_mut().is_some() {
15591 panic!(
15592 "expected completion menu to be hidden, as words completion threshold is not met"
15593 );
15594 }
15595 });
15596
15597 cx.update_editor(|editor, window, cx| {
15598 editor.show_word_completions(&ShowWordCompletions, window, cx);
15599 });
15600 cx.executor().run_until_parked();
15601 cx.update_editor(|editor, window, cx| {
15602 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15603 {
15604 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");
15605 } else {
15606 panic!("expected completion menu to be open after the word completions are called with an action");
15607 }
15608
15609 editor.cancel(&Cancel, window, cx);
15610 });
15611 cx.update_editor(|editor, _, _| {
15612 if editor.context_menu.borrow_mut().is_some() {
15613 panic!("expected completion menu to be hidden after canceling");
15614 }
15615 });
15616
15617 cx.simulate_keystroke("o");
15618 cx.executor().run_until_parked();
15619 cx.update_editor(|editor, _, _| {
15620 if editor.context_menu.borrow_mut().is_some() {
15621 panic!(
15622 "expected completion menu to be hidden, as words completion threshold is not met still"
15623 );
15624 }
15625 });
15626
15627 cx.simulate_keystroke("w");
15628 cx.executor().run_until_parked();
15629 cx.update_editor(|editor, _, _| {
15630 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15631 {
15632 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15633 } else {
15634 panic!("expected completion menu to be open after the word completions threshold is met");
15635 }
15636 });
15637}
15638
15639#[gpui::test]
15640async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15641 init_test(cx, |language_settings| {
15642 language_settings.defaults.completions = Some(CompletionSettingsContent {
15643 words: Some(WordsCompletionMode::Enabled),
15644 words_min_length: Some(0),
15645 lsp_insert_mode: Some(LspInsertMode::Insert),
15646 ..Default::default()
15647 });
15648 });
15649
15650 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15651 cx.update_editor(|editor, _, _| {
15652 editor.disable_word_completions();
15653 });
15654 cx.set_state(indoc! {"ˇ
15655 wow
15656 wowen
15657 wowser
15658 "});
15659 cx.simulate_keystroke("w");
15660 cx.executor().run_until_parked();
15661 cx.update_editor(|editor, _, _| {
15662 if editor.context_menu.borrow_mut().is_some() {
15663 panic!(
15664 "expected completion menu to be hidden, as words completion are disabled for this editor"
15665 );
15666 }
15667 });
15668
15669 cx.update_editor(|editor, window, cx| {
15670 editor.show_word_completions(&ShowWordCompletions, window, cx);
15671 });
15672 cx.executor().run_until_parked();
15673 cx.update_editor(|editor, _, _| {
15674 if editor.context_menu.borrow_mut().is_some() {
15675 panic!(
15676 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15677 );
15678 }
15679 });
15680}
15681
15682#[gpui::test]
15683async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15684 init_test(cx, |language_settings| {
15685 language_settings.defaults.completions = Some(CompletionSettingsContent {
15686 words: Some(WordsCompletionMode::Disabled),
15687 words_min_length: Some(0),
15688 lsp_insert_mode: Some(LspInsertMode::Insert),
15689 ..Default::default()
15690 });
15691 });
15692
15693 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15694 cx.update_editor(|editor, _, _| {
15695 editor.set_completion_provider(None);
15696 });
15697 cx.set_state(indoc! {"ˇ
15698 wow
15699 wowen
15700 wowser
15701 "});
15702 cx.simulate_keystroke("w");
15703 cx.executor().run_until_parked();
15704 cx.update_editor(|editor, _, _| {
15705 if editor.context_menu.borrow_mut().is_some() {
15706 panic!("expected completion menu to be hidden, as disabled in settings");
15707 }
15708 });
15709}
15710
15711fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15712 let position = || lsp::Position {
15713 line: params.text_document_position.position.line,
15714 character: params.text_document_position.position.character,
15715 };
15716 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15717 range: lsp::Range {
15718 start: position(),
15719 end: position(),
15720 },
15721 new_text: text.to_string(),
15722 }))
15723}
15724
15725#[gpui::test]
15726async fn test_multiline_completion(cx: &mut TestAppContext) {
15727 init_test(cx, |_| {});
15728
15729 let fs = FakeFs::new(cx.executor());
15730 fs.insert_tree(
15731 path!("/a"),
15732 json!({
15733 "main.ts": "a",
15734 }),
15735 )
15736 .await;
15737
15738 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15739 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15740 let typescript_language = Arc::new(Language::new(
15741 LanguageConfig {
15742 name: "TypeScript".into(),
15743 matcher: LanguageMatcher {
15744 path_suffixes: vec!["ts".to_string()],
15745 ..LanguageMatcher::default()
15746 },
15747 line_comments: vec!["// ".into()],
15748 ..LanguageConfig::default()
15749 },
15750 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15751 ));
15752 language_registry.add(typescript_language.clone());
15753 let mut fake_servers = language_registry.register_fake_lsp(
15754 "TypeScript",
15755 FakeLspAdapter {
15756 capabilities: lsp::ServerCapabilities {
15757 completion_provider: Some(lsp::CompletionOptions {
15758 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15759 ..lsp::CompletionOptions::default()
15760 }),
15761 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15762 ..lsp::ServerCapabilities::default()
15763 },
15764 // Emulate vtsls label generation
15765 label_for_completion: Some(Box::new(|item, _| {
15766 let text = if let Some(description) = item
15767 .label_details
15768 .as_ref()
15769 .and_then(|label_details| label_details.description.as_ref())
15770 {
15771 format!("{} {}", item.label, description)
15772 } else if let Some(detail) = &item.detail {
15773 format!("{} {}", item.label, detail)
15774 } else {
15775 item.label.clone()
15776 };
15777 Some(language::CodeLabel::plain(text, None))
15778 })),
15779 ..FakeLspAdapter::default()
15780 },
15781 );
15782 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15783 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15784 let worktree_id = workspace
15785 .update(cx, |workspace, _window, cx| {
15786 workspace.project().update(cx, |project, cx| {
15787 project.worktrees(cx).next().unwrap().read(cx).id()
15788 })
15789 })
15790 .unwrap();
15791 let _buffer = project
15792 .update(cx, |project, cx| {
15793 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15794 })
15795 .await
15796 .unwrap();
15797 let editor = workspace
15798 .update(cx, |workspace, window, cx| {
15799 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15800 })
15801 .unwrap()
15802 .await
15803 .unwrap()
15804 .downcast::<Editor>()
15805 .unwrap();
15806 let fake_server = fake_servers.next().await.unwrap();
15807
15808 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15809 let multiline_label_2 = "a\nb\nc\n";
15810 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15811 let multiline_description = "d\ne\nf\n";
15812 let multiline_detail_2 = "g\nh\ni\n";
15813
15814 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15815 move |params, _| async move {
15816 Ok(Some(lsp::CompletionResponse::Array(vec![
15817 lsp::CompletionItem {
15818 label: multiline_label.to_string(),
15819 text_edit: gen_text_edit(¶ms, "new_text_1"),
15820 ..lsp::CompletionItem::default()
15821 },
15822 lsp::CompletionItem {
15823 label: "single line label 1".to_string(),
15824 detail: Some(multiline_detail.to_string()),
15825 text_edit: gen_text_edit(¶ms, "new_text_2"),
15826 ..lsp::CompletionItem::default()
15827 },
15828 lsp::CompletionItem {
15829 label: "single line label 2".to_string(),
15830 label_details: Some(lsp::CompletionItemLabelDetails {
15831 description: Some(multiline_description.to_string()),
15832 detail: None,
15833 }),
15834 text_edit: gen_text_edit(¶ms, "new_text_2"),
15835 ..lsp::CompletionItem::default()
15836 },
15837 lsp::CompletionItem {
15838 label: multiline_label_2.to_string(),
15839 detail: Some(multiline_detail_2.to_string()),
15840 text_edit: gen_text_edit(¶ms, "new_text_3"),
15841 ..lsp::CompletionItem::default()
15842 },
15843 lsp::CompletionItem {
15844 label: "Label with many spaces and \t but without newlines".to_string(),
15845 detail: Some(
15846 "Details with many spaces and \t but without newlines".to_string(),
15847 ),
15848 text_edit: gen_text_edit(¶ms, "new_text_4"),
15849 ..lsp::CompletionItem::default()
15850 },
15851 ])))
15852 },
15853 );
15854
15855 editor.update_in(cx, |editor, window, cx| {
15856 cx.focus_self(window);
15857 editor.move_to_end(&MoveToEnd, window, cx);
15858 editor.handle_input(".", window, cx);
15859 });
15860 cx.run_until_parked();
15861 completion_handle.next().await.unwrap();
15862
15863 editor.update(cx, |editor, _| {
15864 assert!(editor.context_menu_visible());
15865 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15866 {
15867 let completion_labels = menu
15868 .completions
15869 .borrow()
15870 .iter()
15871 .map(|c| c.label.text.clone())
15872 .collect::<Vec<_>>();
15873 assert_eq!(
15874 completion_labels,
15875 &[
15876 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15877 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15878 "single line label 2 d e f ",
15879 "a b c g h i ",
15880 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15881 ],
15882 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15883 );
15884
15885 for completion in menu
15886 .completions
15887 .borrow()
15888 .iter() {
15889 assert_eq!(
15890 completion.label.filter_range,
15891 0..completion.label.text.len(),
15892 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15893 );
15894 }
15895 } else {
15896 panic!("expected completion menu to be open");
15897 }
15898 });
15899}
15900
15901#[gpui::test]
15902async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15903 init_test(cx, |_| {});
15904 let mut cx = EditorLspTestContext::new_rust(
15905 lsp::ServerCapabilities {
15906 completion_provider: Some(lsp::CompletionOptions {
15907 trigger_characters: Some(vec![".".to_string()]),
15908 ..Default::default()
15909 }),
15910 ..Default::default()
15911 },
15912 cx,
15913 )
15914 .await;
15915 cx.lsp
15916 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15917 Ok(Some(lsp::CompletionResponse::Array(vec![
15918 lsp::CompletionItem {
15919 label: "first".into(),
15920 ..Default::default()
15921 },
15922 lsp::CompletionItem {
15923 label: "last".into(),
15924 ..Default::default()
15925 },
15926 ])))
15927 });
15928 cx.set_state("variableˇ");
15929 cx.simulate_keystroke(".");
15930 cx.executor().run_until_parked();
15931
15932 cx.update_editor(|editor, _, _| {
15933 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15934 {
15935 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15936 } else {
15937 panic!("expected completion menu to be open");
15938 }
15939 });
15940
15941 cx.update_editor(|editor, window, cx| {
15942 editor.move_page_down(&MovePageDown::default(), window, cx);
15943 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15944 {
15945 assert!(
15946 menu.selected_item == 1,
15947 "expected PageDown to select the last item from the context menu"
15948 );
15949 } else {
15950 panic!("expected completion menu to stay open after PageDown");
15951 }
15952 });
15953
15954 cx.update_editor(|editor, window, cx| {
15955 editor.move_page_up(&MovePageUp::default(), window, cx);
15956 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15957 {
15958 assert!(
15959 menu.selected_item == 0,
15960 "expected PageUp to select the first item from the context menu"
15961 );
15962 } else {
15963 panic!("expected completion menu to stay open after PageUp");
15964 }
15965 });
15966}
15967
15968#[gpui::test]
15969async fn test_as_is_completions(cx: &mut TestAppContext) {
15970 init_test(cx, |_| {});
15971 let mut cx = EditorLspTestContext::new_rust(
15972 lsp::ServerCapabilities {
15973 completion_provider: Some(lsp::CompletionOptions {
15974 ..Default::default()
15975 }),
15976 ..Default::default()
15977 },
15978 cx,
15979 )
15980 .await;
15981 cx.lsp
15982 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15983 Ok(Some(lsp::CompletionResponse::Array(vec![
15984 lsp::CompletionItem {
15985 label: "unsafe".into(),
15986 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15987 range: lsp::Range {
15988 start: lsp::Position {
15989 line: 1,
15990 character: 2,
15991 },
15992 end: lsp::Position {
15993 line: 1,
15994 character: 3,
15995 },
15996 },
15997 new_text: "unsafe".to_string(),
15998 })),
15999 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16000 ..Default::default()
16001 },
16002 ])))
16003 });
16004 cx.set_state("fn a() {}\n nˇ");
16005 cx.executor().run_until_parked();
16006 cx.update_editor(|editor, window, cx| {
16007 editor.trigger_completion_on_input("n", true, window, cx)
16008 });
16009 cx.executor().run_until_parked();
16010
16011 cx.update_editor(|editor, window, cx| {
16012 editor.confirm_completion(&Default::default(), window, cx)
16013 });
16014 cx.executor().run_until_parked();
16015 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16016}
16017
16018#[gpui::test]
16019async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16020 init_test(cx, |_| {});
16021 let language =
16022 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16023 let mut cx = EditorLspTestContext::new(
16024 language,
16025 lsp::ServerCapabilities {
16026 completion_provider: Some(lsp::CompletionOptions {
16027 ..lsp::CompletionOptions::default()
16028 }),
16029 ..lsp::ServerCapabilities::default()
16030 },
16031 cx,
16032 )
16033 .await;
16034
16035 cx.set_state(
16036 "#ifndef BAR_H
16037#define BAR_H
16038
16039#include <stdbool.h>
16040
16041int fn_branch(bool do_branch1, bool do_branch2);
16042
16043#endif // BAR_H
16044ˇ",
16045 );
16046 cx.executor().run_until_parked();
16047 cx.update_editor(|editor, window, cx| {
16048 editor.handle_input("#", window, cx);
16049 });
16050 cx.executor().run_until_parked();
16051 cx.update_editor(|editor, window, cx| {
16052 editor.handle_input("i", window, cx);
16053 });
16054 cx.executor().run_until_parked();
16055 cx.update_editor(|editor, window, cx| {
16056 editor.handle_input("n", window, cx);
16057 });
16058 cx.executor().run_until_parked();
16059 cx.assert_editor_state(
16060 "#ifndef BAR_H
16061#define BAR_H
16062
16063#include <stdbool.h>
16064
16065int fn_branch(bool do_branch1, bool do_branch2);
16066
16067#endif // BAR_H
16068#inˇ",
16069 );
16070
16071 cx.lsp
16072 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16073 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16074 is_incomplete: false,
16075 item_defaults: None,
16076 items: vec![lsp::CompletionItem {
16077 kind: Some(lsp::CompletionItemKind::SNIPPET),
16078 label_details: Some(lsp::CompletionItemLabelDetails {
16079 detail: Some("header".to_string()),
16080 description: None,
16081 }),
16082 label: " include".to_string(),
16083 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16084 range: lsp::Range {
16085 start: lsp::Position {
16086 line: 8,
16087 character: 1,
16088 },
16089 end: lsp::Position {
16090 line: 8,
16091 character: 1,
16092 },
16093 },
16094 new_text: "include \"$0\"".to_string(),
16095 })),
16096 sort_text: Some("40b67681include".to_string()),
16097 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16098 filter_text: Some("include".to_string()),
16099 insert_text: Some("include \"$0\"".to_string()),
16100 ..lsp::CompletionItem::default()
16101 }],
16102 })))
16103 });
16104 cx.update_editor(|editor, window, cx| {
16105 editor.show_completions(&ShowCompletions, window, cx);
16106 });
16107 cx.executor().run_until_parked();
16108 cx.update_editor(|editor, window, cx| {
16109 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16110 });
16111 cx.executor().run_until_parked();
16112 cx.assert_editor_state(
16113 "#ifndef BAR_H
16114#define BAR_H
16115
16116#include <stdbool.h>
16117
16118int fn_branch(bool do_branch1, bool do_branch2);
16119
16120#endif // BAR_H
16121#include \"ˇ\"",
16122 );
16123
16124 cx.lsp
16125 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16126 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16127 is_incomplete: true,
16128 item_defaults: None,
16129 items: vec![lsp::CompletionItem {
16130 kind: Some(lsp::CompletionItemKind::FILE),
16131 label: "AGL/".to_string(),
16132 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16133 range: lsp::Range {
16134 start: lsp::Position {
16135 line: 8,
16136 character: 10,
16137 },
16138 end: lsp::Position {
16139 line: 8,
16140 character: 11,
16141 },
16142 },
16143 new_text: "AGL/".to_string(),
16144 })),
16145 sort_text: Some("40b67681AGL/".to_string()),
16146 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16147 filter_text: Some("AGL/".to_string()),
16148 insert_text: Some("AGL/".to_string()),
16149 ..lsp::CompletionItem::default()
16150 }],
16151 })))
16152 });
16153 cx.update_editor(|editor, window, cx| {
16154 editor.show_completions(&ShowCompletions, window, cx);
16155 });
16156 cx.executor().run_until_parked();
16157 cx.update_editor(|editor, window, cx| {
16158 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16159 });
16160 cx.executor().run_until_parked();
16161 cx.assert_editor_state(
16162 r##"#ifndef BAR_H
16163#define BAR_H
16164
16165#include <stdbool.h>
16166
16167int fn_branch(bool do_branch1, bool do_branch2);
16168
16169#endif // BAR_H
16170#include "AGL/ˇ"##,
16171 );
16172
16173 cx.update_editor(|editor, window, cx| {
16174 editor.handle_input("\"", window, cx);
16175 });
16176 cx.executor().run_until_parked();
16177 cx.assert_editor_state(
16178 r##"#ifndef BAR_H
16179#define BAR_H
16180
16181#include <stdbool.h>
16182
16183int fn_branch(bool do_branch1, bool do_branch2);
16184
16185#endif // BAR_H
16186#include "AGL/"ˇ"##,
16187 );
16188}
16189
16190#[gpui::test]
16191async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16192 init_test(cx, |_| {});
16193
16194 let mut cx = EditorLspTestContext::new_rust(
16195 lsp::ServerCapabilities {
16196 completion_provider: Some(lsp::CompletionOptions {
16197 trigger_characters: Some(vec![".".to_string()]),
16198 resolve_provider: Some(true),
16199 ..Default::default()
16200 }),
16201 ..Default::default()
16202 },
16203 cx,
16204 )
16205 .await;
16206
16207 cx.set_state("fn main() { let a = 2ˇ; }");
16208 cx.simulate_keystroke(".");
16209 let completion_item = lsp::CompletionItem {
16210 label: "Some".into(),
16211 kind: Some(lsp::CompletionItemKind::SNIPPET),
16212 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16213 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16214 kind: lsp::MarkupKind::Markdown,
16215 value: "```rust\nSome(2)\n```".to_string(),
16216 })),
16217 deprecated: Some(false),
16218 sort_text: Some("Some".to_string()),
16219 filter_text: Some("Some".to_string()),
16220 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16221 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16222 range: lsp::Range {
16223 start: lsp::Position {
16224 line: 0,
16225 character: 22,
16226 },
16227 end: lsp::Position {
16228 line: 0,
16229 character: 22,
16230 },
16231 },
16232 new_text: "Some(2)".to_string(),
16233 })),
16234 additional_text_edits: Some(vec![lsp::TextEdit {
16235 range: lsp::Range {
16236 start: lsp::Position {
16237 line: 0,
16238 character: 20,
16239 },
16240 end: lsp::Position {
16241 line: 0,
16242 character: 22,
16243 },
16244 },
16245 new_text: "".to_string(),
16246 }]),
16247 ..Default::default()
16248 };
16249
16250 let closure_completion_item = completion_item.clone();
16251 let counter = Arc::new(AtomicUsize::new(0));
16252 let counter_clone = counter.clone();
16253 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16254 let task_completion_item = closure_completion_item.clone();
16255 counter_clone.fetch_add(1, atomic::Ordering::Release);
16256 async move {
16257 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16258 is_incomplete: true,
16259 item_defaults: None,
16260 items: vec![task_completion_item],
16261 })))
16262 }
16263 });
16264
16265 cx.condition(|editor, _| editor.context_menu_visible())
16266 .await;
16267 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16268 assert!(request.next().await.is_some());
16269 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16270
16271 cx.simulate_keystrokes("S o m");
16272 cx.condition(|editor, _| editor.context_menu_visible())
16273 .await;
16274 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16275 assert!(request.next().await.is_some());
16276 assert!(request.next().await.is_some());
16277 assert!(request.next().await.is_some());
16278 request.close();
16279 assert!(request.next().await.is_none());
16280 assert_eq!(
16281 counter.load(atomic::Ordering::Acquire),
16282 4,
16283 "With the completions menu open, only one LSP request should happen per input"
16284 );
16285}
16286
16287#[gpui::test]
16288async fn test_toggle_comment(cx: &mut TestAppContext) {
16289 init_test(cx, |_| {});
16290 let mut cx = EditorTestContext::new(cx).await;
16291 let language = Arc::new(Language::new(
16292 LanguageConfig {
16293 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16294 ..Default::default()
16295 },
16296 Some(tree_sitter_rust::LANGUAGE.into()),
16297 ));
16298 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16299
16300 // If multiple selections intersect a line, the line is only toggled once.
16301 cx.set_state(indoc! {"
16302 fn a() {
16303 «//b();
16304 ˇ»// «c();
16305 //ˇ» d();
16306 }
16307 "});
16308
16309 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16310
16311 cx.assert_editor_state(indoc! {"
16312 fn a() {
16313 «b();
16314 ˇ»«c();
16315 ˇ» d();
16316 }
16317 "});
16318
16319 // The comment prefix is inserted at the same column for every line in a
16320 // selection.
16321 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16322
16323 cx.assert_editor_state(indoc! {"
16324 fn a() {
16325 // «b();
16326 ˇ»// «c();
16327 ˇ» // d();
16328 }
16329 "});
16330
16331 // If a selection ends at the beginning of a line, that line is not toggled.
16332 cx.set_selections_state(indoc! {"
16333 fn a() {
16334 // b();
16335 «// c();
16336 ˇ» // d();
16337 }
16338 "});
16339
16340 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16341
16342 cx.assert_editor_state(indoc! {"
16343 fn a() {
16344 // b();
16345 «c();
16346 ˇ» // d();
16347 }
16348 "});
16349
16350 // If a selection span a single line and is empty, the line is toggled.
16351 cx.set_state(indoc! {"
16352 fn a() {
16353 a();
16354 b();
16355 ˇ
16356 }
16357 "});
16358
16359 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16360
16361 cx.assert_editor_state(indoc! {"
16362 fn a() {
16363 a();
16364 b();
16365 //•ˇ
16366 }
16367 "});
16368
16369 // If a selection span multiple lines, empty lines are not toggled.
16370 cx.set_state(indoc! {"
16371 fn a() {
16372 «a();
16373
16374 c();ˇ»
16375 }
16376 "});
16377
16378 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16379
16380 cx.assert_editor_state(indoc! {"
16381 fn a() {
16382 // «a();
16383
16384 // c();ˇ»
16385 }
16386 "});
16387
16388 // If a selection includes multiple comment prefixes, all lines are uncommented.
16389 cx.set_state(indoc! {"
16390 fn a() {
16391 «// a();
16392 /// b();
16393 //! c();ˇ»
16394 }
16395 "});
16396
16397 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16398
16399 cx.assert_editor_state(indoc! {"
16400 fn a() {
16401 «a();
16402 b();
16403 c();ˇ»
16404 }
16405 "});
16406}
16407
16408#[gpui::test]
16409async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16410 init_test(cx, |_| {});
16411 let mut cx = EditorTestContext::new(cx).await;
16412 let language = Arc::new(Language::new(
16413 LanguageConfig {
16414 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16415 ..Default::default()
16416 },
16417 Some(tree_sitter_rust::LANGUAGE.into()),
16418 ));
16419 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16420
16421 let toggle_comments = &ToggleComments {
16422 advance_downwards: false,
16423 ignore_indent: true,
16424 };
16425
16426 // If multiple selections intersect a line, the line is only toggled once.
16427 cx.set_state(indoc! {"
16428 fn a() {
16429 // «b();
16430 // c();
16431 // ˇ» d();
16432 }
16433 "});
16434
16435 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16436
16437 cx.assert_editor_state(indoc! {"
16438 fn a() {
16439 «b();
16440 c();
16441 ˇ» d();
16442 }
16443 "});
16444
16445 // The comment prefix is inserted at the beginning of each line
16446 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16447
16448 cx.assert_editor_state(indoc! {"
16449 fn a() {
16450 // «b();
16451 // c();
16452 // ˇ» d();
16453 }
16454 "});
16455
16456 // If a selection ends at the beginning of a line, that line is not toggled.
16457 cx.set_selections_state(indoc! {"
16458 fn a() {
16459 // b();
16460 // «c();
16461 ˇ»// d();
16462 }
16463 "});
16464
16465 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16466
16467 cx.assert_editor_state(indoc! {"
16468 fn a() {
16469 // b();
16470 «c();
16471 ˇ»// d();
16472 }
16473 "});
16474
16475 // If a selection span a single line and is empty, the line is toggled.
16476 cx.set_state(indoc! {"
16477 fn a() {
16478 a();
16479 b();
16480 ˇ
16481 }
16482 "});
16483
16484 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16485
16486 cx.assert_editor_state(indoc! {"
16487 fn a() {
16488 a();
16489 b();
16490 //ˇ
16491 }
16492 "});
16493
16494 // If a selection span multiple lines, empty lines are not toggled.
16495 cx.set_state(indoc! {"
16496 fn a() {
16497 «a();
16498
16499 c();ˇ»
16500 }
16501 "});
16502
16503 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16504
16505 cx.assert_editor_state(indoc! {"
16506 fn a() {
16507 // «a();
16508
16509 // c();ˇ»
16510 }
16511 "});
16512
16513 // If a selection includes multiple comment prefixes, all lines are uncommented.
16514 cx.set_state(indoc! {"
16515 fn a() {
16516 // «a();
16517 /// b();
16518 //! c();ˇ»
16519 }
16520 "});
16521
16522 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16523
16524 cx.assert_editor_state(indoc! {"
16525 fn a() {
16526 «a();
16527 b();
16528 c();ˇ»
16529 }
16530 "});
16531}
16532
16533#[gpui::test]
16534async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16535 init_test(cx, |_| {});
16536
16537 let language = Arc::new(Language::new(
16538 LanguageConfig {
16539 line_comments: vec!["// ".into()],
16540 ..Default::default()
16541 },
16542 Some(tree_sitter_rust::LANGUAGE.into()),
16543 ));
16544
16545 let mut cx = EditorTestContext::new(cx).await;
16546
16547 cx.language_registry().add(language.clone());
16548 cx.update_buffer(|buffer, cx| {
16549 buffer.set_language(Some(language), cx);
16550 });
16551
16552 let toggle_comments = &ToggleComments {
16553 advance_downwards: true,
16554 ignore_indent: false,
16555 };
16556
16557 // Single cursor on one line -> advance
16558 // Cursor moves horizontally 3 characters as well on non-blank line
16559 cx.set_state(indoc!(
16560 "fn a() {
16561 ˇdog();
16562 cat();
16563 }"
16564 ));
16565 cx.update_editor(|editor, window, cx| {
16566 editor.toggle_comments(toggle_comments, window, cx);
16567 });
16568 cx.assert_editor_state(indoc!(
16569 "fn a() {
16570 // dog();
16571 catˇ();
16572 }"
16573 ));
16574
16575 // Single selection on one line -> don't advance
16576 cx.set_state(indoc!(
16577 "fn a() {
16578 «dog()ˇ»;
16579 cat();
16580 }"
16581 ));
16582 cx.update_editor(|editor, window, cx| {
16583 editor.toggle_comments(toggle_comments, window, cx);
16584 });
16585 cx.assert_editor_state(indoc!(
16586 "fn a() {
16587 // «dog()ˇ»;
16588 cat();
16589 }"
16590 ));
16591
16592 // Multiple cursors on one line -> advance
16593 cx.set_state(indoc!(
16594 "fn a() {
16595 ˇdˇog();
16596 cat();
16597 }"
16598 ));
16599 cx.update_editor(|editor, window, cx| {
16600 editor.toggle_comments(toggle_comments, window, cx);
16601 });
16602 cx.assert_editor_state(indoc!(
16603 "fn a() {
16604 // dog();
16605 catˇ(ˇ);
16606 }"
16607 ));
16608
16609 // Multiple cursors on one line, with selection -> don't advance
16610 cx.set_state(indoc!(
16611 "fn a() {
16612 ˇdˇog«()ˇ»;
16613 cat();
16614 }"
16615 ));
16616 cx.update_editor(|editor, window, cx| {
16617 editor.toggle_comments(toggle_comments, window, cx);
16618 });
16619 cx.assert_editor_state(indoc!(
16620 "fn a() {
16621 // ˇdˇog«()ˇ»;
16622 cat();
16623 }"
16624 ));
16625
16626 // Single cursor on one line -> advance
16627 // Cursor moves to column 0 on blank line
16628 cx.set_state(indoc!(
16629 "fn a() {
16630 ˇdog();
16631
16632 cat();
16633 }"
16634 ));
16635 cx.update_editor(|editor, window, cx| {
16636 editor.toggle_comments(toggle_comments, window, cx);
16637 });
16638 cx.assert_editor_state(indoc!(
16639 "fn a() {
16640 // dog();
16641 ˇ
16642 cat();
16643 }"
16644 ));
16645
16646 // Single cursor on one line -> advance
16647 // Cursor starts and ends at column 0
16648 cx.set_state(indoc!(
16649 "fn a() {
16650 ˇ dog();
16651 cat();
16652 }"
16653 ));
16654 cx.update_editor(|editor, window, cx| {
16655 editor.toggle_comments(toggle_comments, window, cx);
16656 });
16657 cx.assert_editor_state(indoc!(
16658 "fn a() {
16659 // dog();
16660 ˇ cat();
16661 }"
16662 ));
16663}
16664
16665#[gpui::test]
16666async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16667 init_test(cx, |_| {});
16668
16669 let mut cx = EditorTestContext::new(cx).await;
16670
16671 let html_language = Arc::new(
16672 Language::new(
16673 LanguageConfig {
16674 name: "HTML".into(),
16675 block_comment: Some(BlockCommentConfig {
16676 start: "<!-- ".into(),
16677 prefix: "".into(),
16678 end: " -->".into(),
16679 tab_size: 0,
16680 }),
16681 ..Default::default()
16682 },
16683 Some(tree_sitter_html::LANGUAGE.into()),
16684 )
16685 .with_injection_query(
16686 r#"
16687 (script_element
16688 (raw_text) @injection.content
16689 (#set! injection.language "javascript"))
16690 "#,
16691 )
16692 .unwrap(),
16693 );
16694
16695 let javascript_language = Arc::new(Language::new(
16696 LanguageConfig {
16697 name: "JavaScript".into(),
16698 line_comments: vec!["// ".into()],
16699 ..Default::default()
16700 },
16701 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16702 ));
16703
16704 cx.language_registry().add(html_language.clone());
16705 cx.language_registry().add(javascript_language);
16706 cx.update_buffer(|buffer, cx| {
16707 buffer.set_language(Some(html_language), cx);
16708 });
16709
16710 // Toggle comments for empty selections
16711 cx.set_state(
16712 &r#"
16713 <p>A</p>ˇ
16714 <p>B</p>ˇ
16715 <p>C</p>ˇ
16716 "#
16717 .unindent(),
16718 );
16719 cx.update_editor(|editor, window, cx| {
16720 editor.toggle_comments(&ToggleComments::default(), window, cx)
16721 });
16722 cx.assert_editor_state(
16723 &r#"
16724 <!-- <p>A</p>ˇ -->
16725 <!-- <p>B</p>ˇ -->
16726 <!-- <p>C</p>ˇ -->
16727 "#
16728 .unindent(),
16729 );
16730 cx.update_editor(|editor, window, cx| {
16731 editor.toggle_comments(&ToggleComments::default(), window, cx)
16732 });
16733 cx.assert_editor_state(
16734 &r#"
16735 <p>A</p>ˇ
16736 <p>B</p>ˇ
16737 <p>C</p>ˇ
16738 "#
16739 .unindent(),
16740 );
16741
16742 // Toggle comments for mixture of empty and non-empty selections, where
16743 // multiple selections occupy a given line.
16744 cx.set_state(
16745 &r#"
16746 <p>A«</p>
16747 <p>ˇ»B</p>ˇ
16748 <p>C«</p>
16749 <p>ˇ»D</p>ˇ
16750 "#
16751 .unindent(),
16752 );
16753
16754 cx.update_editor(|editor, window, cx| {
16755 editor.toggle_comments(&ToggleComments::default(), window, cx)
16756 });
16757 cx.assert_editor_state(
16758 &r#"
16759 <!-- <p>A«</p>
16760 <p>ˇ»B</p>ˇ -->
16761 <!-- <p>C«</p>
16762 <p>ˇ»D</p>ˇ -->
16763 "#
16764 .unindent(),
16765 );
16766 cx.update_editor(|editor, window, cx| {
16767 editor.toggle_comments(&ToggleComments::default(), window, cx)
16768 });
16769 cx.assert_editor_state(
16770 &r#"
16771 <p>A«</p>
16772 <p>ˇ»B</p>ˇ
16773 <p>C«</p>
16774 <p>ˇ»D</p>ˇ
16775 "#
16776 .unindent(),
16777 );
16778
16779 // Toggle comments when different languages are active for different
16780 // selections.
16781 cx.set_state(
16782 &r#"
16783 ˇ<script>
16784 ˇvar x = new Y();
16785 ˇ</script>
16786 "#
16787 .unindent(),
16788 );
16789 cx.executor().run_until_parked();
16790 cx.update_editor(|editor, window, cx| {
16791 editor.toggle_comments(&ToggleComments::default(), window, cx)
16792 });
16793 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16794 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16795 cx.assert_editor_state(
16796 &r#"
16797 <!-- ˇ<script> -->
16798 // ˇvar x = new Y();
16799 <!-- ˇ</script> -->
16800 "#
16801 .unindent(),
16802 );
16803}
16804
16805#[gpui::test]
16806fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16807 init_test(cx, |_| {});
16808
16809 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16810 let multibuffer = cx.new(|cx| {
16811 let mut multibuffer = MultiBuffer::new(ReadWrite);
16812 multibuffer.push_excerpts(
16813 buffer.clone(),
16814 [
16815 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16816 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16817 ],
16818 cx,
16819 );
16820 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16821 multibuffer
16822 });
16823
16824 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16825 editor.update_in(cx, |editor, window, cx| {
16826 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16827 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16828 s.select_ranges([
16829 Point::new(0, 0)..Point::new(0, 0),
16830 Point::new(1, 0)..Point::new(1, 0),
16831 ])
16832 });
16833
16834 editor.handle_input("X", window, cx);
16835 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16836 assert_eq!(
16837 editor.selections.ranges(&editor.display_snapshot(cx)),
16838 [
16839 Point::new(0, 1)..Point::new(0, 1),
16840 Point::new(1, 1)..Point::new(1, 1),
16841 ]
16842 );
16843
16844 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16845 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16846 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16847 });
16848 editor.backspace(&Default::default(), window, cx);
16849 assert_eq!(editor.text(cx), "Xa\nbbb");
16850 assert_eq!(
16851 editor.selections.ranges(&editor.display_snapshot(cx)),
16852 [Point::new(1, 0)..Point::new(1, 0)]
16853 );
16854
16855 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16856 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16857 });
16858 editor.backspace(&Default::default(), window, cx);
16859 assert_eq!(editor.text(cx), "X\nbb");
16860 assert_eq!(
16861 editor.selections.ranges(&editor.display_snapshot(cx)),
16862 [Point::new(0, 1)..Point::new(0, 1)]
16863 );
16864 });
16865}
16866
16867#[gpui::test]
16868fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16869 init_test(cx, |_| {});
16870
16871 let markers = vec![('[', ']').into(), ('(', ')').into()];
16872 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16873 indoc! {"
16874 [aaaa
16875 (bbbb]
16876 cccc)",
16877 },
16878 markers.clone(),
16879 );
16880 let excerpt_ranges = markers.into_iter().map(|marker| {
16881 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16882 ExcerptRange::new(context)
16883 });
16884 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16885 let multibuffer = cx.new(|cx| {
16886 let mut multibuffer = MultiBuffer::new(ReadWrite);
16887 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16888 multibuffer
16889 });
16890
16891 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16892 editor.update_in(cx, |editor, window, cx| {
16893 let (expected_text, selection_ranges) = marked_text_ranges(
16894 indoc! {"
16895 aaaa
16896 bˇbbb
16897 bˇbbˇb
16898 cccc"
16899 },
16900 true,
16901 );
16902 assert_eq!(editor.text(cx), expected_text);
16903 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16904 s.select_ranges(
16905 selection_ranges
16906 .iter()
16907 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16908 )
16909 });
16910
16911 editor.handle_input("X", window, cx);
16912
16913 let (expected_text, expected_selections) = marked_text_ranges(
16914 indoc! {"
16915 aaaa
16916 bXˇbbXb
16917 bXˇbbXˇb
16918 cccc"
16919 },
16920 false,
16921 );
16922 assert_eq!(editor.text(cx), expected_text);
16923 assert_eq!(
16924 editor.selections.ranges(&editor.display_snapshot(cx)),
16925 expected_selections
16926 .iter()
16927 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16928 .collect::<Vec<_>>()
16929 );
16930
16931 editor.newline(&Newline, window, cx);
16932 let (expected_text, expected_selections) = marked_text_ranges(
16933 indoc! {"
16934 aaaa
16935 bX
16936 ˇbbX
16937 b
16938 bX
16939 ˇbbX
16940 ˇb
16941 cccc"
16942 },
16943 false,
16944 );
16945 assert_eq!(editor.text(cx), expected_text);
16946 assert_eq!(
16947 editor.selections.ranges(&editor.display_snapshot(cx)),
16948 expected_selections
16949 .iter()
16950 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16951 .collect::<Vec<_>>()
16952 );
16953 });
16954}
16955
16956#[gpui::test]
16957fn test_refresh_selections(cx: &mut TestAppContext) {
16958 init_test(cx, |_| {});
16959
16960 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16961 let mut excerpt1_id = None;
16962 let multibuffer = cx.new(|cx| {
16963 let mut multibuffer = MultiBuffer::new(ReadWrite);
16964 excerpt1_id = multibuffer
16965 .push_excerpts(
16966 buffer.clone(),
16967 [
16968 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16969 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16970 ],
16971 cx,
16972 )
16973 .into_iter()
16974 .next();
16975 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16976 multibuffer
16977 });
16978
16979 let editor = cx.add_window(|window, cx| {
16980 let mut editor = build_editor(multibuffer.clone(), window, cx);
16981 let snapshot = editor.snapshot(window, cx);
16982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16983 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16984 });
16985 editor.begin_selection(
16986 Point::new(2, 1).to_display_point(&snapshot),
16987 true,
16988 1,
16989 window,
16990 cx,
16991 );
16992 assert_eq!(
16993 editor.selections.ranges(&editor.display_snapshot(cx)),
16994 [
16995 Point::new(1, 3)..Point::new(1, 3),
16996 Point::new(2, 1)..Point::new(2, 1),
16997 ]
16998 );
16999 editor
17000 });
17001
17002 // Refreshing selections is a no-op when excerpts haven't changed.
17003 _ = editor.update(cx, |editor, window, cx| {
17004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17005 assert_eq!(
17006 editor.selections.ranges(&editor.display_snapshot(cx)),
17007 [
17008 Point::new(1, 3)..Point::new(1, 3),
17009 Point::new(2, 1)..Point::new(2, 1),
17010 ]
17011 );
17012 });
17013
17014 multibuffer.update(cx, |multibuffer, cx| {
17015 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17016 });
17017 _ = editor.update(cx, |editor, window, cx| {
17018 // Removing an excerpt causes the first selection to become degenerate.
17019 assert_eq!(
17020 editor.selections.ranges(&editor.display_snapshot(cx)),
17021 [
17022 Point::new(0, 0)..Point::new(0, 0),
17023 Point::new(0, 1)..Point::new(0, 1)
17024 ]
17025 );
17026
17027 // Refreshing selections will relocate the first selection to the original buffer
17028 // location.
17029 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17030 assert_eq!(
17031 editor.selections.ranges(&editor.display_snapshot(cx)),
17032 [
17033 Point::new(0, 1)..Point::new(0, 1),
17034 Point::new(0, 3)..Point::new(0, 3)
17035 ]
17036 );
17037 assert!(editor.selections.pending_anchor().is_some());
17038 });
17039}
17040
17041#[gpui::test]
17042fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17043 init_test(cx, |_| {});
17044
17045 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17046 let mut excerpt1_id = None;
17047 let multibuffer = cx.new(|cx| {
17048 let mut multibuffer = MultiBuffer::new(ReadWrite);
17049 excerpt1_id = multibuffer
17050 .push_excerpts(
17051 buffer.clone(),
17052 [
17053 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17054 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17055 ],
17056 cx,
17057 )
17058 .into_iter()
17059 .next();
17060 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17061 multibuffer
17062 });
17063
17064 let editor = cx.add_window(|window, cx| {
17065 let mut editor = build_editor(multibuffer.clone(), window, cx);
17066 let snapshot = editor.snapshot(window, cx);
17067 editor.begin_selection(
17068 Point::new(1, 3).to_display_point(&snapshot),
17069 false,
17070 1,
17071 window,
17072 cx,
17073 );
17074 assert_eq!(
17075 editor.selections.ranges(&editor.display_snapshot(cx)),
17076 [Point::new(1, 3)..Point::new(1, 3)]
17077 );
17078 editor
17079 });
17080
17081 multibuffer.update(cx, |multibuffer, cx| {
17082 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17083 });
17084 _ = editor.update(cx, |editor, window, cx| {
17085 assert_eq!(
17086 editor.selections.ranges(&editor.display_snapshot(cx)),
17087 [Point::new(0, 0)..Point::new(0, 0)]
17088 );
17089
17090 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17092 assert_eq!(
17093 editor.selections.ranges(&editor.display_snapshot(cx)),
17094 [Point::new(0, 3)..Point::new(0, 3)]
17095 );
17096 assert!(editor.selections.pending_anchor().is_some());
17097 });
17098}
17099
17100#[gpui::test]
17101async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17102 init_test(cx, |_| {});
17103
17104 let language = Arc::new(
17105 Language::new(
17106 LanguageConfig {
17107 brackets: BracketPairConfig {
17108 pairs: vec![
17109 BracketPair {
17110 start: "{".to_string(),
17111 end: "}".to_string(),
17112 close: true,
17113 surround: true,
17114 newline: true,
17115 },
17116 BracketPair {
17117 start: "/* ".to_string(),
17118 end: " */".to_string(),
17119 close: true,
17120 surround: true,
17121 newline: true,
17122 },
17123 ],
17124 ..Default::default()
17125 },
17126 ..Default::default()
17127 },
17128 Some(tree_sitter_rust::LANGUAGE.into()),
17129 )
17130 .with_indents_query("")
17131 .unwrap(),
17132 );
17133
17134 let text = concat!(
17135 "{ }\n", //
17136 " x\n", //
17137 " /* */\n", //
17138 "x\n", //
17139 "{{} }\n", //
17140 );
17141
17142 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17143 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17144 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17145 editor
17146 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17147 .await;
17148
17149 editor.update_in(cx, |editor, window, cx| {
17150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17151 s.select_display_ranges([
17152 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17153 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17154 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17155 ])
17156 });
17157 editor.newline(&Newline, window, cx);
17158
17159 assert_eq!(
17160 editor.buffer().read(cx).read(cx).text(),
17161 concat!(
17162 "{ \n", // Suppress rustfmt
17163 "\n", //
17164 "}\n", //
17165 " x\n", //
17166 " /* \n", //
17167 " \n", //
17168 " */\n", //
17169 "x\n", //
17170 "{{} \n", //
17171 "}\n", //
17172 )
17173 );
17174 });
17175}
17176
17177#[gpui::test]
17178fn test_highlighted_ranges(cx: &mut TestAppContext) {
17179 init_test(cx, |_| {});
17180
17181 let editor = cx.add_window(|window, cx| {
17182 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17183 build_editor(buffer, window, cx)
17184 });
17185
17186 _ = editor.update(cx, |editor, window, cx| {
17187 struct Type1;
17188 struct Type2;
17189
17190 let buffer = editor.buffer.read(cx).snapshot(cx);
17191
17192 let anchor_range =
17193 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17194
17195 editor.highlight_background::<Type1>(
17196 &[
17197 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17198 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17199 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17200 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17201 ],
17202 |_, _| Hsla::red(),
17203 cx,
17204 );
17205 editor.highlight_background::<Type2>(
17206 &[
17207 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17208 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17209 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17210 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17211 ],
17212 |_, _| Hsla::green(),
17213 cx,
17214 );
17215
17216 let snapshot = editor.snapshot(window, cx);
17217 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17218 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17219 &snapshot,
17220 cx.theme(),
17221 );
17222 assert_eq!(
17223 highlighted_ranges,
17224 &[
17225 (
17226 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17227 Hsla::green(),
17228 ),
17229 (
17230 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17231 Hsla::red(),
17232 ),
17233 (
17234 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17235 Hsla::green(),
17236 ),
17237 (
17238 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17239 Hsla::red(),
17240 ),
17241 ]
17242 );
17243 assert_eq!(
17244 editor.sorted_background_highlights_in_range(
17245 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17246 &snapshot,
17247 cx.theme(),
17248 ),
17249 &[(
17250 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17251 Hsla::red(),
17252 )]
17253 );
17254 });
17255}
17256
17257#[gpui::test]
17258async fn test_following(cx: &mut TestAppContext) {
17259 init_test(cx, |_| {});
17260
17261 let fs = FakeFs::new(cx.executor());
17262 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17263
17264 let buffer = project.update(cx, |project, cx| {
17265 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17266 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17267 });
17268 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17269 let follower = cx.update(|cx| {
17270 cx.open_window(
17271 WindowOptions {
17272 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17273 gpui::Point::new(px(0.), px(0.)),
17274 gpui::Point::new(px(10.), px(80.)),
17275 ))),
17276 ..Default::default()
17277 },
17278 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17279 )
17280 .unwrap()
17281 });
17282
17283 let is_still_following = Rc::new(RefCell::new(true));
17284 let follower_edit_event_count = Rc::new(RefCell::new(0));
17285 let pending_update = Rc::new(RefCell::new(None));
17286 let leader_entity = leader.root(cx).unwrap();
17287 let follower_entity = follower.root(cx).unwrap();
17288 _ = follower.update(cx, {
17289 let update = pending_update.clone();
17290 let is_still_following = is_still_following.clone();
17291 let follower_edit_event_count = follower_edit_event_count.clone();
17292 |_, window, cx| {
17293 cx.subscribe_in(
17294 &leader_entity,
17295 window,
17296 move |_, leader, event, window, cx| {
17297 leader.read(cx).add_event_to_update_proto(
17298 event,
17299 &mut update.borrow_mut(),
17300 window,
17301 cx,
17302 );
17303 },
17304 )
17305 .detach();
17306
17307 cx.subscribe_in(
17308 &follower_entity,
17309 window,
17310 move |_, _, event: &EditorEvent, _window, _cx| {
17311 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17312 *is_still_following.borrow_mut() = false;
17313 }
17314
17315 if let EditorEvent::BufferEdited = event {
17316 *follower_edit_event_count.borrow_mut() += 1;
17317 }
17318 },
17319 )
17320 .detach();
17321 }
17322 });
17323
17324 // Update the selections only
17325 _ = leader.update(cx, |leader, window, cx| {
17326 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17327 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17328 });
17329 });
17330 follower
17331 .update(cx, |follower, window, cx| {
17332 follower.apply_update_proto(
17333 &project,
17334 pending_update.borrow_mut().take().unwrap(),
17335 window,
17336 cx,
17337 )
17338 })
17339 .unwrap()
17340 .await
17341 .unwrap();
17342 _ = follower.update(cx, |follower, _, cx| {
17343 assert_eq!(
17344 follower.selections.ranges(&follower.display_snapshot(cx)),
17345 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17346 );
17347 });
17348 assert!(*is_still_following.borrow());
17349 assert_eq!(*follower_edit_event_count.borrow(), 0);
17350
17351 // Update the scroll position only
17352 _ = leader.update(cx, |leader, window, cx| {
17353 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17354 });
17355 follower
17356 .update(cx, |follower, window, cx| {
17357 follower.apply_update_proto(
17358 &project,
17359 pending_update.borrow_mut().take().unwrap(),
17360 window,
17361 cx,
17362 )
17363 })
17364 .unwrap()
17365 .await
17366 .unwrap();
17367 assert_eq!(
17368 follower
17369 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17370 .unwrap(),
17371 gpui::Point::new(1.5, 3.5)
17372 );
17373 assert!(*is_still_following.borrow());
17374 assert_eq!(*follower_edit_event_count.borrow(), 0);
17375
17376 // Update the selections and scroll position. The follower's scroll position is updated
17377 // via autoscroll, not via the leader's exact scroll position.
17378 _ = leader.update(cx, |leader, window, cx| {
17379 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17380 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17381 });
17382 leader.request_autoscroll(Autoscroll::newest(), cx);
17383 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17384 });
17385 follower
17386 .update(cx, |follower, window, cx| {
17387 follower.apply_update_proto(
17388 &project,
17389 pending_update.borrow_mut().take().unwrap(),
17390 window,
17391 cx,
17392 )
17393 })
17394 .unwrap()
17395 .await
17396 .unwrap();
17397 _ = follower.update(cx, |follower, _, cx| {
17398 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17399 assert_eq!(
17400 follower.selections.ranges(&follower.display_snapshot(cx)),
17401 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17402 );
17403 });
17404 assert!(*is_still_following.borrow());
17405
17406 // Creating a pending selection that precedes another selection
17407 _ = leader.update(cx, |leader, window, cx| {
17408 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17409 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17410 });
17411 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17412 });
17413 follower
17414 .update(cx, |follower, window, cx| {
17415 follower.apply_update_proto(
17416 &project,
17417 pending_update.borrow_mut().take().unwrap(),
17418 window,
17419 cx,
17420 )
17421 })
17422 .unwrap()
17423 .await
17424 .unwrap();
17425 _ = follower.update(cx, |follower, _, cx| {
17426 assert_eq!(
17427 follower.selections.ranges(&follower.display_snapshot(cx)),
17428 vec![
17429 MultiBufferOffset(0)..MultiBufferOffset(0),
17430 MultiBufferOffset(1)..MultiBufferOffset(1)
17431 ]
17432 );
17433 });
17434 assert!(*is_still_following.borrow());
17435
17436 // Extend the pending selection so that it surrounds another selection
17437 _ = leader.update(cx, |leader, window, cx| {
17438 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17439 });
17440 follower
17441 .update(cx, |follower, window, cx| {
17442 follower.apply_update_proto(
17443 &project,
17444 pending_update.borrow_mut().take().unwrap(),
17445 window,
17446 cx,
17447 )
17448 })
17449 .unwrap()
17450 .await
17451 .unwrap();
17452 _ = follower.update(cx, |follower, _, cx| {
17453 assert_eq!(
17454 follower.selections.ranges(&follower.display_snapshot(cx)),
17455 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17456 );
17457 });
17458
17459 // Scrolling locally breaks the follow
17460 _ = follower.update(cx, |follower, window, cx| {
17461 let top_anchor = follower
17462 .buffer()
17463 .read(cx)
17464 .read(cx)
17465 .anchor_after(MultiBufferOffset(0));
17466 follower.set_scroll_anchor(
17467 ScrollAnchor {
17468 anchor: top_anchor,
17469 offset: gpui::Point::new(0.0, 0.5),
17470 },
17471 window,
17472 cx,
17473 );
17474 });
17475 assert!(!(*is_still_following.borrow()));
17476}
17477
17478#[gpui::test]
17479async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17480 init_test(cx, |_| {});
17481
17482 let fs = FakeFs::new(cx.executor());
17483 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17484 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17485 let pane = workspace
17486 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17487 .unwrap();
17488
17489 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17490
17491 let leader = pane.update_in(cx, |_, window, cx| {
17492 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17493 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17494 });
17495
17496 // Start following the editor when it has no excerpts.
17497 let mut state_message =
17498 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17499 let workspace_entity = workspace.root(cx).unwrap();
17500 let follower_1 = cx
17501 .update_window(*workspace.deref(), |_, window, cx| {
17502 Editor::from_state_proto(
17503 workspace_entity,
17504 ViewId {
17505 creator: CollaboratorId::PeerId(PeerId::default()),
17506 id: 0,
17507 },
17508 &mut state_message,
17509 window,
17510 cx,
17511 )
17512 })
17513 .unwrap()
17514 .unwrap()
17515 .await
17516 .unwrap();
17517
17518 let update_message = Rc::new(RefCell::new(None));
17519 follower_1.update_in(cx, {
17520 let update = update_message.clone();
17521 |_, window, cx| {
17522 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17523 leader.read(cx).add_event_to_update_proto(
17524 event,
17525 &mut update.borrow_mut(),
17526 window,
17527 cx,
17528 );
17529 })
17530 .detach();
17531 }
17532 });
17533
17534 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17535 (
17536 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17537 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17538 )
17539 });
17540
17541 // Insert some excerpts.
17542 leader.update(cx, |leader, cx| {
17543 leader.buffer.update(cx, |multibuffer, cx| {
17544 multibuffer.set_excerpts_for_path(
17545 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17546 buffer_1.clone(),
17547 vec![
17548 Point::row_range(0..3),
17549 Point::row_range(1..6),
17550 Point::row_range(12..15),
17551 ],
17552 0,
17553 cx,
17554 );
17555 multibuffer.set_excerpts_for_path(
17556 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17557 buffer_2.clone(),
17558 vec![Point::row_range(0..6), Point::row_range(8..12)],
17559 0,
17560 cx,
17561 );
17562 });
17563 });
17564
17565 // Apply the update of adding the excerpts.
17566 follower_1
17567 .update_in(cx, |follower, window, cx| {
17568 follower.apply_update_proto(
17569 &project,
17570 update_message.borrow().clone().unwrap(),
17571 window,
17572 cx,
17573 )
17574 })
17575 .await
17576 .unwrap();
17577 assert_eq!(
17578 follower_1.update(cx, |editor, cx| editor.text(cx)),
17579 leader.update(cx, |editor, cx| editor.text(cx))
17580 );
17581 update_message.borrow_mut().take();
17582
17583 // Start following separately after it already has excerpts.
17584 let mut state_message =
17585 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17586 let workspace_entity = workspace.root(cx).unwrap();
17587 let follower_2 = cx
17588 .update_window(*workspace.deref(), |_, window, cx| {
17589 Editor::from_state_proto(
17590 workspace_entity,
17591 ViewId {
17592 creator: CollaboratorId::PeerId(PeerId::default()),
17593 id: 0,
17594 },
17595 &mut state_message,
17596 window,
17597 cx,
17598 )
17599 })
17600 .unwrap()
17601 .unwrap()
17602 .await
17603 .unwrap();
17604 assert_eq!(
17605 follower_2.update(cx, |editor, cx| editor.text(cx)),
17606 leader.update(cx, |editor, cx| editor.text(cx))
17607 );
17608
17609 // Remove some excerpts.
17610 leader.update(cx, |leader, cx| {
17611 leader.buffer.update(cx, |multibuffer, cx| {
17612 let excerpt_ids = multibuffer.excerpt_ids();
17613 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17614 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17615 });
17616 });
17617
17618 // Apply the update of removing the excerpts.
17619 follower_1
17620 .update_in(cx, |follower, window, cx| {
17621 follower.apply_update_proto(
17622 &project,
17623 update_message.borrow().clone().unwrap(),
17624 window,
17625 cx,
17626 )
17627 })
17628 .await
17629 .unwrap();
17630 follower_2
17631 .update_in(cx, |follower, window, cx| {
17632 follower.apply_update_proto(
17633 &project,
17634 update_message.borrow().clone().unwrap(),
17635 window,
17636 cx,
17637 )
17638 })
17639 .await
17640 .unwrap();
17641 update_message.borrow_mut().take();
17642 assert_eq!(
17643 follower_1.update(cx, |editor, cx| editor.text(cx)),
17644 leader.update(cx, |editor, cx| editor.text(cx))
17645 );
17646}
17647
17648#[gpui::test]
17649async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17650 init_test(cx, |_| {});
17651
17652 let mut cx = EditorTestContext::new(cx).await;
17653 let lsp_store =
17654 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17655
17656 cx.set_state(indoc! {"
17657 ˇfn func(abc def: i32) -> u32 {
17658 }
17659 "});
17660
17661 cx.update(|_, cx| {
17662 lsp_store.update(cx, |lsp_store, cx| {
17663 lsp_store
17664 .update_diagnostics(
17665 LanguageServerId(0),
17666 lsp::PublishDiagnosticsParams {
17667 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17668 version: None,
17669 diagnostics: vec![
17670 lsp::Diagnostic {
17671 range: lsp::Range::new(
17672 lsp::Position::new(0, 11),
17673 lsp::Position::new(0, 12),
17674 ),
17675 severity: Some(lsp::DiagnosticSeverity::ERROR),
17676 ..Default::default()
17677 },
17678 lsp::Diagnostic {
17679 range: lsp::Range::new(
17680 lsp::Position::new(0, 12),
17681 lsp::Position::new(0, 15),
17682 ),
17683 severity: Some(lsp::DiagnosticSeverity::ERROR),
17684 ..Default::default()
17685 },
17686 lsp::Diagnostic {
17687 range: lsp::Range::new(
17688 lsp::Position::new(0, 25),
17689 lsp::Position::new(0, 28),
17690 ),
17691 severity: Some(lsp::DiagnosticSeverity::ERROR),
17692 ..Default::default()
17693 },
17694 ],
17695 },
17696 None,
17697 DiagnosticSourceKind::Pushed,
17698 &[],
17699 cx,
17700 )
17701 .unwrap()
17702 });
17703 });
17704
17705 executor.run_until_parked();
17706
17707 cx.update_editor(|editor, window, cx| {
17708 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17709 });
17710
17711 cx.assert_editor_state(indoc! {"
17712 fn func(abc def: i32) -> ˇu32 {
17713 }
17714 "});
17715
17716 cx.update_editor(|editor, window, cx| {
17717 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17718 });
17719
17720 cx.assert_editor_state(indoc! {"
17721 fn func(abc ˇdef: i32) -> u32 {
17722 }
17723 "});
17724
17725 cx.update_editor(|editor, window, cx| {
17726 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17727 });
17728
17729 cx.assert_editor_state(indoc! {"
17730 fn func(abcˇ def: i32) -> u32 {
17731 }
17732 "});
17733
17734 cx.update_editor(|editor, window, cx| {
17735 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17736 });
17737
17738 cx.assert_editor_state(indoc! {"
17739 fn func(abc def: i32) -> ˇu32 {
17740 }
17741 "});
17742}
17743
17744#[gpui::test]
17745async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17746 init_test(cx, |_| {});
17747
17748 let mut cx = EditorTestContext::new(cx).await;
17749
17750 let diff_base = r#"
17751 use some::mod;
17752
17753 const A: u32 = 42;
17754
17755 fn main() {
17756 println!("hello");
17757
17758 println!("world");
17759 }
17760 "#
17761 .unindent();
17762
17763 // Edits are modified, removed, modified, added
17764 cx.set_state(
17765 &r#"
17766 use some::modified;
17767
17768 ˇ
17769 fn main() {
17770 println!("hello there");
17771
17772 println!("around the");
17773 println!("world");
17774 }
17775 "#
17776 .unindent(),
17777 );
17778
17779 cx.set_head_text(&diff_base);
17780 executor.run_until_parked();
17781
17782 cx.update_editor(|editor, window, cx| {
17783 //Wrap around the bottom of the buffer
17784 for _ in 0..3 {
17785 editor.go_to_next_hunk(&GoToHunk, window, cx);
17786 }
17787 });
17788
17789 cx.assert_editor_state(
17790 &r#"
17791 ˇuse some::modified;
17792
17793
17794 fn main() {
17795 println!("hello there");
17796
17797 println!("around the");
17798 println!("world");
17799 }
17800 "#
17801 .unindent(),
17802 );
17803
17804 cx.update_editor(|editor, window, cx| {
17805 //Wrap around the top of the buffer
17806 for _ in 0..2 {
17807 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17808 }
17809 });
17810
17811 cx.assert_editor_state(
17812 &r#"
17813 use some::modified;
17814
17815
17816 fn main() {
17817 ˇ println!("hello there");
17818
17819 println!("around the");
17820 println!("world");
17821 }
17822 "#
17823 .unindent(),
17824 );
17825
17826 cx.update_editor(|editor, window, cx| {
17827 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17828 });
17829
17830 cx.assert_editor_state(
17831 &r#"
17832 use some::modified;
17833
17834 ˇ
17835 fn main() {
17836 println!("hello there");
17837
17838 println!("around the");
17839 println!("world");
17840 }
17841 "#
17842 .unindent(),
17843 );
17844
17845 cx.update_editor(|editor, window, cx| {
17846 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17847 });
17848
17849 cx.assert_editor_state(
17850 &r#"
17851 ˇuse some::modified;
17852
17853
17854 fn main() {
17855 println!("hello there");
17856
17857 println!("around the");
17858 println!("world");
17859 }
17860 "#
17861 .unindent(),
17862 );
17863
17864 cx.update_editor(|editor, window, cx| {
17865 for _ in 0..2 {
17866 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17867 }
17868 });
17869
17870 cx.assert_editor_state(
17871 &r#"
17872 use some::modified;
17873
17874
17875 fn main() {
17876 ˇ println!("hello there");
17877
17878 println!("around the");
17879 println!("world");
17880 }
17881 "#
17882 .unindent(),
17883 );
17884
17885 cx.update_editor(|editor, window, cx| {
17886 editor.fold(&Fold, window, cx);
17887 });
17888
17889 cx.update_editor(|editor, window, cx| {
17890 editor.go_to_next_hunk(&GoToHunk, window, cx);
17891 });
17892
17893 cx.assert_editor_state(
17894 &r#"
17895 ˇuse some::modified;
17896
17897
17898 fn main() {
17899 println!("hello there");
17900
17901 println!("around the");
17902 println!("world");
17903 }
17904 "#
17905 .unindent(),
17906 );
17907}
17908
17909#[test]
17910fn test_split_words() {
17911 fn split(text: &str) -> Vec<&str> {
17912 split_words(text).collect()
17913 }
17914
17915 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17916 assert_eq!(split("hello_world"), &["hello_", "world"]);
17917 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17918 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17919 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17920 assert_eq!(split("helloworld"), &["helloworld"]);
17921
17922 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17923}
17924
17925#[test]
17926fn test_split_words_for_snippet_prefix() {
17927 fn split(text: &str) -> Vec<&str> {
17928 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17929 }
17930
17931 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17932 assert_eq!(split("hello_world"), &["hello_world"]);
17933 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17934 assert_eq!(split("Hello_World"), &["Hello_World"]);
17935 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17936 assert_eq!(split("helloworld"), &["helloworld"]);
17937 assert_eq!(
17938 split("this@is!@#$^many . symbols"),
17939 &[
17940 "symbols",
17941 " symbols",
17942 ". symbols",
17943 " . symbols",
17944 " . symbols",
17945 " . symbols",
17946 "many . symbols",
17947 "^many . symbols",
17948 "$^many . symbols",
17949 "#$^many . symbols",
17950 "@#$^many . symbols",
17951 "!@#$^many . symbols",
17952 "is!@#$^many . symbols",
17953 "@is!@#$^many . symbols",
17954 "this@is!@#$^many . symbols",
17955 ],
17956 );
17957 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17958}
17959
17960#[gpui::test]
17961async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17962 init_test(cx, |_| {});
17963
17964 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17965
17966 #[track_caller]
17967 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17968 let _state_context = cx.set_state(before);
17969 cx.run_until_parked();
17970 cx.update_editor(|editor, window, cx| {
17971 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17972 });
17973 cx.run_until_parked();
17974 cx.assert_editor_state(after);
17975 }
17976
17977 // Outside bracket jumps to outside of matching bracket
17978 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17979 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17980
17981 // Inside bracket jumps to inside of matching bracket
17982 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17983 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17984
17985 // When outside a bracket and inside, favor jumping to the inside bracket
17986 assert(
17987 "console.log('foo', [1, 2, 3]ˇ);",
17988 "console.log('foo', ˇ[1, 2, 3]);",
17989 &mut cx,
17990 );
17991 assert(
17992 "console.log(ˇ'foo', [1, 2, 3]);",
17993 "console.log('foo'ˇ, [1, 2, 3]);",
17994 &mut cx,
17995 );
17996
17997 // Bias forward if two options are equally likely
17998 assert(
17999 "let result = curried_fun()ˇ();",
18000 "let result = curried_fun()()ˇ;",
18001 &mut cx,
18002 );
18003
18004 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18005 assert(
18006 indoc! {"
18007 function test() {
18008 console.log('test')ˇ
18009 }"},
18010 indoc! {"
18011 function test() {
18012 console.logˇ('test')
18013 }"},
18014 &mut cx,
18015 );
18016}
18017
18018#[gpui::test]
18019async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18020 init_test(cx, |_| {});
18021 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18022 language_registry.add(markdown_lang());
18023 language_registry.add(rust_lang());
18024 let buffer = cx.new(|cx| {
18025 let mut buffer = language::Buffer::local(
18026 indoc! {"
18027 ```rs
18028 impl Worktree {
18029 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18030 }
18031 }
18032 ```
18033 "},
18034 cx,
18035 );
18036 buffer.set_language_registry(language_registry.clone());
18037 buffer.set_language(Some(markdown_lang()), cx);
18038 buffer
18039 });
18040 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18041 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18042 cx.executor().run_until_parked();
18043 _ = editor.update(cx, |editor, window, cx| {
18044 // Case 1: Test outer enclosing brackets
18045 select_ranges(
18046 editor,
18047 &indoc! {"
18048 ```rs
18049 impl Worktree {
18050 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18051 }
18052 }ˇ
18053 ```
18054 "},
18055 window,
18056 cx,
18057 );
18058 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18059 assert_text_with_selections(
18060 editor,
18061 &indoc! {"
18062 ```rs
18063 impl Worktree ˇ{
18064 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18065 }
18066 }
18067 ```
18068 "},
18069 cx,
18070 );
18071 // Case 2: Test inner enclosing brackets
18072 select_ranges(
18073 editor,
18074 &indoc! {"
18075 ```rs
18076 impl Worktree {
18077 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18078 }ˇ
18079 }
18080 ```
18081 "},
18082 window,
18083 cx,
18084 );
18085 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18086 assert_text_with_selections(
18087 editor,
18088 &indoc! {"
18089 ```rs
18090 impl Worktree {
18091 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18092 }
18093 }
18094 ```
18095 "},
18096 cx,
18097 );
18098 });
18099}
18100
18101#[gpui::test]
18102async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18103 init_test(cx, |_| {});
18104
18105 let fs = FakeFs::new(cx.executor());
18106 fs.insert_tree(
18107 path!("/a"),
18108 json!({
18109 "main.rs": "fn main() { let a = 5; }",
18110 "other.rs": "// Test file",
18111 }),
18112 )
18113 .await;
18114 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18115
18116 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18117 language_registry.add(Arc::new(Language::new(
18118 LanguageConfig {
18119 name: "Rust".into(),
18120 matcher: LanguageMatcher {
18121 path_suffixes: vec!["rs".to_string()],
18122 ..Default::default()
18123 },
18124 brackets: BracketPairConfig {
18125 pairs: vec![BracketPair {
18126 start: "{".to_string(),
18127 end: "}".to_string(),
18128 close: true,
18129 surround: true,
18130 newline: true,
18131 }],
18132 disabled_scopes_by_bracket_ix: Vec::new(),
18133 },
18134 ..Default::default()
18135 },
18136 Some(tree_sitter_rust::LANGUAGE.into()),
18137 )));
18138 let mut fake_servers = language_registry.register_fake_lsp(
18139 "Rust",
18140 FakeLspAdapter {
18141 capabilities: lsp::ServerCapabilities {
18142 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18143 first_trigger_character: "{".to_string(),
18144 more_trigger_character: None,
18145 }),
18146 ..Default::default()
18147 },
18148 ..Default::default()
18149 },
18150 );
18151
18152 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18153
18154 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18155
18156 let worktree_id = workspace
18157 .update(cx, |workspace, _, cx| {
18158 workspace.project().update(cx, |project, cx| {
18159 project.worktrees(cx).next().unwrap().read(cx).id()
18160 })
18161 })
18162 .unwrap();
18163
18164 let buffer = project
18165 .update(cx, |project, cx| {
18166 project.open_local_buffer(path!("/a/main.rs"), cx)
18167 })
18168 .await
18169 .unwrap();
18170 let editor_handle = workspace
18171 .update(cx, |workspace, window, cx| {
18172 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18173 })
18174 .unwrap()
18175 .await
18176 .unwrap()
18177 .downcast::<Editor>()
18178 .unwrap();
18179
18180 cx.executor().start_waiting();
18181 let fake_server = fake_servers.next().await.unwrap();
18182
18183 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18184 |params, _| async move {
18185 assert_eq!(
18186 params.text_document_position.text_document.uri,
18187 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18188 );
18189 assert_eq!(
18190 params.text_document_position.position,
18191 lsp::Position::new(0, 21),
18192 );
18193
18194 Ok(Some(vec![lsp::TextEdit {
18195 new_text: "]".to_string(),
18196 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18197 }]))
18198 },
18199 );
18200
18201 editor_handle.update_in(cx, |editor, window, cx| {
18202 window.focus(&editor.focus_handle(cx));
18203 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18204 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18205 });
18206 editor.handle_input("{", window, cx);
18207 });
18208
18209 cx.executor().run_until_parked();
18210
18211 buffer.update(cx, |buffer, _| {
18212 assert_eq!(
18213 buffer.text(),
18214 "fn main() { let a = {5}; }",
18215 "No extra braces from on type formatting should appear in the buffer"
18216 )
18217 });
18218}
18219
18220#[gpui::test(iterations = 20, seeds(31))]
18221async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18222 init_test(cx, |_| {});
18223
18224 let mut cx = EditorLspTestContext::new_rust(
18225 lsp::ServerCapabilities {
18226 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18227 first_trigger_character: ".".to_string(),
18228 more_trigger_character: None,
18229 }),
18230 ..Default::default()
18231 },
18232 cx,
18233 )
18234 .await;
18235
18236 cx.update_buffer(|buffer, _| {
18237 // This causes autoindent to be async.
18238 buffer.set_sync_parse_timeout(Duration::ZERO)
18239 });
18240
18241 cx.set_state("fn c() {\n d()ˇ\n}\n");
18242 cx.simulate_keystroke("\n");
18243 cx.run_until_parked();
18244
18245 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18246 let mut request =
18247 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18248 let buffer_cloned = buffer_cloned.clone();
18249 async move {
18250 buffer_cloned.update(&mut cx, |buffer, _| {
18251 assert_eq!(
18252 buffer.text(),
18253 "fn c() {\n d()\n .\n}\n",
18254 "OnTypeFormatting should triggered after autoindent applied"
18255 )
18256 })?;
18257
18258 Ok(Some(vec![]))
18259 }
18260 });
18261
18262 cx.simulate_keystroke(".");
18263 cx.run_until_parked();
18264
18265 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18266 assert!(request.next().await.is_some());
18267 request.close();
18268 assert!(request.next().await.is_none());
18269}
18270
18271#[gpui::test]
18272async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18273 init_test(cx, |_| {});
18274
18275 let fs = FakeFs::new(cx.executor());
18276 fs.insert_tree(
18277 path!("/a"),
18278 json!({
18279 "main.rs": "fn main() { let a = 5; }",
18280 "other.rs": "// Test file",
18281 }),
18282 )
18283 .await;
18284
18285 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18286
18287 let server_restarts = Arc::new(AtomicUsize::new(0));
18288 let closure_restarts = Arc::clone(&server_restarts);
18289 let language_server_name = "test language server";
18290 let language_name: LanguageName = "Rust".into();
18291
18292 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18293 language_registry.add(Arc::new(Language::new(
18294 LanguageConfig {
18295 name: language_name.clone(),
18296 matcher: LanguageMatcher {
18297 path_suffixes: vec!["rs".to_string()],
18298 ..Default::default()
18299 },
18300 ..Default::default()
18301 },
18302 Some(tree_sitter_rust::LANGUAGE.into()),
18303 )));
18304 let mut fake_servers = language_registry.register_fake_lsp(
18305 "Rust",
18306 FakeLspAdapter {
18307 name: language_server_name,
18308 initialization_options: Some(json!({
18309 "testOptionValue": true
18310 })),
18311 initializer: Some(Box::new(move |fake_server| {
18312 let task_restarts = Arc::clone(&closure_restarts);
18313 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18314 task_restarts.fetch_add(1, atomic::Ordering::Release);
18315 futures::future::ready(Ok(()))
18316 });
18317 })),
18318 ..Default::default()
18319 },
18320 );
18321
18322 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18323 let _buffer = project
18324 .update(cx, |project, cx| {
18325 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18326 })
18327 .await
18328 .unwrap();
18329 let _fake_server = fake_servers.next().await.unwrap();
18330 update_test_language_settings(cx, |language_settings| {
18331 language_settings.languages.0.insert(
18332 language_name.clone().0,
18333 LanguageSettingsContent {
18334 tab_size: NonZeroU32::new(8),
18335 ..Default::default()
18336 },
18337 );
18338 });
18339 cx.executor().run_until_parked();
18340 assert_eq!(
18341 server_restarts.load(atomic::Ordering::Acquire),
18342 0,
18343 "Should not restart LSP server on an unrelated change"
18344 );
18345
18346 update_test_project_settings(cx, |project_settings| {
18347 project_settings.lsp.insert(
18348 "Some other server name".into(),
18349 LspSettings {
18350 binary: None,
18351 settings: None,
18352 initialization_options: Some(json!({
18353 "some other init value": false
18354 })),
18355 enable_lsp_tasks: false,
18356 fetch: None,
18357 },
18358 );
18359 });
18360 cx.executor().run_until_parked();
18361 assert_eq!(
18362 server_restarts.load(atomic::Ordering::Acquire),
18363 0,
18364 "Should not restart LSP server on an unrelated LSP settings change"
18365 );
18366
18367 update_test_project_settings(cx, |project_settings| {
18368 project_settings.lsp.insert(
18369 language_server_name.into(),
18370 LspSettings {
18371 binary: None,
18372 settings: None,
18373 initialization_options: Some(json!({
18374 "anotherInitValue": false
18375 })),
18376 enable_lsp_tasks: false,
18377 fetch: None,
18378 },
18379 );
18380 });
18381 cx.executor().run_until_parked();
18382 assert_eq!(
18383 server_restarts.load(atomic::Ordering::Acquire),
18384 1,
18385 "Should restart LSP server on a related LSP settings change"
18386 );
18387
18388 update_test_project_settings(cx, |project_settings| {
18389 project_settings.lsp.insert(
18390 language_server_name.into(),
18391 LspSettings {
18392 binary: None,
18393 settings: None,
18394 initialization_options: Some(json!({
18395 "anotherInitValue": false
18396 })),
18397 enable_lsp_tasks: false,
18398 fetch: None,
18399 },
18400 );
18401 });
18402 cx.executor().run_until_parked();
18403 assert_eq!(
18404 server_restarts.load(atomic::Ordering::Acquire),
18405 1,
18406 "Should not restart LSP server on a related LSP settings change that is the same"
18407 );
18408
18409 update_test_project_settings(cx, |project_settings| {
18410 project_settings.lsp.insert(
18411 language_server_name.into(),
18412 LspSettings {
18413 binary: None,
18414 settings: None,
18415 initialization_options: None,
18416 enable_lsp_tasks: false,
18417 fetch: None,
18418 },
18419 );
18420 });
18421 cx.executor().run_until_parked();
18422 assert_eq!(
18423 server_restarts.load(atomic::Ordering::Acquire),
18424 2,
18425 "Should restart LSP server on another related LSP settings change"
18426 );
18427}
18428
18429#[gpui::test]
18430async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18431 init_test(cx, |_| {});
18432
18433 let mut cx = EditorLspTestContext::new_rust(
18434 lsp::ServerCapabilities {
18435 completion_provider: Some(lsp::CompletionOptions {
18436 trigger_characters: Some(vec![".".to_string()]),
18437 resolve_provider: Some(true),
18438 ..Default::default()
18439 }),
18440 ..Default::default()
18441 },
18442 cx,
18443 )
18444 .await;
18445
18446 cx.set_state("fn main() { let a = 2ˇ; }");
18447 cx.simulate_keystroke(".");
18448 let completion_item = lsp::CompletionItem {
18449 label: "some".into(),
18450 kind: Some(lsp::CompletionItemKind::SNIPPET),
18451 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18452 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18453 kind: lsp::MarkupKind::Markdown,
18454 value: "```rust\nSome(2)\n```".to_string(),
18455 })),
18456 deprecated: Some(false),
18457 sort_text: Some("fffffff2".to_string()),
18458 filter_text: Some("some".to_string()),
18459 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18460 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18461 range: lsp::Range {
18462 start: lsp::Position {
18463 line: 0,
18464 character: 22,
18465 },
18466 end: lsp::Position {
18467 line: 0,
18468 character: 22,
18469 },
18470 },
18471 new_text: "Some(2)".to_string(),
18472 })),
18473 additional_text_edits: Some(vec![lsp::TextEdit {
18474 range: lsp::Range {
18475 start: lsp::Position {
18476 line: 0,
18477 character: 20,
18478 },
18479 end: lsp::Position {
18480 line: 0,
18481 character: 22,
18482 },
18483 },
18484 new_text: "".to_string(),
18485 }]),
18486 ..Default::default()
18487 };
18488
18489 let closure_completion_item = completion_item.clone();
18490 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18491 let task_completion_item = closure_completion_item.clone();
18492 async move {
18493 Ok(Some(lsp::CompletionResponse::Array(vec![
18494 task_completion_item,
18495 ])))
18496 }
18497 });
18498
18499 request.next().await;
18500
18501 cx.condition(|editor, _| editor.context_menu_visible())
18502 .await;
18503 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18504 editor
18505 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18506 .unwrap()
18507 });
18508 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18509
18510 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18511 let task_completion_item = completion_item.clone();
18512 async move { Ok(task_completion_item) }
18513 })
18514 .next()
18515 .await
18516 .unwrap();
18517 apply_additional_edits.await.unwrap();
18518 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18519}
18520
18521#[gpui::test]
18522async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18523 init_test(cx, |_| {});
18524
18525 let mut cx = EditorLspTestContext::new_rust(
18526 lsp::ServerCapabilities {
18527 completion_provider: Some(lsp::CompletionOptions {
18528 trigger_characters: Some(vec![".".to_string()]),
18529 resolve_provider: Some(true),
18530 ..Default::default()
18531 }),
18532 ..Default::default()
18533 },
18534 cx,
18535 )
18536 .await;
18537
18538 cx.set_state("fn main() { let a = 2ˇ; }");
18539 cx.simulate_keystroke(".");
18540
18541 let item1 = lsp::CompletionItem {
18542 label: "method id()".to_string(),
18543 filter_text: Some("id".to_string()),
18544 detail: None,
18545 documentation: None,
18546 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18547 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18548 new_text: ".id".to_string(),
18549 })),
18550 ..lsp::CompletionItem::default()
18551 };
18552
18553 let item2 = lsp::CompletionItem {
18554 label: "other".to_string(),
18555 filter_text: Some("other".to_string()),
18556 detail: None,
18557 documentation: None,
18558 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18559 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18560 new_text: ".other".to_string(),
18561 })),
18562 ..lsp::CompletionItem::default()
18563 };
18564
18565 let item1 = item1.clone();
18566 cx.set_request_handler::<lsp::request::Completion, _, _>({
18567 let item1 = item1.clone();
18568 move |_, _, _| {
18569 let item1 = item1.clone();
18570 let item2 = item2.clone();
18571 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18572 }
18573 })
18574 .next()
18575 .await;
18576
18577 cx.condition(|editor, _| editor.context_menu_visible())
18578 .await;
18579 cx.update_editor(|editor, _, _| {
18580 let context_menu = editor.context_menu.borrow_mut();
18581 let context_menu = context_menu
18582 .as_ref()
18583 .expect("Should have the context menu deployed");
18584 match context_menu {
18585 CodeContextMenu::Completions(completions_menu) => {
18586 let completions = completions_menu.completions.borrow_mut();
18587 assert_eq!(
18588 completions
18589 .iter()
18590 .map(|completion| &completion.label.text)
18591 .collect::<Vec<_>>(),
18592 vec!["method id()", "other"]
18593 )
18594 }
18595 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18596 }
18597 });
18598
18599 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18600 let item1 = item1.clone();
18601 move |_, item_to_resolve, _| {
18602 let item1 = item1.clone();
18603 async move {
18604 if item1 == item_to_resolve {
18605 Ok(lsp::CompletionItem {
18606 label: "method id()".to_string(),
18607 filter_text: Some("id".to_string()),
18608 detail: Some("Now resolved!".to_string()),
18609 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18610 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18611 range: lsp::Range::new(
18612 lsp::Position::new(0, 22),
18613 lsp::Position::new(0, 22),
18614 ),
18615 new_text: ".id".to_string(),
18616 })),
18617 ..lsp::CompletionItem::default()
18618 })
18619 } else {
18620 Ok(item_to_resolve)
18621 }
18622 }
18623 }
18624 })
18625 .next()
18626 .await
18627 .unwrap();
18628 cx.run_until_parked();
18629
18630 cx.update_editor(|editor, window, cx| {
18631 editor.context_menu_next(&Default::default(), window, cx);
18632 });
18633
18634 cx.update_editor(|editor, _, _| {
18635 let context_menu = editor.context_menu.borrow_mut();
18636 let context_menu = context_menu
18637 .as_ref()
18638 .expect("Should have the context menu deployed");
18639 match context_menu {
18640 CodeContextMenu::Completions(completions_menu) => {
18641 let completions = completions_menu.completions.borrow_mut();
18642 assert_eq!(
18643 completions
18644 .iter()
18645 .map(|completion| &completion.label.text)
18646 .collect::<Vec<_>>(),
18647 vec!["method id() Now resolved!", "other"],
18648 "Should update first completion label, but not second as the filter text did not match."
18649 );
18650 }
18651 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18652 }
18653 });
18654}
18655
18656#[gpui::test]
18657async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18658 init_test(cx, |_| {});
18659 let mut cx = EditorLspTestContext::new_rust(
18660 lsp::ServerCapabilities {
18661 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18662 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18663 completion_provider: Some(lsp::CompletionOptions {
18664 resolve_provider: Some(true),
18665 ..Default::default()
18666 }),
18667 ..Default::default()
18668 },
18669 cx,
18670 )
18671 .await;
18672 cx.set_state(indoc! {"
18673 struct TestStruct {
18674 field: i32
18675 }
18676
18677 fn mainˇ() {
18678 let unused_var = 42;
18679 let test_struct = TestStruct { field: 42 };
18680 }
18681 "});
18682 let symbol_range = cx.lsp_range(indoc! {"
18683 struct TestStruct {
18684 field: i32
18685 }
18686
18687 «fn main»() {
18688 let unused_var = 42;
18689 let test_struct = TestStruct { field: 42 };
18690 }
18691 "});
18692 let mut hover_requests =
18693 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18694 Ok(Some(lsp::Hover {
18695 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18696 kind: lsp::MarkupKind::Markdown,
18697 value: "Function documentation".to_string(),
18698 }),
18699 range: Some(symbol_range),
18700 }))
18701 });
18702
18703 // Case 1: Test that code action menu hide hover popover
18704 cx.dispatch_action(Hover);
18705 hover_requests.next().await;
18706 cx.condition(|editor, _| editor.hover_state.visible()).await;
18707 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18708 move |_, _, _| async move {
18709 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18710 lsp::CodeAction {
18711 title: "Remove unused variable".to_string(),
18712 kind: Some(CodeActionKind::QUICKFIX),
18713 edit: Some(lsp::WorkspaceEdit {
18714 changes: Some(
18715 [(
18716 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18717 vec![lsp::TextEdit {
18718 range: lsp::Range::new(
18719 lsp::Position::new(5, 4),
18720 lsp::Position::new(5, 27),
18721 ),
18722 new_text: "".to_string(),
18723 }],
18724 )]
18725 .into_iter()
18726 .collect(),
18727 ),
18728 ..Default::default()
18729 }),
18730 ..Default::default()
18731 },
18732 )]))
18733 },
18734 );
18735 cx.update_editor(|editor, window, cx| {
18736 editor.toggle_code_actions(
18737 &ToggleCodeActions {
18738 deployed_from: None,
18739 quick_launch: false,
18740 },
18741 window,
18742 cx,
18743 );
18744 });
18745 code_action_requests.next().await;
18746 cx.run_until_parked();
18747 cx.condition(|editor, _| editor.context_menu_visible())
18748 .await;
18749 cx.update_editor(|editor, _, _| {
18750 assert!(
18751 !editor.hover_state.visible(),
18752 "Hover popover should be hidden when code action menu is shown"
18753 );
18754 // Hide code actions
18755 editor.context_menu.take();
18756 });
18757
18758 // Case 2: Test that code completions hide hover popover
18759 cx.dispatch_action(Hover);
18760 hover_requests.next().await;
18761 cx.condition(|editor, _| editor.hover_state.visible()).await;
18762 let counter = Arc::new(AtomicUsize::new(0));
18763 let mut completion_requests =
18764 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18765 let counter = counter.clone();
18766 async move {
18767 counter.fetch_add(1, atomic::Ordering::Release);
18768 Ok(Some(lsp::CompletionResponse::Array(vec![
18769 lsp::CompletionItem {
18770 label: "main".into(),
18771 kind: Some(lsp::CompletionItemKind::FUNCTION),
18772 detail: Some("() -> ()".to_string()),
18773 ..Default::default()
18774 },
18775 lsp::CompletionItem {
18776 label: "TestStruct".into(),
18777 kind: Some(lsp::CompletionItemKind::STRUCT),
18778 detail: Some("struct TestStruct".to_string()),
18779 ..Default::default()
18780 },
18781 ])))
18782 }
18783 });
18784 cx.update_editor(|editor, window, cx| {
18785 editor.show_completions(&ShowCompletions, window, cx);
18786 });
18787 completion_requests.next().await;
18788 cx.condition(|editor, _| editor.context_menu_visible())
18789 .await;
18790 cx.update_editor(|editor, _, _| {
18791 assert!(
18792 !editor.hover_state.visible(),
18793 "Hover popover should be hidden when completion menu is shown"
18794 );
18795 });
18796}
18797
18798#[gpui::test]
18799async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18800 init_test(cx, |_| {});
18801
18802 let mut cx = EditorLspTestContext::new_rust(
18803 lsp::ServerCapabilities {
18804 completion_provider: Some(lsp::CompletionOptions {
18805 trigger_characters: Some(vec![".".to_string()]),
18806 resolve_provider: Some(true),
18807 ..Default::default()
18808 }),
18809 ..Default::default()
18810 },
18811 cx,
18812 )
18813 .await;
18814
18815 cx.set_state("fn main() { let a = 2ˇ; }");
18816 cx.simulate_keystroke(".");
18817
18818 let unresolved_item_1 = lsp::CompletionItem {
18819 label: "id".to_string(),
18820 filter_text: Some("id".to_string()),
18821 detail: None,
18822 documentation: None,
18823 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18824 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18825 new_text: ".id".to_string(),
18826 })),
18827 ..lsp::CompletionItem::default()
18828 };
18829 let resolved_item_1 = lsp::CompletionItem {
18830 additional_text_edits: Some(vec![lsp::TextEdit {
18831 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18832 new_text: "!!".to_string(),
18833 }]),
18834 ..unresolved_item_1.clone()
18835 };
18836 let unresolved_item_2 = lsp::CompletionItem {
18837 label: "other".to_string(),
18838 filter_text: Some("other".to_string()),
18839 detail: None,
18840 documentation: None,
18841 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18842 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18843 new_text: ".other".to_string(),
18844 })),
18845 ..lsp::CompletionItem::default()
18846 };
18847 let resolved_item_2 = lsp::CompletionItem {
18848 additional_text_edits: Some(vec![lsp::TextEdit {
18849 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18850 new_text: "??".to_string(),
18851 }]),
18852 ..unresolved_item_2.clone()
18853 };
18854
18855 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18856 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18857 cx.lsp
18858 .server
18859 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18860 let unresolved_item_1 = unresolved_item_1.clone();
18861 let resolved_item_1 = resolved_item_1.clone();
18862 let unresolved_item_2 = unresolved_item_2.clone();
18863 let resolved_item_2 = resolved_item_2.clone();
18864 let resolve_requests_1 = resolve_requests_1.clone();
18865 let resolve_requests_2 = resolve_requests_2.clone();
18866 move |unresolved_request, _| {
18867 let unresolved_item_1 = unresolved_item_1.clone();
18868 let resolved_item_1 = resolved_item_1.clone();
18869 let unresolved_item_2 = unresolved_item_2.clone();
18870 let resolved_item_2 = resolved_item_2.clone();
18871 let resolve_requests_1 = resolve_requests_1.clone();
18872 let resolve_requests_2 = resolve_requests_2.clone();
18873 async move {
18874 if unresolved_request == unresolved_item_1 {
18875 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18876 Ok(resolved_item_1.clone())
18877 } else if unresolved_request == unresolved_item_2 {
18878 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18879 Ok(resolved_item_2.clone())
18880 } else {
18881 panic!("Unexpected completion item {unresolved_request:?}")
18882 }
18883 }
18884 }
18885 })
18886 .detach();
18887
18888 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18889 let unresolved_item_1 = unresolved_item_1.clone();
18890 let unresolved_item_2 = unresolved_item_2.clone();
18891 async move {
18892 Ok(Some(lsp::CompletionResponse::Array(vec![
18893 unresolved_item_1,
18894 unresolved_item_2,
18895 ])))
18896 }
18897 })
18898 .next()
18899 .await;
18900
18901 cx.condition(|editor, _| editor.context_menu_visible())
18902 .await;
18903 cx.update_editor(|editor, _, _| {
18904 let context_menu = editor.context_menu.borrow_mut();
18905 let context_menu = context_menu
18906 .as_ref()
18907 .expect("Should have the context menu deployed");
18908 match context_menu {
18909 CodeContextMenu::Completions(completions_menu) => {
18910 let completions = completions_menu.completions.borrow_mut();
18911 assert_eq!(
18912 completions
18913 .iter()
18914 .map(|completion| &completion.label.text)
18915 .collect::<Vec<_>>(),
18916 vec!["id", "other"]
18917 )
18918 }
18919 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18920 }
18921 });
18922 cx.run_until_parked();
18923
18924 cx.update_editor(|editor, window, cx| {
18925 editor.context_menu_next(&ContextMenuNext, window, cx);
18926 });
18927 cx.run_until_parked();
18928 cx.update_editor(|editor, window, cx| {
18929 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18930 });
18931 cx.run_until_parked();
18932 cx.update_editor(|editor, window, cx| {
18933 editor.context_menu_next(&ContextMenuNext, window, cx);
18934 });
18935 cx.run_until_parked();
18936 cx.update_editor(|editor, window, cx| {
18937 editor
18938 .compose_completion(&ComposeCompletion::default(), window, cx)
18939 .expect("No task returned")
18940 })
18941 .await
18942 .expect("Completion failed");
18943 cx.run_until_parked();
18944
18945 cx.update_editor(|editor, _, cx| {
18946 assert_eq!(
18947 resolve_requests_1.load(atomic::Ordering::Acquire),
18948 1,
18949 "Should always resolve once despite multiple selections"
18950 );
18951 assert_eq!(
18952 resolve_requests_2.load(atomic::Ordering::Acquire),
18953 1,
18954 "Should always resolve once after multiple selections and applying the completion"
18955 );
18956 assert_eq!(
18957 editor.text(cx),
18958 "fn main() { let a = ??.other; }",
18959 "Should use resolved data when applying the completion"
18960 );
18961 });
18962}
18963
18964#[gpui::test]
18965async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18966 init_test(cx, |_| {});
18967
18968 let item_0 = lsp::CompletionItem {
18969 label: "abs".into(),
18970 insert_text: Some("abs".into()),
18971 data: Some(json!({ "very": "special"})),
18972 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18973 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18974 lsp::InsertReplaceEdit {
18975 new_text: "abs".to_string(),
18976 insert: lsp::Range::default(),
18977 replace: lsp::Range::default(),
18978 },
18979 )),
18980 ..lsp::CompletionItem::default()
18981 };
18982 let items = iter::once(item_0.clone())
18983 .chain((11..51).map(|i| lsp::CompletionItem {
18984 label: format!("item_{}", i),
18985 insert_text: Some(format!("item_{}", i)),
18986 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18987 ..lsp::CompletionItem::default()
18988 }))
18989 .collect::<Vec<_>>();
18990
18991 let default_commit_characters = vec!["?".to_string()];
18992 let default_data = json!({ "default": "data"});
18993 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18994 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18995 let default_edit_range = lsp::Range {
18996 start: lsp::Position {
18997 line: 0,
18998 character: 5,
18999 },
19000 end: lsp::Position {
19001 line: 0,
19002 character: 5,
19003 },
19004 };
19005
19006 let mut cx = EditorLspTestContext::new_rust(
19007 lsp::ServerCapabilities {
19008 completion_provider: Some(lsp::CompletionOptions {
19009 trigger_characters: Some(vec![".".to_string()]),
19010 resolve_provider: Some(true),
19011 ..Default::default()
19012 }),
19013 ..Default::default()
19014 },
19015 cx,
19016 )
19017 .await;
19018
19019 cx.set_state("fn main() { let a = 2ˇ; }");
19020 cx.simulate_keystroke(".");
19021
19022 let completion_data = default_data.clone();
19023 let completion_characters = default_commit_characters.clone();
19024 let completion_items = items.clone();
19025 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19026 let default_data = completion_data.clone();
19027 let default_commit_characters = completion_characters.clone();
19028 let items = completion_items.clone();
19029 async move {
19030 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19031 items,
19032 item_defaults: Some(lsp::CompletionListItemDefaults {
19033 data: Some(default_data.clone()),
19034 commit_characters: Some(default_commit_characters.clone()),
19035 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19036 default_edit_range,
19037 )),
19038 insert_text_format: Some(default_insert_text_format),
19039 insert_text_mode: Some(default_insert_text_mode),
19040 }),
19041 ..lsp::CompletionList::default()
19042 })))
19043 }
19044 })
19045 .next()
19046 .await;
19047
19048 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19049 cx.lsp
19050 .server
19051 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19052 let closure_resolved_items = resolved_items.clone();
19053 move |item_to_resolve, _| {
19054 let closure_resolved_items = closure_resolved_items.clone();
19055 async move {
19056 closure_resolved_items.lock().push(item_to_resolve.clone());
19057 Ok(item_to_resolve)
19058 }
19059 }
19060 })
19061 .detach();
19062
19063 cx.condition(|editor, _| editor.context_menu_visible())
19064 .await;
19065 cx.run_until_parked();
19066 cx.update_editor(|editor, _, _| {
19067 let menu = editor.context_menu.borrow_mut();
19068 match menu.as_ref().expect("should have the completions menu") {
19069 CodeContextMenu::Completions(completions_menu) => {
19070 assert_eq!(
19071 completions_menu
19072 .entries
19073 .borrow()
19074 .iter()
19075 .map(|mat| mat.string.clone())
19076 .collect::<Vec<String>>(),
19077 items
19078 .iter()
19079 .map(|completion| completion.label.clone())
19080 .collect::<Vec<String>>()
19081 );
19082 }
19083 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19084 }
19085 });
19086 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19087 // with 4 from the end.
19088 assert_eq!(
19089 *resolved_items.lock(),
19090 [&items[0..16], &items[items.len() - 4..items.len()]]
19091 .concat()
19092 .iter()
19093 .cloned()
19094 .map(|mut item| {
19095 if item.data.is_none() {
19096 item.data = Some(default_data.clone());
19097 }
19098 item
19099 })
19100 .collect::<Vec<lsp::CompletionItem>>(),
19101 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19102 );
19103 resolved_items.lock().clear();
19104
19105 cx.update_editor(|editor, window, cx| {
19106 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19107 });
19108 cx.run_until_parked();
19109 // Completions that have already been resolved are skipped.
19110 assert_eq!(
19111 *resolved_items.lock(),
19112 items[items.len() - 17..items.len() - 4]
19113 .iter()
19114 .cloned()
19115 .map(|mut item| {
19116 if item.data.is_none() {
19117 item.data = Some(default_data.clone());
19118 }
19119 item
19120 })
19121 .collect::<Vec<lsp::CompletionItem>>()
19122 );
19123 resolved_items.lock().clear();
19124}
19125
19126#[gpui::test]
19127async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19128 init_test(cx, |_| {});
19129
19130 let mut cx = EditorLspTestContext::new(
19131 Language::new(
19132 LanguageConfig {
19133 matcher: LanguageMatcher {
19134 path_suffixes: vec!["jsx".into()],
19135 ..Default::default()
19136 },
19137 overrides: [(
19138 "element".into(),
19139 LanguageConfigOverride {
19140 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19141 ..Default::default()
19142 },
19143 )]
19144 .into_iter()
19145 .collect(),
19146 ..Default::default()
19147 },
19148 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19149 )
19150 .with_override_query("(jsx_self_closing_element) @element")
19151 .unwrap(),
19152 lsp::ServerCapabilities {
19153 completion_provider: Some(lsp::CompletionOptions {
19154 trigger_characters: Some(vec![":".to_string()]),
19155 ..Default::default()
19156 }),
19157 ..Default::default()
19158 },
19159 cx,
19160 )
19161 .await;
19162
19163 cx.lsp
19164 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19165 Ok(Some(lsp::CompletionResponse::Array(vec![
19166 lsp::CompletionItem {
19167 label: "bg-blue".into(),
19168 ..Default::default()
19169 },
19170 lsp::CompletionItem {
19171 label: "bg-red".into(),
19172 ..Default::default()
19173 },
19174 lsp::CompletionItem {
19175 label: "bg-yellow".into(),
19176 ..Default::default()
19177 },
19178 ])))
19179 });
19180
19181 cx.set_state(r#"<p class="bgˇ" />"#);
19182
19183 // Trigger completion when typing a dash, because the dash is an extra
19184 // word character in the 'element' scope, which contains the cursor.
19185 cx.simulate_keystroke("-");
19186 cx.executor().run_until_parked();
19187 cx.update_editor(|editor, _, _| {
19188 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19189 {
19190 assert_eq!(
19191 completion_menu_entries(menu),
19192 &["bg-blue", "bg-red", "bg-yellow"]
19193 );
19194 } else {
19195 panic!("expected completion menu to be open");
19196 }
19197 });
19198
19199 cx.simulate_keystroke("l");
19200 cx.executor().run_until_parked();
19201 cx.update_editor(|editor, _, _| {
19202 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19203 {
19204 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19205 } else {
19206 panic!("expected completion menu to be open");
19207 }
19208 });
19209
19210 // When filtering completions, consider the character after the '-' to
19211 // be the start of a subword.
19212 cx.set_state(r#"<p class="yelˇ" />"#);
19213 cx.simulate_keystroke("l");
19214 cx.executor().run_until_parked();
19215 cx.update_editor(|editor, _, _| {
19216 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19217 {
19218 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19219 } else {
19220 panic!("expected completion menu to be open");
19221 }
19222 });
19223}
19224
19225fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19226 let entries = menu.entries.borrow();
19227 entries.iter().map(|mat| mat.string.clone()).collect()
19228}
19229
19230#[gpui::test]
19231async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19232 init_test(cx, |settings| {
19233 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19234 });
19235
19236 let fs = FakeFs::new(cx.executor());
19237 fs.insert_file(path!("/file.ts"), Default::default()).await;
19238
19239 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19240 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19241
19242 language_registry.add(Arc::new(Language::new(
19243 LanguageConfig {
19244 name: "TypeScript".into(),
19245 matcher: LanguageMatcher {
19246 path_suffixes: vec!["ts".to_string()],
19247 ..Default::default()
19248 },
19249 ..Default::default()
19250 },
19251 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19252 )));
19253 update_test_language_settings(cx, |settings| {
19254 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19255 });
19256
19257 let test_plugin = "test_plugin";
19258 let _ = language_registry.register_fake_lsp(
19259 "TypeScript",
19260 FakeLspAdapter {
19261 prettier_plugins: vec![test_plugin],
19262 ..Default::default()
19263 },
19264 );
19265
19266 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19267 let buffer = project
19268 .update(cx, |project, cx| {
19269 project.open_local_buffer(path!("/file.ts"), cx)
19270 })
19271 .await
19272 .unwrap();
19273
19274 let buffer_text = "one\ntwo\nthree\n";
19275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19276 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19277 editor.update_in(cx, |editor, window, cx| {
19278 editor.set_text(buffer_text, window, cx)
19279 });
19280
19281 editor
19282 .update_in(cx, |editor, window, cx| {
19283 editor.perform_format(
19284 project.clone(),
19285 FormatTrigger::Manual,
19286 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19287 window,
19288 cx,
19289 )
19290 })
19291 .unwrap()
19292 .await;
19293 assert_eq!(
19294 editor.update(cx, |editor, cx| editor.text(cx)),
19295 buffer_text.to_string() + prettier_format_suffix,
19296 "Test prettier formatting was not applied to the original buffer text",
19297 );
19298
19299 update_test_language_settings(cx, |settings| {
19300 settings.defaults.formatter = Some(FormatterList::default())
19301 });
19302 let format = editor.update_in(cx, |editor, window, cx| {
19303 editor.perform_format(
19304 project.clone(),
19305 FormatTrigger::Manual,
19306 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19307 window,
19308 cx,
19309 )
19310 });
19311 format.await.unwrap();
19312 assert_eq!(
19313 editor.update(cx, |editor, cx| editor.text(cx)),
19314 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19315 "Autoformatting (via test prettier) was not applied to the original buffer text",
19316 );
19317}
19318
19319#[gpui::test]
19320async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19321 init_test(cx, |settings| {
19322 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19323 });
19324
19325 let fs = FakeFs::new(cx.executor());
19326 fs.insert_file(path!("/file.settings"), Default::default())
19327 .await;
19328
19329 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19330 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19331
19332 let ts_lang = Arc::new(Language::new(
19333 LanguageConfig {
19334 name: "TypeScript".into(),
19335 matcher: LanguageMatcher {
19336 path_suffixes: vec!["ts".to_string()],
19337 ..LanguageMatcher::default()
19338 },
19339 prettier_parser_name: Some("typescript".to_string()),
19340 ..LanguageConfig::default()
19341 },
19342 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19343 ));
19344
19345 language_registry.add(ts_lang.clone());
19346
19347 update_test_language_settings(cx, |settings| {
19348 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19349 });
19350
19351 let test_plugin = "test_plugin";
19352 let _ = language_registry.register_fake_lsp(
19353 "TypeScript",
19354 FakeLspAdapter {
19355 prettier_plugins: vec![test_plugin],
19356 ..Default::default()
19357 },
19358 );
19359
19360 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19361 let buffer = project
19362 .update(cx, |project, cx| {
19363 project.open_local_buffer(path!("/file.settings"), cx)
19364 })
19365 .await
19366 .unwrap();
19367
19368 project.update(cx, |project, cx| {
19369 project.set_language_for_buffer(&buffer, ts_lang, cx)
19370 });
19371
19372 let buffer_text = "one\ntwo\nthree\n";
19373 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19374 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19375 editor.update_in(cx, |editor, window, cx| {
19376 editor.set_text(buffer_text, window, cx)
19377 });
19378
19379 editor
19380 .update_in(cx, |editor, window, cx| {
19381 editor.perform_format(
19382 project.clone(),
19383 FormatTrigger::Manual,
19384 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19385 window,
19386 cx,
19387 )
19388 })
19389 .unwrap()
19390 .await;
19391 assert_eq!(
19392 editor.update(cx, |editor, cx| editor.text(cx)),
19393 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19394 "Test prettier formatting was not applied to the original buffer text",
19395 );
19396
19397 update_test_language_settings(cx, |settings| {
19398 settings.defaults.formatter = Some(FormatterList::default())
19399 });
19400 let format = editor.update_in(cx, |editor, window, cx| {
19401 editor.perform_format(
19402 project.clone(),
19403 FormatTrigger::Manual,
19404 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19405 window,
19406 cx,
19407 )
19408 });
19409 format.await.unwrap();
19410
19411 assert_eq!(
19412 editor.update(cx, |editor, cx| editor.text(cx)),
19413 buffer_text.to_string()
19414 + prettier_format_suffix
19415 + "\ntypescript\n"
19416 + prettier_format_suffix
19417 + "\ntypescript",
19418 "Autoformatting (via test prettier) was not applied to the original buffer text",
19419 );
19420}
19421
19422#[gpui::test]
19423async fn test_addition_reverts(cx: &mut TestAppContext) {
19424 init_test(cx, |_| {});
19425 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19426 let base_text = indoc! {r#"
19427 struct Row;
19428 struct Row1;
19429 struct Row2;
19430
19431 struct Row4;
19432 struct Row5;
19433 struct Row6;
19434
19435 struct Row8;
19436 struct Row9;
19437 struct Row10;"#};
19438
19439 // When addition hunks are not adjacent to carets, no hunk revert is performed
19440 assert_hunk_revert(
19441 indoc! {r#"struct Row;
19442 struct Row1;
19443 struct Row1.1;
19444 struct Row1.2;
19445 struct Row2;ˇ
19446
19447 struct Row4;
19448 struct Row5;
19449 struct Row6;
19450
19451 struct Row8;
19452 ˇstruct Row9;
19453 struct Row9.1;
19454 struct Row9.2;
19455 struct Row9.3;
19456 struct Row10;"#},
19457 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19458 indoc! {r#"struct Row;
19459 struct Row1;
19460 struct Row1.1;
19461 struct Row1.2;
19462 struct Row2;ˇ
19463
19464 struct Row4;
19465 struct Row5;
19466 struct Row6;
19467
19468 struct Row8;
19469 ˇstruct Row9;
19470 struct Row9.1;
19471 struct Row9.2;
19472 struct Row9.3;
19473 struct Row10;"#},
19474 base_text,
19475 &mut cx,
19476 );
19477 // Same for selections
19478 assert_hunk_revert(
19479 indoc! {r#"struct Row;
19480 struct Row1;
19481 struct Row2;
19482 struct Row2.1;
19483 struct Row2.2;
19484 «ˇ
19485 struct Row4;
19486 struct» Row5;
19487 «struct Row6;
19488 ˇ»
19489 struct Row9.1;
19490 struct Row9.2;
19491 struct Row9.3;
19492 struct Row8;
19493 struct Row9;
19494 struct Row10;"#},
19495 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19496 indoc! {r#"struct Row;
19497 struct Row1;
19498 struct Row2;
19499 struct Row2.1;
19500 struct Row2.2;
19501 «ˇ
19502 struct Row4;
19503 struct» Row5;
19504 «struct Row6;
19505 ˇ»
19506 struct Row9.1;
19507 struct Row9.2;
19508 struct Row9.3;
19509 struct Row8;
19510 struct Row9;
19511 struct Row10;"#},
19512 base_text,
19513 &mut cx,
19514 );
19515
19516 // When carets and selections intersect the addition hunks, those are reverted.
19517 // Adjacent carets got merged.
19518 assert_hunk_revert(
19519 indoc! {r#"struct Row;
19520 ˇ// something on the top
19521 struct Row1;
19522 struct Row2;
19523 struct Roˇw3.1;
19524 struct Row2.2;
19525 struct Row2.3;ˇ
19526
19527 struct Row4;
19528 struct ˇRow5.1;
19529 struct Row5.2;
19530 struct «Rowˇ»5.3;
19531 struct Row5;
19532 struct Row6;
19533 ˇ
19534 struct Row9.1;
19535 struct «Rowˇ»9.2;
19536 struct «ˇRow»9.3;
19537 struct Row8;
19538 struct Row9;
19539 «ˇ// something on bottom»
19540 struct Row10;"#},
19541 vec![
19542 DiffHunkStatusKind::Added,
19543 DiffHunkStatusKind::Added,
19544 DiffHunkStatusKind::Added,
19545 DiffHunkStatusKind::Added,
19546 DiffHunkStatusKind::Added,
19547 ],
19548 indoc! {r#"struct Row;
19549 ˇstruct Row1;
19550 struct Row2;
19551 ˇ
19552 struct Row4;
19553 ˇstruct Row5;
19554 struct Row6;
19555 ˇ
19556 ˇstruct Row8;
19557 struct Row9;
19558 ˇstruct Row10;"#},
19559 base_text,
19560 &mut cx,
19561 );
19562}
19563
19564#[gpui::test]
19565async fn test_modification_reverts(cx: &mut TestAppContext) {
19566 init_test(cx, |_| {});
19567 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19568 let base_text = indoc! {r#"
19569 struct Row;
19570 struct Row1;
19571 struct Row2;
19572
19573 struct Row4;
19574 struct Row5;
19575 struct Row6;
19576
19577 struct Row8;
19578 struct Row9;
19579 struct Row10;"#};
19580
19581 // Modification hunks behave the same as the addition ones.
19582 assert_hunk_revert(
19583 indoc! {r#"struct Row;
19584 struct Row1;
19585 struct Row33;
19586 ˇ
19587 struct Row4;
19588 struct Row5;
19589 struct Row6;
19590 ˇ
19591 struct Row99;
19592 struct Row9;
19593 struct Row10;"#},
19594 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19595 indoc! {r#"struct Row;
19596 struct Row1;
19597 struct Row33;
19598 ˇ
19599 struct Row4;
19600 struct Row5;
19601 struct Row6;
19602 ˇ
19603 struct Row99;
19604 struct Row9;
19605 struct Row10;"#},
19606 base_text,
19607 &mut cx,
19608 );
19609 assert_hunk_revert(
19610 indoc! {r#"struct Row;
19611 struct Row1;
19612 struct Row33;
19613 «ˇ
19614 struct Row4;
19615 struct» Row5;
19616 «struct Row6;
19617 ˇ»
19618 struct Row99;
19619 struct Row9;
19620 struct Row10;"#},
19621 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19622 indoc! {r#"struct Row;
19623 struct Row1;
19624 struct Row33;
19625 «ˇ
19626 struct Row4;
19627 struct» Row5;
19628 «struct Row6;
19629 ˇ»
19630 struct Row99;
19631 struct Row9;
19632 struct Row10;"#},
19633 base_text,
19634 &mut cx,
19635 );
19636
19637 assert_hunk_revert(
19638 indoc! {r#"ˇstruct Row1.1;
19639 struct Row1;
19640 «ˇstr»uct Row22;
19641
19642 struct ˇRow44;
19643 struct Row5;
19644 struct «Rˇ»ow66;ˇ
19645
19646 «struˇ»ct Row88;
19647 struct Row9;
19648 struct Row1011;ˇ"#},
19649 vec![
19650 DiffHunkStatusKind::Modified,
19651 DiffHunkStatusKind::Modified,
19652 DiffHunkStatusKind::Modified,
19653 DiffHunkStatusKind::Modified,
19654 DiffHunkStatusKind::Modified,
19655 DiffHunkStatusKind::Modified,
19656 ],
19657 indoc! {r#"struct Row;
19658 ˇstruct Row1;
19659 struct Row2;
19660 ˇ
19661 struct Row4;
19662 ˇstruct Row5;
19663 struct Row6;
19664 ˇ
19665 struct Row8;
19666 ˇstruct Row9;
19667 struct Row10;ˇ"#},
19668 base_text,
19669 &mut cx,
19670 );
19671}
19672
19673#[gpui::test]
19674async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19675 init_test(cx, |_| {});
19676 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19677 let base_text = indoc! {r#"
19678 one
19679
19680 two
19681 three
19682 "#};
19683
19684 cx.set_head_text(base_text);
19685 cx.set_state("\nˇ\n");
19686 cx.executor().run_until_parked();
19687 cx.update_editor(|editor, _window, cx| {
19688 editor.expand_selected_diff_hunks(cx);
19689 });
19690 cx.executor().run_until_parked();
19691 cx.update_editor(|editor, window, cx| {
19692 editor.backspace(&Default::default(), window, cx);
19693 });
19694 cx.run_until_parked();
19695 cx.assert_state_with_diff(
19696 indoc! {r#"
19697
19698 - two
19699 - threeˇ
19700 +
19701 "#}
19702 .to_string(),
19703 );
19704}
19705
19706#[gpui::test]
19707async fn test_deletion_reverts(cx: &mut TestAppContext) {
19708 init_test(cx, |_| {});
19709 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19710 let base_text = indoc! {r#"struct Row;
19711struct Row1;
19712struct Row2;
19713
19714struct Row4;
19715struct Row5;
19716struct Row6;
19717
19718struct Row8;
19719struct Row9;
19720struct Row10;"#};
19721
19722 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19723 assert_hunk_revert(
19724 indoc! {r#"struct Row;
19725 struct Row2;
19726
19727 ˇstruct Row4;
19728 struct Row5;
19729 struct Row6;
19730 ˇ
19731 struct Row8;
19732 struct Row10;"#},
19733 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19734 indoc! {r#"struct Row;
19735 struct Row2;
19736
19737 ˇstruct Row4;
19738 struct Row5;
19739 struct Row6;
19740 ˇ
19741 struct Row8;
19742 struct Row10;"#},
19743 base_text,
19744 &mut cx,
19745 );
19746 assert_hunk_revert(
19747 indoc! {r#"struct Row;
19748 struct Row2;
19749
19750 «ˇstruct Row4;
19751 struct» Row5;
19752 «struct Row6;
19753 ˇ»
19754 struct Row8;
19755 struct Row10;"#},
19756 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19757 indoc! {r#"struct Row;
19758 struct Row2;
19759
19760 «ˇstruct Row4;
19761 struct» Row5;
19762 «struct Row6;
19763 ˇ»
19764 struct Row8;
19765 struct Row10;"#},
19766 base_text,
19767 &mut cx,
19768 );
19769
19770 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19771 assert_hunk_revert(
19772 indoc! {r#"struct Row;
19773 ˇstruct Row2;
19774
19775 struct Row4;
19776 struct Row5;
19777 struct Row6;
19778
19779 struct Row8;ˇ
19780 struct Row10;"#},
19781 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19782 indoc! {r#"struct Row;
19783 struct Row1;
19784 ˇstruct Row2;
19785
19786 struct Row4;
19787 struct Row5;
19788 struct Row6;
19789
19790 struct Row8;ˇ
19791 struct Row9;
19792 struct Row10;"#},
19793 base_text,
19794 &mut cx,
19795 );
19796 assert_hunk_revert(
19797 indoc! {r#"struct Row;
19798 struct Row2«ˇ;
19799 struct Row4;
19800 struct» Row5;
19801 «struct Row6;
19802
19803 struct Row8;ˇ»
19804 struct Row10;"#},
19805 vec![
19806 DiffHunkStatusKind::Deleted,
19807 DiffHunkStatusKind::Deleted,
19808 DiffHunkStatusKind::Deleted,
19809 ],
19810 indoc! {r#"struct Row;
19811 struct Row1;
19812 struct Row2«ˇ;
19813
19814 struct Row4;
19815 struct» Row5;
19816 «struct Row6;
19817
19818 struct Row8;ˇ»
19819 struct Row9;
19820 struct Row10;"#},
19821 base_text,
19822 &mut cx,
19823 );
19824}
19825
19826#[gpui::test]
19827async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19828 init_test(cx, |_| {});
19829
19830 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19831 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19832 let base_text_3 =
19833 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19834
19835 let text_1 = edit_first_char_of_every_line(base_text_1);
19836 let text_2 = edit_first_char_of_every_line(base_text_2);
19837 let text_3 = edit_first_char_of_every_line(base_text_3);
19838
19839 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19840 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19841 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19842
19843 let multibuffer = cx.new(|cx| {
19844 let mut multibuffer = MultiBuffer::new(ReadWrite);
19845 multibuffer.push_excerpts(
19846 buffer_1.clone(),
19847 [
19848 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19849 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19850 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19851 ],
19852 cx,
19853 );
19854 multibuffer.push_excerpts(
19855 buffer_2.clone(),
19856 [
19857 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19858 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19859 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19860 ],
19861 cx,
19862 );
19863 multibuffer.push_excerpts(
19864 buffer_3.clone(),
19865 [
19866 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19867 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19868 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19869 ],
19870 cx,
19871 );
19872 multibuffer
19873 });
19874
19875 let fs = FakeFs::new(cx.executor());
19876 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19877 let (editor, cx) = cx
19878 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19879 editor.update_in(cx, |editor, _window, cx| {
19880 for (buffer, diff_base) in [
19881 (buffer_1.clone(), base_text_1),
19882 (buffer_2.clone(), base_text_2),
19883 (buffer_3.clone(), base_text_3),
19884 ] {
19885 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19886 editor
19887 .buffer
19888 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19889 }
19890 });
19891 cx.executor().run_until_parked();
19892
19893 editor.update_in(cx, |editor, window, cx| {
19894 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}");
19895 editor.select_all(&SelectAll, window, cx);
19896 editor.git_restore(&Default::default(), window, cx);
19897 });
19898 cx.executor().run_until_parked();
19899
19900 // When all ranges are selected, all buffer hunks are reverted.
19901 editor.update(cx, |editor, cx| {
19902 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");
19903 });
19904 buffer_1.update(cx, |buffer, _| {
19905 assert_eq!(buffer.text(), base_text_1);
19906 });
19907 buffer_2.update(cx, |buffer, _| {
19908 assert_eq!(buffer.text(), base_text_2);
19909 });
19910 buffer_3.update(cx, |buffer, _| {
19911 assert_eq!(buffer.text(), base_text_3);
19912 });
19913
19914 editor.update_in(cx, |editor, window, cx| {
19915 editor.undo(&Default::default(), window, cx);
19916 });
19917
19918 editor.update_in(cx, |editor, window, cx| {
19919 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19920 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19921 });
19922 editor.git_restore(&Default::default(), window, cx);
19923 });
19924
19925 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19926 // but not affect buffer_2 and its related excerpts.
19927 editor.update(cx, |editor, cx| {
19928 assert_eq!(
19929 editor.text(cx),
19930 "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}"
19931 );
19932 });
19933 buffer_1.update(cx, |buffer, _| {
19934 assert_eq!(buffer.text(), base_text_1);
19935 });
19936 buffer_2.update(cx, |buffer, _| {
19937 assert_eq!(
19938 buffer.text(),
19939 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19940 );
19941 });
19942 buffer_3.update(cx, |buffer, _| {
19943 assert_eq!(
19944 buffer.text(),
19945 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19946 );
19947 });
19948
19949 fn edit_first_char_of_every_line(text: &str) -> String {
19950 text.split('\n')
19951 .map(|line| format!("X{}", &line[1..]))
19952 .collect::<Vec<_>>()
19953 .join("\n")
19954 }
19955}
19956
19957#[gpui::test]
19958async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19959 init_test(cx, |_| {});
19960
19961 let cols = 4;
19962 let rows = 10;
19963 let sample_text_1 = sample_text(rows, cols, 'a');
19964 assert_eq!(
19965 sample_text_1,
19966 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19967 );
19968 let sample_text_2 = sample_text(rows, cols, 'l');
19969 assert_eq!(
19970 sample_text_2,
19971 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19972 );
19973 let sample_text_3 = sample_text(rows, cols, 'v');
19974 assert_eq!(
19975 sample_text_3,
19976 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19977 );
19978
19979 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19980 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19981 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19982
19983 let multi_buffer = cx.new(|cx| {
19984 let mut multibuffer = MultiBuffer::new(ReadWrite);
19985 multibuffer.push_excerpts(
19986 buffer_1.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, 4)),
19991 ],
19992 cx,
19993 );
19994 multibuffer.push_excerpts(
19995 buffer_2.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, 4)),
20000 ],
20001 cx,
20002 );
20003 multibuffer.push_excerpts(
20004 buffer_3.clone(),
20005 [
20006 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20007 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20008 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20009 ],
20010 cx,
20011 );
20012 multibuffer
20013 });
20014
20015 let fs = FakeFs::new(cx.executor());
20016 fs.insert_tree(
20017 "/a",
20018 json!({
20019 "main.rs": sample_text_1,
20020 "other.rs": sample_text_2,
20021 "lib.rs": sample_text_3,
20022 }),
20023 )
20024 .await;
20025 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20026 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20027 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20028 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20029 Editor::new(
20030 EditorMode::full(),
20031 multi_buffer,
20032 Some(project.clone()),
20033 window,
20034 cx,
20035 )
20036 });
20037 let multibuffer_item_id = workspace
20038 .update(cx, |workspace, window, cx| {
20039 assert!(
20040 workspace.active_item(cx).is_none(),
20041 "active item should be None before the first item is added"
20042 );
20043 workspace.add_item_to_active_pane(
20044 Box::new(multi_buffer_editor.clone()),
20045 None,
20046 true,
20047 window,
20048 cx,
20049 );
20050 let active_item = workspace
20051 .active_item(cx)
20052 .expect("should have an active item after adding the multi buffer");
20053 assert_eq!(
20054 active_item.buffer_kind(cx),
20055 ItemBufferKind::Multibuffer,
20056 "A multi buffer was expected to active after adding"
20057 );
20058 active_item.item_id()
20059 })
20060 .unwrap();
20061 cx.executor().run_until_parked();
20062
20063 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20064 editor.change_selections(
20065 SelectionEffects::scroll(Autoscroll::Next),
20066 window,
20067 cx,
20068 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20069 );
20070 editor.open_excerpts(&OpenExcerpts, window, cx);
20071 });
20072 cx.executor().run_until_parked();
20073 let first_item_id = workspace
20074 .update(cx, |workspace, window, cx| {
20075 let active_item = workspace
20076 .active_item(cx)
20077 .expect("should have an active item after navigating into the 1st buffer");
20078 let first_item_id = active_item.item_id();
20079 assert_ne!(
20080 first_item_id, multibuffer_item_id,
20081 "Should navigate into the 1st buffer and activate it"
20082 );
20083 assert_eq!(
20084 active_item.buffer_kind(cx),
20085 ItemBufferKind::Singleton,
20086 "New active item should be a singleton buffer"
20087 );
20088 assert_eq!(
20089 active_item
20090 .act_as::<Editor>(cx)
20091 .expect("should have navigated into an editor for the 1st buffer")
20092 .read(cx)
20093 .text(cx),
20094 sample_text_1
20095 );
20096
20097 workspace
20098 .go_back(workspace.active_pane().downgrade(), window, cx)
20099 .detach_and_log_err(cx);
20100
20101 first_item_id
20102 })
20103 .unwrap();
20104 cx.executor().run_until_parked();
20105 workspace
20106 .update(cx, |workspace, _, cx| {
20107 let active_item = workspace
20108 .active_item(cx)
20109 .expect("should have an active item after navigating back");
20110 assert_eq!(
20111 active_item.item_id(),
20112 multibuffer_item_id,
20113 "Should navigate back to the multi buffer"
20114 );
20115 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20116 })
20117 .unwrap();
20118
20119 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20120 editor.change_selections(
20121 SelectionEffects::scroll(Autoscroll::Next),
20122 window,
20123 cx,
20124 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20125 );
20126 editor.open_excerpts(&OpenExcerpts, window, cx);
20127 });
20128 cx.executor().run_until_parked();
20129 let second_item_id = workspace
20130 .update(cx, |workspace, window, cx| {
20131 let active_item = workspace
20132 .active_item(cx)
20133 .expect("should have an active item after navigating into the 2nd buffer");
20134 let second_item_id = active_item.item_id();
20135 assert_ne!(
20136 second_item_id, multibuffer_item_id,
20137 "Should navigate away from the multibuffer"
20138 );
20139 assert_ne!(
20140 second_item_id, first_item_id,
20141 "Should navigate into the 2nd buffer and activate it"
20142 );
20143 assert_eq!(
20144 active_item.buffer_kind(cx),
20145 ItemBufferKind::Singleton,
20146 "New active item should be a singleton buffer"
20147 );
20148 assert_eq!(
20149 active_item
20150 .act_as::<Editor>(cx)
20151 .expect("should have navigated into an editor")
20152 .read(cx)
20153 .text(cx),
20154 sample_text_2
20155 );
20156
20157 workspace
20158 .go_back(workspace.active_pane().downgrade(), window, cx)
20159 .detach_and_log_err(cx);
20160
20161 second_item_id
20162 })
20163 .unwrap();
20164 cx.executor().run_until_parked();
20165 workspace
20166 .update(cx, |workspace, _, cx| {
20167 let active_item = workspace
20168 .active_item(cx)
20169 .expect("should have an active item after navigating back from the 2nd buffer");
20170 assert_eq!(
20171 active_item.item_id(),
20172 multibuffer_item_id,
20173 "Should navigate back from the 2nd buffer to the multi buffer"
20174 );
20175 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20176 })
20177 .unwrap();
20178
20179 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20180 editor.change_selections(
20181 SelectionEffects::scroll(Autoscroll::Next),
20182 window,
20183 cx,
20184 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20185 );
20186 editor.open_excerpts(&OpenExcerpts, window, cx);
20187 });
20188 cx.executor().run_until_parked();
20189 workspace
20190 .update(cx, |workspace, window, cx| {
20191 let active_item = workspace
20192 .active_item(cx)
20193 .expect("should have an active item after navigating into the 3rd buffer");
20194 let third_item_id = active_item.item_id();
20195 assert_ne!(
20196 third_item_id, multibuffer_item_id,
20197 "Should navigate into the 3rd buffer and activate it"
20198 );
20199 assert_ne!(third_item_id, first_item_id);
20200 assert_ne!(third_item_id, second_item_id);
20201 assert_eq!(
20202 active_item.buffer_kind(cx),
20203 ItemBufferKind::Singleton,
20204 "New active item should be a singleton buffer"
20205 );
20206 assert_eq!(
20207 active_item
20208 .act_as::<Editor>(cx)
20209 .expect("should have navigated into an editor")
20210 .read(cx)
20211 .text(cx),
20212 sample_text_3
20213 );
20214
20215 workspace
20216 .go_back(workspace.active_pane().downgrade(), window, cx)
20217 .detach_and_log_err(cx);
20218 })
20219 .unwrap();
20220 cx.executor().run_until_parked();
20221 workspace
20222 .update(cx, |workspace, _, cx| {
20223 let active_item = workspace
20224 .active_item(cx)
20225 .expect("should have an active item after navigating back from the 3rd buffer");
20226 assert_eq!(
20227 active_item.item_id(),
20228 multibuffer_item_id,
20229 "Should navigate back from the 3rd buffer to the multi buffer"
20230 );
20231 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20232 })
20233 .unwrap();
20234}
20235
20236#[gpui::test]
20237async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20238 init_test(cx, |_| {});
20239
20240 let mut cx = EditorTestContext::new(cx).await;
20241
20242 let diff_base = r#"
20243 use some::mod;
20244
20245 const A: u32 = 42;
20246
20247 fn main() {
20248 println!("hello");
20249
20250 println!("world");
20251 }
20252 "#
20253 .unindent();
20254
20255 cx.set_state(
20256 &r#"
20257 use some::modified;
20258
20259 ˇ
20260 fn main() {
20261 println!("hello there");
20262
20263 println!("around the");
20264 println!("world");
20265 }
20266 "#
20267 .unindent(),
20268 );
20269
20270 cx.set_head_text(&diff_base);
20271 executor.run_until_parked();
20272
20273 cx.update_editor(|editor, window, cx| {
20274 editor.go_to_next_hunk(&GoToHunk, window, cx);
20275 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20276 });
20277 executor.run_until_parked();
20278 cx.assert_state_with_diff(
20279 r#"
20280 use some::modified;
20281
20282
20283 fn main() {
20284 - println!("hello");
20285 + ˇ println!("hello there");
20286
20287 println!("around the");
20288 println!("world");
20289 }
20290 "#
20291 .unindent(),
20292 );
20293
20294 cx.update_editor(|editor, window, cx| {
20295 for _ in 0..2 {
20296 editor.go_to_next_hunk(&GoToHunk, window, cx);
20297 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20298 }
20299 });
20300 executor.run_until_parked();
20301 cx.assert_state_with_diff(
20302 r#"
20303 - use some::mod;
20304 + ˇuse some::modified;
20305
20306
20307 fn main() {
20308 - println!("hello");
20309 + println!("hello there");
20310
20311 + println!("around the");
20312 println!("world");
20313 }
20314 "#
20315 .unindent(),
20316 );
20317
20318 cx.update_editor(|editor, window, cx| {
20319 editor.go_to_next_hunk(&GoToHunk, window, cx);
20320 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20321 });
20322 executor.run_until_parked();
20323 cx.assert_state_with_diff(
20324 r#"
20325 - use some::mod;
20326 + use some::modified;
20327
20328 - const A: u32 = 42;
20329 ˇ
20330 fn main() {
20331 - println!("hello");
20332 + println!("hello there");
20333
20334 + println!("around the");
20335 println!("world");
20336 }
20337 "#
20338 .unindent(),
20339 );
20340
20341 cx.update_editor(|editor, window, cx| {
20342 editor.cancel(&Cancel, window, cx);
20343 });
20344
20345 cx.assert_state_with_diff(
20346 r#"
20347 use some::modified;
20348
20349 ˇ
20350 fn main() {
20351 println!("hello there");
20352
20353 println!("around the");
20354 println!("world");
20355 }
20356 "#
20357 .unindent(),
20358 );
20359}
20360
20361#[gpui::test]
20362async fn test_diff_base_change_with_expanded_diff_hunks(
20363 executor: BackgroundExecutor,
20364 cx: &mut TestAppContext,
20365) {
20366 init_test(cx, |_| {});
20367
20368 let mut cx = EditorTestContext::new(cx).await;
20369
20370 let diff_base = r#"
20371 use some::mod1;
20372 use some::mod2;
20373
20374 const A: u32 = 42;
20375 const B: u32 = 42;
20376 const C: u32 = 42;
20377
20378 fn main() {
20379 println!("hello");
20380
20381 println!("world");
20382 }
20383 "#
20384 .unindent();
20385
20386 cx.set_state(
20387 &r#"
20388 use some::mod2;
20389
20390 const A: u32 = 42;
20391 const C: u32 = 42;
20392
20393 fn main(ˇ) {
20394 //println!("hello");
20395
20396 println!("world");
20397 //
20398 //
20399 }
20400 "#
20401 .unindent(),
20402 );
20403
20404 cx.set_head_text(&diff_base);
20405 executor.run_until_parked();
20406
20407 cx.update_editor(|editor, window, cx| {
20408 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20409 });
20410 executor.run_until_parked();
20411 cx.assert_state_with_diff(
20412 r#"
20413 - use some::mod1;
20414 use some::mod2;
20415
20416 const A: u32 = 42;
20417 - const B: u32 = 42;
20418 const C: u32 = 42;
20419
20420 fn main(ˇ) {
20421 - println!("hello");
20422 + //println!("hello");
20423
20424 println!("world");
20425 + //
20426 + //
20427 }
20428 "#
20429 .unindent(),
20430 );
20431
20432 cx.set_head_text("new diff base!");
20433 executor.run_until_parked();
20434 cx.assert_state_with_diff(
20435 r#"
20436 - new diff base!
20437 + use some::mod2;
20438 +
20439 + const A: u32 = 42;
20440 + const C: u32 = 42;
20441 +
20442 + fn main(ˇ) {
20443 + //println!("hello");
20444 +
20445 + println!("world");
20446 + //
20447 + //
20448 + }
20449 "#
20450 .unindent(),
20451 );
20452}
20453
20454#[gpui::test]
20455async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20456 init_test(cx, |_| {});
20457
20458 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20459 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20460 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20461 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20462 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20463 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20464
20465 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20466 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20467 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20468
20469 let multi_buffer = cx.new(|cx| {
20470 let mut multibuffer = MultiBuffer::new(ReadWrite);
20471 multibuffer.push_excerpts(
20472 buffer_1.clone(),
20473 [
20474 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20475 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20476 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20477 ],
20478 cx,
20479 );
20480 multibuffer.push_excerpts(
20481 buffer_2.clone(),
20482 [
20483 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20484 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20485 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20486 ],
20487 cx,
20488 );
20489 multibuffer.push_excerpts(
20490 buffer_3.clone(),
20491 [
20492 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20493 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20494 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20495 ],
20496 cx,
20497 );
20498 multibuffer
20499 });
20500
20501 let editor =
20502 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20503 editor
20504 .update(cx, |editor, _window, cx| {
20505 for (buffer, diff_base) in [
20506 (buffer_1.clone(), file_1_old),
20507 (buffer_2.clone(), file_2_old),
20508 (buffer_3.clone(), file_3_old),
20509 ] {
20510 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20511 editor
20512 .buffer
20513 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20514 }
20515 })
20516 .unwrap();
20517
20518 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20519 cx.run_until_parked();
20520
20521 cx.assert_editor_state(
20522 &"
20523 ˇaaa
20524 ccc
20525 ddd
20526
20527 ggg
20528 hhh
20529
20530
20531 lll
20532 mmm
20533 NNN
20534
20535 qqq
20536 rrr
20537
20538 uuu
20539 111
20540 222
20541 333
20542
20543 666
20544 777
20545
20546 000
20547 !!!"
20548 .unindent(),
20549 );
20550
20551 cx.update_editor(|editor, window, cx| {
20552 editor.select_all(&SelectAll, window, cx);
20553 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20554 });
20555 cx.executor().run_until_parked();
20556
20557 cx.assert_state_with_diff(
20558 "
20559 «aaa
20560 - bbb
20561 ccc
20562 ddd
20563
20564 ggg
20565 hhh
20566
20567
20568 lll
20569 mmm
20570 - nnn
20571 + NNN
20572
20573 qqq
20574 rrr
20575
20576 uuu
20577 111
20578 222
20579 333
20580
20581 + 666
20582 777
20583
20584 000
20585 !!!ˇ»"
20586 .unindent(),
20587 );
20588}
20589
20590#[gpui::test]
20591async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20592 init_test(cx, |_| {});
20593
20594 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20595 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20596
20597 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20598 let multi_buffer = cx.new(|cx| {
20599 let mut multibuffer = MultiBuffer::new(ReadWrite);
20600 multibuffer.push_excerpts(
20601 buffer.clone(),
20602 [
20603 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20604 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20605 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20606 ],
20607 cx,
20608 );
20609 multibuffer
20610 });
20611
20612 let editor =
20613 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20614 editor
20615 .update(cx, |editor, _window, cx| {
20616 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20617 editor
20618 .buffer
20619 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20620 })
20621 .unwrap();
20622
20623 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20624 cx.run_until_parked();
20625
20626 cx.update_editor(|editor, window, cx| {
20627 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20628 });
20629 cx.executor().run_until_parked();
20630
20631 // When the start of a hunk coincides with the start of its excerpt,
20632 // the hunk is expanded. When the start of a hunk is earlier than
20633 // the start of its excerpt, the hunk is not expanded.
20634 cx.assert_state_with_diff(
20635 "
20636 ˇaaa
20637 - bbb
20638 + BBB
20639
20640 - ddd
20641 - eee
20642 + DDD
20643 + EEE
20644 fff
20645
20646 iii
20647 "
20648 .unindent(),
20649 );
20650}
20651
20652#[gpui::test]
20653async fn test_edits_around_expanded_insertion_hunks(
20654 executor: BackgroundExecutor,
20655 cx: &mut TestAppContext,
20656) {
20657 init_test(cx, |_| {});
20658
20659 let mut cx = EditorTestContext::new(cx).await;
20660
20661 let diff_base = r#"
20662 use some::mod1;
20663 use some::mod2;
20664
20665 const A: u32 = 42;
20666
20667 fn main() {
20668 println!("hello");
20669
20670 println!("world");
20671 }
20672 "#
20673 .unindent();
20674 executor.run_until_parked();
20675 cx.set_state(
20676 &r#"
20677 use some::mod1;
20678 use some::mod2;
20679
20680 const A: u32 = 42;
20681 const B: u32 = 42;
20682 const C: u32 = 42;
20683 ˇ
20684
20685 fn main() {
20686 println!("hello");
20687
20688 println!("world");
20689 }
20690 "#
20691 .unindent(),
20692 );
20693
20694 cx.set_head_text(&diff_base);
20695 executor.run_until_parked();
20696
20697 cx.update_editor(|editor, window, cx| {
20698 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20699 });
20700 executor.run_until_parked();
20701
20702 cx.assert_state_with_diff(
20703 r#"
20704 use some::mod1;
20705 use some::mod2;
20706
20707 const A: u32 = 42;
20708 + const B: u32 = 42;
20709 + const C: u32 = 42;
20710 + ˇ
20711
20712 fn main() {
20713 println!("hello");
20714
20715 println!("world");
20716 }
20717 "#
20718 .unindent(),
20719 );
20720
20721 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20722 executor.run_until_parked();
20723
20724 cx.assert_state_with_diff(
20725 r#"
20726 use some::mod1;
20727 use some::mod2;
20728
20729 const A: u32 = 42;
20730 + const B: u32 = 42;
20731 + const C: u32 = 42;
20732 + const D: u32 = 42;
20733 + ˇ
20734
20735 fn main() {
20736 println!("hello");
20737
20738 println!("world");
20739 }
20740 "#
20741 .unindent(),
20742 );
20743
20744 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20745 executor.run_until_parked();
20746
20747 cx.assert_state_with_diff(
20748 r#"
20749 use some::mod1;
20750 use some::mod2;
20751
20752 const A: u32 = 42;
20753 + const B: u32 = 42;
20754 + const C: u32 = 42;
20755 + const D: u32 = 42;
20756 + const E: u32 = 42;
20757 + ˇ
20758
20759 fn main() {
20760 println!("hello");
20761
20762 println!("world");
20763 }
20764 "#
20765 .unindent(),
20766 );
20767
20768 cx.update_editor(|editor, window, cx| {
20769 editor.delete_line(&DeleteLine, window, cx);
20770 });
20771 executor.run_until_parked();
20772
20773 cx.assert_state_with_diff(
20774 r#"
20775 use some::mod1;
20776 use some::mod2;
20777
20778 const A: u32 = 42;
20779 + const B: u32 = 42;
20780 + const C: u32 = 42;
20781 + const D: u32 = 42;
20782 + const E: u32 = 42;
20783 ˇ
20784 fn main() {
20785 println!("hello");
20786
20787 println!("world");
20788 }
20789 "#
20790 .unindent(),
20791 );
20792
20793 cx.update_editor(|editor, window, cx| {
20794 editor.move_up(&MoveUp, window, cx);
20795 editor.delete_line(&DeleteLine, window, cx);
20796 editor.move_up(&MoveUp, window, cx);
20797 editor.delete_line(&DeleteLine, window, cx);
20798 editor.move_up(&MoveUp, window, cx);
20799 editor.delete_line(&DeleteLine, window, cx);
20800 });
20801 executor.run_until_parked();
20802 cx.assert_state_with_diff(
20803 r#"
20804 use some::mod1;
20805 use some::mod2;
20806
20807 const A: u32 = 42;
20808 + const B: u32 = 42;
20809 ˇ
20810 fn main() {
20811 println!("hello");
20812
20813 println!("world");
20814 }
20815 "#
20816 .unindent(),
20817 );
20818
20819 cx.update_editor(|editor, window, cx| {
20820 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20821 editor.delete_line(&DeleteLine, window, cx);
20822 });
20823 executor.run_until_parked();
20824 cx.assert_state_with_diff(
20825 r#"
20826 ˇ
20827 fn main() {
20828 println!("hello");
20829
20830 println!("world");
20831 }
20832 "#
20833 .unindent(),
20834 );
20835}
20836
20837#[gpui::test]
20838async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20839 init_test(cx, |_| {});
20840
20841 let mut cx = EditorTestContext::new(cx).await;
20842 cx.set_head_text(indoc! { "
20843 one
20844 two
20845 three
20846 four
20847 five
20848 "
20849 });
20850 cx.set_state(indoc! { "
20851 one
20852 ˇthree
20853 five
20854 "});
20855 cx.run_until_parked();
20856 cx.update_editor(|editor, window, cx| {
20857 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20858 });
20859 cx.assert_state_with_diff(
20860 indoc! { "
20861 one
20862 - two
20863 ˇthree
20864 - four
20865 five
20866 "}
20867 .to_string(),
20868 );
20869 cx.update_editor(|editor, window, cx| {
20870 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20871 });
20872
20873 cx.assert_state_with_diff(
20874 indoc! { "
20875 one
20876 ˇthree
20877 five
20878 "}
20879 .to_string(),
20880 );
20881
20882 cx.set_state(indoc! { "
20883 one
20884 ˇTWO
20885 three
20886 four
20887 five
20888 "});
20889 cx.run_until_parked();
20890 cx.update_editor(|editor, window, cx| {
20891 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20892 });
20893
20894 cx.assert_state_with_diff(
20895 indoc! { "
20896 one
20897 - two
20898 + ˇTWO
20899 three
20900 four
20901 five
20902 "}
20903 .to_string(),
20904 );
20905 cx.update_editor(|editor, window, cx| {
20906 editor.move_up(&Default::default(), window, cx);
20907 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20908 });
20909 cx.assert_state_with_diff(
20910 indoc! { "
20911 one
20912 ˇTWO
20913 three
20914 four
20915 five
20916 "}
20917 .to_string(),
20918 );
20919}
20920
20921#[gpui::test]
20922async fn test_edits_around_expanded_deletion_hunks(
20923 executor: BackgroundExecutor,
20924 cx: &mut TestAppContext,
20925) {
20926 init_test(cx, |_| {});
20927
20928 let mut cx = EditorTestContext::new(cx).await;
20929
20930 let diff_base = r#"
20931 use some::mod1;
20932 use some::mod2;
20933
20934 const A: u32 = 42;
20935 const B: u32 = 42;
20936 const C: u32 = 42;
20937
20938
20939 fn main() {
20940 println!("hello");
20941
20942 println!("world");
20943 }
20944 "#
20945 .unindent();
20946 executor.run_until_parked();
20947 cx.set_state(
20948 &r#"
20949 use some::mod1;
20950 use some::mod2;
20951
20952 ˇconst B: u32 = 42;
20953 const C: u32 = 42;
20954
20955
20956 fn main() {
20957 println!("hello");
20958
20959 println!("world");
20960 }
20961 "#
20962 .unindent(),
20963 );
20964
20965 cx.set_head_text(&diff_base);
20966 executor.run_until_parked();
20967
20968 cx.update_editor(|editor, window, cx| {
20969 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20970 });
20971 executor.run_until_parked();
20972
20973 cx.assert_state_with_diff(
20974 r#"
20975 use some::mod1;
20976 use some::mod2;
20977
20978 - const A: u32 = 42;
20979 ˇconst B: u32 = 42;
20980 const C: u32 = 42;
20981
20982
20983 fn main() {
20984 println!("hello");
20985
20986 println!("world");
20987 }
20988 "#
20989 .unindent(),
20990 );
20991
20992 cx.update_editor(|editor, window, cx| {
20993 editor.delete_line(&DeleteLine, window, cx);
20994 });
20995 executor.run_until_parked();
20996 cx.assert_state_with_diff(
20997 r#"
20998 use some::mod1;
20999 use some::mod2;
21000
21001 - const A: u32 = 42;
21002 - const B: u32 = 42;
21003 ˇconst C: u32 = 42;
21004
21005
21006 fn main() {
21007 println!("hello");
21008
21009 println!("world");
21010 }
21011 "#
21012 .unindent(),
21013 );
21014
21015 cx.update_editor(|editor, window, cx| {
21016 editor.delete_line(&DeleteLine, window, cx);
21017 });
21018 executor.run_until_parked();
21019 cx.assert_state_with_diff(
21020 r#"
21021 use some::mod1;
21022 use some::mod2;
21023
21024 - const A: u32 = 42;
21025 - const B: u32 = 42;
21026 - const C: u32 = 42;
21027 ˇ
21028
21029 fn main() {
21030 println!("hello");
21031
21032 println!("world");
21033 }
21034 "#
21035 .unindent(),
21036 );
21037
21038 cx.update_editor(|editor, window, cx| {
21039 editor.handle_input("replacement", window, cx);
21040 });
21041 executor.run_until_parked();
21042 cx.assert_state_with_diff(
21043 r#"
21044 use some::mod1;
21045 use some::mod2;
21046
21047 - const A: u32 = 42;
21048 - const B: u32 = 42;
21049 - const C: u32 = 42;
21050 -
21051 + replacementˇ
21052
21053 fn main() {
21054 println!("hello");
21055
21056 println!("world");
21057 }
21058 "#
21059 .unindent(),
21060 );
21061}
21062
21063#[gpui::test]
21064async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21065 init_test(cx, |_| {});
21066
21067 let mut cx = EditorTestContext::new(cx).await;
21068
21069 let base_text = r#"
21070 one
21071 two
21072 three
21073 four
21074 five
21075 "#
21076 .unindent();
21077 executor.run_until_parked();
21078 cx.set_state(
21079 &r#"
21080 one
21081 two
21082 fˇour
21083 five
21084 "#
21085 .unindent(),
21086 );
21087
21088 cx.set_head_text(&base_text);
21089 executor.run_until_parked();
21090
21091 cx.update_editor(|editor, window, cx| {
21092 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21093 });
21094 executor.run_until_parked();
21095
21096 cx.assert_state_with_diff(
21097 r#"
21098 one
21099 two
21100 - three
21101 fˇour
21102 five
21103 "#
21104 .unindent(),
21105 );
21106
21107 cx.update_editor(|editor, window, cx| {
21108 editor.backspace(&Backspace, window, cx);
21109 editor.backspace(&Backspace, window, cx);
21110 });
21111 executor.run_until_parked();
21112 cx.assert_state_with_diff(
21113 r#"
21114 one
21115 two
21116 - threeˇ
21117 - four
21118 + our
21119 five
21120 "#
21121 .unindent(),
21122 );
21123}
21124
21125#[gpui::test]
21126async fn test_edit_after_expanded_modification_hunk(
21127 executor: BackgroundExecutor,
21128 cx: &mut TestAppContext,
21129) {
21130 init_test(cx, |_| {});
21131
21132 let mut cx = EditorTestContext::new(cx).await;
21133
21134 let diff_base = r#"
21135 use some::mod1;
21136 use some::mod2;
21137
21138 const A: u32 = 42;
21139 const B: u32 = 42;
21140 const C: u32 = 42;
21141 const D: u32 = 42;
21142
21143
21144 fn main() {
21145 println!("hello");
21146
21147 println!("world");
21148 }"#
21149 .unindent();
21150
21151 cx.set_state(
21152 &r#"
21153 use some::mod1;
21154 use some::mod2;
21155
21156 const A: u32 = 42;
21157 const B: u32 = 42;
21158 const C: u32 = 43ˇ
21159 const D: u32 = 42;
21160
21161
21162 fn main() {
21163 println!("hello");
21164
21165 println!("world");
21166 }"#
21167 .unindent(),
21168 );
21169
21170 cx.set_head_text(&diff_base);
21171 executor.run_until_parked();
21172 cx.update_editor(|editor, window, cx| {
21173 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21174 });
21175 executor.run_until_parked();
21176
21177 cx.assert_state_with_diff(
21178 r#"
21179 use some::mod1;
21180 use some::mod2;
21181
21182 const A: u32 = 42;
21183 const B: u32 = 42;
21184 - const C: u32 = 42;
21185 + const C: u32 = 43ˇ
21186 const D: u32 = 42;
21187
21188
21189 fn main() {
21190 println!("hello");
21191
21192 println!("world");
21193 }"#
21194 .unindent(),
21195 );
21196
21197 cx.update_editor(|editor, window, cx| {
21198 editor.handle_input("\nnew_line\n", window, cx);
21199 });
21200 executor.run_until_parked();
21201
21202 cx.assert_state_with_diff(
21203 r#"
21204 use some::mod1;
21205 use some::mod2;
21206
21207 const A: u32 = 42;
21208 const B: u32 = 42;
21209 - const C: u32 = 42;
21210 + const C: u32 = 43
21211 + new_line
21212 + ˇ
21213 const D: u32 = 42;
21214
21215
21216 fn main() {
21217 println!("hello");
21218
21219 println!("world");
21220 }"#
21221 .unindent(),
21222 );
21223}
21224
21225#[gpui::test]
21226async fn test_stage_and_unstage_added_file_hunk(
21227 executor: BackgroundExecutor,
21228 cx: &mut TestAppContext,
21229) {
21230 init_test(cx, |_| {});
21231
21232 let mut cx = EditorTestContext::new(cx).await;
21233 cx.update_editor(|editor, _, cx| {
21234 editor.set_expand_all_diff_hunks(cx);
21235 });
21236
21237 let working_copy = r#"
21238 ˇfn main() {
21239 println!("hello, world!");
21240 }
21241 "#
21242 .unindent();
21243
21244 cx.set_state(&working_copy);
21245 executor.run_until_parked();
21246
21247 cx.assert_state_with_diff(
21248 r#"
21249 + ˇfn main() {
21250 + println!("hello, world!");
21251 + }
21252 "#
21253 .unindent(),
21254 );
21255 cx.assert_index_text(None);
21256
21257 cx.update_editor(|editor, window, cx| {
21258 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21259 });
21260 executor.run_until_parked();
21261 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21262 cx.assert_state_with_diff(
21263 r#"
21264 + ˇfn main() {
21265 + println!("hello, world!");
21266 + }
21267 "#
21268 .unindent(),
21269 );
21270
21271 cx.update_editor(|editor, window, cx| {
21272 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21273 });
21274 executor.run_until_parked();
21275 cx.assert_index_text(None);
21276}
21277
21278async fn setup_indent_guides_editor(
21279 text: &str,
21280 cx: &mut TestAppContext,
21281) -> (BufferId, EditorTestContext) {
21282 init_test(cx, |_| {});
21283
21284 let mut cx = EditorTestContext::new(cx).await;
21285
21286 let buffer_id = cx.update_editor(|editor, window, cx| {
21287 editor.set_text(text, window, cx);
21288 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21289
21290 buffer_ids[0]
21291 });
21292
21293 (buffer_id, cx)
21294}
21295
21296fn assert_indent_guides(
21297 range: Range<u32>,
21298 expected: Vec<IndentGuide>,
21299 active_indices: Option<Vec<usize>>,
21300 cx: &mut EditorTestContext,
21301) {
21302 let indent_guides = cx.update_editor(|editor, window, cx| {
21303 let snapshot = editor.snapshot(window, cx).display_snapshot;
21304 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21305 editor,
21306 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21307 true,
21308 &snapshot,
21309 cx,
21310 );
21311
21312 indent_guides.sort_by(|a, b| {
21313 a.depth.cmp(&b.depth).then(
21314 a.start_row
21315 .cmp(&b.start_row)
21316 .then(a.end_row.cmp(&b.end_row)),
21317 )
21318 });
21319 indent_guides
21320 });
21321
21322 if let Some(expected) = active_indices {
21323 let active_indices = cx.update_editor(|editor, window, cx| {
21324 let snapshot = editor.snapshot(window, cx).display_snapshot;
21325 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21326 });
21327
21328 assert_eq!(
21329 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21330 expected,
21331 "Active indent guide indices do not match"
21332 );
21333 }
21334
21335 assert_eq!(indent_guides, expected, "Indent guides do not match");
21336}
21337
21338fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21339 IndentGuide {
21340 buffer_id,
21341 start_row: MultiBufferRow(start_row),
21342 end_row: MultiBufferRow(end_row),
21343 depth,
21344 tab_size: 4,
21345 settings: IndentGuideSettings {
21346 enabled: true,
21347 line_width: 1,
21348 active_line_width: 1,
21349 coloring: IndentGuideColoring::default(),
21350 background_coloring: IndentGuideBackgroundColoring::default(),
21351 },
21352 }
21353}
21354
21355#[gpui::test]
21356async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21357 let (buffer_id, mut cx) = setup_indent_guides_editor(
21358 &"
21359 fn main() {
21360 let a = 1;
21361 }"
21362 .unindent(),
21363 cx,
21364 )
21365 .await;
21366
21367 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21368}
21369
21370#[gpui::test]
21371async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21372 let (buffer_id, mut cx) = setup_indent_guides_editor(
21373 &"
21374 fn main() {
21375 let a = 1;
21376 let b = 2;
21377 }"
21378 .unindent(),
21379 cx,
21380 )
21381 .await;
21382
21383 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21384}
21385
21386#[gpui::test]
21387async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21388 let (buffer_id, mut cx) = setup_indent_guides_editor(
21389 &"
21390 fn main() {
21391 let a = 1;
21392 if a == 3 {
21393 let b = 2;
21394 } else {
21395 let c = 3;
21396 }
21397 }"
21398 .unindent(),
21399 cx,
21400 )
21401 .await;
21402
21403 assert_indent_guides(
21404 0..8,
21405 vec![
21406 indent_guide(buffer_id, 1, 6, 0),
21407 indent_guide(buffer_id, 3, 3, 1),
21408 indent_guide(buffer_id, 5, 5, 1),
21409 ],
21410 None,
21411 &mut cx,
21412 );
21413}
21414
21415#[gpui::test]
21416async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21417 let (buffer_id, mut cx) = setup_indent_guides_editor(
21418 &"
21419 fn main() {
21420 let a = 1;
21421 let b = 2;
21422 let c = 3;
21423 }"
21424 .unindent(),
21425 cx,
21426 )
21427 .await;
21428
21429 assert_indent_guides(
21430 0..5,
21431 vec![
21432 indent_guide(buffer_id, 1, 3, 0),
21433 indent_guide(buffer_id, 2, 2, 1),
21434 ],
21435 None,
21436 &mut cx,
21437 );
21438}
21439
21440#[gpui::test]
21441async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21442 let (buffer_id, mut cx) = setup_indent_guides_editor(
21443 &"
21444 fn main() {
21445 let a = 1;
21446
21447 let c = 3;
21448 }"
21449 .unindent(),
21450 cx,
21451 )
21452 .await;
21453
21454 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21455}
21456
21457#[gpui::test]
21458async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21459 let (buffer_id, mut cx) = setup_indent_guides_editor(
21460 &"
21461 fn main() {
21462 let a = 1;
21463
21464 let c = 3;
21465
21466 if a == 3 {
21467 let b = 2;
21468 } else {
21469 let c = 3;
21470 }
21471 }"
21472 .unindent(),
21473 cx,
21474 )
21475 .await;
21476
21477 assert_indent_guides(
21478 0..11,
21479 vec![
21480 indent_guide(buffer_id, 1, 9, 0),
21481 indent_guide(buffer_id, 6, 6, 1),
21482 indent_guide(buffer_id, 8, 8, 1),
21483 ],
21484 None,
21485 &mut cx,
21486 );
21487}
21488
21489#[gpui::test]
21490async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21491 let (buffer_id, mut cx) = setup_indent_guides_editor(
21492 &"
21493 fn main() {
21494 let a = 1;
21495
21496 let c = 3;
21497
21498 if a == 3 {
21499 let b = 2;
21500 } else {
21501 let c = 3;
21502 }
21503 }"
21504 .unindent(),
21505 cx,
21506 )
21507 .await;
21508
21509 assert_indent_guides(
21510 1..11,
21511 vec![
21512 indent_guide(buffer_id, 1, 9, 0),
21513 indent_guide(buffer_id, 6, 6, 1),
21514 indent_guide(buffer_id, 8, 8, 1),
21515 ],
21516 None,
21517 &mut cx,
21518 );
21519}
21520
21521#[gpui::test]
21522async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21523 let (buffer_id, mut cx) = setup_indent_guides_editor(
21524 &"
21525 fn main() {
21526 let a = 1;
21527
21528 let c = 3;
21529
21530 if a == 3 {
21531 let b = 2;
21532 } else {
21533 let c = 3;
21534 }
21535 }"
21536 .unindent(),
21537 cx,
21538 )
21539 .await;
21540
21541 assert_indent_guides(
21542 1..10,
21543 vec![
21544 indent_guide(buffer_id, 1, 9, 0),
21545 indent_guide(buffer_id, 6, 6, 1),
21546 indent_guide(buffer_id, 8, 8, 1),
21547 ],
21548 None,
21549 &mut cx,
21550 );
21551}
21552
21553#[gpui::test]
21554async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21555 let (buffer_id, mut cx) = setup_indent_guides_editor(
21556 &"
21557 fn main() {
21558 if a {
21559 b(
21560 c,
21561 d,
21562 )
21563 } else {
21564 e(
21565 f
21566 )
21567 }
21568 }"
21569 .unindent(),
21570 cx,
21571 )
21572 .await;
21573
21574 assert_indent_guides(
21575 0..11,
21576 vec![
21577 indent_guide(buffer_id, 1, 10, 0),
21578 indent_guide(buffer_id, 2, 5, 1),
21579 indent_guide(buffer_id, 7, 9, 1),
21580 indent_guide(buffer_id, 3, 4, 2),
21581 indent_guide(buffer_id, 8, 8, 2),
21582 ],
21583 None,
21584 &mut cx,
21585 );
21586
21587 cx.update_editor(|editor, window, cx| {
21588 editor.fold_at(MultiBufferRow(2), window, cx);
21589 assert_eq!(
21590 editor.display_text(cx),
21591 "
21592 fn main() {
21593 if a {
21594 b(⋯
21595 )
21596 } else {
21597 e(
21598 f
21599 )
21600 }
21601 }"
21602 .unindent()
21603 );
21604 });
21605
21606 assert_indent_guides(
21607 0..11,
21608 vec![
21609 indent_guide(buffer_id, 1, 10, 0),
21610 indent_guide(buffer_id, 2, 5, 1),
21611 indent_guide(buffer_id, 7, 9, 1),
21612 indent_guide(buffer_id, 8, 8, 2),
21613 ],
21614 None,
21615 &mut cx,
21616 );
21617}
21618
21619#[gpui::test]
21620async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21621 let (buffer_id, mut cx) = setup_indent_guides_editor(
21622 &"
21623 block1
21624 block2
21625 block3
21626 block4
21627 block2
21628 block1
21629 block1"
21630 .unindent(),
21631 cx,
21632 )
21633 .await;
21634
21635 assert_indent_guides(
21636 1..10,
21637 vec![
21638 indent_guide(buffer_id, 1, 4, 0),
21639 indent_guide(buffer_id, 2, 3, 1),
21640 indent_guide(buffer_id, 3, 3, 2),
21641 ],
21642 None,
21643 &mut cx,
21644 );
21645}
21646
21647#[gpui::test]
21648async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21649 let (buffer_id, mut cx) = setup_indent_guides_editor(
21650 &"
21651 block1
21652 block2
21653 block3
21654
21655 block1
21656 block1"
21657 .unindent(),
21658 cx,
21659 )
21660 .await;
21661
21662 assert_indent_guides(
21663 0..6,
21664 vec![
21665 indent_guide(buffer_id, 1, 2, 0),
21666 indent_guide(buffer_id, 2, 2, 1),
21667 ],
21668 None,
21669 &mut cx,
21670 );
21671}
21672
21673#[gpui::test]
21674async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21675 let (buffer_id, mut cx) = setup_indent_guides_editor(
21676 &"
21677 function component() {
21678 \treturn (
21679 \t\t\t
21680 \t\t<div>
21681 \t\t\t<abc></abc>
21682 \t\t</div>
21683 \t)
21684 }"
21685 .unindent(),
21686 cx,
21687 )
21688 .await;
21689
21690 assert_indent_guides(
21691 0..8,
21692 vec![
21693 indent_guide(buffer_id, 1, 6, 0),
21694 indent_guide(buffer_id, 2, 5, 1),
21695 indent_guide(buffer_id, 4, 4, 2),
21696 ],
21697 None,
21698 &mut cx,
21699 );
21700}
21701
21702#[gpui::test]
21703async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21704 let (buffer_id, mut cx) = setup_indent_guides_editor(
21705 &"
21706 function component() {
21707 \treturn (
21708 \t
21709 \t\t<div>
21710 \t\t\t<abc></abc>
21711 \t\t</div>
21712 \t)
21713 }"
21714 .unindent(),
21715 cx,
21716 )
21717 .await;
21718
21719 assert_indent_guides(
21720 0..8,
21721 vec![
21722 indent_guide(buffer_id, 1, 6, 0),
21723 indent_guide(buffer_id, 2, 5, 1),
21724 indent_guide(buffer_id, 4, 4, 2),
21725 ],
21726 None,
21727 &mut cx,
21728 );
21729}
21730
21731#[gpui::test]
21732async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21733 let (buffer_id, mut cx) = setup_indent_guides_editor(
21734 &"
21735 block1
21736
21737
21738
21739 block2
21740 "
21741 .unindent(),
21742 cx,
21743 )
21744 .await;
21745
21746 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21747}
21748
21749#[gpui::test]
21750async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21751 let (buffer_id, mut cx) = setup_indent_guides_editor(
21752 &"
21753 def a:
21754 \tb = 3
21755 \tif True:
21756 \t\tc = 4
21757 \t\td = 5
21758 \tprint(b)
21759 "
21760 .unindent(),
21761 cx,
21762 )
21763 .await;
21764
21765 assert_indent_guides(
21766 0..6,
21767 vec![
21768 indent_guide(buffer_id, 1, 5, 0),
21769 indent_guide(buffer_id, 3, 4, 1),
21770 ],
21771 None,
21772 &mut cx,
21773 );
21774}
21775
21776#[gpui::test]
21777async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21778 let (buffer_id, mut cx) = setup_indent_guides_editor(
21779 &"
21780 fn main() {
21781 let a = 1;
21782 }"
21783 .unindent(),
21784 cx,
21785 )
21786 .await;
21787
21788 cx.update_editor(|editor, window, cx| {
21789 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21790 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21791 });
21792 });
21793
21794 assert_indent_guides(
21795 0..3,
21796 vec![indent_guide(buffer_id, 1, 1, 0)],
21797 Some(vec![0]),
21798 &mut cx,
21799 );
21800}
21801
21802#[gpui::test]
21803async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21804 let (buffer_id, mut cx) = setup_indent_guides_editor(
21805 &"
21806 fn main() {
21807 if 1 == 2 {
21808 let a = 1;
21809 }
21810 }"
21811 .unindent(),
21812 cx,
21813 )
21814 .await;
21815
21816 cx.update_editor(|editor, window, cx| {
21817 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21818 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21819 });
21820 });
21821
21822 assert_indent_guides(
21823 0..4,
21824 vec![
21825 indent_guide(buffer_id, 1, 3, 0),
21826 indent_guide(buffer_id, 2, 2, 1),
21827 ],
21828 Some(vec![1]),
21829 &mut cx,
21830 );
21831
21832 cx.update_editor(|editor, window, cx| {
21833 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21834 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21835 });
21836 });
21837
21838 assert_indent_guides(
21839 0..4,
21840 vec![
21841 indent_guide(buffer_id, 1, 3, 0),
21842 indent_guide(buffer_id, 2, 2, 1),
21843 ],
21844 Some(vec![1]),
21845 &mut cx,
21846 );
21847
21848 cx.update_editor(|editor, window, cx| {
21849 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21850 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21851 });
21852 });
21853
21854 assert_indent_guides(
21855 0..4,
21856 vec![
21857 indent_guide(buffer_id, 1, 3, 0),
21858 indent_guide(buffer_id, 2, 2, 1),
21859 ],
21860 Some(vec![0]),
21861 &mut cx,
21862 );
21863}
21864
21865#[gpui::test]
21866async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21867 let (buffer_id, mut cx) = setup_indent_guides_editor(
21868 &"
21869 fn main() {
21870 let a = 1;
21871
21872 let b = 2;
21873 }"
21874 .unindent(),
21875 cx,
21876 )
21877 .await;
21878
21879 cx.update_editor(|editor, window, cx| {
21880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21881 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21882 });
21883 });
21884
21885 assert_indent_guides(
21886 0..5,
21887 vec![indent_guide(buffer_id, 1, 3, 0)],
21888 Some(vec![0]),
21889 &mut cx,
21890 );
21891}
21892
21893#[gpui::test]
21894async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21895 let (buffer_id, mut cx) = setup_indent_guides_editor(
21896 &"
21897 def m:
21898 a = 1
21899 pass"
21900 .unindent(),
21901 cx,
21902 )
21903 .await;
21904
21905 cx.update_editor(|editor, window, cx| {
21906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21907 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21908 });
21909 });
21910
21911 assert_indent_guides(
21912 0..3,
21913 vec![indent_guide(buffer_id, 1, 2, 0)],
21914 Some(vec![0]),
21915 &mut cx,
21916 );
21917}
21918
21919#[gpui::test]
21920async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21921 init_test(cx, |_| {});
21922 let mut cx = EditorTestContext::new(cx).await;
21923 let text = indoc! {
21924 "
21925 impl A {
21926 fn b() {
21927 0;
21928 3;
21929 5;
21930 6;
21931 7;
21932 }
21933 }
21934 "
21935 };
21936 let base_text = indoc! {
21937 "
21938 impl A {
21939 fn b() {
21940 0;
21941 1;
21942 2;
21943 3;
21944 4;
21945 }
21946 fn c() {
21947 5;
21948 6;
21949 7;
21950 }
21951 }
21952 "
21953 };
21954
21955 cx.update_editor(|editor, window, cx| {
21956 editor.set_text(text, window, cx);
21957
21958 editor.buffer().update(cx, |multibuffer, cx| {
21959 let buffer = multibuffer.as_singleton().unwrap();
21960 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21961
21962 multibuffer.set_all_diff_hunks_expanded(cx);
21963 multibuffer.add_diff(diff, cx);
21964
21965 buffer.read(cx).remote_id()
21966 })
21967 });
21968 cx.run_until_parked();
21969
21970 cx.assert_state_with_diff(
21971 indoc! { "
21972 impl A {
21973 fn b() {
21974 0;
21975 - 1;
21976 - 2;
21977 3;
21978 - 4;
21979 - }
21980 - fn c() {
21981 5;
21982 6;
21983 7;
21984 }
21985 }
21986 ˇ"
21987 }
21988 .to_string(),
21989 );
21990
21991 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21992 editor
21993 .snapshot(window, cx)
21994 .buffer_snapshot()
21995 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21996 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21997 .collect::<Vec<_>>()
21998 });
21999 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22000 assert_eq!(
22001 actual_guides,
22002 vec![
22003 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22004 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22005 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22006 ]
22007 );
22008}
22009
22010#[gpui::test]
22011async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22012 init_test(cx, |_| {});
22013 let mut cx = EditorTestContext::new(cx).await;
22014
22015 let diff_base = r#"
22016 a
22017 b
22018 c
22019 "#
22020 .unindent();
22021
22022 cx.set_state(
22023 &r#"
22024 ˇA
22025 b
22026 C
22027 "#
22028 .unindent(),
22029 );
22030 cx.set_head_text(&diff_base);
22031 cx.update_editor(|editor, window, cx| {
22032 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22033 });
22034 executor.run_until_parked();
22035
22036 let both_hunks_expanded = r#"
22037 - a
22038 + ˇA
22039 b
22040 - c
22041 + C
22042 "#
22043 .unindent();
22044
22045 cx.assert_state_with_diff(both_hunks_expanded.clone());
22046
22047 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22048 let snapshot = editor.snapshot(window, cx);
22049 let hunks = editor
22050 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22051 .collect::<Vec<_>>();
22052 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22053 hunks
22054 .into_iter()
22055 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22056 .collect::<Vec<_>>()
22057 });
22058 assert_eq!(hunk_ranges.len(), 2);
22059
22060 cx.update_editor(|editor, _, cx| {
22061 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22062 });
22063 executor.run_until_parked();
22064
22065 let second_hunk_expanded = r#"
22066 ˇA
22067 b
22068 - c
22069 + C
22070 "#
22071 .unindent();
22072
22073 cx.assert_state_with_diff(second_hunk_expanded);
22074
22075 cx.update_editor(|editor, _, cx| {
22076 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22077 });
22078 executor.run_until_parked();
22079
22080 cx.assert_state_with_diff(both_hunks_expanded.clone());
22081
22082 cx.update_editor(|editor, _, cx| {
22083 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22084 });
22085 executor.run_until_parked();
22086
22087 let first_hunk_expanded = r#"
22088 - a
22089 + ˇA
22090 b
22091 C
22092 "#
22093 .unindent();
22094
22095 cx.assert_state_with_diff(first_hunk_expanded);
22096
22097 cx.update_editor(|editor, _, cx| {
22098 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22099 });
22100 executor.run_until_parked();
22101
22102 cx.assert_state_with_diff(both_hunks_expanded);
22103
22104 cx.set_state(
22105 &r#"
22106 ˇA
22107 b
22108 "#
22109 .unindent(),
22110 );
22111 cx.run_until_parked();
22112
22113 // TODO this cursor position seems bad
22114 cx.assert_state_with_diff(
22115 r#"
22116 - ˇa
22117 + A
22118 b
22119 "#
22120 .unindent(),
22121 );
22122
22123 cx.update_editor(|editor, window, cx| {
22124 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22125 });
22126
22127 cx.assert_state_with_diff(
22128 r#"
22129 - ˇa
22130 + A
22131 b
22132 - c
22133 "#
22134 .unindent(),
22135 );
22136
22137 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22138 let snapshot = editor.snapshot(window, cx);
22139 let hunks = editor
22140 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22141 .collect::<Vec<_>>();
22142 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22143 hunks
22144 .into_iter()
22145 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22146 .collect::<Vec<_>>()
22147 });
22148 assert_eq!(hunk_ranges.len(), 2);
22149
22150 cx.update_editor(|editor, _, cx| {
22151 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22152 });
22153 executor.run_until_parked();
22154
22155 cx.assert_state_with_diff(
22156 r#"
22157 - ˇa
22158 + A
22159 b
22160 "#
22161 .unindent(),
22162 );
22163}
22164
22165#[gpui::test]
22166async fn test_toggle_deletion_hunk_at_start_of_file(
22167 executor: BackgroundExecutor,
22168 cx: &mut TestAppContext,
22169) {
22170 init_test(cx, |_| {});
22171 let mut cx = EditorTestContext::new(cx).await;
22172
22173 let diff_base = r#"
22174 a
22175 b
22176 c
22177 "#
22178 .unindent();
22179
22180 cx.set_state(
22181 &r#"
22182 ˇb
22183 c
22184 "#
22185 .unindent(),
22186 );
22187 cx.set_head_text(&diff_base);
22188 cx.update_editor(|editor, window, cx| {
22189 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22190 });
22191 executor.run_until_parked();
22192
22193 let hunk_expanded = r#"
22194 - a
22195 ˇb
22196 c
22197 "#
22198 .unindent();
22199
22200 cx.assert_state_with_diff(hunk_expanded.clone());
22201
22202 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22203 let snapshot = editor.snapshot(window, cx);
22204 let hunks = editor
22205 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22206 .collect::<Vec<_>>();
22207 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22208 hunks
22209 .into_iter()
22210 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22211 .collect::<Vec<_>>()
22212 });
22213 assert_eq!(hunk_ranges.len(), 1);
22214
22215 cx.update_editor(|editor, _, cx| {
22216 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22217 });
22218 executor.run_until_parked();
22219
22220 let hunk_collapsed = r#"
22221 ˇb
22222 c
22223 "#
22224 .unindent();
22225
22226 cx.assert_state_with_diff(hunk_collapsed);
22227
22228 cx.update_editor(|editor, _, cx| {
22229 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22230 });
22231 executor.run_until_parked();
22232
22233 cx.assert_state_with_diff(hunk_expanded);
22234}
22235
22236#[gpui::test]
22237async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22238 executor: BackgroundExecutor,
22239 cx: &mut TestAppContext,
22240) {
22241 init_test(cx, |_| {});
22242 let mut cx = EditorTestContext::new(cx).await;
22243
22244 cx.set_state("ˇnew\nsecond\nthird\n");
22245 cx.set_head_text("old\nsecond\nthird\n");
22246 cx.update_editor(|editor, window, cx| {
22247 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22248 });
22249 executor.run_until_parked();
22250 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22251
22252 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22253 cx.update_editor(|editor, window, cx| {
22254 let snapshot = editor.snapshot(window, cx);
22255 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22256 let hunks = editor
22257 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22258 .collect::<Vec<_>>();
22259 assert_eq!(hunks.len(), 1);
22260 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22261 editor.toggle_single_diff_hunk(hunk_range, cx)
22262 });
22263 executor.run_until_parked();
22264 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22265
22266 // Keep the editor scrolled to the top so the full hunk remains visible.
22267 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22268}
22269
22270#[gpui::test]
22271async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22272 init_test(cx, |_| {});
22273
22274 let fs = FakeFs::new(cx.executor());
22275 fs.insert_tree(
22276 path!("/test"),
22277 json!({
22278 ".git": {},
22279 "file-1": "ONE\n",
22280 "file-2": "TWO\n",
22281 "file-3": "THREE\n",
22282 }),
22283 )
22284 .await;
22285
22286 fs.set_head_for_repo(
22287 path!("/test/.git").as_ref(),
22288 &[
22289 ("file-1", "one\n".into()),
22290 ("file-2", "two\n".into()),
22291 ("file-3", "three\n".into()),
22292 ],
22293 "deadbeef",
22294 );
22295
22296 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22297 let mut buffers = vec![];
22298 for i in 1..=3 {
22299 let buffer = project
22300 .update(cx, |project, cx| {
22301 let path = format!(path!("/test/file-{}"), i);
22302 project.open_local_buffer(path, cx)
22303 })
22304 .await
22305 .unwrap();
22306 buffers.push(buffer);
22307 }
22308
22309 let multibuffer = cx.new(|cx| {
22310 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22311 multibuffer.set_all_diff_hunks_expanded(cx);
22312 for buffer in &buffers {
22313 let snapshot = buffer.read(cx).snapshot();
22314 multibuffer.set_excerpts_for_path(
22315 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22316 buffer.clone(),
22317 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22318 2,
22319 cx,
22320 );
22321 }
22322 multibuffer
22323 });
22324
22325 let editor = cx.add_window(|window, cx| {
22326 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22327 });
22328 cx.run_until_parked();
22329
22330 let snapshot = editor
22331 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22332 .unwrap();
22333 let hunks = snapshot
22334 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22335 .map(|hunk| match hunk {
22336 DisplayDiffHunk::Unfolded {
22337 display_row_range, ..
22338 } => display_row_range,
22339 DisplayDiffHunk::Folded { .. } => unreachable!(),
22340 })
22341 .collect::<Vec<_>>();
22342 assert_eq!(
22343 hunks,
22344 [
22345 DisplayRow(2)..DisplayRow(4),
22346 DisplayRow(7)..DisplayRow(9),
22347 DisplayRow(12)..DisplayRow(14),
22348 ]
22349 );
22350}
22351
22352#[gpui::test]
22353async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22354 init_test(cx, |_| {});
22355
22356 let mut cx = EditorTestContext::new(cx).await;
22357 cx.set_head_text(indoc! { "
22358 one
22359 two
22360 three
22361 four
22362 five
22363 "
22364 });
22365 cx.set_index_text(indoc! { "
22366 one
22367 two
22368 three
22369 four
22370 five
22371 "
22372 });
22373 cx.set_state(indoc! {"
22374 one
22375 TWO
22376 ˇTHREE
22377 FOUR
22378 five
22379 "});
22380 cx.run_until_parked();
22381 cx.update_editor(|editor, window, cx| {
22382 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22383 });
22384 cx.run_until_parked();
22385 cx.assert_index_text(Some(indoc! {"
22386 one
22387 TWO
22388 THREE
22389 FOUR
22390 five
22391 "}));
22392 cx.set_state(indoc! { "
22393 one
22394 TWO
22395 ˇTHREE-HUNDRED
22396 FOUR
22397 five
22398 "});
22399 cx.run_until_parked();
22400 cx.update_editor(|editor, window, cx| {
22401 let snapshot = editor.snapshot(window, cx);
22402 let hunks = editor
22403 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22404 .collect::<Vec<_>>();
22405 assert_eq!(hunks.len(), 1);
22406 assert_eq!(
22407 hunks[0].status(),
22408 DiffHunkStatus {
22409 kind: DiffHunkStatusKind::Modified,
22410 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22411 }
22412 );
22413
22414 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22415 });
22416 cx.run_until_parked();
22417 cx.assert_index_text(Some(indoc! {"
22418 one
22419 TWO
22420 THREE-HUNDRED
22421 FOUR
22422 five
22423 "}));
22424}
22425
22426#[gpui::test]
22427fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22428 init_test(cx, |_| {});
22429
22430 let editor = cx.add_window(|window, cx| {
22431 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22432 build_editor(buffer, window, cx)
22433 });
22434
22435 let render_args = Arc::new(Mutex::new(None));
22436 let snapshot = editor
22437 .update(cx, |editor, window, cx| {
22438 let snapshot = editor.buffer().read(cx).snapshot(cx);
22439 let range =
22440 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22441
22442 struct RenderArgs {
22443 row: MultiBufferRow,
22444 folded: bool,
22445 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22446 }
22447
22448 let crease = Crease::inline(
22449 range,
22450 FoldPlaceholder::test(),
22451 {
22452 let toggle_callback = render_args.clone();
22453 move |row, folded, callback, _window, _cx| {
22454 *toggle_callback.lock() = Some(RenderArgs {
22455 row,
22456 folded,
22457 callback,
22458 });
22459 div()
22460 }
22461 },
22462 |_row, _folded, _window, _cx| div(),
22463 );
22464
22465 editor.insert_creases(Some(crease), cx);
22466 let snapshot = editor.snapshot(window, cx);
22467 let _div =
22468 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22469 snapshot
22470 })
22471 .unwrap();
22472
22473 let render_args = render_args.lock().take().unwrap();
22474 assert_eq!(render_args.row, MultiBufferRow(1));
22475 assert!(!render_args.folded);
22476 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22477
22478 cx.update_window(*editor, |_, window, cx| {
22479 (render_args.callback)(true, window, cx)
22480 })
22481 .unwrap();
22482 let snapshot = editor
22483 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22484 .unwrap();
22485 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22486
22487 cx.update_window(*editor, |_, window, cx| {
22488 (render_args.callback)(false, window, cx)
22489 })
22490 .unwrap();
22491 let snapshot = editor
22492 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22493 .unwrap();
22494 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22495}
22496
22497#[gpui::test]
22498async fn test_input_text(cx: &mut TestAppContext) {
22499 init_test(cx, |_| {});
22500 let mut cx = EditorTestContext::new(cx).await;
22501
22502 cx.set_state(
22503 &r#"ˇone
22504 two
22505
22506 three
22507 fourˇ
22508 five
22509
22510 siˇx"#
22511 .unindent(),
22512 );
22513
22514 cx.dispatch_action(HandleInput(String::new()));
22515 cx.assert_editor_state(
22516 &r#"ˇone
22517 two
22518
22519 three
22520 fourˇ
22521 five
22522
22523 siˇx"#
22524 .unindent(),
22525 );
22526
22527 cx.dispatch_action(HandleInput("AAAA".to_string()));
22528 cx.assert_editor_state(
22529 &r#"AAAAˇone
22530 two
22531
22532 three
22533 fourAAAAˇ
22534 five
22535
22536 siAAAAˇx"#
22537 .unindent(),
22538 );
22539}
22540
22541#[gpui::test]
22542async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22543 init_test(cx, |_| {});
22544
22545 let mut cx = EditorTestContext::new(cx).await;
22546 cx.set_state(
22547 r#"let foo = 1;
22548let foo = 2;
22549let foo = 3;
22550let fooˇ = 4;
22551let foo = 5;
22552let foo = 6;
22553let foo = 7;
22554let foo = 8;
22555let foo = 9;
22556let foo = 10;
22557let foo = 11;
22558let foo = 12;
22559let foo = 13;
22560let foo = 14;
22561let foo = 15;"#,
22562 );
22563
22564 cx.update_editor(|e, window, cx| {
22565 assert_eq!(
22566 e.next_scroll_position,
22567 NextScrollCursorCenterTopBottom::Center,
22568 "Default next scroll direction is center",
22569 );
22570
22571 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22572 assert_eq!(
22573 e.next_scroll_position,
22574 NextScrollCursorCenterTopBottom::Top,
22575 "After center, next scroll direction should be top",
22576 );
22577
22578 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22579 assert_eq!(
22580 e.next_scroll_position,
22581 NextScrollCursorCenterTopBottom::Bottom,
22582 "After top, next scroll direction should be bottom",
22583 );
22584
22585 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22586 assert_eq!(
22587 e.next_scroll_position,
22588 NextScrollCursorCenterTopBottom::Center,
22589 "After bottom, scrolling should start over",
22590 );
22591
22592 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22593 assert_eq!(
22594 e.next_scroll_position,
22595 NextScrollCursorCenterTopBottom::Top,
22596 "Scrolling continues if retriggered fast enough"
22597 );
22598 });
22599
22600 cx.executor()
22601 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22602 cx.executor().run_until_parked();
22603 cx.update_editor(|e, _, _| {
22604 assert_eq!(
22605 e.next_scroll_position,
22606 NextScrollCursorCenterTopBottom::Center,
22607 "If scrolling is not triggered fast enough, it should reset"
22608 );
22609 });
22610}
22611
22612#[gpui::test]
22613async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22614 init_test(cx, |_| {});
22615 let mut cx = EditorLspTestContext::new_rust(
22616 lsp::ServerCapabilities {
22617 definition_provider: Some(lsp::OneOf::Left(true)),
22618 references_provider: Some(lsp::OneOf::Left(true)),
22619 ..lsp::ServerCapabilities::default()
22620 },
22621 cx,
22622 )
22623 .await;
22624
22625 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22626 let go_to_definition = cx
22627 .lsp
22628 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22629 move |params, _| async move {
22630 if empty_go_to_definition {
22631 Ok(None)
22632 } else {
22633 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22634 uri: params.text_document_position_params.text_document.uri,
22635 range: lsp::Range::new(
22636 lsp::Position::new(4, 3),
22637 lsp::Position::new(4, 6),
22638 ),
22639 })))
22640 }
22641 },
22642 );
22643 let references = cx
22644 .lsp
22645 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22646 Ok(Some(vec![lsp::Location {
22647 uri: params.text_document_position.text_document.uri,
22648 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22649 }]))
22650 });
22651 (go_to_definition, references)
22652 };
22653
22654 cx.set_state(
22655 &r#"fn one() {
22656 let mut a = ˇtwo();
22657 }
22658
22659 fn two() {}"#
22660 .unindent(),
22661 );
22662 set_up_lsp_handlers(false, &mut cx);
22663 let navigated = cx
22664 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22665 .await
22666 .expect("Failed to navigate to definition");
22667 assert_eq!(
22668 navigated,
22669 Navigated::Yes,
22670 "Should have navigated to definition from the GetDefinition response"
22671 );
22672 cx.assert_editor_state(
22673 &r#"fn one() {
22674 let mut a = two();
22675 }
22676
22677 fn «twoˇ»() {}"#
22678 .unindent(),
22679 );
22680
22681 let editors = cx.update_workspace(|workspace, _, cx| {
22682 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22683 });
22684 cx.update_editor(|_, _, test_editor_cx| {
22685 assert_eq!(
22686 editors.len(),
22687 1,
22688 "Initially, only one, test, editor should be open in the workspace"
22689 );
22690 assert_eq!(
22691 test_editor_cx.entity(),
22692 editors.last().expect("Asserted len is 1").clone()
22693 );
22694 });
22695
22696 set_up_lsp_handlers(true, &mut cx);
22697 let navigated = cx
22698 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22699 .await
22700 .expect("Failed to navigate to lookup references");
22701 assert_eq!(
22702 navigated,
22703 Navigated::Yes,
22704 "Should have navigated to references as a fallback after empty GoToDefinition response"
22705 );
22706 // We should not change the selections in the existing file,
22707 // if opening another milti buffer with the references
22708 cx.assert_editor_state(
22709 &r#"fn one() {
22710 let mut a = two();
22711 }
22712
22713 fn «twoˇ»() {}"#
22714 .unindent(),
22715 );
22716 let editors = cx.update_workspace(|workspace, _, cx| {
22717 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22718 });
22719 cx.update_editor(|_, _, test_editor_cx| {
22720 assert_eq!(
22721 editors.len(),
22722 2,
22723 "After falling back to references search, we open a new editor with the results"
22724 );
22725 let references_fallback_text = editors
22726 .into_iter()
22727 .find(|new_editor| *new_editor != test_editor_cx.entity())
22728 .expect("Should have one non-test editor now")
22729 .read(test_editor_cx)
22730 .text(test_editor_cx);
22731 assert_eq!(
22732 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22733 "Should use the range from the references response and not the GoToDefinition one"
22734 );
22735 });
22736}
22737
22738#[gpui::test]
22739async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22740 init_test(cx, |_| {});
22741 cx.update(|cx| {
22742 let mut editor_settings = EditorSettings::get_global(cx).clone();
22743 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22744 EditorSettings::override_global(editor_settings, cx);
22745 });
22746 let mut cx = EditorLspTestContext::new_rust(
22747 lsp::ServerCapabilities {
22748 definition_provider: Some(lsp::OneOf::Left(true)),
22749 references_provider: Some(lsp::OneOf::Left(true)),
22750 ..lsp::ServerCapabilities::default()
22751 },
22752 cx,
22753 )
22754 .await;
22755 let original_state = r#"fn one() {
22756 let mut a = ˇtwo();
22757 }
22758
22759 fn two() {}"#
22760 .unindent();
22761 cx.set_state(&original_state);
22762
22763 let mut go_to_definition = cx
22764 .lsp
22765 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22766 move |_, _| async move { Ok(None) },
22767 );
22768 let _references = cx
22769 .lsp
22770 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22771 panic!("Should not call for references with no go to definition fallback")
22772 });
22773
22774 let navigated = cx
22775 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22776 .await
22777 .expect("Failed to navigate to lookup references");
22778 go_to_definition
22779 .next()
22780 .await
22781 .expect("Should have called the go_to_definition handler");
22782
22783 assert_eq!(
22784 navigated,
22785 Navigated::No,
22786 "Should have navigated to references as a fallback after empty GoToDefinition response"
22787 );
22788 cx.assert_editor_state(&original_state);
22789 let editors = cx.update_workspace(|workspace, _, cx| {
22790 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22791 });
22792 cx.update_editor(|_, _, _| {
22793 assert_eq!(
22794 editors.len(),
22795 1,
22796 "After unsuccessful fallback, no other editor should have been opened"
22797 );
22798 });
22799}
22800
22801#[gpui::test]
22802async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22803 init_test(cx, |_| {});
22804 let mut cx = EditorLspTestContext::new_rust(
22805 lsp::ServerCapabilities {
22806 references_provider: Some(lsp::OneOf::Left(true)),
22807 ..lsp::ServerCapabilities::default()
22808 },
22809 cx,
22810 )
22811 .await;
22812
22813 cx.set_state(
22814 &r#"
22815 fn one() {
22816 let mut a = two();
22817 }
22818
22819 fn ˇtwo() {}"#
22820 .unindent(),
22821 );
22822 cx.lsp
22823 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22824 Ok(Some(vec![
22825 lsp::Location {
22826 uri: params.text_document_position.text_document.uri.clone(),
22827 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22828 },
22829 lsp::Location {
22830 uri: params.text_document_position.text_document.uri,
22831 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22832 },
22833 ]))
22834 });
22835 let navigated = cx
22836 .update_editor(|editor, window, cx| {
22837 editor.find_all_references(&FindAllReferences::default(), window, cx)
22838 })
22839 .unwrap()
22840 .await
22841 .expect("Failed to navigate to references");
22842 assert_eq!(
22843 navigated,
22844 Navigated::Yes,
22845 "Should have navigated to references from the FindAllReferences response"
22846 );
22847 cx.assert_editor_state(
22848 &r#"fn one() {
22849 let mut a = two();
22850 }
22851
22852 fn ˇtwo() {}"#
22853 .unindent(),
22854 );
22855
22856 let editors = cx.update_workspace(|workspace, _, cx| {
22857 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22858 });
22859 cx.update_editor(|_, _, _| {
22860 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22861 });
22862
22863 cx.set_state(
22864 &r#"fn one() {
22865 let mut a = ˇtwo();
22866 }
22867
22868 fn two() {}"#
22869 .unindent(),
22870 );
22871 let navigated = cx
22872 .update_editor(|editor, window, cx| {
22873 editor.find_all_references(&FindAllReferences::default(), window, cx)
22874 })
22875 .unwrap()
22876 .await
22877 .expect("Failed to navigate to references");
22878 assert_eq!(
22879 navigated,
22880 Navigated::Yes,
22881 "Should have navigated to references from the FindAllReferences response"
22882 );
22883 cx.assert_editor_state(
22884 &r#"fn one() {
22885 let mut a = ˇtwo();
22886 }
22887
22888 fn two() {}"#
22889 .unindent(),
22890 );
22891 let editors = cx.update_workspace(|workspace, _, cx| {
22892 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22893 });
22894 cx.update_editor(|_, _, _| {
22895 assert_eq!(
22896 editors.len(),
22897 2,
22898 "should have re-used the previous multibuffer"
22899 );
22900 });
22901
22902 cx.set_state(
22903 &r#"fn one() {
22904 let mut a = ˇtwo();
22905 }
22906 fn three() {}
22907 fn two() {}"#
22908 .unindent(),
22909 );
22910 cx.lsp
22911 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22912 Ok(Some(vec![
22913 lsp::Location {
22914 uri: params.text_document_position.text_document.uri.clone(),
22915 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22916 },
22917 lsp::Location {
22918 uri: params.text_document_position.text_document.uri,
22919 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22920 },
22921 ]))
22922 });
22923 let navigated = cx
22924 .update_editor(|editor, window, cx| {
22925 editor.find_all_references(&FindAllReferences::default(), window, cx)
22926 })
22927 .unwrap()
22928 .await
22929 .expect("Failed to navigate to references");
22930 assert_eq!(
22931 navigated,
22932 Navigated::Yes,
22933 "Should have navigated to references from the FindAllReferences response"
22934 );
22935 cx.assert_editor_state(
22936 &r#"fn one() {
22937 let mut a = ˇtwo();
22938 }
22939 fn three() {}
22940 fn two() {}"#
22941 .unindent(),
22942 );
22943 let editors = cx.update_workspace(|workspace, _, cx| {
22944 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22945 });
22946 cx.update_editor(|_, _, _| {
22947 assert_eq!(
22948 editors.len(),
22949 3,
22950 "should have used a new multibuffer as offsets changed"
22951 );
22952 });
22953}
22954#[gpui::test]
22955async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22956 init_test(cx, |_| {});
22957
22958 let language = Arc::new(Language::new(
22959 LanguageConfig::default(),
22960 Some(tree_sitter_rust::LANGUAGE.into()),
22961 ));
22962
22963 let text = r#"
22964 #[cfg(test)]
22965 mod tests() {
22966 #[test]
22967 fn runnable_1() {
22968 let a = 1;
22969 }
22970
22971 #[test]
22972 fn runnable_2() {
22973 let a = 1;
22974 let b = 2;
22975 }
22976 }
22977 "#
22978 .unindent();
22979
22980 let fs = FakeFs::new(cx.executor());
22981 fs.insert_file("/file.rs", Default::default()).await;
22982
22983 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22984 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22985 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22986 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22987 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22988
22989 let editor = cx.new_window_entity(|window, cx| {
22990 Editor::new(
22991 EditorMode::full(),
22992 multi_buffer,
22993 Some(project.clone()),
22994 window,
22995 cx,
22996 )
22997 });
22998
22999 editor.update_in(cx, |editor, window, cx| {
23000 let snapshot = editor.buffer().read(cx).snapshot(cx);
23001 editor.tasks.insert(
23002 (buffer.read(cx).remote_id(), 3),
23003 RunnableTasks {
23004 templates: vec![],
23005 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23006 column: 0,
23007 extra_variables: HashMap::default(),
23008 context_range: BufferOffset(43)..BufferOffset(85),
23009 },
23010 );
23011 editor.tasks.insert(
23012 (buffer.read(cx).remote_id(), 8),
23013 RunnableTasks {
23014 templates: vec![],
23015 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23016 column: 0,
23017 extra_variables: HashMap::default(),
23018 context_range: BufferOffset(86)..BufferOffset(191),
23019 },
23020 );
23021
23022 // Test finding task when cursor is inside function body
23023 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23024 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23025 });
23026 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23027 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23028
23029 // Test finding task when cursor is on function name
23030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23031 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23032 });
23033 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23034 assert_eq!(row, 8, "Should find task when cursor is on function name");
23035 });
23036}
23037
23038#[gpui::test]
23039async fn test_folding_buffers(cx: &mut TestAppContext) {
23040 init_test(cx, |_| {});
23041
23042 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23043 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23044 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23045
23046 let fs = FakeFs::new(cx.executor());
23047 fs.insert_tree(
23048 path!("/a"),
23049 json!({
23050 "first.rs": sample_text_1,
23051 "second.rs": sample_text_2,
23052 "third.rs": sample_text_3,
23053 }),
23054 )
23055 .await;
23056 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23057 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23058 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23059 let worktree = project.update(cx, |project, cx| {
23060 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23061 assert_eq!(worktrees.len(), 1);
23062 worktrees.pop().unwrap()
23063 });
23064 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23065
23066 let buffer_1 = project
23067 .update(cx, |project, cx| {
23068 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23069 })
23070 .await
23071 .unwrap();
23072 let buffer_2 = project
23073 .update(cx, |project, cx| {
23074 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23075 })
23076 .await
23077 .unwrap();
23078 let buffer_3 = project
23079 .update(cx, |project, cx| {
23080 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23081 })
23082 .await
23083 .unwrap();
23084
23085 let multi_buffer = cx.new(|cx| {
23086 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23087 multi_buffer.push_excerpts(
23088 buffer_1.clone(),
23089 [
23090 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23091 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23092 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23093 ],
23094 cx,
23095 );
23096 multi_buffer.push_excerpts(
23097 buffer_2.clone(),
23098 [
23099 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23100 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23101 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23102 ],
23103 cx,
23104 );
23105 multi_buffer.push_excerpts(
23106 buffer_3.clone(),
23107 [
23108 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23109 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23110 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23111 ],
23112 cx,
23113 );
23114 multi_buffer
23115 });
23116 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23117 Editor::new(
23118 EditorMode::full(),
23119 multi_buffer.clone(),
23120 Some(project.clone()),
23121 window,
23122 cx,
23123 )
23124 });
23125
23126 assert_eq!(
23127 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23128 "\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",
23129 );
23130
23131 multi_buffer_editor.update(cx, |editor, cx| {
23132 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23133 });
23134 assert_eq!(
23135 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23136 "\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",
23137 "After folding the first buffer, its text should not be displayed"
23138 );
23139
23140 multi_buffer_editor.update(cx, |editor, cx| {
23141 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23142 });
23143 assert_eq!(
23144 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23145 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23146 "After folding the second buffer, its text should not be displayed"
23147 );
23148
23149 multi_buffer_editor.update(cx, |editor, cx| {
23150 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23151 });
23152 assert_eq!(
23153 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23154 "\n\n\n\n\n",
23155 "After folding the third buffer, its text should not be displayed"
23156 );
23157
23158 // Emulate selection inside the fold logic, that should work
23159 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23160 editor
23161 .snapshot(window, cx)
23162 .next_line_boundary(Point::new(0, 4));
23163 });
23164
23165 multi_buffer_editor.update(cx, |editor, cx| {
23166 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23167 });
23168 assert_eq!(
23169 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23170 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23171 "After unfolding the second buffer, its text should be displayed"
23172 );
23173
23174 // Typing inside of buffer 1 causes that buffer to be unfolded.
23175 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23176 assert_eq!(
23177 multi_buffer
23178 .read(cx)
23179 .snapshot(cx)
23180 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23181 .collect::<String>(),
23182 "bbbb"
23183 );
23184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23185 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23186 });
23187 editor.handle_input("B", window, cx);
23188 });
23189
23190 assert_eq!(
23191 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23192 "\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",
23193 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23194 );
23195
23196 multi_buffer_editor.update(cx, |editor, cx| {
23197 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23198 });
23199 assert_eq!(
23200 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23201 "\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",
23202 "After unfolding the all buffers, all original text should be displayed"
23203 );
23204}
23205
23206#[gpui::test]
23207async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23208 init_test(cx, |_| {});
23209
23210 let sample_text_1 = "1111\n2222\n3333".to_string();
23211 let sample_text_2 = "4444\n5555\n6666".to_string();
23212 let sample_text_3 = "7777\n8888\n9999".to_string();
23213
23214 let fs = FakeFs::new(cx.executor());
23215 fs.insert_tree(
23216 path!("/a"),
23217 json!({
23218 "first.rs": sample_text_1,
23219 "second.rs": sample_text_2,
23220 "third.rs": sample_text_3,
23221 }),
23222 )
23223 .await;
23224 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23225 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23226 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23227 let worktree = project.update(cx, |project, cx| {
23228 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23229 assert_eq!(worktrees.len(), 1);
23230 worktrees.pop().unwrap()
23231 });
23232 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23233
23234 let buffer_1 = project
23235 .update(cx, |project, cx| {
23236 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23237 })
23238 .await
23239 .unwrap();
23240 let buffer_2 = project
23241 .update(cx, |project, cx| {
23242 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23243 })
23244 .await
23245 .unwrap();
23246 let buffer_3 = project
23247 .update(cx, |project, cx| {
23248 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23249 })
23250 .await
23251 .unwrap();
23252
23253 let multi_buffer = cx.new(|cx| {
23254 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23255 multi_buffer.push_excerpts(
23256 buffer_1.clone(),
23257 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23258 cx,
23259 );
23260 multi_buffer.push_excerpts(
23261 buffer_2.clone(),
23262 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23263 cx,
23264 );
23265 multi_buffer.push_excerpts(
23266 buffer_3.clone(),
23267 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23268 cx,
23269 );
23270 multi_buffer
23271 });
23272
23273 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23274 Editor::new(
23275 EditorMode::full(),
23276 multi_buffer,
23277 Some(project.clone()),
23278 window,
23279 cx,
23280 )
23281 });
23282
23283 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23284 assert_eq!(
23285 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23286 full_text,
23287 );
23288
23289 multi_buffer_editor.update(cx, |editor, cx| {
23290 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23291 });
23292 assert_eq!(
23293 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23294 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23295 "After folding the first buffer, its text should not be displayed"
23296 );
23297
23298 multi_buffer_editor.update(cx, |editor, cx| {
23299 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23300 });
23301
23302 assert_eq!(
23303 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23304 "\n\n\n\n\n\n7777\n8888\n9999",
23305 "After folding the second buffer, its text should not be displayed"
23306 );
23307
23308 multi_buffer_editor.update(cx, |editor, cx| {
23309 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23310 });
23311 assert_eq!(
23312 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23313 "\n\n\n\n\n",
23314 "After folding the third buffer, its text should not be displayed"
23315 );
23316
23317 multi_buffer_editor.update(cx, |editor, cx| {
23318 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23319 });
23320 assert_eq!(
23321 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23322 "\n\n\n\n4444\n5555\n6666\n\n",
23323 "After unfolding the second buffer, its text should be displayed"
23324 );
23325
23326 multi_buffer_editor.update(cx, |editor, cx| {
23327 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23328 });
23329 assert_eq!(
23330 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23331 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23332 "After unfolding the first buffer, its text should be displayed"
23333 );
23334
23335 multi_buffer_editor.update(cx, |editor, cx| {
23336 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23337 });
23338 assert_eq!(
23339 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23340 full_text,
23341 "After unfolding all buffers, all original text should be displayed"
23342 );
23343}
23344
23345#[gpui::test]
23346async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23347 init_test(cx, |_| {});
23348
23349 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23350
23351 let fs = FakeFs::new(cx.executor());
23352 fs.insert_tree(
23353 path!("/a"),
23354 json!({
23355 "main.rs": sample_text,
23356 }),
23357 )
23358 .await;
23359 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23360 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23361 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23362 let worktree = project.update(cx, |project, cx| {
23363 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23364 assert_eq!(worktrees.len(), 1);
23365 worktrees.pop().unwrap()
23366 });
23367 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23368
23369 let buffer_1 = project
23370 .update(cx, |project, cx| {
23371 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23372 })
23373 .await
23374 .unwrap();
23375
23376 let multi_buffer = cx.new(|cx| {
23377 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23378 multi_buffer.push_excerpts(
23379 buffer_1.clone(),
23380 [ExcerptRange::new(
23381 Point::new(0, 0)
23382 ..Point::new(
23383 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23384 0,
23385 ),
23386 )],
23387 cx,
23388 );
23389 multi_buffer
23390 });
23391 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23392 Editor::new(
23393 EditorMode::full(),
23394 multi_buffer,
23395 Some(project.clone()),
23396 window,
23397 cx,
23398 )
23399 });
23400
23401 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23402 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23403 enum TestHighlight {}
23404 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23405 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23406 editor.highlight_text::<TestHighlight>(
23407 vec![highlight_range.clone()],
23408 HighlightStyle::color(Hsla::green()),
23409 cx,
23410 );
23411 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23412 s.select_ranges(Some(highlight_range))
23413 });
23414 });
23415
23416 let full_text = format!("\n\n{sample_text}");
23417 assert_eq!(
23418 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23419 full_text,
23420 );
23421}
23422
23423#[gpui::test]
23424async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23425 init_test(cx, |_| {});
23426 cx.update(|cx| {
23427 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23428 "keymaps/default-linux.json",
23429 cx,
23430 )
23431 .unwrap();
23432 cx.bind_keys(default_key_bindings);
23433 });
23434
23435 let (editor, cx) = cx.add_window_view(|window, cx| {
23436 let multi_buffer = MultiBuffer::build_multi(
23437 [
23438 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23439 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23440 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23441 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23442 ],
23443 cx,
23444 );
23445 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23446
23447 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23448 // fold all but the second buffer, so that we test navigating between two
23449 // adjacent folded buffers, as well as folded buffers at the start and
23450 // end the multibuffer
23451 editor.fold_buffer(buffer_ids[0], cx);
23452 editor.fold_buffer(buffer_ids[2], cx);
23453 editor.fold_buffer(buffer_ids[3], cx);
23454
23455 editor
23456 });
23457 cx.simulate_resize(size(px(1000.), px(1000.)));
23458
23459 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23460 cx.assert_excerpts_with_selections(indoc! {"
23461 [EXCERPT]
23462 ˇ[FOLDED]
23463 [EXCERPT]
23464 a1
23465 b1
23466 [EXCERPT]
23467 [FOLDED]
23468 [EXCERPT]
23469 [FOLDED]
23470 "
23471 });
23472 cx.simulate_keystroke("down");
23473 cx.assert_excerpts_with_selections(indoc! {"
23474 [EXCERPT]
23475 [FOLDED]
23476 [EXCERPT]
23477 ˇa1
23478 b1
23479 [EXCERPT]
23480 [FOLDED]
23481 [EXCERPT]
23482 [FOLDED]
23483 "
23484 });
23485 cx.simulate_keystroke("down");
23486 cx.assert_excerpts_with_selections(indoc! {"
23487 [EXCERPT]
23488 [FOLDED]
23489 [EXCERPT]
23490 a1
23491 ˇb1
23492 [EXCERPT]
23493 [FOLDED]
23494 [EXCERPT]
23495 [FOLDED]
23496 "
23497 });
23498 cx.simulate_keystroke("down");
23499 cx.assert_excerpts_with_selections(indoc! {"
23500 [EXCERPT]
23501 [FOLDED]
23502 [EXCERPT]
23503 a1
23504 b1
23505 ˇ[EXCERPT]
23506 [FOLDED]
23507 [EXCERPT]
23508 [FOLDED]
23509 "
23510 });
23511 cx.simulate_keystroke("down");
23512 cx.assert_excerpts_with_selections(indoc! {"
23513 [EXCERPT]
23514 [FOLDED]
23515 [EXCERPT]
23516 a1
23517 b1
23518 [EXCERPT]
23519 ˇ[FOLDED]
23520 [EXCERPT]
23521 [FOLDED]
23522 "
23523 });
23524 for _ in 0..5 {
23525 cx.simulate_keystroke("down");
23526 cx.assert_excerpts_with_selections(indoc! {"
23527 [EXCERPT]
23528 [FOLDED]
23529 [EXCERPT]
23530 a1
23531 b1
23532 [EXCERPT]
23533 [FOLDED]
23534 [EXCERPT]
23535 ˇ[FOLDED]
23536 "
23537 });
23538 }
23539
23540 cx.simulate_keystroke("up");
23541 cx.assert_excerpts_with_selections(indoc! {"
23542 [EXCERPT]
23543 [FOLDED]
23544 [EXCERPT]
23545 a1
23546 b1
23547 [EXCERPT]
23548 ˇ[FOLDED]
23549 [EXCERPT]
23550 [FOLDED]
23551 "
23552 });
23553 cx.simulate_keystroke("up");
23554 cx.assert_excerpts_with_selections(indoc! {"
23555 [EXCERPT]
23556 [FOLDED]
23557 [EXCERPT]
23558 a1
23559 b1
23560 ˇ[EXCERPT]
23561 [FOLDED]
23562 [EXCERPT]
23563 [FOLDED]
23564 "
23565 });
23566 cx.simulate_keystroke("up");
23567 cx.assert_excerpts_with_selections(indoc! {"
23568 [EXCERPT]
23569 [FOLDED]
23570 [EXCERPT]
23571 a1
23572 ˇb1
23573 [EXCERPT]
23574 [FOLDED]
23575 [EXCERPT]
23576 [FOLDED]
23577 "
23578 });
23579 cx.simulate_keystroke("up");
23580 cx.assert_excerpts_with_selections(indoc! {"
23581 [EXCERPT]
23582 [FOLDED]
23583 [EXCERPT]
23584 ˇa1
23585 b1
23586 [EXCERPT]
23587 [FOLDED]
23588 [EXCERPT]
23589 [FOLDED]
23590 "
23591 });
23592 for _ in 0..5 {
23593 cx.simulate_keystroke("up");
23594 cx.assert_excerpts_with_selections(indoc! {"
23595 [EXCERPT]
23596 ˇ[FOLDED]
23597 [EXCERPT]
23598 a1
23599 b1
23600 [EXCERPT]
23601 [FOLDED]
23602 [EXCERPT]
23603 [FOLDED]
23604 "
23605 });
23606 }
23607}
23608
23609#[gpui::test]
23610async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23611 init_test(cx, |_| {});
23612
23613 // Simple insertion
23614 assert_highlighted_edits(
23615 "Hello, world!",
23616 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23617 true,
23618 cx,
23619 |highlighted_edits, cx| {
23620 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23621 assert_eq!(highlighted_edits.highlights.len(), 1);
23622 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23623 assert_eq!(
23624 highlighted_edits.highlights[0].1.background_color,
23625 Some(cx.theme().status().created_background)
23626 );
23627 },
23628 )
23629 .await;
23630
23631 // Replacement
23632 assert_highlighted_edits(
23633 "This is a test.",
23634 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23635 false,
23636 cx,
23637 |highlighted_edits, cx| {
23638 assert_eq!(highlighted_edits.text, "That is a test.");
23639 assert_eq!(highlighted_edits.highlights.len(), 1);
23640 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23641 assert_eq!(
23642 highlighted_edits.highlights[0].1.background_color,
23643 Some(cx.theme().status().created_background)
23644 );
23645 },
23646 )
23647 .await;
23648
23649 // Multiple edits
23650 assert_highlighted_edits(
23651 "Hello, world!",
23652 vec![
23653 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23654 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23655 ],
23656 false,
23657 cx,
23658 |highlighted_edits, cx| {
23659 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23660 assert_eq!(highlighted_edits.highlights.len(), 2);
23661 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23662 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23663 assert_eq!(
23664 highlighted_edits.highlights[0].1.background_color,
23665 Some(cx.theme().status().created_background)
23666 );
23667 assert_eq!(
23668 highlighted_edits.highlights[1].1.background_color,
23669 Some(cx.theme().status().created_background)
23670 );
23671 },
23672 )
23673 .await;
23674
23675 // Multiple lines with edits
23676 assert_highlighted_edits(
23677 "First line\nSecond line\nThird line\nFourth line",
23678 vec![
23679 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23680 (
23681 Point::new(2, 0)..Point::new(2, 10),
23682 "New third line".to_string(),
23683 ),
23684 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23685 ],
23686 false,
23687 cx,
23688 |highlighted_edits, cx| {
23689 assert_eq!(
23690 highlighted_edits.text,
23691 "Second modified\nNew third line\nFourth updated line"
23692 );
23693 assert_eq!(highlighted_edits.highlights.len(), 3);
23694 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23695 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23696 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23697 for highlight in &highlighted_edits.highlights {
23698 assert_eq!(
23699 highlight.1.background_color,
23700 Some(cx.theme().status().created_background)
23701 );
23702 }
23703 },
23704 )
23705 .await;
23706}
23707
23708#[gpui::test]
23709async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23710 init_test(cx, |_| {});
23711
23712 // Deletion
23713 assert_highlighted_edits(
23714 "Hello, world!",
23715 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23716 true,
23717 cx,
23718 |highlighted_edits, cx| {
23719 assert_eq!(highlighted_edits.text, "Hello, world!");
23720 assert_eq!(highlighted_edits.highlights.len(), 1);
23721 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23722 assert_eq!(
23723 highlighted_edits.highlights[0].1.background_color,
23724 Some(cx.theme().status().deleted_background)
23725 );
23726 },
23727 )
23728 .await;
23729
23730 // Insertion
23731 assert_highlighted_edits(
23732 "Hello, world!",
23733 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23734 true,
23735 cx,
23736 |highlighted_edits, cx| {
23737 assert_eq!(highlighted_edits.highlights.len(), 1);
23738 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23739 assert_eq!(
23740 highlighted_edits.highlights[0].1.background_color,
23741 Some(cx.theme().status().created_background)
23742 );
23743 },
23744 )
23745 .await;
23746}
23747
23748async fn assert_highlighted_edits(
23749 text: &str,
23750 edits: Vec<(Range<Point>, String)>,
23751 include_deletions: bool,
23752 cx: &mut TestAppContext,
23753 assertion_fn: impl Fn(HighlightedText, &App),
23754) {
23755 let window = cx.add_window(|window, cx| {
23756 let buffer = MultiBuffer::build_simple(text, cx);
23757 Editor::new(EditorMode::full(), buffer, None, window, cx)
23758 });
23759 let cx = &mut VisualTestContext::from_window(*window, cx);
23760
23761 let (buffer, snapshot) = window
23762 .update(cx, |editor, _window, cx| {
23763 (
23764 editor.buffer().clone(),
23765 editor.buffer().read(cx).snapshot(cx),
23766 )
23767 })
23768 .unwrap();
23769
23770 let edits = edits
23771 .into_iter()
23772 .map(|(range, edit)| {
23773 (
23774 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23775 edit,
23776 )
23777 })
23778 .collect::<Vec<_>>();
23779
23780 let text_anchor_edits = edits
23781 .clone()
23782 .into_iter()
23783 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23784 .collect::<Vec<_>>();
23785
23786 let edit_preview = window
23787 .update(cx, |_, _window, cx| {
23788 buffer
23789 .read(cx)
23790 .as_singleton()
23791 .unwrap()
23792 .read(cx)
23793 .preview_edits(text_anchor_edits.into(), cx)
23794 })
23795 .unwrap()
23796 .await;
23797
23798 cx.update(|_window, cx| {
23799 let highlighted_edits = edit_prediction_edit_text(
23800 snapshot.as_singleton().unwrap().2,
23801 &edits,
23802 &edit_preview,
23803 include_deletions,
23804 cx,
23805 );
23806 assertion_fn(highlighted_edits, cx)
23807 });
23808}
23809
23810#[track_caller]
23811fn assert_breakpoint(
23812 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23813 path: &Arc<Path>,
23814 expected: Vec<(u32, Breakpoint)>,
23815) {
23816 if expected.is_empty() {
23817 assert!(!breakpoints.contains_key(path), "{}", path.display());
23818 } else {
23819 let mut breakpoint = breakpoints
23820 .get(path)
23821 .unwrap()
23822 .iter()
23823 .map(|breakpoint| {
23824 (
23825 breakpoint.row,
23826 Breakpoint {
23827 message: breakpoint.message.clone(),
23828 state: breakpoint.state,
23829 condition: breakpoint.condition.clone(),
23830 hit_condition: breakpoint.hit_condition.clone(),
23831 },
23832 )
23833 })
23834 .collect::<Vec<_>>();
23835
23836 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23837
23838 assert_eq!(expected, breakpoint);
23839 }
23840}
23841
23842fn add_log_breakpoint_at_cursor(
23843 editor: &mut Editor,
23844 log_message: &str,
23845 window: &mut Window,
23846 cx: &mut Context<Editor>,
23847) {
23848 let (anchor, bp) = editor
23849 .breakpoints_at_cursors(window, cx)
23850 .first()
23851 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23852 .unwrap_or_else(|| {
23853 let snapshot = editor.snapshot(window, cx);
23854 let cursor_position: Point =
23855 editor.selections.newest(&snapshot.display_snapshot).head();
23856
23857 let breakpoint_position = snapshot
23858 .buffer_snapshot()
23859 .anchor_before(Point::new(cursor_position.row, 0));
23860
23861 (breakpoint_position, Breakpoint::new_log(log_message))
23862 });
23863
23864 editor.edit_breakpoint_at_anchor(
23865 anchor,
23866 bp,
23867 BreakpointEditAction::EditLogMessage(log_message.into()),
23868 cx,
23869 );
23870}
23871
23872#[gpui::test]
23873async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23874 init_test(cx, |_| {});
23875
23876 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23877 let fs = FakeFs::new(cx.executor());
23878 fs.insert_tree(
23879 path!("/a"),
23880 json!({
23881 "main.rs": sample_text,
23882 }),
23883 )
23884 .await;
23885 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23886 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23887 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23888
23889 let fs = FakeFs::new(cx.executor());
23890 fs.insert_tree(
23891 path!("/a"),
23892 json!({
23893 "main.rs": sample_text,
23894 }),
23895 )
23896 .await;
23897 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23898 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23899 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23900 let worktree_id = workspace
23901 .update(cx, |workspace, _window, cx| {
23902 workspace.project().update(cx, |project, cx| {
23903 project.worktrees(cx).next().unwrap().read(cx).id()
23904 })
23905 })
23906 .unwrap();
23907
23908 let buffer = project
23909 .update(cx, |project, cx| {
23910 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23911 })
23912 .await
23913 .unwrap();
23914
23915 let (editor, cx) = cx.add_window_view(|window, cx| {
23916 Editor::new(
23917 EditorMode::full(),
23918 MultiBuffer::build_from_buffer(buffer, cx),
23919 Some(project.clone()),
23920 window,
23921 cx,
23922 )
23923 });
23924
23925 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23926 let abs_path = project.read_with(cx, |project, cx| {
23927 project
23928 .absolute_path(&project_path, cx)
23929 .map(Arc::from)
23930 .unwrap()
23931 });
23932
23933 // assert we can add breakpoint on the first line
23934 editor.update_in(cx, |editor, window, cx| {
23935 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23936 editor.move_to_end(&MoveToEnd, window, cx);
23937 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23938 });
23939
23940 let breakpoints = editor.update(cx, |editor, cx| {
23941 editor
23942 .breakpoint_store()
23943 .as_ref()
23944 .unwrap()
23945 .read(cx)
23946 .all_source_breakpoints(cx)
23947 });
23948
23949 assert_eq!(1, breakpoints.len());
23950 assert_breakpoint(
23951 &breakpoints,
23952 &abs_path,
23953 vec![
23954 (0, Breakpoint::new_standard()),
23955 (3, Breakpoint::new_standard()),
23956 ],
23957 );
23958
23959 editor.update_in(cx, |editor, window, cx| {
23960 editor.move_to_beginning(&MoveToBeginning, window, cx);
23961 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23962 });
23963
23964 let breakpoints = editor.update(cx, |editor, cx| {
23965 editor
23966 .breakpoint_store()
23967 .as_ref()
23968 .unwrap()
23969 .read(cx)
23970 .all_source_breakpoints(cx)
23971 });
23972
23973 assert_eq!(1, breakpoints.len());
23974 assert_breakpoint(
23975 &breakpoints,
23976 &abs_path,
23977 vec![(3, Breakpoint::new_standard())],
23978 );
23979
23980 editor.update_in(cx, |editor, window, cx| {
23981 editor.move_to_end(&MoveToEnd, window, cx);
23982 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23983 });
23984
23985 let breakpoints = editor.update(cx, |editor, cx| {
23986 editor
23987 .breakpoint_store()
23988 .as_ref()
23989 .unwrap()
23990 .read(cx)
23991 .all_source_breakpoints(cx)
23992 });
23993
23994 assert_eq!(0, breakpoints.len());
23995 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23996}
23997
23998#[gpui::test]
23999async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24000 init_test(cx, |_| {});
24001
24002 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24003
24004 let fs = FakeFs::new(cx.executor());
24005 fs.insert_tree(
24006 path!("/a"),
24007 json!({
24008 "main.rs": sample_text,
24009 }),
24010 )
24011 .await;
24012 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24013 let (workspace, cx) =
24014 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24015
24016 let worktree_id = workspace.update(cx, |workspace, cx| {
24017 workspace.project().update(cx, |project, cx| {
24018 project.worktrees(cx).next().unwrap().read(cx).id()
24019 })
24020 });
24021
24022 let buffer = project
24023 .update(cx, |project, cx| {
24024 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24025 })
24026 .await
24027 .unwrap();
24028
24029 let (editor, cx) = cx.add_window_view(|window, cx| {
24030 Editor::new(
24031 EditorMode::full(),
24032 MultiBuffer::build_from_buffer(buffer, cx),
24033 Some(project.clone()),
24034 window,
24035 cx,
24036 )
24037 });
24038
24039 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24040 let abs_path = project.read_with(cx, |project, cx| {
24041 project
24042 .absolute_path(&project_path, cx)
24043 .map(Arc::from)
24044 .unwrap()
24045 });
24046
24047 editor.update_in(cx, |editor, window, cx| {
24048 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24049 });
24050
24051 let breakpoints = editor.update(cx, |editor, cx| {
24052 editor
24053 .breakpoint_store()
24054 .as_ref()
24055 .unwrap()
24056 .read(cx)
24057 .all_source_breakpoints(cx)
24058 });
24059
24060 assert_breakpoint(
24061 &breakpoints,
24062 &abs_path,
24063 vec![(0, Breakpoint::new_log("hello world"))],
24064 );
24065
24066 // Removing a log message from a log breakpoint should remove it
24067 editor.update_in(cx, |editor, window, cx| {
24068 add_log_breakpoint_at_cursor(editor, "", window, cx);
24069 });
24070
24071 let breakpoints = editor.update(cx, |editor, cx| {
24072 editor
24073 .breakpoint_store()
24074 .as_ref()
24075 .unwrap()
24076 .read(cx)
24077 .all_source_breakpoints(cx)
24078 });
24079
24080 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24081
24082 editor.update_in(cx, |editor, window, cx| {
24083 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24084 editor.move_to_end(&MoveToEnd, window, cx);
24085 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24086 // Not adding a log message to a standard breakpoint shouldn't remove it
24087 add_log_breakpoint_at_cursor(editor, "", window, cx);
24088 });
24089
24090 let breakpoints = editor.update(cx, |editor, cx| {
24091 editor
24092 .breakpoint_store()
24093 .as_ref()
24094 .unwrap()
24095 .read(cx)
24096 .all_source_breakpoints(cx)
24097 });
24098
24099 assert_breakpoint(
24100 &breakpoints,
24101 &abs_path,
24102 vec![
24103 (0, Breakpoint::new_standard()),
24104 (3, Breakpoint::new_standard()),
24105 ],
24106 );
24107
24108 editor.update_in(cx, |editor, window, cx| {
24109 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24110 });
24111
24112 let breakpoints = editor.update(cx, |editor, cx| {
24113 editor
24114 .breakpoint_store()
24115 .as_ref()
24116 .unwrap()
24117 .read(cx)
24118 .all_source_breakpoints(cx)
24119 });
24120
24121 assert_breakpoint(
24122 &breakpoints,
24123 &abs_path,
24124 vec![
24125 (0, Breakpoint::new_standard()),
24126 (3, Breakpoint::new_log("hello world")),
24127 ],
24128 );
24129
24130 editor.update_in(cx, |editor, window, cx| {
24131 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24132 });
24133
24134 let breakpoints = editor.update(cx, |editor, cx| {
24135 editor
24136 .breakpoint_store()
24137 .as_ref()
24138 .unwrap()
24139 .read(cx)
24140 .all_source_breakpoints(cx)
24141 });
24142
24143 assert_breakpoint(
24144 &breakpoints,
24145 &abs_path,
24146 vec![
24147 (0, Breakpoint::new_standard()),
24148 (3, Breakpoint::new_log("hello Earth!!")),
24149 ],
24150 );
24151}
24152
24153/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24154/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24155/// or when breakpoints were placed out of order. This tests for a regression too
24156#[gpui::test]
24157async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24158 init_test(cx, |_| {});
24159
24160 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24161 let fs = FakeFs::new(cx.executor());
24162 fs.insert_tree(
24163 path!("/a"),
24164 json!({
24165 "main.rs": sample_text,
24166 }),
24167 )
24168 .await;
24169 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24170 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24171 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24172
24173 let fs = FakeFs::new(cx.executor());
24174 fs.insert_tree(
24175 path!("/a"),
24176 json!({
24177 "main.rs": sample_text,
24178 }),
24179 )
24180 .await;
24181 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24182 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24183 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24184 let worktree_id = workspace
24185 .update(cx, |workspace, _window, cx| {
24186 workspace.project().update(cx, |project, cx| {
24187 project.worktrees(cx).next().unwrap().read(cx).id()
24188 })
24189 })
24190 .unwrap();
24191
24192 let buffer = project
24193 .update(cx, |project, cx| {
24194 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24195 })
24196 .await
24197 .unwrap();
24198
24199 let (editor, cx) = cx.add_window_view(|window, cx| {
24200 Editor::new(
24201 EditorMode::full(),
24202 MultiBuffer::build_from_buffer(buffer, cx),
24203 Some(project.clone()),
24204 window,
24205 cx,
24206 )
24207 });
24208
24209 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24210 let abs_path = project.read_with(cx, |project, cx| {
24211 project
24212 .absolute_path(&project_path, cx)
24213 .map(Arc::from)
24214 .unwrap()
24215 });
24216
24217 // assert we can add breakpoint on the first line
24218 editor.update_in(cx, |editor, window, cx| {
24219 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24220 editor.move_to_end(&MoveToEnd, window, cx);
24221 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24222 editor.move_up(&MoveUp, window, cx);
24223 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24224 });
24225
24226 let breakpoints = editor.update(cx, |editor, cx| {
24227 editor
24228 .breakpoint_store()
24229 .as_ref()
24230 .unwrap()
24231 .read(cx)
24232 .all_source_breakpoints(cx)
24233 });
24234
24235 assert_eq!(1, breakpoints.len());
24236 assert_breakpoint(
24237 &breakpoints,
24238 &abs_path,
24239 vec![
24240 (0, Breakpoint::new_standard()),
24241 (2, Breakpoint::new_standard()),
24242 (3, Breakpoint::new_standard()),
24243 ],
24244 );
24245
24246 editor.update_in(cx, |editor, window, cx| {
24247 editor.move_to_beginning(&MoveToBeginning, window, cx);
24248 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24249 editor.move_to_end(&MoveToEnd, window, cx);
24250 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24251 // Disabling a breakpoint that doesn't exist should do nothing
24252 editor.move_up(&MoveUp, window, cx);
24253 editor.move_up(&MoveUp, window, cx);
24254 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24255 });
24256
24257 let breakpoints = editor.update(cx, |editor, cx| {
24258 editor
24259 .breakpoint_store()
24260 .as_ref()
24261 .unwrap()
24262 .read(cx)
24263 .all_source_breakpoints(cx)
24264 });
24265
24266 let disable_breakpoint = {
24267 let mut bp = Breakpoint::new_standard();
24268 bp.state = BreakpointState::Disabled;
24269 bp
24270 };
24271
24272 assert_eq!(1, breakpoints.len());
24273 assert_breakpoint(
24274 &breakpoints,
24275 &abs_path,
24276 vec![
24277 (0, disable_breakpoint.clone()),
24278 (2, Breakpoint::new_standard()),
24279 (3, disable_breakpoint.clone()),
24280 ],
24281 );
24282
24283 editor.update_in(cx, |editor, window, cx| {
24284 editor.move_to_beginning(&MoveToBeginning, window, cx);
24285 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24286 editor.move_to_end(&MoveToEnd, window, cx);
24287 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24288 editor.move_up(&MoveUp, window, cx);
24289 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24290 });
24291
24292 let breakpoints = editor.update(cx, |editor, cx| {
24293 editor
24294 .breakpoint_store()
24295 .as_ref()
24296 .unwrap()
24297 .read(cx)
24298 .all_source_breakpoints(cx)
24299 });
24300
24301 assert_eq!(1, breakpoints.len());
24302 assert_breakpoint(
24303 &breakpoints,
24304 &abs_path,
24305 vec![
24306 (0, Breakpoint::new_standard()),
24307 (2, disable_breakpoint),
24308 (3, Breakpoint::new_standard()),
24309 ],
24310 );
24311}
24312
24313#[gpui::test]
24314async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24315 init_test(cx, |_| {});
24316 let capabilities = lsp::ServerCapabilities {
24317 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24318 prepare_provider: Some(true),
24319 work_done_progress_options: Default::default(),
24320 })),
24321 ..Default::default()
24322 };
24323 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24324
24325 cx.set_state(indoc! {"
24326 struct Fˇoo {}
24327 "});
24328
24329 cx.update_editor(|editor, _, cx| {
24330 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24331 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24332 editor.highlight_background::<DocumentHighlightRead>(
24333 &[highlight_range],
24334 |_, theme| theme.colors().editor_document_highlight_read_background,
24335 cx,
24336 );
24337 });
24338
24339 let mut prepare_rename_handler = cx
24340 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24341 move |_, _, _| async move {
24342 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24343 start: lsp::Position {
24344 line: 0,
24345 character: 7,
24346 },
24347 end: lsp::Position {
24348 line: 0,
24349 character: 10,
24350 },
24351 })))
24352 },
24353 );
24354 let prepare_rename_task = cx
24355 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24356 .expect("Prepare rename was not started");
24357 prepare_rename_handler.next().await.unwrap();
24358 prepare_rename_task.await.expect("Prepare rename failed");
24359
24360 let mut rename_handler =
24361 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24362 let edit = lsp::TextEdit {
24363 range: lsp::Range {
24364 start: lsp::Position {
24365 line: 0,
24366 character: 7,
24367 },
24368 end: lsp::Position {
24369 line: 0,
24370 character: 10,
24371 },
24372 },
24373 new_text: "FooRenamed".to_string(),
24374 };
24375 Ok(Some(lsp::WorkspaceEdit::new(
24376 // Specify the same edit twice
24377 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24378 )))
24379 });
24380 let rename_task = cx
24381 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24382 .expect("Confirm rename was not started");
24383 rename_handler.next().await.unwrap();
24384 rename_task.await.expect("Confirm rename failed");
24385 cx.run_until_parked();
24386
24387 // Despite two edits, only one is actually applied as those are identical
24388 cx.assert_editor_state(indoc! {"
24389 struct FooRenamedˇ {}
24390 "});
24391}
24392
24393#[gpui::test]
24394async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24395 init_test(cx, |_| {});
24396 // These capabilities indicate that the server does not support prepare rename.
24397 let capabilities = lsp::ServerCapabilities {
24398 rename_provider: Some(lsp::OneOf::Left(true)),
24399 ..Default::default()
24400 };
24401 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24402
24403 cx.set_state(indoc! {"
24404 struct Fˇoo {}
24405 "});
24406
24407 cx.update_editor(|editor, _window, cx| {
24408 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24409 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24410 editor.highlight_background::<DocumentHighlightRead>(
24411 &[highlight_range],
24412 |_, theme| theme.colors().editor_document_highlight_read_background,
24413 cx,
24414 );
24415 });
24416
24417 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24418 .expect("Prepare rename was not started")
24419 .await
24420 .expect("Prepare rename failed");
24421
24422 let mut rename_handler =
24423 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24424 let edit = lsp::TextEdit {
24425 range: lsp::Range {
24426 start: lsp::Position {
24427 line: 0,
24428 character: 7,
24429 },
24430 end: lsp::Position {
24431 line: 0,
24432 character: 10,
24433 },
24434 },
24435 new_text: "FooRenamed".to_string(),
24436 };
24437 Ok(Some(lsp::WorkspaceEdit::new(
24438 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24439 )))
24440 });
24441 let rename_task = cx
24442 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24443 .expect("Confirm rename was not started");
24444 rename_handler.next().await.unwrap();
24445 rename_task.await.expect("Confirm rename failed");
24446 cx.run_until_parked();
24447
24448 // Correct range is renamed, as `surrounding_word` is used to find it.
24449 cx.assert_editor_state(indoc! {"
24450 struct FooRenamedˇ {}
24451 "});
24452}
24453
24454#[gpui::test]
24455async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24456 init_test(cx, |_| {});
24457 let mut cx = EditorTestContext::new(cx).await;
24458
24459 let language = Arc::new(
24460 Language::new(
24461 LanguageConfig::default(),
24462 Some(tree_sitter_html::LANGUAGE.into()),
24463 )
24464 .with_brackets_query(
24465 r#"
24466 ("<" @open "/>" @close)
24467 ("</" @open ">" @close)
24468 ("<" @open ">" @close)
24469 ("\"" @open "\"" @close)
24470 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24471 "#,
24472 )
24473 .unwrap(),
24474 );
24475 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24476
24477 cx.set_state(indoc! {"
24478 <span>ˇ</span>
24479 "});
24480 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24481 cx.assert_editor_state(indoc! {"
24482 <span>
24483 ˇ
24484 </span>
24485 "});
24486
24487 cx.set_state(indoc! {"
24488 <span><span></span>ˇ</span>
24489 "});
24490 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24491 cx.assert_editor_state(indoc! {"
24492 <span><span></span>
24493 ˇ</span>
24494 "});
24495
24496 cx.set_state(indoc! {"
24497 <span>ˇ
24498 </span>
24499 "});
24500 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24501 cx.assert_editor_state(indoc! {"
24502 <span>
24503 ˇ
24504 </span>
24505 "});
24506}
24507
24508#[gpui::test(iterations = 10)]
24509async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24510 init_test(cx, |_| {});
24511
24512 let fs = FakeFs::new(cx.executor());
24513 fs.insert_tree(
24514 path!("/dir"),
24515 json!({
24516 "a.ts": "a",
24517 }),
24518 )
24519 .await;
24520
24521 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24522 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24523 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24524
24525 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24526 language_registry.add(Arc::new(Language::new(
24527 LanguageConfig {
24528 name: "TypeScript".into(),
24529 matcher: LanguageMatcher {
24530 path_suffixes: vec!["ts".to_string()],
24531 ..Default::default()
24532 },
24533 ..Default::default()
24534 },
24535 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24536 )));
24537 let mut fake_language_servers = language_registry.register_fake_lsp(
24538 "TypeScript",
24539 FakeLspAdapter {
24540 capabilities: lsp::ServerCapabilities {
24541 code_lens_provider: Some(lsp::CodeLensOptions {
24542 resolve_provider: Some(true),
24543 }),
24544 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24545 commands: vec!["_the/command".to_string()],
24546 ..lsp::ExecuteCommandOptions::default()
24547 }),
24548 ..lsp::ServerCapabilities::default()
24549 },
24550 ..FakeLspAdapter::default()
24551 },
24552 );
24553
24554 let editor = workspace
24555 .update(cx, |workspace, window, cx| {
24556 workspace.open_abs_path(
24557 PathBuf::from(path!("/dir/a.ts")),
24558 OpenOptions::default(),
24559 window,
24560 cx,
24561 )
24562 })
24563 .unwrap()
24564 .await
24565 .unwrap()
24566 .downcast::<Editor>()
24567 .unwrap();
24568 cx.executor().run_until_parked();
24569
24570 let fake_server = fake_language_servers.next().await.unwrap();
24571
24572 let buffer = editor.update(cx, |editor, cx| {
24573 editor
24574 .buffer()
24575 .read(cx)
24576 .as_singleton()
24577 .expect("have opened a single file by path")
24578 });
24579
24580 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24581 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24582 drop(buffer_snapshot);
24583 let actions = cx
24584 .update_window(*workspace, |_, window, cx| {
24585 project.code_actions(&buffer, anchor..anchor, window, cx)
24586 })
24587 .unwrap();
24588
24589 fake_server
24590 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24591 Ok(Some(vec![
24592 lsp::CodeLens {
24593 range: lsp::Range::default(),
24594 command: Some(lsp::Command {
24595 title: "Code lens command".to_owned(),
24596 command: "_the/command".to_owned(),
24597 arguments: None,
24598 }),
24599 data: None,
24600 },
24601 lsp::CodeLens {
24602 range: lsp::Range::default(),
24603 command: Some(lsp::Command {
24604 title: "Command not in capabilities".to_owned(),
24605 command: "not in capabilities".to_owned(),
24606 arguments: None,
24607 }),
24608 data: None,
24609 },
24610 lsp::CodeLens {
24611 range: lsp::Range {
24612 start: lsp::Position {
24613 line: 1,
24614 character: 1,
24615 },
24616 end: lsp::Position {
24617 line: 1,
24618 character: 1,
24619 },
24620 },
24621 command: Some(lsp::Command {
24622 title: "Command not in range".to_owned(),
24623 command: "_the/command".to_owned(),
24624 arguments: None,
24625 }),
24626 data: None,
24627 },
24628 ]))
24629 })
24630 .next()
24631 .await;
24632
24633 let actions = actions.await.unwrap();
24634 assert_eq!(
24635 actions.len(),
24636 1,
24637 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24638 );
24639 let action = actions[0].clone();
24640 let apply = project.update(cx, |project, cx| {
24641 project.apply_code_action(buffer.clone(), action, true, cx)
24642 });
24643
24644 // Resolving the code action does not populate its edits. In absence of
24645 // edits, we must execute the given command.
24646 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24647 |mut lens, _| async move {
24648 let lens_command = lens.command.as_mut().expect("should have a command");
24649 assert_eq!(lens_command.title, "Code lens command");
24650 lens_command.arguments = Some(vec![json!("the-argument")]);
24651 Ok(lens)
24652 },
24653 );
24654
24655 // While executing the command, the language server sends the editor
24656 // a `workspaceEdit` request.
24657 fake_server
24658 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24659 let fake = fake_server.clone();
24660 move |params, _| {
24661 assert_eq!(params.command, "_the/command");
24662 let fake = fake.clone();
24663 async move {
24664 fake.server
24665 .request::<lsp::request::ApplyWorkspaceEdit>(
24666 lsp::ApplyWorkspaceEditParams {
24667 label: None,
24668 edit: lsp::WorkspaceEdit {
24669 changes: Some(
24670 [(
24671 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24672 vec![lsp::TextEdit {
24673 range: lsp::Range::new(
24674 lsp::Position::new(0, 0),
24675 lsp::Position::new(0, 0),
24676 ),
24677 new_text: "X".into(),
24678 }],
24679 )]
24680 .into_iter()
24681 .collect(),
24682 ),
24683 ..lsp::WorkspaceEdit::default()
24684 },
24685 },
24686 )
24687 .await
24688 .into_response()
24689 .unwrap();
24690 Ok(Some(json!(null)))
24691 }
24692 }
24693 })
24694 .next()
24695 .await;
24696
24697 // Applying the code lens command returns a project transaction containing the edits
24698 // sent by the language server in its `workspaceEdit` request.
24699 let transaction = apply.await.unwrap();
24700 assert!(transaction.0.contains_key(&buffer));
24701 buffer.update(cx, |buffer, cx| {
24702 assert_eq!(buffer.text(), "Xa");
24703 buffer.undo(cx);
24704 assert_eq!(buffer.text(), "a");
24705 });
24706
24707 let actions_after_edits = cx
24708 .update_window(*workspace, |_, window, cx| {
24709 project.code_actions(&buffer, anchor..anchor, window, cx)
24710 })
24711 .unwrap()
24712 .await
24713 .unwrap();
24714 assert_eq!(
24715 actions, actions_after_edits,
24716 "For the same selection, same code lens actions should be returned"
24717 );
24718
24719 let _responses =
24720 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24721 panic!("No more code lens requests are expected");
24722 });
24723 editor.update_in(cx, |editor, window, cx| {
24724 editor.select_all(&SelectAll, window, cx);
24725 });
24726 cx.executor().run_until_parked();
24727 let new_actions = cx
24728 .update_window(*workspace, |_, window, cx| {
24729 project.code_actions(&buffer, anchor..anchor, window, cx)
24730 })
24731 .unwrap()
24732 .await
24733 .unwrap();
24734 assert_eq!(
24735 actions, new_actions,
24736 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24737 );
24738}
24739
24740#[gpui::test]
24741async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24742 init_test(cx, |_| {});
24743
24744 let fs = FakeFs::new(cx.executor());
24745 let main_text = r#"fn main() {
24746println!("1");
24747println!("2");
24748println!("3");
24749println!("4");
24750println!("5");
24751}"#;
24752 let lib_text = "mod foo {}";
24753 fs.insert_tree(
24754 path!("/a"),
24755 json!({
24756 "lib.rs": lib_text,
24757 "main.rs": main_text,
24758 }),
24759 )
24760 .await;
24761
24762 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24763 let (workspace, cx) =
24764 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24765 let worktree_id = workspace.update(cx, |workspace, cx| {
24766 workspace.project().update(cx, |project, cx| {
24767 project.worktrees(cx).next().unwrap().read(cx).id()
24768 })
24769 });
24770
24771 let expected_ranges = vec![
24772 Point::new(0, 0)..Point::new(0, 0),
24773 Point::new(1, 0)..Point::new(1, 1),
24774 Point::new(2, 0)..Point::new(2, 2),
24775 Point::new(3, 0)..Point::new(3, 3),
24776 ];
24777
24778 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24779 let editor_1 = workspace
24780 .update_in(cx, |workspace, window, cx| {
24781 workspace.open_path(
24782 (worktree_id, rel_path("main.rs")),
24783 Some(pane_1.downgrade()),
24784 true,
24785 window,
24786 cx,
24787 )
24788 })
24789 .unwrap()
24790 .await
24791 .downcast::<Editor>()
24792 .unwrap();
24793 pane_1.update(cx, |pane, cx| {
24794 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24795 open_editor.update(cx, |editor, cx| {
24796 assert_eq!(
24797 editor.display_text(cx),
24798 main_text,
24799 "Original main.rs text on initial open",
24800 );
24801 assert_eq!(
24802 editor
24803 .selections
24804 .all::<Point>(&editor.display_snapshot(cx))
24805 .into_iter()
24806 .map(|s| s.range())
24807 .collect::<Vec<_>>(),
24808 vec![Point::zero()..Point::zero()],
24809 "Default selections on initial open",
24810 );
24811 })
24812 });
24813 editor_1.update_in(cx, |editor, window, cx| {
24814 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24815 s.select_ranges(expected_ranges.clone());
24816 });
24817 });
24818
24819 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24820 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24821 });
24822 let editor_2 = workspace
24823 .update_in(cx, |workspace, window, cx| {
24824 workspace.open_path(
24825 (worktree_id, rel_path("main.rs")),
24826 Some(pane_2.downgrade()),
24827 true,
24828 window,
24829 cx,
24830 )
24831 })
24832 .unwrap()
24833 .await
24834 .downcast::<Editor>()
24835 .unwrap();
24836 pane_2.update(cx, |pane, cx| {
24837 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24838 open_editor.update(cx, |editor, cx| {
24839 assert_eq!(
24840 editor.display_text(cx),
24841 main_text,
24842 "Original main.rs text on initial open in another panel",
24843 );
24844 assert_eq!(
24845 editor
24846 .selections
24847 .all::<Point>(&editor.display_snapshot(cx))
24848 .into_iter()
24849 .map(|s| s.range())
24850 .collect::<Vec<_>>(),
24851 vec![Point::zero()..Point::zero()],
24852 "Default selections on initial open in another panel",
24853 );
24854 })
24855 });
24856
24857 editor_2.update_in(cx, |editor, window, cx| {
24858 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24859 });
24860
24861 let _other_editor_1 = workspace
24862 .update_in(cx, |workspace, window, cx| {
24863 workspace.open_path(
24864 (worktree_id, rel_path("lib.rs")),
24865 Some(pane_1.downgrade()),
24866 true,
24867 window,
24868 cx,
24869 )
24870 })
24871 .unwrap()
24872 .await
24873 .downcast::<Editor>()
24874 .unwrap();
24875 pane_1
24876 .update_in(cx, |pane, window, cx| {
24877 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24878 })
24879 .await
24880 .unwrap();
24881 drop(editor_1);
24882 pane_1.update(cx, |pane, cx| {
24883 pane.active_item()
24884 .unwrap()
24885 .downcast::<Editor>()
24886 .unwrap()
24887 .update(cx, |editor, cx| {
24888 assert_eq!(
24889 editor.display_text(cx),
24890 lib_text,
24891 "Other file should be open and active",
24892 );
24893 });
24894 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24895 });
24896
24897 let _other_editor_2 = workspace
24898 .update_in(cx, |workspace, window, cx| {
24899 workspace.open_path(
24900 (worktree_id, rel_path("lib.rs")),
24901 Some(pane_2.downgrade()),
24902 true,
24903 window,
24904 cx,
24905 )
24906 })
24907 .unwrap()
24908 .await
24909 .downcast::<Editor>()
24910 .unwrap();
24911 pane_2
24912 .update_in(cx, |pane, window, cx| {
24913 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24914 })
24915 .await
24916 .unwrap();
24917 drop(editor_2);
24918 pane_2.update(cx, |pane, cx| {
24919 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24920 open_editor.update(cx, |editor, cx| {
24921 assert_eq!(
24922 editor.display_text(cx),
24923 lib_text,
24924 "Other file should be open and active in another panel too",
24925 );
24926 });
24927 assert_eq!(
24928 pane.items().count(),
24929 1,
24930 "No other editors should be open in another pane",
24931 );
24932 });
24933
24934 let _editor_1_reopened = workspace
24935 .update_in(cx, |workspace, window, cx| {
24936 workspace.open_path(
24937 (worktree_id, rel_path("main.rs")),
24938 Some(pane_1.downgrade()),
24939 true,
24940 window,
24941 cx,
24942 )
24943 })
24944 .unwrap()
24945 .await
24946 .downcast::<Editor>()
24947 .unwrap();
24948 let _editor_2_reopened = workspace
24949 .update_in(cx, |workspace, window, cx| {
24950 workspace.open_path(
24951 (worktree_id, rel_path("main.rs")),
24952 Some(pane_2.downgrade()),
24953 true,
24954 window,
24955 cx,
24956 )
24957 })
24958 .unwrap()
24959 .await
24960 .downcast::<Editor>()
24961 .unwrap();
24962 pane_1.update(cx, |pane, cx| {
24963 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24964 open_editor.update(cx, |editor, cx| {
24965 assert_eq!(
24966 editor.display_text(cx),
24967 main_text,
24968 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24969 );
24970 assert_eq!(
24971 editor
24972 .selections
24973 .all::<Point>(&editor.display_snapshot(cx))
24974 .into_iter()
24975 .map(|s| s.range())
24976 .collect::<Vec<_>>(),
24977 expected_ranges,
24978 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24979 );
24980 })
24981 });
24982 pane_2.update(cx, |pane, cx| {
24983 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24984 open_editor.update(cx, |editor, cx| {
24985 assert_eq!(
24986 editor.display_text(cx),
24987 r#"fn main() {
24988⋯rintln!("1");
24989⋯intln!("2");
24990⋯ntln!("3");
24991println!("4");
24992println!("5");
24993}"#,
24994 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24995 );
24996 assert_eq!(
24997 editor
24998 .selections
24999 .all::<Point>(&editor.display_snapshot(cx))
25000 .into_iter()
25001 .map(|s| s.range())
25002 .collect::<Vec<_>>(),
25003 vec![Point::zero()..Point::zero()],
25004 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25005 );
25006 })
25007 });
25008}
25009
25010#[gpui::test]
25011async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25012 init_test(cx, |_| {});
25013
25014 let fs = FakeFs::new(cx.executor());
25015 let main_text = r#"fn main() {
25016println!("1");
25017println!("2");
25018println!("3");
25019println!("4");
25020println!("5");
25021}"#;
25022 let lib_text = "mod foo {}";
25023 fs.insert_tree(
25024 path!("/a"),
25025 json!({
25026 "lib.rs": lib_text,
25027 "main.rs": main_text,
25028 }),
25029 )
25030 .await;
25031
25032 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25033 let (workspace, cx) =
25034 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25035 let worktree_id = workspace.update(cx, |workspace, cx| {
25036 workspace.project().update(cx, |project, cx| {
25037 project.worktrees(cx).next().unwrap().read(cx).id()
25038 })
25039 });
25040
25041 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25042 let editor = workspace
25043 .update_in(cx, |workspace, window, cx| {
25044 workspace.open_path(
25045 (worktree_id, rel_path("main.rs")),
25046 Some(pane.downgrade()),
25047 true,
25048 window,
25049 cx,
25050 )
25051 })
25052 .unwrap()
25053 .await
25054 .downcast::<Editor>()
25055 .unwrap();
25056 pane.update(cx, |pane, cx| {
25057 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25058 open_editor.update(cx, |editor, cx| {
25059 assert_eq!(
25060 editor.display_text(cx),
25061 main_text,
25062 "Original main.rs text on initial open",
25063 );
25064 })
25065 });
25066 editor.update_in(cx, |editor, window, cx| {
25067 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25068 });
25069
25070 cx.update_global(|store: &mut SettingsStore, cx| {
25071 store.update_user_settings(cx, |s| {
25072 s.workspace.restore_on_file_reopen = Some(false);
25073 });
25074 });
25075 editor.update_in(cx, |editor, window, cx| {
25076 editor.fold_ranges(
25077 vec![
25078 Point::new(1, 0)..Point::new(1, 1),
25079 Point::new(2, 0)..Point::new(2, 2),
25080 Point::new(3, 0)..Point::new(3, 3),
25081 ],
25082 false,
25083 window,
25084 cx,
25085 );
25086 });
25087 pane.update_in(cx, |pane, window, cx| {
25088 pane.close_all_items(&CloseAllItems::default(), window, cx)
25089 })
25090 .await
25091 .unwrap();
25092 pane.update(cx, |pane, _| {
25093 assert!(pane.active_item().is_none());
25094 });
25095 cx.update_global(|store: &mut SettingsStore, cx| {
25096 store.update_user_settings(cx, |s| {
25097 s.workspace.restore_on_file_reopen = Some(true);
25098 });
25099 });
25100
25101 let _editor_reopened = workspace
25102 .update_in(cx, |workspace, window, cx| {
25103 workspace.open_path(
25104 (worktree_id, rel_path("main.rs")),
25105 Some(pane.downgrade()),
25106 true,
25107 window,
25108 cx,
25109 )
25110 })
25111 .unwrap()
25112 .await
25113 .downcast::<Editor>()
25114 .unwrap();
25115 pane.update(cx, |pane, cx| {
25116 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25117 open_editor.update(cx, |editor, cx| {
25118 assert_eq!(
25119 editor.display_text(cx),
25120 main_text,
25121 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25122 );
25123 })
25124 });
25125}
25126
25127#[gpui::test]
25128async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25129 struct EmptyModalView {
25130 focus_handle: gpui::FocusHandle,
25131 }
25132 impl EventEmitter<DismissEvent> for EmptyModalView {}
25133 impl Render for EmptyModalView {
25134 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25135 div()
25136 }
25137 }
25138 impl Focusable for EmptyModalView {
25139 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25140 self.focus_handle.clone()
25141 }
25142 }
25143 impl workspace::ModalView for EmptyModalView {}
25144 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25145 EmptyModalView {
25146 focus_handle: cx.focus_handle(),
25147 }
25148 }
25149
25150 init_test(cx, |_| {});
25151
25152 let fs = FakeFs::new(cx.executor());
25153 let project = Project::test(fs, [], cx).await;
25154 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25155 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25156 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25157 let editor = cx.new_window_entity(|window, cx| {
25158 Editor::new(
25159 EditorMode::full(),
25160 buffer,
25161 Some(project.clone()),
25162 window,
25163 cx,
25164 )
25165 });
25166 workspace
25167 .update(cx, |workspace, window, cx| {
25168 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25169 })
25170 .unwrap();
25171 editor.update_in(cx, |editor, window, cx| {
25172 editor.open_context_menu(&OpenContextMenu, window, cx);
25173 assert!(editor.mouse_context_menu.is_some());
25174 });
25175 workspace
25176 .update(cx, |workspace, window, cx| {
25177 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25178 })
25179 .unwrap();
25180 cx.read(|cx| {
25181 assert!(editor.read(cx).mouse_context_menu.is_none());
25182 });
25183}
25184
25185fn set_linked_edit_ranges(
25186 opening: (Point, Point),
25187 closing: (Point, Point),
25188 editor: &mut Editor,
25189 cx: &mut Context<Editor>,
25190) {
25191 let Some((buffer, _)) = editor
25192 .buffer
25193 .read(cx)
25194 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25195 else {
25196 panic!("Failed to get buffer for selection position");
25197 };
25198 let buffer = buffer.read(cx);
25199 let buffer_id = buffer.remote_id();
25200 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25201 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25202 let mut linked_ranges = HashMap::default();
25203 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25204 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25205}
25206
25207#[gpui::test]
25208async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25209 init_test(cx, |_| {});
25210
25211 let fs = FakeFs::new(cx.executor());
25212 fs.insert_file(path!("/file.html"), Default::default())
25213 .await;
25214
25215 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25216
25217 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25218 let html_language = Arc::new(Language::new(
25219 LanguageConfig {
25220 name: "HTML".into(),
25221 matcher: LanguageMatcher {
25222 path_suffixes: vec!["html".to_string()],
25223 ..LanguageMatcher::default()
25224 },
25225 brackets: BracketPairConfig {
25226 pairs: vec![BracketPair {
25227 start: "<".into(),
25228 end: ">".into(),
25229 close: true,
25230 ..Default::default()
25231 }],
25232 ..Default::default()
25233 },
25234 ..Default::default()
25235 },
25236 Some(tree_sitter_html::LANGUAGE.into()),
25237 ));
25238 language_registry.add(html_language);
25239 let mut fake_servers = language_registry.register_fake_lsp(
25240 "HTML",
25241 FakeLspAdapter {
25242 capabilities: lsp::ServerCapabilities {
25243 completion_provider: Some(lsp::CompletionOptions {
25244 resolve_provider: Some(true),
25245 ..Default::default()
25246 }),
25247 ..Default::default()
25248 },
25249 ..Default::default()
25250 },
25251 );
25252
25253 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25254 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25255
25256 let worktree_id = workspace
25257 .update(cx, |workspace, _window, cx| {
25258 workspace.project().update(cx, |project, cx| {
25259 project.worktrees(cx).next().unwrap().read(cx).id()
25260 })
25261 })
25262 .unwrap();
25263 project
25264 .update(cx, |project, cx| {
25265 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25266 })
25267 .await
25268 .unwrap();
25269 let editor = workspace
25270 .update(cx, |workspace, window, cx| {
25271 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25272 })
25273 .unwrap()
25274 .await
25275 .unwrap()
25276 .downcast::<Editor>()
25277 .unwrap();
25278
25279 let fake_server = fake_servers.next().await.unwrap();
25280 editor.update_in(cx, |editor, window, cx| {
25281 editor.set_text("<ad></ad>", window, cx);
25282 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25283 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25284 });
25285 set_linked_edit_ranges(
25286 (Point::new(0, 1), Point::new(0, 3)),
25287 (Point::new(0, 6), Point::new(0, 8)),
25288 editor,
25289 cx,
25290 );
25291 });
25292 let mut completion_handle =
25293 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25294 Ok(Some(lsp::CompletionResponse::Array(vec![
25295 lsp::CompletionItem {
25296 label: "head".to_string(),
25297 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25298 lsp::InsertReplaceEdit {
25299 new_text: "head".to_string(),
25300 insert: lsp::Range::new(
25301 lsp::Position::new(0, 1),
25302 lsp::Position::new(0, 3),
25303 ),
25304 replace: lsp::Range::new(
25305 lsp::Position::new(0, 1),
25306 lsp::Position::new(0, 3),
25307 ),
25308 },
25309 )),
25310 ..Default::default()
25311 },
25312 ])))
25313 });
25314 editor.update_in(cx, |editor, window, cx| {
25315 editor.show_completions(&ShowCompletions, window, cx);
25316 });
25317 cx.run_until_parked();
25318 completion_handle.next().await.unwrap();
25319 editor.update(cx, |editor, _| {
25320 assert!(
25321 editor.context_menu_visible(),
25322 "Completion menu should be visible"
25323 );
25324 });
25325 editor.update_in(cx, |editor, window, cx| {
25326 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25327 });
25328 cx.executor().run_until_parked();
25329 editor.update(cx, |editor, cx| {
25330 assert_eq!(editor.text(cx), "<head></head>");
25331 });
25332}
25333
25334#[gpui::test]
25335async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25336 init_test(cx, |_| {});
25337
25338 let mut cx = EditorTestContext::new(cx).await;
25339 let language = Arc::new(Language::new(
25340 LanguageConfig {
25341 name: "TSX".into(),
25342 matcher: LanguageMatcher {
25343 path_suffixes: vec!["tsx".to_string()],
25344 ..LanguageMatcher::default()
25345 },
25346 brackets: BracketPairConfig {
25347 pairs: vec![BracketPair {
25348 start: "<".into(),
25349 end: ">".into(),
25350 close: true,
25351 ..Default::default()
25352 }],
25353 ..Default::default()
25354 },
25355 linked_edit_characters: HashSet::from_iter(['.']),
25356 ..Default::default()
25357 },
25358 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25359 ));
25360 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25361
25362 // Test typing > does not extend linked pair
25363 cx.set_state("<divˇ<div></div>");
25364 cx.update_editor(|editor, _, cx| {
25365 set_linked_edit_ranges(
25366 (Point::new(0, 1), Point::new(0, 4)),
25367 (Point::new(0, 11), Point::new(0, 14)),
25368 editor,
25369 cx,
25370 );
25371 });
25372 cx.update_editor(|editor, window, cx| {
25373 editor.handle_input(">", window, cx);
25374 });
25375 cx.assert_editor_state("<div>ˇ<div></div>");
25376
25377 // Test typing . do extend linked pair
25378 cx.set_state("<Animatedˇ></Animated>");
25379 cx.update_editor(|editor, _, cx| {
25380 set_linked_edit_ranges(
25381 (Point::new(0, 1), Point::new(0, 9)),
25382 (Point::new(0, 12), Point::new(0, 20)),
25383 editor,
25384 cx,
25385 );
25386 });
25387 cx.update_editor(|editor, window, cx| {
25388 editor.handle_input(".", window, cx);
25389 });
25390 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25391 cx.update_editor(|editor, _, cx| {
25392 set_linked_edit_ranges(
25393 (Point::new(0, 1), Point::new(0, 10)),
25394 (Point::new(0, 13), Point::new(0, 21)),
25395 editor,
25396 cx,
25397 );
25398 });
25399 cx.update_editor(|editor, window, cx| {
25400 editor.handle_input("V", window, cx);
25401 });
25402 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25403}
25404
25405#[gpui::test]
25406async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25407 init_test(cx, |_| {});
25408
25409 let fs = FakeFs::new(cx.executor());
25410 fs.insert_tree(
25411 path!("/root"),
25412 json!({
25413 "a": {
25414 "main.rs": "fn main() {}",
25415 },
25416 "foo": {
25417 "bar": {
25418 "external_file.rs": "pub mod external {}",
25419 }
25420 }
25421 }),
25422 )
25423 .await;
25424
25425 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25426 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25427 language_registry.add(rust_lang());
25428 let _fake_servers = language_registry.register_fake_lsp(
25429 "Rust",
25430 FakeLspAdapter {
25431 ..FakeLspAdapter::default()
25432 },
25433 );
25434 let (workspace, cx) =
25435 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25436 let worktree_id = workspace.update(cx, |workspace, cx| {
25437 workspace.project().update(cx, |project, cx| {
25438 project.worktrees(cx).next().unwrap().read(cx).id()
25439 })
25440 });
25441
25442 let assert_language_servers_count =
25443 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25444 project.update(cx, |project, cx| {
25445 let current = project
25446 .lsp_store()
25447 .read(cx)
25448 .as_local()
25449 .unwrap()
25450 .language_servers
25451 .len();
25452 assert_eq!(expected, current, "{context}");
25453 });
25454 };
25455
25456 assert_language_servers_count(
25457 0,
25458 "No servers should be running before any file is open",
25459 cx,
25460 );
25461 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25462 let main_editor = workspace
25463 .update_in(cx, |workspace, window, cx| {
25464 workspace.open_path(
25465 (worktree_id, rel_path("main.rs")),
25466 Some(pane.downgrade()),
25467 true,
25468 window,
25469 cx,
25470 )
25471 })
25472 .unwrap()
25473 .await
25474 .downcast::<Editor>()
25475 .unwrap();
25476 pane.update(cx, |pane, cx| {
25477 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25478 open_editor.update(cx, |editor, cx| {
25479 assert_eq!(
25480 editor.display_text(cx),
25481 "fn main() {}",
25482 "Original main.rs text on initial open",
25483 );
25484 });
25485 assert_eq!(open_editor, main_editor);
25486 });
25487 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25488
25489 let external_editor = workspace
25490 .update_in(cx, |workspace, window, cx| {
25491 workspace.open_abs_path(
25492 PathBuf::from("/root/foo/bar/external_file.rs"),
25493 OpenOptions::default(),
25494 window,
25495 cx,
25496 )
25497 })
25498 .await
25499 .expect("opening external file")
25500 .downcast::<Editor>()
25501 .expect("downcasted external file's open element to editor");
25502 pane.update(cx, |pane, cx| {
25503 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25504 open_editor.update(cx, |editor, cx| {
25505 assert_eq!(
25506 editor.display_text(cx),
25507 "pub mod external {}",
25508 "External file is open now",
25509 );
25510 });
25511 assert_eq!(open_editor, external_editor);
25512 });
25513 assert_language_servers_count(
25514 1,
25515 "Second, external, *.rs file should join the existing server",
25516 cx,
25517 );
25518
25519 pane.update_in(cx, |pane, window, cx| {
25520 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25521 })
25522 .await
25523 .unwrap();
25524 pane.update_in(cx, |pane, window, cx| {
25525 pane.navigate_backward(&Default::default(), window, cx);
25526 });
25527 cx.run_until_parked();
25528 pane.update(cx, |pane, cx| {
25529 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25530 open_editor.update(cx, |editor, cx| {
25531 assert_eq!(
25532 editor.display_text(cx),
25533 "pub mod external {}",
25534 "External file is open now",
25535 );
25536 });
25537 });
25538 assert_language_servers_count(
25539 1,
25540 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25541 cx,
25542 );
25543
25544 cx.update(|_, cx| {
25545 workspace::reload(cx);
25546 });
25547 assert_language_servers_count(
25548 1,
25549 "After reloading the worktree with local and external files opened, only one project should be started",
25550 cx,
25551 );
25552}
25553
25554#[gpui::test]
25555async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25556 init_test(cx, |_| {});
25557
25558 let mut cx = EditorTestContext::new(cx).await;
25559 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25560 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25561
25562 // test cursor move to start of each line on tab
25563 // for `if`, `elif`, `else`, `while`, `with` and `for`
25564 cx.set_state(indoc! {"
25565 def main():
25566 ˇ for item in items:
25567 ˇ while item.active:
25568 ˇ if item.value > 10:
25569 ˇ continue
25570 ˇ elif item.value < 0:
25571 ˇ break
25572 ˇ else:
25573 ˇ with item.context() as ctx:
25574 ˇ yield count
25575 ˇ else:
25576 ˇ log('while else')
25577 ˇ else:
25578 ˇ log('for else')
25579 "});
25580 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25581 cx.assert_editor_state(indoc! {"
25582 def main():
25583 ˇfor item in items:
25584 ˇwhile item.active:
25585 ˇif item.value > 10:
25586 ˇcontinue
25587 ˇelif item.value < 0:
25588 ˇbreak
25589 ˇelse:
25590 ˇwith item.context() as ctx:
25591 ˇyield count
25592 ˇelse:
25593 ˇlog('while else')
25594 ˇelse:
25595 ˇlog('for else')
25596 "});
25597 // test relative indent is preserved when tab
25598 // for `if`, `elif`, `else`, `while`, `with` and `for`
25599 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25600 cx.assert_editor_state(indoc! {"
25601 def main():
25602 ˇfor item in items:
25603 ˇwhile item.active:
25604 ˇif item.value > 10:
25605 ˇcontinue
25606 ˇelif item.value < 0:
25607 ˇbreak
25608 ˇelse:
25609 ˇwith item.context() as ctx:
25610 ˇyield count
25611 ˇelse:
25612 ˇlog('while else')
25613 ˇelse:
25614 ˇlog('for else')
25615 "});
25616
25617 // test cursor move to start of each line on tab
25618 // for `try`, `except`, `else`, `finally`, `match` and `def`
25619 cx.set_state(indoc! {"
25620 def main():
25621 ˇ try:
25622 ˇ fetch()
25623 ˇ except ValueError:
25624 ˇ handle_error()
25625 ˇ else:
25626 ˇ match value:
25627 ˇ case _:
25628 ˇ finally:
25629 ˇ def status():
25630 ˇ return 0
25631 "});
25632 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25633 cx.assert_editor_state(indoc! {"
25634 def main():
25635 ˇtry:
25636 ˇfetch()
25637 ˇexcept ValueError:
25638 ˇhandle_error()
25639 ˇelse:
25640 ˇmatch value:
25641 ˇcase _:
25642 ˇfinally:
25643 ˇdef status():
25644 ˇreturn 0
25645 "});
25646 // test relative indent is preserved when tab
25647 // for `try`, `except`, `else`, `finally`, `match` and `def`
25648 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25649 cx.assert_editor_state(indoc! {"
25650 def main():
25651 ˇtry:
25652 ˇfetch()
25653 ˇexcept ValueError:
25654 ˇhandle_error()
25655 ˇelse:
25656 ˇmatch value:
25657 ˇcase _:
25658 ˇfinally:
25659 ˇdef status():
25660 ˇreturn 0
25661 "});
25662}
25663
25664#[gpui::test]
25665async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25666 init_test(cx, |_| {});
25667
25668 let mut cx = EditorTestContext::new(cx).await;
25669 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25671
25672 // test `else` auto outdents when typed inside `if` block
25673 cx.set_state(indoc! {"
25674 def main():
25675 if i == 2:
25676 return
25677 ˇ
25678 "});
25679 cx.update_editor(|editor, window, cx| {
25680 editor.handle_input("else:", window, cx);
25681 });
25682 cx.assert_editor_state(indoc! {"
25683 def main():
25684 if i == 2:
25685 return
25686 else:ˇ
25687 "});
25688
25689 // test `except` auto outdents when typed inside `try` block
25690 cx.set_state(indoc! {"
25691 def main():
25692 try:
25693 i = 2
25694 ˇ
25695 "});
25696 cx.update_editor(|editor, window, cx| {
25697 editor.handle_input("except:", window, cx);
25698 });
25699 cx.assert_editor_state(indoc! {"
25700 def main():
25701 try:
25702 i = 2
25703 except:ˇ
25704 "});
25705
25706 // test `else` auto outdents when typed inside `except` block
25707 cx.set_state(indoc! {"
25708 def main():
25709 try:
25710 i = 2
25711 except:
25712 j = 2
25713 ˇ
25714 "});
25715 cx.update_editor(|editor, window, cx| {
25716 editor.handle_input("else:", window, cx);
25717 });
25718 cx.assert_editor_state(indoc! {"
25719 def main():
25720 try:
25721 i = 2
25722 except:
25723 j = 2
25724 else:ˇ
25725 "});
25726
25727 // test `finally` auto outdents when typed inside `else` block
25728 cx.set_state(indoc! {"
25729 def main():
25730 try:
25731 i = 2
25732 except:
25733 j = 2
25734 else:
25735 k = 2
25736 ˇ
25737 "});
25738 cx.update_editor(|editor, window, cx| {
25739 editor.handle_input("finally:", window, cx);
25740 });
25741 cx.assert_editor_state(indoc! {"
25742 def main():
25743 try:
25744 i = 2
25745 except:
25746 j = 2
25747 else:
25748 k = 2
25749 finally:ˇ
25750 "});
25751
25752 // test `else` does not outdents when typed inside `except` block right after for block
25753 cx.set_state(indoc! {"
25754 def main():
25755 try:
25756 i = 2
25757 except:
25758 for i in range(n):
25759 pass
25760 ˇ
25761 "});
25762 cx.update_editor(|editor, window, cx| {
25763 editor.handle_input("else:", window, cx);
25764 });
25765 cx.assert_editor_state(indoc! {"
25766 def main():
25767 try:
25768 i = 2
25769 except:
25770 for i in range(n):
25771 pass
25772 else:ˇ
25773 "});
25774
25775 // test `finally` auto outdents when typed inside `else` block right after for block
25776 cx.set_state(indoc! {"
25777 def main():
25778 try:
25779 i = 2
25780 except:
25781 j = 2
25782 else:
25783 for i in range(n):
25784 pass
25785 ˇ
25786 "});
25787 cx.update_editor(|editor, window, cx| {
25788 editor.handle_input("finally:", window, cx);
25789 });
25790 cx.assert_editor_state(indoc! {"
25791 def main():
25792 try:
25793 i = 2
25794 except:
25795 j = 2
25796 else:
25797 for i in range(n):
25798 pass
25799 finally:ˇ
25800 "});
25801
25802 // test `except` outdents to inner "try" block
25803 cx.set_state(indoc! {"
25804 def main():
25805 try:
25806 i = 2
25807 if i == 2:
25808 try:
25809 i = 3
25810 ˇ
25811 "});
25812 cx.update_editor(|editor, window, cx| {
25813 editor.handle_input("except:", window, cx);
25814 });
25815 cx.assert_editor_state(indoc! {"
25816 def main():
25817 try:
25818 i = 2
25819 if i == 2:
25820 try:
25821 i = 3
25822 except:ˇ
25823 "});
25824
25825 // test `except` outdents to outer "try" block
25826 cx.set_state(indoc! {"
25827 def main():
25828 try:
25829 i = 2
25830 if i == 2:
25831 try:
25832 i = 3
25833 ˇ
25834 "});
25835 cx.update_editor(|editor, window, cx| {
25836 editor.handle_input("except:", window, cx);
25837 });
25838 cx.assert_editor_state(indoc! {"
25839 def main():
25840 try:
25841 i = 2
25842 if i == 2:
25843 try:
25844 i = 3
25845 except:ˇ
25846 "});
25847
25848 // test `else` stays at correct indent when typed after `for` block
25849 cx.set_state(indoc! {"
25850 def main():
25851 for i in range(10):
25852 if i == 3:
25853 break
25854 ˇ
25855 "});
25856 cx.update_editor(|editor, window, cx| {
25857 editor.handle_input("else:", window, cx);
25858 });
25859 cx.assert_editor_state(indoc! {"
25860 def main():
25861 for i in range(10):
25862 if i == 3:
25863 break
25864 else:ˇ
25865 "});
25866
25867 // test does not outdent on typing after line with square brackets
25868 cx.set_state(indoc! {"
25869 def f() -> list[str]:
25870 ˇ
25871 "});
25872 cx.update_editor(|editor, window, cx| {
25873 editor.handle_input("a", window, cx);
25874 });
25875 cx.assert_editor_state(indoc! {"
25876 def f() -> list[str]:
25877 aˇ
25878 "});
25879
25880 // test does not outdent on typing : after case keyword
25881 cx.set_state(indoc! {"
25882 match 1:
25883 caseˇ
25884 "});
25885 cx.update_editor(|editor, window, cx| {
25886 editor.handle_input(":", window, cx);
25887 });
25888 cx.assert_editor_state(indoc! {"
25889 match 1:
25890 case:ˇ
25891 "});
25892}
25893
25894#[gpui::test]
25895async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25896 init_test(cx, |_| {});
25897 update_test_language_settings(cx, |settings| {
25898 settings.defaults.extend_comment_on_newline = Some(false);
25899 });
25900 let mut cx = EditorTestContext::new(cx).await;
25901 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25902 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25903
25904 // test correct indent after newline on comment
25905 cx.set_state(indoc! {"
25906 # COMMENT:ˇ
25907 "});
25908 cx.update_editor(|editor, window, cx| {
25909 editor.newline(&Newline, window, cx);
25910 });
25911 cx.assert_editor_state(indoc! {"
25912 # COMMENT:
25913 ˇ
25914 "});
25915
25916 // test correct indent after newline in brackets
25917 cx.set_state(indoc! {"
25918 {ˇ}
25919 "});
25920 cx.update_editor(|editor, window, cx| {
25921 editor.newline(&Newline, window, cx);
25922 });
25923 cx.run_until_parked();
25924 cx.assert_editor_state(indoc! {"
25925 {
25926 ˇ
25927 }
25928 "});
25929
25930 cx.set_state(indoc! {"
25931 (ˇ)
25932 "});
25933 cx.update_editor(|editor, window, cx| {
25934 editor.newline(&Newline, window, cx);
25935 });
25936 cx.run_until_parked();
25937 cx.assert_editor_state(indoc! {"
25938 (
25939 ˇ
25940 )
25941 "});
25942
25943 // do not indent after empty lists or dictionaries
25944 cx.set_state(indoc! {"
25945 a = []ˇ
25946 "});
25947 cx.update_editor(|editor, window, cx| {
25948 editor.newline(&Newline, window, cx);
25949 });
25950 cx.run_until_parked();
25951 cx.assert_editor_state(indoc! {"
25952 a = []
25953 ˇ
25954 "});
25955}
25956
25957#[gpui::test]
25958async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25959 init_test(cx, |_| {});
25960
25961 let mut cx = EditorTestContext::new(cx).await;
25962 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25963 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25964
25965 // test cursor move to start of each line on tab
25966 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25967 cx.set_state(indoc! {"
25968 function main() {
25969 ˇ for item in $items; do
25970 ˇ while [ -n \"$item\" ]; do
25971 ˇ if [ \"$value\" -gt 10 ]; then
25972 ˇ continue
25973 ˇ elif [ \"$value\" -lt 0 ]; then
25974 ˇ break
25975 ˇ else
25976 ˇ echo \"$item\"
25977 ˇ fi
25978 ˇ done
25979 ˇ done
25980 ˇ}
25981 "});
25982 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25983 cx.assert_editor_state(indoc! {"
25984 function main() {
25985 ˇfor item in $items; do
25986 ˇwhile [ -n \"$item\" ]; do
25987 ˇif [ \"$value\" -gt 10 ]; then
25988 ˇcontinue
25989 ˇelif [ \"$value\" -lt 0 ]; then
25990 ˇbreak
25991 ˇelse
25992 ˇecho \"$item\"
25993 ˇfi
25994 ˇdone
25995 ˇdone
25996 ˇ}
25997 "});
25998 // test relative indent is preserved when tab
25999 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26000 cx.assert_editor_state(indoc! {"
26001 function main() {
26002 ˇfor item in $items; do
26003 ˇwhile [ -n \"$item\" ]; do
26004 ˇif [ \"$value\" -gt 10 ]; then
26005 ˇcontinue
26006 ˇelif [ \"$value\" -lt 0 ]; then
26007 ˇbreak
26008 ˇelse
26009 ˇecho \"$item\"
26010 ˇfi
26011 ˇdone
26012 ˇdone
26013 ˇ}
26014 "});
26015
26016 // test cursor move to start of each line on tab
26017 // for `case` statement with patterns
26018 cx.set_state(indoc! {"
26019 function handle() {
26020 ˇ case \"$1\" in
26021 ˇ start)
26022 ˇ echo \"a\"
26023 ˇ ;;
26024 ˇ stop)
26025 ˇ echo \"b\"
26026 ˇ ;;
26027 ˇ *)
26028 ˇ echo \"c\"
26029 ˇ ;;
26030 ˇ esac
26031 ˇ}
26032 "});
26033 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26034 cx.assert_editor_state(indoc! {"
26035 function handle() {
26036 ˇcase \"$1\" in
26037 ˇstart)
26038 ˇecho \"a\"
26039 ˇ;;
26040 ˇstop)
26041 ˇecho \"b\"
26042 ˇ;;
26043 ˇ*)
26044 ˇecho \"c\"
26045 ˇ;;
26046 ˇesac
26047 ˇ}
26048 "});
26049}
26050
26051#[gpui::test]
26052async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26053 init_test(cx, |_| {});
26054
26055 let mut cx = EditorTestContext::new(cx).await;
26056 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26057 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26058
26059 // test indents on comment insert
26060 cx.set_state(indoc! {"
26061 function main() {
26062 ˇ for item in $items; do
26063 ˇ while [ -n \"$item\" ]; do
26064 ˇ if [ \"$value\" -gt 10 ]; then
26065 ˇ continue
26066 ˇ elif [ \"$value\" -lt 0 ]; then
26067 ˇ break
26068 ˇ else
26069 ˇ echo \"$item\"
26070 ˇ fi
26071 ˇ done
26072 ˇ done
26073 ˇ}
26074 "});
26075 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26076 cx.assert_editor_state(indoc! {"
26077 function main() {
26078 #ˇ for item in $items; do
26079 #ˇ while [ -n \"$item\" ]; do
26080 #ˇ if [ \"$value\" -gt 10 ]; then
26081 #ˇ continue
26082 #ˇ elif [ \"$value\" -lt 0 ]; then
26083 #ˇ break
26084 #ˇ else
26085 #ˇ echo \"$item\"
26086 #ˇ fi
26087 #ˇ done
26088 #ˇ done
26089 #ˇ}
26090 "});
26091}
26092
26093#[gpui::test]
26094async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26095 init_test(cx, |_| {});
26096
26097 let mut cx = EditorTestContext::new(cx).await;
26098 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26099 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26100
26101 // test `else` auto outdents when typed inside `if` block
26102 cx.set_state(indoc! {"
26103 if [ \"$1\" = \"test\" ]; then
26104 echo \"foo bar\"
26105 ˇ
26106 "});
26107 cx.update_editor(|editor, window, cx| {
26108 editor.handle_input("else", window, cx);
26109 });
26110 cx.assert_editor_state(indoc! {"
26111 if [ \"$1\" = \"test\" ]; then
26112 echo \"foo bar\"
26113 elseˇ
26114 "});
26115
26116 // test `elif` auto outdents when typed inside `if` block
26117 cx.set_state(indoc! {"
26118 if [ \"$1\" = \"test\" ]; then
26119 echo \"foo bar\"
26120 ˇ
26121 "});
26122 cx.update_editor(|editor, window, cx| {
26123 editor.handle_input("elif", window, cx);
26124 });
26125 cx.assert_editor_state(indoc! {"
26126 if [ \"$1\" = \"test\" ]; then
26127 echo \"foo bar\"
26128 elifˇ
26129 "});
26130
26131 // test `fi` auto outdents when typed inside `else` block
26132 cx.set_state(indoc! {"
26133 if [ \"$1\" = \"test\" ]; then
26134 echo \"foo bar\"
26135 else
26136 echo \"bar baz\"
26137 ˇ
26138 "});
26139 cx.update_editor(|editor, window, cx| {
26140 editor.handle_input("fi", window, cx);
26141 });
26142 cx.assert_editor_state(indoc! {"
26143 if [ \"$1\" = \"test\" ]; then
26144 echo \"foo bar\"
26145 else
26146 echo \"bar baz\"
26147 fiˇ
26148 "});
26149
26150 // test `done` auto outdents when typed inside `while` block
26151 cx.set_state(indoc! {"
26152 while read line; do
26153 echo \"$line\"
26154 ˇ
26155 "});
26156 cx.update_editor(|editor, window, cx| {
26157 editor.handle_input("done", window, cx);
26158 });
26159 cx.assert_editor_state(indoc! {"
26160 while read line; do
26161 echo \"$line\"
26162 doneˇ
26163 "});
26164
26165 // test `done` auto outdents when typed inside `for` block
26166 cx.set_state(indoc! {"
26167 for file in *.txt; do
26168 cat \"$file\"
26169 ˇ
26170 "});
26171 cx.update_editor(|editor, window, cx| {
26172 editor.handle_input("done", window, cx);
26173 });
26174 cx.assert_editor_state(indoc! {"
26175 for file in *.txt; do
26176 cat \"$file\"
26177 doneˇ
26178 "});
26179
26180 // test `esac` auto outdents when typed inside `case` block
26181 cx.set_state(indoc! {"
26182 case \"$1\" in
26183 start)
26184 echo \"foo bar\"
26185 ;;
26186 stop)
26187 echo \"bar baz\"
26188 ;;
26189 ˇ
26190 "});
26191 cx.update_editor(|editor, window, cx| {
26192 editor.handle_input("esac", window, cx);
26193 });
26194 cx.assert_editor_state(indoc! {"
26195 case \"$1\" in
26196 start)
26197 echo \"foo bar\"
26198 ;;
26199 stop)
26200 echo \"bar baz\"
26201 ;;
26202 esacˇ
26203 "});
26204
26205 // test `*)` auto outdents when typed inside `case` block
26206 cx.set_state(indoc! {"
26207 case \"$1\" in
26208 start)
26209 echo \"foo bar\"
26210 ;;
26211 ˇ
26212 "});
26213 cx.update_editor(|editor, window, cx| {
26214 editor.handle_input("*)", window, cx);
26215 });
26216 cx.assert_editor_state(indoc! {"
26217 case \"$1\" in
26218 start)
26219 echo \"foo bar\"
26220 ;;
26221 *)ˇ
26222 "});
26223
26224 // test `fi` outdents to correct level with nested if blocks
26225 cx.set_state(indoc! {"
26226 if [ \"$1\" = \"test\" ]; then
26227 echo \"outer if\"
26228 if [ \"$2\" = \"debug\" ]; then
26229 echo \"inner if\"
26230 ˇ
26231 "});
26232 cx.update_editor(|editor, window, cx| {
26233 editor.handle_input("fi", window, cx);
26234 });
26235 cx.assert_editor_state(indoc! {"
26236 if [ \"$1\" = \"test\" ]; then
26237 echo \"outer if\"
26238 if [ \"$2\" = \"debug\" ]; then
26239 echo \"inner if\"
26240 fiˇ
26241 "});
26242}
26243
26244#[gpui::test]
26245async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26246 init_test(cx, |_| {});
26247 update_test_language_settings(cx, |settings| {
26248 settings.defaults.extend_comment_on_newline = Some(false);
26249 });
26250 let mut cx = EditorTestContext::new(cx).await;
26251 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26252 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26253
26254 // test correct indent after newline on comment
26255 cx.set_state(indoc! {"
26256 # COMMENT:ˇ
26257 "});
26258 cx.update_editor(|editor, window, cx| {
26259 editor.newline(&Newline, window, cx);
26260 });
26261 cx.assert_editor_state(indoc! {"
26262 # COMMENT:
26263 ˇ
26264 "});
26265
26266 // test correct indent after newline after `then`
26267 cx.set_state(indoc! {"
26268
26269 if [ \"$1\" = \"test\" ]; thenˇ
26270 "});
26271 cx.update_editor(|editor, window, cx| {
26272 editor.newline(&Newline, window, cx);
26273 });
26274 cx.run_until_parked();
26275 cx.assert_editor_state(indoc! {"
26276
26277 if [ \"$1\" = \"test\" ]; then
26278 ˇ
26279 "});
26280
26281 // test correct indent after newline after `else`
26282 cx.set_state(indoc! {"
26283 if [ \"$1\" = \"test\" ]; then
26284 elseˇ
26285 "});
26286 cx.update_editor(|editor, window, cx| {
26287 editor.newline(&Newline, window, cx);
26288 });
26289 cx.run_until_parked();
26290 cx.assert_editor_state(indoc! {"
26291 if [ \"$1\" = \"test\" ]; then
26292 else
26293 ˇ
26294 "});
26295
26296 // test correct indent after newline after `elif`
26297 cx.set_state(indoc! {"
26298 if [ \"$1\" = \"test\" ]; then
26299 elifˇ
26300 "});
26301 cx.update_editor(|editor, window, cx| {
26302 editor.newline(&Newline, window, cx);
26303 });
26304 cx.run_until_parked();
26305 cx.assert_editor_state(indoc! {"
26306 if [ \"$1\" = \"test\" ]; then
26307 elif
26308 ˇ
26309 "});
26310
26311 // test correct indent after newline after `do`
26312 cx.set_state(indoc! {"
26313 for file in *.txt; doˇ
26314 "});
26315 cx.update_editor(|editor, window, cx| {
26316 editor.newline(&Newline, window, cx);
26317 });
26318 cx.run_until_parked();
26319 cx.assert_editor_state(indoc! {"
26320 for file in *.txt; do
26321 ˇ
26322 "});
26323
26324 // test correct indent after newline after case pattern
26325 cx.set_state(indoc! {"
26326 case \"$1\" in
26327 start)ˇ
26328 "});
26329 cx.update_editor(|editor, window, cx| {
26330 editor.newline(&Newline, window, cx);
26331 });
26332 cx.run_until_parked();
26333 cx.assert_editor_state(indoc! {"
26334 case \"$1\" in
26335 start)
26336 ˇ
26337 "});
26338
26339 // test correct indent after newline after case pattern
26340 cx.set_state(indoc! {"
26341 case \"$1\" in
26342 start)
26343 ;;
26344 *)ˇ
26345 "});
26346 cx.update_editor(|editor, window, cx| {
26347 editor.newline(&Newline, window, cx);
26348 });
26349 cx.run_until_parked();
26350 cx.assert_editor_state(indoc! {"
26351 case \"$1\" in
26352 start)
26353 ;;
26354 *)
26355 ˇ
26356 "});
26357
26358 // test correct indent after newline after function opening brace
26359 cx.set_state(indoc! {"
26360 function test() {ˇ}
26361 "});
26362 cx.update_editor(|editor, window, cx| {
26363 editor.newline(&Newline, window, cx);
26364 });
26365 cx.run_until_parked();
26366 cx.assert_editor_state(indoc! {"
26367 function test() {
26368 ˇ
26369 }
26370 "});
26371
26372 // test no extra indent after semicolon on same line
26373 cx.set_state(indoc! {"
26374 echo \"test\";ˇ
26375 "});
26376 cx.update_editor(|editor, window, cx| {
26377 editor.newline(&Newline, window, cx);
26378 });
26379 cx.run_until_parked();
26380 cx.assert_editor_state(indoc! {"
26381 echo \"test\";
26382 ˇ
26383 "});
26384}
26385
26386fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26387 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26388 point..point
26389}
26390
26391#[track_caller]
26392fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26393 let (text, ranges) = marked_text_ranges(marked_text, true);
26394 assert_eq!(editor.text(cx), text);
26395 assert_eq!(
26396 editor.selections.ranges(&editor.display_snapshot(cx)),
26397 ranges
26398 .iter()
26399 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26400 .collect::<Vec<_>>(),
26401 "Assert selections are {}",
26402 marked_text
26403 );
26404}
26405
26406pub fn handle_signature_help_request(
26407 cx: &mut EditorLspTestContext,
26408 mocked_response: lsp::SignatureHelp,
26409) -> impl Future<Output = ()> + use<> {
26410 let mut request =
26411 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26412 let mocked_response = mocked_response.clone();
26413 async move { Ok(Some(mocked_response)) }
26414 });
26415
26416 async move {
26417 request.next().await;
26418 }
26419}
26420
26421#[track_caller]
26422pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26423 cx.update_editor(|editor, _, _| {
26424 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26425 let entries = menu.entries.borrow();
26426 let entries = entries
26427 .iter()
26428 .map(|entry| entry.string.as_str())
26429 .collect::<Vec<_>>();
26430 assert_eq!(entries, expected);
26431 } else {
26432 panic!("Expected completions menu");
26433 }
26434 });
26435}
26436
26437#[gpui::test]
26438async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26439 init_test(cx, |_| {});
26440 let mut cx = EditorLspTestContext::new_rust(
26441 lsp::ServerCapabilities {
26442 completion_provider: Some(lsp::CompletionOptions {
26443 ..Default::default()
26444 }),
26445 ..Default::default()
26446 },
26447 cx,
26448 )
26449 .await;
26450 cx.lsp
26451 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26452 Ok(Some(lsp::CompletionResponse::Array(vec![
26453 lsp::CompletionItem {
26454 label: "unsafe".into(),
26455 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26456 range: lsp::Range {
26457 start: lsp::Position {
26458 line: 0,
26459 character: 9,
26460 },
26461 end: lsp::Position {
26462 line: 0,
26463 character: 11,
26464 },
26465 },
26466 new_text: "unsafe".to_string(),
26467 })),
26468 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26469 ..Default::default()
26470 },
26471 ])))
26472 });
26473
26474 cx.update_editor(|editor, _, cx| {
26475 editor.project().unwrap().update(cx, |project, cx| {
26476 project.snippets().update(cx, |snippets, _cx| {
26477 snippets.add_snippet_for_test(
26478 None,
26479 PathBuf::from("test_snippets.json"),
26480 vec![
26481 Arc::new(project::snippet_provider::Snippet {
26482 prefix: vec![
26483 "unlimited word count".to_string(),
26484 "unlimit word count".to_string(),
26485 "unlimited unknown".to_string(),
26486 ],
26487 body: "this is many words".to_string(),
26488 description: Some("description".to_string()),
26489 name: "multi-word snippet test".to_string(),
26490 }),
26491 Arc::new(project::snippet_provider::Snippet {
26492 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26493 body: "fewer words".to_string(),
26494 description: Some("alt description".to_string()),
26495 name: "other name".to_string(),
26496 }),
26497 Arc::new(project::snippet_provider::Snippet {
26498 prefix: vec!["ab aa".to_string()],
26499 body: "abcd".to_string(),
26500 description: None,
26501 name: "alphabet".to_string(),
26502 }),
26503 ],
26504 );
26505 });
26506 })
26507 });
26508
26509 let get_completions = |cx: &mut EditorLspTestContext| {
26510 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26511 Some(CodeContextMenu::Completions(context_menu)) => {
26512 let entries = context_menu.entries.borrow();
26513 entries
26514 .iter()
26515 .map(|entry| entry.string.clone())
26516 .collect_vec()
26517 }
26518 _ => vec![],
26519 })
26520 };
26521
26522 // snippets:
26523 // @foo
26524 // foo bar
26525 //
26526 // when typing:
26527 //
26528 // when typing:
26529 // - if I type a symbol "open the completions with snippets only"
26530 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26531 //
26532 // stuff we need:
26533 // - filtering logic change?
26534 // - remember how far back the completion started.
26535
26536 let test_cases: &[(&str, &[&str])] = &[
26537 (
26538 "un",
26539 &[
26540 "unsafe",
26541 "unlimit word count",
26542 "unlimited unknown",
26543 "unlimited word count",
26544 "unsnip",
26545 ],
26546 ),
26547 (
26548 "u ",
26549 &[
26550 "unlimit word count",
26551 "unlimited unknown",
26552 "unlimited word count",
26553 ],
26554 ),
26555 ("u a", &["ab aa", "unsafe"]), // unsAfe
26556 (
26557 "u u",
26558 &[
26559 "unsafe",
26560 "unlimit word count",
26561 "unlimited unknown", // ranked highest among snippets
26562 "unlimited word count",
26563 "unsnip",
26564 ],
26565 ),
26566 ("uw c", &["unlimit word count", "unlimited word count"]),
26567 (
26568 "u w",
26569 &[
26570 "unlimit word count",
26571 "unlimited word count",
26572 "unlimited unknown",
26573 ],
26574 ),
26575 ("u w ", &["unlimit word count", "unlimited word count"]),
26576 (
26577 "u ",
26578 &[
26579 "unlimit word count",
26580 "unlimited unknown",
26581 "unlimited word count",
26582 ],
26583 ),
26584 ("wor", &[]),
26585 ("uf", &["unsafe"]),
26586 ("af", &["unsafe"]),
26587 ("afu", &[]),
26588 (
26589 "ue",
26590 &["unsafe", "unlimited unknown", "unlimited word count"],
26591 ),
26592 ("@", &["@few"]),
26593 ("@few", &["@few"]),
26594 ("@ ", &[]),
26595 ("a@", &["@few"]),
26596 ("a@f", &["@few", "unsafe"]),
26597 ("a@fw", &["@few"]),
26598 ("a", &["ab aa", "unsafe"]),
26599 ("aa", &["ab aa"]),
26600 ("aaa", &["ab aa"]),
26601 ("ab", &["ab aa"]),
26602 ("ab ", &["ab aa"]),
26603 ("ab a", &["ab aa", "unsafe"]),
26604 ("ab ab", &["ab aa"]),
26605 ("ab ab aa", &["ab aa"]),
26606 ];
26607
26608 for &(input_to_simulate, expected_completions) in test_cases {
26609 cx.set_state("fn a() { ˇ }\n");
26610 for c in input_to_simulate.split("") {
26611 cx.simulate_input(c);
26612 cx.run_until_parked();
26613 }
26614 let expected_completions = expected_completions
26615 .iter()
26616 .map(|s| s.to_string())
26617 .collect_vec();
26618 assert_eq!(
26619 get_completions(&mut cx),
26620 expected_completions,
26621 "< actual / expected >, input = {input_to_simulate:?}",
26622 );
26623 }
26624}
26625
26626/// Handle completion request passing a marked string specifying where the completion
26627/// should be triggered from using '|' character, what range should be replaced, and what completions
26628/// should be returned using '<' and '>' to delimit the range.
26629///
26630/// Also see `handle_completion_request_with_insert_and_replace`.
26631#[track_caller]
26632pub fn handle_completion_request(
26633 marked_string: &str,
26634 completions: Vec<&'static str>,
26635 is_incomplete: bool,
26636 counter: Arc<AtomicUsize>,
26637 cx: &mut EditorLspTestContext,
26638) -> impl Future<Output = ()> {
26639 let complete_from_marker: TextRangeMarker = '|'.into();
26640 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26641 let (_, mut marked_ranges) = marked_text_ranges_by(
26642 marked_string,
26643 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26644 );
26645
26646 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26647 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26648 ));
26649 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26650 let replace_range =
26651 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26652
26653 let mut request =
26654 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26655 let completions = completions.clone();
26656 counter.fetch_add(1, atomic::Ordering::Release);
26657 async move {
26658 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26659 assert_eq!(
26660 params.text_document_position.position,
26661 complete_from_position
26662 );
26663 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26664 is_incomplete,
26665 item_defaults: None,
26666 items: completions
26667 .iter()
26668 .map(|completion_text| lsp::CompletionItem {
26669 label: completion_text.to_string(),
26670 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26671 range: replace_range,
26672 new_text: completion_text.to_string(),
26673 })),
26674 ..Default::default()
26675 })
26676 .collect(),
26677 })))
26678 }
26679 });
26680
26681 async move {
26682 request.next().await;
26683 }
26684}
26685
26686/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26687/// given instead, which also contains an `insert` range.
26688///
26689/// This function uses markers to define ranges:
26690/// - `|` marks the cursor position
26691/// - `<>` marks the replace range
26692/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26693pub fn handle_completion_request_with_insert_and_replace(
26694 cx: &mut EditorLspTestContext,
26695 marked_string: &str,
26696 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26697 counter: Arc<AtomicUsize>,
26698) -> impl Future<Output = ()> {
26699 let complete_from_marker: TextRangeMarker = '|'.into();
26700 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26701 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26702
26703 let (_, mut marked_ranges) = marked_text_ranges_by(
26704 marked_string,
26705 vec![
26706 complete_from_marker.clone(),
26707 replace_range_marker.clone(),
26708 insert_range_marker.clone(),
26709 ],
26710 );
26711
26712 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26713 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26714 ));
26715 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26716 let replace_range =
26717 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26718
26719 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26720 Some(ranges) if !ranges.is_empty() => {
26721 let range1 = ranges[0].clone();
26722 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26723 }
26724 _ => lsp::Range {
26725 start: replace_range.start,
26726 end: complete_from_position,
26727 },
26728 };
26729
26730 let mut request =
26731 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26732 let completions = completions.clone();
26733 counter.fetch_add(1, atomic::Ordering::Release);
26734 async move {
26735 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26736 assert_eq!(
26737 params.text_document_position.position, complete_from_position,
26738 "marker `|` position doesn't match",
26739 );
26740 Ok(Some(lsp::CompletionResponse::Array(
26741 completions
26742 .iter()
26743 .map(|(label, new_text)| lsp::CompletionItem {
26744 label: label.to_string(),
26745 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26746 lsp::InsertReplaceEdit {
26747 insert: insert_range,
26748 replace: replace_range,
26749 new_text: new_text.to_string(),
26750 },
26751 )),
26752 ..Default::default()
26753 })
26754 .collect(),
26755 )))
26756 }
26757 });
26758
26759 async move {
26760 request.next().await;
26761 }
26762}
26763
26764fn handle_resolve_completion_request(
26765 cx: &mut EditorLspTestContext,
26766 edits: Option<Vec<(&'static str, &'static str)>>,
26767) -> impl Future<Output = ()> {
26768 let edits = edits.map(|edits| {
26769 edits
26770 .iter()
26771 .map(|(marked_string, new_text)| {
26772 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26773 let replace_range = cx.to_lsp_range(
26774 MultiBufferOffset(marked_ranges[0].start)
26775 ..MultiBufferOffset(marked_ranges[0].end),
26776 );
26777 lsp::TextEdit::new(replace_range, new_text.to_string())
26778 })
26779 .collect::<Vec<_>>()
26780 });
26781
26782 let mut request =
26783 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26784 let edits = edits.clone();
26785 async move {
26786 Ok(lsp::CompletionItem {
26787 additional_text_edits: edits,
26788 ..Default::default()
26789 })
26790 }
26791 });
26792
26793 async move {
26794 request.next().await;
26795 }
26796}
26797
26798pub(crate) fn update_test_language_settings(
26799 cx: &mut TestAppContext,
26800 f: impl Fn(&mut AllLanguageSettingsContent),
26801) {
26802 cx.update(|cx| {
26803 SettingsStore::update_global(cx, |store, cx| {
26804 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26805 });
26806 });
26807}
26808
26809pub(crate) fn update_test_project_settings(
26810 cx: &mut TestAppContext,
26811 f: impl Fn(&mut ProjectSettingsContent),
26812) {
26813 cx.update(|cx| {
26814 SettingsStore::update_global(cx, |store, cx| {
26815 store.update_user_settings(cx, |settings| f(&mut settings.project));
26816 });
26817 });
26818}
26819
26820pub(crate) fn update_test_editor_settings(
26821 cx: &mut TestAppContext,
26822 f: impl Fn(&mut EditorSettingsContent),
26823) {
26824 cx.update(|cx| {
26825 SettingsStore::update_global(cx, |store, cx| {
26826 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26827 })
26828 })
26829}
26830
26831pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26832 cx.update(|cx| {
26833 assets::Assets.load_test_fonts(cx);
26834 let store = SettingsStore::test(cx);
26835 cx.set_global(store);
26836 theme::init(theme::LoadThemes::JustBase, cx);
26837 release_channel::init(semver::Version::new(0, 0, 0), cx);
26838 crate::init(cx);
26839 });
26840 zlog::init_test();
26841 update_test_language_settings(cx, f);
26842}
26843
26844#[track_caller]
26845fn assert_hunk_revert(
26846 not_reverted_text_with_selections: &str,
26847 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26848 expected_reverted_text_with_selections: &str,
26849 base_text: &str,
26850 cx: &mut EditorLspTestContext,
26851) {
26852 cx.set_state(not_reverted_text_with_selections);
26853 cx.set_head_text(base_text);
26854 cx.executor().run_until_parked();
26855
26856 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26857 let snapshot = editor.snapshot(window, cx);
26858 let reverted_hunk_statuses = snapshot
26859 .buffer_snapshot()
26860 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26861 .map(|hunk| hunk.status().kind)
26862 .collect::<Vec<_>>();
26863
26864 editor.git_restore(&Default::default(), window, cx);
26865 reverted_hunk_statuses
26866 });
26867 cx.executor().run_until_parked();
26868 cx.assert_editor_state(expected_reverted_text_with_selections);
26869 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26870}
26871
26872#[gpui::test(iterations = 10)]
26873async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26874 init_test(cx, |_| {});
26875
26876 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26877 let counter = diagnostic_requests.clone();
26878
26879 let fs = FakeFs::new(cx.executor());
26880 fs.insert_tree(
26881 path!("/a"),
26882 json!({
26883 "first.rs": "fn main() { let a = 5; }",
26884 "second.rs": "// Test file",
26885 }),
26886 )
26887 .await;
26888
26889 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26890 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26891 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26892
26893 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26894 language_registry.add(rust_lang());
26895 let mut fake_servers = language_registry.register_fake_lsp(
26896 "Rust",
26897 FakeLspAdapter {
26898 capabilities: lsp::ServerCapabilities {
26899 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26900 lsp::DiagnosticOptions {
26901 identifier: None,
26902 inter_file_dependencies: true,
26903 workspace_diagnostics: true,
26904 work_done_progress_options: Default::default(),
26905 },
26906 )),
26907 ..Default::default()
26908 },
26909 ..Default::default()
26910 },
26911 );
26912
26913 let editor = workspace
26914 .update(cx, |workspace, window, cx| {
26915 workspace.open_abs_path(
26916 PathBuf::from(path!("/a/first.rs")),
26917 OpenOptions::default(),
26918 window,
26919 cx,
26920 )
26921 })
26922 .unwrap()
26923 .await
26924 .unwrap()
26925 .downcast::<Editor>()
26926 .unwrap();
26927 let fake_server = fake_servers.next().await.unwrap();
26928 let server_id = fake_server.server.server_id();
26929 let mut first_request = fake_server
26930 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26931 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26932 let result_id = Some(new_result_id.to_string());
26933 assert_eq!(
26934 params.text_document.uri,
26935 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26936 );
26937 async move {
26938 Ok(lsp::DocumentDiagnosticReportResult::Report(
26939 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26940 related_documents: None,
26941 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26942 items: Vec::new(),
26943 result_id,
26944 },
26945 }),
26946 ))
26947 }
26948 });
26949
26950 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
26951 project.update(cx, |project, cx| {
26952 let buffer_id = editor
26953 .read(cx)
26954 .buffer()
26955 .read(cx)
26956 .as_singleton()
26957 .expect("created a singleton buffer")
26958 .read(cx)
26959 .remote_id();
26960 let buffer_result_id = project
26961 .lsp_store()
26962 .read(cx)
26963 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
26964 assert_eq!(expected, buffer_result_id);
26965 });
26966 };
26967
26968 ensure_result_id(None, cx);
26969 cx.executor().advance_clock(Duration::from_millis(60));
26970 cx.executor().run_until_parked();
26971 assert_eq!(
26972 diagnostic_requests.load(atomic::Ordering::Acquire),
26973 1,
26974 "Opening file should trigger diagnostic request"
26975 );
26976 first_request
26977 .next()
26978 .await
26979 .expect("should have sent the first diagnostics pull request");
26980 ensure_result_id(Some(SharedString::new("1")), cx);
26981
26982 // Editing should trigger diagnostics
26983 editor.update_in(cx, |editor, window, cx| {
26984 editor.handle_input("2", window, cx)
26985 });
26986 cx.executor().advance_clock(Duration::from_millis(60));
26987 cx.executor().run_until_parked();
26988 assert_eq!(
26989 diagnostic_requests.load(atomic::Ordering::Acquire),
26990 2,
26991 "Editing should trigger diagnostic request"
26992 );
26993 ensure_result_id(Some(SharedString::new("2")), cx);
26994
26995 // Moving cursor should not trigger diagnostic request
26996 editor.update_in(cx, |editor, window, cx| {
26997 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26998 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26999 });
27000 });
27001 cx.executor().advance_clock(Duration::from_millis(60));
27002 cx.executor().run_until_parked();
27003 assert_eq!(
27004 diagnostic_requests.load(atomic::Ordering::Acquire),
27005 2,
27006 "Cursor movement should not trigger diagnostic request"
27007 );
27008 ensure_result_id(Some(SharedString::new("2")), cx);
27009 // Multiple rapid edits should be debounced
27010 for _ in 0..5 {
27011 editor.update_in(cx, |editor, window, cx| {
27012 editor.handle_input("x", window, cx)
27013 });
27014 }
27015 cx.executor().advance_clock(Duration::from_millis(60));
27016 cx.executor().run_until_parked();
27017
27018 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27019 assert!(
27020 final_requests <= 4,
27021 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27022 );
27023 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27024}
27025
27026#[gpui::test]
27027async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27028 // Regression test for issue #11671
27029 // Previously, adding a cursor after moving multiple cursors would reset
27030 // the cursor count instead of adding to the existing cursors.
27031 init_test(cx, |_| {});
27032 let mut cx = EditorTestContext::new(cx).await;
27033
27034 // Create a simple buffer with cursor at start
27035 cx.set_state(indoc! {"
27036 ˇaaaa
27037 bbbb
27038 cccc
27039 dddd
27040 eeee
27041 ffff
27042 gggg
27043 hhhh"});
27044
27045 // Add 2 cursors below (so we have 3 total)
27046 cx.update_editor(|editor, window, cx| {
27047 editor.add_selection_below(&Default::default(), window, cx);
27048 editor.add_selection_below(&Default::default(), window, cx);
27049 });
27050
27051 // Verify we have 3 cursors
27052 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27053 assert_eq!(
27054 initial_count, 3,
27055 "Should have 3 cursors after adding 2 below"
27056 );
27057
27058 // Move down one line
27059 cx.update_editor(|editor, window, cx| {
27060 editor.move_down(&MoveDown, window, cx);
27061 });
27062
27063 // Add another cursor below
27064 cx.update_editor(|editor, window, cx| {
27065 editor.add_selection_below(&Default::default(), window, cx);
27066 });
27067
27068 // Should now have 4 cursors (3 original + 1 new)
27069 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27070 assert_eq!(
27071 final_count, 4,
27072 "Should have 4 cursors after moving and adding another"
27073 );
27074}
27075
27076#[gpui::test]
27077async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27078 init_test(cx, |_| {});
27079
27080 let mut cx = EditorTestContext::new(cx).await;
27081
27082 cx.set_state(indoc!(
27083 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27084 Second line here"#
27085 ));
27086
27087 cx.update_editor(|editor, window, cx| {
27088 // Enable soft wrapping with a narrow width to force soft wrapping and
27089 // confirm that more than 2 rows are being displayed.
27090 editor.set_wrap_width(Some(100.0.into()), cx);
27091 assert!(editor.display_text(cx).lines().count() > 2);
27092
27093 editor.add_selection_below(
27094 &AddSelectionBelow {
27095 skip_soft_wrap: true,
27096 },
27097 window,
27098 cx,
27099 );
27100
27101 assert_eq!(
27102 display_ranges(editor, cx),
27103 &[
27104 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27105 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27106 ]
27107 );
27108
27109 editor.add_selection_above(
27110 &AddSelectionAbove {
27111 skip_soft_wrap: true,
27112 },
27113 window,
27114 cx,
27115 );
27116
27117 assert_eq!(
27118 display_ranges(editor, cx),
27119 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27120 );
27121
27122 editor.add_selection_below(
27123 &AddSelectionBelow {
27124 skip_soft_wrap: false,
27125 },
27126 window,
27127 cx,
27128 );
27129
27130 assert_eq!(
27131 display_ranges(editor, cx),
27132 &[
27133 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27134 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27135 ]
27136 );
27137
27138 editor.add_selection_above(
27139 &AddSelectionAbove {
27140 skip_soft_wrap: false,
27141 },
27142 window,
27143 cx,
27144 );
27145
27146 assert_eq!(
27147 display_ranges(editor, cx),
27148 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27149 );
27150 });
27151}
27152
27153#[gpui::test]
27154async fn test_insert_snippet(cx: &mut TestAppContext) {
27155 init_test(cx, |_| {});
27156 let mut cx = EditorTestContext::new(cx).await;
27157
27158 cx.update_editor(|editor, _, cx| {
27159 editor.project().unwrap().update(cx, |project, cx| {
27160 project.snippets().update(cx, |snippets, _cx| {
27161 let snippet = project::snippet_provider::Snippet {
27162 prefix: vec![], // no prefix needed!
27163 body: "an Unspecified".to_string(),
27164 description: Some("shhhh it's a secret".to_string()),
27165 name: "super secret snippet".to_string(),
27166 };
27167 snippets.add_snippet_for_test(
27168 None,
27169 PathBuf::from("test_snippets.json"),
27170 vec![Arc::new(snippet)],
27171 );
27172
27173 let snippet = project::snippet_provider::Snippet {
27174 prefix: vec![], // no prefix needed!
27175 body: " Location".to_string(),
27176 description: Some("the word 'location'".to_string()),
27177 name: "location word".to_string(),
27178 };
27179 snippets.add_snippet_for_test(
27180 Some("Markdown".to_string()),
27181 PathBuf::from("test_snippets.json"),
27182 vec![Arc::new(snippet)],
27183 );
27184 });
27185 })
27186 });
27187
27188 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27189
27190 cx.update_editor(|editor, window, cx| {
27191 editor.insert_snippet_at_selections(
27192 &InsertSnippet {
27193 language: None,
27194 name: Some("super secret snippet".to_string()),
27195 snippet: None,
27196 },
27197 window,
27198 cx,
27199 );
27200
27201 // Language is specified in the action,
27202 // so the buffer language does not need to match
27203 editor.insert_snippet_at_selections(
27204 &InsertSnippet {
27205 language: Some("Markdown".to_string()),
27206 name: Some("location word".to_string()),
27207 snippet: None,
27208 },
27209 window,
27210 cx,
27211 );
27212
27213 editor.insert_snippet_at_selections(
27214 &InsertSnippet {
27215 language: None,
27216 name: None,
27217 snippet: Some("$0 after".to_string()),
27218 },
27219 window,
27220 cx,
27221 );
27222 });
27223
27224 cx.assert_editor_state(
27225 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27226 );
27227}
27228
27229#[gpui::test(iterations = 10)]
27230async fn test_document_colors(cx: &mut TestAppContext) {
27231 let expected_color = Rgba {
27232 r: 0.33,
27233 g: 0.33,
27234 b: 0.33,
27235 a: 0.33,
27236 };
27237
27238 init_test(cx, |_| {});
27239
27240 let fs = FakeFs::new(cx.executor());
27241 fs.insert_tree(
27242 path!("/a"),
27243 json!({
27244 "first.rs": "fn main() { let a = 5; }",
27245 }),
27246 )
27247 .await;
27248
27249 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27250 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27251 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27252
27253 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27254 language_registry.add(rust_lang());
27255 let mut fake_servers = language_registry.register_fake_lsp(
27256 "Rust",
27257 FakeLspAdapter {
27258 capabilities: lsp::ServerCapabilities {
27259 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27260 ..lsp::ServerCapabilities::default()
27261 },
27262 name: "rust-analyzer",
27263 ..FakeLspAdapter::default()
27264 },
27265 );
27266 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27267 "Rust",
27268 FakeLspAdapter {
27269 capabilities: lsp::ServerCapabilities {
27270 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27271 ..lsp::ServerCapabilities::default()
27272 },
27273 name: "not-rust-analyzer",
27274 ..FakeLspAdapter::default()
27275 },
27276 );
27277
27278 let editor = workspace
27279 .update(cx, |workspace, window, cx| {
27280 workspace.open_abs_path(
27281 PathBuf::from(path!("/a/first.rs")),
27282 OpenOptions::default(),
27283 window,
27284 cx,
27285 )
27286 })
27287 .unwrap()
27288 .await
27289 .unwrap()
27290 .downcast::<Editor>()
27291 .unwrap();
27292 let fake_language_server = fake_servers.next().await.unwrap();
27293 let fake_language_server_without_capabilities =
27294 fake_servers_without_capabilities.next().await.unwrap();
27295 let requests_made = Arc::new(AtomicUsize::new(0));
27296 let closure_requests_made = Arc::clone(&requests_made);
27297 let mut color_request_handle = fake_language_server
27298 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27299 let requests_made = Arc::clone(&closure_requests_made);
27300 async move {
27301 assert_eq!(
27302 params.text_document.uri,
27303 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27304 );
27305 requests_made.fetch_add(1, atomic::Ordering::Release);
27306 Ok(vec![
27307 lsp::ColorInformation {
27308 range: lsp::Range {
27309 start: lsp::Position {
27310 line: 0,
27311 character: 0,
27312 },
27313 end: lsp::Position {
27314 line: 0,
27315 character: 1,
27316 },
27317 },
27318 color: lsp::Color {
27319 red: 0.33,
27320 green: 0.33,
27321 blue: 0.33,
27322 alpha: 0.33,
27323 },
27324 },
27325 lsp::ColorInformation {
27326 range: lsp::Range {
27327 start: lsp::Position {
27328 line: 0,
27329 character: 0,
27330 },
27331 end: lsp::Position {
27332 line: 0,
27333 character: 1,
27334 },
27335 },
27336 color: lsp::Color {
27337 red: 0.33,
27338 green: 0.33,
27339 blue: 0.33,
27340 alpha: 0.33,
27341 },
27342 },
27343 ])
27344 }
27345 });
27346
27347 let _handle = fake_language_server_without_capabilities
27348 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27349 panic!("Should not be called");
27350 });
27351 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27352 color_request_handle.next().await.unwrap();
27353 cx.run_until_parked();
27354 assert_eq!(
27355 1,
27356 requests_made.load(atomic::Ordering::Acquire),
27357 "Should query for colors once per editor open"
27358 );
27359 editor.update_in(cx, |editor, _, cx| {
27360 assert_eq!(
27361 vec![expected_color],
27362 extract_color_inlays(editor, cx),
27363 "Should have an initial inlay"
27364 );
27365 });
27366
27367 // opening another file in a split should not influence the LSP query counter
27368 workspace
27369 .update(cx, |workspace, window, cx| {
27370 assert_eq!(
27371 workspace.panes().len(),
27372 1,
27373 "Should have one pane with one editor"
27374 );
27375 workspace.move_item_to_pane_in_direction(
27376 &MoveItemToPaneInDirection {
27377 direction: SplitDirection::Right,
27378 focus: false,
27379 clone: true,
27380 },
27381 window,
27382 cx,
27383 );
27384 })
27385 .unwrap();
27386 cx.run_until_parked();
27387 workspace
27388 .update(cx, |workspace, _, cx| {
27389 let panes = workspace.panes();
27390 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27391 for pane in panes {
27392 let editor = pane
27393 .read(cx)
27394 .active_item()
27395 .and_then(|item| item.downcast::<Editor>())
27396 .expect("Should have opened an editor in each split");
27397 let editor_file = editor
27398 .read(cx)
27399 .buffer()
27400 .read(cx)
27401 .as_singleton()
27402 .expect("test deals with singleton buffers")
27403 .read(cx)
27404 .file()
27405 .expect("test buffese should have a file")
27406 .path();
27407 assert_eq!(
27408 editor_file.as_ref(),
27409 rel_path("first.rs"),
27410 "Both editors should be opened for the same file"
27411 )
27412 }
27413 })
27414 .unwrap();
27415
27416 cx.executor().advance_clock(Duration::from_millis(500));
27417 let save = editor.update_in(cx, |editor, window, cx| {
27418 editor.move_to_end(&MoveToEnd, window, cx);
27419 editor.handle_input("dirty", window, cx);
27420 editor.save(
27421 SaveOptions {
27422 format: true,
27423 autosave: true,
27424 },
27425 project.clone(),
27426 window,
27427 cx,
27428 )
27429 });
27430 save.await.unwrap();
27431
27432 color_request_handle.next().await.unwrap();
27433 cx.run_until_parked();
27434 assert_eq!(
27435 2,
27436 requests_made.load(atomic::Ordering::Acquire),
27437 "Should query for colors once per save (deduplicated) and once per formatting after save"
27438 );
27439
27440 drop(editor);
27441 let close = workspace
27442 .update(cx, |workspace, window, cx| {
27443 workspace.active_pane().update(cx, |pane, cx| {
27444 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27445 })
27446 })
27447 .unwrap();
27448 close.await.unwrap();
27449 let close = workspace
27450 .update(cx, |workspace, window, cx| {
27451 workspace.active_pane().update(cx, |pane, cx| {
27452 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27453 })
27454 })
27455 .unwrap();
27456 close.await.unwrap();
27457 assert_eq!(
27458 2,
27459 requests_made.load(atomic::Ordering::Acquire),
27460 "After saving and closing all editors, no extra requests should be made"
27461 );
27462 workspace
27463 .update(cx, |workspace, _, cx| {
27464 assert!(
27465 workspace.active_item(cx).is_none(),
27466 "Should close all editors"
27467 )
27468 })
27469 .unwrap();
27470
27471 workspace
27472 .update(cx, |workspace, window, cx| {
27473 workspace.active_pane().update(cx, |pane, cx| {
27474 pane.navigate_backward(&workspace::GoBack, window, cx);
27475 })
27476 })
27477 .unwrap();
27478 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27479 cx.run_until_parked();
27480 let editor = workspace
27481 .update(cx, |workspace, _, cx| {
27482 workspace
27483 .active_item(cx)
27484 .expect("Should have reopened the editor again after navigating back")
27485 .downcast::<Editor>()
27486 .expect("Should be an editor")
27487 })
27488 .unwrap();
27489
27490 assert_eq!(
27491 2,
27492 requests_made.load(atomic::Ordering::Acquire),
27493 "Cache should be reused on buffer close and reopen"
27494 );
27495 editor.update(cx, |editor, cx| {
27496 assert_eq!(
27497 vec![expected_color],
27498 extract_color_inlays(editor, cx),
27499 "Should have an initial inlay"
27500 );
27501 });
27502
27503 drop(color_request_handle);
27504 let closure_requests_made = Arc::clone(&requests_made);
27505 let mut empty_color_request_handle = fake_language_server
27506 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27507 let requests_made = Arc::clone(&closure_requests_made);
27508 async move {
27509 assert_eq!(
27510 params.text_document.uri,
27511 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27512 );
27513 requests_made.fetch_add(1, atomic::Ordering::Release);
27514 Ok(Vec::new())
27515 }
27516 });
27517 let save = editor.update_in(cx, |editor, window, cx| {
27518 editor.move_to_end(&MoveToEnd, window, cx);
27519 editor.handle_input("dirty_again", window, cx);
27520 editor.save(
27521 SaveOptions {
27522 format: false,
27523 autosave: true,
27524 },
27525 project.clone(),
27526 window,
27527 cx,
27528 )
27529 });
27530 save.await.unwrap();
27531
27532 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27533 empty_color_request_handle.next().await.unwrap();
27534 cx.run_until_parked();
27535 assert_eq!(
27536 3,
27537 requests_made.load(atomic::Ordering::Acquire),
27538 "Should query for colors once per save only, as formatting was not requested"
27539 );
27540 editor.update(cx, |editor, cx| {
27541 assert_eq!(
27542 Vec::<Rgba>::new(),
27543 extract_color_inlays(editor, cx),
27544 "Should clear all colors when the server returns an empty response"
27545 );
27546 });
27547}
27548
27549#[gpui::test]
27550async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27551 init_test(cx, |_| {});
27552 let (editor, cx) = cx.add_window_view(Editor::single_line);
27553 editor.update_in(cx, |editor, window, cx| {
27554 editor.set_text("oops\n\nwow\n", window, cx)
27555 });
27556 cx.run_until_parked();
27557 editor.update(cx, |editor, cx| {
27558 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27559 });
27560 editor.update(cx, |editor, cx| {
27561 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27562 });
27563 cx.run_until_parked();
27564 editor.update(cx, |editor, cx| {
27565 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27566 });
27567}
27568
27569#[gpui::test]
27570async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27571 init_test(cx, |_| {});
27572
27573 cx.update(|cx| {
27574 register_project_item::<Editor>(cx);
27575 });
27576
27577 let fs = FakeFs::new(cx.executor());
27578 fs.insert_tree("/root1", json!({})).await;
27579 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27580 .await;
27581
27582 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27583 let (workspace, cx) =
27584 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27585
27586 let worktree_id = project.update(cx, |project, cx| {
27587 project.worktrees(cx).next().unwrap().read(cx).id()
27588 });
27589
27590 let handle = workspace
27591 .update_in(cx, |workspace, window, cx| {
27592 let project_path = (worktree_id, rel_path("one.pdf"));
27593 workspace.open_path(project_path, None, true, window, cx)
27594 })
27595 .await
27596 .unwrap();
27597
27598 assert_eq!(
27599 handle.to_any_view().entity_type(),
27600 TypeId::of::<InvalidItemView>()
27601 );
27602}
27603
27604#[gpui::test]
27605async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27606 init_test(cx, |_| {});
27607
27608 let language = Arc::new(Language::new(
27609 LanguageConfig::default(),
27610 Some(tree_sitter_rust::LANGUAGE.into()),
27611 ));
27612
27613 // Test hierarchical sibling navigation
27614 let text = r#"
27615 fn outer() {
27616 if condition {
27617 let a = 1;
27618 }
27619 let b = 2;
27620 }
27621
27622 fn another() {
27623 let c = 3;
27624 }
27625 "#;
27626
27627 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27628 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27629 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27630
27631 // Wait for parsing to complete
27632 editor
27633 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27634 .await;
27635
27636 editor.update_in(cx, |editor, window, cx| {
27637 // Start by selecting "let a = 1;" inside the if block
27638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27639 s.select_display_ranges([
27640 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27641 ]);
27642 });
27643
27644 let initial_selection = editor
27645 .selections
27646 .display_ranges(&editor.display_snapshot(cx));
27647 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27648
27649 // Test select next sibling - should move up levels to find the next sibling
27650 // Since "let a = 1;" has no siblings in the if block, it should move up
27651 // to find "let b = 2;" which is a sibling of the if block
27652 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27653 let next_selection = editor
27654 .selections
27655 .display_ranges(&editor.display_snapshot(cx));
27656
27657 // Should have a selection and it should be different from the initial
27658 assert_eq!(
27659 next_selection.len(),
27660 1,
27661 "Should have one selection after next"
27662 );
27663 assert_ne!(
27664 next_selection[0], initial_selection[0],
27665 "Next sibling selection should be different"
27666 );
27667
27668 // Test hierarchical navigation by going to the end of the current function
27669 // and trying to navigate to the next function
27670 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27671 s.select_display_ranges([
27672 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27673 ]);
27674 });
27675
27676 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27677 let function_next_selection = editor
27678 .selections
27679 .display_ranges(&editor.display_snapshot(cx));
27680
27681 // Should move to the next function
27682 assert_eq!(
27683 function_next_selection.len(),
27684 1,
27685 "Should have one selection after function next"
27686 );
27687
27688 // Test select previous sibling navigation
27689 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27690 let prev_selection = editor
27691 .selections
27692 .display_ranges(&editor.display_snapshot(cx));
27693
27694 // Should have a selection and it should be different
27695 assert_eq!(
27696 prev_selection.len(),
27697 1,
27698 "Should have one selection after prev"
27699 );
27700 assert_ne!(
27701 prev_selection[0], function_next_selection[0],
27702 "Previous sibling selection should be different from next"
27703 );
27704 });
27705}
27706
27707#[gpui::test]
27708async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27709 init_test(cx, |_| {});
27710
27711 let mut cx = EditorTestContext::new(cx).await;
27712 cx.set_state(
27713 "let ˇvariable = 42;
27714let another = variable + 1;
27715let result = variable * 2;",
27716 );
27717
27718 // Set up document highlights manually (simulating LSP response)
27719 cx.update_editor(|editor, _window, cx| {
27720 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27721
27722 // Create highlights for "variable" occurrences
27723 let highlight_ranges = [
27724 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27725 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27726 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27727 ];
27728
27729 let anchor_ranges: Vec<_> = highlight_ranges
27730 .iter()
27731 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27732 .collect();
27733
27734 editor.highlight_background::<DocumentHighlightRead>(
27735 &anchor_ranges,
27736 |_, theme| theme.colors().editor_document_highlight_read_background,
27737 cx,
27738 );
27739 });
27740
27741 // Go to next highlight - should move to second "variable"
27742 cx.update_editor(|editor, window, cx| {
27743 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27744 });
27745 cx.assert_editor_state(
27746 "let variable = 42;
27747let another = ˇvariable + 1;
27748let result = variable * 2;",
27749 );
27750
27751 // Go to next highlight - should move to third "variable"
27752 cx.update_editor(|editor, window, cx| {
27753 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27754 });
27755 cx.assert_editor_state(
27756 "let variable = 42;
27757let another = variable + 1;
27758let result = ˇvariable * 2;",
27759 );
27760
27761 // Go to next highlight - should stay at third "variable" (no wrap-around)
27762 cx.update_editor(|editor, window, cx| {
27763 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27764 });
27765 cx.assert_editor_state(
27766 "let variable = 42;
27767let another = variable + 1;
27768let result = ˇvariable * 2;",
27769 );
27770
27771 // Now test going backwards from third position
27772 cx.update_editor(|editor, window, cx| {
27773 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27774 });
27775 cx.assert_editor_state(
27776 "let variable = 42;
27777let another = ˇvariable + 1;
27778let result = variable * 2;",
27779 );
27780
27781 // Go to previous highlight - should move to first "variable"
27782 cx.update_editor(|editor, window, cx| {
27783 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27784 });
27785 cx.assert_editor_state(
27786 "let ˇvariable = 42;
27787let another = variable + 1;
27788let result = variable * 2;",
27789 );
27790
27791 // Go to previous highlight - should stay on first "variable"
27792 cx.update_editor(|editor, window, cx| {
27793 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27794 });
27795 cx.assert_editor_state(
27796 "let ˇvariable = 42;
27797let another = variable + 1;
27798let result = variable * 2;",
27799 );
27800}
27801
27802#[gpui::test]
27803async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27804 cx: &mut gpui::TestAppContext,
27805) {
27806 init_test(cx, |_| {});
27807
27808 let url = "https://zed.dev";
27809
27810 let markdown_language = Arc::new(Language::new(
27811 LanguageConfig {
27812 name: "Markdown".into(),
27813 ..LanguageConfig::default()
27814 },
27815 None,
27816 ));
27817
27818 let mut cx = EditorTestContext::new(cx).await;
27819 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27820 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27821
27822 cx.update_editor(|editor, window, cx| {
27823 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27824 editor.paste(&Paste, window, cx);
27825 });
27826
27827 cx.assert_editor_state(&format!(
27828 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27829 ));
27830}
27831
27832#[gpui::test]
27833async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
27834 init_test(cx, |_| {});
27835
27836 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
27837 let mut cx = EditorTestContext::new(cx).await;
27838
27839 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27840
27841 // Case 1: Test if adding a character with multi cursors preserves nested list indents
27842 cx.set_state(&indoc! {"
27843 - [ ] Item 1
27844 - [ ] Item 1.a
27845 - [ˇ] Item 2
27846 - [ˇ] Item 2.a
27847 - [ˇ] Item 2.b
27848 "
27849 });
27850 cx.update_editor(|editor, window, cx| {
27851 editor.handle_input("x", window, cx);
27852 });
27853 cx.run_until_parked();
27854 cx.assert_editor_state(indoc! {"
27855 - [ ] Item 1
27856 - [ ] Item 1.a
27857 - [xˇ] Item 2
27858 - [xˇ] Item 2.a
27859 - [xˇ] Item 2.b
27860 "
27861 });
27862
27863 // Case 2: Test adding new line after nested list preserves indent of previous line
27864 cx.set_state(&indoc! {"
27865 - [ ] Item 1
27866 - [ ] Item 1.a
27867 - [x] Item 2
27868 - [x] Item 2.a
27869 - [x] Item 2.bˇ"
27870 });
27871 cx.update_editor(|editor, window, cx| {
27872 editor.newline(&Newline, window, cx);
27873 });
27874 cx.assert_editor_state(indoc! {"
27875 - [ ] Item 1
27876 - [ ] Item 1.a
27877 - [x] Item 2
27878 - [x] Item 2.a
27879 - [x] Item 2.b
27880 ˇ"
27881 });
27882
27883 // Case 3: Test adding a new nested list item preserves indent
27884 cx.set_state(&indoc! {"
27885 - [ ] Item 1
27886 - [ ] Item 1.a
27887 - [x] Item 2
27888 - [x] Item 2.a
27889 - [x] Item 2.b
27890 ˇ"
27891 });
27892 cx.update_editor(|editor, window, cx| {
27893 editor.handle_input("-", window, cx);
27894 });
27895 cx.run_until_parked();
27896 cx.assert_editor_state(indoc! {"
27897 - [ ] Item 1
27898 - [ ] Item 1.a
27899 - [x] Item 2
27900 - [x] Item 2.a
27901 - [x] Item 2.b
27902 -ˇ"
27903 });
27904 cx.update_editor(|editor, window, cx| {
27905 editor.handle_input(" [x] Item 2.c", window, cx);
27906 });
27907 cx.run_until_parked();
27908 cx.assert_editor_state(indoc! {"
27909 - [ ] Item 1
27910 - [ ] Item 1.a
27911 - [x] Item 2
27912 - [x] Item 2.a
27913 - [x] Item 2.b
27914 - [x] Item 2.cˇ"
27915 });
27916
27917 // Case 4: Test adding new line after nested ordered list preserves indent of previous line
27918 cx.set_state(indoc! {"
27919 1. Item 1
27920 1. Item 1.a
27921 2. Item 2
27922 1. Item 2.a
27923 2. Item 2.bˇ"
27924 });
27925 cx.update_editor(|editor, window, cx| {
27926 editor.newline(&Newline, window, cx);
27927 });
27928 cx.assert_editor_state(indoc! {"
27929 1. Item 1
27930 1. Item 1.a
27931 2. Item 2
27932 1. Item 2.a
27933 2. Item 2.b
27934 ˇ"
27935 });
27936
27937 // Case 5: Adding new ordered list item preserves indent
27938 cx.set_state(indoc! {"
27939 1. Item 1
27940 1. Item 1.a
27941 2. Item 2
27942 1. Item 2.a
27943 2. Item 2.b
27944 ˇ"
27945 });
27946 cx.update_editor(|editor, window, cx| {
27947 editor.handle_input("3", window, cx);
27948 });
27949 cx.run_until_parked();
27950 cx.assert_editor_state(indoc! {"
27951 1. Item 1
27952 1. Item 1.a
27953 2. Item 2
27954 1. Item 2.a
27955 2. Item 2.b
27956 3ˇ"
27957 });
27958 cx.update_editor(|editor, window, cx| {
27959 editor.handle_input(".", window, cx);
27960 });
27961 cx.run_until_parked();
27962 cx.assert_editor_state(indoc! {"
27963 1. Item 1
27964 1. Item 1.a
27965 2. Item 2
27966 1. Item 2.a
27967 2. Item 2.b
27968 3.ˇ"
27969 });
27970 cx.update_editor(|editor, window, cx| {
27971 editor.handle_input(" Item 2.c", window, cx);
27972 });
27973 cx.run_until_parked();
27974 cx.assert_editor_state(indoc! {"
27975 1. Item 1
27976 1. Item 1.a
27977 2. Item 2
27978 1. Item 2.a
27979 2. Item 2.b
27980 3. Item 2.cˇ"
27981 });
27982
27983 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
27984 cx.set_state(indoc! {"
27985 - Item 1
27986 - Item 1.a
27987 - Item 1.a
27988 ˇ"});
27989 cx.update_editor(|editor, window, cx| {
27990 editor.handle_input("-", window, cx);
27991 });
27992 cx.run_until_parked();
27993 cx.assert_editor_state(indoc! {"
27994 - Item 1
27995 - Item 1.a
27996 - Item 1.a
27997 -ˇ"});
27998
27999 // Case 7: Test blockquote newline preserves something
28000 cx.set_state(indoc! {"
28001 > Item 1ˇ"
28002 });
28003 cx.update_editor(|editor, window, cx| {
28004 editor.newline(&Newline, window, cx);
28005 });
28006 cx.assert_editor_state(indoc! {"
28007 > Item 1
28008 ˇ"
28009 });
28010}
28011
28012#[gpui::test]
28013async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28014 cx: &mut gpui::TestAppContext,
28015) {
28016 init_test(cx, |_| {});
28017
28018 let url = "https://zed.dev";
28019
28020 let markdown_language = Arc::new(Language::new(
28021 LanguageConfig {
28022 name: "Markdown".into(),
28023 ..LanguageConfig::default()
28024 },
28025 None,
28026 ));
28027
28028 let mut cx = EditorTestContext::new(cx).await;
28029 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28030 cx.set_state(&format!(
28031 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28032 ));
28033
28034 cx.update_editor(|editor, window, cx| {
28035 editor.copy(&Copy, window, cx);
28036 });
28037
28038 cx.set_state(&format!(
28039 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28040 ));
28041
28042 cx.update_editor(|editor, window, cx| {
28043 editor.paste(&Paste, window, cx);
28044 });
28045
28046 cx.assert_editor_state(&format!(
28047 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28048 ));
28049}
28050
28051#[gpui::test]
28052async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28053 cx: &mut gpui::TestAppContext,
28054) {
28055 init_test(cx, |_| {});
28056
28057 let url = "https://zed.dev";
28058
28059 let markdown_language = Arc::new(Language::new(
28060 LanguageConfig {
28061 name: "Markdown".into(),
28062 ..LanguageConfig::default()
28063 },
28064 None,
28065 ));
28066
28067 let mut cx = EditorTestContext::new(cx).await;
28068 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28069 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28070
28071 cx.update_editor(|editor, window, cx| {
28072 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28073 editor.paste(&Paste, window, cx);
28074 });
28075
28076 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28077}
28078
28079#[gpui::test]
28080async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28081 cx: &mut gpui::TestAppContext,
28082) {
28083 init_test(cx, |_| {});
28084
28085 let text = "Awesome";
28086
28087 let markdown_language = Arc::new(Language::new(
28088 LanguageConfig {
28089 name: "Markdown".into(),
28090 ..LanguageConfig::default()
28091 },
28092 None,
28093 ));
28094
28095 let mut cx = EditorTestContext::new(cx).await;
28096 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28097 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28098
28099 cx.update_editor(|editor, window, cx| {
28100 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28101 editor.paste(&Paste, window, cx);
28102 });
28103
28104 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28105}
28106
28107#[gpui::test]
28108async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28109 cx: &mut gpui::TestAppContext,
28110) {
28111 init_test(cx, |_| {});
28112
28113 let url = "https://zed.dev";
28114
28115 let markdown_language = Arc::new(Language::new(
28116 LanguageConfig {
28117 name: "Rust".into(),
28118 ..LanguageConfig::default()
28119 },
28120 None,
28121 ));
28122
28123 let mut cx = EditorTestContext::new(cx).await;
28124 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28125 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28126
28127 cx.update_editor(|editor, window, cx| {
28128 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28129 editor.paste(&Paste, window, cx);
28130 });
28131
28132 cx.assert_editor_state(&format!(
28133 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28134 ));
28135}
28136
28137#[gpui::test]
28138async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28139 cx: &mut TestAppContext,
28140) {
28141 init_test(cx, |_| {});
28142
28143 let url = "https://zed.dev";
28144
28145 let markdown_language = Arc::new(Language::new(
28146 LanguageConfig {
28147 name: "Markdown".into(),
28148 ..LanguageConfig::default()
28149 },
28150 None,
28151 ));
28152
28153 let (editor, cx) = cx.add_window_view(|window, cx| {
28154 let multi_buffer = MultiBuffer::build_multi(
28155 [
28156 ("this will embed -> link", vec![Point::row_range(0..1)]),
28157 ("this will replace -> link", vec![Point::row_range(0..1)]),
28158 ],
28159 cx,
28160 );
28161 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28162 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28163 s.select_ranges(vec![
28164 Point::new(0, 19)..Point::new(0, 23),
28165 Point::new(1, 21)..Point::new(1, 25),
28166 ])
28167 });
28168 let first_buffer_id = multi_buffer
28169 .read(cx)
28170 .excerpt_buffer_ids()
28171 .into_iter()
28172 .next()
28173 .unwrap();
28174 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28175 first_buffer.update(cx, |buffer, cx| {
28176 buffer.set_language(Some(markdown_language.clone()), cx);
28177 });
28178
28179 editor
28180 });
28181 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28182
28183 cx.update_editor(|editor, window, cx| {
28184 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28185 editor.paste(&Paste, window, cx);
28186 });
28187
28188 cx.assert_editor_state(&format!(
28189 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28190 ));
28191}
28192
28193#[gpui::test]
28194async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28195 init_test(cx, |_| {});
28196
28197 let fs = FakeFs::new(cx.executor());
28198 fs.insert_tree(
28199 path!("/project"),
28200 json!({
28201 "first.rs": "# First Document\nSome content here.",
28202 "second.rs": "Plain text content for second file.",
28203 }),
28204 )
28205 .await;
28206
28207 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28208 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28209 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28210
28211 let language = rust_lang();
28212 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28213 language_registry.add(language.clone());
28214 let mut fake_servers = language_registry.register_fake_lsp(
28215 "Rust",
28216 FakeLspAdapter {
28217 ..FakeLspAdapter::default()
28218 },
28219 );
28220
28221 let buffer1 = project
28222 .update(cx, |project, cx| {
28223 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28224 })
28225 .await
28226 .unwrap();
28227 let buffer2 = project
28228 .update(cx, |project, cx| {
28229 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28230 })
28231 .await
28232 .unwrap();
28233
28234 let multi_buffer = cx.new(|cx| {
28235 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28236 multi_buffer.set_excerpts_for_path(
28237 PathKey::for_buffer(&buffer1, cx),
28238 buffer1.clone(),
28239 [Point::zero()..buffer1.read(cx).max_point()],
28240 3,
28241 cx,
28242 );
28243 multi_buffer.set_excerpts_for_path(
28244 PathKey::for_buffer(&buffer2, cx),
28245 buffer2.clone(),
28246 [Point::zero()..buffer1.read(cx).max_point()],
28247 3,
28248 cx,
28249 );
28250 multi_buffer
28251 });
28252
28253 let (editor, cx) = cx.add_window_view(|window, cx| {
28254 Editor::new(
28255 EditorMode::full(),
28256 multi_buffer,
28257 Some(project.clone()),
28258 window,
28259 cx,
28260 )
28261 });
28262
28263 let fake_language_server = fake_servers.next().await.unwrap();
28264
28265 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28266
28267 let save = editor.update_in(cx, |editor, window, cx| {
28268 assert!(editor.is_dirty(cx));
28269
28270 editor.save(
28271 SaveOptions {
28272 format: true,
28273 autosave: true,
28274 },
28275 project,
28276 window,
28277 cx,
28278 )
28279 });
28280 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28281 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28282 let mut done_edit_rx = Some(done_edit_rx);
28283 let mut start_edit_tx = Some(start_edit_tx);
28284
28285 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28286 start_edit_tx.take().unwrap().send(()).unwrap();
28287 let done_edit_rx = done_edit_rx.take().unwrap();
28288 async move {
28289 done_edit_rx.await.unwrap();
28290 Ok(None)
28291 }
28292 });
28293
28294 start_edit_rx.await.unwrap();
28295 buffer2
28296 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28297 .unwrap();
28298
28299 done_edit_tx.send(()).unwrap();
28300
28301 save.await.unwrap();
28302 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28303}
28304
28305#[track_caller]
28306fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28307 editor
28308 .all_inlays(cx)
28309 .into_iter()
28310 .filter_map(|inlay| inlay.get_color())
28311 .map(Rgba::from)
28312 .collect()
28313}
28314
28315#[gpui::test]
28316fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28317 init_test(cx, |_| {});
28318
28319 let editor = cx.add_window(|window, cx| {
28320 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28321 build_editor(buffer, window, cx)
28322 });
28323
28324 editor
28325 .update(cx, |editor, window, cx| {
28326 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28327 s.select_display_ranges([
28328 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28329 ])
28330 });
28331
28332 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28333
28334 assert_eq!(
28335 editor.display_text(cx),
28336 "line1\nline2\nline2",
28337 "Duplicating last line upward should create duplicate above, not on same line"
28338 );
28339
28340 assert_eq!(
28341 editor
28342 .selections
28343 .display_ranges(&editor.display_snapshot(cx)),
28344 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28345 "Selection should move to the duplicated line"
28346 );
28347 })
28348 .unwrap();
28349}
28350
28351#[gpui::test]
28352async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28353 init_test(cx, |_| {});
28354
28355 let mut cx = EditorTestContext::new(cx).await;
28356
28357 cx.set_state("line1\nline2ˇ");
28358
28359 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28360
28361 let clipboard_text = cx
28362 .read_from_clipboard()
28363 .and_then(|item| item.text().as_deref().map(str::to_string));
28364
28365 assert_eq!(
28366 clipboard_text,
28367 Some("line2\n".to_string()),
28368 "Copying a line without trailing newline should include a newline"
28369 );
28370
28371 cx.set_state("line1\nˇ");
28372
28373 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28374
28375 cx.assert_editor_state("line1\nline2\nˇ");
28376}
28377
28378#[gpui::test]
28379async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28380 init_test(cx, |_| {});
28381
28382 let mut cx = EditorTestContext::new(cx).await;
28383
28384 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28385
28386 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28387
28388 let clipboard_text = cx
28389 .read_from_clipboard()
28390 .and_then(|item| item.text().as_deref().map(str::to_string));
28391
28392 assert_eq!(
28393 clipboard_text,
28394 Some("line1\nline2\nline3\n".to_string()),
28395 "Copying multiple lines should include a single newline between lines"
28396 );
28397
28398 cx.set_state("lineA\nˇ");
28399
28400 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28401
28402 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28403}
28404
28405#[gpui::test]
28406async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28407 init_test(cx, |_| {});
28408
28409 let mut cx = EditorTestContext::new(cx).await;
28410
28411 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28412
28413 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28414
28415 let clipboard_text = cx
28416 .read_from_clipboard()
28417 .and_then(|item| item.text().as_deref().map(str::to_string));
28418
28419 assert_eq!(
28420 clipboard_text,
28421 Some("line1\nline2\nline3\n".to_string()),
28422 "Copying multiple lines should include a single newline between lines"
28423 );
28424
28425 cx.set_state("lineA\nˇ");
28426
28427 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28428
28429 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28430}
28431
28432#[gpui::test]
28433async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28434 init_test(cx, |_| {});
28435
28436 let mut cx = EditorTestContext::new(cx).await;
28437
28438 cx.set_state("line1\nline2ˇ");
28439 cx.update_editor(|e, window, cx| {
28440 e.set_mode(EditorMode::SingleLine);
28441 assert!(e.key_context(window, cx).contains("end_of_input"));
28442 });
28443 cx.set_state("ˇline1\nline2");
28444 cx.update_editor(|e, window, cx| {
28445 assert!(!e.key_context(window, cx).contains("end_of_input"));
28446 });
28447 cx.set_state("line1ˇ\nline2");
28448 cx.update_editor(|e, window, cx| {
28449 assert!(!e.key_context(window, cx).contains("end_of_input"));
28450 });
28451}
28452
28453#[gpui::test]
28454async fn test_sticky_scroll(cx: &mut TestAppContext) {
28455 init_test(cx, |_| {});
28456 let mut cx = EditorTestContext::new(cx).await;
28457
28458 let buffer = indoc! {"
28459 ˇfn foo() {
28460 let abc = 123;
28461 }
28462 struct Bar;
28463 impl Bar {
28464 fn new() -> Self {
28465 Self
28466 }
28467 }
28468 fn baz() {
28469 }
28470 "};
28471 cx.set_state(&buffer);
28472
28473 cx.update_editor(|e, _, cx| {
28474 e.buffer()
28475 .read(cx)
28476 .as_singleton()
28477 .unwrap()
28478 .update(cx, |buffer, cx| {
28479 buffer.set_language(Some(rust_lang()), cx);
28480 })
28481 });
28482
28483 let mut sticky_headers = |offset: ScrollOffset| {
28484 cx.update_editor(|e, window, cx| {
28485 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28486 let style = e.style(cx).clone();
28487 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28488 .into_iter()
28489 .map(
28490 |StickyHeader {
28491 start_point,
28492 offset,
28493 ..
28494 }| { (start_point, offset) },
28495 )
28496 .collect::<Vec<_>>()
28497 })
28498 };
28499
28500 let fn_foo = Point { row: 0, column: 0 };
28501 let impl_bar = Point { row: 4, column: 0 };
28502 let fn_new = Point { row: 5, column: 4 };
28503
28504 assert_eq!(sticky_headers(0.0), vec![]);
28505 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28506 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28507 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28508 assert_eq!(sticky_headers(2.0), vec![]);
28509 assert_eq!(sticky_headers(2.5), vec![]);
28510 assert_eq!(sticky_headers(3.0), vec![]);
28511 assert_eq!(sticky_headers(3.5), vec![]);
28512 assert_eq!(sticky_headers(4.0), vec![]);
28513 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28514 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28515 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28516 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28517 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28518 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28519 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28520 assert_eq!(sticky_headers(8.0), vec![]);
28521 assert_eq!(sticky_headers(8.5), vec![]);
28522 assert_eq!(sticky_headers(9.0), vec![]);
28523 assert_eq!(sticky_headers(9.5), vec![]);
28524 assert_eq!(sticky_headers(10.0), vec![]);
28525}
28526
28527#[gpui::test]
28528async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
28529 init_test(cx, |_| {});
28530 cx.update(|cx| {
28531 SettingsStore::update_global(cx, |store, cx| {
28532 store.update_user_settings(cx, |settings| {
28533 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
28534 enabled: Some(true),
28535 })
28536 });
28537 });
28538 });
28539 let mut cx = EditorTestContext::new(cx).await;
28540
28541 let line_height = cx.update_editor(|editor, window, cx| {
28542 editor
28543 .style(cx)
28544 .text
28545 .line_height_in_pixels(window.rem_size())
28546 });
28547
28548 let buffer = indoc! {"
28549 ˇfn foo() {
28550 let abc = 123;
28551 }
28552 struct Bar;
28553 impl Bar {
28554 fn new() -> Self {
28555 Self
28556 }
28557 }
28558 fn baz() {
28559 }
28560 "};
28561 cx.set_state(&buffer);
28562
28563 cx.update_editor(|e, _, cx| {
28564 e.buffer()
28565 .read(cx)
28566 .as_singleton()
28567 .unwrap()
28568 .update(cx, |buffer, cx| {
28569 buffer.set_language(Some(rust_lang()), cx);
28570 })
28571 });
28572
28573 let fn_foo = || empty_range(0, 0);
28574 let impl_bar = || empty_range(4, 0);
28575 let fn_new = || empty_range(5, 4);
28576
28577 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
28578 cx.update_editor(|e, window, cx| {
28579 e.scroll(
28580 gpui::Point {
28581 x: 0.,
28582 y: scroll_offset,
28583 },
28584 None,
28585 window,
28586 cx,
28587 );
28588 });
28589 cx.simulate_click(
28590 gpui::Point {
28591 x: px(0.),
28592 y: click_offset as f32 * line_height,
28593 },
28594 Modifiers::none(),
28595 );
28596 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
28597 };
28598
28599 assert_eq!(
28600 scroll_and_click(
28601 4.5, // impl Bar is halfway off the screen
28602 0.0 // click top of screen
28603 ),
28604 // scrolled to impl Bar
28605 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28606 );
28607
28608 assert_eq!(
28609 scroll_and_click(
28610 4.5, // impl Bar is halfway off the screen
28611 0.25 // click middle of impl Bar
28612 ),
28613 // scrolled to impl Bar
28614 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28615 );
28616
28617 assert_eq!(
28618 scroll_and_click(
28619 4.5, // impl Bar is halfway off the screen
28620 1.5 // click below impl Bar (e.g. fn new())
28621 ),
28622 // scrolled to fn new() - this is below the impl Bar header which has persisted
28623 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28624 );
28625
28626 assert_eq!(
28627 scroll_and_click(
28628 5.5, // fn new is halfway underneath impl Bar
28629 0.75 // click on the overlap of impl Bar and fn new()
28630 ),
28631 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28632 );
28633
28634 assert_eq!(
28635 scroll_and_click(
28636 5.5, // fn new is halfway underneath impl Bar
28637 1.25 // click on the visible part of fn new()
28638 ),
28639 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28640 );
28641
28642 assert_eq!(
28643 scroll_and_click(
28644 1.5, // fn foo is halfway off the screen
28645 0.0 // click top of screen
28646 ),
28647 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28648 );
28649
28650 assert_eq!(
28651 scroll_and_click(
28652 1.5, // fn foo is halfway off the screen
28653 0.75 // click visible part of let abc...
28654 )
28655 .0,
28656 // no change in scroll
28657 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28658 (gpui::Point { x: 0., y: 1.5 })
28659 );
28660}
28661
28662#[gpui::test]
28663async fn test_next_prev_reference(cx: &mut TestAppContext) {
28664 const CYCLE_POSITIONS: &[&'static str] = &[
28665 indoc! {"
28666 fn foo() {
28667 let ˇabc = 123;
28668 let x = abc + 1;
28669 let y = abc + 2;
28670 let z = abc + 2;
28671 }
28672 "},
28673 indoc! {"
28674 fn foo() {
28675 let abc = 123;
28676 let x = ˇabc + 1;
28677 let y = abc + 2;
28678 let z = abc + 2;
28679 }
28680 "},
28681 indoc! {"
28682 fn foo() {
28683 let abc = 123;
28684 let x = abc + 1;
28685 let y = ˇabc + 2;
28686 let z = abc + 2;
28687 }
28688 "},
28689 indoc! {"
28690 fn foo() {
28691 let abc = 123;
28692 let x = abc + 1;
28693 let y = abc + 2;
28694 let z = ˇabc + 2;
28695 }
28696 "},
28697 ];
28698
28699 init_test(cx, |_| {});
28700
28701 let mut cx = EditorLspTestContext::new_rust(
28702 lsp::ServerCapabilities {
28703 references_provider: Some(lsp::OneOf::Left(true)),
28704 ..Default::default()
28705 },
28706 cx,
28707 )
28708 .await;
28709
28710 // importantly, the cursor is in the middle
28711 cx.set_state(indoc! {"
28712 fn foo() {
28713 let aˇbc = 123;
28714 let x = abc + 1;
28715 let y = abc + 2;
28716 let z = abc + 2;
28717 }
28718 "});
28719
28720 let reference_ranges = [
28721 lsp::Position::new(1, 8),
28722 lsp::Position::new(2, 12),
28723 lsp::Position::new(3, 12),
28724 lsp::Position::new(4, 12),
28725 ]
28726 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28727
28728 cx.lsp
28729 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
28730 Ok(Some(
28731 reference_ranges
28732 .map(|range| lsp::Location {
28733 uri: params.text_document_position.text_document.uri.clone(),
28734 range,
28735 })
28736 .to_vec(),
28737 ))
28738 });
28739
28740 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
28741 cx.update_editor(|editor, window, cx| {
28742 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
28743 })
28744 .unwrap()
28745 .await
28746 .unwrap()
28747 };
28748
28749 _move(Direction::Next, 1, &mut cx).await;
28750 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28751
28752 _move(Direction::Next, 1, &mut cx).await;
28753 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28754
28755 _move(Direction::Next, 1, &mut cx).await;
28756 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28757
28758 // loops back to the start
28759 _move(Direction::Next, 1, &mut cx).await;
28760 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28761
28762 // loops back to the end
28763 _move(Direction::Prev, 1, &mut cx).await;
28764 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28765
28766 _move(Direction::Prev, 1, &mut cx).await;
28767 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28768
28769 _move(Direction::Prev, 1, &mut cx).await;
28770 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28771
28772 _move(Direction::Prev, 1, &mut cx).await;
28773 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28774
28775 _move(Direction::Next, 3, &mut cx).await;
28776 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28777
28778 _move(Direction::Prev, 2, &mut cx).await;
28779 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28780}
28781
28782#[gpui::test]
28783async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
28784 init_test(cx, |_| {});
28785
28786 let (editor, cx) = cx.add_window_view(|window, cx| {
28787 let multi_buffer = MultiBuffer::build_multi(
28788 [
28789 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28790 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28791 ],
28792 cx,
28793 );
28794 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28795 });
28796
28797 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28798 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28799
28800 cx.assert_excerpts_with_selections(indoc! {"
28801 [EXCERPT]
28802 ˇ1
28803 2
28804 3
28805 [EXCERPT]
28806 1
28807 2
28808 3
28809 "});
28810
28811 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28812 cx.update_editor(|editor, window, cx| {
28813 editor.change_selections(None.into(), window, cx, |s| {
28814 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28815 });
28816 });
28817 cx.assert_excerpts_with_selections(indoc! {"
28818 [EXCERPT]
28819 1
28820 2ˇ
28821 3
28822 [EXCERPT]
28823 1
28824 2
28825 3
28826 "});
28827
28828 cx.update_editor(|editor, window, cx| {
28829 editor
28830 .select_all_matches(&SelectAllMatches, window, cx)
28831 .unwrap();
28832 });
28833 cx.assert_excerpts_with_selections(indoc! {"
28834 [EXCERPT]
28835 1
28836 2ˇ
28837 3
28838 [EXCERPT]
28839 1
28840 2ˇ
28841 3
28842 "});
28843
28844 cx.update_editor(|editor, window, cx| {
28845 editor.handle_input("X", window, cx);
28846 });
28847 cx.assert_excerpts_with_selections(indoc! {"
28848 [EXCERPT]
28849 1
28850 Xˇ
28851 3
28852 [EXCERPT]
28853 1
28854 Xˇ
28855 3
28856 "});
28857
28858 // Scenario 2: Select "2", then fold second buffer before insertion
28859 cx.update_multibuffer(|mb, cx| {
28860 for buffer_id in buffer_ids.iter() {
28861 let buffer = mb.buffer(*buffer_id).unwrap();
28862 buffer.update(cx, |buffer, cx| {
28863 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28864 });
28865 }
28866 });
28867
28868 // Select "2" and select all matches
28869 cx.update_editor(|editor, window, cx| {
28870 editor.change_selections(None.into(), window, cx, |s| {
28871 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28872 });
28873 editor
28874 .select_all_matches(&SelectAllMatches, window, cx)
28875 .unwrap();
28876 });
28877
28878 // Fold second buffer - should remove selections from folded buffer
28879 cx.update_editor(|editor, _, cx| {
28880 editor.fold_buffer(buffer_ids[1], cx);
28881 });
28882 cx.assert_excerpts_with_selections(indoc! {"
28883 [EXCERPT]
28884 1
28885 2ˇ
28886 3
28887 [EXCERPT]
28888 [FOLDED]
28889 "});
28890
28891 // Insert text - should only affect first buffer
28892 cx.update_editor(|editor, window, cx| {
28893 editor.handle_input("Y", window, cx);
28894 });
28895 cx.update_editor(|editor, _, cx| {
28896 editor.unfold_buffer(buffer_ids[1], cx);
28897 });
28898 cx.assert_excerpts_with_selections(indoc! {"
28899 [EXCERPT]
28900 1
28901 Yˇ
28902 3
28903 [EXCERPT]
28904 1
28905 2
28906 3
28907 "});
28908
28909 // Scenario 3: Select "2", then fold first buffer before insertion
28910 cx.update_multibuffer(|mb, cx| {
28911 for buffer_id in buffer_ids.iter() {
28912 let buffer = mb.buffer(*buffer_id).unwrap();
28913 buffer.update(cx, |buffer, cx| {
28914 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28915 });
28916 }
28917 });
28918
28919 // Select "2" and select all matches
28920 cx.update_editor(|editor, window, cx| {
28921 editor.change_selections(None.into(), window, cx, |s| {
28922 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28923 });
28924 editor
28925 .select_all_matches(&SelectAllMatches, window, cx)
28926 .unwrap();
28927 });
28928
28929 // Fold first buffer - should remove selections from folded buffer
28930 cx.update_editor(|editor, _, cx| {
28931 editor.fold_buffer(buffer_ids[0], cx);
28932 });
28933 cx.assert_excerpts_with_selections(indoc! {"
28934 [EXCERPT]
28935 [FOLDED]
28936 [EXCERPT]
28937 1
28938 2ˇ
28939 3
28940 "});
28941
28942 // Insert text - should only affect second buffer
28943 cx.update_editor(|editor, window, cx| {
28944 editor.handle_input("Z", window, cx);
28945 });
28946 cx.update_editor(|editor, _, cx| {
28947 editor.unfold_buffer(buffer_ids[0], cx);
28948 });
28949 cx.assert_excerpts_with_selections(indoc! {"
28950 [EXCERPT]
28951 1
28952 2
28953 3
28954 [EXCERPT]
28955 1
28956 Zˇ
28957 3
28958 "});
28959
28960 // Test correct folded header is selected upon fold
28961 cx.update_editor(|editor, _, cx| {
28962 editor.fold_buffer(buffer_ids[0], cx);
28963 editor.fold_buffer(buffer_ids[1], cx);
28964 });
28965 cx.assert_excerpts_with_selections(indoc! {"
28966 [EXCERPT]
28967 [FOLDED]
28968 [EXCERPT]
28969 ˇ[FOLDED]
28970 "});
28971
28972 // Test selection inside folded buffer unfolds it on type
28973 cx.update_editor(|editor, window, cx| {
28974 editor.handle_input("W", window, cx);
28975 });
28976 cx.update_editor(|editor, _, cx| {
28977 editor.unfold_buffer(buffer_ids[0], cx);
28978 });
28979 cx.assert_excerpts_with_selections(indoc! {"
28980 [EXCERPT]
28981 1
28982 2
28983 3
28984 [EXCERPT]
28985 Wˇ1
28986 Z
28987 3
28988 "});
28989}
28990
28991#[gpui::test]
28992async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
28993 init_test(cx, |_| {});
28994 let mut leader_cx = EditorTestContext::new(cx).await;
28995
28996 let diff_base = indoc!(
28997 r#"
28998 one
28999 two
29000 three
29001 four
29002 five
29003 six
29004 "#
29005 );
29006
29007 let initial_state = indoc!(
29008 r#"
29009 ˇone
29010 two
29011 THREE
29012 four
29013 five
29014 six
29015 "#
29016 );
29017
29018 leader_cx.set_state(initial_state);
29019
29020 leader_cx.set_head_text(&diff_base);
29021 leader_cx.run_until_parked();
29022
29023 let follower = leader_cx.update_multibuffer(|leader, cx| {
29024 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29025 leader.set_all_diff_hunks_expanded(cx);
29026 leader.get_or_create_follower(cx)
29027 });
29028 follower.update(cx, |follower, cx| {
29029 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29030 follower.set_all_diff_hunks_expanded(cx);
29031 });
29032
29033 let follower_editor =
29034 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
29035 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
29036
29037 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
29038 cx.run_until_parked();
29039
29040 leader_cx.assert_editor_state(initial_state);
29041 follower_cx.assert_editor_state(indoc! {
29042 r#"
29043 ˇone
29044 two
29045 three
29046 four
29047 five
29048 six
29049 "#
29050 });
29051
29052 follower_cx.editor(|editor, _window, cx| {
29053 assert!(editor.read_only(cx));
29054 });
29055
29056 leader_cx.update_editor(|editor, _window, cx| {
29057 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
29058 });
29059 cx.run_until_parked();
29060
29061 leader_cx.assert_editor_state(indoc! {
29062 r#"
29063 ˇone
29064 two
29065 THREE
29066 four
29067 FIVE
29068 six
29069 "#
29070 });
29071
29072 follower_cx.assert_editor_state(indoc! {
29073 r#"
29074 ˇone
29075 two
29076 three
29077 four
29078 five
29079 six
29080 "#
29081 });
29082
29083 leader_cx.update_editor(|editor, _window, cx| {
29084 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
29085 });
29086 cx.run_until_parked();
29087
29088 leader_cx.assert_editor_state(indoc! {
29089 r#"
29090 ˇone
29091 two
29092 THREE
29093 four
29094 FIVE
29095 six
29096 SEVEN"#
29097 });
29098
29099 follower_cx.assert_editor_state(indoc! {
29100 r#"
29101 ˇone
29102 two
29103 three
29104 four
29105 five
29106 six
29107 "#
29108 });
29109
29110 leader_cx.update_editor(|editor, window, cx| {
29111 editor.move_down(&MoveDown, window, cx);
29112 editor.refresh_selected_text_highlights(true, window, cx);
29113 });
29114 leader_cx.run_until_parked();
29115}
29116
29117#[gpui::test]
29118async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
29119 init_test(cx, |_| {});
29120 let base_text = "base\n";
29121 let buffer_text = "buffer\n";
29122
29123 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
29124 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
29125
29126 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
29127 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
29128 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
29129 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
29130
29131 let leader = cx.new(|cx| {
29132 let mut leader = MultiBuffer::new(Capability::ReadWrite);
29133 leader.set_all_diff_hunks_expanded(cx);
29134 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29135 leader
29136 });
29137 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
29138 follower.update(cx, |follower, _| {
29139 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29140 });
29141
29142 leader.update(cx, |leader, cx| {
29143 leader.insert_excerpts_after(
29144 ExcerptId::min(),
29145 extra_buffer_2.clone(),
29146 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29147 cx,
29148 );
29149 leader.add_diff(extra_diff_2.clone(), cx);
29150
29151 leader.insert_excerpts_after(
29152 ExcerptId::min(),
29153 extra_buffer_1.clone(),
29154 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29155 cx,
29156 );
29157 leader.add_diff(extra_diff_1.clone(), cx);
29158
29159 leader.insert_excerpts_after(
29160 ExcerptId::min(),
29161 buffer1.clone(),
29162 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29163 cx,
29164 );
29165 leader.add_diff(diff1.clone(), cx);
29166 });
29167
29168 cx.run_until_parked();
29169 let mut cx = cx.add_empty_window();
29170
29171 let leader_editor = cx
29172 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
29173 let follower_editor = cx.new_window_entity(|window, cx| {
29174 Editor::for_multibuffer(follower.clone(), None, window, cx)
29175 });
29176
29177 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
29178 leader_cx.assert_editor_state(indoc! {"
29179 ˇbuffer
29180
29181 dummy text 1
29182
29183 dummy text 2
29184 "});
29185 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
29186 follower_cx.assert_editor_state(indoc! {"
29187 ˇbase
29188
29189
29190 "});
29191}
29192
29193#[gpui::test]
29194async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29195 init_test(cx, |_| {});
29196
29197 let (editor, cx) = cx.add_window_view(|window, cx| {
29198 let multi_buffer = MultiBuffer::build_multi(
29199 [
29200 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29201 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29202 ],
29203 cx,
29204 );
29205 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29206 });
29207
29208 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29209
29210 cx.assert_excerpts_with_selections(indoc! {"
29211 [EXCERPT]
29212 ˇ1
29213 2
29214 3
29215 [EXCERPT]
29216 1
29217 2
29218 3
29219 4
29220 5
29221 6
29222 7
29223 8
29224 9
29225 "});
29226
29227 cx.update_editor(|editor, window, cx| {
29228 editor.change_selections(None.into(), window, cx, |s| {
29229 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29230 });
29231 });
29232
29233 cx.assert_excerpts_with_selections(indoc! {"
29234 [EXCERPT]
29235 1
29236 2
29237 3
29238 [EXCERPT]
29239 1
29240 2
29241 3
29242 4
29243 5
29244 6
29245 ˇ7
29246 8
29247 9
29248 "});
29249
29250 cx.update_editor(|editor, _window, cx| {
29251 editor.set_vertical_scroll_margin(0, cx);
29252 });
29253
29254 cx.update_editor(|editor, window, cx| {
29255 assert_eq!(editor.vertical_scroll_margin(), 0);
29256 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29257 assert_eq!(
29258 editor.snapshot(window, cx).scroll_position(),
29259 gpui::Point::new(0., 12.0)
29260 );
29261 });
29262
29263 cx.update_editor(|editor, _window, cx| {
29264 editor.set_vertical_scroll_margin(3, cx);
29265 });
29266
29267 cx.update_editor(|editor, window, cx| {
29268 assert_eq!(editor.vertical_scroll_margin(), 3);
29269 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29270 assert_eq!(
29271 editor.snapshot(window, cx).scroll_position(),
29272 gpui::Point::new(0., 9.0)
29273 );
29274 });
29275}
29276
29277#[gpui::test]
29278async fn test_find_references_single_case(cx: &mut TestAppContext) {
29279 init_test(cx, |_| {});
29280 let mut cx = EditorLspTestContext::new_rust(
29281 lsp::ServerCapabilities {
29282 references_provider: Some(lsp::OneOf::Left(true)),
29283 ..lsp::ServerCapabilities::default()
29284 },
29285 cx,
29286 )
29287 .await;
29288
29289 let before = indoc!(
29290 r#"
29291 fn main() {
29292 let aˇbc = 123;
29293 let xyz = abc;
29294 }
29295 "#
29296 );
29297 let after = indoc!(
29298 r#"
29299 fn main() {
29300 let abc = 123;
29301 let xyz = ˇabc;
29302 }
29303 "#
29304 );
29305
29306 cx.lsp
29307 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29308 Ok(Some(vec![
29309 lsp::Location {
29310 uri: params.text_document_position.text_document.uri.clone(),
29311 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29312 },
29313 lsp::Location {
29314 uri: params.text_document_position.text_document.uri,
29315 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29316 },
29317 ]))
29318 });
29319
29320 cx.set_state(before);
29321
29322 let action = FindAllReferences {
29323 always_open_multibuffer: false,
29324 };
29325
29326 let navigated = cx
29327 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29328 .expect("should have spawned a task")
29329 .await
29330 .unwrap();
29331
29332 assert_eq!(navigated, Navigated::No);
29333
29334 cx.run_until_parked();
29335
29336 cx.assert_editor_state(after);
29337}