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 assert_eq!(
7754 display_ranges(editor, cx),
7755 vec![
7756 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7757 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7758 ]
7759 );
7760 });
7761
7762 _ = editor.update(cx, |editor, window, cx| {
7763 editor.select_line(&SelectLine, window, cx);
7764 assert_eq!(
7765 display_ranges(editor, cx),
7766 vec![
7767 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7768 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7769 ]
7770 );
7771 });
7772
7773 _ = editor.update(cx, |editor, window, cx| {
7774 editor.select_line(&SelectLine, window, cx);
7775 assert_eq!(
7776 display_ranges(editor, cx),
7777 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7778 );
7779 });
7780}
7781
7782#[gpui::test]
7783async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7784 init_test(cx, |_| {});
7785 let mut cx = EditorTestContext::new(cx).await;
7786
7787 #[track_caller]
7788 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7789 cx.set_state(initial_state);
7790 cx.update_editor(|e, window, cx| {
7791 e.split_selection_into_lines(&Default::default(), window, cx)
7792 });
7793 cx.assert_editor_state(expected_state);
7794 }
7795
7796 // Selection starts and ends at the middle of lines, left-to-right
7797 test(
7798 &mut cx,
7799 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7800 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7801 );
7802 // Same thing, right-to-left
7803 test(
7804 &mut cx,
7805 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7806 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7807 );
7808
7809 // Whole buffer, left-to-right, last line *doesn't* end with newline
7810 test(
7811 &mut cx,
7812 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7813 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7814 );
7815 // Same thing, right-to-left
7816 test(
7817 &mut cx,
7818 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7819 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7820 );
7821
7822 // Whole buffer, left-to-right, last line ends with newline
7823 test(
7824 &mut cx,
7825 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7826 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7827 );
7828 // Same thing, right-to-left
7829 test(
7830 &mut cx,
7831 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7832 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7833 );
7834
7835 // Starts at the end of a line, ends at the start of another
7836 test(
7837 &mut cx,
7838 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7839 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7840 );
7841}
7842
7843#[gpui::test]
7844async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7845 init_test(cx, |_| {});
7846
7847 let editor = cx.add_window(|window, cx| {
7848 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7849 build_editor(buffer, window, cx)
7850 });
7851
7852 // setup
7853 _ = editor.update(cx, |editor, window, cx| {
7854 editor.fold_creases(
7855 vec![
7856 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7857 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7858 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7859 ],
7860 true,
7861 window,
7862 cx,
7863 );
7864 assert_eq!(
7865 editor.display_text(cx),
7866 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7867 );
7868 });
7869
7870 _ = editor.update(cx, |editor, window, cx| {
7871 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7872 s.select_display_ranges([
7873 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7874 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7875 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7876 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7877 ])
7878 });
7879 editor.split_selection_into_lines(&Default::default(), window, cx);
7880 assert_eq!(
7881 editor.display_text(cx),
7882 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7883 );
7884 });
7885 EditorTestContext::for_editor(editor, cx)
7886 .await
7887 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7888
7889 _ = editor.update(cx, |editor, window, cx| {
7890 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7891 s.select_display_ranges([
7892 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7893 ])
7894 });
7895 editor.split_selection_into_lines(&Default::default(), window, cx);
7896 assert_eq!(
7897 editor.display_text(cx),
7898 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7899 );
7900 assert_eq!(
7901 display_ranges(editor, cx),
7902 [
7903 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7904 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7905 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7906 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7907 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7908 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7909 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7910 ]
7911 );
7912 });
7913 EditorTestContext::for_editor(editor, cx)
7914 .await
7915 .assert_editor_state(
7916 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7917 );
7918}
7919
7920#[gpui::test]
7921async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7922 init_test(cx, |_| {});
7923
7924 let mut cx = EditorTestContext::new(cx).await;
7925
7926 cx.set_state(indoc!(
7927 r#"abc
7928 defˇghi
7929
7930 jk
7931 nlmo
7932 "#
7933 ));
7934
7935 cx.update_editor(|editor, window, cx| {
7936 editor.add_selection_above(&Default::default(), window, cx);
7937 });
7938
7939 cx.assert_editor_state(indoc!(
7940 r#"abcˇ
7941 defˇghi
7942
7943 jk
7944 nlmo
7945 "#
7946 ));
7947
7948 cx.update_editor(|editor, window, cx| {
7949 editor.add_selection_above(&Default::default(), window, cx);
7950 });
7951
7952 cx.assert_editor_state(indoc!(
7953 r#"abcˇ
7954 defˇghi
7955
7956 jk
7957 nlmo
7958 "#
7959 ));
7960
7961 cx.update_editor(|editor, window, cx| {
7962 editor.add_selection_below(&Default::default(), window, cx);
7963 });
7964
7965 cx.assert_editor_state(indoc!(
7966 r#"abc
7967 defˇghi
7968
7969 jk
7970 nlmo
7971 "#
7972 ));
7973
7974 cx.update_editor(|editor, window, cx| {
7975 editor.undo_selection(&Default::default(), window, cx);
7976 });
7977
7978 cx.assert_editor_state(indoc!(
7979 r#"abcˇ
7980 defˇghi
7981
7982 jk
7983 nlmo
7984 "#
7985 ));
7986
7987 cx.update_editor(|editor, window, cx| {
7988 editor.redo_selection(&Default::default(), window, cx);
7989 });
7990
7991 cx.assert_editor_state(indoc!(
7992 r#"abc
7993 defˇghi
7994
7995 jk
7996 nlmo
7997 "#
7998 ));
7999
8000 cx.update_editor(|editor, window, cx| {
8001 editor.add_selection_below(&Default::default(), window, cx);
8002 });
8003
8004 cx.assert_editor_state(indoc!(
8005 r#"abc
8006 defˇghi
8007 ˇ
8008 jk
8009 nlmo
8010 "#
8011 ));
8012
8013 cx.update_editor(|editor, window, cx| {
8014 editor.add_selection_below(&Default::default(), window, cx);
8015 });
8016
8017 cx.assert_editor_state(indoc!(
8018 r#"abc
8019 defˇghi
8020 ˇ
8021 jkˇ
8022 nlmo
8023 "#
8024 ));
8025
8026 cx.update_editor(|editor, window, cx| {
8027 editor.add_selection_below(&Default::default(), window, cx);
8028 });
8029
8030 cx.assert_editor_state(indoc!(
8031 r#"abc
8032 defˇghi
8033 ˇ
8034 jkˇ
8035 nlmˇo
8036 "#
8037 ));
8038
8039 cx.update_editor(|editor, window, cx| {
8040 editor.add_selection_below(&Default::default(), window, cx);
8041 });
8042
8043 cx.assert_editor_state(indoc!(
8044 r#"abc
8045 defˇghi
8046 ˇ
8047 jkˇ
8048 nlmˇo
8049 ˇ"#
8050 ));
8051
8052 // change selections
8053 cx.set_state(indoc!(
8054 r#"abc
8055 def«ˇg»hi
8056
8057 jk
8058 nlmo
8059 "#
8060 ));
8061
8062 cx.update_editor(|editor, window, cx| {
8063 editor.add_selection_below(&Default::default(), window, cx);
8064 });
8065
8066 cx.assert_editor_state(indoc!(
8067 r#"abc
8068 def«ˇg»hi
8069
8070 jk
8071 nlm«ˇo»
8072 "#
8073 ));
8074
8075 cx.update_editor(|editor, window, cx| {
8076 editor.add_selection_below(&Default::default(), window, cx);
8077 });
8078
8079 cx.assert_editor_state(indoc!(
8080 r#"abc
8081 def«ˇg»hi
8082
8083 jk
8084 nlm«ˇo»
8085 "#
8086 ));
8087
8088 cx.update_editor(|editor, window, cx| {
8089 editor.add_selection_above(&Default::default(), window, cx);
8090 });
8091
8092 cx.assert_editor_state(indoc!(
8093 r#"abc
8094 def«ˇg»hi
8095
8096 jk
8097 nlmo
8098 "#
8099 ));
8100
8101 cx.update_editor(|editor, window, cx| {
8102 editor.add_selection_above(&Default::default(), window, cx);
8103 });
8104
8105 cx.assert_editor_state(indoc!(
8106 r#"abc
8107 def«ˇg»hi
8108
8109 jk
8110 nlmo
8111 "#
8112 ));
8113
8114 // Change selections again
8115 cx.set_state(indoc!(
8116 r#"a«bc
8117 defgˇ»hi
8118
8119 jk
8120 nlmo
8121 "#
8122 ));
8123
8124 cx.update_editor(|editor, window, cx| {
8125 editor.add_selection_below(&Default::default(), window, cx);
8126 });
8127
8128 cx.assert_editor_state(indoc!(
8129 r#"a«bcˇ»
8130 d«efgˇ»hi
8131
8132 j«kˇ»
8133 nlmo
8134 "#
8135 ));
8136
8137 cx.update_editor(|editor, window, cx| {
8138 editor.add_selection_below(&Default::default(), window, cx);
8139 });
8140 cx.assert_editor_state(indoc!(
8141 r#"a«bcˇ»
8142 d«efgˇ»hi
8143
8144 j«kˇ»
8145 n«lmoˇ»
8146 "#
8147 ));
8148 cx.update_editor(|editor, window, cx| {
8149 editor.add_selection_above(&Default::default(), window, cx);
8150 });
8151
8152 cx.assert_editor_state(indoc!(
8153 r#"a«bcˇ»
8154 d«efgˇ»hi
8155
8156 j«kˇ»
8157 nlmo
8158 "#
8159 ));
8160
8161 // Change selections again
8162 cx.set_state(indoc!(
8163 r#"abc
8164 d«ˇefghi
8165
8166 jk
8167 nlm»o
8168 "#
8169 ));
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.add_selection_above(&Default::default(), window, cx);
8173 });
8174
8175 cx.assert_editor_state(indoc!(
8176 r#"a«ˇbc»
8177 d«ˇef»ghi
8178
8179 j«ˇk»
8180 n«ˇlm»o
8181 "#
8182 ));
8183
8184 cx.update_editor(|editor, window, cx| {
8185 editor.add_selection_below(&Default::default(), window, cx);
8186 });
8187
8188 cx.assert_editor_state(indoc!(
8189 r#"abc
8190 d«ˇef»ghi
8191
8192 j«ˇk»
8193 n«ˇlm»o
8194 "#
8195 ));
8196}
8197
8198#[gpui::test]
8199async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8200 init_test(cx, |_| {});
8201 let mut cx = EditorTestContext::new(cx).await;
8202
8203 cx.set_state(indoc!(
8204 r#"line onˇe
8205 liˇne two
8206 line three
8207 line four"#
8208 ));
8209
8210 cx.update_editor(|editor, window, cx| {
8211 editor.add_selection_below(&Default::default(), window, cx);
8212 });
8213
8214 // test multiple cursors expand in the same direction
8215 cx.assert_editor_state(indoc!(
8216 r#"line onˇe
8217 liˇne twˇo
8218 liˇne three
8219 line four"#
8220 ));
8221
8222 cx.update_editor(|editor, window, cx| {
8223 editor.add_selection_below(&Default::default(), window, cx);
8224 });
8225
8226 cx.update_editor(|editor, window, cx| {
8227 editor.add_selection_below(&Default::default(), window, cx);
8228 });
8229
8230 // test multiple cursors expand below overflow
8231 cx.assert_editor_state(indoc!(
8232 r#"line onˇe
8233 liˇne twˇo
8234 liˇne thˇree
8235 liˇne foˇur"#
8236 ));
8237
8238 cx.update_editor(|editor, window, cx| {
8239 editor.add_selection_above(&Default::default(), window, cx);
8240 });
8241
8242 // test multiple cursors retrieves back correctly
8243 cx.assert_editor_state(indoc!(
8244 r#"line onˇe
8245 liˇne twˇo
8246 liˇne thˇree
8247 line four"#
8248 ));
8249
8250 cx.update_editor(|editor, window, cx| {
8251 editor.add_selection_above(&Default::default(), window, cx);
8252 });
8253
8254 cx.update_editor(|editor, window, cx| {
8255 editor.add_selection_above(&Default::default(), window, cx);
8256 });
8257
8258 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8259 cx.assert_editor_state(indoc!(
8260 r#"liˇne onˇe
8261 liˇne two
8262 line three
8263 line four"#
8264 ));
8265
8266 cx.update_editor(|editor, window, cx| {
8267 editor.undo_selection(&Default::default(), window, cx);
8268 });
8269
8270 // test undo
8271 cx.assert_editor_state(indoc!(
8272 r#"line onˇe
8273 liˇne twˇo
8274 line three
8275 line four"#
8276 ));
8277
8278 cx.update_editor(|editor, window, cx| {
8279 editor.redo_selection(&Default::default(), window, cx);
8280 });
8281
8282 // test redo
8283 cx.assert_editor_state(indoc!(
8284 r#"liˇne onˇe
8285 liˇne two
8286 line three
8287 line four"#
8288 ));
8289
8290 cx.set_state(indoc!(
8291 r#"abcd
8292 ef«ghˇ»
8293 ijkl
8294 «mˇ»nop"#
8295 ));
8296
8297 cx.update_editor(|editor, window, cx| {
8298 editor.add_selection_above(&Default::default(), window, cx);
8299 });
8300
8301 // test multiple selections expand in the same direction
8302 cx.assert_editor_state(indoc!(
8303 r#"ab«cdˇ»
8304 ef«ghˇ»
8305 «iˇ»jkl
8306 «mˇ»nop"#
8307 ));
8308
8309 cx.update_editor(|editor, window, cx| {
8310 editor.add_selection_above(&Default::default(), window, cx);
8311 });
8312
8313 // test multiple selection upward overflow
8314 cx.assert_editor_state(indoc!(
8315 r#"ab«cdˇ»
8316 «eˇ»f«ghˇ»
8317 «iˇ»jkl
8318 «mˇ»nop"#
8319 ));
8320
8321 cx.update_editor(|editor, window, cx| {
8322 editor.add_selection_below(&Default::default(), window, cx);
8323 });
8324
8325 // test multiple selection retrieves back correctly
8326 cx.assert_editor_state(indoc!(
8327 r#"abcd
8328 ef«ghˇ»
8329 «iˇ»jkl
8330 «mˇ»nop"#
8331 ));
8332
8333 cx.update_editor(|editor, window, cx| {
8334 editor.add_selection_below(&Default::default(), window, cx);
8335 });
8336
8337 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8338 cx.assert_editor_state(indoc!(
8339 r#"abcd
8340 ef«ghˇ»
8341 ij«klˇ»
8342 «mˇ»nop"#
8343 ));
8344
8345 cx.update_editor(|editor, window, cx| {
8346 editor.undo_selection(&Default::default(), window, cx);
8347 });
8348
8349 // test undo
8350 cx.assert_editor_state(indoc!(
8351 r#"abcd
8352 ef«ghˇ»
8353 «iˇ»jkl
8354 «mˇ»nop"#
8355 ));
8356
8357 cx.update_editor(|editor, window, cx| {
8358 editor.redo_selection(&Default::default(), window, cx);
8359 });
8360
8361 // test redo
8362 cx.assert_editor_state(indoc!(
8363 r#"abcd
8364 ef«ghˇ»
8365 ij«klˇ»
8366 «mˇ»nop"#
8367 ));
8368}
8369
8370#[gpui::test]
8371async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8372 init_test(cx, |_| {});
8373 let mut cx = EditorTestContext::new(cx).await;
8374
8375 cx.set_state(indoc!(
8376 r#"line onˇe
8377 liˇne two
8378 line three
8379 line four"#
8380 ));
8381
8382 cx.update_editor(|editor, window, cx| {
8383 editor.add_selection_below(&Default::default(), window, cx);
8384 editor.add_selection_below(&Default::default(), window, cx);
8385 editor.add_selection_below(&Default::default(), window, cx);
8386 });
8387
8388 // initial state with two multi cursor groups
8389 cx.assert_editor_state(indoc!(
8390 r#"line onˇe
8391 liˇne twˇo
8392 liˇne thˇree
8393 liˇne foˇur"#
8394 ));
8395
8396 // add single cursor in middle - simulate opt click
8397 cx.update_editor(|editor, window, cx| {
8398 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8399 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8400 editor.end_selection(window, cx);
8401 });
8402
8403 cx.assert_editor_state(indoc!(
8404 r#"line onˇe
8405 liˇne twˇo
8406 liˇneˇ thˇree
8407 liˇne foˇur"#
8408 ));
8409
8410 cx.update_editor(|editor, window, cx| {
8411 editor.add_selection_above(&Default::default(), window, cx);
8412 });
8413
8414 // test new added selection expands above and existing selection shrinks
8415 cx.assert_editor_state(indoc!(
8416 r#"line onˇe
8417 liˇneˇ twˇo
8418 liˇneˇ thˇree
8419 line four"#
8420 ));
8421
8422 cx.update_editor(|editor, window, cx| {
8423 editor.add_selection_above(&Default::default(), window, cx);
8424 });
8425
8426 // test new added selection expands above and existing selection shrinks
8427 cx.assert_editor_state(indoc!(
8428 r#"lineˇ onˇe
8429 liˇneˇ twˇo
8430 lineˇ three
8431 line four"#
8432 ));
8433
8434 // intial state with two selection groups
8435 cx.set_state(indoc!(
8436 r#"abcd
8437 ef«ghˇ»
8438 ijkl
8439 «mˇ»nop"#
8440 ));
8441
8442 cx.update_editor(|editor, window, cx| {
8443 editor.add_selection_above(&Default::default(), window, cx);
8444 editor.add_selection_above(&Default::default(), window, cx);
8445 });
8446
8447 cx.assert_editor_state(indoc!(
8448 r#"ab«cdˇ»
8449 «eˇ»f«ghˇ»
8450 «iˇ»jkl
8451 «mˇ»nop"#
8452 ));
8453
8454 // add single selection in middle - simulate opt drag
8455 cx.update_editor(|editor, window, cx| {
8456 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8457 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8458 editor.update_selection(
8459 DisplayPoint::new(DisplayRow(2), 4),
8460 0,
8461 gpui::Point::<f32>::default(),
8462 window,
8463 cx,
8464 );
8465 editor.end_selection(window, cx);
8466 });
8467
8468 cx.assert_editor_state(indoc!(
8469 r#"ab«cdˇ»
8470 «eˇ»f«ghˇ»
8471 «iˇ»jk«lˇ»
8472 «mˇ»nop"#
8473 ));
8474
8475 cx.update_editor(|editor, window, cx| {
8476 editor.add_selection_below(&Default::default(), window, cx);
8477 });
8478
8479 // test new added selection expands below, others shrinks from above
8480 cx.assert_editor_state(indoc!(
8481 r#"abcd
8482 ef«ghˇ»
8483 «iˇ»jk«lˇ»
8484 «mˇ»no«pˇ»"#
8485 ));
8486}
8487
8488#[gpui::test]
8489async fn test_select_next(cx: &mut TestAppContext) {
8490 init_test(cx, |_| {});
8491 let mut cx = EditorTestContext::new(cx).await;
8492
8493 // Enable case sensitive search.
8494 update_test_editor_settings(&mut cx, |settings| {
8495 let mut search_settings = SearchSettingsContent::default();
8496 search_settings.case_sensitive = Some(true);
8497 settings.search = Some(search_settings);
8498 });
8499
8500 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8501
8502 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8503 .unwrap();
8504 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8505
8506 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8507 .unwrap();
8508 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8509
8510 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8511 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8512
8513 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8514 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8515
8516 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8517 .unwrap();
8518 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8519
8520 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8521 .unwrap();
8522 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8523
8524 // Test selection direction should be preserved
8525 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8526
8527 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8528 .unwrap();
8529 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8530
8531 // Test case sensitivity
8532 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8533 cx.update_editor(|e, window, cx| {
8534 e.select_next(&SelectNext::default(), window, cx).unwrap();
8535 });
8536 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8537
8538 // Disable case sensitive search.
8539 update_test_editor_settings(&mut cx, |settings| {
8540 let mut search_settings = SearchSettingsContent::default();
8541 search_settings.case_sensitive = Some(false);
8542 settings.search = Some(search_settings);
8543 });
8544
8545 cx.set_state("«ˇfoo»\nFOO\nFoo");
8546 cx.update_editor(|e, window, cx| {
8547 e.select_next(&SelectNext::default(), window, cx).unwrap();
8548 e.select_next(&SelectNext::default(), window, cx).unwrap();
8549 });
8550 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8551}
8552
8553#[gpui::test]
8554async fn test_select_all_matches(cx: &mut TestAppContext) {
8555 init_test(cx, |_| {});
8556 let mut cx = EditorTestContext::new(cx).await;
8557
8558 // Enable case sensitive search.
8559 update_test_editor_settings(&mut cx, |settings| {
8560 let mut search_settings = SearchSettingsContent::default();
8561 search_settings.case_sensitive = Some(true);
8562 settings.search = Some(search_settings);
8563 });
8564
8565 // Test caret-only selections
8566 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8567 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8568 .unwrap();
8569 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8570
8571 // Test left-to-right selections
8572 cx.set_state("abc\n«abcˇ»\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ˇ»\n«abcˇ»");
8576
8577 // Test right-to-left 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 selecting whitespace with caret selection
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\nabc");
8588
8589 // Test selecting whitespace with left-to-right 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 no matches with right-to-left 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 with a single word and clip_at_line_ends=true (#29823)
8602 cx.set_state("aˇbc");
8603 cx.update_editor(|e, window, cx| {
8604 e.set_clip_at_line_ends(true, cx);
8605 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8606 e.set_clip_at_line_ends(false, cx);
8607 });
8608 cx.assert_editor_state("«abcˇ»");
8609
8610 // Test case sensitivity
8611 cx.set_state("fˇoo\nFOO\nFoo");
8612 cx.update_editor(|e, window, cx| {
8613 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8614 });
8615 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8616
8617 // Disable case sensitive search.
8618 update_test_editor_settings(&mut cx, |settings| {
8619 let mut search_settings = SearchSettingsContent::default();
8620 search_settings.case_sensitive = Some(false);
8621 settings.search = Some(search_settings);
8622 });
8623
8624 cx.set_state("fˇoo\nFOO\nFoo");
8625 cx.update_editor(|e, window, cx| {
8626 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8627 });
8628 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8629}
8630
8631#[gpui::test]
8632async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8633 init_test(cx, |_| {});
8634
8635 let mut cx = EditorTestContext::new(cx).await;
8636
8637 let large_body_1 = "\nd".repeat(200);
8638 let large_body_2 = "\ne".repeat(200);
8639
8640 cx.set_state(&format!(
8641 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8642 ));
8643 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8644 let scroll_position = editor.scroll_position(cx);
8645 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8646 scroll_position
8647 });
8648
8649 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8650 .unwrap();
8651 cx.assert_editor_state(&format!(
8652 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8653 ));
8654 let scroll_position_after_selection =
8655 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8656 assert_eq!(
8657 initial_scroll_position, scroll_position_after_selection,
8658 "Scroll position should not change after selecting all matches"
8659 );
8660}
8661
8662#[gpui::test]
8663async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8664 init_test(cx, |_| {});
8665
8666 let mut cx = EditorLspTestContext::new_rust(
8667 lsp::ServerCapabilities {
8668 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8669 ..Default::default()
8670 },
8671 cx,
8672 )
8673 .await;
8674
8675 cx.set_state(indoc! {"
8676 line 1
8677 line 2
8678 linˇe 3
8679 line 4
8680 line 5
8681 "});
8682
8683 // Make an edit
8684 cx.update_editor(|editor, window, cx| {
8685 editor.handle_input("X", window, cx);
8686 });
8687
8688 // Move cursor to a different position
8689 cx.update_editor(|editor, window, cx| {
8690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8691 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8692 });
8693 });
8694
8695 cx.assert_editor_state(indoc! {"
8696 line 1
8697 line 2
8698 linXe 3
8699 line 4
8700 liˇne 5
8701 "});
8702
8703 cx.lsp
8704 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8705 Ok(Some(vec![lsp::TextEdit::new(
8706 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8707 "PREFIX ".to_string(),
8708 )]))
8709 });
8710
8711 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8712 .unwrap()
8713 .await
8714 .unwrap();
8715
8716 cx.assert_editor_state(indoc! {"
8717 PREFIX line 1
8718 line 2
8719 linXe 3
8720 line 4
8721 liˇne 5
8722 "});
8723
8724 // Undo formatting
8725 cx.update_editor(|editor, window, cx| {
8726 editor.undo(&Default::default(), window, cx);
8727 });
8728
8729 // Verify cursor moved back to position after edit
8730 cx.assert_editor_state(indoc! {"
8731 line 1
8732 line 2
8733 linXˇe 3
8734 line 4
8735 line 5
8736 "});
8737}
8738
8739#[gpui::test]
8740async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8741 init_test(cx, |_| {});
8742
8743 let mut cx = EditorTestContext::new(cx).await;
8744
8745 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
8746 cx.update_editor(|editor, window, cx| {
8747 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8748 });
8749
8750 cx.set_state(indoc! {"
8751 line 1
8752 line 2
8753 linˇe 3
8754 line 4
8755 line 5
8756 line 6
8757 line 7
8758 line 8
8759 line 9
8760 line 10
8761 "});
8762
8763 let snapshot = cx.buffer_snapshot();
8764 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8765
8766 cx.update(|_, cx| {
8767 provider.update(cx, |provider, _| {
8768 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
8769 id: None,
8770 edits: vec![(edit_position..edit_position, "X".into())],
8771 edit_preview: None,
8772 }))
8773 })
8774 });
8775
8776 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8777 cx.update_editor(|editor, window, cx| {
8778 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8779 });
8780
8781 cx.assert_editor_state(indoc! {"
8782 line 1
8783 line 2
8784 lineXˇ 3
8785 line 4
8786 line 5
8787 line 6
8788 line 7
8789 line 8
8790 line 9
8791 line 10
8792 "});
8793
8794 cx.update_editor(|editor, window, cx| {
8795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8796 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8797 });
8798 });
8799
8800 cx.assert_editor_state(indoc! {"
8801 line 1
8802 line 2
8803 lineX 3
8804 line 4
8805 line 5
8806 line 6
8807 line 7
8808 line 8
8809 line 9
8810 liˇne 10
8811 "});
8812
8813 cx.update_editor(|editor, window, cx| {
8814 editor.undo(&Default::default(), window, cx);
8815 });
8816
8817 cx.assert_editor_state(indoc! {"
8818 line 1
8819 line 2
8820 lineˇ 3
8821 line 4
8822 line 5
8823 line 6
8824 line 7
8825 line 8
8826 line 9
8827 line 10
8828 "});
8829}
8830
8831#[gpui::test]
8832async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8833 init_test(cx, |_| {});
8834
8835 let mut cx = EditorTestContext::new(cx).await;
8836 cx.set_state(
8837 r#"let foo = 2;
8838lˇet foo = 2;
8839let fooˇ = 2;
8840let foo = 2;
8841let foo = ˇ2;"#,
8842 );
8843
8844 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8845 .unwrap();
8846 cx.assert_editor_state(
8847 r#"let foo = 2;
8848«letˇ» foo = 2;
8849let «fooˇ» = 2;
8850let foo = 2;
8851let foo = «2ˇ»;"#,
8852 );
8853
8854 // noop for multiple selections with different contents
8855 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8856 .unwrap();
8857 cx.assert_editor_state(
8858 r#"let foo = 2;
8859«letˇ» foo = 2;
8860let «fooˇ» = 2;
8861let foo = 2;
8862let foo = «2ˇ»;"#,
8863 );
8864
8865 // Test last selection direction should be preserved
8866 cx.set_state(
8867 r#"let foo = 2;
8868let foo = 2;
8869let «fooˇ» = 2;
8870let «ˇfoo» = 2;
8871let foo = 2;"#,
8872 );
8873
8874 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8875 .unwrap();
8876 cx.assert_editor_state(
8877 r#"let foo = 2;
8878let foo = 2;
8879let «fooˇ» = 2;
8880let «ˇfoo» = 2;
8881let «ˇfoo» = 2;"#,
8882 );
8883}
8884
8885#[gpui::test]
8886async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8887 init_test(cx, |_| {});
8888
8889 let mut cx =
8890 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8891
8892 cx.assert_editor_state(indoc! {"
8893 ˇbbb
8894 ccc
8895
8896 bbb
8897 ccc
8898 "});
8899 cx.dispatch_action(SelectPrevious::default());
8900 cx.assert_editor_state(indoc! {"
8901 «bbbˇ»
8902 ccc
8903
8904 bbb
8905 ccc
8906 "});
8907 cx.dispatch_action(SelectPrevious::default());
8908 cx.assert_editor_state(indoc! {"
8909 «bbbˇ»
8910 ccc
8911
8912 «bbbˇ»
8913 ccc
8914 "});
8915}
8916
8917#[gpui::test]
8918async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8919 init_test(cx, |_| {});
8920
8921 let mut cx = EditorTestContext::new(cx).await;
8922 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8923
8924 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8925 .unwrap();
8926 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8927
8928 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8929 .unwrap();
8930 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8931
8932 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8933 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8934
8935 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8936 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8937
8938 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8939 .unwrap();
8940 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8941
8942 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8943 .unwrap();
8944 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8945}
8946
8947#[gpui::test]
8948async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8949 init_test(cx, |_| {});
8950
8951 let mut cx = EditorTestContext::new(cx).await;
8952 cx.set_state("aˇ");
8953
8954 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8955 .unwrap();
8956 cx.assert_editor_state("«aˇ»");
8957 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8958 .unwrap();
8959 cx.assert_editor_state("«aˇ»");
8960}
8961
8962#[gpui::test]
8963async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8964 init_test(cx, |_| {});
8965
8966 let mut cx = EditorTestContext::new(cx).await;
8967 cx.set_state(
8968 r#"let foo = 2;
8969lˇet foo = 2;
8970let fooˇ = 2;
8971let foo = 2;
8972let foo = ˇ2;"#,
8973 );
8974
8975 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8976 .unwrap();
8977 cx.assert_editor_state(
8978 r#"let foo = 2;
8979«letˇ» foo = 2;
8980let «fooˇ» = 2;
8981let foo = 2;
8982let foo = «2ˇ»;"#,
8983 );
8984
8985 // noop for multiple selections with different contents
8986 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8987 .unwrap();
8988 cx.assert_editor_state(
8989 r#"let foo = 2;
8990«letˇ» foo = 2;
8991let «fooˇ» = 2;
8992let foo = 2;
8993let foo = «2ˇ»;"#,
8994 );
8995}
8996
8997#[gpui::test]
8998async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8999 init_test(cx, |_| {});
9000 let mut cx = EditorTestContext::new(cx).await;
9001
9002 // Enable case sensitive search.
9003 update_test_editor_settings(&mut cx, |settings| {
9004 let mut search_settings = SearchSettingsContent::default();
9005 search_settings.case_sensitive = Some(true);
9006 settings.search = Some(search_settings);
9007 });
9008
9009 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9010
9011 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9012 .unwrap();
9013 // selection direction is preserved
9014 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9015
9016 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9017 .unwrap();
9018 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9019
9020 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9021 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9022
9023 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9024 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9025
9026 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9027 .unwrap();
9028 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9029
9030 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9031 .unwrap();
9032 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9033
9034 // Test case sensitivity
9035 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9036 cx.update_editor(|e, window, cx| {
9037 e.select_previous(&SelectPrevious::default(), window, cx)
9038 .unwrap();
9039 e.select_previous(&SelectPrevious::default(), window, cx)
9040 .unwrap();
9041 });
9042 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9043
9044 // Disable case sensitive search.
9045 update_test_editor_settings(&mut cx, |settings| {
9046 let mut search_settings = SearchSettingsContent::default();
9047 search_settings.case_sensitive = Some(false);
9048 settings.search = Some(search_settings);
9049 });
9050
9051 cx.set_state("foo\nFOO\n«ˇFoo»");
9052 cx.update_editor(|e, window, cx| {
9053 e.select_previous(&SelectPrevious::default(), window, cx)
9054 .unwrap();
9055 e.select_previous(&SelectPrevious::default(), window, cx)
9056 .unwrap();
9057 });
9058 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9059}
9060
9061#[gpui::test]
9062async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9063 init_test(cx, |_| {});
9064
9065 let language = Arc::new(Language::new(
9066 LanguageConfig::default(),
9067 Some(tree_sitter_rust::LANGUAGE.into()),
9068 ));
9069
9070 let text = r#"
9071 use mod1::mod2::{mod3, mod4};
9072
9073 fn fn_1(param1: bool, param2: &str) {
9074 let var1 = "text";
9075 }
9076 "#
9077 .unindent();
9078
9079 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9080 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9081 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9082
9083 editor
9084 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9085 .await;
9086
9087 editor.update_in(cx, |editor, window, cx| {
9088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9089 s.select_display_ranges([
9090 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9091 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9092 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9093 ]);
9094 });
9095 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9096 });
9097 editor.update(cx, |editor, cx| {
9098 assert_text_with_selections(
9099 editor,
9100 indoc! {r#"
9101 use mod1::mod2::{mod3, «mod4ˇ»};
9102
9103 fn fn_1«ˇ(param1: bool, param2: &str)» {
9104 let var1 = "«ˇtext»";
9105 }
9106 "#},
9107 cx,
9108 );
9109 });
9110
9111 editor.update_in(cx, |editor, window, cx| {
9112 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9113 });
9114 editor.update(cx, |editor, cx| {
9115 assert_text_with_selections(
9116 editor,
9117 indoc! {r#"
9118 use mod1::mod2::«{mod3, mod4}ˇ»;
9119
9120 «ˇfn fn_1(param1: bool, param2: &str) {
9121 let var1 = "text";
9122 }»
9123 "#},
9124 cx,
9125 );
9126 });
9127
9128 editor.update_in(cx, |editor, window, cx| {
9129 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9130 });
9131 assert_eq!(
9132 editor.update(cx, |editor, cx| editor
9133 .selections
9134 .display_ranges(&editor.display_snapshot(cx))),
9135 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9136 );
9137
9138 // Trying to expand the selected syntax node one more time has no effect.
9139 editor.update_in(cx, |editor, window, cx| {
9140 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9141 });
9142 assert_eq!(
9143 editor.update(cx, |editor, cx| editor
9144 .selections
9145 .display_ranges(&editor.display_snapshot(cx))),
9146 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9147 );
9148
9149 editor.update_in(cx, |editor, window, cx| {
9150 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9151 });
9152 editor.update(cx, |editor, cx| {
9153 assert_text_with_selections(
9154 editor,
9155 indoc! {r#"
9156 use mod1::mod2::«{mod3, mod4}ˇ»;
9157
9158 «ˇfn fn_1(param1: bool, param2: &str) {
9159 let var1 = "text";
9160 }»
9161 "#},
9162 cx,
9163 );
9164 });
9165
9166 editor.update_in(cx, |editor, window, cx| {
9167 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9168 });
9169 editor.update(cx, |editor, cx| {
9170 assert_text_with_selections(
9171 editor,
9172 indoc! {r#"
9173 use mod1::mod2::{mod3, «mod4ˇ»};
9174
9175 fn fn_1«ˇ(param1: bool, param2: &str)» {
9176 let var1 = "«ˇtext»";
9177 }
9178 "#},
9179 cx,
9180 );
9181 });
9182
9183 editor.update_in(cx, |editor, window, cx| {
9184 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9185 });
9186 editor.update(cx, |editor, cx| {
9187 assert_text_with_selections(
9188 editor,
9189 indoc! {r#"
9190 use mod1::mod2::{mod3, moˇd4};
9191
9192 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9193 let var1 = "teˇxt";
9194 }
9195 "#},
9196 cx,
9197 );
9198 });
9199
9200 // Trying to shrink the selected syntax node one more time has no effect.
9201 editor.update_in(cx, |editor, window, cx| {
9202 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9203 });
9204 editor.update_in(cx, |editor, _, cx| {
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 use mod1::mod2::{mod3, moˇd4};
9209
9210 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9211 let var1 = "teˇxt";
9212 }
9213 "#},
9214 cx,
9215 );
9216 });
9217
9218 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9219 // a fold.
9220 editor.update_in(cx, |editor, window, cx| {
9221 editor.fold_creases(
9222 vec![
9223 Crease::simple(
9224 Point::new(0, 21)..Point::new(0, 24),
9225 FoldPlaceholder::test(),
9226 ),
9227 Crease::simple(
9228 Point::new(3, 20)..Point::new(3, 22),
9229 FoldPlaceholder::test(),
9230 ),
9231 ],
9232 true,
9233 window,
9234 cx,
9235 );
9236 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9237 });
9238 editor.update(cx, |editor, cx| {
9239 assert_text_with_selections(
9240 editor,
9241 indoc! {r#"
9242 use mod1::mod2::«{mod3, mod4}ˇ»;
9243
9244 fn fn_1«ˇ(param1: bool, param2: &str)» {
9245 let var1 = "«ˇtext»";
9246 }
9247 "#},
9248 cx,
9249 );
9250 });
9251}
9252
9253#[gpui::test]
9254async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9255 init_test(cx, |_| {});
9256
9257 let language = Arc::new(Language::new(
9258 LanguageConfig::default(),
9259 Some(tree_sitter_rust::LANGUAGE.into()),
9260 ));
9261
9262 let text = "let a = 2;";
9263
9264 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9265 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9266 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9267
9268 editor
9269 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9270 .await;
9271
9272 // Test case 1: Cursor at end of word
9273 editor.update_in(cx, |editor, window, cx| {
9274 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9275 s.select_display_ranges([
9276 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9277 ]);
9278 });
9279 });
9280 editor.update(cx, |editor, cx| {
9281 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9282 });
9283 editor.update_in(cx, |editor, window, cx| {
9284 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
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
9296 // Test case 2: Cursor at end of statement
9297 editor.update_in(cx, |editor, window, cx| {
9298 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9299 s.select_display_ranges([
9300 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9301 ]);
9302 });
9303 });
9304 editor.update(cx, |editor, cx| {
9305 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9306 });
9307 editor.update_in(cx, |editor, window, cx| {
9308 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9309 });
9310 editor.update(cx, |editor, cx| {
9311 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9312 });
9313}
9314
9315#[gpui::test]
9316async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9317 init_test(cx, |_| {});
9318
9319 let language = Arc::new(Language::new(
9320 LanguageConfig {
9321 name: "JavaScript".into(),
9322 ..Default::default()
9323 },
9324 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9325 ));
9326
9327 let text = r#"
9328 let a = {
9329 key: "value",
9330 };
9331 "#
9332 .unindent();
9333
9334 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9335 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9336 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9337
9338 editor
9339 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9340 .await;
9341
9342 // Test case 1: Cursor after '{'
9343 editor.update_in(cx, |editor, window, cx| {
9344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9345 s.select_display_ranges([
9346 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9347 ]);
9348 });
9349 });
9350 editor.update(cx, |editor, cx| {
9351 assert_text_with_selections(
9352 editor,
9353 indoc! {r#"
9354 let a = {ˇ
9355 key: "value",
9356 };
9357 "#},
9358 cx,
9359 );
9360 });
9361 editor.update_in(cx, |editor, window, cx| {
9362 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9363 });
9364 editor.update(cx, |editor, cx| {
9365 assert_text_with_selections(
9366 editor,
9367 indoc! {r#"
9368 let a = «ˇ{
9369 key: "value",
9370 }»;
9371 "#},
9372 cx,
9373 );
9374 });
9375
9376 // Test case 2: Cursor after ':'
9377 editor.update_in(cx, |editor, window, cx| {
9378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9379 s.select_display_ranges([
9380 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9381 ]);
9382 });
9383 });
9384 editor.update(cx, |editor, cx| {
9385 assert_text_with_selections(
9386 editor,
9387 indoc! {r#"
9388 let a = {
9389 key:ˇ "value",
9390 };
9391 "#},
9392 cx,
9393 );
9394 });
9395 editor.update_in(cx, |editor, window, cx| {
9396 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9397 });
9398 editor.update(cx, |editor, cx| {
9399 assert_text_with_selections(
9400 editor,
9401 indoc! {r#"
9402 let a = {
9403 «ˇkey: "value"»,
9404 };
9405 "#},
9406 cx,
9407 );
9408 });
9409 editor.update_in(cx, |editor, window, cx| {
9410 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9411 });
9412 editor.update(cx, |editor, cx| {
9413 assert_text_with_selections(
9414 editor,
9415 indoc! {r#"
9416 let a = «ˇ{
9417 key: "value",
9418 }»;
9419 "#},
9420 cx,
9421 );
9422 });
9423
9424 // Test case 3: Cursor after ','
9425 editor.update_in(cx, |editor, window, cx| {
9426 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9427 s.select_display_ranges([
9428 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9429 ]);
9430 });
9431 });
9432 editor.update(cx, |editor, cx| {
9433 assert_text_with_selections(
9434 editor,
9435 indoc! {r#"
9436 let a = {
9437 key: "value",ˇ
9438 };
9439 "#},
9440 cx,
9441 );
9442 });
9443 editor.update_in(cx, |editor, window, cx| {
9444 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9445 });
9446 editor.update(cx, |editor, cx| {
9447 assert_text_with_selections(
9448 editor,
9449 indoc! {r#"
9450 let a = «ˇ{
9451 key: "value",
9452 }»;
9453 "#},
9454 cx,
9455 );
9456 });
9457
9458 // Test case 4: Cursor after ';'
9459 editor.update_in(cx, |editor, window, cx| {
9460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9461 s.select_display_ranges([
9462 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9463 ]);
9464 });
9465 });
9466 editor.update(cx, |editor, cx| {
9467 assert_text_with_selections(
9468 editor,
9469 indoc! {r#"
9470 let a = {
9471 key: "value",
9472 };ˇ
9473 "#},
9474 cx,
9475 );
9476 });
9477 editor.update_in(cx, |editor, window, cx| {
9478 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9479 });
9480 editor.update(cx, |editor, cx| {
9481 assert_text_with_selections(
9482 editor,
9483 indoc! {r#"
9484 «ˇlet a = {
9485 key: "value",
9486 };
9487 »"#},
9488 cx,
9489 );
9490 });
9491}
9492
9493#[gpui::test]
9494async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9495 init_test(cx, |_| {});
9496
9497 let language = Arc::new(Language::new(
9498 LanguageConfig::default(),
9499 Some(tree_sitter_rust::LANGUAGE.into()),
9500 ));
9501
9502 let text = r#"
9503 use mod1::mod2::{mod3, mod4};
9504
9505 fn fn_1(param1: bool, param2: &str) {
9506 let var1 = "hello world";
9507 }
9508 "#
9509 .unindent();
9510
9511 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9512 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9513 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9514
9515 editor
9516 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9517 .await;
9518
9519 // Test 1: Cursor on a letter of a string word
9520 editor.update_in(cx, |editor, window, cx| {
9521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9522 s.select_display_ranges([
9523 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9524 ]);
9525 });
9526 });
9527 editor.update_in(cx, |editor, window, cx| {
9528 assert_text_with_selections(
9529 editor,
9530 indoc! {r#"
9531 use mod1::mod2::{mod3, mod4};
9532
9533 fn fn_1(param1: bool, param2: &str) {
9534 let var1 = "hˇello world";
9535 }
9536 "#},
9537 cx,
9538 );
9539 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9540 assert_text_with_selections(
9541 editor,
9542 indoc! {r#"
9543 use mod1::mod2::{mod3, mod4};
9544
9545 fn fn_1(param1: bool, param2: &str) {
9546 let var1 = "«ˇhello» world";
9547 }
9548 "#},
9549 cx,
9550 );
9551 });
9552
9553 // Test 2: Partial selection within a word
9554 editor.update_in(cx, |editor, window, cx| {
9555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9556 s.select_display_ranges([
9557 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9558 ]);
9559 });
9560 });
9561 editor.update_in(cx, |editor, window, cx| {
9562 assert_text_with_selections(
9563 editor,
9564 indoc! {r#"
9565 use mod1::mod2::{mod3, mod4};
9566
9567 fn fn_1(param1: bool, param2: &str) {
9568 let var1 = "h«elˇ»lo world";
9569 }
9570 "#},
9571 cx,
9572 );
9573 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9574 assert_text_with_selections(
9575 editor,
9576 indoc! {r#"
9577 use mod1::mod2::{mod3, mod4};
9578
9579 fn fn_1(param1: bool, param2: &str) {
9580 let var1 = "«ˇhello» world";
9581 }
9582 "#},
9583 cx,
9584 );
9585 });
9586
9587 // Test 3: Complete word already selected
9588 editor.update_in(cx, |editor, window, cx| {
9589 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9590 s.select_display_ranges([
9591 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9592 ]);
9593 });
9594 });
9595 editor.update_in(cx, |editor, window, cx| {
9596 assert_text_with_selections(
9597 editor,
9598 indoc! {r#"
9599 use mod1::mod2::{mod3, mod4};
9600
9601 fn fn_1(param1: bool, param2: &str) {
9602 let var1 = "«helloˇ» world";
9603 }
9604 "#},
9605 cx,
9606 );
9607 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9608 assert_text_with_selections(
9609 editor,
9610 indoc! {r#"
9611 use mod1::mod2::{mod3, mod4};
9612
9613 fn fn_1(param1: bool, param2: &str) {
9614 let var1 = "«hello worldˇ»";
9615 }
9616 "#},
9617 cx,
9618 );
9619 });
9620
9621 // Test 4: Selection spanning across words
9622 editor.update_in(cx, |editor, window, cx| {
9623 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9624 s.select_display_ranges([
9625 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9626 ]);
9627 });
9628 });
9629 editor.update_in(cx, |editor, window, cx| {
9630 assert_text_with_selections(
9631 editor,
9632 indoc! {r#"
9633 use mod1::mod2::{mod3, mod4};
9634
9635 fn fn_1(param1: bool, param2: &str) {
9636 let var1 = "hel«lo woˇ»rld";
9637 }
9638 "#},
9639 cx,
9640 );
9641 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9642 assert_text_with_selections(
9643 editor,
9644 indoc! {r#"
9645 use mod1::mod2::{mod3, mod4};
9646
9647 fn fn_1(param1: bool, param2: &str) {
9648 let var1 = "«ˇhello world»";
9649 }
9650 "#},
9651 cx,
9652 );
9653 });
9654
9655 // Test 5: Expansion beyond string
9656 editor.update_in(cx, |editor, window, cx| {
9657 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9658 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9659 assert_text_with_selections(
9660 editor,
9661 indoc! {r#"
9662 use mod1::mod2::{mod3, mod4};
9663
9664 fn fn_1(param1: bool, param2: &str) {
9665 «ˇlet var1 = "hello world";»
9666 }
9667 "#},
9668 cx,
9669 );
9670 });
9671}
9672
9673#[gpui::test]
9674async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9675 init_test(cx, |_| {});
9676
9677 let mut cx = EditorTestContext::new(cx).await;
9678
9679 let language = Arc::new(Language::new(
9680 LanguageConfig::default(),
9681 Some(tree_sitter_rust::LANGUAGE.into()),
9682 ));
9683
9684 cx.update_buffer(|buffer, cx| {
9685 buffer.set_language(Some(language), cx);
9686 });
9687
9688 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9689 cx.update_editor(|editor, window, cx| {
9690 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9691 });
9692
9693 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9694
9695 cx.set_state(indoc! { r#"fn a() {
9696 // what
9697 // a
9698 // ˇlong
9699 // method
9700 // I
9701 // sure
9702 // hope
9703 // it
9704 // works
9705 }"# });
9706
9707 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9708 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9709 cx.update(|_, cx| {
9710 multi_buffer.update(cx, |multi_buffer, cx| {
9711 multi_buffer.set_excerpts_for_path(
9712 PathKey::for_buffer(&buffer, cx),
9713 buffer,
9714 [Point::new(1, 0)..Point::new(1, 0)],
9715 3,
9716 cx,
9717 );
9718 });
9719 });
9720
9721 let editor2 = cx.new_window_entity(|window, cx| {
9722 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9723 });
9724
9725 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9726 cx.update_editor(|editor, window, cx| {
9727 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9728 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9729 })
9730 });
9731
9732 cx.assert_editor_state(indoc! { "
9733 fn a() {
9734 // what
9735 // a
9736 ˇ // long
9737 // method"});
9738
9739 cx.update_editor(|editor, window, cx| {
9740 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9741 });
9742
9743 // Although we could potentially make the action work when the syntax node
9744 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9745 // did. Maybe we could also expand the excerpt to contain the range?
9746 cx.assert_editor_state(indoc! { "
9747 fn a() {
9748 // what
9749 // a
9750 ˇ // long
9751 // method"});
9752}
9753
9754#[gpui::test]
9755async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9756 init_test(cx, |_| {});
9757
9758 let base_text = r#"
9759 impl A {
9760 // this is an uncommitted comment
9761
9762 fn b() {
9763 c();
9764 }
9765
9766 // this is another uncommitted comment
9767
9768 fn d() {
9769 // e
9770 // f
9771 }
9772 }
9773
9774 fn g() {
9775 // h
9776 }
9777 "#
9778 .unindent();
9779
9780 let text = r#"
9781 ˇimpl A {
9782
9783 fn b() {
9784 c();
9785 }
9786
9787 fn d() {
9788 // e
9789 // f
9790 }
9791 }
9792
9793 fn g() {
9794 // h
9795 }
9796 "#
9797 .unindent();
9798
9799 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9800 cx.set_state(&text);
9801 cx.set_head_text(&base_text);
9802 cx.update_editor(|editor, window, cx| {
9803 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9804 });
9805
9806 cx.assert_state_with_diff(
9807 "
9808 ˇimpl A {
9809 - // this is an uncommitted comment
9810
9811 fn b() {
9812 c();
9813 }
9814
9815 - // this is another uncommitted comment
9816 -
9817 fn d() {
9818 // e
9819 // f
9820 }
9821 }
9822
9823 fn g() {
9824 // h
9825 }
9826 "
9827 .unindent(),
9828 );
9829
9830 let expected_display_text = "
9831 impl A {
9832 // this is an uncommitted comment
9833
9834 fn b() {
9835 ⋯
9836 }
9837
9838 // this is another uncommitted comment
9839
9840 fn d() {
9841 ⋯
9842 }
9843 }
9844
9845 fn g() {
9846 ⋯
9847 }
9848 "
9849 .unindent();
9850
9851 cx.update_editor(|editor, window, cx| {
9852 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9853 assert_eq!(editor.display_text(cx), expected_display_text);
9854 });
9855}
9856
9857#[gpui::test]
9858async fn test_autoindent(cx: &mut TestAppContext) {
9859 init_test(cx, |_| {});
9860
9861 let language = Arc::new(
9862 Language::new(
9863 LanguageConfig {
9864 brackets: BracketPairConfig {
9865 pairs: vec![
9866 BracketPair {
9867 start: "{".to_string(),
9868 end: "}".to_string(),
9869 close: false,
9870 surround: false,
9871 newline: true,
9872 },
9873 BracketPair {
9874 start: "(".to_string(),
9875 end: ")".to_string(),
9876 close: false,
9877 surround: false,
9878 newline: true,
9879 },
9880 ],
9881 ..Default::default()
9882 },
9883 ..Default::default()
9884 },
9885 Some(tree_sitter_rust::LANGUAGE.into()),
9886 )
9887 .with_indents_query(
9888 r#"
9889 (_ "(" ")" @end) @indent
9890 (_ "{" "}" @end) @indent
9891 "#,
9892 )
9893 .unwrap(),
9894 );
9895
9896 let text = "fn a() {}";
9897
9898 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9899 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9900 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9901 editor
9902 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9903 .await;
9904
9905 editor.update_in(cx, |editor, window, cx| {
9906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9907 s.select_ranges([
9908 MultiBufferOffset(5)..MultiBufferOffset(5),
9909 MultiBufferOffset(8)..MultiBufferOffset(8),
9910 MultiBufferOffset(9)..MultiBufferOffset(9),
9911 ])
9912 });
9913 editor.newline(&Newline, window, cx);
9914 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9915 assert_eq!(
9916 editor.selections.ranges(&editor.display_snapshot(cx)),
9917 &[
9918 Point::new(1, 4)..Point::new(1, 4),
9919 Point::new(3, 4)..Point::new(3, 4),
9920 Point::new(5, 0)..Point::new(5, 0)
9921 ]
9922 );
9923 });
9924}
9925
9926#[gpui::test]
9927async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9928 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9929
9930 let language = Arc::new(
9931 Language::new(
9932 LanguageConfig {
9933 brackets: BracketPairConfig {
9934 pairs: vec![
9935 BracketPair {
9936 start: "{".to_string(),
9937 end: "}".to_string(),
9938 close: false,
9939 surround: false,
9940 newline: true,
9941 },
9942 BracketPair {
9943 start: "(".to_string(),
9944 end: ")".to_string(),
9945 close: false,
9946 surround: false,
9947 newline: true,
9948 },
9949 ],
9950 ..Default::default()
9951 },
9952 ..Default::default()
9953 },
9954 Some(tree_sitter_rust::LANGUAGE.into()),
9955 )
9956 .with_indents_query(
9957 r#"
9958 (_ "(" ")" @end) @indent
9959 (_ "{" "}" @end) @indent
9960 "#,
9961 )
9962 .unwrap(),
9963 );
9964
9965 let text = "fn a() {}";
9966
9967 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9968 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9969 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9970 editor
9971 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9972 .await;
9973
9974 editor.update_in(cx, |editor, window, cx| {
9975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9976 s.select_ranges([
9977 MultiBufferOffset(5)..MultiBufferOffset(5),
9978 MultiBufferOffset(8)..MultiBufferOffset(8),
9979 MultiBufferOffset(9)..MultiBufferOffset(9),
9980 ])
9981 });
9982 editor.newline(&Newline, window, cx);
9983 assert_eq!(
9984 editor.text(cx),
9985 indoc!(
9986 "
9987 fn a(
9988
9989 ) {
9990
9991 }
9992 "
9993 )
9994 );
9995 assert_eq!(
9996 editor.selections.ranges(&editor.display_snapshot(cx)),
9997 &[
9998 Point::new(1, 0)..Point::new(1, 0),
9999 Point::new(3, 0)..Point::new(3, 0),
10000 Point::new(5, 0)..Point::new(5, 0)
10001 ]
10002 );
10003 });
10004}
10005
10006#[gpui::test]
10007async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10008 init_test(cx, |settings| {
10009 settings.defaults.auto_indent = Some(true);
10010 settings.languages.0.insert(
10011 "python".into(),
10012 LanguageSettingsContent {
10013 auto_indent: Some(false),
10014 ..Default::default()
10015 },
10016 );
10017 });
10018
10019 let mut cx = EditorTestContext::new(cx).await;
10020
10021 let injected_language = Arc::new(
10022 Language::new(
10023 LanguageConfig {
10024 brackets: BracketPairConfig {
10025 pairs: vec![
10026 BracketPair {
10027 start: "{".to_string(),
10028 end: "}".to_string(),
10029 close: false,
10030 surround: false,
10031 newline: true,
10032 },
10033 BracketPair {
10034 start: "(".to_string(),
10035 end: ")".to_string(),
10036 close: true,
10037 surround: false,
10038 newline: true,
10039 },
10040 ],
10041 ..Default::default()
10042 },
10043 name: "python".into(),
10044 ..Default::default()
10045 },
10046 Some(tree_sitter_python::LANGUAGE.into()),
10047 )
10048 .with_indents_query(
10049 r#"
10050 (_ "(" ")" @end) @indent
10051 (_ "{" "}" @end) @indent
10052 "#,
10053 )
10054 .unwrap(),
10055 );
10056
10057 let language = Arc::new(
10058 Language::new(
10059 LanguageConfig {
10060 brackets: BracketPairConfig {
10061 pairs: vec![
10062 BracketPair {
10063 start: "{".to_string(),
10064 end: "}".to_string(),
10065 close: false,
10066 surround: false,
10067 newline: true,
10068 },
10069 BracketPair {
10070 start: "(".to_string(),
10071 end: ")".to_string(),
10072 close: true,
10073 surround: false,
10074 newline: true,
10075 },
10076 ],
10077 ..Default::default()
10078 },
10079 name: LanguageName::new_static("rust"),
10080 ..Default::default()
10081 },
10082 Some(tree_sitter_rust::LANGUAGE.into()),
10083 )
10084 .with_indents_query(
10085 r#"
10086 (_ "(" ")" @end) @indent
10087 (_ "{" "}" @end) @indent
10088 "#,
10089 )
10090 .unwrap()
10091 .with_injection_query(
10092 r#"
10093 (macro_invocation
10094 macro: (identifier) @_macro_name
10095 (token_tree) @injection.content
10096 (#set! injection.language "python"))
10097 "#,
10098 )
10099 .unwrap(),
10100 );
10101
10102 cx.language_registry().add(injected_language);
10103 cx.language_registry().add(language.clone());
10104
10105 cx.update_buffer(|buffer, cx| {
10106 buffer.set_language(Some(language), cx);
10107 });
10108
10109 cx.set_state(r#"struct A {ˇ}"#);
10110
10111 cx.update_editor(|editor, window, cx| {
10112 editor.newline(&Default::default(), window, cx);
10113 });
10114
10115 cx.assert_editor_state(indoc!(
10116 "struct A {
10117 ˇ
10118 }"
10119 ));
10120
10121 cx.set_state(r#"select_biased!(ˇ)"#);
10122
10123 cx.update_editor(|editor, window, cx| {
10124 editor.newline(&Default::default(), window, cx);
10125 editor.handle_input("def ", window, cx);
10126 editor.handle_input("(", window, cx);
10127 editor.newline(&Default::default(), window, cx);
10128 editor.handle_input("a", window, cx);
10129 });
10130
10131 cx.assert_editor_state(indoc!(
10132 "select_biased!(
10133 def (
10134 aˇ
10135 )
10136 )"
10137 ));
10138}
10139
10140#[gpui::test]
10141async fn test_autoindent_selections(cx: &mut TestAppContext) {
10142 init_test(cx, |_| {});
10143
10144 {
10145 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10146 cx.set_state(indoc! {"
10147 impl A {
10148
10149 fn b() {}
10150
10151 «fn c() {
10152
10153 }ˇ»
10154 }
10155 "});
10156
10157 cx.update_editor(|editor, window, cx| {
10158 editor.autoindent(&Default::default(), window, cx);
10159 });
10160
10161 cx.assert_editor_state(indoc! {"
10162 impl A {
10163
10164 fn b() {}
10165
10166 «fn c() {
10167
10168 }ˇ»
10169 }
10170 "});
10171 }
10172
10173 {
10174 let mut cx = EditorTestContext::new_multibuffer(
10175 cx,
10176 [indoc! { "
10177 impl A {
10178 «
10179 // a
10180 fn b(){}
10181 »
10182 «
10183 }
10184 fn c(){}
10185 »
10186 "}],
10187 );
10188
10189 let buffer = cx.update_editor(|editor, _, cx| {
10190 let buffer = editor.buffer().update(cx, |buffer, _| {
10191 buffer.all_buffers().iter().next().unwrap().clone()
10192 });
10193 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10194 buffer
10195 });
10196
10197 cx.run_until_parked();
10198 cx.update_editor(|editor, window, cx| {
10199 editor.select_all(&Default::default(), window, cx);
10200 editor.autoindent(&Default::default(), window, cx)
10201 });
10202 cx.run_until_parked();
10203
10204 cx.update(|_, cx| {
10205 assert_eq!(
10206 buffer.read(cx).text(),
10207 indoc! { "
10208 impl A {
10209
10210 // a
10211 fn b(){}
10212
10213
10214 }
10215 fn c(){}
10216
10217 " }
10218 )
10219 });
10220 }
10221}
10222
10223#[gpui::test]
10224async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10225 init_test(cx, |_| {});
10226
10227 let mut cx = EditorTestContext::new(cx).await;
10228
10229 let language = Arc::new(Language::new(
10230 LanguageConfig {
10231 brackets: BracketPairConfig {
10232 pairs: vec![
10233 BracketPair {
10234 start: "{".to_string(),
10235 end: "}".to_string(),
10236 close: true,
10237 surround: true,
10238 newline: true,
10239 },
10240 BracketPair {
10241 start: "(".to_string(),
10242 end: ")".to_string(),
10243 close: true,
10244 surround: true,
10245 newline: true,
10246 },
10247 BracketPair {
10248 start: "/*".to_string(),
10249 end: " */".to_string(),
10250 close: true,
10251 surround: true,
10252 newline: true,
10253 },
10254 BracketPair {
10255 start: "[".to_string(),
10256 end: "]".to_string(),
10257 close: false,
10258 surround: false,
10259 newline: true,
10260 },
10261 BracketPair {
10262 start: "\"".to_string(),
10263 end: "\"".to_string(),
10264 close: true,
10265 surround: true,
10266 newline: false,
10267 },
10268 BracketPair {
10269 start: "<".to_string(),
10270 end: ">".to_string(),
10271 close: false,
10272 surround: true,
10273 newline: true,
10274 },
10275 ],
10276 ..Default::default()
10277 },
10278 autoclose_before: "})]".to_string(),
10279 ..Default::default()
10280 },
10281 Some(tree_sitter_rust::LANGUAGE.into()),
10282 ));
10283
10284 cx.language_registry().add(language.clone());
10285 cx.update_buffer(|buffer, cx| {
10286 buffer.set_language(Some(language), cx);
10287 });
10288
10289 cx.set_state(
10290 &r#"
10291 🏀ˇ
10292 εˇ
10293 ❤️ˇ
10294 "#
10295 .unindent(),
10296 );
10297
10298 // autoclose multiple nested brackets at multiple cursors
10299 cx.update_editor(|editor, window, cx| {
10300 editor.handle_input("{", window, cx);
10301 editor.handle_input("{", window, cx);
10302 editor.handle_input("{", window, cx);
10303 });
10304 cx.assert_editor_state(
10305 &"
10306 🏀{{{ˇ}}}
10307 ε{{{ˇ}}}
10308 ❤️{{{ˇ}}}
10309 "
10310 .unindent(),
10311 );
10312
10313 // insert a different closing bracket
10314 cx.update_editor(|editor, window, cx| {
10315 editor.handle_input(")", window, cx);
10316 });
10317 cx.assert_editor_state(
10318 &"
10319 🏀{{{)ˇ}}}
10320 ε{{{)ˇ}}}
10321 ❤️{{{)ˇ}}}
10322 "
10323 .unindent(),
10324 );
10325
10326 // skip over the auto-closed brackets when typing a closing bracket
10327 cx.update_editor(|editor, window, cx| {
10328 editor.move_right(&MoveRight, window, cx);
10329 editor.handle_input("}", window, cx);
10330 editor.handle_input("}", window, cx);
10331 editor.handle_input("}", window, cx);
10332 });
10333 cx.assert_editor_state(
10334 &"
10335 🏀{{{)}}}}ˇ
10336 ε{{{)}}}}ˇ
10337 ❤️{{{)}}}}ˇ
10338 "
10339 .unindent(),
10340 );
10341
10342 // autoclose multi-character pairs
10343 cx.set_state(
10344 &"
10345 ˇ
10346 ˇ
10347 "
10348 .unindent(),
10349 );
10350 cx.update_editor(|editor, window, cx| {
10351 editor.handle_input("/", window, cx);
10352 editor.handle_input("*", window, cx);
10353 });
10354 cx.assert_editor_state(
10355 &"
10356 /*ˇ */
10357 /*ˇ */
10358 "
10359 .unindent(),
10360 );
10361
10362 // one cursor autocloses a multi-character pair, one cursor
10363 // does not autoclose.
10364 cx.set_state(
10365 &"
10366 /ˇ
10367 ˇ
10368 "
10369 .unindent(),
10370 );
10371 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10372 cx.assert_editor_state(
10373 &"
10374 /*ˇ */
10375 *ˇ
10376 "
10377 .unindent(),
10378 );
10379
10380 // Don't autoclose if the next character isn't whitespace and isn't
10381 // listed in the language's "autoclose_before" section.
10382 cx.set_state("ˇa b");
10383 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10384 cx.assert_editor_state("{ˇa b");
10385
10386 // Don't autoclose if `close` is false for the bracket pair
10387 cx.set_state("ˇ");
10388 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10389 cx.assert_editor_state("[ˇ");
10390
10391 // Surround with brackets if text is selected
10392 cx.set_state("«aˇ» b");
10393 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10394 cx.assert_editor_state("{«aˇ»} b");
10395
10396 // Autoclose when not immediately after a word character
10397 cx.set_state("a ˇ");
10398 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10399 cx.assert_editor_state("a \"ˇ\"");
10400
10401 // Autoclose pair where the start and end characters are the same
10402 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10403 cx.assert_editor_state("a \"\"ˇ");
10404
10405 // Don't autoclose when immediately after a word character
10406 cx.set_state("aˇ");
10407 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10408 cx.assert_editor_state("a\"ˇ");
10409
10410 // Do autoclose when after a non-word character
10411 cx.set_state("{ˇ");
10412 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10413 cx.assert_editor_state("{\"ˇ\"");
10414
10415 // Non identical pairs autoclose regardless of preceding character
10416 cx.set_state("aˇ");
10417 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10418 cx.assert_editor_state("a{ˇ}");
10419
10420 // Don't autoclose pair if autoclose is disabled
10421 cx.set_state("ˇ");
10422 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10423 cx.assert_editor_state("<ˇ");
10424
10425 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10426 cx.set_state("«aˇ» b");
10427 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10428 cx.assert_editor_state("<«aˇ»> b");
10429}
10430
10431#[gpui::test]
10432async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10433 init_test(cx, |settings| {
10434 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10435 });
10436
10437 let mut cx = EditorTestContext::new(cx).await;
10438
10439 let language = Arc::new(Language::new(
10440 LanguageConfig {
10441 brackets: BracketPairConfig {
10442 pairs: vec![
10443 BracketPair {
10444 start: "{".to_string(),
10445 end: "}".to_string(),
10446 close: true,
10447 surround: true,
10448 newline: true,
10449 },
10450 BracketPair {
10451 start: "(".to_string(),
10452 end: ")".to_string(),
10453 close: true,
10454 surround: true,
10455 newline: true,
10456 },
10457 BracketPair {
10458 start: "[".to_string(),
10459 end: "]".to_string(),
10460 close: false,
10461 surround: false,
10462 newline: true,
10463 },
10464 ],
10465 ..Default::default()
10466 },
10467 autoclose_before: "})]".to_string(),
10468 ..Default::default()
10469 },
10470 Some(tree_sitter_rust::LANGUAGE.into()),
10471 ));
10472
10473 cx.language_registry().add(language.clone());
10474 cx.update_buffer(|buffer, cx| {
10475 buffer.set_language(Some(language), cx);
10476 });
10477
10478 cx.set_state(
10479 &"
10480 ˇ
10481 ˇ
10482 ˇ
10483 "
10484 .unindent(),
10485 );
10486
10487 // ensure only matching closing brackets are skipped over
10488 cx.update_editor(|editor, window, cx| {
10489 editor.handle_input("}", window, cx);
10490 editor.move_left(&MoveLeft, window, cx);
10491 editor.handle_input(")", window, cx);
10492 editor.move_left(&MoveLeft, window, cx);
10493 });
10494 cx.assert_editor_state(
10495 &"
10496 ˇ)}
10497 ˇ)}
10498 ˇ)}
10499 "
10500 .unindent(),
10501 );
10502
10503 // skip-over closing brackets at multiple cursors
10504 cx.update_editor(|editor, window, cx| {
10505 editor.handle_input(")", window, cx);
10506 editor.handle_input("}", window, cx);
10507 });
10508 cx.assert_editor_state(
10509 &"
10510 )}ˇ
10511 )}ˇ
10512 )}ˇ
10513 "
10514 .unindent(),
10515 );
10516
10517 // ignore non-close brackets
10518 cx.update_editor(|editor, window, cx| {
10519 editor.handle_input("]", window, cx);
10520 editor.move_left(&MoveLeft, window, cx);
10521 editor.handle_input("]", window, cx);
10522 });
10523 cx.assert_editor_state(
10524 &"
10525 )}]ˇ]
10526 )}]ˇ]
10527 )}]ˇ]
10528 "
10529 .unindent(),
10530 );
10531}
10532
10533#[gpui::test]
10534async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10535 init_test(cx, |_| {});
10536
10537 let mut cx = EditorTestContext::new(cx).await;
10538
10539 let html_language = Arc::new(
10540 Language::new(
10541 LanguageConfig {
10542 name: "HTML".into(),
10543 brackets: BracketPairConfig {
10544 pairs: vec![
10545 BracketPair {
10546 start: "<".into(),
10547 end: ">".into(),
10548 close: true,
10549 ..Default::default()
10550 },
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 ],
10564 ..Default::default()
10565 },
10566 autoclose_before: "})]>".into(),
10567 ..Default::default()
10568 },
10569 Some(tree_sitter_html::LANGUAGE.into()),
10570 )
10571 .with_injection_query(
10572 r#"
10573 (script_element
10574 (raw_text) @injection.content
10575 (#set! injection.language "javascript"))
10576 "#,
10577 )
10578 .unwrap(),
10579 );
10580
10581 let javascript_language = Arc::new(Language::new(
10582 LanguageConfig {
10583 name: "JavaScript".into(),
10584 brackets: BracketPairConfig {
10585 pairs: vec![
10586 BracketPair {
10587 start: "/*".into(),
10588 end: " */".into(),
10589 close: true,
10590 ..Default::default()
10591 },
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 ],
10605 ..Default::default()
10606 },
10607 autoclose_before: "})]>".into(),
10608 ..Default::default()
10609 },
10610 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10611 ));
10612
10613 cx.language_registry().add(html_language.clone());
10614 cx.language_registry().add(javascript_language);
10615 cx.executor().run_until_parked();
10616
10617 cx.update_buffer(|buffer, cx| {
10618 buffer.set_language(Some(html_language), cx);
10619 });
10620
10621 cx.set_state(
10622 &r#"
10623 <body>ˇ
10624 <script>
10625 var x = 1;ˇ
10626 </script>
10627 </body>ˇ
10628 "#
10629 .unindent(),
10630 );
10631
10632 // Precondition: different languages are active at different locations.
10633 cx.update_editor(|editor, window, cx| {
10634 let snapshot = editor.snapshot(window, cx);
10635 let cursors = editor
10636 .selections
10637 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10638 let languages = cursors
10639 .iter()
10640 .map(|c| snapshot.language_at(c.start).unwrap().name())
10641 .collect::<Vec<_>>();
10642 assert_eq!(
10643 languages,
10644 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10645 );
10646 });
10647
10648 // Angle brackets autoclose in HTML, but not JavaScript.
10649 cx.update_editor(|editor, window, cx| {
10650 editor.handle_input("<", window, cx);
10651 editor.handle_input("a", window, cx);
10652 });
10653 cx.assert_editor_state(
10654 &r#"
10655 <body><aˇ>
10656 <script>
10657 var x = 1;<aˇ
10658 </script>
10659 </body><aˇ>
10660 "#
10661 .unindent(),
10662 );
10663
10664 // Curly braces and parens autoclose in both HTML and JavaScript.
10665 cx.update_editor(|editor, window, cx| {
10666 editor.handle_input(" b=", window, cx);
10667 editor.handle_input("{", window, cx);
10668 editor.handle_input("c", window, cx);
10669 editor.handle_input("(", window, cx);
10670 });
10671 cx.assert_editor_state(
10672 &r#"
10673 <body><a b={c(ˇ)}>
10674 <script>
10675 var x = 1;<a b={c(ˇ)}
10676 </script>
10677 </body><a b={c(ˇ)}>
10678 "#
10679 .unindent(),
10680 );
10681
10682 // Brackets that were already autoclosed are skipped.
10683 cx.update_editor(|editor, window, cx| {
10684 editor.handle_input(")", window, cx);
10685 editor.handle_input("d", window, cx);
10686 editor.handle_input("}", window, cx);
10687 });
10688 cx.assert_editor_state(
10689 &r#"
10690 <body><a b={c()d}ˇ>
10691 <script>
10692 var x = 1;<a b={c()d}ˇ
10693 </script>
10694 </body><a b={c()d}ˇ>
10695 "#
10696 .unindent(),
10697 );
10698 cx.update_editor(|editor, window, cx| {
10699 editor.handle_input(">", window, cx);
10700 });
10701 cx.assert_editor_state(
10702 &r#"
10703 <body><a b={c()d}>ˇ
10704 <script>
10705 var x = 1;<a b={c()d}>ˇ
10706 </script>
10707 </body><a b={c()d}>ˇ
10708 "#
10709 .unindent(),
10710 );
10711
10712 // Reset
10713 cx.set_state(
10714 &r#"
10715 <body>ˇ
10716 <script>
10717 var x = 1;ˇ
10718 </script>
10719 </body>ˇ
10720 "#
10721 .unindent(),
10722 );
10723
10724 cx.update_editor(|editor, window, cx| {
10725 editor.handle_input("<", window, cx);
10726 });
10727 cx.assert_editor_state(
10728 &r#"
10729 <body><ˇ>
10730 <script>
10731 var x = 1;<ˇ
10732 </script>
10733 </body><ˇ>
10734 "#
10735 .unindent(),
10736 );
10737
10738 // When backspacing, the closing angle brackets are removed.
10739 cx.update_editor(|editor, window, cx| {
10740 editor.backspace(&Backspace, window, cx);
10741 });
10742 cx.assert_editor_state(
10743 &r#"
10744 <body>ˇ
10745 <script>
10746 var x = 1;ˇ
10747 </script>
10748 </body>ˇ
10749 "#
10750 .unindent(),
10751 );
10752
10753 // Block comments autoclose in JavaScript, but not HTML.
10754 cx.update_editor(|editor, window, cx| {
10755 editor.handle_input("/", window, cx);
10756 editor.handle_input("*", window, cx);
10757 });
10758 cx.assert_editor_state(
10759 &r#"
10760 <body>/*ˇ
10761 <script>
10762 var x = 1;/*ˇ */
10763 </script>
10764 </body>/*ˇ
10765 "#
10766 .unindent(),
10767 );
10768}
10769
10770#[gpui::test]
10771async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10772 init_test(cx, |_| {});
10773
10774 let mut cx = EditorTestContext::new(cx).await;
10775
10776 let rust_language = Arc::new(
10777 Language::new(
10778 LanguageConfig {
10779 name: "Rust".into(),
10780 brackets: serde_json::from_value(json!([
10781 { "start": "{", "end": "}", "close": true, "newline": true },
10782 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10783 ]))
10784 .unwrap(),
10785 autoclose_before: "})]>".into(),
10786 ..Default::default()
10787 },
10788 Some(tree_sitter_rust::LANGUAGE.into()),
10789 )
10790 .with_override_query("(string_literal) @string")
10791 .unwrap(),
10792 );
10793
10794 cx.language_registry().add(rust_language.clone());
10795 cx.update_buffer(|buffer, cx| {
10796 buffer.set_language(Some(rust_language), cx);
10797 });
10798
10799 cx.set_state(
10800 &r#"
10801 let x = ˇ
10802 "#
10803 .unindent(),
10804 );
10805
10806 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10807 cx.update_editor(|editor, window, cx| {
10808 editor.handle_input("\"", window, cx);
10809 });
10810 cx.assert_editor_state(
10811 &r#"
10812 let x = "ˇ"
10813 "#
10814 .unindent(),
10815 );
10816
10817 // Inserting another quotation mark. The cursor moves across the existing
10818 // automatically-inserted quotation mark.
10819 cx.update_editor(|editor, window, cx| {
10820 editor.handle_input("\"", window, cx);
10821 });
10822 cx.assert_editor_state(
10823 &r#"
10824 let x = ""ˇ
10825 "#
10826 .unindent(),
10827 );
10828
10829 // Reset
10830 cx.set_state(
10831 &r#"
10832 let x = ˇ
10833 "#
10834 .unindent(),
10835 );
10836
10837 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10838 cx.update_editor(|editor, window, cx| {
10839 editor.handle_input("\"", window, cx);
10840 editor.handle_input(" ", window, cx);
10841 editor.move_left(&Default::default(), window, cx);
10842 editor.handle_input("\\", window, cx);
10843 editor.handle_input("\"", window, cx);
10844 });
10845 cx.assert_editor_state(
10846 &r#"
10847 let x = "\"ˇ "
10848 "#
10849 .unindent(),
10850 );
10851
10852 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10853 // mark. Nothing is inserted.
10854 cx.update_editor(|editor, window, cx| {
10855 editor.move_right(&Default::default(), window, cx);
10856 editor.handle_input("\"", window, cx);
10857 });
10858 cx.assert_editor_state(
10859 &r#"
10860 let x = "\" "ˇ
10861 "#
10862 .unindent(),
10863 );
10864}
10865
10866#[gpui::test]
10867async fn test_surround_with_pair(cx: &mut TestAppContext) {
10868 init_test(cx, |_| {});
10869
10870 let language = Arc::new(Language::new(
10871 LanguageConfig {
10872 brackets: BracketPairConfig {
10873 pairs: vec![
10874 BracketPair {
10875 start: "{".to_string(),
10876 end: "}".to_string(),
10877 close: true,
10878 surround: true,
10879 newline: true,
10880 },
10881 BracketPair {
10882 start: "/* ".to_string(),
10883 end: "*/".to_string(),
10884 close: true,
10885 surround: true,
10886 ..Default::default()
10887 },
10888 ],
10889 ..Default::default()
10890 },
10891 ..Default::default()
10892 },
10893 Some(tree_sitter_rust::LANGUAGE.into()),
10894 ));
10895
10896 let text = r#"
10897 a
10898 b
10899 c
10900 "#
10901 .unindent();
10902
10903 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10904 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10905 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10906 editor
10907 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10908 .await;
10909
10910 editor.update_in(cx, |editor, window, cx| {
10911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10912 s.select_display_ranges([
10913 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10914 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10915 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10916 ])
10917 });
10918
10919 editor.handle_input("{", window, cx);
10920 editor.handle_input("{", window, cx);
10921 editor.handle_input("{", window, cx);
10922 assert_eq!(
10923 editor.text(cx),
10924 "
10925 {{{a}}}
10926 {{{b}}}
10927 {{{c}}}
10928 "
10929 .unindent()
10930 );
10931 assert_eq!(
10932 display_ranges(editor, cx),
10933 [
10934 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10935 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10936 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10937 ]
10938 );
10939
10940 editor.undo(&Undo, window, cx);
10941 editor.undo(&Undo, window, cx);
10942 editor.undo(&Undo, window, cx);
10943 assert_eq!(
10944 editor.text(cx),
10945 "
10946 a
10947 b
10948 c
10949 "
10950 .unindent()
10951 );
10952 assert_eq!(
10953 display_ranges(editor, cx),
10954 [
10955 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10956 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10957 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10958 ]
10959 );
10960
10961 // Ensure inserting the first character of a multi-byte bracket pair
10962 // doesn't surround the selections with the bracket.
10963 editor.handle_input("/", window, cx);
10964 assert_eq!(
10965 editor.text(cx),
10966 "
10967 /
10968 /
10969 /
10970 "
10971 .unindent()
10972 );
10973 assert_eq!(
10974 display_ranges(editor, cx),
10975 [
10976 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10977 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10978 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10979 ]
10980 );
10981
10982 editor.undo(&Undo, window, cx);
10983 assert_eq!(
10984 editor.text(cx),
10985 "
10986 a
10987 b
10988 c
10989 "
10990 .unindent()
10991 );
10992 assert_eq!(
10993 display_ranges(editor, cx),
10994 [
10995 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10996 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10997 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10998 ]
10999 );
11000
11001 // Ensure inserting the last character of a multi-byte bracket pair
11002 // doesn't surround the selections with the bracket.
11003 editor.handle_input("*", window, cx);
11004 assert_eq!(
11005 editor.text(cx),
11006 "
11007 *
11008 *
11009 *
11010 "
11011 .unindent()
11012 );
11013 assert_eq!(
11014 display_ranges(editor, cx),
11015 [
11016 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11017 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11018 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11019 ]
11020 );
11021 });
11022}
11023
11024#[gpui::test]
11025async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11026 init_test(cx, |_| {});
11027
11028 let language = Arc::new(Language::new(
11029 LanguageConfig {
11030 brackets: BracketPairConfig {
11031 pairs: vec![BracketPair {
11032 start: "{".to_string(),
11033 end: "}".to_string(),
11034 close: true,
11035 surround: true,
11036 newline: true,
11037 }],
11038 ..Default::default()
11039 },
11040 autoclose_before: "}".to_string(),
11041 ..Default::default()
11042 },
11043 Some(tree_sitter_rust::LANGUAGE.into()),
11044 ));
11045
11046 let text = r#"
11047 a
11048 b
11049 c
11050 "#
11051 .unindent();
11052
11053 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11054 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11055 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11056 editor
11057 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11058 .await;
11059
11060 editor.update_in(cx, |editor, window, cx| {
11061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11062 s.select_ranges([
11063 Point::new(0, 1)..Point::new(0, 1),
11064 Point::new(1, 1)..Point::new(1, 1),
11065 Point::new(2, 1)..Point::new(2, 1),
11066 ])
11067 });
11068
11069 editor.handle_input("{", window, cx);
11070 editor.handle_input("{", window, cx);
11071 editor.handle_input("_", window, cx);
11072 assert_eq!(
11073 editor.text(cx),
11074 "
11075 a{{_}}
11076 b{{_}}
11077 c{{_}}
11078 "
11079 .unindent()
11080 );
11081 assert_eq!(
11082 editor
11083 .selections
11084 .ranges::<Point>(&editor.display_snapshot(cx)),
11085 [
11086 Point::new(0, 4)..Point::new(0, 4),
11087 Point::new(1, 4)..Point::new(1, 4),
11088 Point::new(2, 4)..Point::new(2, 4)
11089 ]
11090 );
11091
11092 editor.backspace(&Default::default(), window, cx);
11093 editor.backspace(&Default::default(), window, cx);
11094 assert_eq!(
11095 editor.text(cx),
11096 "
11097 a{}
11098 b{}
11099 c{}
11100 "
11101 .unindent()
11102 );
11103 assert_eq!(
11104 editor
11105 .selections
11106 .ranges::<Point>(&editor.display_snapshot(cx)),
11107 [
11108 Point::new(0, 2)..Point::new(0, 2),
11109 Point::new(1, 2)..Point::new(1, 2),
11110 Point::new(2, 2)..Point::new(2, 2)
11111 ]
11112 );
11113
11114 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11115 assert_eq!(
11116 editor.text(cx),
11117 "
11118 a
11119 b
11120 c
11121 "
11122 .unindent()
11123 );
11124 assert_eq!(
11125 editor
11126 .selections
11127 .ranges::<Point>(&editor.display_snapshot(cx)),
11128 [
11129 Point::new(0, 1)..Point::new(0, 1),
11130 Point::new(1, 1)..Point::new(1, 1),
11131 Point::new(2, 1)..Point::new(2, 1)
11132 ]
11133 );
11134 });
11135}
11136
11137#[gpui::test]
11138async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11139 init_test(cx, |settings| {
11140 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11141 });
11142
11143 let mut cx = EditorTestContext::new(cx).await;
11144
11145 let language = Arc::new(Language::new(
11146 LanguageConfig {
11147 brackets: BracketPairConfig {
11148 pairs: vec![
11149 BracketPair {
11150 start: "{".to_string(),
11151 end: "}".to_string(),
11152 close: true,
11153 surround: true,
11154 newline: true,
11155 },
11156 BracketPair {
11157 start: "(".to_string(),
11158 end: ")".to_string(),
11159 close: true,
11160 surround: true,
11161 newline: true,
11162 },
11163 BracketPair {
11164 start: "[".to_string(),
11165 end: "]".to_string(),
11166 close: false,
11167 surround: true,
11168 newline: true,
11169 },
11170 ],
11171 ..Default::default()
11172 },
11173 autoclose_before: "})]".to_string(),
11174 ..Default::default()
11175 },
11176 Some(tree_sitter_rust::LANGUAGE.into()),
11177 ));
11178
11179 cx.language_registry().add(language.clone());
11180 cx.update_buffer(|buffer, cx| {
11181 buffer.set_language(Some(language), cx);
11182 });
11183
11184 cx.set_state(
11185 &"
11186 {(ˇ)}
11187 [[ˇ]]
11188 {(ˇ)}
11189 "
11190 .unindent(),
11191 );
11192
11193 cx.update_editor(|editor, window, cx| {
11194 editor.backspace(&Default::default(), window, cx);
11195 editor.backspace(&Default::default(), window, cx);
11196 });
11197
11198 cx.assert_editor_state(
11199 &"
11200 ˇ
11201 ˇ]]
11202 ˇ
11203 "
11204 .unindent(),
11205 );
11206
11207 cx.update_editor(|editor, window, cx| {
11208 editor.handle_input("{", window, cx);
11209 editor.handle_input("{", window, cx);
11210 editor.move_right(&MoveRight, window, cx);
11211 editor.move_right(&MoveRight, window, cx);
11212 editor.move_left(&MoveLeft, window, cx);
11213 editor.move_left(&MoveLeft, window, cx);
11214 editor.backspace(&Default::default(), window, cx);
11215 });
11216
11217 cx.assert_editor_state(
11218 &"
11219 {ˇ}
11220 {ˇ}]]
11221 {ˇ}
11222 "
11223 .unindent(),
11224 );
11225
11226 cx.update_editor(|editor, window, cx| {
11227 editor.backspace(&Default::default(), window, cx);
11228 });
11229
11230 cx.assert_editor_state(
11231 &"
11232 ˇ
11233 ˇ]]
11234 ˇ
11235 "
11236 .unindent(),
11237 );
11238}
11239
11240#[gpui::test]
11241async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11242 init_test(cx, |_| {});
11243
11244 let language = Arc::new(Language::new(
11245 LanguageConfig::default(),
11246 Some(tree_sitter_rust::LANGUAGE.into()),
11247 ));
11248
11249 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11250 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11251 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11252 editor
11253 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11254 .await;
11255
11256 editor.update_in(cx, |editor, window, cx| {
11257 editor.set_auto_replace_emoji_shortcode(true);
11258
11259 editor.handle_input("Hello ", window, cx);
11260 editor.handle_input(":wave", window, cx);
11261 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11262
11263 editor.handle_input(":", window, cx);
11264 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11265
11266 editor.handle_input(" :smile", window, cx);
11267 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11268
11269 editor.handle_input(":", window, cx);
11270 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11271
11272 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11273 editor.handle_input(":wave", window, cx);
11274 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11275
11276 editor.handle_input(":", window, cx);
11277 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11278
11279 editor.handle_input(":1", window, cx);
11280 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11281
11282 editor.handle_input(":", window, cx);
11283 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11284
11285 // Ensure shortcode does not get replaced when it is part of a word
11286 editor.handle_input(" Test:wave", window, cx);
11287 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11288
11289 editor.handle_input(":", window, cx);
11290 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11291
11292 editor.set_auto_replace_emoji_shortcode(false);
11293
11294 // Ensure shortcode does not get replaced when auto replace is off
11295 editor.handle_input(" :wave", window, cx);
11296 assert_eq!(
11297 editor.text(cx),
11298 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11299 );
11300
11301 editor.handle_input(":", window, cx);
11302 assert_eq!(
11303 editor.text(cx),
11304 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11305 );
11306 });
11307}
11308
11309#[gpui::test]
11310async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11311 init_test(cx, |_| {});
11312
11313 let (text, insertion_ranges) = marked_text_ranges(
11314 indoc! {"
11315 ˇ
11316 "},
11317 false,
11318 );
11319
11320 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11321 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11322
11323 _ = editor.update_in(cx, |editor, window, cx| {
11324 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11325
11326 editor
11327 .insert_snippet(
11328 &insertion_ranges
11329 .iter()
11330 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11331 .collect::<Vec<_>>(),
11332 snippet,
11333 window,
11334 cx,
11335 )
11336 .unwrap();
11337
11338 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11339 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11340 assert_eq!(editor.text(cx), expected_text);
11341 assert_eq!(
11342 editor.selections.ranges(&editor.display_snapshot(cx)),
11343 selection_ranges
11344 .iter()
11345 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11346 .collect::<Vec<_>>()
11347 );
11348 }
11349
11350 assert(
11351 editor,
11352 cx,
11353 indoc! {"
11354 type «» =•
11355 "},
11356 );
11357
11358 assert!(editor.context_menu_visible(), "There should be a matches");
11359 });
11360}
11361
11362#[gpui::test]
11363async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11364 init_test(cx, |_| {});
11365
11366 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11367 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11368 assert_eq!(editor.text(cx), expected_text);
11369 assert_eq!(
11370 editor.selections.ranges(&editor.display_snapshot(cx)),
11371 selection_ranges
11372 .iter()
11373 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11374 .collect::<Vec<_>>()
11375 );
11376 }
11377
11378 let (text, insertion_ranges) = marked_text_ranges(
11379 indoc! {"
11380 ˇ
11381 "},
11382 false,
11383 );
11384
11385 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11386 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11387
11388 _ = editor.update_in(cx, |editor, window, cx| {
11389 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11390
11391 editor
11392 .insert_snippet(
11393 &insertion_ranges
11394 .iter()
11395 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11396 .collect::<Vec<_>>(),
11397 snippet,
11398 window,
11399 cx,
11400 )
11401 .unwrap();
11402
11403 assert_state(
11404 editor,
11405 cx,
11406 indoc! {"
11407 type «» = ;•
11408 "},
11409 );
11410
11411 assert!(
11412 editor.context_menu_visible(),
11413 "Context menu should be visible for placeholder choices"
11414 );
11415
11416 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11417
11418 assert_state(
11419 editor,
11420 cx,
11421 indoc! {"
11422 type = «»;•
11423 "},
11424 );
11425
11426 assert!(
11427 !editor.context_menu_visible(),
11428 "Context menu should be hidden after moving to next tabstop"
11429 );
11430
11431 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11432
11433 assert_state(
11434 editor,
11435 cx,
11436 indoc! {"
11437 type = ; ˇ
11438 "},
11439 );
11440
11441 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11442
11443 assert_state(
11444 editor,
11445 cx,
11446 indoc! {"
11447 type = ; ˇ
11448 "},
11449 );
11450 });
11451
11452 _ = editor.update_in(cx, |editor, window, cx| {
11453 editor.select_all(&SelectAll, window, cx);
11454 editor.backspace(&Backspace, window, cx);
11455
11456 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11457 let insertion_ranges = editor
11458 .selections
11459 .all(&editor.display_snapshot(cx))
11460 .iter()
11461 .map(|s| s.range())
11462 .collect::<Vec<_>>();
11463
11464 editor
11465 .insert_snippet(&insertion_ranges, snippet, window, cx)
11466 .unwrap();
11467
11468 assert_state(editor, cx, "fn «» = value;•");
11469
11470 assert!(
11471 editor.context_menu_visible(),
11472 "Context menu should be visible for placeholder choices"
11473 );
11474
11475 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11476
11477 assert_state(editor, cx, "fn = «valueˇ»;•");
11478
11479 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11480
11481 assert_state(editor, cx, "fn «» = value;•");
11482
11483 assert!(
11484 editor.context_menu_visible(),
11485 "Context menu should be visible again after returning to first tabstop"
11486 );
11487
11488 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11489
11490 assert_state(editor, cx, "fn «» = value;•");
11491 });
11492}
11493
11494#[gpui::test]
11495async fn test_snippets(cx: &mut TestAppContext) {
11496 init_test(cx, |_| {});
11497
11498 let mut cx = EditorTestContext::new(cx).await;
11499
11500 cx.set_state(indoc! {"
11501 a.ˇ b
11502 a.ˇ b
11503 a.ˇ b
11504 "});
11505
11506 cx.update_editor(|editor, window, cx| {
11507 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11508 let insertion_ranges = editor
11509 .selections
11510 .all(&editor.display_snapshot(cx))
11511 .iter()
11512 .map(|s| s.range())
11513 .collect::<Vec<_>>();
11514 editor
11515 .insert_snippet(&insertion_ranges, snippet, window, cx)
11516 .unwrap();
11517 });
11518
11519 cx.assert_editor_state(indoc! {"
11520 a.f(«oneˇ», two, «threeˇ») b
11521 a.f(«oneˇ», two, «threeˇ») b
11522 a.f(«oneˇ», two, «threeˇ») b
11523 "});
11524
11525 // Can't move earlier than the first tab stop
11526 cx.update_editor(|editor, window, cx| {
11527 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11528 });
11529 cx.assert_editor_state(indoc! {"
11530 a.f(«oneˇ», two, «threeˇ») b
11531 a.f(«oneˇ», two, «threeˇ») b
11532 a.f(«oneˇ», two, «threeˇ») b
11533 "});
11534
11535 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11536 cx.assert_editor_state(indoc! {"
11537 a.f(one, «twoˇ», three) b
11538 a.f(one, «twoˇ», three) b
11539 a.f(one, «twoˇ», three) b
11540 "});
11541
11542 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11543 cx.assert_editor_state(indoc! {"
11544 a.f(«oneˇ», two, «threeˇ») b
11545 a.f(«oneˇ», two, «threeˇ») b
11546 a.f(«oneˇ», two, «threeˇ») b
11547 "});
11548
11549 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11550 cx.assert_editor_state(indoc! {"
11551 a.f(one, «twoˇ», three) b
11552 a.f(one, «twoˇ», three) b
11553 a.f(one, «twoˇ», three) b
11554 "});
11555 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11556 cx.assert_editor_state(indoc! {"
11557 a.f(one, two, three)ˇ b
11558 a.f(one, two, three)ˇ b
11559 a.f(one, two, three)ˇ b
11560 "});
11561
11562 // As soon as the last tab stop is reached, snippet state is gone
11563 cx.update_editor(|editor, window, cx| {
11564 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11565 });
11566 cx.assert_editor_state(indoc! {"
11567 a.f(one, two, three)ˇ b
11568 a.f(one, two, three)ˇ b
11569 a.f(one, two, three)ˇ b
11570 "});
11571}
11572
11573#[gpui::test]
11574async fn test_snippet_indentation(cx: &mut TestAppContext) {
11575 init_test(cx, |_| {});
11576
11577 let mut cx = EditorTestContext::new(cx).await;
11578
11579 cx.update_editor(|editor, window, cx| {
11580 let snippet = Snippet::parse(indoc! {"
11581 /*
11582 * Multiline comment with leading indentation
11583 *
11584 * $1
11585 */
11586 $0"})
11587 .unwrap();
11588 let insertion_ranges = editor
11589 .selections
11590 .all(&editor.display_snapshot(cx))
11591 .iter()
11592 .map(|s| s.range())
11593 .collect::<Vec<_>>();
11594 editor
11595 .insert_snippet(&insertion_ranges, snippet, window, cx)
11596 .unwrap();
11597 });
11598
11599 cx.assert_editor_state(indoc! {"
11600 /*
11601 * Multiline comment with leading indentation
11602 *
11603 * ˇ
11604 */
11605 "});
11606
11607 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11608 cx.assert_editor_state(indoc! {"
11609 /*
11610 * Multiline comment with leading indentation
11611 *
11612 *•
11613 */
11614 ˇ"});
11615}
11616
11617#[gpui::test]
11618async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11619 init_test(cx, |_| {});
11620
11621 let mut cx = EditorTestContext::new(cx).await;
11622 cx.update_editor(|editor, _, cx| {
11623 editor.project().unwrap().update(cx, |project, cx| {
11624 project.snippets().update(cx, |snippets, _cx| {
11625 let snippet = project::snippet_provider::Snippet {
11626 prefix: vec!["multi word".to_string()],
11627 body: "this is many words".to_string(),
11628 description: Some("description".to_string()),
11629 name: "multi-word snippet test".to_string(),
11630 };
11631 snippets.add_snippet_for_test(
11632 None,
11633 PathBuf::from("test_snippets.json"),
11634 vec![Arc::new(snippet)],
11635 );
11636 });
11637 })
11638 });
11639
11640 for (input_to_simulate, should_match_snippet) in [
11641 ("m", true),
11642 ("m ", true),
11643 ("m w", true),
11644 ("aa m w", true),
11645 ("aa m g", false),
11646 ] {
11647 cx.set_state("ˇ");
11648 cx.simulate_input(input_to_simulate); // fails correctly
11649
11650 cx.update_editor(|editor, _, _| {
11651 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11652 else {
11653 assert!(!should_match_snippet); // no completions! don't even show the menu
11654 return;
11655 };
11656 assert!(context_menu.visible());
11657 let completions = context_menu.completions.borrow();
11658
11659 assert_eq!(!completions.is_empty(), should_match_snippet);
11660 });
11661 }
11662}
11663
11664#[gpui::test]
11665async fn test_document_format_during_save(cx: &mut TestAppContext) {
11666 init_test(cx, |_| {});
11667
11668 let fs = FakeFs::new(cx.executor());
11669 fs.insert_file(path!("/file.rs"), Default::default()).await;
11670
11671 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11672
11673 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11674 language_registry.add(rust_lang());
11675 let mut fake_servers = language_registry.register_fake_lsp(
11676 "Rust",
11677 FakeLspAdapter {
11678 capabilities: lsp::ServerCapabilities {
11679 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11680 ..Default::default()
11681 },
11682 ..Default::default()
11683 },
11684 );
11685
11686 let buffer = project
11687 .update(cx, |project, cx| {
11688 project.open_local_buffer(path!("/file.rs"), cx)
11689 })
11690 .await
11691 .unwrap();
11692
11693 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11694 let (editor, cx) = cx.add_window_view(|window, cx| {
11695 build_editor_with_project(project.clone(), buffer, window, cx)
11696 });
11697 editor.update_in(cx, |editor, window, cx| {
11698 editor.set_text("one\ntwo\nthree\n", window, cx)
11699 });
11700 assert!(cx.read(|cx| editor.is_dirty(cx)));
11701
11702 cx.executor().start_waiting();
11703 let fake_server = fake_servers.next().await.unwrap();
11704
11705 {
11706 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11707 move |params, _| async move {
11708 assert_eq!(
11709 params.text_document.uri,
11710 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11711 );
11712 assert_eq!(params.options.tab_size, 4);
11713 Ok(Some(vec![lsp::TextEdit::new(
11714 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11715 ", ".to_string(),
11716 )]))
11717 },
11718 );
11719 let save = editor
11720 .update_in(cx, |editor, window, cx| {
11721 editor.save(
11722 SaveOptions {
11723 format: true,
11724 autosave: false,
11725 },
11726 project.clone(),
11727 window,
11728 cx,
11729 )
11730 })
11731 .unwrap();
11732 cx.executor().start_waiting();
11733 save.await;
11734
11735 assert_eq!(
11736 editor.update(cx, |editor, cx| editor.text(cx)),
11737 "one, two\nthree\n"
11738 );
11739 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11740 }
11741
11742 {
11743 editor.update_in(cx, |editor, window, cx| {
11744 editor.set_text("one\ntwo\nthree\n", window, cx)
11745 });
11746 assert!(cx.read(|cx| editor.is_dirty(cx)));
11747
11748 // Ensure we can still save even if formatting hangs.
11749 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11750 move |params, _| async move {
11751 assert_eq!(
11752 params.text_document.uri,
11753 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11754 );
11755 futures::future::pending::<()>().await;
11756 unreachable!()
11757 },
11758 );
11759 let save = editor
11760 .update_in(cx, |editor, window, cx| {
11761 editor.save(
11762 SaveOptions {
11763 format: true,
11764 autosave: false,
11765 },
11766 project.clone(),
11767 window,
11768 cx,
11769 )
11770 })
11771 .unwrap();
11772 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11773 cx.executor().start_waiting();
11774 save.await;
11775 assert_eq!(
11776 editor.update(cx, |editor, cx| editor.text(cx)),
11777 "one\ntwo\nthree\n"
11778 );
11779 }
11780
11781 // Set rust language override and assert overridden tabsize is sent to language server
11782 update_test_language_settings(cx, |settings| {
11783 settings.languages.0.insert(
11784 "Rust".into(),
11785 LanguageSettingsContent {
11786 tab_size: NonZeroU32::new(8),
11787 ..Default::default()
11788 },
11789 );
11790 });
11791
11792 {
11793 editor.update_in(cx, |editor, window, cx| {
11794 editor.set_text("somehting_new\n", window, cx)
11795 });
11796 assert!(cx.read(|cx| editor.is_dirty(cx)));
11797 let _formatting_request_signal = fake_server
11798 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11799 assert_eq!(
11800 params.text_document.uri,
11801 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11802 );
11803 assert_eq!(params.options.tab_size, 8);
11804 Ok(Some(vec![]))
11805 });
11806 let save = editor
11807 .update_in(cx, |editor, window, cx| {
11808 editor.save(
11809 SaveOptions {
11810 format: true,
11811 autosave: false,
11812 },
11813 project.clone(),
11814 window,
11815 cx,
11816 )
11817 })
11818 .unwrap();
11819 cx.executor().start_waiting();
11820 save.await;
11821 }
11822}
11823
11824#[gpui::test]
11825async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11826 init_test(cx, |settings| {
11827 settings.defaults.ensure_final_newline_on_save = Some(false);
11828 });
11829
11830 let fs = FakeFs::new(cx.executor());
11831 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11832
11833 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11834
11835 let buffer = project
11836 .update(cx, |project, cx| {
11837 project.open_local_buffer(path!("/file.txt"), cx)
11838 })
11839 .await
11840 .unwrap();
11841
11842 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11843 let (editor, cx) = cx.add_window_view(|window, cx| {
11844 build_editor_with_project(project.clone(), buffer, window, cx)
11845 });
11846 editor.update_in(cx, |editor, window, cx| {
11847 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11848 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11849 });
11850 });
11851 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11852
11853 editor.update_in(cx, |editor, window, cx| {
11854 editor.handle_input("\n", window, cx)
11855 });
11856 cx.run_until_parked();
11857 save(&editor, &project, cx).await;
11858 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11859
11860 editor.update_in(cx, |editor, window, cx| {
11861 editor.undo(&Default::default(), window, cx);
11862 });
11863 save(&editor, &project, cx).await;
11864 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11865
11866 editor.update_in(cx, |editor, window, cx| {
11867 editor.redo(&Default::default(), window, cx);
11868 });
11869 cx.run_until_parked();
11870 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11871
11872 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11873 let save = editor
11874 .update_in(cx, |editor, window, cx| {
11875 editor.save(
11876 SaveOptions {
11877 format: true,
11878 autosave: false,
11879 },
11880 project.clone(),
11881 window,
11882 cx,
11883 )
11884 })
11885 .unwrap();
11886 cx.executor().start_waiting();
11887 save.await;
11888 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11889 }
11890}
11891
11892#[gpui::test]
11893async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11894 init_test(cx, |_| {});
11895
11896 let cols = 4;
11897 let rows = 10;
11898 let sample_text_1 = sample_text(rows, cols, 'a');
11899 assert_eq!(
11900 sample_text_1,
11901 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11902 );
11903 let sample_text_2 = sample_text(rows, cols, 'l');
11904 assert_eq!(
11905 sample_text_2,
11906 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11907 );
11908 let sample_text_3 = sample_text(rows, cols, 'v');
11909 assert_eq!(
11910 sample_text_3,
11911 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11912 );
11913
11914 let fs = FakeFs::new(cx.executor());
11915 fs.insert_tree(
11916 path!("/a"),
11917 json!({
11918 "main.rs": sample_text_1,
11919 "other.rs": sample_text_2,
11920 "lib.rs": sample_text_3,
11921 }),
11922 )
11923 .await;
11924
11925 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11926 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11927 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11928
11929 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11930 language_registry.add(rust_lang());
11931 let mut fake_servers = language_registry.register_fake_lsp(
11932 "Rust",
11933 FakeLspAdapter {
11934 capabilities: lsp::ServerCapabilities {
11935 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11936 ..Default::default()
11937 },
11938 ..Default::default()
11939 },
11940 );
11941
11942 let worktree = project.update(cx, |project, cx| {
11943 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11944 assert_eq!(worktrees.len(), 1);
11945 worktrees.pop().unwrap()
11946 });
11947 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11948
11949 let buffer_1 = project
11950 .update(cx, |project, cx| {
11951 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11952 })
11953 .await
11954 .unwrap();
11955 let buffer_2 = project
11956 .update(cx, |project, cx| {
11957 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11958 })
11959 .await
11960 .unwrap();
11961 let buffer_3 = project
11962 .update(cx, |project, cx| {
11963 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11964 })
11965 .await
11966 .unwrap();
11967
11968 let multi_buffer = cx.new(|cx| {
11969 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11970 multi_buffer.push_excerpts(
11971 buffer_1.clone(),
11972 [
11973 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11974 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11975 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11976 ],
11977 cx,
11978 );
11979 multi_buffer.push_excerpts(
11980 buffer_2.clone(),
11981 [
11982 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11983 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11984 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11985 ],
11986 cx,
11987 );
11988 multi_buffer.push_excerpts(
11989 buffer_3.clone(),
11990 [
11991 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11992 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11993 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11994 ],
11995 cx,
11996 );
11997 multi_buffer
11998 });
11999 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12000 Editor::new(
12001 EditorMode::full(),
12002 multi_buffer,
12003 Some(project.clone()),
12004 window,
12005 cx,
12006 )
12007 });
12008
12009 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12010 editor.change_selections(
12011 SelectionEffects::scroll(Autoscroll::Next),
12012 window,
12013 cx,
12014 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12015 );
12016 editor.insert("|one|two|three|", window, cx);
12017 });
12018 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12019 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12020 editor.change_selections(
12021 SelectionEffects::scroll(Autoscroll::Next),
12022 window,
12023 cx,
12024 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12025 );
12026 editor.insert("|four|five|six|", window, cx);
12027 });
12028 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12029
12030 // First two buffers should be edited, but not the third one.
12031 assert_eq!(
12032 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12033 "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}",
12034 );
12035 buffer_1.update(cx, |buffer, _| {
12036 assert!(buffer.is_dirty());
12037 assert_eq!(
12038 buffer.text(),
12039 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12040 )
12041 });
12042 buffer_2.update(cx, |buffer, _| {
12043 assert!(buffer.is_dirty());
12044 assert_eq!(
12045 buffer.text(),
12046 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12047 )
12048 });
12049 buffer_3.update(cx, |buffer, _| {
12050 assert!(!buffer.is_dirty());
12051 assert_eq!(buffer.text(), sample_text_3,)
12052 });
12053 cx.executor().run_until_parked();
12054
12055 cx.executor().start_waiting();
12056 let save = multi_buffer_editor
12057 .update_in(cx, |editor, window, cx| {
12058 editor.save(
12059 SaveOptions {
12060 format: true,
12061 autosave: false,
12062 },
12063 project.clone(),
12064 window,
12065 cx,
12066 )
12067 })
12068 .unwrap();
12069
12070 let fake_server = fake_servers.next().await.unwrap();
12071 fake_server
12072 .server
12073 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12074 Ok(Some(vec![lsp::TextEdit::new(
12075 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12076 format!("[{} formatted]", params.text_document.uri),
12077 )]))
12078 })
12079 .detach();
12080 save.await;
12081
12082 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12083 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12084 assert_eq!(
12085 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12086 uri!(
12087 "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}"
12088 ),
12089 );
12090 buffer_1.update(cx, |buffer, _| {
12091 assert!(!buffer.is_dirty());
12092 assert_eq!(
12093 buffer.text(),
12094 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12095 )
12096 });
12097 buffer_2.update(cx, |buffer, _| {
12098 assert!(!buffer.is_dirty());
12099 assert_eq!(
12100 buffer.text(),
12101 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12102 )
12103 });
12104 buffer_3.update(cx, |buffer, _| {
12105 assert!(!buffer.is_dirty());
12106 assert_eq!(buffer.text(), sample_text_3,)
12107 });
12108}
12109
12110#[gpui::test]
12111async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12112 init_test(cx, |_| {});
12113
12114 let fs = FakeFs::new(cx.executor());
12115 fs.insert_tree(
12116 path!("/dir"),
12117 json!({
12118 "file1.rs": "fn main() { println!(\"hello\"); }",
12119 "file2.rs": "fn test() { println!(\"test\"); }",
12120 "file3.rs": "fn other() { println!(\"other\"); }\n",
12121 }),
12122 )
12123 .await;
12124
12125 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12126 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12127 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12128
12129 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12130 language_registry.add(rust_lang());
12131
12132 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12133 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12134
12135 // Open three buffers
12136 let buffer_1 = project
12137 .update(cx, |project, cx| {
12138 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12139 })
12140 .await
12141 .unwrap();
12142 let buffer_2 = project
12143 .update(cx, |project, cx| {
12144 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12145 })
12146 .await
12147 .unwrap();
12148 let buffer_3 = project
12149 .update(cx, |project, cx| {
12150 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12151 })
12152 .await
12153 .unwrap();
12154
12155 // Create a multi-buffer with all three buffers
12156 let multi_buffer = cx.new(|cx| {
12157 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12158 multi_buffer.push_excerpts(
12159 buffer_1.clone(),
12160 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12161 cx,
12162 );
12163 multi_buffer.push_excerpts(
12164 buffer_2.clone(),
12165 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12166 cx,
12167 );
12168 multi_buffer.push_excerpts(
12169 buffer_3.clone(),
12170 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12171 cx,
12172 );
12173 multi_buffer
12174 });
12175
12176 let editor = cx.new_window_entity(|window, cx| {
12177 Editor::new(
12178 EditorMode::full(),
12179 multi_buffer,
12180 Some(project.clone()),
12181 window,
12182 cx,
12183 )
12184 });
12185
12186 // Edit only the first buffer
12187 editor.update_in(cx, |editor, window, cx| {
12188 editor.change_selections(
12189 SelectionEffects::scroll(Autoscroll::Next),
12190 window,
12191 cx,
12192 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12193 );
12194 editor.insert("// edited", window, cx);
12195 });
12196
12197 // Verify that only buffer 1 is dirty
12198 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12199 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12200 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12201
12202 // Get write counts after file creation (files were created with initial content)
12203 // We expect each file to have been written once during creation
12204 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12205 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12206 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12207
12208 // Perform autosave
12209 let save_task = editor.update_in(cx, |editor, window, cx| {
12210 editor.save(
12211 SaveOptions {
12212 format: true,
12213 autosave: true,
12214 },
12215 project.clone(),
12216 window,
12217 cx,
12218 )
12219 });
12220 save_task.await.unwrap();
12221
12222 // Only the dirty buffer should have been saved
12223 assert_eq!(
12224 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12225 1,
12226 "Buffer 1 was dirty, so it should have been written once during autosave"
12227 );
12228 assert_eq!(
12229 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12230 0,
12231 "Buffer 2 was clean, so it should not have been written during autosave"
12232 );
12233 assert_eq!(
12234 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12235 0,
12236 "Buffer 3 was clean, so it should not have been written during autosave"
12237 );
12238
12239 // Verify buffer states after autosave
12240 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12241 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12242 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12243
12244 // Now perform a manual save (format = true)
12245 let save_task = editor.update_in(cx, |editor, window, cx| {
12246 editor.save(
12247 SaveOptions {
12248 format: true,
12249 autosave: false,
12250 },
12251 project.clone(),
12252 window,
12253 cx,
12254 )
12255 });
12256 save_task.await.unwrap();
12257
12258 // During manual save, clean buffers don't get written to disk
12259 // They just get did_save called for language server notifications
12260 assert_eq!(
12261 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12262 1,
12263 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12264 );
12265 assert_eq!(
12266 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12267 0,
12268 "Buffer 2 should not have been written at all"
12269 );
12270 assert_eq!(
12271 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12272 0,
12273 "Buffer 3 should not have been written at all"
12274 );
12275}
12276
12277async fn setup_range_format_test(
12278 cx: &mut TestAppContext,
12279) -> (
12280 Entity<Project>,
12281 Entity<Editor>,
12282 &mut gpui::VisualTestContext,
12283 lsp::FakeLanguageServer,
12284) {
12285 init_test(cx, |_| {});
12286
12287 let fs = FakeFs::new(cx.executor());
12288 fs.insert_file(path!("/file.rs"), Default::default()).await;
12289
12290 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12291
12292 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12293 language_registry.add(rust_lang());
12294 let mut fake_servers = language_registry.register_fake_lsp(
12295 "Rust",
12296 FakeLspAdapter {
12297 capabilities: lsp::ServerCapabilities {
12298 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12299 ..lsp::ServerCapabilities::default()
12300 },
12301 ..FakeLspAdapter::default()
12302 },
12303 );
12304
12305 let buffer = project
12306 .update(cx, |project, cx| {
12307 project.open_local_buffer(path!("/file.rs"), cx)
12308 })
12309 .await
12310 .unwrap();
12311
12312 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12313 let (editor, cx) = cx.add_window_view(|window, cx| {
12314 build_editor_with_project(project.clone(), buffer, window, cx)
12315 });
12316
12317 cx.executor().start_waiting();
12318 let fake_server = fake_servers.next().await.unwrap();
12319
12320 (project, editor, cx, fake_server)
12321}
12322
12323#[gpui::test]
12324async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12325 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12326
12327 editor.update_in(cx, |editor, window, cx| {
12328 editor.set_text("one\ntwo\nthree\n", window, cx)
12329 });
12330 assert!(cx.read(|cx| editor.is_dirty(cx)));
12331
12332 let save = editor
12333 .update_in(cx, |editor, window, cx| {
12334 editor.save(
12335 SaveOptions {
12336 format: true,
12337 autosave: false,
12338 },
12339 project.clone(),
12340 window,
12341 cx,
12342 )
12343 })
12344 .unwrap();
12345 fake_server
12346 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12347 assert_eq!(
12348 params.text_document.uri,
12349 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12350 );
12351 assert_eq!(params.options.tab_size, 4);
12352 Ok(Some(vec![lsp::TextEdit::new(
12353 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12354 ", ".to_string(),
12355 )]))
12356 })
12357 .next()
12358 .await;
12359 cx.executor().start_waiting();
12360 save.await;
12361 assert_eq!(
12362 editor.update(cx, |editor, cx| editor.text(cx)),
12363 "one, two\nthree\n"
12364 );
12365 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12366}
12367
12368#[gpui::test]
12369async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12370 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12371
12372 editor.update_in(cx, |editor, window, cx| {
12373 editor.set_text("one\ntwo\nthree\n", window, cx)
12374 });
12375 assert!(cx.read(|cx| editor.is_dirty(cx)));
12376
12377 // Test that save still works when formatting hangs
12378 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12379 move |params, _| async move {
12380 assert_eq!(
12381 params.text_document.uri,
12382 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12383 );
12384 futures::future::pending::<()>().await;
12385 unreachable!()
12386 },
12387 );
12388 let save = editor
12389 .update_in(cx, |editor, window, cx| {
12390 editor.save(
12391 SaveOptions {
12392 format: true,
12393 autosave: false,
12394 },
12395 project.clone(),
12396 window,
12397 cx,
12398 )
12399 })
12400 .unwrap();
12401 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12402 cx.executor().start_waiting();
12403 save.await;
12404 assert_eq!(
12405 editor.update(cx, |editor, cx| editor.text(cx)),
12406 "one\ntwo\nthree\n"
12407 );
12408 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12409}
12410
12411#[gpui::test]
12412async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12413 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12414
12415 // Buffer starts clean, no formatting should be requested
12416 let save = editor
12417 .update_in(cx, |editor, window, cx| {
12418 editor.save(
12419 SaveOptions {
12420 format: false,
12421 autosave: false,
12422 },
12423 project.clone(),
12424 window,
12425 cx,
12426 )
12427 })
12428 .unwrap();
12429 let _pending_format_request = fake_server
12430 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12431 panic!("Should not be invoked");
12432 })
12433 .next();
12434 cx.executor().start_waiting();
12435 save.await;
12436 cx.run_until_parked();
12437}
12438
12439#[gpui::test]
12440async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12441 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12442
12443 // Set Rust language override and assert overridden tabsize is sent to language server
12444 update_test_language_settings(cx, |settings| {
12445 settings.languages.0.insert(
12446 "Rust".into(),
12447 LanguageSettingsContent {
12448 tab_size: NonZeroU32::new(8),
12449 ..Default::default()
12450 },
12451 );
12452 });
12453
12454 editor.update_in(cx, |editor, window, cx| {
12455 editor.set_text("something_new\n", window, cx)
12456 });
12457 assert!(cx.read(|cx| editor.is_dirty(cx)));
12458 let save = editor
12459 .update_in(cx, |editor, window, cx| {
12460 editor.save(
12461 SaveOptions {
12462 format: true,
12463 autosave: false,
12464 },
12465 project.clone(),
12466 window,
12467 cx,
12468 )
12469 })
12470 .unwrap();
12471 fake_server
12472 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12473 assert_eq!(
12474 params.text_document.uri,
12475 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12476 );
12477 assert_eq!(params.options.tab_size, 8);
12478 Ok(Some(Vec::new()))
12479 })
12480 .next()
12481 .await;
12482 save.await;
12483}
12484
12485#[gpui::test]
12486async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12487 init_test(cx, |settings| {
12488 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12489 settings::LanguageServerFormatterSpecifier::Current,
12490 )))
12491 });
12492
12493 let fs = FakeFs::new(cx.executor());
12494 fs.insert_file(path!("/file.rs"), Default::default()).await;
12495
12496 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12497
12498 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12499 language_registry.add(Arc::new(Language::new(
12500 LanguageConfig {
12501 name: "Rust".into(),
12502 matcher: LanguageMatcher {
12503 path_suffixes: vec!["rs".to_string()],
12504 ..Default::default()
12505 },
12506 ..LanguageConfig::default()
12507 },
12508 Some(tree_sitter_rust::LANGUAGE.into()),
12509 )));
12510 update_test_language_settings(cx, |settings| {
12511 // Enable Prettier formatting for the same buffer, and ensure
12512 // LSP is called instead of Prettier.
12513 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12514 });
12515 let mut fake_servers = language_registry.register_fake_lsp(
12516 "Rust",
12517 FakeLspAdapter {
12518 capabilities: lsp::ServerCapabilities {
12519 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12520 ..Default::default()
12521 },
12522 ..Default::default()
12523 },
12524 );
12525
12526 let buffer = project
12527 .update(cx, |project, cx| {
12528 project.open_local_buffer(path!("/file.rs"), cx)
12529 })
12530 .await
12531 .unwrap();
12532
12533 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12534 let (editor, cx) = cx.add_window_view(|window, cx| {
12535 build_editor_with_project(project.clone(), buffer, window, cx)
12536 });
12537 editor.update_in(cx, |editor, window, cx| {
12538 editor.set_text("one\ntwo\nthree\n", window, cx)
12539 });
12540
12541 cx.executor().start_waiting();
12542 let fake_server = fake_servers.next().await.unwrap();
12543
12544 let format = editor
12545 .update_in(cx, |editor, window, cx| {
12546 editor.perform_format(
12547 project.clone(),
12548 FormatTrigger::Manual,
12549 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12550 window,
12551 cx,
12552 )
12553 })
12554 .unwrap();
12555 fake_server
12556 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12557 assert_eq!(
12558 params.text_document.uri,
12559 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12560 );
12561 assert_eq!(params.options.tab_size, 4);
12562 Ok(Some(vec![lsp::TextEdit::new(
12563 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12564 ", ".to_string(),
12565 )]))
12566 })
12567 .next()
12568 .await;
12569 cx.executor().start_waiting();
12570 format.await;
12571 assert_eq!(
12572 editor.update(cx, |editor, cx| editor.text(cx)),
12573 "one, two\nthree\n"
12574 );
12575
12576 editor.update_in(cx, |editor, window, cx| {
12577 editor.set_text("one\ntwo\nthree\n", window, cx)
12578 });
12579 // Ensure we don't lock if formatting hangs.
12580 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12581 move |params, _| async move {
12582 assert_eq!(
12583 params.text_document.uri,
12584 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12585 );
12586 futures::future::pending::<()>().await;
12587 unreachable!()
12588 },
12589 );
12590 let format = editor
12591 .update_in(cx, |editor, window, cx| {
12592 editor.perform_format(
12593 project,
12594 FormatTrigger::Manual,
12595 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12596 window,
12597 cx,
12598 )
12599 })
12600 .unwrap();
12601 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12602 cx.executor().start_waiting();
12603 format.await;
12604 assert_eq!(
12605 editor.update(cx, |editor, cx| editor.text(cx)),
12606 "one\ntwo\nthree\n"
12607 );
12608}
12609
12610#[gpui::test]
12611async fn test_multiple_formatters(cx: &mut TestAppContext) {
12612 init_test(cx, |settings| {
12613 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12614 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12615 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12616 Formatter::CodeAction("code-action-1".into()),
12617 Formatter::CodeAction("code-action-2".into()),
12618 ]))
12619 });
12620
12621 let fs = FakeFs::new(cx.executor());
12622 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12623 .await;
12624
12625 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12626 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12627 language_registry.add(rust_lang());
12628
12629 let mut fake_servers = language_registry.register_fake_lsp(
12630 "Rust",
12631 FakeLspAdapter {
12632 capabilities: lsp::ServerCapabilities {
12633 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12634 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12635 commands: vec!["the-command-for-code-action-1".into()],
12636 ..Default::default()
12637 }),
12638 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12639 ..Default::default()
12640 },
12641 ..Default::default()
12642 },
12643 );
12644
12645 let buffer = project
12646 .update(cx, |project, cx| {
12647 project.open_local_buffer(path!("/file.rs"), cx)
12648 })
12649 .await
12650 .unwrap();
12651
12652 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12653 let (editor, cx) = cx.add_window_view(|window, cx| {
12654 build_editor_with_project(project.clone(), buffer, window, cx)
12655 });
12656
12657 cx.executor().start_waiting();
12658
12659 let fake_server = fake_servers.next().await.unwrap();
12660 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12661 move |_params, _| async move {
12662 Ok(Some(vec![lsp::TextEdit::new(
12663 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12664 "applied-formatting\n".to_string(),
12665 )]))
12666 },
12667 );
12668 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12669 move |params, _| async move {
12670 let requested_code_actions = params.context.only.expect("Expected code action request");
12671 assert_eq!(requested_code_actions.len(), 1);
12672
12673 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12674 let code_action = match requested_code_actions[0].as_str() {
12675 "code-action-1" => lsp::CodeAction {
12676 kind: Some("code-action-1".into()),
12677 edit: Some(lsp::WorkspaceEdit::new(
12678 [(
12679 uri,
12680 vec![lsp::TextEdit::new(
12681 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12682 "applied-code-action-1-edit\n".to_string(),
12683 )],
12684 )]
12685 .into_iter()
12686 .collect(),
12687 )),
12688 command: Some(lsp::Command {
12689 command: "the-command-for-code-action-1".into(),
12690 ..Default::default()
12691 }),
12692 ..Default::default()
12693 },
12694 "code-action-2" => lsp::CodeAction {
12695 kind: Some("code-action-2".into()),
12696 edit: Some(lsp::WorkspaceEdit::new(
12697 [(
12698 uri,
12699 vec![lsp::TextEdit::new(
12700 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12701 "applied-code-action-2-edit\n".to_string(),
12702 )],
12703 )]
12704 .into_iter()
12705 .collect(),
12706 )),
12707 ..Default::default()
12708 },
12709 req => panic!("Unexpected code action request: {:?}", req),
12710 };
12711 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12712 code_action,
12713 )]))
12714 },
12715 );
12716
12717 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12718 move |params, _| async move { Ok(params) }
12719 });
12720
12721 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12722 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12723 let fake = fake_server.clone();
12724 let lock = command_lock.clone();
12725 move |params, _| {
12726 assert_eq!(params.command, "the-command-for-code-action-1");
12727 let fake = fake.clone();
12728 let lock = lock.clone();
12729 async move {
12730 lock.lock().await;
12731 fake.server
12732 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12733 label: None,
12734 edit: lsp::WorkspaceEdit {
12735 changes: Some(
12736 [(
12737 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12738 vec![lsp::TextEdit {
12739 range: lsp::Range::new(
12740 lsp::Position::new(0, 0),
12741 lsp::Position::new(0, 0),
12742 ),
12743 new_text: "applied-code-action-1-command\n".into(),
12744 }],
12745 )]
12746 .into_iter()
12747 .collect(),
12748 ),
12749 ..Default::default()
12750 },
12751 })
12752 .await
12753 .into_response()
12754 .unwrap();
12755 Ok(Some(json!(null)))
12756 }
12757 }
12758 });
12759
12760 cx.executor().start_waiting();
12761 editor
12762 .update_in(cx, |editor, window, cx| {
12763 editor.perform_format(
12764 project.clone(),
12765 FormatTrigger::Manual,
12766 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12767 window,
12768 cx,
12769 )
12770 })
12771 .unwrap()
12772 .await;
12773 editor.update(cx, |editor, cx| {
12774 assert_eq!(
12775 editor.text(cx),
12776 r#"
12777 applied-code-action-2-edit
12778 applied-code-action-1-command
12779 applied-code-action-1-edit
12780 applied-formatting
12781 one
12782 two
12783 three
12784 "#
12785 .unindent()
12786 );
12787 });
12788
12789 editor.update_in(cx, |editor, window, cx| {
12790 editor.undo(&Default::default(), window, cx);
12791 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12792 });
12793
12794 // Perform a manual edit while waiting for an LSP command
12795 // that's being run as part of a formatting code action.
12796 let lock_guard = command_lock.lock().await;
12797 let format = editor
12798 .update_in(cx, |editor, window, cx| {
12799 editor.perform_format(
12800 project.clone(),
12801 FormatTrigger::Manual,
12802 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12803 window,
12804 cx,
12805 )
12806 })
12807 .unwrap();
12808 cx.run_until_parked();
12809 editor.update(cx, |editor, cx| {
12810 assert_eq!(
12811 editor.text(cx),
12812 r#"
12813 applied-code-action-1-edit
12814 applied-formatting
12815 one
12816 two
12817 three
12818 "#
12819 .unindent()
12820 );
12821
12822 editor.buffer.update(cx, |buffer, cx| {
12823 let ix = buffer.len(cx);
12824 buffer.edit([(ix..ix, "edited\n")], None, cx);
12825 });
12826 });
12827
12828 // Allow the LSP command to proceed. Because the buffer was edited,
12829 // the second code action will not be run.
12830 drop(lock_guard);
12831 format.await;
12832 editor.update_in(cx, |editor, window, cx| {
12833 assert_eq!(
12834 editor.text(cx),
12835 r#"
12836 applied-code-action-1-command
12837 applied-code-action-1-edit
12838 applied-formatting
12839 one
12840 two
12841 three
12842 edited
12843 "#
12844 .unindent()
12845 );
12846
12847 // The manual edit is undone first, because it is the last thing the user did
12848 // (even though the command completed afterwards).
12849 editor.undo(&Default::default(), window, cx);
12850 assert_eq!(
12851 editor.text(cx),
12852 r#"
12853 applied-code-action-1-command
12854 applied-code-action-1-edit
12855 applied-formatting
12856 one
12857 two
12858 three
12859 "#
12860 .unindent()
12861 );
12862
12863 // All the formatting (including the command, which completed after the manual edit)
12864 // is undone together.
12865 editor.undo(&Default::default(), window, cx);
12866 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12867 });
12868}
12869
12870#[gpui::test]
12871async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12872 init_test(cx, |settings| {
12873 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12874 settings::LanguageServerFormatterSpecifier::Current,
12875 )]))
12876 });
12877
12878 let fs = FakeFs::new(cx.executor());
12879 fs.insert_file(path!("/file.ts"), Default::default()).await;
12880
12881 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12882
12883 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12884 language_registry.add(Arc::new(Language::new(
12885 LanguageConfig {
12886 name: "TypeScript".into(),
12887 matcher: LanguageMatcher {
12888 path_suffixes: vec!["ts".to_string()],
12889 ..Default::default()
12890 },
12891 ..LanguageConfig::default()
12892 },
12893 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12894 )));
12895 update_test_language_settings(cx, |settings| {
12896 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12897 });
12898 let mut fake_servers = language_registry.register_fake_lsp(
12899 "TypeScript",
12900 FakeLspAdapter {
12901 capabilities: lsp::ServerCapabilities {
12902 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12903 ..Default::default()
12904 },
12905 ..Default::default()
12906 },
12907 );
12908
12909 let buffer = project
12910 .update(cx, |project, cx| {
12911 project.open_local_buffer(path!("/file.ts"), cx)
12912 })
12913 .await
12914 .unwrap();
12915
12916 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12917 let (editor, cx) = cx.add_window_view(|window, cx| {
12918 build_editor_with_project(project.clone(), buffer, window, cx)
12919 });
12920 editor.update_in(cx, |editor, window, cx| {
12921 editor.set_text(
12922 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12923 window,
12924 cx,
12925 )
12926 });
12927
12928 cx.executor().start_waiting();
12929 let fake_server = fake_servers.next().await.unwrap();
12930
12931 let format = editor
12932 .update_in(cx, |editor, window, cx| {
12933 editor.perform_code_action_kind(
12934 project.clone(),
12935 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12936 window,
12937 cx,
12938 )
12939 })
12940 .unwrap();
12941 fake_server
12942 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12943 assert_eq!(
12944 params.text_document.uri,
12945 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12946 );
12947 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12948 lsp::CodeAction {
12949 title: "Organize Imports".to_string(),
12950 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12951 edit: Some(lsp::WorkspaceEdit {
12952 changes: Some(
12953 [(
12954 params.text_document.uri.clone(),
12955 vec![lsp::TextEdit::new(
12956 lsp::Range::new(
12957 lsp::Position::new(1, 0),
12958 lsp::Position::new(2, 0),
12959 ),
12960 "".to_string(),
12961 )],
12962 )]
12963 .into_iter()
12964 .collect(),
12965 ),
12966 ..Default::default()
12967 }),
12968 ..Default::default()
12969 },
12970 )]))
12971 })
12972 .next()
12973 .await;
12974 cx.executor().start_waiting();
12975 format.await;
12976 assert_eq!(
12977 editor.update(cx, |editor, cx| editor.text(cx)),
12978 "import { a } from 'module';\n\nconst x = a;\n"
12979 );
12980
12981 editor.update_in(cx, |editor, window, cx| {
12982 editor.set_text(
12983 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12984 window,
12985 cx,
12986 )
12987 });
12988 // Ensure we don't lock if code action hangs.
12989 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12990 move |params, _| async move {
12991 assert_eq!(
12992 params.text_document.uri,
12993 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12994 );
12995 futures::future::pending::<()>().await;
12996 unreachable!()
12997 },
12998 );
12999 let format = editor
13000 .update_in(cx, |editor, window, cx| {
13001 editor.perform_code_action_kind(
13002 project,
13003 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13004 window,
13005 cx,
13006 )
13007 })
13008 .unwrap();
13009 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13010 cx.executor().start_waiting();
13011 format.await;
13012 assert_eq!(
13013 editor.update(cx, |editor, cx| editor.text(cx)),
13014 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13015 );
13016}
13017
13018#[gpui::test]
13019async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13020 init_test(cx, |_| {});
13021
13022 let mut cx = EditorLspTestContext::new_rust(
13023 lsp::ServerCapabilities {
13024 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13025 ..Default::default()
13026 },
13027 cx,
13028 )
13029 .await;
13030
13031 cx.set_state(indoc! {"
13032 one.twoˇ
13033 "});
13034
13035 // The format request takes a long time. When it completes, it inserts
13036 // a newline and an indent before the `.`
13037 cx.lsp
13038 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13039 let executor = cx.background_executor().clone();
13040 async move {
13041 executor.timer(Duration::from_millis(100)).await;
13042 Ok(Some(vec![lsp::TextEdit {
13043 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13044 new_text: "\n ".into(),
13045 }]))
13046 }
13047 });
13048
13049 // Submit a format request.
13050 let format_1 = cx
13051 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13052 .unwrap();
13053 cx.executor().run_until_parked();
13054
13055 // Submit a second format request.
13056 let format_2 = cx
13057 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13058 .unwrap();
13059 cx.executor().run_until_parked();
13060
13061 // Wait for both format requests to complete
13062 cx.executor().advance_clock(Duration::from_millis(200));
13063 cx.executor().start_waiting();
13064 format_1.await.unwrap();
13065 cx.executor().start_waiting();
13066 format_2.await.unwrap();
13067
13068 // The formatting edits only happens once.
13069 cx.assert_editor_state(indoc! {"
13070 one
13071 .twoˇ
13072 "});
13073}
13074
13075#[gpui::test]
13076async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13077 init_test(cx, |settings| {
13078 settings.defaults.formatter = Some(FormatterList::default())
13079 });
13080
13081 let mut cx = EditorLspTestContext::new_rust(
13082 lsp::ServerCapabilities {
13083 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13084 ..Default::default()
13085 },
13086 cx,
13087 )
13088 .await;
13089
13090 // Record which buffer changes have been sent to the language server
13091 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13092 cx.lsp
13093 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13094 let buffer_changes = buffer_changes.clone();
13095 move |params, _| {
13096 buffer_changes.lock().extend(
13097 params
13098 .content_changes
13099 .into_iter()
13100 .map(|e| (e.range.unwrap(), e.text)),
13101 );
13102 }
13103 });
13104 // Handle formatting requests to the language server.
13105 cx.lsp
13106 .set_request_handler::<lsp::request::Formatting, _, _>({
13107 let buffer_changes = buffer_changes.clone();
13108 move |_, _| {
13109 let buffer_changes = buffer_changes.clone();
13110 // Insert blank lines between each line of the buffer.
13111 async move {
13112 // When formatting is requested, trailing whitespace has already been stripped,
13113 // and the trailing newline has already been added.
13114 assert_eq!(
13115 &buffer_changes.lock()[1..],
13116 &[
13117 (
13118 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13119 "".into()
13120 ),
13121 (
13122 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13123 "".into()
13124 ),
13125 (
13126 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13127 "\n".into()
13128 ),
13129 ]
13130 );
13131
13132 Ok(Some(vec![
13133 lsp::TextEdit {
13134 range: lsp::Range::new(
13135 lsp::Position::new(1, 0),
13136 lsp::Position::new(1, 0),
13137 ),
13138 new_text: "\n".into(),
13139 },
13140 lsp::TextEdit {
13141 range: lsp::Range::new(
13142 lsp::Position::new(2, 0),
13143 lsp::Position::new(2, 0),
13144 ),
13145 new_text: "\n".into(),
13146 },
13147 ]))
13148 }
13149 }
13150 });
13151
13152 // Set up a buffer white some trailing whitespace and no trailing newline.
13153 cx.set_state(
13154 &[
13155 "one ", //
13156 "twoˇ", //
13157 "three ", //
13158 "four", //
13159 ]
13160 .join("\n"),
13161 );
13162 cx.run_until_parked();
13163
13164 // Submit a format request.
13165 let format = cx
13166 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13167 .unwrap();
13168
13169 cx.run_until_parked();
13170 // After formatting the buffer, the trailing whitespace is stripped,
13171 // a newline is appended, and the edits provided by the language server
13172 // have been applied.
13173 format.await.unwrap();
13174
13175 cx.assert_editor_state(
13176 &[
13177 "one", //
13178 "", //
13179 "twoˇ", //
13180 "", //
13181 "three", //
13182 "four", //
13183 "", //
13184 ]
13185 .join("\n"),
13186 );
13187
13188 // Undoing the formatting undoes the trailing whitespace removal, the
13189 // trailing newline, and the LSP edits.
13190 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13191 cx.assert_editor_state(
13192 &[
13193 "one ", //
13194 "twoˇ", //
13195 "three ", //
13196 "four", //
13197 ]
13198 .join("\n"),
13199 );
13200}
13201
13202#[gpui::test]
13203async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13204 cx: &mut TestAppContext,
13205) {
13206 init_test(cx, |_| {});
13207
13208 cx.update(|cx| {
13209 cx.update_global::<SettingsStore, _>(|settings, cx| {
13210 settings.update_user_settings(cx, |settings| {
13211 settings.editor.auto_signature_help = Some(true);
13212 });
13213 });
13214 });
13215
13216 let mut cx = EditorLspTestContext::new_rust(
13217 lsp::ServerCapabilities {
13218 signature_help_provider: Some(lsp::SignatureHelpOptions {
13219 ..Default::default()
13220 }),
13221 ..Default::default()
13222 },
13223 cx,
13224 )
13225 .await;
13226
13227 let language = Language::new(
13228 LanguageConfig {
13229 name: "Rust".into(),
13230 brackets: BracketPairConfig {
13231 pairs: vec![
13232 BracketPair {
13233 start: "{".to_string(),
13234 end: "}".to_string(),
13235 close: true,
13236 surround: true,
13237 newline: true,
13238 },
13239 BracketPair {
13240 start: "(".to_string(),
13241 end: ")".to_string(),
13242 close: true,
13243 surround: true,
13244 newline: true,
13245 },
13246 BracketPair {
13247 start: "/*".to_string(),
13248 end: " */".to_string(),
13249 close: true,
13250 surround: true,
13251 newline: true,
13252 },
13253 BracketPair {
13254 start: "[".to_string(),
13255 end: "]".to_string(),
13256 close: false,
13257 surround: false,
13258 newline: true,
13259 },
13260 BracketPair {
13261 start: "\"".to_string(),
13262 end: "\"".to_string(),
13263 close: true,
13264 surround: true,
13265 newline: false,
13266 },
13267 BracketPair {
13268 start: "<".to_string(),
13269 end: ">".to_string(),
13270 close: false,
13271 surround: true,
13272 newline: true,
13273 },
13274 ],
13275 ..Default::default()
13276 },
13277 autoclose_before: "})]".to_string(),
13278 ..Default::default()
13279 },
13280 Some(tree_sitter_rust::LANGUAGE.into()),
13281 );
13282 let language = Arc::new(language);
13283
13284 cx.language_registry().add(language.clone());
13285 cx.update_buffer(|buffer, cx| {
13286 buffer.set_language(Some(language), cx);
13287 });
13288
13289 cx.set_state(
13290 &r#"
13291 fn main() {
13292 sampleˇ
13293 }
13294 "#
13295 .unindent(),
13296 );
13297
13298 cx.update_editor(|editor, window, cx| {
13299 editor.handle_input("(", window, cx);
13300 });
13301 cx.assert_editor_state(
13302 &"
13303 fn main() {
13304 sample(ˇ)
13305 }
13306 "
13307 .unindent(),
13308 );
13309
13310 let mocked_response = lsp::SignatureHelp {
13311 signatures: vec![lsp::SignatureInformation {
13312 label: "fn sample(param1: u8, param2: u8)".to_string(),
13313 documentation: None,
13314 parameters: Some(vec![
13315 lsp::ParameterInformation {
13316 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13317 documentation: None,
13318 },
13319 lsp::ParameterInformation {
13320 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13321 documentation: None,
13322 },
13323 ]),
13324 active_parameter: None,
13325 }],
13326 active_signature: Some(0),
13327 active_parameter: Some(0),
13328 };
13329 handle_signature_help_request(&mut cx, mocked_response).await;
13330
13331 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13332 .await;
13333
13334 cx.editor(|editor, _, _| {
13335 let signature_help_state = editor.signature_help_state.popover().cloned();
13336 let signature = signature_help_state.unwrap();
13337 assert_eq!(
13338 signature.signatures[signature.current_signature].label,
13339 "fn sample(param1: u8, param2: u8)"
13340 );
13341 });
13342}
13343
13344#[gpui::test]
13345async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13346 init_test(cx, |_| {});
13347
13348 cx.update(|cx| {
13349 cx.update_global::<SettingsStore, _>(|settings, cx| {
13350 settings.update_user_settings(cx, |settings| {
13351 settings.editor.auto_signature_help = Some(false);
13352 settings.editor.show_signature_help_after_edits = Some(false);
13353 });
13354 });
13355 });
13356
13357 let mut cx = EditorLspTestContext::new_rust(
13358 lsp::ServerCapabilities {
13359 signature_help_provider: Some(lsp::SignatureHelpOptions {
13360 ..Default::default()
13361 }),
13362 ..Default::default()
13363 },
13364 cx,
13365 )
13366 .await;
13367
13368 let language = Language::new(
13369 LanguageConfig {
13370 name: "Rust".into(),
13371 brackets: BracketPairConfig {
13372 pairs: vec![
13373 BracketPair {
13374 start: "{".to_string(),
13375 end: "}".to_string(),
13376 close: true,
13377 surround: true,
13378 newline: true,
13379 },
13380 BracketPair {
13381 start: "(".to_string(),
13382 end: ")".to_string(),
13383 close: true,
13384 surround: true,
13385 newline: true,
13386 },
13387 BracketPair {
13388 start: "/*".to_string(),
13389 end: " */".to_string(),
13390 close: true,
13391 surround: true,
13392 newline: true,
13393 },
13394 BracketPair {
13395 start: "[".to_string(),
13396 end: "]".to_string(),
13397 close: false,
13398 surround: false,
13399 newline: true,
13400 },
13401 BracketPair {
13402 start: "\"".to_string(),
13403 end: "\"".to_string(),
13404 close: true,
13405 surround: true,
13406 newline: false,
13407 },
13408 BracketPair {
13409 start: "<".to_string(),
13410 end: ">".to_string(),
13411 close: false,
13412 surround: true,
13413 newline: true,
13414 },
13415 ],
13416 ..Default::default()
13417 },
13418 autoclose_before: "})]".to_string(),
13419 ..Default::default()
13420 },
13421 Some(tree_sitter_rust::LANGUAGE.into()),
13422 );
13423 let language = Arc::new(language);
13424
13425 cx.language_registry().add(language.clone());
13426 cx.update_buffer(|buffer, cx| {
13427 buffer.set_language(Some(language), cx);
13428 });
13429
13430 // Ensure that signature_help is not called when no signature help is enabled.
13431 cx.set_state(
13432 &r#"
13433 fn main() {
13434 sampleˇ
13435 }
13436 "#
13437 .unindent(),
13438 );
13439 cx.update_editor(|editor, window, cx| {
13440 editor.handle_input("(", window, cx);
13441 });
13442 cx.assert_editor_state(
13443 &"
13444 fn main() {
13445 sample(ˇ)
13446 }
13447 "
13448 .unindent(),
13449 );
13450 cx.editor(|editor, _, _| {
13451 assert!(editor.signature_help_state.task().is_none());
13452 });
13453
13454 let mocked_response = lsp::SignatureHelp {
13455 signatures: vec![lsp::SignatureInformation {
13456 label: "fn sample(param1: u8, param2: u8)".to_string(),
13457 documentation: None,
13458 parameters: Some(vec![
13459 lsp::ParameterInformation {
13460 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13461 documentation: None,
13462 },
13463 lsp::ParameterInformation {
13464 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13465 documentation: None,
13466 },
13467 ]),
13468 active_parameter: None,
13469 }],
13470 active_signature: Some(0),
13471 active_parameter: Some(0),
13472 };
13473
13474 // Ensure that signature_help is called when enabled afte edits
13475 cx.update(|_, cx| {
13476 cx.update_global::<SettingsStore, _>(|settings, cx| {
13477 settings.update_user_settings(cx, |settings| {
13478 settings.editor.auto_signature_help = Some(false);
13479 settings.editor.show_signature_help_after_edits = Some(true);
13480 });
13481 });
13482 });
13483 cx.set_state(
13484 &r#"
13485 fn main() {
13486 sampleˇ
13487 }
13488 "#
13489 .unindent(),
13490 );
13491 cx.update_editor(|editor, window, cx| {
13492 editor.handle_input("(", window, cx);
13493 });
13494 cx.assert_editor_state(
13495 &"
13496 fn main() {
13497 sample(ˇ)
13498 }
13499 "
13500 .unindent(),
13501 );
13502 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13503 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13504 .await;
13505 cx.update_editor(|editor, _, _| {
13506 let signature_help_state = editor.signature_help_state.popover().cloned();
13507 assert!(signature_help_state.is_some());
13508 let signature = signature_help_state.unwrap();
13509 assert_eq!(
13510 signature.signatures[signature.current_signature].label,
13511 "fn sample(param1: u8, param2: u8)"
13512 );
13513 editor.signature_help_state = SignatureHelpState::default();
13514 });
13515
13516 // Ensure that signature_help is called when auto signature help override is enabled
13517 cx.update(|_, cx| {
13518 cx.update_global::<SettingsStore, _>(|settings, cx| {
13519 settings.update_user_settings(cx, |settings| {
13520 settings.editor.auto_signature_help = Some(true);
13521 settings.editor.show_signature_help_after_edits = Some(false);
13522 });
13523 });
13524 });
13525 cx.set_state(
13526 &r#"
13527 fn main() {
13528 sampleˇ
13529 }
13530 "#
13531 .unindent(),
13532 );
13533 cx.update_editor(|editor, window, cx| {
13534 editor.handle_input("(", window, cx);
13535 });
13536 cx.assert_editor_state(
13537 &"
13538 fn main() {
13539 sample(ˇ)
13540 }
13541 "
13542 .unindent(),
13543 );
13544 handle_signature_help_request(&mut cx, mocked_response).await;
13545 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13546 .await;
13547 cx.editor(|editor, _, _| {
13548 let signature_help_state = editor.signature_help_state.popover().cloned();
13549 assert!(signature_help_state.is_some());
13550 let signature = signature_help_state.unwrap();
13551 assert_eq!(
13552 signature.signatures[signature.current_signature].label,
13553 "fn sample(param1: u8, param2: u8)"
13554 );
13555 });
13556}
13557
13558#[gpui::test]
13559async fn test_signature_help(cx: &mut TestAppContext) {
13560 init_test(cx, |_| {});
13561 cx.update(|cx| {
13562 cx.update_global::<SettingsStore, _>(|settings, cx| {
13563 settings.update_user_settings(cx, |settings| {
13564 settings.editor.auto_signature_help = Some(true);
13565 });
13566 });
13567 });
13568
13569 let mut cx = EditorLspTestContext::new_rust(
13570 lsp::ServerCapabilities {
13571 signature_help_provider: Some(lsp::SignatureHelpOptions {
13572 ..Default::default()
13573 }),
13574 ..Default::default()
13575 },
13576 cx,
13577 )
13578 .await;
13579
13580 // A test that directly calls `show_signature_help`
13581 cx.update_editor(|editor, window, cx| {
13582 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13583 });
13584
13585 let mocked_response = lsp::SignatureHelp {
13586 signatures: vec![lsp::SignatureInformation {
13587 label: "fn sample(param1: u8, param2: u8)".to_string(),
13588 documentation: None,
13589 parameters: Some(vec![
13590 lsp::ParameterInformation {
13591 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13592 documentation: None,
13593 },
13594 lsp::ParameterInformation {
13595 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13596 documentation: None,
13597 },
13598 ]),
13599 active_parameter: None,
13600 }],
13601 active_signature: Some(0),
13602 active_parameter: Some(0),
13603 };
13604 handle_signature_help_request(&mut cx, mocked_response).await;
13605
13606 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13607 .await;
13608
13609 cx.editor(|editor, _, _| {
13610 let signature_help_state = editor.signature_help_state.popover().cloned();
13611 assert!(signature_help_state.is_some());
13612 let signature = signature_help_state.unwrap();
13613 assert_eq!(
13614 signature.signatures[signature.current_signature].label,
13615 "fn sample(param1: u8, param2: u8)"
13616 );
13617 });
13618
13619 // When exiting outside from inside the brackets, `signature_help` is closed.
13620 cx.set_state(indoc! {"
13621 fn main() {
13622 sample(ˇ);
13623 }
13624
13625 fn sample(param1: u8, param2: u8) {}
13626 "});
13627
13628 cx.update_editor(|editor, window, cx| {
13629 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13630 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13631 });
13632 });
13633
13634 let mocked_response = lsp::SignatureHelp {
13635 signatures: Vec::new(),
13636 active_signature: None,
13637 active_parameter: None,
13638 };
13639 handle_signature_help_request(&mut cx, mocked_response).await;
13640
13641 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13642 .await;
13643
13644 cx.editor(|editor, _, _| {
13645 assert!(!editor.signature_help_state.is_shown());
13646 });
13647
13648 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13649 cx.set_state(indoc! {"
13650 fn main() {
13651 sample(ˇ);
13652 }
13653
13654 fn sample(param1: u8, param2: u8) {}
13655 "});
13656
13657 let mocked_response = lsp::SignatureHelp {
13658 signatures: vec![lsp::SignatureInformation {
13659 label: "fn sample(param1: u8, param2: u8)".to_string(),
13660 documentation: None,
13661 parameters: Some(vec![
13662 lsp::ParameterInformation {
13663 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13664 documentation: None,
13665 },
13666 lsp::ParameterInformation {
13667 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13668 documentation: None,
13669 },
13670 ]),
13671 active_parameter: None,
13672 }],
13673 active_signature: Some(0),
13674 active_parameter: Some(0),
13675 };
13676 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13677 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13678 .await;
13679 cx.editor(|editor, _, _| {
13680 assert!(editor.signature_help_state.is_shown());
13681 });
13682
13683 // Restore the popover with more parameter input
13684 cx.set_state(indoc! {"
13685 fn main() {
13686 sample(param1, param2ˇ);
13687 }
13688
13689 fn sample(param1: u8, param2: u8) {}
13690 "});
13691
13692 let mocked_response = lsp::SignatureHelp {
13693 signatures: vec![lsp::SignatureInformation {
13694 label: "fn sample(param1: u8, param2: u8)".to_string(),
13695 documentation: None,
13696 parameters: Some(vec![
13697 lsp::ParameterInformation {
13698 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13699 documentation: None,
13700 },
13701 lsp::ParameterInformation {
13702 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13703 documentation: None,
13704 },
13705 ]),
13706 active_parameter: None,
13707 }],
13708 active_signature: Some(0),
13709 active_parameter: Some(1),
13710 };
13711 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13712 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13713 .await;
13714
13715 // When selecting a range, the popover is gone.
13716 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13717 cx.update_editor(|editor, window, cx| {
13718 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13719 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13720 })
13721 });
13722 cx.assert_editor_state(indoc! {"
13723 fn main() {
13724 sample(param1, «ˇparam2»);
13725 }
13726
13727 fn sample(param1: u8, param2: u8) {}
13728 "});
13729 cx.editor(|editor, _, _| {
13730 assert!(!editor.signature_help_state.is_shown());
13731 });
13732
13733 // When unselecting again, the popover is back if within the brackets.
13734 cx.update_editor(|editor, window, cx| {
13735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13736 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13737 })
13738 });
13739 cx.assert_editor_state(indoc! {"
13740 fn main() {
13741 sample(param1, ˇparam2);
13742 }
13743
13744 fn sample(param1: u8, param2: u8) {}
13745 "});
13746 handle_signature_help_request(&mut cx, mocked_response).await;
13747 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13748 .await;
13749 cx.editor(|editor, _, _| {
13750 assert!(editor.signature_help_state.is_shown());
13751 });
13752
13753 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13754 cx.update_editor(|editor, window, cx| {
13755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13756 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13757 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13758 })
13759 });
13760 cx.assert_editor_state(indoc! {"
13761 fn main() {
13762 sample(param1, ˇparam2);
13763 }
13764
13765 fn sample(param1: u8, param2: u8) {}
13766 "});
13767
13768 let mocked_response = lsp::SignatureHelp {
13769 signatures: vec![lsp::SignatureInformation {
13770 label: "fn sample(param1: u8, param2: u8)".to_string(),
13771 documentation: None,
13772 parameters: Some(vec![
13773 lsp::ParameterInformation {
13774 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13775 documentation: None,
13776 },
13777 lsp::ParameterInformation {
13778 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13779 documentation: None,
13780 },
13781 ]),
13782 active_parameter: None,
13783 }],
13784 active_signature: Some(0),
13785 active_parameter: Some(1),
13786 };
13787 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13788 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13789 .await;
13790 cx.update_editor(|editor, _, cx| {
13791 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13792 });
13793 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13794 .await;
13795 cx.update_editor(|editor, window, cx| {
13796 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13797 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13798 })
13799 });
13800 cx.assert_editor_state(indoc! {"
13801 fn main() {
13802 sample(param1, «ˇparam2»);
13803 }
13804
13805 fn sample(param1: u8, param2: u8) {}
13806 "});
13807 cx.update_editor(|editor, window, cx| {
13808 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13809 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13810 })
13811 });
13812 cx.assert_editor_state(indoc! {"
13813 fn main() {
13814 sample(param1, ˇparam2);
13815 }
13816
13817 fn sample(param1: u8, param2: u8) {}
13818 "});
13819 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13820 .await;
13821}
13822
13823#[gpui::test]
13824async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13825 init_test(cx, |_| {});
13826
13827 let mut cx = EditorLspTestContext::new_rust(
13828 lsp::ServerCapabilities {
13829 signature_help_provider: Some(lsp::SignatureHelpOptions {
13830 ..Default::default()
13831 }),
13832 ..Default::default()
13833 },
13834 cx,
13835 )
13836 .await;
13837
13838 cx.set_state(indoc! {"
13839 fn main() {
13840 overloadedˇ
13841 }
13842 "});
13843
13844 cx.update_editor(|editor, window, cx| {
13845 editor.handle_input("(", window, cx);
13846 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13847 });
13848
13849 // Mock response with 3 signatures
13850 let mocked_response = lsp::SignatureHelp {
13851 signatures: vec![
13852 lsp::SignatureInformation {
13853 label: "fn overloaded(x: i32)".to_string(),
13854 documentation: None,
13855 parameters: Some(vec![lsp::ParameterInformation {
13856 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13857 documentation: None,
13858 }]),
13859 active_parameter: None,
13860 },
13861 lsp::SignatureInformation {
13862 label: "fn overloaded(x: i32, y: i32)".to_string(),
13863 documentation: None,
13864 parameters: Some(vec![
13865 lsp::ParameterInformation {
13866 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13867 documentation: None,
13868 },
13869 lsp::ParameterInformation {
13870 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13871 documentation: None,
13872 },
13873 ]),
13874 active_parameter: None,
13875 },
13876 lsp::SignatureInformation {
13877 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13878 documentation: None,
13879 parameters: Some(vec![
13880 lsp::ParameterInformation {
13881 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13882 documentation: None,
13883 },
13884 lsp::ParameterInformation {
13885 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13886 documentation: None,
13887 },
13888 lsp::ParameterInformation {
13889 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13890 documentation: None,
13891 },
13892 ]),
13893 active_parameter: None,
13894 },
13895 ],
13896 active_signature: Some(1),
13897 active_parameter: Some(0),
13898 };
13899 handle_signature_help_request(&mut cx, mocked_response).await;
13900
13901 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13902 .await;
13903
13904 // Verify we have multiple signatures and the right one is selected
13905 cx.editor(|editor, _, _| {
13906 let popover = editor.signature_help_state.popover().cloned().unwrap();
13907 assert_eq!(popover.signatures.len(), 3);
13908 // active_signature was 1, so that should be the current
13909 assert_eq!(popover.current_signature, 1);
13910 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13911 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13912 assert_eq!(
13913 popover.signatures[2].label,
13914 "fn overloaded(x: i32, y: i32, z: i32)"
13915 );
13916 });
13917
13918 // Test navigation functionality
13919 cx.update_editor(|editor, window, cx| {
13920 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13921 });
13922
13923 cx.editor(|editor, _, _| {
13924 let popover = editor.signature_help_state.popover().cloned().unwrap();
13925 assert_eq!(popover.current_signature, 2);
13926 });
13927
13928 // Test wrap around
13929 cx.update_editor(|editor, window, cx| {
13930 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13931 });
13932
13933 cx.editor(|editor, _, _| {
13934 let popover = editor.signature_help_state.popover().cloned().unwrap();
13935 assert_eq!(popover.current_signature, 0);
13936 });
13937
13938 // Test previous navigation
13939 cx.update_editor(|editor, window, cx| {
13940 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13941 });
13942
13943 cx.editor(|editor, _, _| {
13944 let popover = editor.signature_help_state.popover().cloned().unwrap();
13945 assert_eq!(popover.current_signature, 2);
13946 });
13947}
13948
13949#[gpui::test]
13950async fn test_completion_mode(cx: &mut TestAppContext) {
13951 init_test(cx, |_| {});
13952 let mut cx = EditorLspTestContext::new_rust(
13953 lsp::ServerCapabilities {
13954 completion_provider: Some(lsp::CompletionOptions {
13955 resolve_provider: Some(true),
13956 ..Default::default()
13957 }),
13958 ..Default::default()
13959 },
13960 cx,
13961 )
13962 .await;
13963
13964 struct Run {
13965 run_description: &'static str,
13966 initial_state: String,
13967 buffer_marked_text: String,
13968 completion_label: &'static str,
13969 completion_text: &'static str,
13970 expected_with_insert_mode: String,
13971 expected_with_replace_mode: String,
13972 expected_with_replace_subsequence_mode: String,
13973 expected_with_replace_suffix_mode: String,
13974 }
13975
13976 let runs = [
13977 Run {
13978 run_description: "Start of word matches completion text",
13979 initial_state: "before ediˇ after".into(),
13980 buffer_marked_text: "before <edi|> after".into(),
13981 completion_label: "editor",
13982 completion_text: "editor",
13983 expected_with_insert_mode: "before editorˇ after".into(),
13984 expected_with_replace_mode: "before editorˇ after".into(),
13985 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13986 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13987 },
13988 Run {
13989 run_description: "Accept same text at the middle of the word",
13990 initial_state: "before ediˇtor after".into(),
13991 buffer_marked_text: "before <edi|tor> after".into(),
13992 completion_label: "editor",
13993 completion_text: "editor",
13994 expected_with_insert_mode: "before editorˇtor after".into(),
13995 expected_with_replace_mode: "before editorˇ after".into(),
13996 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13997 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13998 },
13999 Run {
14000 run_description: "End of word matches completion text -- cursor at end",
14001 initial_state: "before torˇ after".into(),
14002 buffer_marked_text: "before <tor|> after".into(),
14003 completion_label: "editor",
14004 completion_text: "editor",
14005 expected_with_insert_mode: "before editorˇ after".into(),
14006 expected_with_replace_mode: "before editorˇ after".into(),
14007 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14008 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14009 },
14010 Run {
14011 run_description: "End of word matches completion text -- cursor at start",
14012 initial_state: "before ˇtor after".into(),
14013 buffer_marked_text: "before <|tor> after".into(),
14014 completion_label: "editor",
14015 completion_text: "editor",
14016 expected_with_insert_mode: "before editorˇtor after".into(),
14017 expected_with_replace_mode: "before editorˇ after".into(),
14018 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14019 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14020 },
14021 Run {
14022 run_description: "Prepend text containing whitespace",
14023 initial_state: "pˇfield: bool".into(),
14024 buffer_marked_text: "<p|field>: bool".into(),
14025 completion_label: "pub ",
14026 completion_text: "pub ",
14027 expected_with_insert_mode: "pub ˇfield: bool".into(),
14028 expected_with_replace_mode: "pub ˇ: bool".into(),
14029 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14030 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14031 },
14032 Run {
14033 run_description: "Add element to start of list",
14034 initial_state: "[element_ˇelement_2]".into(),
14035 buffer_marked_text: "[<element_|element_2>]".into(),
14036 completion_label: "element_1",
14037 completion_text: "element_1",
14038 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14039 expected_with_replace_mode: "[element_1ˇ]".into(),
14040 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14041 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14042 },
14043 Run {
14044 run_description: "Add element to start of list -- first and second elements are equal",
14045 initial_state: "[elˇelement]".into(),
14046 buffer_marked_text: "[<el|element>]".into(),
14047 completion_label: "element",
14048 completion_text: "element",
14049 expected_with_insert_mode: "[elementˇelement]".into(),
14050 expected_with_replace_mode: "[elementˇ]".into(),
14051 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14052 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14053 },
14054 Run {
14055 run_description: "Ends with matching suffix",
14056 initial_state: "SubˇError".into(),
14057 buffer_marked_text: "<Sub|Error>".into(),
14058 completion_label: "SubscriptionError",
14059 completion_text: "SubscriptionError",
14060 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14061 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14062 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14063 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14064 },
14065 Run {
14066 run_description: "Suffix is a subsequence -- contiguous",
14067 initial_state: "SubˇErr".into(),
14068 buffer_marked_text: "<Sub|Err>".into(),
14069 completion_label: "SubscriptionError",
14070 completion_text: "SubscriptionError",
14071 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14072 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14073 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14074 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14075 },
14076 Run {
14077 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14078 initial_state: "Suˇscrirr".into(),
14079 buffer_marked_text: "<Su|scrirr>".into(),
14080 completion_label: "SubscriptionError",
14081 completion_text: "SubscriptionError",
14082 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14083 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14084 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14085 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14086 },
14087 Run {
14088 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14089 initial_state: "foo(indˇix)".into(),
14090 buffer_marked_text: "foo(<ind|ix>)".into(),
14091 completion_label: "node_index",
14092 completion_text: "node_index",
14093 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14094 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14095 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14096 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14097 },
14098 Run {
14099 run_description: "Replace range ends before cursor - should extend to cursor",
14100 initial_state: "before editˇo after".into(),
14101 buffer_marked_text: "before <{ed}>it|o after".into(),
14102 completion_label: "editor",
14103 completion_text: "editor",
14104 expected_with_insert_mode: "before editorˇo after".into(),
14105 expected_with_replace_mode: "before editorˇo after".into(),
14106 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14107 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14108 },
14109 Run {
14110 run_description: "Uses label for suffix matching",
14111 initial_state: "before ediˇtor after".into(),
14112 buffer_marked_text: "before <edi|tor> after".into(),
14113 completion_label: "editor",
14114 completion_text: "editor()",
14115 expected_with_insert_mode: "before editor()ˇtor after".into(),
14116 expected_with_replace_mode: "before editor()ˇ after".into(),
14117 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14118 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14119 },
14120 Run {
14121 run_description: "Case insensitive subsequence and suffix matching",
14122 initial_state: "before EDiˇtoR after".into(),
14123 buffer_marked_text: "before <EDi|toR> after".into(),
14124 completion_label: "editor",
14125 completion_text: "editor",
14126 expected_with_insert_mode: "before editorˇtoR after".into(),
14127 expected_with_replace_mode: "before editorˇ after".into(),
14128 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14129 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14130 },
14131 ];
14132
14133 for run in runs {
14134 let run_variations = [
14135 (LspInsertMode::Insert, run.expected_with_insert_mode),
14136 (LspInsertMode::Replace, run.expected_with_replace_mode),
14137 (
14138 LspInsertMode::ReplaceSubsequence,
14139 run.expected_with_replace_subsequence_mode,
14140 ),
14141 (
14142 LspInsertMode::ReplaceSuffix,
14143 run.expected_with_replace_suffix_mode,
14144 ),
14145 ];
14146
14147 for (lsp_insert_mode, expected_text) in run_variations {
14148 eprintln!(
14149 "run = {:?}, mode = {lsp_insert_mode:.?}",
14150 run.run_description,
14151 );
14152
14153 update_test_language_settings(&mut cx, |settings| {
14154 settings.defaults.completions = Some(CompletionSettingsContent {
14155 lsp_insert_mode: Some(lsp_insert_mode),
14156 words: Some(WordsCompletionMode::Disabled),
14157 words_min_length: Some(0),
14158 ..Default::default()
14159 });
14160 });
14161
14162 cx.set_state(&run.initial_state);
14163 cx.update_editor(|editor, window, cx| {
14164 editor.show_completions(&ShowCompletions, window, cx);
14165 });
14166
14167 let counter = Arc::new(AtomicUsize::new(0));
14168 handle_completion_request_with_insert_and_replace(
14169 &mut cx,
14170 &run.buffer_marked_text,
14171 vec![(run.completion_label, run.completion_text)],
14172 counter.clone(),
14173 )
14174 .await;
14175 cx.condition(|editor, _| editor.context_menu_visible())
14176 .await;
14177 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14178
14179 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14180 editor
14181 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14182 .unwrap()
14183 });
14184 cx.assert_editor_state(&expected_text);
14185 handle_resolve_completion_request(&mut cx, None).await;
14186 apply_additional_edits.await.unwrap();
14187 }
14188 }
14189}
14190
14191#[gpui::test]
14192async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14193 init_test(cx, |_| {});
14194 let mut cx = EditorLspTestContext::new_rust(
14195 lsp::ServerCapabilities {
14196 completion_provider: Some(lsp::CompletionOptions {
14197 resolve_provider: Some(true),
14198 ..Default::default()
14199 }),
14200 ..Default::default()
14201 },
14202 cx,
14203 )
14204 .await;
14205
14206 let initial_state = "SubˇError";
14207 let buffer_marked_text = "<Sub|Error>";
14208 let completion_text = "SubscriptionError";
14209 let expected_with_insert_mode = "SubscriptionErrorˇError";
14210 let expected_with_replace_mode = "SubscriptionErrorˇ";
14211
14212 update_test_language_settings(&mut cx, |settings| {
14213 settings.defaults.completions = Some(CompletionSettingsContent {
14214 words: Some(WordsCompletionMode::Disabled),
14215 words_min_length: Some(0),
14216 // set the opposite here to ensure that the action is overriding the default behavior
14217 lsp_insert_mode: Some(LspInsertMode::Insert),
14218 ..Default::default()
14219 });
14220 });
14221
14222 cx.set_state(initial_state);
14223 cx.update_editor(|editor, window, cx| {
14224 editor.show_completions(&ShowCompletions, window, cx);
14225 });
14226
14227 let counter = Arc::new(AtomicUsize::new(0));
14228 handle_completion_request_with_insert_and_replace(
14229 &mut cx,
14230 buffer_marked_text,
14231 vec![(completion_text, completion_text)],
14232 counter.clone(),
14233 )
14234 .await;
14235 cx.condition(|editor, _| editor.context_menu_visible())
14236 .await;
14237 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14238
14239 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14240 editor
14241 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14242 .unwrap()
14243 });
14244 cx.assert_editor_state(expected_with_replace_mode);
14245 handle_resolve_completion_request(&mut cx, None).await;
14246 apply_additional_edits.await.unwrap();
14247
14248 update_test_language_settings(&mut cx, |settings| {
14249 settings.defaults.completions = Some(CompletionSettingsContent {
14250 words: Some(WordsCompletionMode::Disabled),
14251 words_min_length: Some(0),
14252 // set the opposite here to ensure that the action is overriding the default behavior
14253 lsp_insert_mode: Some(LspInsertMode::Replace),
14254 ..Default::default()
14255 });
14256 });
14257
14258 cx.set_state(initial_state);
14259 cx.update_editor(|editor, window, cx| {
14260 editor.show_completions(&ShowCompletions, window, cx);
14261 });
14262 handle_completion_request_with_insert_and_replace(
14263 &mut cx,
14264 buffer_marked_text,
14265 vec![(completion_text, completion_text)],
14266 counter.clone(),
14267 )
14268 .await;
14269 cx.condition(|editor, _| editor.context_menu_visible())
14270 .await;
14271 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14272
14273 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14274 editor
14275 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14276 .unwrap()
14277 });
14278 cx.assert_editor_state(expected_with_insert_mode);
14279 handle_resolve_completion_request(&mut cx, None).await;
14280 apply_additional_edits.await.unwrap();
14281}
14282
14283#[gpui::test]
14284async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14285 init_test(cx, |_| {});
14286 let mut cx = EditorLspTestContext::new_rust(
14287 lsp::ServerCapabilities {
14288 completion_provider: Some(lsp::CompletionOptions {
14289 resolve_provider: Some(true),
14290 ..Default::default()
14291 }),
14292 ..Default::default()
14293 },
14294 cx,
14295 )
14296 .await;
14297
14298 // scenario: surrounding text matches completion text
14299 let completion_text = "to_offset";
14300 let initial_state = indoc! {"
14301 1. buf.to_offˇsuffix
14302 2. buf.to_offˇsuf
14303 3. buf.to_offˇfix
14304 4. buf.to_offˇ
14305 5. into_offˇensive
14306 6. ˇsuffix
14307 7. let ˇ //
14308 8. aaˇzz
14309 9. buf.to_off«zzzzzˇ»suffix
14310 10. buf.«ˇzzzzz»suffix
14311 11. to_off«ˇzzzzz»
14312
14313 buf.to_offˇsuffix // newest cursor
14314 "};
14315 let completion_marked_buffer = indoc! {"
14316 1. buf.to_offsuffix
14317 2. buf.to_offsuf
14318 3. buf.to_offfix
14319 4. buf.to_off
14320 5. into_offensive
14321 6. suffix
14322 7. let //
14323 8. aazz
14324 9. buf.to_offzzzzzsuffix
14325 10. buf.zzzzzsuffix
14326 11. to_offzzzzz
14327
14328 buf.<to_off|suffix> // newest cursor
14329 "};
14330 let expected = indoc! {"
14331 1. buf.to_offsetˇ
14332 2. buf.to_offsetˇsuf
14333 3. buf.to_offsetˇfix
14334 4. buf.to_offsetˇ
14335 5. into_offsetˇensive
14336 6. to_offsetˇsuffix
14337 7. let to_offsetˇ //
14338 8. aato_offsetˇzz
14339 9. buf.to_offsetˇ
14340 10. buf.to_offsetˇsuffix
14341 11. to_offsetˇ
14342
14343 buf.to_offsetˇ // newest cursor
14344 "};
14345 cx.set_state(initial_state);
14346 cx.update_editor(|editor, window, cx| {
14347 editor.show_completions(&ShowCompletions, window, cx);
14348 });
14349 handle_completion_request_with_insert_and_replace(
14350 &mut cx,
14351 completion_marked_buffer,
14352 vec![(completion_text, completion_text)],
14353 Arc::new(AtomicUsize::new(0)),
14354 )
14355 .await;
14356 cx.condition(|editor, _| editor.context_menu_visible())
14357 .await;
14358 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14359 editor
14360 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14361 .unwrap()
14362 });
14363 cx.assert_editor_state(expected);
14364 handle_resolve_completion_request(&mut cx, None).await;
14365 apply_additional_edits.await.unwrap();
14366
14367 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14368 let completion_text = "foo_and_bar";
14369 let initial_state = indoc! {"
14370 1. ooanbˇ
14371 2. zooanbˇ
14372 3. ooanbˇz
14373 4. zooanbˇz
14374 5. ooanˇ
14375 6. oanbˇ
14376
14377 ooanbˇ
14378 "};
14379 let completion_marked_buffer = indoc! {"
14380 1. ooanb
14381 2. zooanb
14382 3. ooanbz
14383 4. zooanbz
14384 5. ooan
14385 6. oanb
14386
14387 <ooanb|>
14388 "};
14389 let expected = indoc! {"
14390 1. foo_and_barˇ
14391 2. zfoo_and_barˇ
14392 3. foo_and_barˇz
14393 4. zfoo_and_barˇz
14394 5. ooanfoo_and_barˇ
14395 6. oanbfoo_and_barˇ
14396
14397 foo_and_barˇ
14398 "};
14399 cx.set_state(initial_state);
14400 cx.update_editor(|editor, window, cx| {
14401 editor.show_completions(&ShowCompletions, window, cx);
14402 });
14403 handle_completion_request_with_insert_and_replace(
14404 &mut cx,
14405 completion_marked_buffer,
14406 vec![(completion_text, completion_text)],
14407 Arc::new(AtomicUsize::new(0)),
14408 )
14409 .await;
14410 cx.condition(|editor, _| editor.context_menu_visible())
14411 .await;
14412 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14413 editor
14414 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14415 .unwrap()
14416 });
14417 cx.assert_editor_state(expected);
14418 handle_resolve_completion_request(&mut cx, None).await;
14419 apply_additional_edits.await.unwrap();
14420
14421 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14422 // (expects the same as if it was inserted at the end)
14423 let completion_text = "foo_and_bar";
14424 let initial_state = indoc! {"
14425 1. ooˇanb
14426 2. zooˇanb
14427 3. ooˇanbz
14428 4. zooˇanbz
14429
14430 ooˇanb
14431 "};
14432 let completion_marked_buffer = indoc! {"
14433 1. ooanb
14434 2. zooanb
14435 3. ooanbz
14436 4. zooanbz
14437
14438 <oo|anb>
14439 "};
14440 let expected = indoc! {"
14441 1. foo_and_barˇ
14442 2. zfoo_and_barˇ
14443 3. foo_and_barˇz
14444 4. zfoo_and_barˇz
14445
14446 foo_and_barˇ
14447 "};
14448 cx.set_state(initial_state);
14449 cx.update_editor(|editor, window, cx| {
14450 editor.show_completions(&ShowCompletions, window, cx);
14451 });
14452 handle_completion_request_with_insert_and_replace(
14453 &mut cx,
14454 completion_marked_buffer,
14455 vec![(completion_text, completion_text)],
14456 Arc::new(AtomicUsize::new(0)),
14457 )
14458 .await;
14459 cx.condition(|editor, _| editor.context_menu_visible())
14460 .await;
14461 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14462 editor
14463 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14464 .unwrap()
14465 });
14466 cx.assert_editor_state(expected);
14467 handle_resolve_completion_request(&mut cx, None).await;
14468 apply_additional_edits.await.unwrap();
14469}
14470
14471// This used to crash
14472#[gpui::test]
14473async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14474 init_test(cx, |_| {});
14475
14476 let buffer_text = indoc! {"
14477 fn main() {
14478 10.satu;
14479
14480 //
14481 // separate cursors so they open in different excerpts (manually reproducible)
14482 //
14483
14484 10.satu20;
14485 }
14486 "};
14487 let multibuffer_text_with_selections = indoc! {"
14488 fn main() {
14489 10.satuˇ;
14490
14491 //
14492
14493 //
14494
14495 10.satuˇ20;
14496 }
14497 "};
14498 let expected_multibuffer = indoc! {"
14499 fn main() {
14500 10.saturating_sub()ˇ;
14501
14502 //
14503
14504 //
14505
14506 10.saturating_sub()ˇ;
14507 }
14508 "};
14509
14510 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14511 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14512
14513 let fs = FakeFs::new(cx.executor());
14514 fs.insert_tree(
14515 path!("/a"),
14516 json!({
14517 "main.rs": buffer_text,
14518 }),
14519 )
14520 .await;
14521
14522 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14523 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14524 language_registry.add(rust_lang());
14525 let mut fake_servers = language_registry.register_fake_lsp(
14526 "Rust",
14527 FakeLspAdapter {
14528 capabilities: lsp::ServerCapabilities {
14529 completion_provider: Some(lsp::CompletionOptions {
14530 resolve_provider: None,
14531 ..lsp::CompletionOptions::default()
14532 }),
14533 ..lsp::ServerCapabilities::default()
14534 },
14535 ..FakeLspAdapter::default()
14536 },
14537 );
14538 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14539 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14540 let buffer = project
14541 .update(cx, |project, cx| {
14542 project.open_local_buffer(path!("/a/main.rs"), cx)
14543 })
14544 .await
14545 .unwrap();
14546
14547 let multi_buffer = cx.new(|cx| {
14548 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14549 multi_buffer.push_excerpts(
14550 buffer.clone(),
14551 [ExcerptRange::new(0..first_excerpt_end)],
14552 cx,
14553 );
14554 multi_buffer.push_excerpts(
14555 buffer.clone(),
14556 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14557 cx,
14558 );
14559 multi_buffer
14560 });
14561
14562 let editor = workspace
14563 .update(cx, |_, window, cx| {
14564 cx.new(|cx| {
14565 Editor::new(
14566 EditorMode::Full {
14567 scale_ui_elements_with_buffer_font_size: false,
14568 show_active_line_background: false,
14569 sizing_behavior: SizingBehavior::Default,
14570 },
14571 multi_buffer.clone(),
14572 Some(project.clone()),
14573 window,
14574 cx,
14575 )
14576 })
14577 })
14578 .unwrap();
14579
14580 let pane = workspace
14581 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14582 .unwrap();
14583 pane.update_in(cx, |pane, window, cx| {
14584 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14585 });
14586
14587 let fake_server = fake_servers.next().await.unwrap();
14588
14589 editor.update_in(cx, |editor, window, cx| {
14590 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14591 s.select_ranges([
14592 Point::new(1, 11)..Point::new(1, 11),
14593 Point::new(7, 11)..Point::new(7, 11),
14594 ])
14595 });
14596
14597 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14598 });
14599
14600 editor.update_in(cx, |editor, window, cx| {
14601 editor.show_completions(&ShowCompletions, window, cx);
14602 });
14603
14604 fake_server
14605 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14606 let completion_item = lsp::CompletionItem {
14607 label: "saturating_sub()".into(),
14608 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14609 lsp::InsertReplaceEdit {
14610 new_text: "saturating_sub()".to_owned(),
14611 insert: lsp::Range::new(
14612 lsp::Position::new(7, 7),
14613 lsp::Position::new(7, 11),
14614 ),
14615 replace: lsp::Range::new(
14616 lsp::Position::new(7, 7),
14617 lsp::Position::new(7, 13),
14618 ),
14619 },
14620 )),
14621 ..lsp::CompletionItem::default()
14622 };
14623
14624 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14625 })
14626 .next()
14627 .await
14628 .unwrap();
14629
14630 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14631 .await;
14632
14633 editor
14634 .update_in(cx, |editor, window, cx| {
14635 editor
14636 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14637 .unwrap()
14638 })
14639 .await
14640 .unwrap();
14641
14642 editor.update(cx, |editor, cx| {
14643 assert_text_with_selections(editor, expected_multibuffer, cx);
14644 })
14645}
14646
14647#[gpui::test]
14648async fn test_completion(cx: &mut TestAppContext) {
14649 init_test(cx, |_| {});
14650
14651 let mut cx = EditorLspTestContext::new_rust(
14652 lsp::ServerCapabilities {
14653 completion_provider: Some(lsp::CompletionOptions {
14654 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14655 resolve_provider: Some(true),
14656 ..Default::default()
14657 }),
14658 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14659 ..Default::default()
14660 },
14661 cx,
14662 )
14663 .await;
14664 let counter = Arc::new(AtomicUsize::new(0));
14665
14666 cx.set_state(indoc! {"
14667 oneˇ
14668 two
14669 three
14670 "});
14671 cx.simulate_keystroke(".");
14672 handle_completion_request(
14673 indoc! {"
14674 one.|<>
14675 two
14676 three
14677 "},
14678 vec!["first_completion", "second_completion"],
14679 true,
14680 counter.clone(),
14681 &mut cx,
14682 )
14683 .await;
14684 cx.condition(|editor, _| editor.context_menu_visible())
14685 .await;
14686 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14687
14688 let _handler = handle_signature_help_request(
14689 &mut cx,
14690 lsp::SignatureHelp {
14691 signatures: vec![lsp::SignatureInformation {
14692 label: "test signature".to_string(),
14693 documentation: None,
14694 parameters: Some(vec![lsp::ParameterInformation {
14695 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14696 documentation: None,
14697 }]),
14698 active_parameter: None,
14699 }],
14700 active_signature: None,
14701 active_parameter: None,
14702 },
14703 );
14704 cx.update_editor(|editor, window, cx| {
14705 assert!(
14706 !editor.signature_help_state.is_shown(),
14707 "No signature help was called for"
14708 );
14709 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14710 });
14711 cx.run_until_parked();
14712 cx.update_editor(|editor, _, _| {
14713 assert!(
14714 !editor.signature_help_state.is_shown(),
14715 "No signature help should be shown when completions menu is open"
14716 );
14717 });
14718
14719 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14720 editor.context_menu_next(&Default::default(), window, cx);
14721 editor
14722 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14723 .unwrap()
14724 });
14725 cx.assert_editor_state(indoc! {"
14726 one.second_completionˇ
14727 two
14728 three
14729 "});
14730
14731 handle_resolve_completion_request(
14732 &mut cx,
14733 Some(vec![
14734 (
14735 //This overlaps with the primary completion edit which is
14736 //misbehavior from the LSP spec, test that we filter it out
14737 indoc! {"
14738 one.second_ˇcompletion
14739 two
14740 threeˇ
14741 "},
14742 "overlapping additional edit",
14743 ),
14744 (
14745 indoc! {"
14746 one.second_completion
14747 two
14748 threeˇ
14749 "},
14750 "\nadditional edit",
14751 ),
14752 ]),
14753 )
14754 .await;
14755 apply_additional_edits.await.unwrap();
14756 cx.assert_editor_state(indoc! {"
14757 one.second_completionˇ
14758 two
14759 three
14760 additional edit
14761 "});
14762
14763 cx.set_state(indoc! {"
14764 one.second_completion
14765 twoˇ
14766 threeˇ
14767 additional edit
14768 "});
14769 cx.simulate_keystroke(" ");
14770 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14771 cx.simulate_keystroke("s");
14772 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14773
14774 cx.assert_editor_state(indoc! {"
14775 one.second_completion
14776 two sˇ
14777 three sˇ
14778 additional edit
14779 "});
14780 handle_completion_request(
14781 indoc! {"
14782 one.second_completion
14783 two s
14784 three <s|>
14785 additional edit
14786 "},
14787 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14788 true,
14789 counter.clone(),
14790 &mut cx,
14791 )
14792 .await;
14793 cx.condition(|editor, _| editor.context_menu_visible())
14794 .await;
14795 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14796
14797 cx.simulate_keystroke("i");
14798
14799 handle_completion_request(
14800 indoc! {"
14801 one.second_completion
14802 two si
14803 three <si|>
14804 additional edit
14805 "},
14806 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14807 true,
14808 counter.clone(),
14809 &mut cx,
14810 )
14811 .await;
14812 cx.condition(|editor, _| editor.context_menu_visible())
14813 .await;
14814 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14815
14816 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14817 editor
14818 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14819 .unwrap()
14820 });
14821 cx.assert_editor_state(indoc! {"
14822 one.second_completion
14823 two sixth_completionˇ
14824 three sixth_completionˇ
14825 additional edit
14826 "});
14827
14828 apply_additional_edits.await.unwrap();
14829
14830 update_test_language_settings(&mut cx, |settings| {
14831 settings.defaults.show_completions_on_input = Some(false);
14832 });
14833 cx.set_state("editorˇ");
14834 cx.simulate_keystroke(".");
14835 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14836 cx.simulate_keystrokes("c l o");
14837 cx.assert_editor_state("editor.cloˇ");
14838 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14839 cx.update_editor(|editor, window, cx| {
14840 editor.show_completions(&ShowCompletions, window, cx);
14841 });
14842 handle_completion_request(
14843 "editor.<clo|>",
14844 vec!["close", "clobber"],
14845 true,
14846 counter.clone(),
14847 &mut cx,
14848 )
14849 .await;
14850 cx.condition(|editor, _| editor.context_menu_visible())
14851 .await;
14852 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14853
14854 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14855 editor
14856 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14857 .unwrap()
14858 });
14859 cx.assert_editor_state("editor.clobberˇ");
14860 handle_resolve_completion_request(&mut cx, None).await;
14861 apply_additional_edits.await.unwrap();
14862}
14863
14864#[gpui::test]
14865async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
14866 init_test(cx, |_| {});
14867
14868 let fs = FakeFs::new(cx.executor());
14869 fs.insert_tree(
14870 path!("/a"),
14871 json!({
14872 "main.rs": "",
14873 }),
14874 )
14875 .await;
14876
14877 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14878 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14879 language_registry.add(rust_lang());
14880 let command_calls = Arc::new(AtomicUsize::new(0));
14881 let registered_command = "_the/command";
14882
14883 let closure_command_calls = command_calls.clone();
14884 let mut fake_servers = language_registry.register_fake_lsp(
14885 "Rust",
14886 FakeLspAdapter {
14887 capabilities: lsp::ServerCapabilities {
14888 completion_provider: Some(lsp::CompletionOptions {
14889 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14890 ..lsp::CompletionOptions::default()
14891 }),
14892 execute_command_provider: Some(lsp::ExecuteCommandOptions {
14893 commands: vec![registered_command.to_owned()],
14894 ..lsp::ExecuteCommandOptions::default()
14895 }),
14896 ..lsp::ServerCapabilities::default()
14897 },
14898 initializer: Some(Box::new(move |fake_server| {
14899 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14900 move |params, _| async move {
14901 Ok(Some(lsp::CompletionResponse::Array(vec![
14902 lsp::CompletionItem {
14903 label: "registered_command".to_owned(),
14904 text_edit: gen_text_edit(¶ms, ""),
14905 command: Some(lsp::Command {
14906 title: registered_command.to_owned(),
14907 command: "_the/command".to_owned(),
14908 arguments: Some(vec![serde_json::Value::Bool(true)]),
14909 }),
14910 ..lsp::CompletionItem::default()
14911 },
14912 lsp::CompletionItem {
14913 label: "unregistered_command".to_owned(),
14914 text_edit: gen_text_edit(¶ms, ""),
14915 command: Some(lsp::Command {
14916 title: "????????????".to_owned(),
14917 command: "????????????".to_owned(),
14918 arguments: Some(vec![serde_json::Value::Null]),
14919 }),
14920 ..lsp::CompletionItem::default()
14921 },
14922 ])))
14923 },
14924 );
14925 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
14926 let command_calls = closure_command_calls.clone();
14927 move |params, _| {
14928 assert_eq!(params.command, registered_command);
14929 let command_calls = command_calls.clone();
14930 async move {
14931 command_calls.fetch_add(1, atomic::Ordering::Release);
14932 Ok(Some(json!(null)))
14933 }
14934 }
14935 });
14936 })),
14937 ..FakeLspAdapter::default()
14938 },
14939 );
14940 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14941 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14942 let editor = workspace
14943 .update(cx, |workspace, window, cx| {
14944 workspace.open_abs_path(
14945 PathBuf::from(path!("/a/main.rs")),
14946 OpenOptions::default(),
14947 window,
14948 cx,
14949 )
14950 })
14951 .unwrap()
14952 .await
14953 .unwrap()
14954 .downcast::<Editor>()
14955 .unwrap();
14956 let _fake_server = fake_servers.next().await.unwrap();
14957
14958 editor.update_in(cx, |editor, window, cx| {
14959 cx.focus_self(window);
14960 editor.move_to_end(&MoveToEnd, window, cx);
14961 editor.handle_input(".", window, cx);
14962 });
14963 cx.run_until_parked();
14964 editor.update(cx, |editor, _| {
14965 assert!(editor.context_menu_visible());
14966 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14967 {
14968 let completion_labels = menu
14969 .completions
14970 .borrow()
14971 .iter()
14972 .map(|c| c.label.text.clone())
14973 .collect::<Vec<_>>();
14974 assert_eq!(
14975 completion_labels,
14976 &["registered_command", "unregistered_command",],
14977 );
14978 } else {
14979 panic!("expected completion menu to be open");
14980 }
14981 });
14982
14983 editor
14984 .update_in(cx, |editor, window, cx| {
14985 editor
14986 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14987 .unwrap()
14988 })
14989 .await
14990 .unwrap();
14991 cx.run_until_parked();
14992 assert_eq!(
14993 command_calls.load(atomic::Ordering::Acquire),
14994 1,
14995 "For completion with a registered command, Zed should send a command execution request",
14996 );
14997
14998 editor.update_in(cx, |editor, window, cx| {
14999 cx.focus_self(window);
15000 editor.handle_input(".", window, cx);
15001 });
15002 cx.run_until_parked();
15003 editor.update(cx, |editor, _| {
15004 assert!(editor.context_menu_visible());
15005 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15006 {
15007 let completion_labels = menu
15008 .completions
15009 .borrow()
15010 .iter()
15011 .map(|c| c.label.text.clone())
15012 .collect::<Vec<_>>();
15013 assert_eq!(
15014 completion_labels,
15015 &["registered_command", "unregistered_command",],
15016 );
15017 } else {
15018 panic!("expected completion menu to be open");
15019 }
15020 });
15021 editor
15022 .update_in(cx, |editor, window, cx| {
15023 editor.context_menu_next(&Default::default(), window, cx);
15024 editor
15025 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15026 .unwrap()
15027 })
15028 .await
15029 .unwrap();
15030 cx.run_until_parked();
15031 assert_eq!(
15032 command_calls.load(atomic::Ordering::Acquire),
15033 1,
15034 "For completion with an unregistered command, Zed should not send a command execution request",
15035 );
15036}
15037
15038#[gpui::test]
15039async fn test_completion_reuse(cx: &mut TestAppContext) {
15040 init_test(cx, |_| {});
15041
15042 let mut cx = EditorLspTestContext::new_rust(
15043 lsp::ServerCapabilities {
15044 completion_provider: Some(lsp::CompletionOptions {
15045 trigger_characters: Some(vec![".".to_string()]),
15046 ..Default::default()
15047 }),
15048 ..Default::default()
15049 },
15050 cx,
15051 )
15052 .await;
15053
15054 let counter = Arc::new(AtomicUsize::new(0));
15055 cx.set_state("objˇ");
15056 cx.simulate_keystroke(".");
15057
15058 // Initial completion request returns complete results
15059 let is_incomplete = false;
15060 handle_completion_request(
15061 "obj.|<>",
15062 vec!["a", "ab", "abc"],
15063 is_incomplete,
15064 counter.clone(),
15065 &mut cx,
15066 )
15067 .await;
15068 cx.run_until_parked();
15069 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15070 cx.assert_editor_state("obj.ˇ");
15071 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15072
15073 // Type "a" - filters existing completions
15074 cx.simulate_keystroke("a");
15075 cx.run_until_parked();
15076 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15077 cx.assert_editor_state("obj.aˇ");
15078 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15079
15080 // Type "b" - filters existing completions
15081 cx.simulate_keystroke("b");
15082 cx.run_until_parked();
15083 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15084 cx.assert_editor_state("obj.abˇ");
15085 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15086
15087 // Type "c" - filters existing completions
15088 cx.simulate_keystroke("c");
15089 cx.run_until_parked();
15090 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15091 cx.assert_editor_state("obj.abcˇ");
15092 check_displayed_completions(vec!["abc"], &mut cx);
15093
15094 // Backspace to delete "c" - filters existing completions
15095 cx.update_editor(|editor, window, cx| {
15096 editor.backspace(&Backspace, window, cx);
15097 });
15098 cx.run_until_parked();
15099 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15100 cx.assert_editor_state("obj.abˇ");
15101 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15102
15103 // Moving cursor to the left dismisses menu.
15104 cx.update_editor(|editor, window, cx| {
15105 editor.move_left(&MoveLeft, window, cx);
15106 });
15107 cx.run_until_parked();
15108 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15109 cx.assert_editor_state("obj.aˇb");
15110 cx.update_editor(|editor, _, _| {
15111 assert_eq!(editor.context_menu_visible(), false);
15112 });
15113
15114 // Type "b" - new request
15115 cx.simulate_keystroke("b");
15116 let is_incomplete = false;
15117 handle_completion_request(
15118 "obj.<ab|>a",
15119 vec!["ab", "abc"],
15120 is_incomplete,
15121 counter.clone(),
15122 &mut cx,
15123 )
15124 .await;
15125 cx.run_until_parked();
15126 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15127 cx.assert_editor_state("obj.abˇb");
15128 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15129
15130 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15131 cx.update_editor(|editor, window, cx| {
15132 editor.backspace(&Backspace, window, cx);
15133 });
15134 let is_incomplete = false;
15135 handle_completion_request(
15136 "obj.<a|>b",
15137 vec!["a", "ab", "abc"],
15138 is_incomplete,
15139 counter.clone(),
15140 &mut cx,
15141 )
15142 .await;
15143 cx.run_until_parked();
15144 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15145 cx.assert_editor_state("obj.aˇb");
15146 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15147
15148 // Backspace to delete "a" - dismisses menu.
15149 cx.update_editor(|editor, window, cx| {
15150 editor.backspace(&Backspace, window, cx);
15151 });
15152 cx.run_until_parked();
15153 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15154 cx.assert_editor_state("obj.ˇb");
15155 cx.update_editor(|editor, _, _| {
15156 assert_eq!(editor.context_menu_visible(), false);
15157 });
15158}
15159
15160#[gpui::test]
15161async fn test_word_completion(cx: &mut TestAppContext) {
15162 let lsp_fetch_timeout_ms = 10;
15163 init_test(cx, |language_settings| {
15164 language_settings.defaults.completions = Some(CompletionSettingsContent {
15165 words_min_length: Some(0),
15166 lsp_fetch_timeout_ms: Some(10),
15167 lsp_insert_mode: Some(LspInsertMode::Insert),
15168 ..Default::default()
15169 });
15170 });
15171
15172 let mut cx = EditorLspTestContext::new_rust(
15173 lsp::ServerCapabilities {
15174 completion_provider: Some(lsp::CompletionOptions {
15175 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15176 ..lsp::CompletionOptions::default()
15177 }),
15178 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15179 ..lsp::ServerCapabilities::default()
15180 },
15181 cx,
15182 )
15183 .await;
15184
15185 let throttle_completions = Arc::new(AtomicBool::new(false));
15186
15187 let lsp_throttle_completions = throttle_completions.clone();
15188 let _completion_requests_handler =
15189 cx.lsp
15190 .server
15191 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15192 let lsp_throttle_completions = lsp_throttle_completions.clone();
15193 let cx = cx.clone();
15194 async move {
15195 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15196 cx.background_executor()
15197 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15198 .await;
15199 }
15200 Ok(Some(lsp::CompletionResponse::Array(vec![
15201 lsp::CompletionItem {
15202 label: "first".into(),
15203 ..lsp::CompletionItem::default()
15204 },
15205 lsp::CompletionItem {
15206 label: "last".into(),
15207 ..lsp::CompletionItem::default()
15208 },
15209 ])))
15210 }
15211 });
15212
15213 cx.set_state(indoc! {"
15214 oneˇ
15215 two
15216 three
15217 "});
15218 cx.simulate_keystroke(".");
15219 cx.executor().run_until_parked();
15220 cx.condition(|editor, _| editor.context_menu_visible())
15221 .await;
15222 cx.update_editor(|editor, window, cx| {
15223 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15224 {
15225 assert_eq!(
15226 completion_menu_entries(menu),
15227 &["first", "last"],
15228 "When LSP server is fast to reply, no fallback word completions are used"
15229 );
15230 } else {
15231 panic!("expected completion menu to be open");
15232 }
15233 editor.cancel(&Cancel, window, cx);
15234 });
15235 cx.executor().run_until_parked();
15236 cx.condition(|editor, _| !editor.context_menu_visible())
15237 .await;
15238
15239 throttle_completions.store(true, atomic::Ordering::Release);
15240 cx.simulate_keystroke(".");
15241 cx.executor()
15242 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15243 cx.executor().run_until_parked();
15244 cx.condition(|editor, _| editor.context_menu_visible())
15245 .await;
15246 cx.update_editor(|editor, _, _| {
15247 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15248 {
15249 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15250 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15251 } else {
15252 panic!("expected completion menu to be open");
15253 }
15254 });
15255}
15256
15257#[gpui::test]
15258async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15259 init_test(cx, |language_settings| {
15260 language_settings.defaults.completions = Some(CompletionSettingsContent {
15261 words: Some(WordsCompletionMode::Enabled),
15262 words_min_length: Some(0),
15263 lsp_insert_mode: Some(LspInsertMode::Insert),
15264 ..Default::default()
15265 });
15266 });
15267
15268 let mut cx = EditorLspTestContext::new_rust(
15269 lsp::ServerCapabilities {
15270 completion_provider: Some(lsp::CompletionOptions {
15271 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15272 ..lsp::CompletionOptions::default()
15273 }),
15274 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15275 ..lsp::ServerCapabilities::default()
15276 },
15277 cx,
15278 )
15279 .await;
15280
15281 let _completion_requests_handler =
15282 cx.lsp
15283 .server
15284 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15285 Ok(Some(lsp::CompletionResponse::Array(vec![
15286 lsp::CompletionItem {
15287 label: "first".into(),
15288 ..lsp::CompletionItem::default()
15289 },
15290 lsp::CompletionItem {
15291 label: "last".into(),
15292 ..lsp::CompletionItem::default()
15293 },
15294 ])))
15295 });
15296
15297 cx.set_state(indoc! {"ˇ
15298 first
15299 last
15300 second
15301 "});
15302 cx.simulate_keystroke(".");
15303 cx.executor().run_until_parked();
15304 cx.condition(|editor, _| editor.context_menu_visible())
15305 .await;
15306 cx.update_editor(|editor, _, _| {
15307 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15308 {
15309 assert_eq!(
15310 completion_menu_entries(menu),
15311 &["first", "last", "second"],
15312 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15313 );
15314 } else {
15315 panic!("expected completion menu to be open");
15316 }
15317 });
15318}
15319
15320#[gpui::test]
15321async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15322 init_test(cx, |language_settings| {
15323 language_settings.defaults.completions = Some(CompletionSettingsContent {
15324 words: Some(WordsCompletionMode::Disabled),
15325 words_min_length: Some(0),
15326 lsp_insert_mode: Some(LspInsertMode::Insert),
15327 ..Default::default()
15328 });
15329 });
15330
15331 let mut cx = EditorLspTestContext::new_rust(
15332 lsp::ServerCapabilities {
15333 completion_provider: Some(lsp::CompletionOptions {
15334 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15335 ..lsp::CompletionOptions::default()
15336 }),
15337 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15338 ..lsp::ServerCapabilities::default()
15339 },
15340 cx,
15341 )
15342 .await;
15343
15344 let _completion_requests_handler =
15345 cx.lsp
15346 .server
15347 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15348 panic!("LSP completions should not be queried when dealing with word completions")
15349 });
15350
15351 cx.set_state(indoc! {"ˇ
15352 first
15353 last
15354 second
15355 "});
15356 cx.update_editor(|editor, window, cx| {
15357 editor.show_word_completions(&ShowWordCompletions, window, cx);
15358 });
15359 cx.executor().run_until_parked();
15360 cx.condition(|editor, _| editor.context_menu_visible())
15361 .await;
15362 cx.update_editor(|editor, _, _| {
15363 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15364 {
15365 assert_eq!(
15366 completion_menu_entries(menu),
15367 &["first", "last", "second"],
15368 "`ShowWordCompletions` action should show word completions"
15369 );
15370 } else {
15371 panic!("expected completion menu to be open");
15372 }
15373 });
15374
15375 cx.simulate_keystroke("l");
15376 cx.executor().run_until_parked();
15377 cx.condition(|editor, _| editor.context_menu_visible())
15378 .await;
15379 cx.update_editor(|editor, _, _| {
15380 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15381 {
15382 assert_eq!(
15383 completion_menu_entries(menu),
15384 &["last"],
15385 "After showing word completions, further editing should filter them and not query the LSP"
15386 );
15387 } else {
15388 panic!("expected completion menu to be open");
15389 }
15390 });
15391}
15392
15393#[gpui::test]
15394async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15395 init_test(cx, |language_settings| {
15396 language_settings.defaults.completions = Some(CompletionSettingsContent {
15397 words_min_length: Some(0),
15398 lsp: Some(false),
15399 lsp_insert_mode: Some(LspInsertMode::Insert),
15400 ..Default::default()
15401 });
15402 });
15403
15404 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15405
15406 cx.set_state(indoc! {"ˇ
15407 0_usize
15408 let
15409 33
15410 4.5f32
15411 "});
15412 cx.update_editor(|editor, window, cx| {
15413 editor.show_completions(&ShowCompletions, window, cx);
15414 });
15415 cx.executor().run_until_parked();
15416 cx.condition(|editor, _| editor.context_menu_visible())
15417 .await;
15418 cx.update_editor(|editor, window, cx| {
15419 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15420 {
15421 assert_eq!(
15422 completion_menu_entries(menu),
15423 &["let"],
15424 "With no digits in the completion query, no digits should be in the word completions"
15425 );
15426 } else {
15427 panic!("expected completion menu to be open");
15428 }
15429 editor.cancel(&Cancel, window, cx);
15430 });
15431
15432 cx.set_state(indoc! {"3ˇ
15433 0_usize
15434 let
15435 3
15436 33.35f32
15437 "});
15438 cx.update_editor(|editor, window, cx| {
15439 editor.show_completions(&ShowCompletions, window, cx);
15440 });
15441 cx.executor().run_until_parked();
15442 cx.condition(|editor, _| editor.context_menu_visible())
15443 .await;
15444 cx.update_editor(|editor, _, _| {
15445 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15446 {
15447 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15448 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15449 } else {
15450 panic!("expected completion menu to be open");
15451 }
15452 });
15453}
15454
15455#[gpui::test]
15456async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15457 init_test(cx, |language_settings| {
15458 language_settings.defaults.completions = Some(CompletionSettingsContent {
15459 words: Some(WordsCompletionMode::Enabled),
15460 words_min_length: Some(3),
15461 lsp_insert_mode: Some(LspInsertMode::Insert),
15462 ..Default::default()
15463 });
15464 });
15465
15466 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15467 cx.set_state(indoc! {"ˇ
15468 wow
15469 wowen
15470 wowser
15471 "});
15472 cx.simulate_keystroke("w");
15473 cx.executor().run_until_parked();
15474 cx.update_editor(|editor, _, _| {
15475 if editor.context_menu.borrow_mut().is_some() {
15476 panic!(
15477 "expected completion menu to be hidden, as words completion threshold is not met"
15478 );
15479 }
15480 });
15481
15482 cx.update_editor(|editor, window, cx| {
15483 editor.show_word_completions(&ShowWordCompletions, window, cx);
15484 });
15485 cx.executor().run_until_parked();
15486 cx.update_editor(|editor, window, cx| {
15487 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15488 {
15489 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");
15490 } else {
15491 panic!("expected completion menu to be open after the word completions are called with an action");
15492 }
15493
15494 editor.cancel(&Cancel, window, cx);
15495 });
15496 cx.update_editor(|editor, _, _| {
15497 if editor.context_menu.borrow_mut().is_some() {
15498 panic!("expected completion menu to be hidden after canceling");
15499 }
15500 });
15501
15502 cx.simulate_keystroke("o");
15503 cx.executor().run_until_parked();
15504 cx.update_editor(|editor, _, _| {
15505 if editor.context_menu.borrow_mut().is_some() {
15506 panic!(
15507 "expected completion menu to be hidden, as words completion threshold is not met still"
15508 );
15509 }
15510 });
15511
15512 cx.simulate_keystroke("w");
15513 cx.executor().run_until_parked();
15514 cx.update_editor(|editor, _, _| {
15515 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15516 {
15517 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15518 } else {
15519 panic!("expected completion menu to be open after the word completions threshold is met");
15520 }
15521 });
15522}
15523
15524#[gpui::test]
15525async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15526 init_test(cx, |language_settings| {
15527 language_settings.defaults.completions = Some(CompletionSettingsContent {
15528 words: Some(WordsCompletionMode::Enabled),
15529 words_min_length: Some(0),
15530 lsp_insert_mode: Some(LspInsertMode::Insert),
15531 ..Default::default()
15532 });
15533 });
15534
15535 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15536 cx.update_editor(|editor, _, _| {
15537 editor.disable_word_completions();
15538 });
15539 cx.set_state(indoc! {"ˇ
15540 wow
15541 wowen
15542 wowser
15543 "});
15544 cx.simulate_keystroke("w");
15545 cx.executor().run_until_parked();
15546 cx.update_editor(|editor, _, _| {
15547 if editor.context_menu.borrow_mut().is_some() {
15548 panic!(
15549 "expected completion menu to be hidden, as words completion are disabled for this editor"
15550 );
15551 }
15552 });
15553
15554 cx.update_editor(|editor, window, cx| {
15555 editor.show_word_completions(&ShowWordCompletions, window, cx);
15556 });
15557 cx.executor().run_until_parked();
15558 cx.update_editor(|editor, _, _| {
15559 if editor.context_menu.borrow_mut().is_some() {
15560 panic!(
15561 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15562 );
15563 }
15564 });
15565}
15566
15567#[gpui::test]
15568async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15569 init_test(cx, |language_settings| {
15570 language_settings.defaults.completions = Some(CompletionSettingsContent {
15571 words: Some(WordsCompletionMode::Disabled),
15572 words_min_length: Some(0),
15573 lsp_insert_mode: Some(LspInsertMode::Insert),
15574 ..Default::default()
15575 });
15576 });
15577
15578 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15579 cx.update_editor(|editor, _, _| {
15580 editor.set_completion_provider(None);
15581 });
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!("expected completion menu to be hidden, as disabled in settings");
15592 }
15593 });
15594}
15595
15596fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15597 let position = || lsp::Position {
15598 line: params.text_document_position.position.line,
15599 character: params.text_document_position.position.character,
15600 };
15601 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15602 range: lsp::Range {
15603 start: position(),
15604 end: position(),
15605 },
15606 new_text: text.to_string(),
15607 }))
15608}
15609
15610#[gpui::test]
15611async fn test_multiline_completion(cx: &mut TestAppContext) {
15612 init_test(cx, |_| {});
15613
15614 let fs = FakeFs::new(cx.executor());
15615 fs.insert_tree(
15616 path!("/a"),
15617 json!({
15618 "main.ts": "a",
15619 }),
15620 )
15621 .await;
15622
15623 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15624 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15625 let typescript_language = Arc::new(Language::new(
15626 LanguageConfig {
15627 name: "TypeScript".into(),
15628 matcher: LanguageMatcher {
15629 path_suffixes: vec!["ts".to_string()],
15630 ..LanguageMatcher::default()
15631 },
15632 line_comments: vec!["// ".into()],
15633 ..LanguageConfig::default()
15634 },
15635 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15636 ));
15637 language_registry.add(typescript_language.clone());
15638 let mut fake_servers = language_registry.register_fake_lsp(
15639 "TypeScript",
15640 FakeLspAdapter {
15641 capabilities: lsp::ServerCapabilities {
15642 completion_provider: Some(lsp::CompletionOptions {
15643 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15644 ..lsp::CompletionOptions::default()
15645 }),
15646 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15647 ..lsp::ServerCapabilities::default()
15648 },
15649 // Emulate vtsls label generation
15650 label_for_completion: Some(Box::new(|item, _| {
15651 let text = if let Some(description) = item
15652 .label_details
15653 .as_ref()
15654 .and_then(|label_details| label_details.description.as_ref())
15655 {
15656 format!("{} {}", item.label, description)
15657 } else if let Some(detail) = &item.detail {
15658 format!("{} {}", item.label, detail)
15659 } else {
15660 item.label.clone()
15661 };
15662 Some(language::CodeLabel::plain(text, None))
15663 })),
15664 ..FakeLspAdapter::default()
15665 },
15666 );
15667 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15668 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15669 let worktree_id = workspace
15670 .update(cx, |workspace, _window, cx| {
15671 workspace.project().update(cx, |project, cx| {
15672 project.worktrees(cx).next().unwrap().read(cx).id()
15673 })
15674 })
15675 .unwrap();
15676 let _buffer = project
15677 .update(cx, |project, cx| {
15678 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15679 })
15680 .await
15681 .unwrap();
15682 let editor = workspace
15683 .update(cx, |workspace, window, cx| {
15684 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15685 })
15686 .unwrap()
15687 .await
15688 .unwrap()
15689 .downcast::<Editor>()
15690 .unwrap();
15691 let fake_server = fake_servers.next().await.unwrap();
15692
15693 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15694 let multiline_label_2 = "a\nb\nc\n";
15695 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15696 let multiline_description = "d\ne\nf\n";
15697 let multiline_detail_2 = "g\nh\ni\n";
15698
15699 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15700 move |params, _| async move {
15701 Ok(Some(lsp::CompletionResponse::Array(vec![
15702 lsp::CompletionItem {
15703 label: multiline_label.to_string(),
15704 text_edit: gen_text_edit(¶ms, "new_text_1"),
15705 ..lsp::CompletionItem::default()
15706 },
15707 lsp::CompletionItem {
15708 label: "single line label 1".to_string(),
15709 detail: Some(multiline_detail.to_string()),
15710 text_edit: gen_text_edit(¶ms, "new_text_2"),
15711 ..lsp::CompletionItem::default()
15712 },
15713 lsp::CompletionItem {
15714 label: "single line label 2".to_string(),
15715 label_details: Some(lsp::CompletionItemLabelDetails {
15716 description: Some(multiline_description.to_string()),
15717 detail: None,
15718 }),
15719 text_edit: gen_text_edit(¶ms, "new_text_2"),
15720 ..lsp::CompletionItem::default()
15721 },
15722 lsp::CompletionItem {
15723 label: multiline_label_2.to_string(),
15724 detail: Some(multiline_detail_2.to_string()),
15725 text_edit: gen_text_edit(¶ms, "new_text_3"),
15726 ..lsp::CompletionItem::default()
15727 },
15728 lsp::CompletionItem {
15729 label: "Label with many spaces and \t but without newlines".to_string(),
15730 detail: Some(
15731 "Details with many spaces and \t but without newlines".to_string(),
15732 ),
15733 text_edit: gen_text_edit(¶ms, "new_text_4"),
15734 ..lsp::CompletionItem::default()
15735 },
15736 ])))
15737 },
15738 );
15739
15740 editor.update_in(cx, |editor, window, cx| {
15741 cx.focus_self(window);
15742 editor.move_to_end(&MoveToEnd, window, cx);
15743 editor.handle_input(".", window, cx);
15744 });
15745 cx.run_until_parked();
15746 completion_handle.next().await.unwrap();
15747
15748 editor.update(cx, |editor, _| {
15749 assert!(editor.context_menu_visible());
15750 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15751 {
15752 let completion_labels = menu
15753 .completions
15754 .borrow()
15755 .iter()
15756 .map(|c| c.label.text.clone())
15757 .collect::<Vec<_>>();
15758 assert_eq!(
15759 completion_labels,
15760 &[
15761 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15762 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15763 "single line label 2 d e f ",
15764 "a b c g h i ",
15765 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15766 ],
15767 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15768 );
15769
15770 for completion in menu
15771 .completions
15772 .borrow()
15773 .iter() {
15774 assert_eq!(
15775 completion.label.filter_range,
15776 0..completion.label.text.len(),
15777 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15778 );
15779 }
15780 } else {
15781 panic!("expected completion menu to be open");
15782 }
15783 });
15784}
15785
15786#[gpui::test]
15787async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15788 init_test(cx, |_| {});
15789 let mut cx = EditorLspTestContext::new_rust(
15790 lsp::ServerCapabilities {
15791 completion_provider: Some(lsp::CompletionOptions {
15792 trigger_characters: Some(vec![".".to_string()]),
15793 ..Default::default()
15794 }),
15795 ..Default::default()
15796 },
15797 cx,
15798 )
15799 .await;
15800 cx.lsp
15801 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15802 Ok(Some(lsp::CompletionResponse::Array(vec![
15803 lsp::CompletionItem {
15804 label: "first".into(),
15805 ..Default::default()
15806 },
15807 lsp::CompletionItem {
15808 label: "last".into(),
15809 ..Default::default()
15810 },
15811 ])))
15812 });
15813 cx.set_state("variableˇ");
15814 cx.simulate_keystroke(".");
15815 cx.executor().run_until_parked();
15816
15817 cx.update_editor(|editor, _, _| {
15818 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15819 {
15820 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15821 } else {
15822 panic!("expected completion menu to be open");
15823 }
15824 });
15825
15826 cx.update_editor(|editor, window, cx| {
15827 editor.move_page_down(&MovePageDown::default(), window, cx);
15828 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15829 {
15830 assert!(
15831 menu.selected_item == 1,
15832 "expected PageDown to select the last item from the context menu"
15833 );
15834 } else {
15835 panic!("expected completion menu to stay open after PageDown");
15836 }
15837 });
15838
15839 cx.update_editor(|editor, window, cx| {
15840 editor.move_page_up(&MovePageUp::default(), window, cx);
15841 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15842 {
15843 assert!(
15844 menu.selected_item == 0,
15845 "expected PageUp to select the first item from the context menu"
15846 );
15847 } else {
15848 panic!("expected completion menu to stay open after PageUp");
15849 }
15850 });
15851}
15852
15853#[gpui::test]
15854async fn test_as_is_completions(cx: &mut TestAppContext) {
15855 init_test(cx, |_| {});
15856 let mut cx = EditorLspTestContext::new_rust(
15857 lsp::ServerCapabilities {
15858 completion_provider: Some(lsp::CompletionOptions {
15859 ..Default::default()
15860 }),
15861 ..Default::default()
15862 },
15863 cx,
15864 )
15865 .await;
15866 cx.lsp
15867 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15868 Ok(Some(lsp::CompletionResponse::Array(vec![
15869 lsp::CompletionItem {
15870 label: "unsafe".into(),
15871 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15872 range: lsp::Range {
15873 start: lsp::Position {
15874 line: 1,
15875 character: 2,
15876 },
15877 end: lsp::Position {
15878 line: 1,
15879 character: 3,
15880 },
15881 },
15882 new_text: "unsafe".to_string(),
15883 })),
15884 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15885 ..Default::default()
15886 },
15887 ])))
15888 });
15889 cx.set_state("fn a() {}\n nˇ");
15890 cx.executor().run_until_parked();
15891 cx.update_editor(|editor, window, cx| {
15892 editor.trigger_completion_on_input("n", true, window, cx)
15893 });
15894 cx.executor().run_until_parked();
15895
15896 cx.update_editor(|editor, window, cx| {
15897 editor.confirm_completion(&Default::default(), window, cx)
15898 });
15899 cx.executor().run_until_parked();
15900 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15901}
15902
15903#[gpui::test]
15904async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15905 init_test(cx, |_| {});
15906 let language =
15907 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15908 let mut cx = EditorLspTestContext::new(
15909 language,
15910 lsp::ServerCapabilities {
15911 completion_provider: Some(lsp::CompletionOptions {
15912 ..lsp::CompletionOptions::default()
15913 }),
15914 ..lsp::ServerCapabilities::default()
15915 },
15916 cx,
15917 )
15918 .await;
15919
15920 cx.set_state(
15921 "#ifndef BAR_H
15922#define BAR_H
15923
15924#include <stdbool.h>
15925
15926int fn_branch(bool do_branch1, bool do_branch2);
15927
15928#endif // BAR_H
15929ˇ",
15930 );
15931 cx.executor().run_until_parked();
15932 cx.update_editor(|editor, window, cx| {
15933 editor.handle_input("#", window, cx);
15934 });
15935 cx.executor().run_until_parked();
15936 cx.update_editor(|editor, window, cx| {
15937 editor.handle_input("i", window, cx);
15938 });
15939 cx.executor().run_until_parked();
15940 cx.update_editor(|editor, window, cx| {
15941 editor.handle_input("n", window, cx);
15942 });
15943 cx.executor().run_until_parked();
15944 cx.assert_editor_state(
15945 "#ifndef BAR_H
15946#define BAR_H
15947
15948#include <stdbool.h>
15949
15950int fn_branch(bool do_branch1, bool do_branch2);
15951
15952#endif // BAR_H
15953#inˇ",
15954 );
15955
15956 cx.lsp
15957 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15958 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15959 is_incomplete: false,
15960 item_defaults: None,
15961 items: vec![lsp::CompletionItem {
15962 kind: Some(lsp::CompletionItemKind::SNIPPET),
15963 label_details: Some(lsp::CompletionItemLabelDetails {
15964 detail: Some("header".to_string()),
15965 description: None,
15966 }),
15967 label: " include".to_string(),
15968 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15969 range: lsp::Range {
15970 start: lsp::Position {
15971 line: 8,
15972 character: 1,
15973 },
15974 end: lsp::Position {
15975 line: 8,
15976 character: 1,
15977 },
15978 },
15979 new_text: "include \"$0\"".to_string(),
15980 })),
15981 sort_text: Some("40b67681include".to_string()),
15982 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15983 filter_text: Some("include".to_string()),
15984 insert_text: Some("include \"$0\"".to_string()),
15985 ..lsp::CompletionItem::default()
15986 }],
15987 })))
15988 });
15989 cx.update_editor(|editor, window, cx| {
15990 editor.show_completions(&ShowCompletions, window, cx);
15991 });
15992 cx.executor().run_until_parked();
15993 cx.update_editor(|editor, window, cx| {
15994 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15995 });
15996 cx.executor().run_until_parked();
15997 cx.assert_editor_state(
15998 "#ifndef BAR_H
15999#define BAR_H
16000
16001#include <stdbool.h>
16002
16003int fn_branch(bool do_branch1, bool do_branch2);
16004
16005#endif // BAR_H
16006#include \"ˇ\"",
16007 );
16008
16009 cx.lsp
16010 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16011 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16012 is_incomplete: true,
16013 item_defaults: None,
16014 items: vec![lsp::CompletionItem {
16015 kind: Some(lsp::CompletionItemKind::FILE),
16016 label: "AGL/".to_string(),
16017 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16018 range: lsp::Range {
16019 start: lsp::Position {
16020 line: 8,
16021 character: 10,
16022 },
16023 end: lsp::Position {
16024 line: 8,
16025 character: 11,
16026 },
16027 },
16028 new_text: "AGL/".to_string(),
16029 })),
16030 sort_text: Some("40b67681AGL/".to_string()),
16031 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16032 filter_text: Some("AGL/".to_string()),
16033 insert_text: Some("AGL/".to_string()),
16034 ..lsp::CompletionItem::default()
16035 }],
16036 })))
16037 });
16038 cx.update_editor(|editor, window, cx| {
16039 editor.show_completions(&ShowCompletions, window, cx);
16040 });
16041 cx.executor().run_until_parked();
16042 cx.update_editor(|editor, window, cx| {
16043 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16044 });
16045 cx.executor().run_until_parked();
16046 cx.assert_editor_state(
16047 r##"#ifndef BAR_H
16048#define BAR_H
16049
16050#include <stdbool.h>
16051
16052int fn_branch(bool do_branch1, bool do_branch2);
16053
16054#endif // BAR_H
16055#include "AGL/ˇ"##,
16056 );
16057
16058 cx.update_editor(|editor, window, cx| {
16059 editor.handle_input("\"", window, cx);
16060 });
16061 cx.executor().run_until_parked();
16062 cx.assert_editor_state(
16063 r##"#ifndef BAR_H
16064#define BAR_H
16065
16066#include <stdbool.h>
16067
16068int fn_branch(bool do_branch1, bool do_branch2);
16069
16070#endif // BAR_H
16071#include "AGL/"ˇ"##,
16072 );
16073}
16074
16075#[gpui::test]
16076async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16077 init_test(cx, |_| {});
16078
16079 let mut cx = EditorLspTestContext::new_rust(
16080 lsp::ServerCapabilities {
16081 completion_provider: Some(lsp::CompletionOptions {
16082 trigger_characters: Some(vec![".".to_string()]),
16083 resolve_provider: Some(true),
16084 ..Default::default()
16085 }),
16086 ..Default::default()
16087 },
16088 cx,
16089 )
16090 .await;
16091
16092 cx.set_state("fn main() { let a = 2ˇ; }");
16093 cx.simulate_keystroke(".");
16094 let completion_item = lsp::CompletionItem {
16095 label: "Some".into(),
16096 kind: Some(lsp::CompletionItemKind::SNIPPET),
16097 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16098 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16099 kind: lsp::MarkupKind::Markdown,
16100 value: "```rust\nSome(2)\n```".to_string(),
16101 })),
16102 deprecated: Some(false),
16103 sort_text: Some("Some".to_string()),
16104 filter_text: Some("Some".to_string()),
16105 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16106 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16107 range: lsp::Range {
16108 start: lsp::Position {
16109 line: 0,
16110 character: 22,
16111 },
16112 end: lsp::Position {
16113 line: 0,
16114 character: 22,
16115 },
16116 },
16117 new_text: "Some(2)".to_string(),
16118 })),
16119 additional_text_edits: Some(vec![lsp::TextEdit {
16120 range: lsp::Range {
16121 start: lsp::Position {
16122 line: 0,
16123 character: 20,
16124 },
16125 end: lsp::Position {
16126 line: 0,
16127 character: 22,
16128 },
16129 },
16130 new_text: "".to_string(),
16131 }]),
16132 ..Default::default()
16133 };
16134
16135 let closure_completion_item = completion_item.clone();
16136 let counter = Arc::new(AtomicUsize::new(0));
16137 let counter_clone = counter.clone();
16138 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16139 let task_completion_item = closure_completion_item.clone();
16140 counter_clone.fetch_add(1, atomic::Ordering::Release);
16141 async move {
16142 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16143 is_incomplete: true,
16144 item_defaults: None,
16145 items: vec![task_completion_item],
16146 })))
16147 }
16148 });
16149
16150 cx.condition(|editor, _| editor.context_menu_visible())
16151 .await;
16152 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16153 assert!(request.next().await.is_some());
16154 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16155
16156 cx.simulate_keystrokes("S o m");
16157 cx.condition(|editor, _| editor.context_menu_visible())
16158 .await;
16159 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16160 assert!(request.next().await.is_some());
16161 assert!(request.next().await.is_some());
16162 assert!(request.next().await.is_some());
16163 request.close();
16164 assert!(request.next().await.is_none());
16165 assert_eq!(
16166 counter.load(atomic::Ordering::Acquire),
16167 4,
16168 "With the completions menu open, only one LSP request should happen per input"
16169 );
16170}
16171
16172#[gpui::test]
16173async fn test_toggle_comment(cx: &mut TestAppContext) {
16174 init_test(cx, |_| {});
16175 let mut cx = EditorTestContext::new(cx).await;
16176 let language = Arc::new(Language::new(
16177 LanguageConfig {
16178 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16179 ..Default::default()
16180 },
16181 Some(tree_sitter_rust::LANGUAGE.into()),
16182 ));
16183 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16184
16185 // If multiple selections intersect a line, the line is only toggled once.
16186 cx.set_state(indoc! {"
16187 fn a() {
16188 «//b();
16189 ˇ»// «c();
16190 //ˇ» d();
16191 }
16192 "});
16193
16194 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16195
16196 cx.assert_editor_state(indoc! {"
16197 fn a() {
16198 «b();
16199 c();
16200 ˇ» d();
16201 }
16202 "});
16203
16204 // The comment prefix is inserted at the same column for every line in a
16205 // selection.
16206 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16207
16208 cx.assert_editor_state(indoc! {"
16209 fn a() {
16210 // «b();
16211 // c();
16212 ˇ»// d();
16213 }
16214 "});
16215
16216 // If a selection ends at the beginning of a line, that line is not toggled.
16217 cx.set_selections_state(indoc! {"
16218 fn a() {
16219 // b();
16220 «// c();
16221 ˇ» // d();
16222 }
16223 "});
16224
16225 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16226
16227 cx.assert_editor_state(indoc! {"
16228 fn a() {
16229 // b();
16230 «c();
16231 ˇ» // d();
16232 }
16233 "});
16234
16235 // If a selection span a single line and is empty, the line is toggled.
16236 cx.set_state(indoc! {"
16237 fn a() {
16238 a();
16239 b();
16240 ˇ
16241 }
16242 "});
16243
16244 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16245
16246 cx.assert_editor_state(indoc! {"
16247 fn a() {
16248 a();
16249 b();
16250 //•ˇ
16251 }
16252 "});
16253
16254 // If a selection span multiple lines, empty lines are not toggled.
16255 cx.set_state(indoc! {"
16256 fn a() {
16257 «a();
16258
16259 c();ˇ»
16260 }
16261 "});
16262
16263 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16264
16265 cx.assert_editor_state(indoc! {"
16266 fn a() {
16267 // «a();
16268
16269 // c();ˇ»
16270 }
16271 "});
16272
16273 // If a selection includes multiple comment prefixes, all lines are uncommented.
16274 cx.set_state(indoc! {"
16275 fn a() {
16276 «// a();
16277 /// b();
16278 //! c();ˇ»
16279 }
16280 "});
16281
16282 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16283
16284 cx.assert_editor_state(indoc! {"
16285 fn a() {
16286 «a();
16287 b();
16288 c();ˇ»
16289 }
16290 "});
16291}
16292
16293#[gpui::test]
16294async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16295 init_test(cx, |_| {});
16296 let mut cx = EditorTestContext::new(cx).await;
16297 let language = Arc::new(Language::new(
16298 LanguageConfig {
16299 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16300 ..Default::default()
16301 },
16302 Some(tree_sitter_rust::LANGUAGE.into()),
16303 ));
16304 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16305
16306 let toggle_comments = &ToggleComments {
16307 advance_downwards: false,
16308 ignore_indent: true,
16309 };
16310
16311 // If multiple selections intersect a line, the line is only toggled once.
16312 cx.set_state(indoc! {"
16313 fn a() {
16314 // «b();
16315 // c();
16316 // ˇ» d();
16317 }
16318 "});
16319
16320 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16321
16322 cx.assert_editor_state(indoc! {"
16323 fn a() {
16324 «b();
16325 c();
16326 ˇ» d();
16327 }
16328 "});
16329
16330 // The comment prefix is inserted at the beginning of each line
16331 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16332
16333 cx.assert_editor_state(indoc! {"
16334 fn a() {
16335 // «b();
16336 // c();
16337 // ˇ» d();
16338 }
16339 "});
16340
16341 // If a selection ends at the beginning of a line, that line is not toggled.
16342 cx.set_selections_state(indoc! {"
16343 fn a() {
16344 // b();
16345 // «c();
16346 ˇ»// d();
16347 }
16348 "});
16349
16350 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16351
16352 cx.assert_editor_state(indoc! {"
16353 fn a() {
16354 // b();
16355 «c();
16356 ˇ»// d();
16357 }
16358 "});
16359
16360 // If a selection span a single line and is empty, the line is toggled.
16361 cx.set_state(indoc! {"
16362 fn a() {
16363 a();
16364 b();
16365 ˇ
16366 }
16367 "});
16368
16369 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16370
16371 cx.assert_editor_state(indoc! {"
16372 fn a() {
16373 a();
16374 b();
16375 //ˇ
16376 }
16377 "});
16378
16379 // If a selection span multiple lines, empty lines are not toggled.
16380 cx.set_state(indoc! {"
16381 fn a() {
16382 «a();
16383
16384 c();ˇ»
16385 }
16386 "});
16387
16388 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16389
16390 cx.assert_editor_state(indoc! {"
16391 fn a() {
16392 // «a();
16393
16394 // c();ˇ»
16395 }
16396 "});
16397
16398 // If a selection includes multiple comment prefixes, all lines are uncommented.
16399 cx.set_state(indoc! {"
16400 fn a() {
16401 // «a();
16402 /// b();
16403 //! c();ˇ»
16404 }
16405 "});
16406
16407 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16408
16409 cx.assert_editor_state(indoc! {"
16410 fn a() {
16411 «a();
16412 b();
16413 c();ˇ»
16414 }
16415 "});
16416}
16417
16418#[gpui::test]
16419async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16420 init_test(cx, |_| {});
16421
16422 let language = Arc::new(Language::new(
16423 LanguageConfig {
16424 line_comments: vec!["// ".into()],
16425 ..Default::default()
16426 },
16427 Some(tree_sitter_rust::LANGUAGE.into()),
16428 ));
16429
16430 let mut cx = EditorTestContext::new(cx).await;
16431
16432 cx.language_registry().add(language.clone());
16433 cx.update_buffer(|buffer, cx| {
16434 buffer.set_language(Some(language), cx);
16435 });
16436
16437 let toggle_comments = &ToggleComments {
16438 advance_downwards: true,
16439 ignore_indent: false,
16440 };
16441
16442 // Single cursor on one line -> advance
16443 // Cursor moves horizontally 3 characters as well on non-blank line
16444 cx.set_state(indoc!(
16445 "fn a() {
16446 ˇdog();
16447 cat();
16448 }"
16449 ));
16450 cx.update_editor(|editor, window, cx| {
16451 editor.toggle_comments(toggle_comments, window, cx);
16452 });
16453 cx.assert_editor_state(indoc!(
16454 "fn a() {
16455 // dog();
16456 catˇ();
16457 }"
16458 ));
16459
16460 // Single selection on one line -> don't advance
16461 cx.set_state(indoc!(
16462 "fn a() {
16463 «dog()ˇ»;
16464 cat();
16465 }"
16466 ));
16467 cx.update_editor(|editor, window, cx| {
16468 editor.toggle_comments(toggle_comments, window, cx);
16469 });
16470 cx.assert_editor_state(indoc!(
16471 "fn a() {
16472 // «dog()ˇ»;
16473 cat();
16474 }"
16475 ));
16476
16477 // Multiple cursors on one line -> advance
16478 cx.set_state(indoc!(
16479 "fn a() {
16480 ˇdˇog();
16481 cat();
16482 }"
16483 ));
16484 cx.update_editor(|editor, window, cx| {
16485 editor.toggle_comments(toggle_comments, window, cx);
16486 });
16487 cx.assert_editor_state(indoc!(
16488 "fn a() {
16489 // dog();
16490 catˇ(ˇ);
16491 }"
16492 ));
16493
16494 // Multiple cursors on one line, with selection -> don't advance
16495 cx.set_state(indoc!(
16496 "fn a() {
16497 ˇdˇog«()ˇ»;
16498 cat();
16499 }"
16500 ));
16501 cx.update_editor(|editor, window, cx| {
16502 editor.toggle_comments(toggle_comments, window, cx);
16503 });
16504 cx.assert_editor_state(indoc!(
16505 "fn a() {
16506 // ˇdˇog«()ˇ»;
16507 cat();
16508 }"
16509 ));
16510
16511 // Single cursor on one line -> advance
16512 // Cursor moves to column 0 on blank line
16513 cx.set_state(indoc!(
16514 "fn a() {
16515 ˇdog();
16516
16517 cat();
16518 }"
16519 ));
16520 cx.update_editor(|editor, window, cx| {
16521 editor.toggle_comments(toggle_comments, window, cx);
16522 });
16523 cx.assert_editor_state(indoc!(
16524 "fn a() {
16525 // dog();
16526 ˇ
16527 cat();
16528 }"
16529 ));
16530
16531 // Single cursor on one line -> advance
16532 // Cursor starts and ends at column 0
16533 cx.set_state(indoc!(
16534 "fn a() {
16535 ˇ dog();
16536 cat();
16537 }"
16538 ));
16539 cx.update_editor(|editor, window, cx| {
16540 editor.toggle_comments(toggle_comments, window, cx);
16541 });
16542 cx.assert_editor_state(indoc!(
16543 "fn a() {
16544 // dog();
16545 ˇ cat();
16546 }"
16547 ));
16548}
16549
16550#[gpui::test]
16551async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16552 init_test(cx, |_| {});
16553
16554 let mut cx = EditorTestContext::new(cx).await;
16555
16556 let html_language = Arc::new(
16557 Language::new(
16558 LanguageConfig {
16559 name: "HTML".into(),
16560 block_comment: Some(BlockCommentConfig {
16561 start: "<!-- ".into(),
16562 prefix: "".into(),
16563 end: " -->".into(),
16564 tab_size: 0,
16565 }),
16566 ..Default::default()
16567 },
16568 Some(tree_sitter_html::LANGUAGE.into()),
16569 )
16570 .with_injection_query(
16571 r#"
16572 (script_element
16573 (raw_text) @injection.content
16574 (#set! injection.language "javascript"))
16575 "#,
16576 )
16577 .unwrap(),
16578 );
16579
16580 let javascript_language = Arc::new(Language::new(
16581 LanguageConfig {
16582 name: "JavaScript".into(),
16583 line_comments: vec!["// ".into()],
16584 ..Default::default()
16585 },
16586 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16587 ));
16588
16589 cx.language_registry().add(html_language.clone());
16590 cx.language_registry().add(javascript_language);
16591 cx.update_buffer(|buffer, cx| {
16592 buffer.set_language(Some(html_language), cx);
16593 });
16594
16595 // Toggle comments for empty selections
16596 cx.set_state(
16597 &r#"
16598 <p>A</p>ˇ
16599 <p>B</p>ˇ
16600 <p>C</p>ˇ
16601 "#
16602 .unindent(),
16603 );
16604 cx.update_editor(|editor, window, cx| {
16605 editor.toggle_comments(&ToggleComments::default(), window, cx)
16606 });
16607 cx.assert_editor_state(
16608 &r#"
16609 <!-- <p>A</p>ˇ -->
16610 <!-- <p>B</p>ˇ -->
16611 <!-- <p>C</p>ˇ -->
16612 "#
16613 .unindent(),
16614 );
16615 cx.update_editor(|editor, window, cx| {
16616 editor.toggle_comments(&ToggleComments::default(), window, cx)
16617 });
16618 cx.assert_editor_state(
16619 &r#"
16620 <p>A</p>ˇ
16621 <p>B</p>ˇ
16622 <p>C</p>ˇ
16623 "#
16624 .unindent(),
16625 );
16626
16627 // Toggle comments for mixture of empty and non-empty selections, where
16628 // multiple selections occupy a given line.
16629 cx.set_state(
16630 &r#"
16631 <p>A«</p>
16632 <p>ˇ»B</p>ˇ
16633 <p>C«</p>
16634 <p>ˇ»D</p>ˇ
16635 "#
16636 .unindent(),
16637 );
16638
16639 cx.update_editor(|editor, window, cx| {
16640 editor.toggle_comments(&ToggleComments::default(), window, cx)
16641 });
16642 cx.assert_editor_state(
16643 &r#"
16644 <!-- <p>A«</p>
16645 <p>ˇ»B</p>ˇ -->
16646 <!-- <p>C«</p>
16647 <p>ˇ»D</p>ˇ -->
16648 "#
16649 .unindent(),
16650 );
16651 cx.update_editor(|editor, window, cx| {
16652 editor.toggle_comments(&ToggleComments::default(), window, cx)
16653 });
16654 cx.assert_editor_state(
16655 &r#"
16656 <p>A«</p>
16657 <p>ˇ»B</p>ˇ
16658 <p>C«</p>
16659 <p>ˇ»D</p>ˇ
16660 "#
16661 .unindent(),
16662 );
16663
16664 // Toggle comments when different languages are active for different
16665 // selections.
16666 cx.set_state(
16667 &r#"
16668 ˇ<script>
16669 ˇvar x = new Y();
16670 ˇ</script>
16671 "#
16672 .unindent(),
16673 );
16674 cx.executor().run_until_parked();
16675 cx.update_editor(|editor, window, cx| {
16676 editor.toggle_comments(&ToggleComments::default(), window, cx)
16677 });
16678 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16679 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16680 cx.assert_editor_state(
16681 &r#"
16682 <!-- ˇ<script> -->
16683 // ˇvar x = new Y();
16684 <!-- ˇ</script> -->
16685 "#
16686 .unindent(),
16687 );
16688}
16689
16690#[gpui::test]
16691fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16692 init_test(cx, |_| {});
16693
16694 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16695 let multibuffer = cx.new(|cx| {
16696 let mut multibuffer = MultiBuffer::new(ReadWrite);
16697 multibuffer.push_excerpts(
16698 buffer.clone(),
16699 [
16700 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16701 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16702 ],
16703 cx,
16704 );
16705 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16706 multibuffer
16707 });
16708
16709 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16710 editor.update_in(cx, |editor, window, cx| {
16711 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16712 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16713 s.select_ranges([
16714 Point::new(0, 0)..Point::new(0, 0),
16715 Point::new(1, 0)..Point::new(1, 0),
16716 ])
16717 });
16718
16719 editor.handle_input("X", window, cx);
16720 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16721 assert_eq!(
16722 editor.selections.ranges(&editor.display_snapshot(cx)),
16723 [
16724 Point::new(0, 1)..Point::new(0, 1),
16725 Point::new(1, 1)..Point::new(1, 1),
16726 ]
16727 );
16728
16729 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16730 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16731 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16732 });
16733 editor.backspace(&Default::default(), window, cx);
16734 assert_eq!(editor.text(cx), "Xa\nbbb");
16735 assert_eq!(
16736 editor.selections.ranges(&editor.display_snapshot(cx)),
16737 [Point::new(1, 0)..Point::new(1, 0)]
16738 );
16739
16740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16741 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16742 });
16743 editor.backspace(&Default::default(), window, cx);
16744 assert_eq!(editor.text(cx), "X\nbb");
16745 assert_eq!(
16746 editor.selections.ranges(&editor.display_snapshot(cx)),
16747 [Point::new(0, 1)..Point::new(0, 1)]
16748 );
16749 });
16750}
16751
16752#[gpui::test]
16753fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16754 init_test(cx, |_| {});
16755
16756 let markers = vec![('[', ']').into(), ('(', ')').into()];
16757 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16758 indoc! {"
16759 [aaaa
16760 (bbbb]
16761 cccc)",
16762 },
16763 markers.clone(),
16764 );
16765 let excerpt_ranges = markers.into_iter().map(|marker| {
16766 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16767 ExcerptRange::new(context)
16768 });
16769 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16770 let multibuffer = cx.new(|cx| {
16771 let mut multibuffer = MultiBuffer::new(ReadWrite);
16772 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16773 multibuffer
16774 });
16775
16776 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16777 editor.update_in(cx, |editor, window, cx| {
16778 let (expected_text, selection_ranges) = marked_text_ranges(
16779 indoc! {"
16780 aaaa
16781 bˇbbb
16782 bˇbbˇb
16783 cccc"
16784 },
16785 true,
16786 );
16787 assert_eq!(editor.text(cx), expected_text);
16788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16789 s.select_ranges(
16790 selection_ranges
16791 .iter()
16792 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16793 )
16794 });
16795
16796 editor.handle_input("X", window, cx);
16797
16798 let (expected_text, expected_selections) = marked_text_ranges(
16799 indoc! {"
16800 aaaa
16801 bXˇbbXb
16802 bXˇbbXˇb
16803 cccc"
16804 },
16805 false,
16806 );
16807 assert_eq!(editor.text(cx), expected_text);
16808 assert_eq!(
16809 editor.selections.ranges(&editor.display_snapshot(cx)),
16810 expected_selections
16811 .iter()
16812 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16813 .collect::<Vec<_>>()
16814 );
16815
16816 editor.newline(&Newline, window, cx);
16817 let (expected_text, expected_selections) = marked_text_ranges(
16818 indoc! {"
16819 aaaa
16820 bX
16821 ˇbbX
16822 b
16823 bX
16824 ˇbbX
16825 ˇb
16826 cccc"
16827 },
16828 false,
16829 );
16830 assert_eq!(editor.text(cx), expected_text);
16831 assert_eq!(
16832 editor.selections.ranges(&editor.display_snapshot(cx)),
16833 expected_selections
16834 .iter()
16835 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16836 .collect::<Vec<_>>()
16837 );
16838 });
16839}
16840
16841#[gpui::test]
16842fn test_refresh_selections(cx: &mut TestAppContext) {
16843 init_test(cx, |_| {});
16844
16845 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16846 let mut excerpt1_id = None;
16847 let multibuffer = cx.new(|cx| {
16848 let mut multibuffer = MultiBuffer::new(ReadWrite);
16849 excerpt1_id = multibuffer
16850 .push_excerpts(
16851 buffer.clone(),
16852 [
16853 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16854 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16855 ],
16856 cx,
16857 )
16858 .into_iter()
16859 .next();
16860 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16861 multibuffer
16862 });
16863
16864 let editor = cx.add_window(|window, cx| {
16865 let mut editor = build_editor(multibuffer.clone(), window, cx);
16866 let snapshot = editor.snapshot(window, cx);
16867 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16868 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16869 });
16870 editor.begin_selection(
16871 Point::new(2, 1).to_display_point(&snapshot),
16872 true,
16873 1,
16874 window,
16875 cx,
16876 );
16877 assert_eq!(
16878 editor.selections.ranges(&editor.display_snapshot(cx)),
16879 [
16880 Point::new(1, 3)..Point::new(1, 3),
16881 Point::new(2, 1)..Point::new(2, 1),
16882 ]
16883 );
16884 editor
16885 });
16886
16887 // Refreshing selections is a no-op when excerpts haven't changed.
16888 _ = editor.update(cx, |editor, window, cx| {
16889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16890 assert_eq!(
16891 editor.selections.ranges(&editor.display_snapshot(cx)),
16892 [
16893 Point::new(1, 3)..Point::new(1, 3),
16894 Point::new(2, 1)..Point::new(2, 1),
16895 ]
16896 );
16897 });
16898
16899 multibuffer.update(cx, |multibuffer, cx| {
16900 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16901 });
16902 _ = editor.update(cx, |editor, window, cx| {
16903 // Removing an excerpt causes the first selection to become degenerate.
16904 assert_eq!(
16905 editor.selections.ranges(&editor.display_snapshot(cx)),
16906 [
16907 Point::new(0, 0)..Point::new(0, 0),
16908 Point::new(0, 1)..Point::new(0, 1)
16909 ]
16910 );
16911
16912 // Refreshing selections will relocate the first selection to the original buffer
16913 // location.
16914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16915 assert_eq!(
16916 editor.selections.ranges(&editor.display_snapshot(cx)),
16917 [
16918 Point::new(0, 1)..Point::new(0, 1),
16919 Point::new(0, 3)..Point::new(0, 3)
16920 ]
16921 );
16922 assert!(editor.selections.pending_anchor().is_some());
16923 });
16924}
16925
16926#[gpui::test]
16927fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16928 init_test(cx, |_| {});
16929
16930 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16931 let mut excerpt1_id = None;
16932 let multibuffer = cx.new(|cx| {
16933 let mut multibuffer = MultiBuffer::new(ReadWrite);
16934 excerpt1_id = multibuffer
16935 .push_excerpts(
16936 buffer.clone(),
16937 [
16938 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16939 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16940 ],
16941 cx,
16942 )
16943 .into_iter()
16944 .next();
16945 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16946 multibuffer
16947 });
16948
16949 let editor = cx.add_window(|window, cx| {
16950 let mut editor = build_editor(multibuffer.clone(), window, cx);
16951 let snapshot = editor.snapshot(window, cx);
16952 editor.begin_selection(
16953 Point::new(1, 3).to_display_point(&snapshot),
16954 false,
16955 1,
16956 window,
16957 cx,
16958 );
16959 assert_eq!(
16960 editor.selections.ranges(&editor.display_snapshot(cx)),
16961 [Point::new(1, 3)..Point::new(1, 3)]
16962 );
16963 editor
16964 });
16965
16966 multibuffer.update(cx, |multibuffer, cx| {
16967 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16968 });
16969 _ = editor.update(cx, |editor, window, cx| {
16970 assert_eq!(
16971 editor.selections.ranges(&editor.display_snapshot(cx)),
16972 [Point::new(0, 0)..Point::new(0, 0)]
16973 );
16974
16975 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16976 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16977 assert_eq!(
16978 editor.selections.ranges(&editor.display_snapshot(cx)),
16979 [Point::new(0, 3)..Point::new(0, 3)]
16980 );
16981 assert!(editor.selections.pending_anchor().is_some());
16982 });
16983}
16984
16985#[gpui::test]
16986async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16987 init_test(cx, |_| {});
16988
16989 let language = Arc::new(
16990 Language::new(
16991 LanguageConfig {
16992 brackets: BracketPairConfig {
16993 pairs: vec![
16994 BracketPair {
16995 start: "{".to_string(),
16996 end: "}".to_string(),
16997 close: true,
16998 surround: true,
16999 newline: true,
17000 },
17001 BracketPair {
17002 start: "/* ".to_string(),
17003 end: " */".to_string(),
17004 close: true,
17005 surround: true,
17006 newline: true,
17007 },
17008 ],
17009 ..Default::default()
17010 },
17011 ..Default::default()
17012 },
17013 Some(tree_sitter_rust::LANGUAGE.into()),
17014 )
17015 .with_indents_query("")
17016 .unwrap(),
17017 );
17018
17019 let text = concat!(
17020 "{ }\n", //
17021 " x\n", //
17022 " /* */\n", //
17023 "x\n", //
17024 "{{} }\n", //
17025 );
17026
17027 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17028 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17029 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17030 editor
17031 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17032 .await;
17033
17034 editor.update_in(cx, |editor, window, cx| {
17035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17036 s.select_display_ranges([
17037 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17038 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17039 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17040 ])
17041 });
17042 editor.newline(&Newline, window, cx);
17043
17044 assert_eq!(
17045 editor.buffer().read(cx).read(cx).text(),
17046 concat!(
17047 "{ \n", // Suppress rustfmt
17048 "\n", //
17049 "}\n", //
17050 " x\n", //
17051 " /* \n", //
17052 " \n", //
17053 " */\n", //
17054 "x\n", //
17055 "{{} \n", //
17056 "}\n", //
17057 )
17058 );
17059 });
17060}
17061
17062#[gpui::test]
17063fn test_highlighted_ranges(cx: &mut TestAppContext) {
17064 init_test(cx, |_| {});
17065
17066 let editor = cx.add_window(|window, cx| {
17067 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17068 build_editor(buffer, window, cx)
17069 });
17070
17071 _ = editor.update(cx, |editor, window, cx| {
17072 struct Type1;
17073 struct Type2;
17074
17075 let buffer = editor.buffer.read(cx).snapshot(cx);
17076
17077 let anchor_range =
17078 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17079
17080 editor.highlight_background::<Type1>(
17081 &[
17082 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17083 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17084 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17085 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17086 ],
17087 |_, _| Hsla::red(),
17088 cx,
17089 );
17090 editor.highlight_background::<Type2>(
17091 &[
17092 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17093 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17094 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17095 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17096 ],
17097 |_, _| Hsla::green(),
17098 cx,
17099 );
17100
17101 let snapshot = editor.snapshot(window, cx);
17102 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17103 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17104 &snapshot,
17105 cx.theme(),
17106 );
17107 assert_eq!(
17108 highlighted_ranges,
17109 &[
17110 (
17111 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17112 Hsla::green(),
17113 ),
17114 (
17115 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17116 Hsla::red(),
17117 ),
17118 (
17119 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17120 Hsla::green(),
17121 ),
17122 (
17123 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17124 Hsla::red(),
17125 ),
17126 ]
17127 );
17128 assert_eq!(
17129 editor.sorted_background_highlights_in_range(
17130 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17131 &snapshot,
17132 cx.theme(),
17133 ),
17134 &[(
17135 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17136 Hsla::red(),
17137 )]
17138 );
17139 });
17140}
17141
17142#[gpui::test]
17143async fn test_following(cx: &mut TestAppContext) {
17144 init_test(cx, |_| {});
17145
17146 let fs = FakeFs::new(cx.executor());
17147 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17148
17149 let buffer = project.update(cx, |project, cx| {
17150 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17151 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17152 });
17153 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17154 let follower = cx.update(|cx| {
17155 cx.open_window(
17156 WindowOptions {
17157 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17158 gpui::Point::new(px(0.), px(0.)),
17159 gpui::Point::new(px(10.), px(80.)),
17160 ))),
17161 ..Default::default()
17162 },
17163 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17164 )
17165 .unwrap()
17166 });
17167
17168 let is_still_following = Rc::new(RefCell::new(true));
17169 let follower_edit_event_count = Rc::new(RefCell::new(0));
17170 let pending_update = Rc::new(RefCell::new(None));
17171 let leader_entity = leader.root(cx).unwrap();
17172 let follower_entity = follower.root(cx).unwrap();
17173 _ = follower.update(cx, {
17174 let update = pending_update.clone();
17175 let is_still_following = is_still_following.clone();
17176 let follower_edit_event_count = follower_edit_event_count.clone();
17177 |_, window, cx| {
17178 cx.subscribe_in(
17179 &leader_entity,
17180 window,
17181 move |_, leader, event, window, cx| {
17182 leader.read(cx).add_event_to_update_proto(
17183 event,
17184 &mut update.borrow_mut(),
17185 window,
17186 cx,
17187 );
17188 },
17189 )
17190 .detach();
17191
17192 cx.subscribe_in(
17193 &follower_entity,
17194 window,
17195 move |_, _, event: &EditorEvent, _window, _cx| {
17196 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17197 *is_still_following.borrow_mut() = false;
17198 }
17199
17200 if let EditorEvent::BufferEdited = event {
17201 *follower_edit_event_count.borrow_mut() += 1;
17202 }
17203 },
17204 )
17205 .detach();
17206 }
17207 });
17208
17209 // Update the selections only
17210 _ = leader.update(cx, |leader, window, cx| {
17211 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17212 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17213 });
17214 });
17215 follower
17216 .update(cx, |follower, window, cx| {
17217 follower.apply_update_proto(
17218 &project,
17219 pending_update.borrow_mut().take().unwrap(),
17220 window,
17221 cx,
17222 )
17223 })
17224 .unwrap()
17225 .await
17226 .unwrap();
17227 _ = follower.update(cx, |follower, _, cx| {
17228 assert_eq!(
17229 follower.selections.ranges(&follower.display_snapshot(cx)),
17230 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17231 );
17232 });
17233 assert!(*is_still_following.borrow());
17234 assert_eq!(*follower_edit_event_count.borrow(), 0);
17235
17236 // Update the scroll position only
17237 _ = leader.update(cx, |leader, window, cx| {
17238 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17239 });
17240 follower
17241 .update(cx, |follower, window, cx| {
17242 follower.apply_update_proto(
17243 &project,
17244 pending_update.borrow_mut().take().unwrap(),
17245 window,
17246 cx,
17247 )
17248 })
17249 .unwrap()
17250 .await
17251 .unwrap();
17252 assert_eq!(
17253 follower
17254 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17255 .unwrap(),
17256 gpui::Point::new(1.5, 3.5)
17257 );
17258 assert!(*is_still_following.borrow());
17259 assert_eq!(*follower_edit_event_count.borrow(), 0);
17260
17261 // Update the selections and scroll position. The follower's scroll position is updated
17262 // via autoscroll, not via the leader's exact scroll position.
17263 _ = leader.update(cx, |leader, window, cx| {
17264 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17265 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17266 });
17267 leader.request_autoscroll(Autoscroll::newest(), cx);
17268 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17269 });
17270 follower
17271 .update(cx, |follower, window, cx| {
17272 follower.apply_update_proto(
17273 &project,
17274 pending_update.borrow_mut().take().unwrap(),
17275 window,
17276 cx,
17277 )
17278 })
17279 .unwrap()
17280 .await
17281 .unwrap();
17282 _ = follower.update(cx, |follower, _, cx| {
17283 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17284 assert_eq!(
17285 follower.selections.ranges(&follower.display_snapshot(cx)),
17286 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17287 );
17288 });
17289 assert!(*is_still_following.borrow());
17290
17291 // Creating a pending selection that precedes another selection
17292 _ = leader.update(cx, |leader, window, cx| {
17293 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17294 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17295 });
17296 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17297 });
17298 follower
17299 .update(cx, |follower, window, cx| {
17300 follower.apply_update_proto(
17301 &project,
17302 pending_update.borrow_mut().take().unwrap(),
17303 window,
17304 cx,
17305 )
17306 })
17307 .unwrap()
17308 .await
17309 .unwrap();
17310 _ = follower.update(cx, |follower, _, cx| {
17311 assert_eq!(
17312 follower.selections.ranges(&follower.display_snapshot(cx)),
17313 vec![
17314 MultiBufferOffset(0)..MultiBufferOffset(0),
17315 MultiBufferOffset(1)..MultiBufferOffset(1)
17316 ]
17317 );
17318 });
17319 assert!(*is_still_following.borrow());
17320
17321 // Extend the pending selection so that it surrounds another selection
17322 _ = leader.update(cx, |leader, window, cx| {
17323 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17324 });
17325 follower
17326 .update(cx, |follower, window, cx| {
17327 follower.apply_update_proto(
17328 &project,
17329 pending_update.borrow_mut().take().unwrap(),
17330 window,
17331 cx,
17332 )
17333 })
17334 .unwrap()
17335 .await
17336 .unwrap();
17337 _ = follower.update(cx, |follower, _, cx| {
17338 assert_eq!(
17339 follower.selections.ranges(&follower.display_snapshot(cx)),
17340 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17341 );
17342 });
17343
17344 // Scrolling locally breaks the follow
17345 _ = follower.update(cx, |follower, window, cx| {
17346 let top_anchor = follower
17347 .buffer()
17348 .read(cx)
17349 .read(cx)
17350 .anchor_after(MultiBufferOffset(0));
17351 follower.set_scroll_anchor(
17352 ScrollAnchor {
17353 anchor: top_anchor,
17354 offset: gpui::Point::new(0.0, 0.5),
17355 },
17356 window,
17357 cx,
17358 );
17359 });
17360 assert!(!(*is_still_following.borrow()));
17361}
17362
17363#[gpui::test]
17364async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17365 init_test(cx, |_| {});
17366
17367 let fs = FakeFs::new(cx.executor());
17368 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17369 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17370 let pane = workspace
17371 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17372 .unwrap();
17373
17374 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17375
17376 let leader = pane.update_in(cx, |_, window, cx| {
17377 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17378 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17379 });
17380
17381 // Start following the editor when it has no excerpts.
17382 let mut state_message =
17383 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17384 let workspace_entity = workspace.root(cx).unwrap();
17385 let follower_1 = cx
17386 .update_window(*workspace.deref(), |_, window, cx| {
17387 Editor::from_state_proto(
17388 workspace_entity,
17389 ViewId {
17390 creator: CollaboratorId::PeerId(PeerId::default()),
17391 id: 0,
17392 },
17393 &mut state_message,
17394 window,
17395 cx,
17396 )
17397 })
17398 .unwrap()
17399 .unwrap()
17400 .await
17401 .unwrap();
17402
17403 let update_message = Rc::new(RefCell::new(None));
17404 follower_1.update_in(cx, {
17405 let update = update_message.clone();
17406 |_, window, cx| {
17407 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17408 leader.read(cx).add_event_to_update_proto(
17409 event,
17410 &mut update.borrow_mut(),
17411 window,
17412 cx,
17413 );
17414 })
17415 .detach();
17416 }
17417 });
17418
17419 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17420 (
17421 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17422 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17423 )
17424 });
17425
17426 // Insert some excerpts.
17427 leader.update(cx, |leader, cx| {
17428 leader.buffer.update(cx, |multibuffer, cx| {
17429 multibuffer.set_excerpts_for_path(
17430 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17431 buffer_1.clone(),
17432 vec![
17433 Point::row_range(0..3),
17434 Point::row_range(1..6),
17435 Point::row_range(12..15),
17436 ],
17437 0,
17438 cx,
17439 );
17440 multibuffer.set_excerpts_for_path(
17441 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17442 buffer_2.clone(),
17443 vec![Point::row_range(0..6), Point::row_range(8..12)],
17444 0,
17445 cx,
17446 );
17447 });
17448 });
17449
17450 // Apply the update of adding the excerpts.
17451 follower_1
17452 .update_in(cx, |follower, window, cx| {
17453 follower.apply_update_proto(
17454 &project,
17455 update_message.borrow().clone().unwrap(),
17456 window,
17457 cx,
17458 )
17459 })
17460 .await
17461 .unwrap();
17462 assert_eq!(
17463 follower_1.update(cx, |editor, cx| editor.text(cx)),
17464 leader.update(cx, |editor, cx| editor.text(cx))
17465 );
17466 update_message.borrow_mut().take();
17467
17468 // Start following separately after it already has excerpts.
17469 let mut state_message =
17470 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17471 let workspace_entity = workspace.root(cx).unwrap();
17472 let follower_2 = cx
17473 .update_window(*workspace.deref(), |_, window, cx| {
17474 Editor::from_state_proto(
17475 workspace_entity,
17476 ViewId {
17477 creator: CollaboratorId::PeerId(PeerId::default()),
17478 id: 0,
17479 },
17480 &mut state_message,
17481 window,
17482 cx,
17483 )
17484 })
17485 .unwrap()
17486 .unwrap()
17487 .await
17488 .unwrap();
17489 assert_eq!(
17490 follower_2.update(cx, |editor, cx| editor.text(cx)),
17491 leader.update(cx, |editor, cx| editor.text(cx))
17492 );
17493
17494 // Remove some excerpts.
17495 leader.update(cx, |leader, cx| {
17496 leader.buffer.update(cx, |multibuffer, cx| {
17497 let excerpt_ids = multibuffer.excerpt_ids();
17498 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17499 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17500 });
17501 });
17502
17503 // Apply the update of removing the excerpts.
17504 follower_1
17505 .update_in(cx, |follower, window, cx| {
17506 follower.apply_update_proto(
17507 &project,
17508 update_message.borrow().clone().unwrap(),
17509 window,
17510 cx,
17511 )
17512 })
17513 .await
17514 .unwrap();
17515 follower_2
17516 .update_in(cx, |follower, window, cx| {
17517 follower.apply_update_proto(
17518 &project,
17519 update_message.borrow().clone().unwrap(),
17520 window,
17521 cx,
17522 )
17523 })
17524 .await
17525 .unwrap();
17526 update_message.borrow_mut().take();
17527 assert_eq!(
17528 follower_1.update(cx, |editor, cx| editor.text(cx)),
17529 leader.update(cx, |editor, cx| editor.text(cx))
17530 );
17531}
17532
17533#[gpui::test]
17534async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17535 init_test(cx, |_| {});
17536
17537 let mut cx = EditorTestContext::new(cx).await;
17538 let lsp_store =
17539 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17540
17541 cx.set_state(indoc! {"
17542 ˇfn func(abc def: i32) -> u32 {
17543 }
17544 "});
17545
17546 cx.update(|_, cx| {
17547 lsp_store.update(cx, |lsp_store, cx| {
17548 lsp_store
17549 .update_diagnostics(
17550 LanguageServerId(0),
17551 lsp::PublishDiagnosticsParams {
17552 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17553 version: None,
17554 diagnostics: vec![
17555 lsp::Diagnostic {
17556 range: lsp::Range::new(
17557 lsp::Position::new(0, 11),
17558 lsp::Position::new(0, 12),
17559 ),
17560 severity: Some(lsp::DiagnosticSeverity::ERROR),
17561 ..Default::default()
17562 },
17563 lsp::Diagnostic {
17564 range: lsp::Range::new(
17565 lsp::Position::new(0, 12),
17566 lsp::Position::new(0, 15),
17567 ),
17568 severity: Some(lsp::DiagnosticSeverity::ERROR),
17569 ..Default::default()
17570 },
17571 lsp::Diagnostic {
17572 range: lsp::Range::new(
17573 lsp::Position::new(0, 25),
17574 lsp::Position::new(0, 28),
17575 ),
17576 severity: Some(lsp::DiagnosticSeverity::ERROR),
17577 ..Default::default()
17578 },
17579 ],
17580 },
17581 None,
17582 DiagnosticSourceKind::Pushed,
17583 &[],
17584 cx,
17585 )
17586 .unwrap()
17587 });
17588 });
17589
17590 executor.run_until_parked();
17591
17592 cx.update_editor(|editor, window, cx| {
17593 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17594 });
17595
17596 cx.assert_editor_state(indoc! {"
17597 fn func(abc def: i32) -> ˇu32 {
17598 }
17599 "});
17600
17601 cx.update_editor(|editor, window, cx| {
17602 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17603 });
17604
17605 cx.assert_editor_state(indoc! {"
17606 fn func(abc ˇdef: i32) -> u32 {
17607 }
17608 "});
17609
17610 cx.update_editor(|editor, window, cx| {
17611 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17612 });
17613
17614 cx.assert_editor_state(indoc! {"
17615 fn func(abcˇ def: i32) -> u32 {
17616 }
17617 "});
17618
17619 cx.update_editor(|editor, window, cx| {
17620 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17621 });
17622
17623 cx.assert_editor_state(indoc! {"
17624 fn func(abc def: i32) -> ˇu32 {
17625 }
17626 "});
17627}
17628
17629#[gpui::test]
17630async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17631 init_test(cx, |_| {});
17632
17633 let mut cx = EditorTestContext::new(cx).await;
17634
17635 let diff_base = r#"
17636 use some::mod;
17637
17638 const A: u32 = 42;
17639
17640 fn main() {
17641 println!("hello");
17642
17643 println!("world");
17644 }
17645 "#
17646 .unindent();
17647
17648 // Edits are modified, removed, modified, added
17649 cx.set_state(
17650 &r#"
17651 use some::modified;
17652
17653 ˇ
17654 fn main() {
17655 println!("hello there");
17656
17657 println!("around the");
17658 println!("world");
17659 }
17660 "#
17661 .unindent(),
17662 );
17663
17664 cx.set_head_text(&diff_base);
17665 executor.run_until_parked();
17666
17667 cx.update_editor(|editor, window, cx| {
17668 //Wrap around the bottom of the buffer
17669 for _ in 0..3 {
17670 editor.go_to_next_hunk(&GoToHunk, window, cx);
17671 }
17672 });
17673
17674 cx.assert_editor_state(
17675 &r#"
17676 ˇuse some::modified;
17677
17678
17679 fn main() {
17680 println!("hello there");
17681
17682 println!("around the");
17683 println!("world");
17684 }
17685 "#
17686 .unindent(),
17687 );
17688
17689 cx.update_editor(|editor, window, cx| {
17690 //Wrap around the top of the buffer
17691 for _ in 0..2 {
17692 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17693 }
17694 });
17695
17696 cx.assert_editor_state(
17697 &r#"
17698 use some::modified;
17699
17700
17701 fn main() {
17702 ˇ println!("hello there");
17703
17704 println!("around the");
17705 println!("world");
17706 }
17707 "#
17708 .unindent(),
17709 );
17710
17711 cx.update_editor(|editor, window, cx| {
17712 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17713 });
17714
17715 cx.assert_editor_state(
17716 &r#"
17717 use some::modified;
17718
17719 ˇ
17720 fn main() {
17721 println!("hello there");
17722
17723 println!("around the");
17724 println!("world");
17725 }
17726 "#
17727 .unindent(),
17728 );
17729
17730 cx.update_editor(|editor, window, cx| {
17731 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17732 });
17733
17734 cx.assert_editor_state(
17735 &r#"
17736 ˇuse some::modified;
17737
17738
17739 fn main() {
17740 println!("hello there");
17741
17742 println!("around the");
17743 println!("world");
17744 }
17745 "#
17746 .unindent(),
17747 );
17748
17749 cx.update_editor(|editor, window, cx| {
17750 for _ in 0..2 {
17751 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17752 }
17753 });
17754
17755 cx.assert_editor_state(
17756 &r#"
17757 use some::modified;
17758
17759
17760 fn main() {
17761 ˇ println!("hello there");
17762
17763 println!("around the");
17764 println!("world");
17765 }
17766 "#
17767 .unindent(),
17768 );
17769
17770 cx.update_editor(|editor, window, cx| {
17771 editor.fold(&Fold, window, cx);
17772 });
17773
17774 cx.update_editor(|editor, window, cx| {
17775 editor.go_to_next_hunk(&GoToHunk, window, cx);
17776 });
17777
17778 cx.assert_editor_state(
17779 &r#"
17780 ˇuse some::modified;
17781
17782
17783 fn main() {
17784 println!("hello there");
17785
17786 println!("around the");
17787 println!("world");
17788 }
17789 "#
17790 .unindent(),
17791 );
17792}
17793
17794#[test]
17795fn test_split_words() {
17796 fn split(text: &str) -> Vec<&str> {
17797 split_words(text).collect()
17798 }
17799
17800 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17801 assert_eq!(split("hello_world"), &["hello_", "world"]);
17802 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17803 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17804 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17805 assert_eq!(split("helloworld"), &["helloworld"]);
17806
17807 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17808}
17809
17810#[test]
17811fn test_split_words_for_snippet_prefix() {
17812 fn split(text: &str) -> Vec<&str> {
17813 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17814 }
17815
17816 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17817 assert_eq!(split("hello_world"), &["hello_world"]);
17818 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17819 assert_eq!(split("Hello_World"), &["Hello_World"]);
17820 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17821 assert_eq!(split("helloworld"), &["helloworld"]);
17822 assert_eq!(
17823 split("this@is!@#$^many . symbols"),
17824 &[
17825 "symbols",
17826 " symbols",
17827 ". symbols",
17828 " . symbols",
17829 " . symbols",
17830 " . symbols",
17831 "many . symbols",
17832 "^many . symbols",
17833 "$^many . symbols",
17834 "#$^many . symbols",
17835 "@#$^many . symbols",
17836 "!@#$^many . symbols",
17837 "is!@#$^many . symbols",
17838 "@is!@#$^many . symbols",
17839 "this@is!@#$^many . symbols",
17840 ],
17841 );
17842 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17843}
17844
17845#[gpui::test]
17846async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17847 init_test(cx, |_| {});
17848
17849 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17850
17851 #[track_caller]
17852 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17853 let _state_context = cx.set_state(before);
17854 cx.run_until_parked();
17855 cx.update_editor(|editor, window, cx| {
17856 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17857 });
17858 cx.run_until_parked();
17859 cx.assert_editor_state(after);
17860 }
17861
17862 // Outside bracket jumps to outside of matching bracket
17863 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17864 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17865
17866 // Inside bracket jumps to inside of matching bracket
17867 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17868 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17869
17870 // When outside a bracket and inside, favor jumping to the inside bracket
17871 assert(
17872 "console.log('foo', [1, 2, 3]ˇ);",
17873 "console.log('foo', ˇ[1, 2, 3]);",
17874 &mut cx,
17875 );
17876 assert(
17877 "console.log(ˇ'foo', [1, 2, 3]);",
17878 "console.log('foo'ˇ, [1, 2, 3]);",
17879 &mut cx,
17880 );
17881
17882 // Bias forward if two options are equally likely
17883 assert(
17884 "let result = curried_fun()ˇ();",
17885 "let result = curried_fun()()ˇ;",
17886 &mut cx,
17887 );
17888
17889 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17890 assert(
17891 indoc! {"
17892 function test() {
17893 console.log('test')ˇ
17894 }"},
17895 indoc! {"
17896 function test() {
17897 console.logˇ('test')
17898 }"},
17899 &mut cx,
17900 );
17901}
17902
17903#[gpui::test]
17904async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
17905 init_test(cx, |_| {});
17906 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
17907 language_registry.add(markdown_lang());
17908 language_registry.add(rust_lang());
17909 let buffer = cx.new(|cx| {
17910 let mut buffer = language::Buffer::local(
17911 indoc! {"
17912 ```rs
17913 impl Worktree {
17914 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17915 }
17916 }
17917 ```
17918 "},
17919 cx,
17920 );
17921 buffer.set_language_registry(language_registry.clone());
17922 buffer.set_language(Some(markdown_lang()), cx);
17923 buffer
17924 });
17925 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17926 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17927 cx.executor().run_until_parked();
17928 _ = editor.update(cx, |editor, window, cx| {
17929 // Case 1: Test outer enclosing brackets
17930 select_ranges(
17931 editor,
17932 &indoc! {"
17933 ```rs
17934 impl Worktree {
17935 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17936 }
17937 }ˇ
17938 ```
17939 "},
17940 window,
17941 cx,
17942 );
17943 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17944 assert_text_with_selections(
17945 editor,
17946 &indoc! {"
17947 ```rs
17948 impl Worktree ˇ{
17949 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17950 }
17951 }
17952 ```
17953 "},
17954 cx,
17955 );
17956 // Case 2: Test inner enclosing brackets
17957 select_ranges(
17958 editor,
17959 &indoc! {"
17960 ```rs
17961 impl Worktree {
17962 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17963 }ˇ
17964 }
17965 ```
17966 "},
17967 window,
17968 cx,
17969 );
17970 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17971 assert_text_with_selections(
17972 editor,
17973 &indoc! {"
17974 ```rs
17975 impl Worktree {
17976 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
17977 }
17978 }
17979 ```
17980 "},
17981 cx,
17982 );
17983 });
17984}
17985
17986#[gpui::test]
17987async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17988 init_test(cx, |_| {});
17989
17990 let fs = FakeFs::new(cx.executor());
17991 fs.insert_tree(
17992 path!("/a"),
17993 json!({
17994 "main.rs": "fn main() { let a = 5; }",
17995 "other.rs": "// Test file",
17996 }),
17997 )
17998 .await;
17999 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18000
18001 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18002 language_registry.add(Arc::new(Language::new(
18003 LanguageConfig {
18004 name: "Rust".into(),
18005 matcher: LanguageMatcher {
18006 path_suffixes: vec!["rs".to_string()],
18007 ..Default::default()
18008 },
18009 brackets: BracketPairConfig {
18010 pairs: vec![BracketPair {
18011 start: "{".to_string(),
18012 end: "}".to_string(),
18013 close: true,
18014 surround: true,
18015 newline: true,
18016 }],
18017 disabled_scopes_by_bracket_ix: Vec::new(),
18018 },
18019 ..Default::default()
18020 },
18021 Some(tree_sitter_rust::LANGUAGE.into()),
18022 )));
18023 let mut fake_servers = language_registry.register_fake_lsp(
18024 "Rust",
18025 FakeLspAdapter {
18026 capabilities: lsp::ServerCapabilities {
18027 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18028 first_trigger_character: "{".to_string(),
18029 more_trigger_character: None,
18030 }),
18031 ..Default::default()
18032 },
18033 ..Default::default()
18034 },
18035 );
18036
18037 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18038
18039 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18040
18041 let worktree_id = workspace
18042 .update(cx, |workspace, _, cx| {
18043 workspace.project().update(cx, |project, cx| {
18044 project.worktrees(cx).next().unwrap().read(cx).id()
18045 })
18046 })
18047 .unwrap();
18048
18049 let buffer = project
18050 .update(cx, |project, cx| {
18051 project.open_local_buffer(path!("/a/main.rs"), cx)
18052 })
18053 .await
18054 .unwrap();
18055 let editor_handle = workspace
18056 .update(cx, |workspace, window, cx| {
18057 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18058 })
18059 .unwrap()
18060 .await
18061 .unwrap()
18062 .downcast::<Editor>()
18063 .unwrap();
18064
18065 cx.executor().start_waiting();
18066 let fake_server = fake_servers.next().await.unwrap();
18067
18068 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18069 |params, _| async move {
18070 assert_eq!(
18071 params.text_document_position.text_document.uri,
18072 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18073 );
18074 assert_eq!(
18075 params.text_document_position.position,
18076 lsp::Position::new(0, 21),
18077 );
18078
18079 Ok(Some(vec![lsp::TextEdit {
18080 new_text: "]".to_string(),
18081 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18082 }]))
18083 },
18084 );
18085
18086 editor_handle.update_in(cx, |editor, window, cx| {
18087 window.focus(&editor.focus_handle(cx));
18088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18089 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18090 });
18091 editor.handle_input("{", window, cx);
18092 });
18093
18094 cx.executor().run_until_parked();
18095
18096 buffer.update(cx, |buffer, _| {
18097 assert_eq!(
18098 buffer.text(),
18099 "fn main() { let a = {5}; }",
18100 "No extra braces from on type formatting should appear in the buffer"
18101 )
18102 });
18103}
18104
18105#[gpui::test(iterations = 20, seeds(31))]
18106async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18107 init_test(cx, |_| {});
18108
18109 let mut cx = EditorLspTestContext::new_rust(
18110 lsp::ServerCapabilities {
18111 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18112 first_trigger_character: ".".to_string(),
18113 more_trigger_character: None,
18114 }),
18115 ..Default::default()
18116 },
18117 cx,
18118 )
18119 .await;
18120
18121 cx.update_buffer(|buffer, _| {
18122 // This causes autoindent to be async.
18123 buffer.set_sync_parse_timeout(Duration::ZERO)
18124 });
18125
18126 cx.set_state("fn c() {\n d()ˇ\n}\n");
18127 cx.simulate_keystroke("\n");
18128 cx.run_until_parked();
18129
18130 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18131 let mut request =
18132 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18133 let buffer_cloned = buffer_cloned.clone();
18134 async move {
18135 buffer_cloned.update(&mut cx, |buffer, _| {
18136 assert_eq!(
18137 buffer.text(),
18138 "fn c() {\n d()\n .\n}\n",
18139 "OnTypeFormatting should triggered after autoindent applied"
18140 )
18141 })?;
18142
18143 Ok(Some(vec![]))
18144 }
18145 });
18146
18147 cx.simulate_keystroke(".");
18148 cx.run_until_parked();
18149
18150 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18151 assert!(request.next().await.is_some());
18152 request.close();
18153 assert!(request.next().await.is_none());
18154}
18155
18156#[gpui::test]
18157async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18158 init_test(cx, |_| {});
18159
18160 let fs = FakeFs::new(cx.executor());
18161 fs.insert_tree(
18162 path!("/a"),
18163 json!({
18164 "main.rs": "fn main() { let a = 5; }",
18165 "other.rs": "// Test file",
18166 }),
18167 )
18168 .await;
18169
18170 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18171
18172 let server_restarts = Arc::new(AtomicUsize::new(0));
18173 let closure_restarts = Arc::clone(&server_restarts);
18174 let language_server_name = "test language server";
18175 let language_name: LanguageName = "Rust".into();
18176
18177 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18178 language_registry.add(Arc::new(Language::new(
18179 LanguageConfig {
18180 name: language_name.clone(),
18181 matcher: LanguageMatcher {
18182 path_suffixes: vec!["rs".to_string()],
18183 ..Default::default()
18184 },
18185 ..Default::default()
18186 },
18187 Some(tree_sitter_rust::LANGUAGE.into()),
18188 )));
18189 let mut fake_servers = language_registry.register_fake_lsp(
18190 "Rust",
18191 FakeLspAdapter {
18192 name: language_server_name,
18193 initialization_options: Some(json!({
18194 "testOptionValue": true
18195 })),
18196 initializer: Some(Box::new(move |fake_server| {
18197 let task_restarts = Arc::clone(&closure_restarts);
18198 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18199 task_restarts.fetch_add(1, atomic::Ordering::Release);
18200 futures::future::ready(Ok(()))
18201 });
18202 })),
18203 ..Default::default()
18204 },
18205 );
18206
18207 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18208 let _buffer = project
18209 .update(cx, |project, cx| {
18210 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18211 })
18212 .await
18213 .unwrap();
18214 let _fake_server = fake_servers.next().await.unwrap();
18215 update_test_language_settings(cx, |language_settings| {
18216 language_settings.languages.0.insert(
18217 language_name.clone().0,
18218 LanguageSettingsContent {
18219 tab_size: NonZeroU32::new(8),
18220 ..Default::default()
18221 },
18222 );
18223 });
18224 cx.executor().run_until_parked();
18225 assert_eq!(
18226 server_restarts.load(atomic::Ordering::Acquire),
18227 0,
18228 "Should not restart LSP server on an unrelated change"
18229 );
18230
18231 update_test_project_settings(cx, |project_settings| {
18232 project_settings.lsp.insert(
18233 "Some other server name".into(),
18234 LspSettings {
18235 binary: None,
18236 settings: None,
18237 initialization_options: Some(json!({
18238 "some other init value": false
18239 })),
18240 enable_lsp_tasks: false,
18241 fetch: None,
18242 },
18243 );
18244 });
18245 cx.executor().run_until_parked();
18246 assert_eq!(
18247 server_restarts.load(atomic::Ordering::Acquire),
18248 0,
18249 "Should not restart LSP server on an unrelated LSP settings change"
18250 );
18251
18252 update_test_project_settings(cx, |project_settings| {
18253 project_settings.lsp.insert(
18254 language_server_name.into(),
18255 LspSettings {
18256 binary: None,
18257 settings: None,
18258 initialization_options: Some(json!({
18259 "anotherInitValue": false
18260 })),
18261 enable_lsp_tasks: false,
18262 fetch: None,
18263 },
18264 );
18265 });
18266 cx.executor().run_until_parked();
18267 assert_eq!(
18268 server_restarts.load(atomic::Ordering::Acquire),
18269 1,
18270 "Should restart LSP server on a related LSP settings change"
18271 );
18272
18273 update_test_project_settings(cx, |project_settings| {
18274 project_settings.lsp.insert(
18275 language_server_name.into(),
18276 LspSettings {
18277 binary: None,
18278 settings: None,
18279 initialization_options: Some(json!({
18280 "anotherInitValue": false
18281 })),
18282 enable_lsp_tasks: false,
18283 fetch: None,
18284 },
18285 );
18286 });
18287 cx.executor().run_until_parked();
18288 assert_eq!(
18289 server_restarts.load(atomic::Ordering::Acquire),
18290 1,
18291 "Should not restart LSP server on a related LSP settings change that is the same"
18292 );
18293
18294 update_test_project_settings(cx, |project_settings| {
18295 project_settings.lsp.insert(
18296 language_server_name.into(),
18297 LspSettings {
18298 binary: None,
18299 settings: None,
18300 initialization_options: None,
18301 enable_lsp_tasks: false,
18302 fetch: None,
18303 },
18304 );
18305 });
18306 cx.executor().run_until_parked();
18307 assert_eq!(
18308 server_restarts.load(atomic::Ordering::Acquire),
18309 2,
18310 "Should restart LSP server on another related LSP settings change"
18311 );
18312}
18313
18314#[gpui::test]
18315async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18316 init_test(cx, |_| {});
18317
18318 let mut cx = EditorLspTestContext::new_rust(
18319 lsp::ServerCapabilities {
18320 completion_provider: Some(lsp::CompletionOptions {
18321 trigger_characters: Some(vec![".".to_string()]),
18322 resolve_provider: Some(true),
18323 ..Default::default()
18324 }),
18325 ..Default::default()
18326 },
18327 cx,
18328 )
18329 .await;
18330
18331 cx.set_state("fn main() { let a = 2ˇ; }");
18332 cx.simulate_keystroke(".");
18333 let completion_item = lsp::CompletionItem {
18334 label: "some".into(),
18335 kind: Some(lsp::CompletionItemKind::SNIPPET),
18336 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18337 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18338 kind: lsp::MarkupKind::Markdown,
18339 value: "```rust\nSome(2)\n```".to_string(),
18340 })),
18341 deprecated: Some(false),
18342 sort_text: Some("fffffff2".to_string()),
18343 filter_text: Some("some".to_string()),
18344 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18345 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18346 range: lsp::Range {
18347 start: lsp::Position {
18348 line: 0,
18349 character: 22,
18350 },
18351 end: lsp::Position {
18352 line: 0,
18353 character: 22,
18354 },
18355 },
18356 new_text: "Some(2)".to_string(),
18357 })),
18358 additional_text_edits: Some(vec![lsp::TextEdit {
18359 range: lsp::Range {
18360 start: lsp::Position {
18361 line: 0,
18362 character: 20,
18363 },
18364 end: lsp::Position {
18365 line: 0,
18366 character: 22,
18367 },
18368 },
18369 new_text: "".to_string(),
18370 }]),
18371 ..Default::default()
18372 };
18373
18374 let closure_completion_item = completion_item.clone();
18375 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18376 let task_completion_item = closure_completion_item.clone();
18377 async move {
18378 Ok(Some(lsp::CompletionResponse::Array(vec![
18379 task_completion_item,
18380 ])))
18381 }
18382 });
18383
18384 request.next().await;
18385
18386 cx.condition(|editor, _| editor.context_menu_visible())
18387 .await;
18388 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18389 editor
18390 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18391 .unwrap()
18392 });
18393 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18394
18395 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18396 let task_completion_item = completion_item.clone();
18397 async move { Ok(task_completion_item) }
18398 })
18399 .next()
18400 .await
18401 .unwrap();
18402 apply_additional_edits.await.unwrap();
18403 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18404}
18405
18406#[gpui::test]
18407async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18408 init_test(cx, |_| {});
18409
18410 let mut cx = EditorLspTestContext::new_rust(
18411 lsp::ServerCapabilities {
18412 completion_provider: Some(lsp::CompletionOptions {
18413 trigger_characters: Some(vec![".".to_string()]),
18414 resolve_provider: Some(true),
18415 ..Default::default()
18416 }),
18417 ..Default::default()
18418 },
18419 cx,
18420 )
18421 .await;
18422
18423 cx.set_state("fn main() { let a = 2ˇ; }");
18424 cx.simulate_keystroke(".");
18425
18426 let item1 = lsp::CompletionItem {
18427 label: "method id()".to_string(),
18428 filter_text: Some("id".to_string()),
18429 detail: None,
18430 documentation: None,
18431 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18432 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18433 new_text: ".id".to_string(),
18434 })),
18435 ..lsp::CompletionItem::default()
18436 };
18437
18438 let item2 = lsp::CompletionItem {
18439 label: "other".to_string(),
18440 filter_text: Some("other".to_string()),
18441 detail: None,
18442 documentation: None,
18443 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18444 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18445 new_text: ".other".to_string(),
18446 })),
18447 ..lsp::CompletionItem::default()
18448 };
18449
18450 let item1 = item1.clone();
18451 cx.set_request_handler::<lsp::request::Completion, _, _>({
18452 let item1 = item1.clone();
18453 move |_, _, _| {
18454 let item1 = item1.clone();
18455 let item2 = item2.clone();
18456 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18457 }
18458 })
18459 .next()
18460 .await;
18461
18462 cx.condition(|editor, _| editor.context_menu_visible())
18463 .await;
18464 cx.update_editor(|editor, _, _| {
18465 let context_menu = editor.context_menu.borrow_mut();
18466 let context_menu = context_menu
18467 .as_ref()
18468 .expect("Should have the context menu deployed");
18469 match context_menu {
18470 CodeContextMenu::Completions(completions_menu) => {
18471 let completions = completions_menu.completions.borrow_mut();
18472 assert_eq!(
18473 completions
18474 .iter()
18475 .map(|completion| &completion.label.text)
18476 .collect::<Vec<_>>(),
18477 vec!["method id()", "other"]
18478 )
18479 }
18480 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18481 }
18482 });
18483
18484 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18485 let item1 = item1.clone();
18486 move |_, item_to_resolve, _| {
18487 let item1 = item1.clone();
18488 async move {
18489 if item1 == item_to_resolve {
18490 Ok(lsp::CompletionItem {
18491 label: "method id()".to_string(),
18492 filter_text: Some("id".to_string()),
18493 detail: Some("Now resolved!".to_string()),
18494 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18495 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18496 range: lsp::Range::new(
18497 lsp::Position::new(0, 22),
18498 lsp::Position::new(0, 22),
18499 ),
18500 new_text: ".id".to_string(),
18501 })),
18502 ..lsp::CompletionItem::default()
18503 })
18504 } else {
18505 Ok(item_to_resolve)
18506 }
18507 }
18508 }
18509 })
18510 .next()
18511 .await
18512 .unwrap();
18513 cx.run_until_parked();
18514
18515 cx.update_editor(|editor, window, cx| {
18516 editor.context_menu_next(&Default::default(), window, cx);
18517 });
18518
18519 cx.update_editor(|editor, _, _| {
18520 let context_menu = editor.context_menu.borrow_mut();
18521 let context_menu = context_menu
18522 .as_ref()
18523 .expect("Should have the context menu deployed");
18524 match context_menu {
18525 CodeContextMenu::Completions(completions_menu) => {
18526 let completions = completions_menu.completions.borrow_mut();
18527 assert_eq!(
18528 completions
18529 .iter()
18530 .map(|completion| &completion.label.text)
18531 .collect::<Vec<_>>(),
18532 vec!["method id() Now resolved!", "other"],
18533 "Should update first completion label, but not second as the filter text did not match."
18534 );
18535 }
18536 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18537 }
18538 });
18539}
18540
18541#[gpui::test]
18542async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18543 init_test(cx, |_| {});
18544 let mut cx = EditorLspTestContext::new_rust(
18545 lsp::ServerCapabilities {
18546 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18547 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18548 completion_provider: Some(lsp::CompletionOptions {
18549 resolve_provider: Some(true),
18550 ..Default::default()
18551 }),
18552 ..Default::default()
18553 },
18554 cx,
18555 )
18556 .await;
18557 cx.set_state(indoc! {"
18558 struct TestStruct {
18559 field: i32
18560 }
18561
18562 fn mainˇ() {
18563 let unused_var = 42;
18564 let test_struct = TestStruct { field: 42 };
18565 }
18566 "});
18567 let symbol_range = cx.lsp_range(indoc! {"
18568 struct TestStruct {
18569 field: i32
18570 }
18571
18572 «fn main»() {
18573 let unused_var = 42;
18574 let test_struct = TestStruct { field: 42 };
18575 }
18576 "});
18577 let mut hover_requests =
18578 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18579 Ok(Some(lsp::Hover {
18580 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18581 kind: lsp::MarkupKind::Markdown,
18582 value: "Function documentation".to_string(),
18583 }),
18584 range: Some(symbol_range),
18585 }))
18586 });
18587
18588 // Case 1: Test that code action menu hide hover popover
18589 cx.dispatch_action(Hover);
18590 hover_requests.next().await;
18591 cx.condition(|editor, _| editor.hover_state.visible()).await;
18592 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18593 move |_, _, _| async move {
18594 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18595 lsp::CodeAction {
18596 title: "Remove unused variable".to_string(),
18597 kind: Some(CodeActionKind::QUICKFIX),
18598 edit: Some(lsp::WorkspaceEdit {
18599 changes: Some(
18600 [(
18601 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18602 vec![lsp::TextEdit {
18603 range: lsp::Range::new(
18604 lsp::Position::new(5, 4),
18605 lsp::Position::new(5, 27),
18606 ),
18607 new_text: "".to_string(),
18608 }],
18609 )]
18610 .into_iter()
18611 .collect(),
18612 ),
18613 ..Default::default()
18614 }),
18615 ..Default::default()
18616 },
18617 )]))
18618 },
18619 );
18620 cx.update_editor(|editor, window, cx| {
18621 editor.toggle_code_actions(
18622 &ToggleCodeActions {
18623 deployed_from: None,
18624 quick_launch: false,
18625 },
18626 window,
18627 cx,
18628 );
18629 });
18630 code_action_requests.next().await;
18631 cx.run_until_parked();
18632 cx.condition(|editor, _| editor.context_menu_visible())
18633 .await;
18634 cx.update_editor(|editor, _, _| {
18635 assert!(
18636 !editor.hover_state.visible(),
18637 "Hover popover should be hidden when code action menu is shown"
18638 );
18639 // Hide code actions
18640 editor.context_menu.take();
18641 });
18642
18643 // Case 2: Test that code completions hide hover popover
18644 cx.dispatch_action(Hover);
18645 hover_requests.next().await;
18646 cx.condition(|editor, _| editor.hover_state.visible()).await;
18647 let counter = Arc::new(AtomicUsize::new(0));
18648 let mut completion_requests =
18649 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18650 let counter = counter.clone();
18651 async move {
18652 counter.fetch_add(1, atomic::Ordering::Release);
18653 Ok(Some(lsp::CompletionResponse::Array(vec![
18654 lsp::CompletionItem {
18655 label: "main".into(),
18656 kind: Some(lsp::CompletionItemKind::FUNCTION),
18657 detail: Some("() -> ()".to_string()),
18658 ..Default::default()
18659 },
18660 lsp::CompletionItem {
18661 label: "TestStruct".into(),
18662 kind: Some(lsp::CompletionItemKind::STRUCT),
18663 detail: Some("struct TestStruct".to_string()),
18664 ..Default::default()
18665 },
18666 ])))
18667 }
18668 });
18669 cx.update_editor(|editor, window, cx| {
18670 editor.show_completions(&ShowCompletions, window, cx);
18671 });
18672 completion_requests.next().await;
18673 cx.condition(|editor, _| editor.context_menu_visible())
18674 .await;
18675 cx.update_editor(|editor, _, _| {
18676 assert!(
18677 !editor.hover_state.visible(),
18678 "Hover popover should be hidden when completion menu is shown"
18679 );
18680 });
18681}
18682
18683#[gpui::test]
18684async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18685 init_test(cx, |_| {});
18686
18687 let mut cx = EditorLspTestContext::new_rust(
18688 lsp::ServerCapabilities {
18689 completion_provider: Some(lsp::CompletionOptions {
18690 trigger_characters: Some(vec![".".to_string()]),
18691 resolve_provider: Some(true),
18692 ..Default::default()
18693 }),
18694 ..Default::default()
18695 },
18696 cx,
18697 )
18698 .await;
18699
18700 cx.set_state("fn main() { let a = 2ˇ; }");
18701 cx.simulate_keystroke(".");
18702
18703 let unresolved_item_1 = lsp::CompletionItem {
18704 label: "id".to_string(),
18705 filter_text: Some("id".to_string()),
18706 detail: None,
18707 documentation: None,
18708 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18709 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18710 new_text: ".id".to_string(),
18711 })),
18712 ..lsp::CompletionItem::default()
18713 };
18714 let resolved_item_1 = lsp::CompletionItem {
18715 additional_text_edits: Some(vec![lsp::TextEdit {
18716 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18717 new_text: "!!".to_string(),
18718 }]),
18719 ..unresolved_item_1.clone()
18720 };
18721 let unresolved_item_2 = lsp::CompletionItem {
18722 label: "other".to_string(),
18723 filter_text: Some("other".to_string()),
18724 detail: None,
18725 documentation: None,
18726 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18727 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18728 new_text: ".other".to_string(),
18729 })),
18730 ..lsp::CompletionItem::default()
18731 };
18732 let resolved_item_2 = lsp::CompletionItem {
18733 additional_text_edits: Some(vec![lsp::TextEdit {
18734 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18735 new_text: "??".to_string(),
18736 }]),
18737 ..unresolved_item_2.clone()
18738 };
18739
18740 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18741 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18742 cx.lsp
18743 .server
18744 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18745 let unresolved_item_1 = unresolved_item_1.clone();
18746 let resolved_item_1 = resolved_item_1.clone();
18747 let unresolved_item_2 = unresolved_item_2.clone();
18748 let resolved_item_2 = resolved_item_2.clone();
18749 let resolve_requests_1 = resolve_requests_1.clone();
18750 let resolve_requests_2 = resolve_requests_2.clone();
18751 move |unresolved_request, _| {
18752 let unresolved_item_1 = unresolved_item_1.clone();
18753 let resolved_item_1 = resolved_item_1.clone();
18754 let unresolved_item_2 = unresolved_item_2.clone();
18755 let resolved_item_2 = resolved_item_2.clone();
18756 let resolve_requests_1 = resolve_requests_1.clone();
18757 let resolve_requests_2 = resolve_requests_2.clone();
18758 async move {
18759 if unresolved_request == unresolved_item_1 {
18760 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18761 Ok(resolved_item_1.clone())
18762 } else if unresolved_request == unresolved_item_2 {
18763 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18764 Ok(resolved_item_2.clone())
18765 } else {
18766 panic!("Unexpected completion item {unresolved_request:?}")
18767 }
18768 }
18769 }
18770 })
18771 .detach();
18772
18773 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18774 let unresolved_item_1 = unresolved_item_1.clone();
18775 let unresolved_item_2 = unresolved_item_2.clone();
18776 async move {
18777 Ok(Some(lsp::CompletionResponse::Array(vec![
18778 unresolved_item_1,
18779 unresolved_item_2,
18780 ])))
18781 }
18782 })
18783 .next()
18784 .await;
18785
18786 cx.condition(|editor, _| editor.context_menu_visible())
18787 .await;
18788 cx.update_editor(|editor, _, _| {
18789 let context_menu = editor.context_menu.borrow_mut();
18790 let context_menu = context_menu
18791 .as_ref()
18792 .expect("Should have the context menu deployed");
18793 match context_menu {
18794 CodeContextMenu::Completions(completions_menu) => {
18795 let completions = completions_menu.completions.borrow_mut();
18796 assert_eq!(
18797 completions
18798 .iter()
18799 .map(|completion| &completion.label.text)
18800 .collect::<Vec<_>>(),
18801 vec!["id", "other"]
18802 )
18803 }
18804 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18805 }
18806 });
18807 cx.run_until_parked();
18808
18809 cx.update_editor(|editor, window, cx| {
18810 editor.context_menu_next(&ContextMenuNext, window, cx);
18811 });
18812 cx.run_until_parked();
18813 cx.update_editor(|editor, window, cx| {
18814 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18815 });
18816 cx.run_until_parked();
18817 cx.update_editor(|editor, window, cx| {
18818 editor.context_menu_next(&ContextMenuNext, window, cx);
18819 });
18820 cx.run_until_parked();
18821 cx.update_editor(|editor, window, cx| {
18822 editor
18823 .compose_completion(&ComposeCompletion::default(), window, cx)
18824 .expect("No task returned")
18825 })
18826 .await
18827 .expect("Completion failed");
18828 cx.run_until_parked();
18829
18830 cx.update_editor(|editor, _, cx| {
18831 assert_eq!(
18832 resolve_requests_1.load(atomic::Ordering::Acquire),
18833 1,
18834 "Should always resolve once despite multiple selections"
18835 );
18836 assert_eq!(
18837 resolve_requests_2.load(atomic::Ordering::Acquire),
18838 1,
18839 "Should always resolve once after multiple selections and applying the completion"
18840 );
18841 assert_eq!(
18842 editor.text(cx),
18843 "fn main() { let a = ??.other; }",
18844 "Should use resolved data when applying the completion"
18845 );
18846 });
18847}
18848
18849#[gpui::test]
18850async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18851 init_test(cx, |_| {});
18852
18853 let item_0 = lsp::CompletionItem {
18854 label: "abs".into(),
18855 insert_text: Some("abs".into()),
18856 data: Some(json!({ "very": "special"})),
18857 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18858 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18859 lsp::InsertReplaceEdit {
18860 new_text: "abs".to_string(),
18861 insert: lsp::Range::default(),
18862 replace: lsp::Range::default(),
18863 },
18864 )),
18865 ..lsp::CompletionItem::default()
18866 };
18867 let items = iter::once(item_0.clone())
18868 .chain((11..51).map(|i| lsp::CompletionItem {
18869 label: format!("item_{}", i),
18870 insert_text: Some(format!("item_{}", i)),
18871 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18872 ..lsp::CompletionItem::default()
18873 }))
18874 .collect::<Vec<_>>();
18875
18876 let default_commit_characters = vec!["?".to_string()];
18877 let default_data = json!({ "default": "data"});
18878 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18879 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18880 let default_edit_range = lsp::Range {
18881 start: lsp::Position {
18882 line: 0,
18883 character: 5,
18884 },
18885 end: lsp::Position {
18886 line: 0,
18887 character: 5,
18888 },
18889 };
18890
18891 let mut cx = EditorLspTestContext::new_rust(
18892 lsp::ServerCapabilities {
18893 completion_provider: Some(lsp::CompletionOptions {
18894 trigger_characters: Some(vec![".".to_string()]),
18895 resolve_provider: Some(true),
18896 ..Default::default()
18897 }),
18898 ..Default::default()
18899 },
18900 cx,
18901 )
18902 .await;
18903
18904 cx.set_state("fn main() { let a = 2ˇ; }");
18905 cx.simulate_keystroke(".");
18906
18907 let completion_data = default_data.clone();
18908 let completion_characters = default_commit_characters.clone();
18909 let completion_items = items.clone();
18910 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18911 let default_data = completion_data.clone();
18912 let default_commit_characters = completion_characters.clone();
18913 let items = completion_items.clone();
18914 async move {
18915 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18916 items,
18917 item_defaults: Some(lsp::CompletionListItemDefaults {
18918 data: Some(default_data.clone()),
18919 commit_characters: Some(default_commit_characters.clone()),
18920 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18921 default_edit_range,
18922 )),
18923 insert_text_format: Some(default_insert_text_format),
18924 insert_text_mode: Some(default_insert_text_mode),
18925 }),
18926 ..lsp::CompletionList::default()
18927 })))
18928 }
18929 })
18930 .next()
18931 .await;
18932
18933 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18934 cx.lsp
18935 .server
18936 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18937 let closure_resolved_items = resolved_items.clone();
18938 move |item_to_resolve, _| {
18939 let closure_resolved_items = closure_resolved_items.clone();
18940 async move {
18941 closure_resolved_items.lock().push(item_to_resolve.clone());
18942 Ok(item_to_resolve)
18943 }
18944 }
18945 })
18946 .detach();
18947
18948 cx.condition(|editor, _| editor.context_menu_visible())
18949 .await;
18950 cx.run_until_parked();
18951 cx.update_editor(|editor, _, _| {
18952 let menu = editor.context_menu.borrow_mut();
18953 match menu.as_ref().expect("should have the completions menu") {
18954 CodeContextMenu::Completions(completions_menu) => {
18955 assert_eq!(
18956 completions_menu
18957 .entries
18958 .borrow()
18959 .iter()
18960 .map(|mat| mat.string.clone())
18961 .collect::<Vec<String>>(),
18962 items
18963 .iter()
18964 .map(|completion| completion.label.clone())
18965 .collect::<Vec<String>>()
18966 );
18967 }
18968 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18969 }
18970 });
18971 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18972 // with 4 from the end.
18973 assert_eq!(
18974 *resolved_items.lock(),
18975 [&items[0..16], &items[items.len() - 4..items.len()]]
18976 .concat()
18977 .iter()
18978 .cloned()
18979 .map(|mut item| {
18980 if item.data.is_none() {
18981 item.data = Some(default_data.clone());
18982 }
18983 item
18984 })
18985 .collect::<Vec<lsp::CompletionItem>>(),
18986 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18987 );
18988 resolved_items.lock().clear();
18989
18990 cx.update_editor(|editor, window, cx| {
18991 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18992 });
18993 cx.run_until_parked();
18994 // Completions that have already been resolved are skipped.
18995 assert_eq!(
18996 *resolved_items.lock(),
18997 items[items.len() - 17..items.len() - 4]
18998 .iter()
18999 .cloned()
19000 .map(|mut item| {
19001 if item.data.is_none() {
19002 item.data = Some(default_data.clone());
19003 }
19004 item
19005 })
19006 .collect::<Vec<lsp::CompletionItem>>()
19007 );
19008 resolved_items.lock().clear();
19009}
19010
19011#[gpui::test]
19012async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19013 init_test(cx, |_| {});
19014
19015 let mut cx = EditorLspTestContext::new(
19016 Language::new(
19017 LanguageConfig {
19018 matcher: LanguageMatcher {
19019 path_suffixes: vec!["jsx".into()],
19020 ..Default::default()
19021 },
19022 overrides: [(
19023 "element".into(),
19024 LanguageConfigOverride {
19025 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19026 ..Default::default()
19027 },
19028 )]
19029 .into_iter()
19030 .collect(),
19031 ..Default::default()
19032 },
19033 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19034 )
19035 .with_override_query("(jsx_self_closing_element) @element")
19036 .unwrap(),
19037 lsp::ServerCapabilities {
19038 completion_provider: Some(lsp::CompletionOptions {
19039 trigger_characters: Some(vec![":".to_string()]),
19040 ..Default::default()
19041 }),
19042 ..Default::default()
19043 },
19044 cx,
19045 )
19046 .await;
19047
19048 cx.lsp
19049 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19050 Ok(Some(lsp::CompletionResponse::Array(vec![
19051 lsp::CompletionItem {
19052 label: "bg-blue".into(),
19053 ..Default::default()
19054 },
19055 lsp::CompletionItem {
19056 label: "bg-red".into(),
19057 ..Default::default()
19058 },
19059 lsp::CompletionItem {
19060 label: "bg-yellow".into(),
19061 ..Default::default()
19062 },
19063 ])))
19064 });
19065
19066 cx.set_state(r#"<p class="bgˇ" />"#);
19067
19068 // Trigger completion when typing a dash, because the dash is an extra
19069 // word character in the 'element' scope, which contains the cursor.
19070 cx.simulate_keystroke("-");
19071 cx.executor().run_until_parked();
19072 cx.update_editor(|editor, _, _| {
19073 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19074 {
19075 assert_eq!(
19076 completion_menu_entries(menu),
19077 &["bg-blue", "bg-red", "bg-yellow"]
19078 );
19079 } else {
19080 panic!("expected completion menu to be open");
19081 }
19082 });
19083
19084 cx.simulate_keystroke("l");
19085 cx.executor().run_until_parked();
19086 cx.update_editor(|editor, _, _| {
19087 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19088 {
19089 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19090 } else {
19091 panic!("expected completion menu to be open");
19092 }
19093 });
19094
19095 // When filtering completions, consider the character after the '-' to
19096 // be the start of a subword.
19097 cx.set_state(r#"<p class="yelˇ" />"#);
19098 cx.simulate_keystroke("l");
19099 cx.executor().run_until_parked();
19100 cx.update_editor(|editor, _, _| {
19101 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19102 {
19103 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19104 } else {
19105 panic!("expected completion menu to be open");
19106 }
19107 });
19108}
19109
19110fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19111 let entries = menu.entries.borrow();
19112 entries.iter().map(|mat| mat.string.clone()).collect()
19113}
19114
19115#[gpui::test]
19116async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19117 init_test(cx, |settings| {
19118 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19119 });
19120
19121 let fs = FakeFs::new(cx.executor());
19122 fs.insert_file(path!("/file.ts"), Default::default()).await;
19123
19124 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19125 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19126
19127 language_registry.add(Arc::new(Language::new(
19128 LanguageConfig {
19129 name: "TypeScript".into(),
19130 matcher: LanguageMatcher {
19131 path_suffixes: vec!["ts".to_string()],
19132 ..Default::default()
19133 },
19134 ..Default::default()
19135 },
19136 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19137 )));
19138 update_test_language_settings(cx, |settings| {
19139 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19140 });
19141
19142 let test_plugin = "test_plugin";
19143 let _ = language_registry.register_fake_lsp(
19144 "TypeScript",
19145 FakeLspAdapter {
19146 prettier_plugins: vec![test_plugin],
19147 ..Default::default()
19148 },
19149 );
19150
19151 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19152 let buffer = project
19153 .update(cx, |project, cx| {
19154 project.open_local_buffer(path!("/file.ts"), cx)
19155 })
19156 .await
19157 .unwrap();
19158
19159 let buffer_text = "one\ntwo\nthree\n";
19160 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19161 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19162 editor.update_in(cx, |editor, window, cx| {
19163 editor.set_text(buffer_text, window, cx)
19164 });
19165
19166 editor
19167 .update_in(cx, |editor, window, cx| {
19168 editor.perform_format(
19169 project.clone(),
19170 FormatTrigger::Manual,
19171 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19172 window,
19173 cx,
19174 )
19175 })
19176 .unwrap()
19177 .await;
19178 assert_eq!(
19179 editor.update(cx, |editor, cx| editor.text(cx)),
19180 buffer_text.to_string() + prettier_format_suffix,
19181 "Test prettier formatting was not applied to the original buffer text",
19182 );
19183
19184 update_test_language_settings(cx, |settings| {
19185 settings.defaults.formatter = Some(FormatterList::default())
19186 });
19187 let format = editor.update_in(cx, |editor, window, cx| {
19188 editor.perform_format(
19189 project.clone(),
19190 FormatTrigger::Manual,
19191 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19192 window,
19193 cx,
19194 )
19195 });
19196 format.await.unwrap();
19197 assert_eq!(
19198 editor.update(cx, |editor, cx| editor.text(cx)),
19199 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19200 "Autoformatting (via test prettier) was not applied to the original buffer text",
19201 );
19202}
19203
19204#[gpui::test]
19205async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19206 init_test(cx, |settings| {
19207 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19208 });
19209
19210 let fs = FakeFs::new(cx.executor());
19211 fs.insert_file(path!("/file.settings"), Default::default())
19212 .await;
19213
19214 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19215 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19216
19217 let ts_lang = Arc::new(Language::new(
19218 LanguageConfig {
19219 name: "TypeScript".into(),
19220 matcher: LanguageMatcher {
19221 path_suffixes: vec!["ts".to_string()],
19222 ..LanguageMatcher::default()
19223 },
19224 prettier_parser_name: Some("typescript".to_string()),
19225 ..LanguageConfig::default()
19226 },
19227 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19228 ));
19229
19230 language_registry.add(ts_lang.clone());
19231
19232 update_test_language_settings(cx, |settings| {
19233 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19234 });
19235
19236 let test_plugin = "test_plugin";
19237 let _ = language_registry.register_fake_lsp(
19238 "TypeScript",
19239 FakeLspAdapter {
19240 prettier_plugins: vec![test_plugin],
19241 ..Default::default()
19242 },
19243 );
19244
19245 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19246 let buffer = project
19247 .update(cx, |project, cx| {
19248 project.open_local_buffer(path!("/file.settings"), cx)
19249 })
19250 .await
19251 .unwrap();
19252
19253 project.update(cx, |project, cx| {
19254 project.set_language_for_buffer(&buffer, ts_lang, cx)
19255 });
19256
19257 let buffer_text = "one\ntwo\nthree\n";
19258 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19259 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19260 editor.update_in(cx, |editor, window, cx| {
19261 editor.set_text(buffer_text, window, cx)
19262 });
19263
19264 editor
19265 .update_in(cx, |editor, window, cx| {
19266 editor.perform_format(
19267 project.clone(),
19268 FormatTrigger::Manual,
19269 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19270 window,
19271 cx,
19272 )
19273 })
19274 .unwrap()
19275 .await;
19276 assert_eq!(
19277 editor.update(cx, |editor, cx| editor.text(cx)),
19278 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19279 "Test prettier formatting was not applied to the original buffer text",
19280 );
19281
19282 update_test_language_settings(cx, |settings| {
19283 settings.defaults.formatter = Some(FormatterList::default())
19284 });
19285 let format = editor.update_in(cx, |editor, window, cx| {
19286 editor.perform_format(
19287 project.clone(),
19288 FormatTrigger::Manual,
19289 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19290 window,
19291 cx,
19292 )
19293 });
19294 format.await.unwrap();
19295
19296 assert_eq!(
19297 editor.update(cx, |editor, cx| editor.text(cx)),
19298 buffer_text.to_string()
19299 + prettier_format_suffix
19300 + "\ntypescript\n"
19301 + prettier_format_suffix
19302 + "\ntypescript",
19303 "Autoformatting (via test prettier) was not applied to the original buffer text",
19304 );
19305}
19306
19307#[gpui::test]
19308async fn test_addition_reverts(cx: &mut TestAppContext) {
19309 init_test(cx, |_| {});
19310 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19311 let base_text = indoc! {r#"
19312 struct Row;
19313 struct Row1;
19314 struct Row2;
19315
19316 struct Row4;
19317 struct Row5;
19318 struct Row6;
19319
19320 struct Row8;
19321 struct Row9;
19322 struct Row10;"#};
19323
19324 // When addition hunks are not adjacent to carets, no hunk revert is performed
19325 assert_hunk_revert(
19326 indoc! {r#"struct Row;
19327 struct Row1;
19328 struct Row1.1;
19329 struct Row1.2;
19330 struct Row2;ˇ
19331
19332 struct Row4;
19333 struct Row5;
19334 struct Row6;
19335
19336 struct Row8;
19337 ˇstruct Row9;
19338 struct Row9.1;
19339 struct Row9.2;
19340 struct Row9.3;
19341 struct Row10;"#},
19342 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19343 indoc! {r#"struct Row;
19344 struct Row1;
19345 struct Row1.1;
19346 struct Row1.2;
19347 struct Row2;ˇ
19348
19349 struct Row4;
19350 struct Row5;
19351 struct Row6;
19352
19353 struct Row8;
19354 ˇstruct Row9;
19355 struct Row9.1;
19356 struct Row9.2;
19357 struct Row9.3;
19358 struct Row10;"#},
19359 base_text,
19360 &mut cx,
19361 );
19362 // Same for selections
19363 assert_hunk_revert(
19364 indoc! {r#"struct Row;
19365 struct Row1;
19366 struct Row2;
19367 struct Row2.1;
19368 struct Row2.2;
19369 «ˇ
19370 struct Row4;
19371 struct» Row5;
19372 «struct Row6;
19373 ˇ»
19374 struct Row9.1;
19375 struct Row9.2;
19376 struct Row9.3;
19377 struct Row8;
19378 struct Row9;
19379 struct Row10;"#},
19380 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19381 indoc! {r#"struct Row;
19382 struct Row1;
19383 struct Row2;
19384 struct Row2.1;
19385 struct Row2.2;
19386 «ˇ
19387 struct Row4;
19388 struct» Row5;
19389 «struct Row6;
19390 ˇ»
19391 struct Row9.1;
19392 struct Row9.2;
19393 struct Row9.3;
19394 struct Row8;
19395 struct Row9;
19396 struct Row10;"#},
19397 base_text,
19398 &mut cx,
19399 );
19400
19401 // When carets and selections intersect the addition hunks, those are reverted.
19402 // Adjacent carets got merged.
19403 assert_hunk_revert(
19404 indoc! {r#"struct Row;
19405 ˇ// something on the top
19406 struct Row1;
19407 struct Row2;
19408 struct Roˇw3.1;
19409 struct Row2.2;
19410 struct Row2.3;ˇ
19411
19412 struct Row4;
19413 struct ˇRow5.1;
19414 struct Row5.2;
19415 struct «Rowˇ»5.3;
19416 struct Row5;
19417 struct Row6;
19418 ˇ
19419 struct Row9.1;
19420 struct «Rowˇ»9.2;
19421 struct «ˇRow»9.3;
19422 struct Row8;
19423 struct Row9;
19424 «ˇ// something on bottom»
19425 struct Row10;"#},
19426 vec![
19427 DiffHunkStatusKind::Added,
19428 DiffHunkStatusKind::Added,
19429 DiffHunkStatusKind::Added,
19430 DiffHunkStatusKind::Added,
19431 DiffHunkStatusKind::Added,
19432 ],
19433 indoc! {r#"struct Row;
19434 ˇstruct Row1;
19435 struct Row2;
19436 ˇ
19437 struct Row4;
19438 ˇstruct Row5;
19439 struct Row6;
19440 ˇ
19441 ˇstruct Row8;
19442 struct Row9;
19443 ˇstruct Row10;"#},
19444 base_text,
19445 &mut cx,
19446 );
19447}
19448
19449#[gpui::test]
19450async fn test_modification_reverts(cx: &mut TestAppContext) {
19451 init_test(cx, |_| {});
19452 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19453 let base_text = indoc! {r#"
19454 struct Row;
19455 struct Row1;
19456 struct Row2;
19457
19458 struct Row4;
19459 struct Row5;
19460 struct Row6;
19461
19462 struct Row8;
19463 struct Row9;
19464 struct Row10;"#};
19465
19466 // Modification hunks behave the same as the addition ones.
19467 assert_hunk_revert(
19468 indoc! {r#"struct Row;
19469 struct Row1;
19470 struct Row33;
19471 ˇ
19472 struct Row4;
19473 struct Row5;
19474 struct Row6;
19475 ˇ
19476 struct Row99;
19477 struct Row9;
19478 struct Row10;"#},
19479 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19480 indoc! {r#"struct Row;
19481 struct Row1;
19482 struct Row33;
19483 ˇ
19484 struct Row4;
19485 struct Row5;
19486 struct Row6;
19487 ˇ
19488 struct Row99;
19489 struct Row9;
19490 struct Row10;"#},
19491 base_text,
19492 &mut cx,
19493 );
19494 assert_hunk_revert(
19495 indoc! {r#"struct Row;
19496 struct Row1;
19497 struct Row33;
19498 «ˇ
19499 struct Row4;
19500 struct» Row5;
19501 «struct Row6;
19502 ˇ»
19503 struct Row99;
19504 struct Row9;
19505 struct Row10;"#},
19506 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19507 indoc! {r#"struct Row;
19508 struct Row1;
19509 struct Row33;
19510 «ˇ
19511 struct Row4;
19512 struct» Row5;
19513 «struct Row6;
19514 ˇ»
19515 struct Row99;
19516 struct Row9;
19517 struct Row10;"#},
19518 base_text,
19519 &mut cx,
19520 );
19521
19522 assert_hunk_revert(
19523 indoc! {r#"ˇstruct Row1.1;
19524 struct Row1;
19525 «ˇstr»uct Row22;
19526
19527 struct ˇRow44;
19528 struct Row5;
19529 struct «Rˇ»ow66;ˇ
19530
19531 «struˇ»ct Row88;
19532 struct Row9;
19533 struct Row1011;ˇ"#},
19534 vec![
19535 DiffHunkStatusKind::Modified,
19536 DiffHunkStatusKind::Modified,
19537 DiffHunkStatusKind::Modified,
19538 DiffHunkStatusKind::Modified,
19539 DiffHunkStatusKind::Modified,
19540 DiffHunkStatusKind::Modified,
19541 ],
19542 indoc! {r#"struct Row;
19543 ˇstruct Row1;
19544 struct Row2;
19545 ˇ
19546 struct Row4;
19547 ˇstruct Row5;
19548 struct Row6;
19549 ˇ
19550 struct Row8;
19551 ˇstruct Row9;
19552 struct Row10;ˇ"#},
19553 base_text,
19554 &mut cx,
19555 );
19556}
19557
19558#[gpui::test]
19559async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19560 init_test(cx, |_| {});
19561 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19562 let base_text = indoc! {r#"
19563 one
19564
19565 two
19566 three
19567 "#};
19568
19569 cx.set_head_text(base_text);
19570 cx.set_state("\nˇ\n");
19571 cx.executor().run_until_parked();
19572 cx.update_editor(|editor, _window, cx| {
19573 editor.expand_selected_diff_hunks(cx);
19574 });
19575 cx.executor().run_until_parked();
19576 cx.update_editor(|editor, window, cx| {
19577 editor.backspace(&Default::default(), window, cx);
19578 });
19579 cx.run_until_parked();
19580 cx.assert_state_with_diff(
19581 indoc! {r#"
19582
19583 - two
19584 - threeˇ
19585 +
19586 "#}
19587 .to_string(),
19588 );
19589}
19590
19591#[gpui::test]
19592async fn test_deletion_reverts(cx: &mut TestAppContext) {
19593 init_test(cx, |_| {});
19594 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19595 let base_text = indoc! {r#"struct Row;
19596struct Row1;
19597struct Row2;
19598
19599struct Row4;
19600struct Row5;
19601struct Row6;
19602
19603struct Row8;
19604struct Row9;
19605struct Row10;"#};
19606
19607 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19608 assert_hunk_revert(
19609 indoc! {r#"struct Row;
19610 struct Row2;
19611
19612 ˇstruct Row4;
19613 struct Row5;
19614 struct Row6;
19615 ˇ
19616 struct Row8;
19617 struct Row10;"#},
19618 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19619 indoc! {r#"struct Row;
19620 struct Row2;
19621
19622 ˇstruct Row4;
19623 struct Row5;
19624 struct Row6;
19625 ˇ
19626 struct Row8;
19627 struct Row10;"#},
19628 base_text,
19629 &mut cx,
19630 );
19631 assert_hunk_revert(
19632 indoc! {r#"struct Row;
19633 struct Row2;
19634
19635 «ˇstruct Row4;
19636 struct» Row5;
19637 «struct Row6;
19638 ˇ»
19639 struct Row8;
19640 struct Row10;"#},
19641 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19642 indoc! {r#"struct Row;
19643 struct Row2;
19644
19645 «ˇstruct Row4;
19646 struct» Row5;
19647 «struct Row6;
19648 ˇ»
19649 struct Row8;
19650 struct Row10;"#},
19651 base_text,
19652 &mut cx,
19653 );
19654
19655 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19656 assert_hunk_revert(
19657 indoc! {r#"struct Row;
19658 ˇstruct Row2;
19659
19660 struct Row4;
19661 struct Row5;
19662 struct Row6;
19663
19664 struct Row8;ˇ
19665 struct Row10;"#},
19666 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19667 indoc! {r#"struct Row;
19668 struct Row1;
19669 ˇstruct Row2;
19670
19671 struct Row4;
19672 struct Row5;
19673 struct Row6;
19674
19675 struct Row8;ˇ
19676 struct Row9;
19677 struct Row10;"#},
19678 base_text,
19679 &mut cx,
19680 );
19681 assert_hunk_revert(
19682 indoc! {r#"struct Row;
19683 struct Row2«ˇ;
19684 struct Row4;
19685 struct» Row5;
19686 «struct Row6;
19687
19688 struct Row8;ˇ»
19689 struct Row10;"#},
19690 vec![
19691 DiffHunkStatusKind::Deleted,
19692 DiffHunkStatusKind::Deleted,
19693 DiffHunkStatusKind::Deleted,
19694 ],
19695 indoc! {r#"struct Row;
19696 struct Row1;
19697 struct Row2«ˇ;
19698
19699 struct Row4;
19700 struct» Row5;
19701 «struct Row6;
19702
19703 struct Row8;ˇ»
19704 struct Row9;
19705 struct Row10;"#},
19706 base_text,
19707 &mut cx,
19708 );
19709}
19710
19711#[gpui::test]
19712async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19713 init_test(cx, |_| {});
19714
19715 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19716 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19717 let base_text_3 =
19718 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19719
19720 let text_1 = edit_first_char_of_every_line(base_text_1);
19721 let text_2 = edit_first_char_of_every_line(base_text_2);
19722 let text_3 = edit_first_char_of_every_line(base_text_3);
19723
19724 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19725 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19726 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19727
19728 let multibuffer = cx.new(|cx| {
19729 let mut multibuffer = MultiBuffer::new(ReadWrite);
19730 multibuffer.push_excerpts(
19731 buffer_1.clone(),
19732 [
19733 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19734 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19735 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19736 ],
19737 cx,
19738 );
19739 multibuffer.push_excerpts(
19740 buffer_2.clone(),
19741 [
19742 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19743 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19744 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19745 ],
19746 cx,
19747 );
19748 multibuffer.push_excerpts(
19749 buffer_3.clone(),
19750 [
19751 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19752 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19753 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19754 ],
19755 cx,
19756 );
19757 multibuffer
19758 });
19759
19760 let fs = FakeFs::new(cx.executor());
19761 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19762 let (editor, cx) = cx
19763 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19764 editor.update_in(cx, |editor, _window, cx| {
19765 for (buffer, diff_base) in [
19766 (buffer_1.clone(), base_text_1),
19767 (buffer_2.clone(), base_text_2),
19768 (buffer_3.clone(), base_text_3),
19769 ] {
19770 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19771 editor
19772 .buffer
19773 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19774 }
19775 });
19776 cx.executor().run_until_parked();
19777
19778 editor.update_in(cx, |editor, window, cx| {
19779 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}");
19780 editor.select_all(&SelectAll, window, cx);
19781 editor.git_restore(&Default::default(), window, cx);
19782 });
19783 cx.executor().run_until_parked();
19784
19785 // When all ranges are selected, all buffer hunks are reverted.
19786 editor.update(cx, |editor, cx| {
19787 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");
19788 });
19789 buffer_1.update(cx, |buffer, _| {
19790 assert_eq!(buffer.text(), base_text_1);
19791 });
19792 buffer_2.update(cx, |buffer, _| {
19793 assert_eq!(buffer.text(), base_text_2);
19794 });
19795 buffer_3.update(cx, |buffer, _| {
19796 assert_eq!(buffer.text(), base_text_3);
19797 });
19798
19799 editor.update_in(cx, |editor, window, cx| {
19800 editor.undo(&Default::default(), window, cx);
19801 });
19802
19803 editor.update_in(cx, |editor, window, cx| {
19804 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19805 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19806 });
19807 editor.git_restore(&Default::default(), window, cx);
19808 });
19809
19810 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19811 // but not affect buffer_2 and its related excerpts.
19812 editor.update(cx, |editor, cx| {
19813 assert_eq!(
19814 editor.text(cx),
19815 "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}"
19816 );
19817 });
19818 buffer_1.update(cx, |buffer, _| {
19819 assert_eq!(buffer.text(), base_text_1);
19820 });
19821 buffer_2.update(cx, |buffer, _| {
19822 assert_eq!(
19823 buffer.text(),
19824 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19825 );
19826 });
19827 buffer_3.update(cx, |buffer, _| {
19828 assert_eq!(
19829 buffer.text(),
19830 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19831 );
19832 });
19833
19834 fn edit_first_char_of_every_line(text: &str) -> String {
19835 text.split('\n')
19836 .map(|line| format!("X{}", &line[1..]))
19837 .collect::<Vec<_>>()
19838 .join("\n")
19839 }
19840}
19841
19842#[gpui::test]
19843async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19844 init_test(cx, |_| {});
19845
19846 let cols = 4;
19847 let rows = 10;
19848 let sample_text_1 = sample_text(rows, cols, 'a');
19849 assert_eq!(
19850 sample_text_1,
19851 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19852 );
19853 let sample_text_2 = sample_text(rows, cols, 'l');
19854 assert_eq!(
19855 sample_text_2,
19856 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19857 );
19858 let sample_text_3 = sample_text(rows, cols, 'v');
19859 assert_eq!(
19860 sample_text_3,
19861 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19862 );
19863
19864 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19865 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19866 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19867
19868 let multi_buffer = cx.new(|cx| {
19869 let mut multibuffer = MultiBuffer::new(ReadWrite);
19870 multibuffer.push_excerpts(
19871 buffer_1.clone(),
19872 [
19873 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19874 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19875 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19876 ],
19877 cx,
19878 );
19879 multibuffer.push_excerpts(
19880 buffer_2.clone(),
19881 [
19882 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19883 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19884 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19885 ],
19886 cx,
19887 );
19888 multibuffer.push_excerpts(
19889 buffer_3.clone(),
19890 [
19891 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19892 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19893 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19894 ],
19895 cx,
19896 );
19897 multibuffer
19898 });
19899
19900 let fs = FakeFs::new(cx.executor());
19901 fs.insert_tree(
19902 "/a",
19903 json!({
19904 "main.rs": sample_text_1,
19905 "other.rs": sample_text_2,
19906 "lib.rs": sample_text_3,
19907 }),
19908 )
19909 .await;
19910 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19911 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19912 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19913 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19914 Editor::new(
19915 EditorMode::full(),
19916 multi_buffer,
19917 Some(project.clone()),
19918 window,
19919 cx,
19920 )
19921 });
19922 let multibuffer_item_id = workspace
19923 .update(cx, |workspace, window, cx| {
19924 assert!(
19925 workspace.active_item(cx).is_none(),
19926 "active item should be None before the first item is added"
19927 );
19928 workspace.add_item_to_active_pane(
19929 Box::new(multi_buffer_editor.clone()),
19930 None,
19931 true,
19932 window,
19933 cx,
19934 );
19935 let active_item = workspace
19936 .active_item(cx)
19937 .expect("should have an active item after adding the multi buffer");
19938 assert_eq!(
19939 active_item.buffer_kind(cx),
19940 ItemBufferKind::Multibuffer,
19941 "A multi buffer was expected to active after adding"
19942 );
19943 active_item.item_id()
19944 })
19945 .unwrap();
19946 cx.executor().run_until_parked();
19947
19948 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19949 editor.change_selections(
19950 SelectionEffects::scroll(Autoscroll::Next),
19951 window,
19952 cx,
19953 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
19954 );
19955 editor.open_excerpts(&OpenExcerpts, window, cx);
19956 });
19957 cx.executor().run_until_parked();
19958 let first_item_id = workspace
19959 .update(cx, |workspace, window, cx| {
19960 let active_item = workspace
19961 .active_item(cx)
19962 .expect("should have an active item after navigating into the 1st buffer");
19963 let first_item_id = active_item.item_id();
19964 assert_ne!(
19965 first_item_id, multibuffer_item_id,
19966 "Should navigate into the 1st buffer and activate it"
19967 );
19968 assert_eq!(
19969 active_item.buffer_kind(cx),
19970 ItemBufferKind::Singleton,
19971 "New active item should be a singleton buffer"
19972 );
19973 assert_eq!(
19974 active_item
19975 .act_as::<Editor>(cx)
19976 .expect("should have navigated into an editor for the 1st buffer")
19977 .read(cx)
19978 .text(cx),
19979 sample_text_1
19980 );
19981
19982 workspace
19983 .go_back(workspace.active_pane().downgrade(), window, cx)
19984 .detach_and_log_err(cx);
19985
19986 first_item_id
19987 })
19988 .unwrap();
19989 cx.executor().run_until_parked();
19990 workspace
19991 .update(cx, |workspace, _, cx| {
19992 let active_item = workspace
19993 .active_item(cx)
19994 .expect("should have an active item after navigating back");
19995 assert_eq!(
19996 active_item.item_id(),
19997 multibuffer_item_id,
19998 "Should navigate back to the multi buffer"
19999 );
20000 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20001 })
20002 .unwrap();
20003
20004 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20005 editor.change_selections(
20006 SelectionEffects::scroll(Autoscroll::Next),
20007 window,
20008 cx,
20009 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20010 );
20011 editor.open_excerpts(&OpenExcerpts, window, cx);
20012 });
20013 cx.executor().run_until_parked();
20014 let second_item_id = workspace
20015 .update(cx, |workspace, window, cx| {
20016 let active_item = workspace
20017 .active_item(cx)
20018 .expect("should have an active item after navigating into the 2nd buffer");
20019 let second_item_id = active_item.item_id();
20020 assert_ne!(
20021 second_item_id, multibuffer_item_id,
20022 "Should navigate away from the multibuffer"
20023 );
20024 assert_ne!(
20025 second_item_id, first_item_id,
20026 "Should navigate into the 2nd buffer and activate it"
20027 );
20028 assert_eq!(
20029 active_item.buffer_kind(cx),
20030 ItemBufferKind::Singleton,
20031 "New active item should be a singleton buffer"
20032 );
20033 assert_eq!(
20034 active_item
20035 .act_as::<Editor>(cx)
20036 .expect("should have navigated into an editor")
20037 .read(cx)
20038 .text(cx),
20039 sample_text_2
20040 );
20041
20042 workspace
20043 .go_back(workspace.active_pane().downgrade(), window, cx)
20044 .detach_and_log_err(cx);
20045
20046 second_item_id
20047 })
20048 .unwrap();
20049 cx.executor().run_until_parked();
20050 workspace
20051 .update(cx, |workspace, _, cx| {
20052 let active_item = workspace
20053 .active_item(cx)
20054 .expect("should have an active item after navigating back from the 2nd buffer");
20055 assert_eq!(
20056 active_item.item_id(),
20057 multibuffer_item_id,
20058 "Should navigate back from the 2nd buffer to the multi buffer"
20059 );
20060 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20061 })
20062 .unwrap();
20063
20064 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20065 editor.change_selections(
20066 SelectionEffects::scroll(Autoscroll::Next),
20067 window,
20068 cx,
20069 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20070 );
20071 editor.open_excerpts(&OpenExcerpts, window, cx);
20072 });
20073 cx.executor().run_until_parked();
20074 workspace
20075 .update(cx, |workspace, window, cx| {
20076 let active_item = workspace
20077 .active_item(cx)
20078 .expect("should have an active item after navigating into the 3rd buffer");
20079 let third_item_id = active_item.item_id();
20080 assert_ne!(
20081 third_item_id, multibuffer_item_id,
20082 "Should navigate into the 3rd buffer and activate it"
20083 );
20084 assert_ne!(third_item_id, first_item_id);
20085 assert_ne!(third_item_id, second_item_id);
20086 assert_eq!(
20087 active_item.buffer_kind(cx),
20088 ItemBufferKind::Singleton,
20089 "New active item should be a singleton buffer"
20090 );
20091 assert_eq!(
20092 active_item
20093 .act_as::<Editor>(cx)
20094 .expect("should have navigated into an editor")
20095 .read(cx)
20096 .text(cx),
20097 sample_text_3
20098 );
20099
20100 workspace
20101 .go_back(workspace.active_pane().downgrade(), window, cx)
20102 .detach_and_log_err(cx);
20103 })
20104 .unwrap();
20105 cx.executor().run_until_parked();
20106 workspace
20107 .update(cx, |workspace, _, cx| {
20108 let active_item = workspace
20109 .active_item(cx)
20110 .expect("should have an active item after navigating back from the 3rd buffer");
20111 assert_eq!(
20112 active_item.item_id(),
20113 multibuffer_item_id,
20114 "Should navigate back from the 3rd buffer to the multi buffer"
20115 );
20116 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20117 })
20118 .unwrap();
20119}
20120
20121#[gpui::test]
20122async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20123 init_test(cx, |_| {});
20124
20125 let mut cx = EditorTestContext::new(cx).await;
20126
20127 let diff_base = r#"
20128 use some::mod;
20129
20130 const A: u32 = 42;
20131
20132 fn main() {
20133 println!("hello");
20134
20135 println!("world");
20136 }
20137 "#
20138 .unindent();
20139
20140 cx.set_state(
20141 &r#"
20142 use some::modified;
20143
20144 ˇ
20145 fn main() {
20146 println!("hello there");
20147
20148 println!("around the");
20149 println!("world");
20150 }
20151 "#
20152 .unindent(),
20153 );
20154
20155 cx.set_head_text(&diff_base);
20156 executor.run_until_parked();
20157
20158 cx.update_editor(|editor, window, cx| {
20159 editor.go_to_next_hunk(&GoToHunk, window, cx);
20160 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20161 });
20162 executor.run_until_parked();
20163 cx.assert_state_with_diff(
20164 r#"
20165 use some::modified;
20166
20167
20168 fn main() {
20169 - println!("hello");
20170 + ˇ println!("hello there");
20171
20172 println!("around the");
20173 println!("world");
20174 }
20175 "#
20176 .unindent(),
20177 );
20178
20179 cx.update_editor(|editor, window, cx| {
20180 for _ in 0..2 {
20181 editor.go_to_next_hunk(&GoToHunk, window, cx);
20182 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20183 }
20184 });
20185 executor.run_until_parked();
20186 cx.assert_state_with_diff(
20187 r#"
20188 - use some::mod;
20189 + ˇuse some::modified;
20190
20191
20192 fn main() {
20193 - println!("hello");
20194 + println!("hello there");
20195
20196 + println!("around the");
20197 println!("world");
20198 }
20199 "#
20200 .unindent(),
20201 );
20202
20203 cx.update_editor(|editor, window, cx| {
20204 editor.go_to_next_hunk(&GoToHunk, window, cx);
20205 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20206 });
20207 executor.run_until_parked();
20208 cx.assert_state_with_diff(
20209 r#"
20210 - use some::mod;
20211 + use some::modified;
20212
20213 - const A: u32 = 42;
20214 ˇ
20215 fn main() {
20216 - println!("hello");
20217 + println!("hello there");
20218
20219 + println!("around the");
20220 println!("world");
20221 }
20222 "#
20223 .unindent(),
20224 );
20225
20226 cx.update_editor(|editor, window, cx| {
20227 editor.cancel(&Cancel, window, cx);
20228 });
20229
20230 cx.assert_state_with_diff(
20231 r#"
20232 use some::modified;
20233
20234 ˇ
20235 fn main() {
20236 println!("hello there");
20237
20238 println!("around the");
20239 println!("world");
20240 }
20241 "#
20242 .unindent(),
20243 );
20244}
20245
20246#[gpui::test]
20247async fn test_diff_base_change_with_expanded_diff_hunks(
20248 executor: BackgroundExecutor,
20249 cx: &mut TestAppContext,
20250) {
20251 init_test(cx, |_| {});
20252
20253 let mut cx = EditorTestContext::new(cx).await;
20254
20255 let diff_base = r#"
20256 use some::mod1;
20257 use some::mod2;
20258
20259 const A: u32 = 42;
20260 const B: u32 = 42;
20261 const C: u32 = 42;
20262
20263 fn main() {
20264 println!("hello");
20265
20266 println!("world");
20267 }
20268 "#
20269 .unindent();
20270
20271 cx.set_state(
20272 &r#"
20273 use some::mod2;
20274
20275 const A: u32 = 42;
20276 const C: u32 = 42;
20277
20278 fn main(ˇ) {
20279 //println!("hello");
20280
20281 println!("world");
20282 //
20283 //
20284 }
20285 "#
20286 .unindent(),
20287 );
20288
20289 cx.set_head_text(&diff_base);
20290 executor.run_until_parked();
20291
20292 cx.update_editor(|editor, window, cx| {
20293 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20294 });
20295 executor.run_until_parked();
20296 cx.assert_state_with_diff(
20297 r#"
20298 - use some::mod1;
20299 use some::mod2;
20300
20301 const A: u32 = 42;
20302 - const B: u32 = 42;
20303 const C: u32 = 42;
20304
20305 fn main(ˇ) {
20306 - println!("hello");
20307 + //println!("hello");
20308
20309 println!("world");
20310 + //
20311 + //
20312 }
20313 "#
20314 .unindent(),
20315 );
20316
20317 cx.set_head_text("new diff base!");
20318 executor.run_until_parked();
20319 cx.assert_state_with_diff(
20320 r#"
20321 - new diff base!
20322 + use some::mod2;
20323 +
20324 + const A: u32 = 42;
20325 + const C: u32 = 42;
20326 +
20327 + fn main(ˇ) {
20328 + //println!("hello");
20329 +
20330 + println!("world");
20331 + //
20332 + //
20333 + }
20334 "#
20335 .unindent(),
20336 );
20337}
20338
20339#[gpui::test]
20340async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20341 init_test(cx, |_| {});
20342
20343 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20344 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20345 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20346 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20347 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20348 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20349
20350 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20351 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20352 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20353
20354 let multi_buffer = cx.new(|cx| {
20355 let mut multibuffer = MultiBuffer::new(ReadWrite);
20356 multibuffer.push_excerpts(
20357 buffer_1.clone(),
20358 [
20359 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20360 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20361 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20362 ],
20363 cx,
20364 );
20365 multibuffer.push_excerpts(
20366 buffer_2.clone(),
20367 [
20368 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20369 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20370 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20371 ],
20372 cx,
20373 );
20374 multibuffer.push_excerpts(
20375 buffer_3.clone(),
20376 [
20377 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20378 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20379 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20380 ],
20381 cx,
20382 );
20383 multibuffer
20384 });
20385
20386 let editor =
20387 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20388 editor
20389 .update(cx, |editor, _window, cx| {
20390 for (buffer, diff_base) in [
20391 (buffer_1.clone(), file_1_old),
20392 (buffer_2.clone(), file_2_old),
20393 (buffer_3.clone(), file_3_old),
20394 ] {
20395 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20396 editor
20397 .buffer
20398 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20399 }
20400 })
20401 .unwrap();
20402
20403 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20404 cx.run_until_parked();
20405
20406 cx.assert_editor_state(
20407 &"
20408 ˇaaa
20409 ccc
20410 ddd
20411
20412 ggg
20413 hhh
20414
20415
20416 lll
20417 mmm
20418 NNN
20419
20420 qqq
20421 rrr
20422
20423 uuu
20424 111
20425 222
20426 333
20427
20428 666
20429 777
20430
20431 000
20432 !!!"
20433 .unindent(),
20434 );
20435
20436 cx.update_editor(|editor, window, cx| {
20437 editor.select_all(&SelectAll, window, cx);
20438 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20439 });
20440 cx.executor().run_until_parked();
20441
20442 cx.assert_state_with_diff(
20443 "
20444 «aaa
20445 - bbb
20446 ccc
20447 ddd
20448
20449 ggg
20450 hhh
20451
20452
20453 lll
20454 mmm
20455 - nnn
20456 + NNN
20457
20458 qqq
20459 rrr
20460
20461 uuu
20462 111
20463 222
20464 333
20465
20466 + 666
20467 777
20468
20469 000
20470 !!!ˇ»"
20471 .unindent(),
20472 );
20473}
20474
20475#[gpui::test]
20476async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20477 init_test(cx, |_| {});
20478
20479 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20480 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20481
20482 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20483 let multi_buffer = cx.new(|cx| {
20484 let mut multibuffer = MultiBuffer::new(ReadWrite);
20485 multibuffer.push_excerpts(
20486 buffer.clone(),
20487 [
20488 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20489 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20490 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20491 ],
20492 cx,
20493 );
20494 multibuffer
20495 });
20496
20497 let editor =
20498 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20499 editor
20500 .update(cx, |editor, _window, cx| {
20501 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20502 editor
20503 .buffer
20504 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20505 })
20506 .unwrap();
20507
20508 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20509 cx.run_until_parked();
20510
20511 cx.update_editor(|editor, window, cx| {
20512 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20513 });
20514 cx.executor().run_until_parked();
20515
20516 // When the start of a hunk coincides with the start of its excerpt,
20517 // the hunk is expanded. When the start of a hunk is earlier than
20518 // the start of its excerpt, the hunk is not expanded.
20519 cx.assert_state_with_diff(
20520 "
20521 ˇaaa
20522 - bbb
20523 + BBB
20524
20525 - ddd
20526 - eee
20527 + DDD
20528 + EEE
20529 fff
20530
20531 iii
20532 "
20533 .unindent(),
20534 );
20535}
20536
20537#[gpui::test]
20538async fn test_edits_around_expanded_insertion_hunks(
20539 executor: BackgroundExecutor,
20540 cx: &mut TestAppContext,
20541) {
20542 init_test(cx, |_| {});
20543
20544 let mut cx = EditorTestContext::new(cx).await;
20545
20546 let diff_base = r#"
20547 use some::mod1;
20548 use some::mod2;
20549
20550 const A: u32 = 42;
20551
20552 fn main() {
20553 println!("hello");
20554
20555 println!("world");
20556 }
20557 "#
20558 .unindent();
20559 executor.run_until_parked();
20560 cx.set_state(
20561 &r#"
20562 use some::mod1;
20563 use some::mod2;
20564
20565 const A: u32 = 42;
20566 const B: u32 = 42;
20567 const C: u32 = 42;
20568 ˇ
20569
20570 fn main() {
20571 println!("hello");
20572
20573 println!("world");
20574 }
20575 "#
20576 .unindent(),
20577 );
20578
20579 cx.set_head_text(&diff_base);
20580 executor.run_until_parked();
20581
20582 cx.update_editor(|editor, window, cx| {
20583 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20584 });
20585 executor.run_until_parked();
20586
20587 cx.assert_state_with_diff(
20588 r#"
20589 use some::mod1;
20590 use some::mod2;
20591
20592 const A: u32 = 42;
20593 + const B: u32 = 42;
20594 + const C: u32 = 42;
20595 + ˇ
20596
20597 fn main() {
20598 println!("hello");
20599
20600 println!("world");
20601 }
20602 "#
20603 .unindent(),
20604 );
20605
20606 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20607 executor.run_until_parked();
20608
20609 cx.assert_state_with_diff(
20610 r#"
20611 use some::mod1;
20612 use some::mod2;
20613
20614 const A: u32 = 42;
20615 + const B: u32 = 42;
20616 + const C: u32 = 42;
20617 + const D: u32 = 42;
20618 + ˇ
20619
20620 fn main() {
20621 println!("hello");
20622
20623 println!("world");
20624 }
20625 "#
20626 .unindent(),
20627 );
20628
20629 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20630 executor.run_until_parked();
20631
20632 cx.assert_state_with_diff(
20633 r#"
20634 use some::mod1;
20635 use some::mod2;
20636
20637 const A: u32 = 42;
20638 + const B: u32 = 42;
20639 + const C: u32 = 42;
20640 + const D: u32 = 42;
20641 + const E: u32 = 42;
20642 + ˇ
20643
20644 fn main() {
20645 println!("hello");
20646
20647 println!("world");
20648 }
20649 "#
20650 .unindent(),
20651 );
20652
20653 cx.update_editor(|editor, window, cx| {
20654 editor.delete_line(&DeleteLine, window, cx);
20655 });
20656 executor.run_until_parked();
20657
20658 cx.assert_state_with_diff(
20659 r#"
20660 use some::mod1;
20661 use some::mod2;
20662
20663 const A: u32 = 42;
20664 + const B: u32 = 42;
20665 + const C: u32 = 42;
20666 + const D: u32 = 42;
20667 + const E: u32 = 42;
20668 ˇ
20669 fn main() {
20670 println!("hello");
20671
20672 println!("world");
20673 }
20674 "#
20675 .unindent(),
20676 );
20677
20678 cx.update_editor(|editor, window, cx| {
20679 editor.move_up(&MoveUp, window, cx);
20680 editor.delete_line(&DeleteLine, window, cx);
20681 editor.move_up(&MoveUp, window, cx);
20682 editor.delete_line(&DeleteLine, window, cx);
20683 editor.move_up(&MoveUp, window, cx);
20684 editor.delete_line(&DeleteLine, window, cx);
20685 });
20686 executor.run_until_parked();
20687 cx.assert_state_with_diff(
20688 r#"
20689 use some::mod1;
20690 use some::mod2;
20691
20692 const A: u32 = 42;
20693 + const B: u32 = 42;
20694 ˇ
20695 fn main() {
20696 println!("hello");
20697
20698 println!("world");
20699 }
20700 "#
20701 .unindent(),
20702 );
20703
20704 cx.update_editor(|editor, window, cx| {
20705 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20706 editor.delete_line(&DeleteLine, window, cx);
20707 });
20708 executor.run_until_parked();
20709 cx.assert_state_with_diff(
20710 r#"
20711 ˇ
20712 fn main() {
20713 println!("hello");
20714
20715 println!("world");
20716 }
20717 "#
20718 .unindent(),
20719 );
20720}
20721
20722#[gpui::test]
20723async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20724 init_test(cx, |_| {});
20725
20726 let mut cx = EditorTestContext::new(cx).await;
20727 cx.set_head_text(indoc! { "
20728 one
20729 two
20730 three
20731 four
20732 five
20733 "
20734 });
20735 cx.set_state(indoc! { "
20736 one
20737 ˇthree
20738 five
20739 "});
20740 cx.run_until_parked();
20741 cx.update_editor(|editor, window, cx| {
20742 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20743 });
20744 cx.assert_state_with_diff(
20745 indoc! { "
20746 one
20747 - two
20748 ˇthree
20749 - four
20750 five
20751 "}
20752 .to_string(),
20753 );
20754 cx.update_editor(|editor, window, cx| {
20755 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20756 });
20757
20758 cx.assert_state_with_diff(
20759 indoc! { "
20760 one
20761 ˇthree
20762 five
20763 "}
20764 .to_string(),
20765 );
20766
20767 cx.set_state(indoc! { "
20768 one
20769 ˇTWO
20770 three
20771 four
20772 five
20773 "});
20774 cx.run_until_parked();
20775 cx.update_editor(|editor, window, cx| {
20776 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20777 });
20778
20779 cx.assert_state_with_diff(
20780 indoc! { "
20781 one
20782 - two
20783 + ˇTWO
20784 three
20785 four
20786 five
20787 "}
20788 .to_string(),
20789 );
20790 cx.update_editor(|editor, window, cx| {
20791 editor.move_up(&Default::default(), window, cx);
20792 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20793 });
20794 cx.assert_state_with_diff(
20795 indoc! { "
20796 one
20797 ˇTWO
20798 three
20799 four
20800 five
20801 "}
20802 .to_string(),
20803 );
20804}
20805
20806#[gpui::test]
20807async fn test_edits_around_expanded_deletion_hunks(
20808 executor: BackgroundExecutor,
20809 cx: &mut TestAppContext,
20810) {
20811 init_test(cx, |_| {});
20812
20813 let mut cx = EditorTestContext::new(cx).await;
20814
20815 let diff_base = r#"
20816 use some::mod1;
20817 use some::mod2;
20818
20819 const A: u32 = 42;
20820 const B: u32 = 42;
20821 const C: u32 = 42;
20822
20823
20824 fn main() {
20825 println!("hello");
20826
20827 println!("world");
20828 }
20829 "#
20830 .unindent();
20831 executor.run_until_parked();
20832 cx.set_state(
20833 &r#"
20834 use some::mod1;
20835 use some::mod2;
20836
20837 ˇconst B: u32 = 42;
20838 const C: u32 = 42;
20839
20840
20841 fn main() {
20842 println!("hello");
20843
20844 println!("world");
20845 }
20846 "#
20847 .unindent(),
20848 );
20849
20850 cx.set_head_text(&diff_base);
20851 executor.run_until_parked();
20852
20853 cx.update_editor(|editor, window, cx| {
20854 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20855 });
20856 executor.run_until_parked();
20857
20858 cx.assert_state_with_diff(
20859 r#"
20860 use some::mod1;
20861 use some::mod2;
20862
20863 - const A: u32 = 42;
20864 ˇconst B: u32 = 42;
20865 const C: u32 = 42;
20866
20867
20868 fn main() {
20869 println!("hello");
20870
20871 println!("world");
20872 }
20873 "#
20874 .unindent(),
20875 );
20876
20877 cx.update_editor(|editor, window, cx| {
20878 editor.delete_line(&DeleteLine, window, cx);
20879 });
20880 executor.run_until_parked();
20881 cx.assert_state_with_diff(
20882 r#"
20883 use some::mod1;
20884 use some::mod2;
20885
20886 - const A: u32 = 42;
20887 - const B: u32 = 42;
20888 ˇconst C: u32 = 42;
20889
20890
20891 fn main() {
20892 println!("hello");
20893
20894 println!("world");
20895 }
20896 "#
20897 .unindent(),
20898 );
20899
20900 cx.update_editor(|editor, window, cx| {
20901 editor.delete_line(&DeleteLine, window, cx);
20902 });
20903 executor.run_until_parked();
20904 cx.assert_state_with_diff(
20905 r#"
20906 use some::mod1;
20907 use some::mod2;
20908
20909 - const A: u32 = 42;
20910 - const B: u32 = 42;
20911 - const C: u32 = 42;
20912 ˇ
20913
20914 fn main() {
20915 println!("hello");
20916
20917 println!("world");
20918 }
20919 "#
20920 .unindent(),
20921 );
20922
20923 cx.update_editor(|editor, window, cx| {
20924 editor.handle_input("replacement", window, cx);
20925 });
20926 executor.run_until_parked();
20927 cx.assert_state_with_diff(
20928 r#"
20929 use some::mod1;
20930 use some::mod2;
20931
20932 - const A: u32 = 42;
20933 - const B: u32 = 42;
20934 - const C: u32 = 42;
20935 -
20936 + replacementˇ
20937
20938 fn main() {
20939 println!("hello");
20940
20941 println!("world");
20942 }
20943 "#
20944 .unindent(),
20945 );
20946}
20947
20948#[gpui::test]
20949async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20950 init_test(cx, |_| {});
20951
20952 let mut cx = EditorTestContext::new(cx).await;
20953
20954 let base_text = r#"
20955 one
20956 two
20957 three
20958 four
20959 five
20960 "#
20961 .unindent();
20962 executor.run_until_parked();
20963 cx.set_state(
20964 &r#"
20965 one
20966 two
20967 fˇour
20968 five
20969 "#
20970 .unindent(),
20971 );
20972
20973 cx.set_head_text(&base_text);
20974 executor.run_until_parked();
20975
20976 cx.update_editor(|editor, window, cx| {
20977 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20978 });
20979 executor.run_until_parked();
20980
20981 cx.assert_state_with_diff(
20982 r#"
20983 one
20984 two
20985 - three
20986 fˇour
20987 five
20988 "#
20989 .unindent(),
20990 );
20991
20992 cx.update_editor(|editor, window, cx| {
20993 editor.backspace(&Backspace, window, cx);
20994 editor.backspace(&Backspace, window, cx);
20995 });
20996 executor.run_until_parked();
20997 cx.assert_state_with_diff(
20998 r#"
20999 one
21000 two
21001 - threeˇ
21002 - four
21003 + our
21004 five
21005 "#
21006 .unindent(),
21007 );
21008}
21009
21010#[gpui::test]
21011async fn test_edit_after_expanded_modification_hunk(
21012 executor: BackgroundExecutor,
21013 cx: &mut TestAppContext,
21014) {
21015 init_test(cx, |_| {});
21016
21017 let mut cx = EditorTestContext::new(cx).await;
21018
21019 let diff_base = r#"
21020 use some::mod1;
21021 use some::mod2;
21022
21023 const A: u32 = 42;
21024 const B: u32 = 42;
21025 const C: u32 = 42;
21026 const D: u32 = 42;
21027
21028
21029 fn main() {
21030 println!("hello");
21031
21032 println!("world");
21033 }"#
21034 .unindent();
21035
21036 cx.set_state(
21037 &r#"
21038 use some::mod1;
21039 use some::mod2;
21040
21041 const A: u32 = 42;
21042 const B: u32 = 42;
21043 const C: u32 = 43ˇ
21044 const D: u32 = 42;
21045
21046
21047 fn main() {
21048 println!("hello");
21049
21050 println!("world");
21051 }"#
21052 .unindent(),
21053 );
21054
21055 cx.set_head_text(&diff_base);
21056 executor.run_until_parked();
21057 cx.update_editor(|editor, window, cx| {
21058 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21059 });
21060 executor.run_until_parked();
21061
21062 cx.assert_state_with_diff(
21063 r#"
21064 use some::mod1;
21065 use some::mod2;
21066
21067 const A: u32 = 42;
21068 const B: u32 = 42;
21069 - const C: u32 = 42;
21070 + const C: u32 = 43ˇ
21071 const D: u32 = 42;
21072
21073
21074 fn main() {
21075 println!("hello");
21076
21077 println!("world");
21078 }"#
21079 .unindent(),
21080 );
21081
21082 cx.update_editor(|editor, window, cx| {
21083 editor.handle_input("\nnew_line\n", window, cx);
21084 });
21085 executor.run_until_parked();
21086
21087 cx.assert_state_with_diff(
21088 r#"
21089 use some::mod1;
21090 use some::mod2;
21091
21092 const A: u32 = 42;
21093 const B: u32 = 42;
21094 - const C: u32 = 42;
21095 + const C: u32 = 43
21096 + new_line
21097 + ˇ
21098 const D: u32 = 42;
21099
21100
21101 fn main() {
21102 println!("hello");
21103
21104 println!("world");
21105 }"#
21106 .unindent(),
21107 );
21108}
21109
21110#[gpui::test]
21111async fn test_stage_and_unstage_added_file_hunk(
21112 executor: BackgroundExecutor,
21113 cx: &mut TestAppContext,
21114) {
21115 init_test(cx, |_| {});
21116
21117 let mut cx = EditorTestContext::new(cx).await;
21118 cx.update_editor(|editor, _, cx| {
21119 editor.set_expand_all_diff_hunks(cx);
21120 });
21121
21122 let working_copy = r#"
21123 ˇfn main() {
21124 println!("hello, world!");
21125 }
21126 "#
21127 .unindent();
21128
21129 cx.set_state(&working_copy);
21130 executor.run_until_parked();
21131
21132 cx.assert_state_with_diff(
21133 r#"
21134 + ˇfn main() {
21135 + println!("hello, world!");
21136 + }
21137 "#
21138 .unindent(),
21139 );
21140 cx.assert_index_text(None);
21141
21142 cx.update_editor(|editor, window, cx| {
21143 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21144 });
21145 executor.run_until_parked();
21146 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21147 cx.assert_state_with_diff(
21148 r#"
21149 + ˇfn main() {
21150 + println!("hello, world!");
21151 + }
21152 "#
21153 .unindent(),
21154 );
21155
21156 cx.update_editor(|editor, window, cx| {
21157 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21158 });
21159 executor.run_until_parked();
21160 cx.assert_index_text(None);
21161}
21162
21163async fn setup_indent_guides_editor(
21164 text: &str,
21165 cx: &mut TestAppContext,
21166) -> (BufferId, EditorTestContext) {
21167 init_test(cx, |_| {});
21168
21169 let mut cx = EditorTestContext::new(cx).await;
21170
21171 let buffer_id = cx.update_editor(|editor, window, cx| {
21172 editor.set_text(text, window, cx);
21173 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21174
21175 buffer_ids[0]
21176 });
21177
21178 (buffer_id, cx)
21179}
21180
21181fn assert_indent_guides(
21182 range: Range<u32>,
21183 expected: Vec<IndentGuide>,
21184 active_indices: Option<Vec<usize>>,
21185 cx: &mut EditorTestContext,
21186) {
21187 let indent_guides = cx.update_editor(|editor, window, cx| {
21188 let snapshot = editor.snapshot(window, cx).display_snapshot;
21189 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21190 editor,
21191 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21192 true,
21193 &snapshot,
21194 cx,
21195 );
21196
21197 indent_guides.sort_by(|a, b| {
21198 a.depth.cmp(&b.depth).then(
21199 a.start_row
21200 .cmp(&b.start_row)
21201 .then(a.end_row.cmp(&b.end_row)),
21202 )
21203 });
21204 indent_guides
21205 });
21206
21207 if let Some(expected) = active_indices {
21208 let active_indices = cx.update_editor(|editor, window, cx| {
21209 let snapshot = editor.snapshot(window, cx).display_snapshot;
21210 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21211 });
21212
21213 assert_eq!(
21214 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21215 expected,
21216 "Active indent guide indices do not match"
21217 );
21218 }
21219
21220 assert_eq!(indent_guides, expected, "Indent guides do not match");
21221}
21222
21223fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21224 IndentGuide {
21225 buffer_id,
21226 start_row: MultiBufferRow(start_row),
21227 end_row: MultiBufferRow(end_row),
21228 depth,
21229 tab_size: 4,
21230 settings: IndentGuideSettings {
21231 enabled: true,
21232 line_width: 1,
21233 active_line_width: 1,
21234 coloring: IndentGuideColoring::default(),
21235 background_coloring: IndentGuideBackgroundColoring::default(),
21236 },
21237 }
21238}
21239
21240#[gpui::test]
21241async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21242 let (buffer_id, mut cx) = setup_indent_guides_editor(
21243 &"
21244 fn main() {
21245 let a = 1;
21246 }"
21247 .unindent(),
21248 cx,
21249 )
21250 .await;
21251
21252 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21253}
21254
21255#[gpui::test]
21256async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21257 let (buffer_id, mut cx) = setup_indent_guides_editor(
21258 &"
21259 fn main() {
21260 let a = 1;
21261 let b = 2;
21262 }"
21263 .unindent(),
21264 cx,
21265 )
21266 .await;
21267
21268 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21269}
21270
21271#[gpui::test]
21272async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21273 let (buffer_id, mut cx) = setup_indent_guides_editor(
21274 &"
21275 fn main() {
21276 let a = 1;
21277 if a == 3 {
21278 let b = 2;
21279 } else {
21280 let c = 3;
21281 }
21282 }"
21283 .unindent(),
21284 cx,
21285 )
21286 .await;
21287
21288 assert_indent_guides(
21289 0..8,
21290 vec![
21291 indent_guide(buffer_id, 1, 6, 0),
21292 indent_guide(buffer_id, 3, 3, 1),
21293 indent_guide(buffer_id, 5, 5, 1),
21294 ],
21295 None,
21296 &mut cx,
21297 );
21298}
21299
21300#[gpui::test]
21301async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21302 let (buffer_id, mut cx) = setup_indent_guides_editor(
21303 &"
21304 fn main() {
21305 let a = 1;
21306 let b = 2;
21307 let c = 3;
21308 }"
21309 .unindent(),
21310 cx,
21311 )
21312 .await;
21313
21314 assert_indent_guides(
21315 0..5,
21316 vec![
21317 indent_guide(buffer_id, 1, 3, 0),
21318 indent_guide(buffer_id, 2, 2, 1),
21319 ],
21320 None,
21321 &mut cx,
21322 );
21323}
21324
21325#[gpui::test]
21326async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21327 let (buffer_id, mut cx) = setup_indent_guides_editor(
21328 &"
21329 fn main() {
21330 let a = 1;
21331
21332 let c = 3;
21333 }"
21334 .unindent(),
21335 cx,
21336 )
21337 .await;
21338
21339 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21340}
21341
21342#[gpui::test]
21343async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21344 let (buffer_id, mut cx) = setup_indent_guides_editor(
21345 &"
21346 fn main() {
21347 let a = 1;
21348
21349 let c = 3;
21350
21351 if a == 3 {
21352 let b = 2;
21353 } else {
21354 let c = 3;
21355 }
21356 }"
21357 .unindent(),
21358 cx,
21359 )
21360 .await;
21361
21362 assert_indent_guides(
21363 0..11,
21364 vec![
21365 indent_guide(buffer_id, 1, 9, 0),
21366 indent_guide(buffer_id, 6, 6, 1),
21367 indent_guide(buffer_id, 8, 8, 1),
21368 ],
21369 None,
21370 &mut cx,
21371 );
21372}
21373
21374#[gpui::test]
21375async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21376 let (buffer_id, mut cx) = setup_indent_guides_editor(
21377 &"
21378 fn main() {
21379 let a = 1;
21380
21381 let c = 3;
21382
21383 if a == 3 {
21384 let b = 2;
21385 } else {
21386 let c = 3;
21387 }
21388 }"
21389 .unindent(),
21390 cx,
21391 )
21392 .await;
21393
21394 assert_indent_guides(
21395 1..11,
21396 vec![
21397 indent_guide(buffer_id, 1, 9, 0),
21398 indent_guide(buffer_id, 6, 6, 1),
21399 indent_guide(buffer_id, 8, 8, 1),
21400 ],
21401 None,
21402 &mut cx,
21403 );
21404}
21405
21406#[gpui::test]
21407async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21408 let (buffer_id, mut cx) = setup_indent_guides_editor(
21409 &"
21410 fn main() {
21411 let a = 1;
21412
21413 let c = 3;
21414
21415 if a == 3 {
21416 let b = 2;
21417 } else {
21418 let c = 3;
21419 }
21420 }"
21421 .unindent(),
21422 cx,
21423 )
21424 .await;
21425
21426 assert_indent_guides(
21427 1..10,
21428 vec![
21429 indent_guide(buffer_id, 1, 9, 0),
21430 indent_guide(buffer_id, 6, 6, 1),
21431 indent_guide(buffer_id, 8, 8, 1),
21432 ],
21433 None,
21434 &mut cx,
21435 );
21436}
21437
21438#[gpui::test]
21439async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21440 let (buffer_id, mut cx) = setup_indent_guides_editor(
21441 &"
21442 fn main() {
21443 if a {
21444 b(
21445 c,
21446 d,
21447 )
21448 } else {
21449 e(
21450 f
21451 )
21452 }
21453 }"
21454 .unindent(),
21455 cx,
21456 )
21457 .await;
21458
21459 assert_indent_guides(
21460 0..11,
21461 vec![
21462 indent_guide(buffer_id, 1, 10, 0),
21463 indent_guide(buffer_id, 2, 5, 1),
21464 indent_guide(buffer_id, 7, 9, 1),
21465 indent_guide(buffer_id, 3, 4, 2),
21466 indent_guide(buffer_id, 8, 8, 2),
21467 ],
21468 None,
21469 &mut cx,
21470 );
21471
21472 cx.update_editor(|editor, window, cx| {
21473 editor.fold_at(MultiBufferRow(2), window, cx);
21474 assert_eq!(
21475 editor.display_text(cx),
21476 "
21477 fn main() {
21478 if a {
21479 b(⋯
21480 )
21481 } else {
21482 e(
21483 f
21484 )
21485 }
21486 }"
21487 .unindent()
21488 );
21489 });
21490
21491 assert_indent_guides(
21492 0..11,
21493 vec![
21494 indent_guide(buffer_id, 1, 10, 0),
21495 indent_guide(buffer_id, 2, 5, 1),
21496 indent_guide(buffer_id, 7, 9, 1),
21497 indent_guide(buffer_id, 8, 8, 2),
21498 ],
21499 None,
21500 &mut cx,
21501 );
21502}
21503
21504#[gpui::test]
21505async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21506 let (buffer_id, mut cx) = setup_indent_guides_editor(
21507 &"
21508 block1
21509 block2
21510 block3
21511 block4
21512 block2
21513 block1
21514 block1"
21515 .unindent(),
21516 cx,
21517 )
21518 .await;
21519
21520 assert_indent_guides(
21521 1..10,
21522 vec![
21523 indent_guide(buffer_id, 1, 4, 0),
21524 indent_guide(buffer_id, 2, 3, 1),
21525 indent_guide(buffer_id, 3, 3, 2),
21526 ],
21527 None,
21528 &mut cx,
21529 );
21530}
21531
21532#[gpui::test]
21533async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21534 let (buffer_id, mut cx) = setup_indent_guides_editor(
21535 &"
21536 block1
21537 block2
21538 block3
21539
21540 block1
21541 block1"
21542 .unindent(),
21543 cx,
21544 )
21545 .await;
21546
21547 assert_indent_guides(
21548 0..6,
21549 vec![
21550 indent_guide(buffer_id, 1, 2, 0),
21551 indent_guide(buffer_id, 2, 2, 1),
21552 ],
21553 None,
21554 &mut cx,
21555 );
21556}
21557
21558#[gpui::test]
21559async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21560 let (buffer_id, mut cx) = setup_indent_guides_editor(
21561 &"
21562 function component() {
21563 \treturn (
21564 \t\t\t
21565 \t\t<div>
21566 \t\t\t<abc></abc>
21567 \t\t</div>
21568 \t)
21569 }"
21570 .unindent(),
21571 cx,
21572 )
21573 .await;
21574
21575 assert_indent_guides(
21576 0..8,
21577 vec![
21578 indent_guide(buffer_id, 1, 6, 0),
21579 indent_guide(buffer_id, 2, 5, 1),
21580 indent_guide(buffer_id, 4, 4, 2),
21581 ],
21582 None,
21583 &mut cx,
21584 );
21585}
21586
21587#[gpui::test]
21588async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21589 let (buffer_id, mut cx) = setup_indent_guides_editor(
21590 &"
21591 function component() {
21592 \treturn (
21593 \t
21594 \t\t<div>
21595 \t\t\t<abc></abc>
21596 \t\t</div>
21597 \t)
21598 }"
21599 .unindent(),
21600 cx,
21601 )
21602 .await;
21603
21604 assert_indent_guides(
21605 0..8,
21606 vec![
21607 indent_guide(buffer_id, 1, 6, 0),
21608 indent_guide(buffer_id, 2, 5, 1),
21609 indent_guide(buffer_id, 4, 4, 2),
21610 ],
21611 None,
21612 &mut cx,
21613 );
21614}
21615
21616#[gpui::test]
21617async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21618 let (buffer_id, mut cx) = setup_indent_guides_editor(
21619 &"
21620 block1
21621
21622
21623
21624 block2
21625 "
21626 .unindent(),
21627 cx,
21628 )
21629 .await;
21630
21631 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21632}
21633
21634#[gpui::test]
21635async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21636 let (buffer_id, mut cx) = setup_indent_guides_editor(
21637 &"
21638 def a:
21639 \tb = 3
21640 \tif True:
21641 \t\tc = 4
21642 \t\td = 5
21643 \tprint(b)
21644 "
21645 .unindent(),
21646 cx,
21647 )
21648 .await;
21649
21650 assert_indent_guides(
21651 0..6,
21652 vec![
21653 indent_guide(buffer_id, 1, 5, 0),
21654 indent_guide(buffer_id, 3, 4, 1),
21655 ],
21656 None,
21657 &mut cx,
21658 );
21659}
21660
21661#[gpui::test]
21662async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21663 let (buffer_id, mut cx) = setup_indent_guides_editor(
21664 &"
21665 fn main() {
21666 let a = 1;
21667 }"
21668 .unindent(),
21669 cx,
21670 )
21671 .await;
21672
21673 cx.update_editor(|editor, window, cx| {
21674 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21675 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21676 });
21677 });
21678
21679 assert_indent_guides(
21680 0..3,
21681 vec![indent_guide(buffer_id, 1, 1, 0)],
21682 Some(vec![0]),
21683 &mut cx,
21684 );
21685}
21686
21687#[gpui::test]
21688async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21689 let (buffer_id, mut cx) = setup_indent_guides_editor(
21690 &"
21691 fn main() {
21692 if 1 == 2 {
21693 let a = 1;
21694 }
21695 }"
21696 .unindent(),
21697 cx,
21698 )
21699 .await;
21700
21701 cx.update_editor(|editor, window, cx| {
21702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21703 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21704 });
21705 });
21706
21707 assert_indent_guides(
21708 0..4,
21709 vec![
21710 indent_guide(buffer_id, 1, 3, 0),
21711 indent_guide(buffer_id, 2, 2, 1),
21712 ],
21713 Some(vec![1]),
21714 &mut cx,
21715 );
21716
21717 cx.update_editor(|editor, window, cx| {
21718 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21719 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21720 });
21721 });
21722
21723 assert_indent_guides(
21724 0..4,
21725 vec![
21726 indent_guide(buffer_id, 1, 3, 0),
21727 indent_guide(buffer_id, 2, 2, 1),
21728 ],
21729 Some(vec![1]),
21730 &mut cx,
21731 );
21732
21733 cx.update_editor(|editor, window, cx| {
21734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21735 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21736 });
21737 });
21738
21739 assert_indent_guides(
21740 0..4,
21741 vec![
21742 indent_guide(buffer_id, 1, 3, 0),
21743 indent_guide(buffer_id, 2, 2, 1),
21744 ],
21745 Some(vec![0]),
21746 &mut cx,
21747 );
21748}
21749
21750#[gpui::test]
21751async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21752 let (buffer_id, mut cx) = setup_indent_guides_editor(
21753 &"
21754 fn main() {
21755 let a = 1;
21756
21757 let b = 2;
21758 }"
21759 .unindent(),
21760 cx,
21761 )
21762 .await;
21763
21764 cx.update_editor(|editor, window, cx| {
21765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21766 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21767 });
21768 });
21769
21770 assert_indent_guides(
21771 0..5,
21772 vec![indent_guide(buffer_id, 1, 3, 0)],
21773 Some(vec![0]),
21774 &mut cx,
21775 );
21776}
21777
21778#[gpui::test]
21779async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21780 let (buffer_id, mut cx) = setup_indent_guides_editor(
21781 &"
21782 def m:
21783 a = 1
21784 pass"
21785 .unindent(),
21786 cx,
21787 )
21788 .await;
21789
21790 cx.update_editor(|editor, window, cx| {
21791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21792 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21793 });
21794 });
21795
21796 assert_indent_guides(
21797 0..3,
21798 vec![indent_guide(buffer_id, 1, 2, 0)],
21799 Some(vec![0]),
21800 &mut cx,
21801 );
21802}
21803
21804#[gpui::test]
21805async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21806 init_test(cx, |_| {});
21807 let mut cx = EditorTestContext::new(cx).await;
21808 let text = indoc! {
21809 "
21810 impl A {
21811 fn b() {
21812 0;
21813 3;
21814 5;
21815 6;
21816 7;
21817 }
21818 }
21819 "
21820 };
21821 let base_text = indoc! {
21822 "
21823 impl A {
21824 fn b() {
21825 0;
21826 1;
21827 2;
21828 3;
21829 4;
21830 }
21831 fn c() {
21832 5;
21833 6;
21834 7;
21835 }
21836 }
21837 "
21838 };
21839
21840 cx.update_editor(|editor, window, cx| {
21841 editor.set_text(text, window, cx);
21842
21843 editor.buffer().update(cx, |multibuffer, cx| {
21844 let buffer = multibuffer.as_singleton().unwrap();
21845 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21846
21847 multibuffer.set_all_diff_hunks_expanded(cx);
21848 multibuffer.add_diff(diff, cx);
21849
21850 buffer.read(cx).remote_id()
21851 })
21852 });
21853 cx.run_until_parked();
21854
21855 cx.assert_state_with_diff(
21856 indoc! { "
21857 impl A {
21858 fn b() {
21859 0;
21860 - 1;
21861 - 2;
21862 3;
21863 - 4;
21864 - }
21865 - fn c() {
21866 5;
21867 6;
21868 7;
21869 }
21870 }
21871 ˇ"
21872 }
21873 .to_string(),
21874 );
21875
21876 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21877 editor
21878 .snapshot(window, cx)
21879 .buffer_snapshot()
21880 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21881 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21882 .collect::<Vec<_>>()
21883 });
21884 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21885 assert_eq!(
21886 actual_guides,
21887 vec![
21888 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21889 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21890 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21891 ]
21892 );
21893}
21894
21895#[gpui::test]
21896async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21897 init_test(cx, |_| {});
21898 let mut cx = EditorTestContext::new(cx).await;
21899
21900 let diff_base = r#"
21901 a
21902 b
21903 c
21904 "#
21905 .unindent();
21906
21907 cx.set_state(
21908 &r#"
21909 ˇA
21910 b
21911 C
21912 "#
21913 .unindent(),
21914 );
21915 cx.set_head_text(&diff_base);
21916 cx.update_editor(|editor, window, cx| {
21917 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21918 });
21919 executor.run_until_parked();
21920
21921 let both_hunks_expanded = r#"
21922 - a
21923 + ˇA
21924 b
21925 - c
21926 + C
21927 "#
21928 .unindent();
21929
21930 cx.assert_state_with_diff(both_hunks_expanded.clone());
21931
21932 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21933 let snapshot = editor.snapshot(window, cx);
21934 let hunks = editor
21935 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21936 .collect::<Vec<_>>();
21937 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21938 hunks
21939 .into_iter()
21940 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21941 .collect::<Vec<_>>()
21942 });
21943 assert_eq!(hunk_ranges.len(), 2);
21944
21945 cx.update_editor(|editor, _, cx| {
21946 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21947 });
21948 executor.run_until_parked();
21949
21950 let second_hunk_expanded = r#"
21951 ˇA
21952 b
21953 - c
21954 + C
21955 "#
21956 .unindent();
21957
21958 cx.assert_state_with_diff(second_hunk_expanded);
21959
21960 cx.update_editor(|editor, _, cx| {
21961 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21962 });
21963 executor.run_until_parked();
21964
21965 cx.assert_state_with_diff(both_hunks_expanded.clone());
21966
21967 cx.update_editor(|editor, _, cx| {
21968 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21969 });
21970 executor.run_until_parked();
21971
21972 let first_hunk_expanded = r#"
21973 - a
21974 + ˇA
21975 b
21976 C
21977 "#
21978 .unindent();
21979
21980 cx.assert_state_with_diff(first_hunk_expanded);
21981
21982 cx.update_editor(|editor, _, cx| {
21983 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21984 });
21985 executor.run_until_parked();
21986
21987 cx.assert_state_with_diff(both_hunks_expanded);
21988
21989 cx.set_state(
21990 &r#"
21991 ˇA
21992 b
21993 "#
21994 .unindent(),
21995 );
21996 cx.run_until_parked();
21997
21998 // TODO this cursor position seems bad
21999 cx.assert_state_with_diff(
22000 r#"
22001 - ˇa
22002 + A
22003 b
22004 "#
22005 .unindent(),
22006 );
22007
22008 cx.update_editor(|editor, window, cx| {
22009 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22010 });
22011
22012 cx.assert_state_with_diff(
22013 r#"
22014 - ˇa
22015 + A
22016 b
22017 - c
22018 "#
22019 .unindent(),
22020 );
22021
22022 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22023 let snapshot = editor.snapshot(window, cx);
22024 let hunks = editor
22025 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22026 .collect::<Vec<_>>();
22027 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22028 hunks
22029 .into_iter()
22030 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22031 .collect::<Vec<_>>()
22032 });
22033 assert_eq!(hunk_ranges.len(), 2);
22034
22035 cx.update_editor(|editor, _, cx| {
22036 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22037 });
22038 executor.run_until_parked();
22039
22040 cx.assert_state_with_diff(
22041 r#"
22042 - ˇa
22043 + A
22044 b
22045 "#
22046 .unindent(),
22047 );
22048}
22049
22050#[gpui::test]
22051async fn test_toggle_deletion_hunk_at_start_of_file(
22052 executor: BackgroundExecutor,
22053 cx: &mut TestAppContext,
22054) {
22055 init_test(cx, |_| {});
22056 let mut cx = EditorTestContext::new(cx).await;
22057
22058 let diff_base = r#"
22059 a
22060 b
22061 c
22062 "#
22063 .unindent();
22064
22065 cx.set_state(
22066 &r#"
22067 ˇb
22068 c
22069 "#
22070 .unindent(),
22071 );
22072 cx.set_head_text(&diff_base);
22073 cx.update_editor(|editor, window, cx| {
22074 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22075 });
22076 executor.run_until_parked();
22077
22078 let hunk_expanded = r#"
22079 - a
22080 ˇb
22081 c
22082 "#
22083 .unindent();
22084
22085 cx.assert_state_with_diff(hunk_expanded.clone());
22086
22087 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22088 let snapshot = editor.snapshot(window, cx);
22089 let hunks = editor
22090 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22091 .collect::<Vec<_>>();
22092 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22093 hunks
22094 .into_iter()
22095 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22096 .collect::<Vec<_>>()
22097 });
22098 assert_eq!(hunk_ranges.len(), 1);
22099
22100 cx.update_editor(|editor, _, cx| {
22101 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22102 });
22103 executor.run_until_parked();
22104
22105 let hunk_collapsed = r#"
22106 ˇb
22107 c
22108 "#
22109 .unindent();
22110
22111 cx.assert_state_with_diff(hunk_collapsed);
22112
22113 cx.update_editor(|editor, _, cx| {
22114 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22115 });
22116 executor.run_until_parked();
22117
22118 cx.assert_state_with_diff(hunk_expanded);
22119}
22120
22121#[gpui::test]
22122async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22123 init_test(cx, |_| {});
22124
22125 let fs = FakeFs::new(cx.executor());
22126 fs.insert_tree(
22127 path!("/test"),
22128 json!({
22129 ".git": {},
22130 "file-1": "ONE\n",
22131 "file-2": "TWO\n",
22132 "file-3": "THREE\n",
22133 }),
22134 )
22135 .await;
22136
22137 fs.set_head_for_repo(
22138 path!("/test/.git").as_ref(),
22139 &[
22140 ("file-1", "one\n".into()),
22141 ("file-2", "two\n".into()),
22142 ("file-3", "three\n".into()),
22143 ],
22144 "deadbeef",
22145 );
22146
22147 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22148 let mut buffers = vec![];
22149 for i in 1..=3 {
22150 let buffer = project
22151 .update(cx, |project, cx| {
22152 let path = format!(path!("/test/file-{}"), i);
22153 project.open_local_buffer(path, cx)
22154 })
22155 .await
22156 .unwrap();
22157 buffers.push(buffer);
22158 }
22159
22160 let multibuffer = cx.new(|cx| {
22161 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22162 multibuffer.set_all_diff_hunks_expanded(cx);
22163 for buffer in &buffers {
22164 let snapshot = buffer.read(cx).snapshot();
22165 multibuffer.set_excerpts_for_path(
22166 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22167 buffer.clone(),
22168 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22169 2,
22170 cx,
22171 );
22172 }
22173 multibuffer
22174 });
22175
22176 let editor = cx.add_window(|window, cx| {
22177 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22178 });
22179 cx.run_until_parked();
22180
22181 let snapshot = editor
22182 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22183 .unwrap();
22184 let hunks = snapshot
22185 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22186 .map(|hunk| match hunk {
22187 DisplayDiffHunk::Unfolded {
22188 display_row_range, ..
22189 } => display_row_range,
22190 DisplayDiffHunk::Folded { .. } => unreachable!(),
22191 })
22192 .collect::<Vec<_>>();
22193 assert_eq!(
22194 hunks,
22195 [
22196 DisplayRow(2)..DisplayRow(4),
22197 DisplayRow(7)..DisplayRow(9),
22198 DisplayRow(12)..DisplayRow(14),
22199 ]
22200 );
22201}
22202
22203#[gpui::test]
22204async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22205 init_test(cx, |_| {});
22206
22207 let mut cx = EditorTestContext::new(cx).await;
22208 cx.set_head_text(indoc! { "
22209 one
22210 two
22211 three
22212 four
22213 five
22214 "
22215 });
22216 cx.set_index_text(indoc! { "
22217 one
22218 two
22219 three
22220 four
22221 five
22222 "
22223 });
22224 cx.set_state(indoc! {"
22225 one
22226 TWO
22227 ˇTHREE
22228 FOUR
22229 five
22230 "});
22231 cx.run_until_parked();
22232 cx.update_editor(|editor, window, cx| {
22233 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22234 });
22235 cx.run_until_parked();
22236 cx.assert_index_text(Some(indoc! {"
22237 one
22238 TWO
22239 THREE
22240 FOUR
22241 five
22242 "}));
22243 cx.set_state(indoc! { "
22244 one
22245 TWO
22246 ˇTHREE-HUNDRED
22247 FOUR
22248 five
22249 "});
22250 cx.run_until_parked();
22251 cx.update_editor(|editor, window, cx| {
22252 let snapshot = editor.snapshot(window, cx);
22253 let hunks = editor
22254 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22255 .collect::<Vec<_>>();
22256 assert_eq!(hunks.len(), 1);
22257 assert_eq!(
22258 hunks[0].status(),
22259 DiffHunkStatus {
22260 kind: DiffHunkStatusKind::Modified,
22261 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22262 }
22263 );
22264
22265 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22266 });
22267 cx.run_until_parked();
22268 cx.assert_index_text(Some(indoc! {"
22269 one
22270 TWO
22271 THREE-HUNDRED
22272 FOUR
22273 five
22274 "}));
22275}
22276
22277#[gpui::test]
22278fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22279 init_test(cx, |_| {});
22280
22281 let editor = cx.add_window(|window, cx| {
22282 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22283 build_editor(buffer, window, cx)
22284 });
22285
22286 let render_args = Arc::new(Mutex::new(None));
22287 let snapshot = editor
22288 .update(cx, |editor, window, cx| {
22289 let snapshot = editor.buffer().read(cx).snapshot(cx);
22290 let range =
22291 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22292
22293 struct RenderArgs {
22294 row: MultiBufferRow,
22295 folded: bool,
22296 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22297 }
22298
22299 let crease = Crease::inline(
22300 range,
22301 FoldPlaceholder::test(),
22302 {
22303 let toggle_callback = render_args.clone();
22304 move |row, folded, callback, _window, _cx| {
22305 *toggle_callback.lock() = Some(RenderArgs {
22306 row,
22307 folded,
22308 callback,
22309 });
22310 div()
22311 }
22312 },
22313 |_row, _folded, _window, _cx| div(),
22314 );
22315
22316 editor.insert_creases(Some(crease), cx);
22317 let snapshot = editor.snapshot(window, cx);
22318 let _div =
22319 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22320 snapshot
22321 })
22322 .unwrap();
22323
22324 let render_args = render_args.lock().take().unwrap();
22325 assert_eq!(render_args.row, MultiBufferRow(1));
22326 assert!(!render_args.folded);
22327 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22328
22329 cx.update_window(*editor, |_, window, cx| {
22330 (render_args.callback)(true, window, cx)
22331 })
22332 .unwrap();
22333 let snapshot = editor
22334 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22335 .unwrap();
22336 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22337
22338 cx.update_window(*editor, |_, window, cx| {
22339 (render_args.callback)(false, window, cx)
22340 })
22341 .unwrap();
22342 let snapshot = editor
22343 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22344 .unwrap();
22345 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22346}
22347
22348#[gpui::test]
22349async fn test_input_text(cx: &mut TestAppContext) {
22350 init_test(cx, |_| {});
22351 let mut cx = EditorTestContext::new(cx).await;
22352
22353 cx.set_state(
22354 &r#"ˇone
22355 two
22356
22357 three
22358 fourˇ
22359 five
22360
22361 siˇx"#
22362 .unindent(),
22363 );
22364
22365 cx.dispatch_action(HandleInput(String::new()));
22366 cx.assert_editor_state(
22367 &r#"ˇone
22368 two
22369
22370 three
22371 fourˇ
22372 five
22373
22374 siˇx"#
22375 .unindent(),
22376 );
22377
22378 cx.dispatch_action(HandleInput("AAAA".to_string()));
22379 cx.assert_editor_state(
22380 &r#"AAAAˇone
22381 two
22382
22383 three
22384 fourAAAAˇ
22385 five
22386
22387 siAAAAˇx"#
22388 .unindent(),
22389 );
22390}
22391
22392#[gpui::test]
22393async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22394 init_test(cx, |_| {});
22395
22396 let mut cx = EditorTestContext::new(cx).await;
22397 cx.set_state(
22398 r#"let foo = 1;
22399let foo = 2;
22400let foo = 3;
22401let fooˇ = 4;
22402let foo = 5;
22403let foo = 6;
22404let foo = 7;
22405let foo = 8;
22406let foo = 9;
22407let foo = 10;
22408let foo = 11;
22409let foo = 12;
22410let foo = 13;
22411let foo = 14;
22412let foo = 15;"#,
22413 );
22414
22415 cx.update_editor(|e, window, cx| {
22416 assert_eq!(
22417 e.next_scroll_position,
22418 NextScrollCursorCenterTopBottom::Center,
22419 "Default next scroll direction is center",
22420 );
22421
22422 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22423 assert_eq!(
22424 e.next_scroll_position,
22425 NextScrollCursorCenterTopBottom::Top,
22426 "After center, next scroll direction should be top",
22427 );
22428
22429 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22430 assert_eq!(
22431 e.next_scroll_position,
22432 NextScrollCursorCenterTopBottom::Bottom,
22433 "After top, next scroll direction should be bottom",
22434 );
22435
22436 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22437 assert_eq!(
22438 e.next_scroll_position,
22439 NextScrollCursorCenterTopBottom::Center,
22440 "After bottom, scrolling should start over",
22441 );
22442
22443 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22444 assert_eq!(
22445 e.next_scroll_position,
22446 NextScrollCursorCenterTopBottom::Top,
22447 "Scrolling continues if retriggered fast enough"
22448 );
22449 });
22450
22451 cx.executor()
22452 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22453 cx.executor().run_until_parked();
22454 cx.update_editor(|e, _, _| {
22455 assert_eq!(
22456 e.next_scroll_position,
22457 NextScrollCursorCenterTopBottom::Center,
22458 "If scrolling is not triggered fast enough, it should reset"
22459 );
22460 });
22461}
22462
22463#[gpui::test]
22464async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22465 init_test(cx, |_| {});
22466 let mut cx = EditorLspTestContext::new_rust(
22467 lsp::ServerCapabilities {
22468 definition_provider: Some(lsp::OneOf::Left(true)),
22469 references_provider: Some(lsp::OneOf::Left(true)),
22470 ..lsp::ServerCapabilities::default()
22471 },
22472 cx,
22473 )
22474 .await;
22475
22476 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22477 let go_to_definition = cx
22478 .lsp
22479 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22480 move |params, _| async move {
22481 if empty_go_to_definition {
22482 Ok(None)
22483 } else {
22484 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22485 uri: params.text_document_position_params.text_document.uri,
22486 range: lsp::Range::new(
22487 lsp::Position::new(4, 3),
22488 lsp::Position::new(4, 6),
22489 ),
22490 })))
22491 }
22492 },
22493 );
22494 let references = cx
22495 .lsp
22496 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22497 Ok(Some(vec![lsp::Location {
22498 uri: params.text_document_position.text_document.uri,
22499 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22500 }]))
22501 });
22502 (go_to_definition, references)
22503 };
22504
22505 cx.set_state(
22506 &r#"fn one() {
22507 let mut a = ˇtwo();
22508 }
22509
22510 fn two() {}"#
22511 .unindent(),
22512 );
22513 set_up_lsp_handlers(false, &mut cx);
22514 let navigated = cx
22515 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22516 .await
22517 .expect("Failed to navigate to definition");
22518 assert_eq!(
22519 navigated,
22520 Navigated::Yes,
22521 "Should have navigated to definition from the GetDefinition response"
22522 );
22523 cx.assert_editor_state(
22524 &r#"fn one() {
22525 let mut a = two();
22526 }
22527
22528 fn «twoˇ»() {}"#
22529 .unindent(),
22530 );
22531
22532 let editors = cx.update_workspace(|workspace, _, cx| {
22533 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22534 });
22535 cx.update_editor(|_, _, test_editor_cx| {
22536 assert_eq!(
22537 editors.len(),
22538 1,
22539 "Initially, only one, test, editor should be open in the workspace"
22540 );
22541 assert_eq!(
22542 test_editor_cx.entity(),
22543 editors.last().expect("Asserted len is 1").clone()
22544 );
22545 });
22546
22547 set_up_lsp_handlers(true, &mut cx);
22548 let navigated = cx
22549 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22550 .await
22551 .expect("Failed to navigate to lookup references");
22552 assert_eq!(
22553 navigated,
22554 Navigated::Yes,
22555 "Should have navigated to references as a fallback after empty GoToDefinition response"
22556 );
22557 // We should not change the selections in the existing file,
22558 // if opening another milti buffer with the references
22559 cx.assert_editor_state(
22560 &r#"fn one() {
22561 let mut a = two();
22562 }
22563
22564 fn «twoˇ»() {}"#
22565 .unindent(),
22566 );
22567 let editors = cx.update_workspace(|workspace, _, cx| {
22568 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22569 });
22570 cx.update_editor(|_, _, test_editor_cx| {
22571 assert_eq!(
22572 editors.len(),
22573 2,
22574 "After falling back to references search, we open a new editor with the results"
22575 );
22576 let references_fallback_text = editors
22577 .into_iter()
22578 .find(|new_editor| *new_editor != test_editor_cx.entity())
22579 .expect("Should have one non-test editor now")
22580 .read(test_editor_cx)
22581 .text(test_editor_cx);
22582 assert_eq!(
22583 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22584 "Should use the range from the references response and not the GoToDefinition one"
22585 );
22586 });
22587}
22588
22589#[gpui::test]
22590async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22591 init_test(cx, |_| {});
22592 cx.update(|cx| {
22593 let mut editor_settings = EditorSettings::get_global(cx).clone();
22594 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22595 EditorSettings::override_global(editor_settings, cx);
22596 });
22597 let mut cx = EditorLspTestContext::new_rust(
22598 lsp::ServerCapabilities {
22599 definition_provider: Some(lsp::OneOf::Left(true)),
22600 references_provider: Some(lsp::OneOf::Left(true)),
22601 ..lsp::ServerCapabilities::default()
22602 },
22603 cx,
22604 )
22605 .await;
22606 let original_state = r#"fn one() {
22607 let mut a = ˇtwo();
22608 }
22609
22610 fn two() {}"#
22611 .unindent();
22612 cx.set_state(&original_state);
22613
22614 let mut go_to_definition = cx
22615 .lsp
22616 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22617 move |_, _| async move { Ok(None) },
22618 );
22619 let _references = cx
22620 .lsp
22621 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22622 panic!("Should not call for references with no go to definition fallback")
22623 });
22624
22625 let navigated = cx
22626 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22627 .await
22628 .expect("Failed to navigate to lookup references");
22629 go_to_definition
22630 .next()
22631 .await
22632 .expect("Should have called the go_to_definition handler");
22633
22634 assert_eq!(
22635 navigated,
22636 Navigated::No,
22637 "Should have navigated to references as a fallback after empty GoToDefinition response"
22638 );
22639 cx.assert_editor_state(&original_state);
22640 let editors = cx.update_workspace(|workspace, _, cx| {
22641 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22642 });
22643 cx.update_editor(|_, _, _| {
22644 assert_eq!(
22645 editors.len(),
22646 1,
22647 "After unsuccessful fallback, no other editor should have been opened"
22648 );
22649 });
22650}
22651
22652#[gpui::test]
22653async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22654 init_test(cx, |_| {});
22655 let mut cx = EditorLspTestContext::new_rust(
22656 lsp::ServerCapabilities {
22657 references_provider: Some(lsp::OneOf::Left(true)),
22658 ..lsp::ServerCapabilities::default()
22659 },
22660 cx,
22661 )
22662 .await;
22663
22664 cx.set_state(
22665 &r#"
22666 fn one() {
22667 let mut a = two();
22668 }
22669
22670 fn ˇtwo() {}"#
22671 .unindent(),
22672 );
22673 cx.lsp
22674 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22675 Ok(Some(vec![
22676 lsp::Location {
22677 uri: params.text_document_position.text_document.uri.clone(),
22678 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22679 },
22680 lsp::Location {
22681 uri: params.text_document_position.text_document.uri,
22682 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22683 },
22684 ]))
22685 });
22686 let navigated = cx
22687 .update_editor(|editor, window, cx| {
22688 editor.find_all_references(&FindAllReferences::default(), window, cx)
22689 })
22690 .unwrap()
22691 .await
22692 .expect("Failed to navigate to references");
22693 assert_eq!(
22694 navigated,
22695 Navigated::Yes,
22696 "Should have navigated to references from the FindAllReferences response"
22697 );
22698 cx.assert_editor_state(
22699 &r#"fn one() {
22700 let mut a = two();
22701 }
22702
22703 fn ˇtwo() {}"#
22704 .unindent(),
22705 );
22706
22707 let editors = cx.update_workspace(|workspace, _, cx| {
22708 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22709 });
22710 cx.update_editor(|_, _, _| {
22711 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22712 });
22713
22714 cx.set_state(
22715 &r#"fn one() {
22716 let mut a = ˇtwo();
22717 }
22718
22719 fn two() {}"#
22720 .unindent(),
22721 );
22722 let navigated = cx
22723 .update_editor(|editor, window, cx| {
22724 editor.find_all_references(&FindAllReferences::default(), window, cx)
22725 })
22726 .unwrap()
22727 .await
22728 .expect("Failed to navigate to references");
22729 assert_eq!(
22730 navigated,
22731 Navigated::Yes,
22732 "Should have navigated to references from the FindAllReferences response"
22733 );
22734 cx.assert_editor_state(
22735 &r#"fn one() {
22736 let mut a = ˇtwo();
22737 }
22738
22739 fn two() {}"#
22740 .unindent(),
22741 );
22742 let editors = cx.update_workspace(|workspace, _, cx| {
22743 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22744 });
22745 cx.update_editor(|_, _, _| {
22746 assert_eq!(
22747 editors.len(),
22748 2,
22749 "should have re-used the previous multibuffer"
22750 );
22751 });
22752
22753 cx.set_state(
22754 &r#"fn one() {
22755 let mut a = ˇtwo();
22756 }
22757 fn three() {}
22758 fn two() {}"#
22759 .unindent(),
22760 );
22761 cx.lsp
22762 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22763 Ok(Some(vec![
22764 lsp::Location {
22765 uri: params.text_document_position.text_document.uri.clone(),
22766 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22767 },
22768 lsp::Location {
22769 uri: params.text_document_position.text_document.uri,
22770 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22771 },
22772 ]))
22773 });
22774 let navigated = cx
22775 .update_editor(|editor, window, cx| {
22776 editor.find_all_references(&FindAllReferences::default(), window, cx)
22777 })
22778 .unwrap()
22779 .await
22780 .expect("Failed to navigate to references");
22781 assert_eq!(
22782 navigated,
22783 Navigated::Yes,
22784 "Should have navigated to references from the FindAllReferences response"
22785 );
22786 cx.assert_editor_state(
22787 &r#"fn one() {
22788 let mut a = ˇtwo();
22789 }
22790 fn three() {}
22791 fn two() {}"#
22792 .unindent(),
22793 );
22794 let editors = cx.update_workspace(|workspace, _, cx| {
22795 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22796 });
22797 cx.update_editor(|_, _, _| {
22798 assert_eq!(
22799 editors.len(),
22800 3,
22801 "should have used a new multibuffer as offsets changed"
22802 );
22803 });
22804}
22805#[gpui::test]
22806async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22807 init_test(cx, |_| {});
22808
22809 let language = Arc::new(Language::new(
22810 LanguageConfig::default(),
22811 Some(tree_sitter_rust::LANGUAGE.into()),
22812 ));
22813
22814 let text = r#"
22815 #[cfg(test)]
22816 mod tests() {
22817 #[test]
22818 fn runnable_1() {
22819 let a = 1;
22820 }
22821
22822 #[test]
22823 fn runnable_2() {
22824 let a = 1;
22825 let b = 2;
22826 }
22827 }
22828 "#
22829 .unindent();
22830
22831 let fs = FakeFs::new(cx.executor());
22832 fs.insert_file("/file.rs", Default::default()).await;
22833
22834 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22835 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22836 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22837 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22838 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22839
22840 let editor = cx.new_window_entity(|window, cx| {
22841 Editor::new(
22842 EditorMode::full(),
22843 multi_buffer,
22844 Some(project.clone()),
22845 window,
22846 cx,
22847 )
22848 });
22849
22850 editor.update_in(cx, |editor, window, cx| {
22851 let snapshot = editor.buffer().read(cx).snapshot(cx);
22852 editor.tasks.insert(
22853 (buffer.read(cx).remote_id(), 3),
22854 RunnableTasks {
22855 templates: vec![],
22856 offset: snapshot.anchor_before(MultiBufferOffset(43)),
22857 column: 0,
22858 extra_variables: HashMap::default(),
22859 context_range: BufferOffset(43)..BufferOffset(85),
22860 },
22861 );
22862 editor.tasks.insert(
22863 (buffer.read(cx).remote_id(), 8),
22864 RunnableTasks {
22865 templates: vec![],
22866 offset: snapshot.anchor_before(MultiBufferOffset(86)),
22867 column: 0,
22868 extra_variables: HashMap::default(),
22869 context_range: BufferOffset(86)..BufferOffset(191),
22870 },
22871 );
22872
22873 // Test finding task when cursor is inside function body
22874 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22875 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22876 });
22877 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22878 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22879
22880 // Test finding task when cursor is on function name
22881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22882 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22883 });
22884 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22885 assert_eq!(row, 8, "Should find task when cursor is on function name");
22886 });
22887}
22888
22889#[gpui::test]
22890async fn test_folding_buffers(cx: &mut TestAppContext) {
22891 init_test(cx, |_| {});
22892
22893 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22894 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22895 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22896
22897 let fs = FakeFs::new(cx.executor());
22898 fs.insert_tree(
22899 path!("/a"),
22900 json!({
22901 "first.rs": sample_text_1,
22902 "second.rs": sample_text_2,
22903 "third.rs": sample_text_3,
22904 }),
22905 )
22906 .await;
22907 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22908 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22909 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22910 let worktree = project.update(cx, |project, cx| {
22911 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22912 assert_eq!(worktrees.len(), 1);
22913 worktrees.pop().unwrap()
22914 });
22915 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22916
22917 let buffer_1 = project
22918 .update(cx, |project, cx| {
22919 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22920 })
22921 .await
22922 .unwrap();
22923 let buffer_2 = project
22924 .update(cx, |project, cx| {
22925 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22926 })
22927 .await
22928 .unwrap();
22929 let buffer_3 = project
22930 .update(cx, |project, cx| {
22931 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22932 })
22933 .await
22934 .unwrap();
22935
22936 let multi_buffer = cx.new(|cx| {
22937 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22938 multi_buffer.push_excerpts(
22939 buffer_1.clone(),
22940 [
22941 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22942 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22943 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22944 ],
22945 cx,
22946 );
22947 multi_buffer.push_excerpts(
22948 buffer_2.clone(),
22949 [
22950 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22951 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22952 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22953 ],
22954 cx,
22955 );
22956 multi_buffer.push_excerpts(
22957 buffer_3.clone(),
22958 [
22959 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22960 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22961 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22962 ],
22963 cx,
22964 );
22965 multi_buffer
22966 });
22967 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22968 Editor::new(
22969 EditorMode::full(),
22970 multi_buffer.clone(),
22971 Some(project.clone()),
22972 window,
22973 cx,
22974 )
22975 });
22976
22977 assert_eq!(
22978 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22979 "\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",
22980 );
22981
22982 multi_buffer_editor.update(cx, |editor, cx| {
22983 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22984 });
22985 assert_eq!(
22986 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22987 "\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",
22988 "After folding the first buffer, its text should not be displayed"
22989 );
22990
22991 multi_buffer_editor.update(cx, |editor, cx| {
22992 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22993 });
22994 assert_eq!(
22995 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22996 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22997 "After folding the second buffer, its text should not be displayed"
22998 );
22999
23000 multi_buffer_editor.update(cx, |editor, cx| {
23001 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23002 });
23003 assert_eq!(
23004 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23005 "\n\n\n\n\n",
23006 "After folding the third buffer, its text should not be displayed"
23007 );
23008
23009 // Emulate selection inside the fold logic, that should work
23010 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23011 editor
23012 .snapshot(window, cx)
23013 .next_line_boundary(Point::new(0, 4));
23014 });
23015
23016 multi_buffer_editor.update(cx, |editor, cx| {
23017 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23018 });
23019 assert_eq!(
23020 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23021 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23022 "After unfolding the second buffer, its text should be displayed"
23023 );
23024
23025 // Typing inside of buffer 1 causes that buffer to be unfolded.
23026 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23027 assert_eq!(
23028 multi_buffer
23029 .read(cx)
23030 .snapshot(cx)
23031 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23032 .collect::<String>(),
23033 "bbbb"
23034 );
23035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23036 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23037 });
23038 editor.handle_input("B", window, cx);
23039 });
23040
23041 assert_eq!(
23042 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23043 "\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",
23044 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23045 );
23046
23047 multi_buffer_editor.update(cx, |editor, cx| {
23048 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23049 });
23050 assert_eq!(
23051 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23052 "\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",
23053 "After unfolding the all buffers, all original text should be displayed"
23054 );
23055}
23056
23057#[gpui::test]
23058async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23059 init_test(cx, |_| {});
23060
23061 let sample_text_1 = "1111\n2222\n3333".to_string();
23062 let sample_text_2 = "4444\n5555\n6666".to_string();
23063 let sample_text_3 = "7777\n8888\n9999".to_string();
23064
23065 let fs = FakeFs::new(cx.executor());
23066 fs.insert_tree(
23067 path!("/a"),
23068 json!({
23069 "first.rs": sample_text_1,
23070 "second.rs": sample_text_2,
23071 "third.rs": sample_text_3,
23072 }),
23073 )
23074 .await;
23075 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23076 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23077 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23078 let worktree = project.update(cx, |project, cx| {
23079 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23080 assert_eq!(worktrees.len(), 1);
23081 worktrees.pop().unwrap()
23082 });
23083 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23084
23085 let buffer_1 = project
23086 .update(cx, |project, cx| {
23087 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23088 })
23089 .await
23090 .unwrap();
23091 let buffer_2 = project
23092 .update(cx, |project, cx| {
23093 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23094 })
23095 .await
23096 .unwrap();
23097 let buffer_3 = project
23098 .update(cx, |project, cx| {
23099 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23100 })
23101 .await
23102 .unwrap();
23103
23104 let multi_buffer = cx.new(|cx| {
23105 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23106 multi_buffer.push_excerpts(
23107 buffer_1.clone(),
23108 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23109 cx,
23110 );
23111 multi_buffer.push_excerpts(
23112 buffer_2.clone(),
23113 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23114 cx,
23115 );
23116 multi_buffer.push_excerpts(
23117 buffer_3.clone(),
23118 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23119 cx,
23120 );
23121 multi_buffer
23122 });
23123
23124 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23125 Editor::new(
23126 EditorMode::full(),
23127 multi_buffer,
23128 Some(project.clone()),
23129 window,
23130 cx,
23131 )
23132 });
23133
23134 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23135 assert_eq!(
23136 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23137 full_text,
23138 );
23139
23140 multi_buffer_editor.update(cx, |editor, cx| {
23141 editor.fold_buffer(buffer_1.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\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23146 "After folding the first buffer, its text should not be displayed"
23147 );
23148
23149 multi_buffer_editor.update(cx, |editor, cx| {
23150 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23151 });
23152
23153 assert_eq!(
23154 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23155 "\n\n\n\n\n\n7777\n8888\n9999",
23156 "After folding the second buffer, its text should not be displayed"
23157 );
23158
23159 multi_buffer_editor.update(cx, |editor, cx| {
23160 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23161 });
23162 assert_eq!(
23163 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23164 "\n\n\n\n\n",
23165 "After folding the third buffer, its text should not be displayed"
23166 );
23167
23168 multi_buffer_editor.update(cx, |editor, cx| {
23169 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23170 });
23171 assert_eq!(
23172 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23173 "\n\n\n\n4444\n5555\n6666\n\n",
23174 "After unfolding the second buffer, its text should be displayed"
23175 );
23176
23177 multi_buffer_editor.update(cx, |editor, cx| {
23178 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23179 });
23180 assert_eq!(
23181 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23182 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23183 "After unfolding the first buffer, its text should be displayed"
23184 );
23185
23186 multi_buffer_editor.update(cx, |editor, cx| {
23187 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23188 });
23189 assert_eq!(
23190 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23191 full_text,
23192 "After unfolding all buffers, all original text should be displayed"
23193 );
23194}
23195
23196#[gpui::test]
23197async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23198 init_test(cx, |_| {});
23199
23200 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23201
23202 let fs = FakeFs::new(cx.executor());
23203 fs.insert_tree(
23204 path!("/a"),
23205 json!({
23206 "main.rs": sample_text,
23207 }),
23208 )
23209 .await;
23210 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23211 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23212 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23213 let worktree = project.update(cx, |project, cx| {
23214 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23215 assert_eq!(worktrees.len(), 1);
23216 worktrees.pop().unwrap()
23217 });
23218 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23219
23220 let buffer_1 = project
23221 .update(cx, |project, cx| {
23222 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23223 })
23224 .await
23225 .unwrap();
23226
23227 let multi_buffer = cx.new(|cx| {
23228 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23229 multi_buffer.push_excerpts(
23230 buffer_1.clone(),
23231 [ExcerptRange::new(
23232 Point::new(0, 0)
23233 ..Point::new(
23234 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23235 0,
23236 ),
23237 )],
23238 cx,
23239 );
23240 multi_buffer
23241 });
23242 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23243 Editor::new(
23244 EditorMode::full(),
23245 multi_buffer,
23246 Some(project.clone()),
23247 window,
23248 cx,
23249 )
23250 });
23251
23252 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23253 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23254 enum TestHighlight {}
23255 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23256 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23257 editor.highlight_text::<TestHighlight>(
23258 vec![highlight_range.clone()],
23259 HighlightStyle::color(Hsla::green()),
23260 cx,
23261 );
23262 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23263 s.select_ranges(Some(highlight_range))
23264 });
23265 });
23266
23267 let full_text = format!("\n\n{sample_text}");
23268 assert_eq!(
23269 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23270 full_text,
23271 );
23272}
23273
23274#[gpui::test]
23275async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23276 init_test(cx, |_| {});
23277 cx.update(|cx| {
23278 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23279 "keymaps/default-linux.json",
23280 cx,
23281 )
23282 .unwrap();
23283 cx.bind_keys(default_key_bindings);
23284 });
23285
23286 let (editor, cx) = cx.add_window_view(|window, cx| {
23287 let multi_buffer = MultiBuffer::build_multi(
23288 [
23289 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23290 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23291 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23292 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23293 ],
23294 cx,
23295 );
23296 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23297
23298 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23299 // fold all but the second buffer, so that we test navigating between two
23300 // adjacent folded buffers, as well as folded buffers at the start and
23301 // end the multibuffer
23302 editor.fold_buffer(buffer_ids[0], cx);
23303 editor.fold_buffer(buffer_ids[2], cx);
23304 editor.fold_buffer(buffer_ids[3], cx);
23305
23306 editor
23307 });
23308 cx.simulate_resize(size(px(1000.), px(1000.)));
23309
23310 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23311 cx.assert_excerpts_with_selections(indoc! {"
23312 [EXCERPT]
23313 ˇ[FOLDED]
23314 [EXCERPT]
23315 a1
23316 b1
23317 [EXCERPT]
23318 [FOLDED]
23319 [EXCERPT]
23320 [FOLDED]
23321 "
23322 });
23323 cx.simulate_keystroke("down");
23324 cx.assert_excerpts_with_selections(indoc! {"
23325 [EXCERPT]
23326 [FOLDED]
23327 [EXCERPT]
23328 ˇa1
23329 b1
23330 [EXCERPT]
23331 [FOLDED]
23332 [EXCERPT]
23333 [FOLDED]
23334 "
23335 });
23336 cx.simulate_keystroke("down");
23337 cx.assert_excerpts_with_selections(indoc! {"
23338 [EXCERPT]
23339 [FOLDED]
23340 [EXCERPT]
23341 a1
23342 ˇb1
23343 [EXCERPT]
23344 [FOLDED]
23345 [EXCERPT]
23346 [FOLDED]
23347 "
23348 });
23349 cx.simulate_keystroke("down");
23350 cx.assert_excerpts_with_selections(indoc! {"
23351 [EXCERPT]
23352 [FOLDED]
23353 [EXCERPT]
23354 a1
23355 b1
23356 ˇ[EXCERPT]
23357 [FOLDED]
23358 [EXCERPT]
23359 [FOLDED]
23360 "
23361 });
23362 cx.simulate_keystroke("down");
23363 cx.assert_excerpts_with_selections(indoc! {"
23364 [EXCERPT]
23365 [FOLDED]
23366 [EXCERPT]
23367 a1
23368 b1
23369 [EXCERPT]
23370 ˇ[FOLDED]
23371 [EXCERPT]
23372 [FOLDED]
23373 "
23374 });
23375 for _ in 0..5 {
23376 cx.simulate_keystroke("down");
23377 cx.assert_excerpts_with_selections(indoc! {"
23378 [EXCERPT]
23379 [FOLDED]
23380 [EXCERPT]
23381 a1
23382 b1
23383 [EXCERPT]
23384 [FOLDED]
23385 [EXCERPT]
23386 ˇ[FOLDED]
23387 "
23388 });
23389 }
23390
23391 cx.simulate_keystroke("up");
23392 cx.assert_excerpts_with_selections(indoc! {"
23393 [EXCERPT]
23394 [FOLDED]
23395 [EXCERPT]
23396 a1
23397 b1
23398 [EXCERPT]
23399 ˇ[FOLDED]
23400 [EXCERPT]
23401 [FOLDED]
23402 "
23403 });
23404 cx.simulate_keystroke("up");
23405 cx.assert_excerpts_with_selections(indoc! {"
23406 [EXCERPT]
23407 [FOLDED]
23408 [EXCERPT]
23409 a1
23410 b1
23411 ˇ[EXCERPT]
23412 [FOLDED]
23413 [EXCERPT]
23414 [FOLDED]
23415 "
23416 });
23417 cx.simulate_keystroke("up");
23418 cx.assert_excerpts_with_selections(indoc! {"
23419 [EXCERPT]
23420 [FOLDED]
23421 [EXCERPT]
23422 a1
23423 ˇb1
23424 [EXCERPT]
23425 [FOLDED]
23426 [EXCERPT]
23427 [FOLDED]
23428 "
23429 });
23430 cx.simulate_keystroke("up");
23431 cx.assert_excerpts_with_selections(indoc! {"
23432 [EXCERPT]
23433 [FOLDED]
23434 [EXCERPT]
23435 ˇa1
23436 b1
23437 [EXCERPT]
23438 [FOLDED]
23439 [EXCERPT]
23440 [FOLDED]
23441 "
23442 });
23443 for _ in 0..5 {
23444 cx.simulate_keystroke("up");
23445 cx.assert_excerpts_with_selections(indoc! {"
23446 [EXCERPT]
23447 ˇ[FOLDED]
23448 [EXCERPT]
23449 a1
23450 b1
23451 [EXCERPT]
23452 [FOLDED]
23453 [EXCERPT]
23454 [FOLDED]
23455 "
23456 });
23457 }
23458}
23459
23460#[gpui::test]
23461async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23462 init_test(cx, |_| {});
23463
23464 // Simple insertion
23465 assert_highlighted_edits(
23466 "Hello, world!",
23467 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23468 true,
23469 cx,
23470 |highlighted_edits, cx| {
23471 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23472 assert_eq!(highlighted_edits.highlights.len(), 1);
23473 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23474 assert_eq!(
23475 highlighted_edits.highlights[0].1.background_color,
23476 Some(cx.theme().status().created_background)
23477 );
23478 },
23479 )
23480 .await;
23481
23482 // Replacement
23483 assert_highlighted_edits(
23484 "This is a test.",
23485 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23486 false,
23487 cx,
23488 |highlighted_edits, cx| {
23489 assert_eq!(highlighted_edits.text, "That is a test.");
23490 assert_eq!(highlighted_edits.highlights.len(), 1);
23491 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23492 assert_eq!(
23493 highlighted_edits.highlights[0].1.background_color,
23494 Some(cx.theme().status().created_background)
23495 );
23496 },
23497 )
23498 .await;
23499
23500 // Multiple edits
23501 assert_highlighted_edits(
23502 "Hello, world!",
23503 vec![
23504 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23505 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23506 ],
23507 false,
23508 cx,
23509 |highlighted_edits, cx| {
23510 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23511 assert_eq!(highlighted_edits.highlights.len(), 2);
23512 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23513 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23514 assert_eq!(
23515 highlighted_edits.highlights[0].1.background_color,
23516 Some(cx.theme().status().created_background)
23517 );
23518 assert_eq!(
23519 highlighted_edits.highlights[1].1.background_color,
23520 Some(cx.theme().status().created_background)
23521 );
23522 },
23523 )
23524 .await;
23525
23526 // Multiple lines with edits
23527 assert_highlighted_edits(
23528 "First line\nSecond line\nThird line\nFourth line",
23529 vec![
23530 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23531 (
23532 Point::new(2, 0)..Point::new(2, 10),
23533 "New third line".to_string(),
23534 ),
23535 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23536 ],
23537 false,
23538 cx,
23539 |highlighted_edits, cx| {
23540 assert_eq!(
23541 highlighted_edits.text,
23542 "Second modified\nNew third line\nFourth updated line"
23543 );
23544 assert_eq!(highlighted_edits.highlights.len(), 3);
23545 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23546 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23547 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23548 for highlight in &highlighted_edits.highlights {
23549 assert_eq!(
23550 highlight.1.background_color,
23551 Some(cx.theme().status().created_background)
23552 );
23553 }
23554 },
23555 )
23556 .await;
23557}
23558
23559#[gpui::test]
23560async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23561 init_test(cx, |_| {});
23562
23563 // Deletion
23564 assert_highlighted_edits(
23565 "Hello, world!",
23566 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23567 true,
23568 cx,
23569 |highlighted_edits, cx| {
23570 assert_eq!(highlighted_edits.text, "Hello, world!");
23571 assert_eq!(highlighted_edits.highlights.len(), 1);
23572 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23573 assert_eq!(
23574 highlighted_edits.highlights[0].1.background_color,
23575 Some(cx.theme().status().deleted_background)
23576 );
23577 },
23578 )
23579 .await;
23580
23581 // Insertion
23582 assert_highlighted_edits(
23583 "Hello, world!",
23584 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23585 true,
23586 cx,
23587 |highlighted_edits, cx| {
23588 assert_eq!(highlighted_edits.highlights.len(), 1);
23589 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23590 assert_eq!(
23591 highlighted_edits.highlights[0].1.background_color,
23592 Some(cx.theme().status().created_background)
23593 );
23594 },
23595 )
23596 .await;
23597}
23598
23599async fn assert_highlighted_edits(
23600 text: &str,
23601 edits: Vec<(Range<Point>, String)>,
23602 include_deletions: bool,
23603 cx: &mut TestAppContext,
23604 assertion_fn: impl Fn(HighlightedText, &App),
23605) {
23606 let window = cx.add_window(|window, cx| {
23607 let buffer = MultiBuffer::build_simple(text, cx);
23608 Editor::new(EditorMode::full(), buffer, None, window, cx)
23609 });
23610 let cx = &mut VisualTestContext::from_window(*window, cx);
23611
23612 let (buffer, snapshot) = window
23613 .update(cx, |editor, _window, cx| {
23614 (
23615 editor.buffer().clone(),
23616 editor.buffer().read(cx).snapshot(cx),
23617 )
23618 })
23619 .unwrap();
23620
23621 let edits = edits
23622 .into_iter()
23623 .map(|(range, edit)| {
23624 (
23625 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23626 edit,
23627 )
23628 })
23629 .collect::<Vec<_>>();
23630
23631 let text_anchor_edits = edits
23632 .clone()
23633 .into_iter()
23634 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23635 .collect::<Vec<_>>();
23636
23637 let edit_preview = window
23638 .update(cx, |_, _window, cx| {
23639 buffer
23640 .read(cx)
23641 .as_singleton()
23642 .unwrap()
23643 .read(cx)
23644 .preview_edits(text_anchor_edits.into(), cx)
23645 })
23646 .unwrap()
23647 .await;
23648
23649 cx.update(|_window, cx| {
23650 let highlighted_edits = edit_prediction_edit_text(
23651 snapshot.as_singleton().unwrap().2,
23652 &edits,
23653 &edit_preview,
23654 include_deletions,
23655 cx,
23656 );
23657 assertion_fn(highlighted_edits, cx)
23658 });
23659}
23660
23661#[track_caller]
23662fn assert_breakpoint(
23663 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23664 path: &Arc<Path>,
23665 expected: Vec<(u32, Breakpoint)>,
23666) {
23667 if expected.is_empty() {
23668 assert!(!breakpoints.contains_key(path), "{}", path.display());
23669 } else {
23670 let mut breakpoint = breakpoints
23671 .get(path)
23672 .unwrap()
23673 .iter()
23674 .map(|breakpoint| {
23675 (
23676 breakpoint.row,
23677 Breakpoint {
23678 message: breakpoint.message.clone(),
23679 state: breakpoint.state,
23680 condition: breakpoint.condition.clone(),
23681 hit_condition: breakpoint.hit_condition.clone(),
23682 },
23683 )
23684 })
23685 .collect::<Vec<_>>();
23686
23687 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23688
23689 assert_eq!(expected, breakpoint);
23690 }
23691}
23692
23693fn add_log_breakpoint_at_cursor(
23694 editor: &mut Editor,
23695 log_message: &str,
23696 window: &mut Window,
23697 cx: &mut Context<Editor>,
23698) {
23699 let (anchor, bp) = editor
23700 .breakpoints_at_cursors(window, cx)
23701 .first()
23702 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23703 .unwrap_or_else(|| {
23704 let snapshot = editor.snapshot(window, cx);
23705 let cursor_position: Point =
23706 editor.selections.newest(&snapshot.display_snapshot).head();
23707
23708 let breakpoint_position = snapshot
23709 .buffer_snapshot()
23710 .anchor_before(Point::new(cursor_position.row, 0));
23711
23712 (breakpoint_position, Breakpoint::new_log(log_message))
23713 });
23714
23715 editor.edit_breakpoint_at_anchor(
23716 anchor,
23717 bp,
23718 BreakpointEditAction::EditLogMessage(log_message.into()),
23719 cx,
23720 );
23721}
23722
23723#[gpui::test]
23724async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23725 init_test(cx, |_| {});
23726
23727 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23728 let fs = FakeFs::new(cx.executor());
23729 fs.insert_tree(
23730 path!("/a"),
23731 json!({
23732 "main.rs": sample_text,
23733 }),
23734 )
23735 .await;
23736 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23737 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23738 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23739
23740 let fs = FakeFs::new(cx.executor());
23741 fs.insert_tree(
23742 path!("/a"),
23743 json!({
23744 "main.rs": sample_text,
23745 }),
23746 )
23747 .await;
23748 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23749 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23750 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23751 let worktree_id = workspace
23752 .update(cx, |workspace, _window, cx| {
23753 workspace.project().update(cx, |project, cx| {
23754 project.worktrees(cx).next().unwrap().read(cx).id()
23755 })
23756 })
23757 .unwrap();
23758
23759 let buffer = project
23760 .update(cx, |project, cx| {
23761 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23762 })
23763 .await
23764 .unwrap();
23765
23766 let (editor, cx) = cx.add_window_view(|window, cx| {
23767 Editor::new(
23768 EditorMode::full(),
23769 MultiBuffer::build_from_buffer(buffer, cx),
23770 Some(project.clone()),
23771 window,
23772 cx,
23773 )
23774 });
23775
23776 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23777 let abs_path = project.read_with(cx, |project, cx| {
23778 project
23779 .absolute_path(&project_path, cx)
23780 .map(Arc::from)
23781 .unwrap()
23782 });
23783
23784 // assert we can add breakpoint on the first line
23785 editor.update_in(cx, |editor, window, cx| {
23786 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23787 editor.move_to_end(&MoveToEnd, window, cx);
23788 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23789 });
23790
23791 let breakpoints = editor.update(cx, |editor, cx| {
23792 editor
23793 .breakpoint_store()
23794 .as_ref()
23795 .unwrap()
23796 .read(cx)
23797 .all_source_breakpoints(cx)
23798 });
23799
23800 assert_eq!(1, breakpoints.len());
23801 assert_breakpoint(
23802 &breakpoints,
23803 &abs_path,
23804 vec![
23805 (0, Breakpoint::new_standard()),
23806 (3, Breakpoint::new_standard()),
23807 ],
23808 );
23809
23810 editor.update_in(cx, |editor, window, cx| {
23811 editor.move_to_beginning(&MoveToBeginning, window, cx);
23812 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23813 });
23814
23815 let breakpoints = editor.update(cx, |editor, cx| {
23816 editor
23817 .breakpoint_store()
23818 .as_ref()
23819 .unwrap()
23820 .read(cx)
23821 .all_source_breakpoints(cx)
23822 });
23823
23824 assert_eq!(1, breakpoints.len());
23825 assert_breakpoint(
23826 &breakpoints,
23827 &abs_path,
23828 vec![(3, Breakpoint::new_standard())],
23829 );
23830
23831 editor.update_in(cx, |editor, window, cx| {
23832 editor.move_to_end(&MoveToEnd, window, cx);
23833 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23834 });
23835
23836 let breakpoints = editor.update(cx, |editor, cx| {
23837 editor
23838 .breakpoint_store()
23839 .as_ref()
23840 .unwrap()
23841 .read(cx)
23842 .all_source_breakpoints(cx)
23843 });
23844
23845 assert_eq!(0, breakpoints.len());
23846 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23847}
23848
23849#[gpui::test]
23850async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23851 init_test(cx, |_| {});
23852
23853 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23854
23855 let fs = FakeFs::new(cx.executor());
23856 fs.insert_tree(
23857 path!("/a"),
23858 json!({
23859 "main.rs": sample_text,
23860 }),
23861 )
23862 .await;
23863 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23864 let (workspace, cx) =
23865 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23866
23867 let worktree_id = workspace.update(cx, |workspace, cx| {
23868 workspace.project().update(cx, |project, cx| {
23869 project.worktrees(cx).next().unwrap().read(cx).id()
23870 })
23871 });
23872
23873 let buffer = project
23874 .update(cx, |project, cx| {
23875 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23876 })
23877 .await
23878 .unwrap();
23879
23880 let (editor, cx) = cx.add_window_view(|window, cx| {
23881 Editor::new(
23882 EditorMode::full(),
23883 MultiBuffer::build_from_buffer(buffer, cx),
23884 Some(project.clone()),
23885 window,
23886 cx,
23887 )
23888 });
23889
23890 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23891 let abs_path = project.read_with(cx, |project, cx| {
23892 project
23893 .absolute_path(&project_path, cx)
23894 .map(Arc::from)
23895 .unwrap()
23896 });
23897
23898 editor.update_in(cx, |editor, window, cx| {
23899 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23900 });
23901
23902 let breakpoints = editor.update(cx, |editor, cx| {
23903 editor
23904 .breakpoint_store()
23905 .as_ref()
23906 .unwrap()
23907 .read(cx)
23908 .all_source_breakpoints(cx)
23909 });
23910
23911 assert_breakpoint(
23912 &breakpoints,
23913 &abs_path,
23914 vec![(0, Breakpoint::new_log("hello world"))],
23915 );
23916
23917 // Removing a log message from a log breakpoint should remove it
23918 editor.update_in(cx, |editor, window, cx| {
23919 add_log_breakpoint_at_cursor(editor, "", window, cx);
23920 });
23921
23922 let breakpoints = editor.update(cx, |editor, cx| {
23923 editor
23924 .breakpoint_store()
23925 .as_ref()
23926 .unwrap()
23927 .read(cx)
23928 .all_source_breakpoints(cx)
23929 });
23930
23931 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23932
23933 editor.update_in(cx, |editor, window, cx| {
23934 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23935 editor.move_to_end(&MoveToEnd, window, cx);
23936 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23937 // Not adding a log message to a standard breakpoint shouldn't remove it
23938 add_log_breakpoint_at_cursor(editor, "", window, cx);
23939 });
23940
23941 let breakpoints = editor.update(cx, |editor, cx| {
23942 editor
23943 .breakpoint_store()
23944 .as_ref()
23945 .unwrap()
23946 .read(cx)
23947 .all_source_breakpoints(cx)
23948 });
23949
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 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23961 });
23962
23963 let breakpoints = editor.update(cx, |editor, cx| {
23964 editor
23965 .breakpoint_store()
23966 .as_ref()
23967 .unwrap()
23968 .read(cx)
23969 .all_source_breakpoints(cx)
23970 });
23971
23972 assert_breakpoint(
23973 &breakpoints,
23974 &abs_path,
23975 vec![
23976 (0, Breakpoint::new_standard()),
23977 (3, Breakpoint::new_log("hello world")),
23978 ],
23979 );
23980
23981 editor.update_in(cx, |editor, window, cx| {
23982 add_log_breakpoint_at_cursor(editor, "hello Earth!!", 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_breakpoint(
23995 &breakpoints,
23996 &abs_path,
23997 vec![
23998 (0, Breakpoint::new_standard()),
23999 (3, Breakpoint::new_log("hello Earth!!")),
24000 ],
24001 );
24002}
24003
24004/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24005/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24006/// or when breakpoints were placed out of order. This tests for a regression too
24007#[gpui::test]
24008async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24009 init_test(cx, |_| {});
24010
24011 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24012 let fs = FakeFs::new(cx.executor());
24013 fs.insert_tree(
24014 path!("/a"),
24015 json!({
24016 "main.rs": sample_text,
24017 }),
24018 )
24019 .await;
24020 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24021 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24022 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24023
24024 let fs = FakeFs::new(cx.executor());
24025 fs.insert_tree(
24026 path!("/a"),
24027 json!({
24028 "main.rs": sample_text,
24029 }),
24030 )
24031 .await;
24032 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24033 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24034 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24035 let worktree_id = workspace
24036 .update(cx, |workspace, _window, cx| {
24037 workspace.project().update(cx, |project, cx| {
24038 project.worktrees(cx).next().unwrap().read(cx).id()
24039 })
24040 })
24041 .unwrap();
24042
24043 let buffer = project
24044 .update(cx, |project, cx| {
24045 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24046 })
24047 .await
24048 .unwrap();
24049
24050 let (editor, cx) = cx.add_window_view(|window, cx| {
24051 Editor::new(
24052 EditorMode::full(),
24053 MultiBuffer::build_from_buffer(buffer, cx),
24054 Some(project.clone()),
24055 window,
24056 cx,
24057 )
24058 });
24059
24060 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24061 let abs_path = project.read_with(cx, |project, cx| {
24062 project
24063 .absolute_path(&project_path, cx)
24064 .map(Arc::from)
24065 .unwrap()
24066 });
24067
24068 // assert we can add breakpoint on the first line
24069 editor.update_in(cx, |editor, window, cx| {
24070 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24071 editor.move_to_end(&MoveToEnd, window, cx);
24072 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24073 editor.move_up(&MoveUp, window, cx);
24074 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24075 });
24076
24077 let breakpoints = editor.update(cx, |editor, cx| {
24078 editor
24079 .breakpoint_store()
24080 .as_ref()
24081 .unwrap()
24082 .read(cx)
24083 .all_source_breakpoints(cx)
24084 });
24085
24086 assert_eq!(1, breakpoints.len());
24087 assert_breakpoint(
24088 &breakpoints,
24089 &abs_path,
24090 vec![
24091 (0, Breakpoint::new_standard()),
24092 (2, Breakpoint::new_standard()),
24093 (3, Breakpoint::new_standard()),
24094 ],
24095 );
24096
24097 editor.update_in(cx, |editor, window, cx| {
24098 editor.move_to_beginning(&MoveToBeginning, window, cx);
24099 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24100 editor.move_to_end(&MoveToEnd, window, cx);
24101 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24102 // Disabling a breakpoint that doesn't exist should do nothing
24103 editor.move_up(&MoveUp, window, cx);
24104 editor.move_up(&MoveUp, window, cx);
24105 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24106 });
24107
24108 let breakpoints = editor.update(cx, |editor, cx| {
24109 editor
24110 .breakpoint_store()
24111 .as_ref()
24112 .unwrap()
24113 .read(cx)
24114 .all_source_breakpoints(cx)
24115 });
24116
24117 let disable_breakpoint = {
24118 let mut bp = Breakpoint::new_standard();
24119 bp.state = BreakpointState::Disabled;
24120 bp
24121 };
24122
24123 assert_eq!(1, breakpoints.len());
24124 assert_breakpoint(
24125 &breakpoints,
24126 &abs_path,
24127 vec![
24128 (0, disable_breakpoint.clone()),
24129 (2, Breakpoint::new_standard()),
24130 (3, disable_breakpoint.clone()),
24131 ],
24132 );
24133
24134 editor.update_in(cx, |editor, window, cx| {
24135 editor.move_to_beginning(&MoveToBeginning, window, cx);
24136 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24137 editor.move_to_end(&MoveToEnd, window, cx);
24138 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24139 editor.move_up(&MoveUp, window, cx);
24140 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24141 });
24142
24143 let breakpoints = editor.update(cx, |editor, cx| {
24144 editor
24145 .breakpoint_store()
24146 .as_ref()
24147 .unwrap()
24148 .read(cx)
24149 .all_source_breakpoints(cx)
24150 });
24151
24152 assert_eq!(1, breakpoints.len());
24153 assert_breakpoint(
24154 &breakpoints,
24155 &abs_path,
24156 vec![
24157 (0, Breakpoint::new_standard()),
24158 (2, disable_breakpoint),
24159 (3, Breakpoint::new_standard()),
24160 ],
24161 );
24162}
24163
24164#[gpui::test]
24165async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24166 init_test(cx, |_| {});
24167 let capabilities = lsp::ServerCapabilities {
24168 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24169 prepare_provider: Some(true),
24170 work_done_progress_options: Default::default(),
24171 })),
24172 ..Default::default()
24173 };
24174 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24175
24176 cx.set_state(indoc! {"
24177 struct Fˇoo {}
24178 "});
24179
24180 cx.update_editor(|editor, _, cx| {
24181 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24182 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24183 editor.highlight_background::<DocumentHighlightRead>(
24184 &[highlight_range],
24185 |_, theme| theme.colors().editor_document_highlight_read_background,
24186 cx,
24187 );
24188 });
24189
24190 let mut prepare_rename_handler = cx
24191 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24192 move |_, _, _| async move {
24193 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24194 start: lsp::Position {
24195 line: 0,
24196 character: 7,
24197 },
24198 end: lsp::Position {
24199 line: 0,
24200 character: 10,
24201 },
24202 })))
24203 },
24204 );
24205 let prepare_rename_task = cx
24206 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24207 .expect("Prepare rename was not started");
24208 prepare_rename_handler.next().await.unwrap();
24209 prepare_rename_task.await.expect("Prepare rename failed");
24210
24211 let mut rename_handler =
24212 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24213 let edit = lsp::TextEdit {
24214 range: lsp::Range {
24215 start: lsp::Position {
24216 line: 0,
24217 character: 7,
24218 },
24219 end: lsp::Position {
24220 line: 0,
24221 character: 10,
24222 },
24223 },
24224 new_text: "FooRenamed".to_string(),
24225 };
24226 Ok(Some(lsp::WorkspaceEdit::new(
24227 // Specify the same edit twice
24228 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24229 )))
24230 });
24231 let rename_task = cx
24232 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24233 .expect("Confirm rename was not started");
24234 rename_handler.next().await.unwrap();
24235 rename_task.await.expect("Confirm rename failed");
24236 cx.run_until_parked();
24237
24238 // Despite two edits, only one is actually applied as those are identical
24239 cx.assert_editor_state(indoc! {"
24240 struct FooRenamedˇ {}
24241 "});
24242}
24243
24244#[gpui::test]
24245async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24246 init_test(cx, |_| {});
24247 // These capabilities indicate that the server does not support prepare rename.
24248 let capabilities = lsp::ServerCapabilities {
24249 rename_provider: Some(lsp::OneOf::Left(true)),
24250 ..Default::default()
24251 };
24252 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24253
24254 cx.set_state(indoc! {"
24255 struct Fˇoo {}
24256 "});
24257
24258 cx.update_editor(|editor, _window, cx| {
24259 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24260 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24261 editor.highlight_background::<DocumentHighlightRead>(
24262 &[highlight_range],
24263 |_, theme| theme.colors().editor_document_highlight_read_background,
24264 cx,
24265 );
24266 });
24267
24268 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24269 .expect("Prepare rename was not started")
24270 .await
24271 .expect("Prepare rename failed");
24272
24273 let mut rename_handler =
24274 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24275 let edit = lsp::TextEdit {
24276 range: lsp::Range {
24277 start: lsp::Position {
24278 line: 0,
24279 character: 7,
24280 },
24281 end: lsp::Position {
24282 line: 0,
24283 character: 10,
24284 },
24285 },
24286 new_text: "FooRenamed".to_string(),
24287 };
24288 Ok(Some(lsp::WorkspaceEdit::new(
24289 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24290 )))
24291 });
24292 let rename_task = cx
24293 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24294 .expect("Confirm rename was not started");
24295 rename_handler.next().await.unwrap();
24296 rename_task.await.expect("Confirm rename failed");
24297 cx.run_until_parked();
24298
24299 // Correct range is renamed, as `surrounding_word` is used to find it.
24300 cx.assert_editor_state(indoc! {"
24301 struct FooRenamedˇ {}
24302 "});
24303}
24304
24305#[gpui::test]
24306async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24307 init_test(cx, |_| {});
24308 let mut cx = EditorTestContext::new(cx).await;
24309
24310 let language = Arc::new(
24311 Language::new(
24312 LanguageConfig::default(),
24313 Some(tree_sitter_html::LANGUAGE.into()),
24314 )
24315 .with_brackets_query(
24316 r#"
24317 ("<" @open "/>" @close)
24318 ("</" @open ">" @close)
24319 ("<" @open ">" @close)
24320 ("\"" @open "\"" @close)
24321 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24322 "#,
24323 )
24324 .unwrap(),
24325 );
24326 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24327
24328 cx.set_state(indoc! {"
24329 <span>ˇ</span>
24330 "});
24331 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24332 cx.assert_editor_state(indoc! {"
24333 <span>
24334 ˇ
24335 </span>
24336 "});
24337
24338 cx.set_state(indoc! {"
24339 <span><span></span>ˇ</span>
24340 "});
24341 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24342 cx.assert_editor_state(indoc! {"
24343 <span><span></span>
24344 ˇ</span>
24345 "});
24346
24347 cx.set_state(indoc! {"
24348 <span>ˇ
24349 </span>
24350 "});
24351 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24352 cx.assert_editor_state(indoc! {"
24353 <span>
24354 ˇ
24355 </span>
24356 "});
24357}
24358
24359#[gpui::test(iterations = 10)]
24360async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24361 init_test(cx, |_| {});
24362
24363 let fs = FakeFs::new(cx.executor());
24364 fs.insert_tree(
24365 path!("/dir"),
24366 json!({
24367 "a.ts": "a",
24368 }),
24369 )
24370 .await;
24371
24372 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24373 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24374 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24375
24376 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24377 language_registry.add(Arc::new(Language::new(
24378 LanguageConfig {
24379 name: "TypeScript".into(),
24380 matcher: LanguageMatcher {
24381 path_suffixes: vec!["ts".to_string()],
24382 ..Default::default()
24383 },
24384 ..Default::default()
24385 },
24386 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24387 )));
24388 let mut fake_language_servers = language_registry.register_fake_lsp(
24389 "TypeScript",
24390 FakeLspAdapter {
24391 capabilities: lsp::ServerCapabilities {
24392 code_lens_provider: Some(lsp::CodeLensOptions {
24393 resolve_provider: Some(true),
24394 }),
24395 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24396 commands: vec!["_the/command".to_string()],
24397 ..lsp::ExecuteCommandOptions::default()
24398 }),
24399 ..lsp::ServerCapabilities::default()
24400 },
24401 ..FakeLspAdapter::default()
24402 },
24403 );
24404
24405 let editor = workspace
24406 .update(cx, |workspace, window, cx| {
24407 workspace.open_abs_path(
24408 PathBuf::from(path!("/dir/a.ts")),
24409 OpenOptions::default(),
24410 window,
24411 cx,
24412 )
24413 })
24414 .unwrap()
24415 .await
24416 .unwrap()
24417 .downcast::<Editor>()
24418 .unwrap();
24419 cx.executor().run_until_parked();
24420
24421 let fake_server = fake_language_servers.next().await.unwrap();
24422
24423 let buffer = editor.update(cx, |editor, cx| {
24424 editor
24425 .buffer()
24426 .read(cx)
24427 .as_singleton()
24428 .expect("have opened a single file by path")
24429 });
24430
24431 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24432 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24433 drop(buffer_snapshot);
24434 let actions = cx
24435 .update_window(*workspace, |_, window, cx| {
24436 project.code_actions(&buffer, anchor..anchor, window, cx)
24437 })
24438 .unwrap();
24439
24440 fake_server
24441 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24442 Ok(Some(vec![
24443 lsp::CodeLens {
24444 range: lsp::Range::default(),
24445 command: Some(lsp::Command {
24446 title: "Code lens command".to_owned(),
24447 command: "_the/command".to_owned(),
24448 arguments: None,
24449 }),
24450 data: None,
24451 },
24452 lsp::CodeLens {
24453 range: lsp::Range::default(),
24454 command: Some(lsp::Command {
24455 title: "Command not in capabilities".to_owned(),
24456 command: "not in capabilities".to_owned(),
24457 arguments: None,
24458 }),
24459 data: None,
24460 },
24461 lsp::CodeLens {
24462 range: lsp::Range {
24463 start: lsp::Position {
24464 line: 1,
24465 character: 1,
24466 },
24467 end: lsp::Position {
24468 line: 1,
24469 character: 1,
24470 },
24471 },
24472 command: Some(lsp::Command {
24473 title: "Command not in range".to_owned(),
24474 command: "_the/command".to_owned(),
24475 arguments: None,
24476 }),
24477 data: None,
24478 },
24479 ]))
24480 })
24481 .next()
24482 .await;
24483
24484 let actions = actions.await.unwrap();
24485 assert_eq!(
24486 actions.len(),
24487 1,
24488 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24489 );
24490 let action = actions[0].clone();
24491 let apply = project.update(cx, |project, cx| {
24492 project.apply_code_action(buffer.clone(), action, true, cx)
24493 });
24494
24495 // Resolving the code action does not populate its edits. In absence of
24496 // edits, we must execute the given command.
24497 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24498 |mut lens, _| async move {
24499 let lens_command = lens.command.as_mut().expect("should have a command");
24500 assert_eq!(lens_command.title, "Code lens command");
24501 lens_command.arguments = Some(vec![json!("the-argument")]);
24502 Ok(lens)
24503 },
24504 );
24505
24506 // While executing the command, the language server sends the editor
24507 // a `workspaceEdit` request.
24508 fake_server
24509 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24510 let fake = fake_server.clone();
24511 move |params, _| {
24512 assert_eq!(params.command, "_the/command");
24513 let fake = fake.clone();
24514 async move {
24515 fake.server
24516 .request::<lsp::request::ApplyWorkspaceEdit>(
24517 lsp::ApplyWorkspaceEditParams {
24518 label: None,
24519 edit: lsp::WorkspaceEdit {
24520 changes: Some(
24521 [(
24522 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24523 vec![lsp::TextEdit {
24524 range: lsp::Range::new(
24525 lsp::Position::new(0, 0),
24526 lsp::Position::new(0, 0),
24527 ),
24528 new_text: "X".into(),
24529 }],
24530 )]
24531 .into_iter()
24532 .collect(),
24533 ),
24534 ..lsp::WorkspaceEdit::default()
24535 },
24536 },
24537 )
24538 .await
24539 .into_response()
24540 .unwrap();
24541 Ok(Some(json!(null)))
24542 }
24543 }
24544 })
24545 .next()
24546 .await;
24547
24548 // Applying the code lens command returns a project transaction containing the edits
24549 // sent by the language server in its `workspaceEdit` request.
24550 let transaction = apply.await.unwrap();
24551 assert!(transaction.0.contains_key(&buffer));
24552 buffer.update(cx, |buffer, cx| {
24553 assert_eq!(buffer.text(), "Xa");
24554 buffer.undo(cx);
24555 assert_eq!(buffer.text(), "a");
24556 });
24557
24558 let actions_after_edits = cx
24559 .update_window(*workspace, |_, window, cx| {
24560 project.code_actions(&buffer, anchor..anchor, window, cx)
24561 })
24562 .unwrap()
24563 .await
24564 .unwrap();
24565 assert_eq!(
24566 actions, actions_after_edits,
24567 "For the same selection, same code lens actions should be returned"
24568 );
24569
24570 let _responses =
24571 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24572 panic!("No more code lens requests are expected");
24573 });
24574 editor.update_in(cx, |editor, window, cx| {
24575 editor.select_all(&SelectAll, window, cx);
24576 });
24577 cx.executor().run_until_parked();
24578 let new_actions = cx
24579 .update_window(*workspace, |_, window, cx| {
24580 project.code_actions(&buffer, anchor..anchor, window, cx)
24581 })
24582 .unwrap()
24583 .await
24584 .unwrap();
24585 assert_eq!(
24586 actions, new_actions,
24587 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24588 );
24589}
24590
24591#[gpui::test]
24592async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24593 init_test(cx, |_| {});
24594
24595 let fs = FakeFs::new(cx.executor());
24596 let main_text = r#"fn main() {
24597println!("1");
24598println!("2");
24599println!("3");
24600println!("4");
24601println!("5");
24602}"#;
24603 let lib_text = "mod foo {}";
24604 fs.insert_tree(
24605 path!("/a"),
24606 json!({
24607 "lib.rs": lib_text,
24608 "main.rs": main_text,
24609 }),
24610 )
24611 .await;
24612
24613 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24614 let (workspace, cx) =
24615 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24616 let worktree_id = workspace.update(cx, |workspace, cx| {
24617 workspace.project().update(cx, |project, cx| {
24618 project.worktrees(cx).next().unwrap().read(cx).id()
24619 })
24620 });
24621
24622 let expected_ranges = vec![
24623 Point::new(0, 0)..Point::new(0, 0),
24624 Point::new(1, 0)..Point::new(1, 1),
24625 Point::new(2, 0)..Point::new(2, 2),
24626 Point::new(3, 0)..Point::new(3, 3),
24627 ];
24628
24629 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24630 let editor_1 = workspace
24631 .update_in(cx, |workspace, window, cx| {
24632 workspace.open_path(
24633 (worktree_id, rel_path("main.rs")),
24634 Some(pane_1.downgrade()),
24635 true,
24636 window,
24637 cx,
24638 )
24639 })
24640 .unwrap()
24641 .await
24642 .downcast::<Editor>()
24643 .unwrap();
24644 pane_1.update(cx, |pane, cx| {
24645 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24646 open_editor.update(cx, |editor, cx| {
24647 assert_eq!(
24648 editor.display_text(cx),
24649 main_text,
24650 "Original main.rs text on initial open",
24651 );
24652 assert_eq!(
24653 editor
24654 .selections
24655 .all::<Point>(&editor.display_snapshot(cx))
24656 .into_iter()
24657 .map(|s| s.range())
24658 .collect::<Vec<_>>(),
24659 vec![Point::zero()..Point::zero()],
24660 "Default selections on initial open",
24661 );
24662 })
24663 });
24664 editor_1.update_in(cx, |editor, window, cx| {
24665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24666 s.select_ranges(expected_ranges.clone());
24667 });
24668 });
24669
24670 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24671 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24672 });
24673 let editor_2 = workspace
24674 .update_in(cx, |workspace, window, cx| {
24675 workspace.open_path(
24676 (worktree_id, rel_path("main.rs")),
24677 Some(pane_2.downgrade()),
24678 true,
24679 window,
24680 cx,
24681 )
24682 })
24683 .unwrap()
24684 .await
24685 .downcast::<Editor>()
24686 .unwrap();
24687 pane_2.update(cx, |pane, cx| {
24688 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24689 open_editor.update(cx, |editor, cx| {
24690 assert_eq!(
24691 editor.display_text(cx),
24692 main_text,
24693 "Original main.rs text on initial open in another panel",
24694 );
24695 assert_eq!(
24696 editor
24697 .selections
24698 .all::<Point>(&editor.display_snapshot(cx))
24699 .into_iter()
24700 .map(|s| s.range())
24701 .collect::<Vec<_>>(),
24702 vec![Point::zero()..Point::zero()],
24703 "Default selections on initial open in another panel",
24704 );
24705 })
24706 });
24707
24708 editor_2.update_in(cx, |editor, window, cx| {
24709 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24710 });
24711
24712 let _other_editor_1 = workspace
24713 .update_in(cx, |workspace, window, cx| {
24714 workspace.open_path(
24715 (worktree_id, rel_path("lib.rs")),
24716 Some(pane_1.downgrade()),
24717 true,
24718 window,
24719 cx,
24720 )
24721 })
24722 .unwrap()
24723 .await
24724 .downcast::<Editor>()
24725 .unwrap();
24726 pane_1
24727 .update_in(cx, |pane, window, cx| {
24728 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24729 })
24730 .await
24731 .unwrap();
24732 drop(editor_1);
24733 pane_1.update(cx, |pane, cx| {
24734 pane.active_item()
24735 .unwrap()
24736 .downcast::<Editor>()
24737 .unwrap()
24738 .update(cx, |editor, cx| {
24739 assert_eq!(
24740 editor.display_text(cx),
24741 lib_text,
24742 "Other file should be open and active",
24743 );
24744 });
24745 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24746 });
24747
24748 let _other_editor_2 = workspace
24749 .update_in(cx, |workspace, window, cx| {
24750 workspace.open_path(
24751 (worktree_id, rel_path("lib.rs")),
24752 Some(pane_2.downgrade()),
24753 true,
24754 window,
24755 cx,
24756 )
24757 })
24758 .unwrap()
24759 .await
24760 .downcast::<Editor>()
24761 .unwrap();
24762 pane_2
24763 .update_in(cx, |pane, window, cx| {
24764 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24765 })
24766 .await
24767 .unwrap();
24768 drop(editor_2);
24769 pane_2.update(cx, |pane, cx| {
24770 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24771 open_editor.update(cx, |editor, cx| {
24772 assert_eq!(
24773 editor.display_text(cx),
24774 lib_text,
24775 "Other file should be open and active in another panel too",
24776 );
24777 });
24778 assert_eq!(
24779 pane.items().count(),
24780 1,
24781 "No other editors should be open in another pane",
24782 );
24783 });
24784
24785 let _editor_1_reopened = workspace
24786 .update_in(cx, |workspace, window, cx| {
24787 workspace.open_path(
24788 (worktree_id, rel_path("main.rs")),
24789 Some(pane_1.downgrade()),
24790 true,
24791 window,
24792 cx,
24793 )
24794 })
24795 .unwrap()
24796 .await
24797 .downcast::<Editor>()
24798 .unwrap();
24799 let _editor_2_reopened = workspace
24800 .update_in(cx, |workspace, window, cx| {
24801 workspace.open_path(
24802 (worktree_id, rel_path("main.rs")),
24803 Some(pane_2.downgrade()),
24804 true,
24805 window,
24806 cx,
24807 )
24808 })
24809 .unwrap()
24810 .await
24811 .downcast::<Editor>()
24812 .unwrap();
24813 pane_1.update(cx, |pane, cx| {
24814 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24815 open_editor.update(cx, |editor, cx| {
24816 assert_eq!(
24817 editor.display_text(cx),
24818 main_text,
24819 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24820 );
24821 assert_eq!(
24822 editor
24823 .selections
24824 .all::<Point>(&editor.display_snapshot(cx))
24825 .into_iter()
24826 .map(|s| s.range())
24827 .collect::<Vec<_>>(),
24828 expected_ranges,
24829 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24830 );
24831 })
24832 });
24833 pane_2.update(cx, |pane, cx| {
24834 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24835 open_editor.update(cx, |editor, cx| {
24836 assert_eq!(
24837 editor.display_text(cx),
24838 r#"fn main() {
24839⋯rintln!("1");
24840⋯intln!("2");
24841⋯ntln!("3");
24842println!("4");
24843println!("5");
24844}"#,
24845 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24846 );
24847 assert_eq!(
24848 editor
24849 .selections
24850 .all::<Point>(&editor.display_snapshot(cx))
24851 .into_iter()
24852 .map(|s| s.range())
24853 .collect::<Vec<_>>(),
24854 vec![Point::zero()..Point::zero()],
24855 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24856 );
24857 })
24858 });
24859}
24860
24861#[gpui::test]
24862async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24863 init_test(cx, |_| {});
24864
24865 let fs = FakeFs::new(cx.executor());
24866 let main_text = r#"fn main() {
24867println!("1");
24868println!("2");
24869println!("3");
24870println!("4");
24871println!("5");
24872}"#;
24873 let lib_text = "mod foo {}";
24874 fs.insert_tree(
24875 path!("/a"),
24876 json!({
24877 "lib.rs": lib_text,
24878 "main.rs": main_text,
24879 }),
24880 )
24881 .await;
24882
24883 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24884 let (workspace, cx) =
24885 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24886 let worktree_id = workspace.update(cx, |workspace, cx| {
24887 workspace.project().update(cx, |project, cx| {
24888 project.worktrees(cx).next().unwrap().read(cx).id()
24889 })
24890 });
24891
24892 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24893 let editor = workspace
24894 .update_in(cx, |workspace, window, cx| {
24895 workspace.open_path(
24896 (worktree_id, rel_path("main.rs")),
24897 Some(pane.downgrade()),
24898 true,
24899 window,
24900 cx,
24901 )
24902 })
24903 .unwrap()
24904 .await
24905 .downcast::<Editor>()
24906 .unwrap();
24907 pane.update(cx, |pane, cx| {
24908 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24909 open_editor.update(cx, |editor, cx| {
24910 assert_eq!(
24911 editor.display_text(cx),
24912 main_text,
24913 "Original main.rs text on initial open",
24914 );
24915 })
24916 });
24917 editor.update_in(cx, |editor, window, cx| {
24918 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24919 });
24920
24921 cx.update_global(|store: &mut SettingsStore, cx| {
24922 store.update_user_settings(cx, |s| {
24923 s.workspace.restore_on_file_reopen = Some(false);
24924 });
24925 });
24926 editor.update_in(cx, |editor, window, cx| {
24927 editor.fold_ranges(
24928 vec![
24929 Point::new(1, 0)..Point::new(1, 1),
24930 Point::new(2, 0)..Point::new(2, 2),
24931 Point::new(3, 0)..Point::new(3, 3),
24932 ],
24933 false,
24934 window,
24935 cx,
24936 );
24937 });
24938 pane.update_in(cx, |pane, window, cx| {
24939 pane.close_all_items(&CloseAllItems::default(), window, cx)
24940 })
24941 .await
24942 .unwrap();
24943 pane.update(cx, |pane, _| {
24944 assert!(pane.active_item().is_none());
24945 });
24946 cx.update_global(|store: &mut SettingsStore, cx| {
24947 store.update_user_settings(cx, |s| {
24948 s.workspace.restore_on_file_reopen = Some(true);
24949 });
24950 });
24951
24952 let _editor_reopened = workspace
24953 .update_in(cx, |workspace, window, cx| {
24954 workspace.open_path(
24955 (worktree_id, rel_path("main.rs")),
24956 Some(pane.downgrade()),
24957 true,
24958 window,
24959 cx,
24960 )
24961 })
24962 .unwrap()
24963 .await
24964 .downcast::<Editor>()
24965 .unwrap();
24966 pane.update(cx, |pane, cx| {
24967 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24968 open_editor.update(cx, |editor, cx| {
24969 assert_eq!(
24970 editor.display_text(cx),
24971 main_text,
24972 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24973 );
24974 })
24975 });
24976}
24977
24978#[gpui::test]
24979async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24980 struct EmptyModalView {
24981 focus_handle: gpui::FocusHandle,
24982 }
24983 impl EventEmitter<DismissEvent> for EmptyModalView {}
24984 impl Render for EmptyModalView {
24985 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24986 div()
24987 }
24988 }
24989 impl Focusable for EmptyModalView {
24990 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24991 self.focus_handle.clone()
24992 }
24993 }
24994 impl workspace::ModalView for EmptyModalView {}
24995 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24996 EmptyModalView {
24997 focus_handle: cx.focus_handle(),
24998 }
24999 }
25000
25001 init_test(cx, |_| {});
25002
25003 let fs = FakeFs::new(cx.executor());
25004 let project = Project::test(fs, [], cx).await;
25005 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25006 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25007 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25008 let editor = cx.new_window_entity(|window, cx| {
25009 Editor::new(
25010 EditorMode::full(),
25011 buffer,
25012 Some(project.clone()),
25013 window,
25014 cx,
25015 )
25016 });
25017 workspace
25018 .update(cx, |workspace, window, cx| {
25019 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25020 })
25021 .unwrap();
25022 editor.update_in(cx, |editor, window, cx| {
25023 editor.open_context_menu(&OpenContextMenu, window, cx);
25024 assert!(editor.mouse_context_menu.is_some());
25025 });
25026 workspace
25027 .update(cx, |workspace, window, cx| {
25028 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25029 })
25030 .unwrap();
25031 cx.read(|cx| {
25032 assert!(editor.read(cx).mouse_context_menu.is_none());
25033 });
25034}
25035
25036fn set_linked_edit_ranges(
25037 opening: (Point, Point),
25038 closing: (Point, Point),
25039 editor: &mut Editor,
25040 cx: &mut Context<Editor>,
25041) {
25042 let Some((buffer, _)) = editor
25043 .buffer
25044 .read(cx)
25045 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25046 else {
25047 panic!("Failed to get buffer for selection position");
25048 };
25049 let buffer = buffer.read(cx);
25050 let buffer_id = buffer.remote_id();
25051 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25052 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25053 let mut linked_ranges = HashMap::default();
25054 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25055 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25056}
25057
25058#[gpui::test]
25059async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25060 init_test(cx, |_| {});
25061
25062 let fs = FakeFs::new(cx.executor());
25063 fs.insert_file(path!("/file.html"), Default::default())
25064 .await;
25065
25066 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25067
25068 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25069 let html_language = Arc::new(Language::new(
25070 LanguageConfig {
25071 name: "HTML".into(),
25072 matcher: LanguageMatcher {
25073 path_suffixes: vec!["html".to_string()],
25074 ..LanguageMatcher::default()
25075 },
25076 brackets: BracketPairConfig {
25077 pairs: vec![BracketPair {
25078 start: "<".into(),
25079 end: ">".into(),
25080 close: true,
25081 ..Default::default()
25082 }],
25083 ..Default::default()
25084 },
25085 ..Default::default()
25086 },
25087 Some(tree_sitter_html::LANGUAGE.into()),
25088 ));
25089 language_registry.add(html_language);
25090 let mut fake_servers = language_registry.register_fake_lsp(
25091 "HTML",
25092 FakeLspAdapter {
25093 capabilities: lsp::ServerCapabilities {
25094 completion_provider: Some(lsp::CompletionOptions {
25095 resolve_provider: Some(true),
25096 ..Default::default()
25097 }),
25098 ..Default::default()
25099 },
25100 ..Default::default()
25101 },
25102 );
25103
25104 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25105 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25106
25107 let worktree_id = workspace
25108 .update(cx, |workspace, _window, cx| {
25109 workspace.project().update(cx, |project, cx| {
25110 project.worktrees(cx).next().unwrap().read(cx).id()
25111 })
25112 })
25113 .unwrap();
25114 project
25115 .update(cx, |project, cx| {
25116 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25117 })
25118 .await
25119 .unwrap();
25120 let editor = workspace
25121 .update(cx, |workspace, window, cx| {
25122 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25123 })
25124 .unwrap()
25125 .await
25126 .unwrap()
25127 .downcast::<Editor>()
25128 .unwrap();
25129
25130 let fake_server = fake_servers.next().await.unwrap();
25131 editor.update_in(cx, |editor, window, cx| {
25132 editor.set_text("<ad></ad>", window, cx);
25133 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25134 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25135 });
25136 set_linked_edit_ranges(
25137 (Point::new(0, 1), Point::new(0, 3)),
25138 (Point::new(0, 6), Point::new(0, 8)),
25139 editor,
25140 cx,
25141 );
25142 });
25143 let mut completion_handle =
25144 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25145 Ok(Some(lsp::CompletionResponse::Array(vec![
25146 lsp::CompletionItem {
25147 label: "head".to_string(),
25148 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25149 lsp::InsertReplaceEdit {
25150 new_text: "head".to_string(),
25151 insert: lsp::Range::new(
25152 lsp::Position::new(0, 1),
25153 lsp::Position::new(0, 3),
25154 ),
25155 replace: lsp::Range::new(
25156 lsp::Position::new(0, 1),
25157 lsp::Position::new(0, 3),
25158 ),
25159 },
25160 )),
25161 ..Default::default()
25162 },
25163 ])))
25164 });
25165 editor.update_in(cx, |editor, window, cx| {
25166 editor.show_completions(&ShowCompletions, window, cx);
25167 });
25168 cx.run_until_parked();
25169 completion_handle.next().await.unwrap();
25170 editor.update(cx, |editor, _| {
25171 assert!(
25172 editor.context_menu_visible(),
25173 "Completion menu should be visible"
25174 );
25175 });
25176 editor.update_in(cx, |editor, window, cx| {
25177 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25178 });
25179 cx.executor().run_until_parked();
25180 editor.update(cx, |editor, cx| {
25181 assert_eq!(editor.text(cx), "<head></head>");
25182 });
25183}
25184
25185#[gpui::test]
25186async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25187 init_test(cx, |_| {});
25188
25189 let mut cx = EditorTestContext::new(cx).await;
25190 let language = Arc::new(Language::new(
25191 LanguageConfig {
25192 name: "TSX".into(),
25193 matcher: LanguageMatcher {
25194 path_suffixes: vec!["tsx".to_string()],
25195 ..LanguageMatcher::default()
25196 },
25197 brackets: BracketPairConfig {
25198 pairs: vec![BracketPair {
25199 start: "<".into(),
25200 end: ">".into(),
25201 close: true,
25202 ..Default::default()
25203 }],
25204 ..Default::default()
25205 },
25206 linked_edit_characters: HashSet::from_iter(['.']),
25207 ..Default::default()
25208 },
25209 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25210 ));
25211 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25212
25213 // Test typing > does not extend linked pair
25214 cx.set_state("<divˇ<div></div>");
25215 cx.update_editor(|editor, _, cx| {
25216 set_linked_edit_ranges(
25217 (Point::new(0, 1), Point::new(0, 4)),
25218 (Point::new(0, 11), Point::new(0, 14)),
25219 editor,
25220 cx,
25221 );
25222 });
25223 cx.update_editor(|editor, window, cx| {
25224 editor.handle_input(">", window, cx);
25225 });
25226 cx.assert_editor_state("<div>ˇ<div></div>");
25227
25228 // Test typing . do extend linked pair
25229 cx.set_state("<Animatedˇ></Animated>");
25230 cx.update_editor(|editor, _, cx| {
25231 set_linked_edit_ranges(
25232 (Point::new(0, 1), Point::new(0, 9)),
25233 (Point::new(0, 12), Point::new(0, 20)),
25234 editor,
25235 cx,
25236 );
25237 });
25238 cx.update_editor(|editor, window, cx| {
25239 editor.handle_input(".", window, cx);
25240 });
25241 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25242 cx.update_editor(|editor, _, cx| {
25243 set_linked_edit_ranges(
25244 (Point::new(0, 1), Point::new(0, 10)),
25245 (Point::new(0, 13), Point::new(0, 21)),
25246 editor,
25247 cx,
25248 );
25249 });
25250 cx.update_editor(|editor, window, cx| {
25251 editor.handle_input("V", window, cx);
25252 });
25253 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25254}
25255
25256#[gpui::test]
25257async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25258 init_test(cx, |_| {});
25259
25260 let fs = FakeFs::new(cx.executor());
25261 fs.insert_tree(
25262 path!("/root"),
25263 json!({
25264 "a": {
25265 "main.rs": "fn main() {}",
25266 },
25267 "foo": {
25268 "bar": {
25269 "external_file.rs": "pub mod external {}",
25270 }
25271 }
25272 }),
25273 )
25274 .await;
25275
25276 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25277 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25278 language_registry.add(rust_lang());
25279 let _fake_servers = language_registry.register_fake_lsp(
25280 "Rust",
25281 FakeLspAdapter {
25282 ..FakeLspAdapter::default()
25283 },
25284 );
25285 let (workspace, cx) =
25286 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25287 let worktree_id = workspace.update(cx, |workspace, cx| {
25288 workspace.project().update(cx, |project, cx| {
25289 project.worktrees(cx).next().unwrap().read(cx).id()
25290 })
25291 });
25292
25293 let assert_language_servers_count =
25294 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25295 project.update(cx, |project, cx| {
25296 let current = project
25297 .lsp_store()
25298 .read(cx)
25299 .as_local()
25300 .unwrap()
25301 .language_servers
25302 .len();
25303 assert_eq!(expected, current, "{context}");
25304 });
25305 };
25306
25307 assert_language_servers_count(
25308 0,
25309 "No servers should be running before any file is open",
25310 cx,
25311 );
25312 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25313 let main_editor = workspace
25314 .update_in(cx, |workspace, window, cx| {
25315 workspace.open_path(
25316 (worktree_id, rel_path("main.rs")),
25317 Some(pane.downgrade()),
25318 true,
25319 window,
25320 cx,
25321 )
25322 })
25323 .unwrap()
25324 .await
25325 .downcast::<Editor>()
25326 .unwrap();
25327 pane.update(cx, |pane, cx| {
25328 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25329 open_editor.update(cx, |editor, cx| {
25330 assert_eq!(
25331 editor.display_text(cx),
25332 "fn main() {}",
25333 "Original main.rs text on initial open",
25334 );
25335 });
25336 assert_eq!(open_editor, main_editor);
25337 });
25338 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25339
25340 let external_editor = workspace
25341 .update_in(cx, |workspace, window, cx| {
25342 workspace.open_abs_path(
25343 PathBuf::from("/root/foo/bar/external_file.rs"),
25344 OpenOptions::default(),
25345 window,
25346 cx,
25347 )
25348 })
25349 .await
25350 .expect("opening external file")
25351 .downcast::<Editor>()
25352 .expect("downcasted external file's open element to editor");
25353 pane.update(cx, |pane, cx| {
25354 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25355 open_editor.update(cx, |editor, cx| {
25356 assert_eq!(
25357 editor.display_text(cx),
25358 "pub mod external {}",
25359 "External file is open now",
25360 );
25361 });
25362 assert_eq!(open_editor, external_editor);
25363 });
25364 assert_language_servers_count(
25365 1,
25366 "Second, external, *.rs file should join the existing server",
25367 cx,
25368 );
25369
25370 pane.update_in(cx, |pane, window, cx| {
25371 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25372 })
25373 .await
25374 .unwrap();
25375 pane.update_in(cx, |pane, window, cx| {
25376 pane.navigate_backward(&Default::default(), window, cx);
25377 });
25378 cx.run_until_parked();
25379 pane.update(cx, |pane, cx| {
25380 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25381 open_editor.update(cx, |editor, cx| {
25382 assert_eq!(
25383 editor.display_text(cx),
25384 "pub mod external {}",
25385 "External file is open now",
25386 );
25387 });
25388 });
25389 assert_language_servers_count(
25390 1,
25391 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25392 cx,
25393 );
25394
25395 cx.update(|_, cx| {
25396 workspace::reload(cx);
25397 });
25398 assert_language_servers_count(
25399 1,
25400 "After reloading the worktree with local and external files opened, only one project should be started",
25401 cx,
25402 );
25403}
25404
25405#[gpui::test]
25406async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25407 init_test(cx, |_| {});
25408
25409 let mut cx = EditorTestContext::new(cx).await;
25410 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25411 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25412
25413 // test cursor move to start of each line on tab
25414 // for `if`, `elif`, `else`, `while`, `with` and `for`
25415 cx.set_state(indoc! {"
25416 def main():
25417 ˇ for item in items:
25418 ˇ while item.active:
25419 ˇ if item.value > 10:
25420 ˇ continue
25421 ˇ elif item.value < 0:
25422 ˇ break
25423 ˇ else:
25424 ˇ with item.context() as ctx:
25425 ˇ yield count
25426 ˇ else:
25427 ˇ log('while else')
25428 ˇ else:
25429 ˇ log('for else')
25430 "});
25431 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25432 cx.assert_editor_state(indoc! {"
25433 def main():
25434 ˇfor item in items:
25435 ˇwhile item.active:
25436 ˇif item.value > 10:
25437 ˇcontinue
25438 ˇelif item.value < 0:
25439 ˇbreak
25440 ˇelse:
25441 ˇwith item.context() as ctx:
25442 ˇyield count
25443 ˇelse:
25444 ˇlog('while else')
25445 ˇelse:
25446 ˇlog('for else')
25447 "});
25448 // test relative indent is preserved when tab
25449 // for `if`, `elif`, `else`, `while`, `with` and `for`
25450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25451 cx.assert_editor_state(indoc! {"
25452 def main():
25453 ˇfor item in items:
25454 ˇwhile item.active:
25455 ˇif item.value > 10:
25456 ˇcontinue
25457 ˇelif item.value < 0:
25458 ˇbreak
25459 ˇelse:
25460 ˇwith item.context() as ctx:
25461 ˇyield count
25462 ˇelse:
25463 ˇlog('while else')
25464 ˇelse:
25465 ˇlog('for else')
25466 "});
25467
25468 // test cursor move to start of each line on tab
25469 // for `try`, `except`, `else`, `finally`, `match` and `def`
25470 cx.set_state(indoc! {"
25471 def main():
25472 ˇ try:
25473 ˇ fetch()
25474 ˇ except ValueError:
25475 ˇ handle_error()
25476 ˇ else:
25477 ˇ match value:
25478 ˇ case _:
25479 ˇ finally:
25480 ˇ def status():
25481 ˇ return 0
25482 "});
25483 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25484 cx.assert_editor_state(indoc! {"
25485 def main():
25486 ˇtry:
25487 ˇfetch()
25488 ˇexcept ValueError:
25489 ˇhandle_error()
25490 ˇelse:
25491 ˇmatch value:
25492 ˇcase _:
25493 ˇfinally:
25494 ˇdef status():
25495 ˇreturn 0
25496 "});
25497 // test relative indent is preserved when tab
25498 // for `try`, `except`, `else`, `finally`, `match` and `def`
25499 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25500 cx.assert_editor_state(indoc! {"
25501 def main():
25502 ˇtry:
25503 ˇfetch()
25504 ˇexcept ValueError:
25505 ˇhandle_error()
25506 ˇelse:
25507 ˇmatch value:
25508 ˇcase _:
25509 ˇfinally:
25510 ˇdef status():
25511 ˇreturn 0
25512 "});
25513}
25514
25515#[gpui::test]
25516async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25517 init_test(cx, |_| {});
25518
25519 let mut cx = EditorTestContext::new(cx).await;
25520 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25521 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25522
25523 // test `else` auto outdents when typed inside `if` block
25524 cx.set_state(indoc! {"
25525 def main():
25526 if i == 2:
25527 return
25528 ˇ
25529 "});
25530 cx.update_editor(|editor, window, cx| {
25531 editor.handle_input("else:", window, cx);
25532 });
25533 cx.assert_editor_state(indoc! {"
25534 def main():
25535 if i == 2:
25536 return
25537 else:ˇ
25538 "});
25539
25540 // test `except` auto outdents when typed inside `try` block
25541 cx.set_state(indoc! {"
25542 def main():
25543 try:
25544 i = 2
25545 ˇ
25546 "});
25547 cx.update_editor(|editor, window, cx| {
25548 editor.handle_input("except:", window, cx);
25549 });
25550 cx.assert_editor_state(indoc! {"
25551 def main():
25552 try:
25553 i = 2
25554 except:ˇ
25555 "});
25556
25557 // test `else` auto outdents when typed inside `except` block
25558 cx.set_state(indoc! {"
25559 def main():
25560 try:
25561 i = 2
25562 except:
25563 j = 2
25564 ˇ
25565 "});
25566 cx.update_editor(|editor, window, cx| {
25567 editor.handle_input("else:", window, cx);
25568 });
25569 cx.assert_editor_state(indoc! {"
25570 def main():
25571 try:
25572 i = 2
25573 except:
25574 j = 2
25575 else:ˇ
25576 "});
25577
25578 // test `finally` auto outdents when typed inside `else` block
25579 cx.set_state(indoc! {"
25580 def main():
25581 try:
25582 i = 2
25583 except:
25584 j = 2
25585 else:
25586 k = 2
25587 ˇ
25588 "});
25589 cx.update_editor(|editor, window, cx| {
25590 editor.handle_input("finally:", window, cx);
25591 });
25592 cx.assert_editor_state(indoc! {"
25593 def main():
25594 try:
25595 i = 2
25596 except:
25597 j = 2
25598 else:
25599 k = 2
25600 finally:ˇ
25601 "});
25602
25603 // test `else` does not outdents when typed inside `except` block right after for block
25604 cx.set_state(indoc! {"
25605 def main():
25606 try:
25607 i = 2
25608 except:
25609 for i in range(n):
25610 pass
25611 ˇ
25612 "});
25613 cx.update_editor(|editor, window, cx| {
25614 editor.handle_input("else:", window, cx);
25615 });
25616 cx.assert_editor_state(indoc! {"
25617 def main():
25618 try:
25619 i = 2
25620 except:
25621 for i in range(n):
25622 pass
25623 else:ˇ
25624 "});
25625
25626 // test `finally` auto outdents when typed inside `else` block right after for block
25627 cx.set_state(indoc! {"
25628 def main():
25629 try:
25630 i = 2
25631 except:
25632 j = 2
25633 else:
25634 for i in range(n):
25635 pass
25636 ˇ
25637 "});
25638 cx.update_editor(|editor, window, cx| {
25639 editor.handle_input("finally:", window, cx);
25640 });
25641 cx.assert_editor_state(indoc! {"
25642 def main():
25643 try:
25644 i = 2
25645 except:
25646 j = 2
25647 else:
25648 for i in range(n):
25649 pass
25650 finally:ˇ
25651 "});
25652
25653 // test `except` outdents to inner "try" block
25654 cx.set_state(indoc! {"
25655 def main():
25656 try:
25657 i = 2
25658 if i == 2:
25659 try:
25660 i = 3
25661 ˇ
25662 "});
25663 cx.update_editor(|editor, window, cx| {
25664 editor.handle_input("except:", window, cx);
25665 });
25666 cx.assert_editor_state(indoc! {"
25667 def main():
25668 try:
25669 i = 2
25670 if i == 2:
25671 try:
25672 i = 3
25673 except:ˇ
25674 "});
25675
25676 // test `except` outdents to outer "try" block
25677 cx.set_state(indoc! {"
25678 def main():
25679 try:
25680 i = 2
25681 if i == 2:
25682 try:
25683 i = 3
25684 ˇ
25685 "});
25686 cx.update_editor(|editor, window, cx| {
25687 editor.handle_input("except:", window, cx);
25688 });
25689 cx.assert_editor_state(indoc! {"
25690 def main():
25691 try:
25692 i = 2
25693 if i == 2:
25694 try:
25695 i = 3
25696 except:ˇ
25697 "});
25698
25699 // test `else` stays at correct indent when typed after `for` block
25700 cx.set_state(indoc! {"
25701 def main():
25702 for i in range(10):
25703 if i == 3:
25704 break
25705 ˇ
25706 "});
25707 cx.update_editor(|editor, window, cx| {
25708 editor.handle_input("else:", window, cx);
25709 });
25710 cx.assert_editor_state(indoc! {"
25711 def main():
25712 for i in range(10):
25713 if i == 3:
25714 break
25715 else:ˇ
25716 "});
25717
25718 // test does not outdent on typing after line with square brackets
25719 cx.set_state(indoc! {"
25720 def f() -> list[str]:
25721 ˇ
25722 "});
25723 cx.update_editor(|editor, window, cx| {
25724 editor.handle_input("a", window, cx);
25725 });
25726 cx.assert_editor_state(indoc! {"
25727 def f() -> list[str]:
25728 aˇ
25729 "});
25730
25731 // test does not outdent on typing : after case keyword
25732 cx.set_state(indoc! {"
25733 match 1:
25734 caseˇ
25735 "});
25736 cx.update_editor(|editor, window, cx| {
25737 editor.handle_input(":", window, cx);
25738 });
25739 cx.assert_editor_state(indoc! {"
25740 match 1:
25741 case:ˇ
25742 "});
25743}
25744
25745#[gpui::test]
25746async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25747 init_test(cx, |_| {});
25748 update_test_language_settings(cx, |settings| {
25749 settings.defaults.extend_comment_on_newline = Some(false);
25750 });
25751 let mut cx = EditorTestContext::new(cx).await;
25752 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25753 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25754
25755 // test correct indent after newline on comment
25756 cx.set_state(indoc! {"
25757 # COMMENT:ˇ
25758 "});
25759 cx.update_editor(|editor, window, cx| {
25760 editor.newline(&Newline, window, cx);
25761 });
25762 cx.assert_editor_state(indoc! {"
25763 # COMMENT:
25764 ˇ
25765 "});
25766
25767 // test correct indent after newline in brackets
25768 cx.set_state(indoc! {"
25769 {ˇ}
25770 "});
25771 cx.update_editor(|editor, window, cx| {
25772 editor.newline(&Newline, window, cx);
25773 });
25774 cx.run_until_parked();
25775 cx.assert_editor_state(indoc! {"
25776 {
25777 ˇ
25778 }
25779 "});
25780
25781 cx.set_state(indoc! {"
25782 (ˇ)
25783 "});
25784 cx.update_editor(|editor, window, cx| {
25785 editor.newline(&Newline, window, cx);
25786 });
25787 cx.run_until_parked();
25788 cx.assert_editor_state(indoc! {"
25789 (
25790 ˇ
25791 )
25792 "});
25793
25794 // do not indent after empty lists or dictionaries
25795 cx.set_state(indoc! {"
25796 a = []ˇ
25797 "});
25798 cx.update_editor(|editor, window, cx| {
25799 editor.newline(&Newline, window, cx);
25800 });
25801 cx.run_until_parked();
25802 cx.assert_editor_state(indoc! {"
25803 a = []
25804 ˇ
25805 "});
25806}
25807
25808#[gpui::test]
25809async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25810 init_test(cx, |_| {});
25811
25812 let mut cx = EditorTestContext::new(cx).await;
25813 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25814 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25815
25816 // test cursor move to start of each line on tab
25817 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25818 cx.set_state(indoc! {"
25819 function main() {
25820 ˇ for item in $items; do
25821 ˇ while [ -n \"$item\" ]; do
25822 ˇ if [ \"$value\" -gt 10 ]; then
25823 ˇ continue
25824 ˇ elif [ \"$value\" -lt 0 ]; then
25825 ˇ break
25826 ˇ else
25827 ˇ echo \"$item\"
25828 ˇ fi
25829 ˇ done
25830 ˇ done
25831 ˇ}
25832 "});
25833 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25834 cx.assert_editor_state(indoc! {"
25835 function main() {
25836 ˇfor item in $items; do
25837 ˇwhile [ -n \"$item\" ]; do
25838 ˇif [ \"$value\" -gt 10 ]; then
25839 ˇcontinue
25840 ˇelif [ \"$value\" -lt 0 ]; then
25841 ˇbreak
25842 ˇelse
25843 ˇecho \"$item\"
25844 ˇfi
25845 ˇdone
25846 ˇdone
25847 ˇ}
25848 "});
25849 // test relative indent is preserved when tab
25850 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25851 cx.assert_editor_state(indoc! {"
25852 function main() {
25853 ˇfor item in $items; do
25854 ˇwhile [ -n \"$item\" ]; do
25855 ˇif [ \"$value\" -gt 10 ]; then
25856 ˇcontinue
25857 ˇelif [ \"$value\" -lt 0 ]; then
25858 ˇbreak
25859 ˇelse
25860 ˇecho \"$item\"
25861 ˇfi
25862 ˇdone
25863 ˇdone
25864 ˇ}
25865 "});
25866
25867 // test cursor move to start of each line on tab
25868 // for `case` statement with patterns
25869 cx.set_state(indoc! {"
25870 function handle() {
25871 ˇ case \"$1\" in
25872 ˇ start)
25873 ˇ echo \"a\"
25874 ˇ ;;
25875 ˇ stop)
25876 ˇ echo \"b\"
25877 ˇ ;;
25878 ˇ *)
25879 ˇ echo \"c\"
25880 ˇ ;;
25881 ˇ esac
25882 ˇ}
25883 "});
25884 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25885 cx.assert_editor_state(indoc! {"
25886 function handle() {
25887 ˇcase \"$1\" in
25888 ˇstart)
25889 ˇecho \"a\"
25890 ˇ;;
25891 ˇstop)
25892 ˇecho \"b\"
25893 ˇ;;
25894 ˇ*)
25895 ˇecho \"c\"
25896 ˇ;;
25897 ˇesac
25898 ˇ}
25899 "});
25900}
25901
25902#[gpui::test]
25903async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25904 init_test(cx, |_| {});
25905
25906 let mut cx = EditorTestContext::new(cx).await;
25907 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25909
25910 // test indents on comment insert
25911 cx.set_state(indoc! {"
25912 function main() {
25913 ˇ for item in $items; do
25914 ˇ while [ -n \"$item\" ]; do
25915 ˇ if [ \"$value\" -gt 10 ]; then
25916 ˇ continue
25917 ˇ elif [ \"$value\" -lt 0 ]; then
25918 ˇ break
25919 ˇ else
25920 ˇ echo \"$item\"
25921 ˇ fi
25922 ˇ done
25923 ˇ done
25924 ˇ}
25925 "});
25926 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25927 cx.assert_editor_state(indoc! {"
25928 function main() {
25929 #ˇ for item in $items; do
25930 #ˇ while [ -n \"$item\" ]; do
25931 #ˇ if [ \"$value\" -gt 10 ]; then
25932 #ˇ continue
25933 #ˇ elif [ \"$value\" -lt 0 ]; then
25934 #ˇ break
25935 #ˇ else
25936 #ˇ echo \"$item\"
25937 #ˇ fi
25938 #ˇ done
25939 #ˇ done
25940 #ˇ}
25941 "});
25942}
25943
25944#[gpui::test]
25945async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25946 init_test(cx, |_| {});
25947
25948 let mut cx = EditorTestContext::new(cx).await;
25949 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25950 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25951
25952 // test `else` auto outdents when typed inside `if` block
25953 cx.set_state(indoc! {"
25954 if [ \"$1\" = \"test\" ]; then
25955 echo \"foo bar\"
25956 ˇ
25957 "});
25958 cx.update_editor(|editor, window, cx| {
25959 editor.handle_input("else", window, cx);
25960 });
25961 cx.assert_editor_state(indoc! {"
25962 if [ \"$1\" = \"test\" ]; then
25963 echo \"foo bar\"
25964 elseˇ
25965 "});
25966
25967 // test `elif` auto outdents when typed inside `if` block
25968 cx.set_state(indoc! {"
25969 if [ \"$1\" = \"test\" ]; then
25970 echo \"foo bar\"
25971 ˇ
25972 "});
25973 cx.update_editor(|editor, window, cx| {
25974 editor.handle_input("elif", window, cx);
25975 });
25976 cx.assert_editor_state(indoc! {"
25977 if [ \"$1\" = \"test\" ]; then
25978 echo \"foo bar\"
25979 elifˇ
25980 "});
25981
25982 // test `fi` auto outdents when typed inside `else` block
25983 cx.set_state(indoc! {"
25984 if [ \"$1\" = \"test\" ]; then
25985 echo \"foo bar\"
25986 else
25987 echo \"bar baz\"
25988 ˇ
25989 "});
25990 cx.update_editor(|editor, window, cx| {
25991 editor.handle_input("fi", window, cx);
25992 });
25993 cx.assert_editor_state(indoc! {"
25994 if [ \"$1\" = \"test\" ]; then
25995 echo \"foo bar\"
25996 else
25997 echo \"bar baz\"
25998 fiˇ
25999 "});
26000
26001 // test `done` auto outdents when typed inside `while` block
26002 cx.set_state(indoc! {"
26003 while read line; do
26004 echo \"$line\"
26005 ˇ
26006 "});
26007 cx.update_editor(|editor, window, cx| {
26008 editor.handle_input("done", window, cx);
26009 });
26010 cx.assert_editor_state(indoc! {"
26011 while read line; do
26012 echo \"$line\"
26013 doneˇ
26014 "});
26015
26016 // test `done` auto outdents when typed inside `for` block
26017 cx.set_state(indoc! {"
26018 for file in *.txt; do
26019 cat \"$file\"
26020 ˇ
26021 "});
26022 cx.update_editor(|editor, window, cx| {
26023 editor.handle_input("done", window, cx);
26024 });
26025 cx.assert_editor_state(indoc! {"
26026 for file in *.txt; do
26027 cat \"$file\"
26028 doneˇ
26029 "});
26030
26031 // test `esac` auto outdents when typed inside `case` block
26032 cx.set_state(indoc! {"
26033 case \"$1\" in
26034 start)
26035 echo \"foo bar\"
26036 ;;
26037 stop)
26038 echo \"bar baz\"
26039 ;;
26040 ˇ
26041 "});
26042 cx.update_editor(|editor, window, cx| {
26043 editor.handle_input("esac", window, cx);
26044 });
26045 cx.assert_editor_state(indoc! {"
26046 case \"$1\" in
26047 start)
26048 echo \"foo bar\"
26049 ;;
26050 stop)
26051 echo \"bar baz\"
26052 ;;
26053 esacˇ
26054 "});
26055
26056 // test `*)` auto outdents when typed inside `case` block
26057 cx.set_state(indoc! {"
26058 case \"$1\" in
26059 start)
26060 echo \"foo bar\"
26061 ;;
26062 ˇ
26063 "});
26064 cx.update_editor(|editor, window, cx| {
26065 editor.handle_input("*)", window, cx);
26066 });
26067 cx.assert_editor_state(indoc! {"
26068 case \"$1\" in
26069 start)
26070 echo \"foo bar\"
26071 ;;
26072 *)ˇ
26073 "});
26074
26075 // test `fi` outdents to correct level with nested if blocks
26076 cx.set_state(indoc! {"
26077 if [ \"$1\" = \"test\" ]; then
26078 echo \"outer if\"
26079 if [ \"$2\" = \"debug\" ]; then
26080 echo \"inner if\"
26081 ˇ
26082 "});
26083 cx.update_editor(|editor, window, cx| {
26084 editor.handle_input("fi", window, cx);
26085 });
26086 cx.assert_editor_state(indoc! {"
26087 if [ \"$1\" = \"test\" ]; then
26088 echo \"outer if\"
26089 if [ \"$2\" = \"debug\" ]; then
26090 echo \"inner if\"
26091 fiˇ
26092 "});
26093}
26094
26095#[gpui::test]
26096async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26097 init_test(cx, |_| {});
26098 update_test_language_settings(cx, |settings| {
26099 settings.defaults.extend_comment_on_newline = Some(false);
26100 });
26101 let mut cx = EditorTestContext::new(cx).await;
26102 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26103 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26104
26105 // test correct indent after newline on comment
26106 cx.set_state(indoc! {"
26107 # COMMENT:ˇ
26108 "});
26109 cx.update_editor(|editor, window, cx| {
26110 editor.newline(&Newline, window, cx);
26111 });
26112 cx.assert_editor_state(indoc! {"
26113 # COMMENT:
26114 ˇ
26115 "});
26116
26117 // test correct indent after newline after `then`
26118 cx.set_state(indoc! {"
26119
26120 if [ \"$1\" = \"test\" ]; thenˇ
26121 "});
26122 cx.update_editor(|editor, window, cx| {
26123 editor.newline(&Newline, window, cx);
26124 });
26125 cx.run_until_parked();
26126 cx.assert_editor_state(indoc! {"
26127
26128 if [ \"$1\" = \"test\" ]; then
26129 ˇ
26130 "});
26131
26132 // test correct indent after newline after `else`
26133 cx.set_state(indoc! {"
26134 if [ \"$1\" = \"test\" ]; then
26135 elseˇ
26136 "});
26137 cx.update_editor(|editor, window, cx| {
26138 editor.newline(&Newline, window, cx);
26139 });
26140 cx.run_until_parked();
26141 cx.assert_editor_state(indoc! {"
26142 if [ \"$1\" = \"test\" ]; then
26143 else
26144 ˇ
26145 "});
26146
26147 // test correct indent after newline after `elif`
26148 cx.set_state(indoc! {"
26149 if [ \"$1\" = \"test\" ]; then
26150 elifˇ
26151 "});
26152 cx.update_editor(|editor, window, cx| {
26153 editor.newline(&Newline, window, cx);
26154 });
26155 cx.run_until_parked();
26156 cx.assert_editor_state(indoc! {"
26157 if [ \"$1\" = \"test\" ]; then
26158 elif
26159 ˇ
26160 "});
26161
26162 // test correct indent after newline after `do`
26163 cx.set_state(indoc! {"
26164 for file in *.txt; doˇ
26165 "});
26166 cx.update_editor(|editor, window, cx| {
26167 editor.newline(&Newline, window, cx);
26168 });
26169 cx.run_until_parked();
26170 cx.assert_editor_state(indoc! {"
26171 for file in *.txt; do
26172 ˇ
26173 "});
26174
26175 // test correct indent after newline after case pattern
26176 cx.set_state(indoc! {"
26177 case \"$1\" in
26178 start)ˇ
26179 "});
26180 cx.update_editor(|editor, window, cx| {
26181 editor.newline(&Newline, window, cx);
26182 });
26183 cx.run_until_parked();
26184 cx.assert_editor_state(indoc! {"
26185 case \"$1\" in
26186 start)
26187 ˇ
26188 "});
26189
26190 // test correct indent after newline after case pattern
26191 cx.set_state(indoc! {"
26192 case \"$1\" in
26193 start)
26194 ;;
26195 *)ˇ
26196 "});
26197 cx.update_editor(|editor, window, cx| {
26198 editor.newline(&Newline, window, cx);
26199 });
26200 cx.run_until_parked();
26201 cx.assert_editor_state(indoc! {"
26202 case \"$1\" in
26203 start)
26204 ;;
26205 *)
26206 ˇ
26207 "});
26208
26209 // test correct indent after newline after function opening brace
26210 cx.set_state(indoc! {"
26211 function test() {ˇ}
26212 "});
26213 cx.update_editor(|editor, window, cx| {
26214 editor.newline(&Newline, window, cx);
26215 });
26216 cx.run_until_parked();
26217 cx.assert_editor_state(indoc! {"
26218 function test() {
26219 ˇ
26220 }
26221 "});
26222
26223 // test no extra indent after semicolon on same line
26224 cx.set_state(indoc! {"
26225 echo \"test\";ˇ
26226 "});
26227 cx.update_editor(|editor, window, cx| {
26228 editor.newline(&Newline, window, cx);
26229 });
26230 cx.run_until_parked();
26231 cx.assert_editor_state(indoc! {"
26232 echo \"test\";
26233 ˇ
26234 "});
26235}
26236
26237fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26238 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26239 point..point
26240}
26241
26242#[track_caller]
26243fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26244 let (text, ranges) = marked_text_ranges(marked_text, true);
26245 assert_eq!(editor.text(cx), text);
26246 assert_eq!(
26247 editor.selections.ranges(&editor.display_snapshot(cx)),
26248 ranges
26249 .iter()
26250 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26251 .collect::<Vec<_>>(),
26252 "Assert selections are {}",
26253 marked_text
26254 );
26255}
26256
26257pub fn handle_signature_help_request(
26258 cx: &mut EditorLspTestContext,
26259 mocked_response: lsp::SignatureHelp,
26260) -> impl Future<Output = ()> + use<> {
26261 let mut request =
26262 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26263 let mocked_response = mocked_response.clone();
26264 async move { Ok(Some(mocked_response)) }
26265 });
26266
26267 async move {
26268 request.next().await;
26269 }
26270}
26271
26272#[track_caller]
26273pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26274 cx.update_editor(|editor, _, _| {
26275 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26276 let entries = menu.entries.borrow();
26277 let entries = entries
26278 .iter()
26279 .map(|entry| entry.string.as_str())
26280 .collect::<Vec<_>>();
26281 assert_eq!(entries, expected);
26282 } else {
26283 panic!("Expected completions menu");
26284 }
26285 });
26286}
26287
26288#[gpui::test]
26289async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26290 init_test(cx, |_| {});
26291 let mut cx = EditorLspTestContext::new_rust(
26292 lsp::ServerCapabilities {
26293 completion_provider: Some(lsp::CompletionOptions {
26294 ..Default::default()
26295 }),
26296 ..Default::default()
26297 },
26298 cx,
26299 )
26300 .await;
26301 cx.lsp
26302 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26303 Ok(Some(lsp::CompletionResponse::Array(vec![
26304 lsp::CompletionItem {
26305 label: "unsafe".into(),
26306 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26307 range: lsp::Range {
26308 start: lsp::Position {
26309 line: 0,
26310 character: 9,
26311 },
26312 end: lsp::Position {
26313 line: 0,
26314 character: 11,
26315 },
26316 },
26317 new_text: "unsafe".to_string(),
26318 })),
26319 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26320 ..Default::default()
26321 },
26322 ])))
26323 });
26324
26325 cx.update_editor(|editor, _, cx| {
26326 editor.project().unwrap().update(cx, |project, cx| {
26327 project.snippets().update(cx, |snippets, _cx| {
26328 snippets.add_snippet_for_test(
26329 None,
26330 PathBuf::from("test_snippets.json"),
26331 vec![
26332 Arc::new(project::snippet_provider::Snippet {
26333 prefix: vec![
26334 "unlimited word count".to_string(),
26335 "unlimit word count".to_string(),
26336 "unlimited unknown".to_string(),
26337 ],
26338 body: "this is many words".to_string(),
26339 description: Some("description".to_string()),
26340 name: "multi-word snippet test".to_string(),
26341 }),
26342 Arc::new(project::snippet_provider::Snippet {
26343 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26344 body: "fewer words".to_string(),
26345 description: Some("alt description".to_string()),
26346 name: "other name".to_string(),
26347 }),
26348 Arc::new(project::snippet_provider::Snippet {
26349 prefix: vec!["ab aa".to_string()],
26350 body: "abcd".to_string(),
26351 description: None,
26352 name: "alphabet".to_string(),
26353 }),
26354 ],
26355 );
26356 });
26357 })
26358 });
26359
26360 let get_completions = |cx: &mut EditorLspTestContext| {
26361 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26362 Some(CodeContextMenu::Completions(context_menu)) => {
26363 let entries = context_menu.entries.borrow();
26364 entries
26365 .iter()
26366 .map(|entry| entry.string.clone())
26367 .collect_vec()
26368 }
26369 _ => vec![],
26370 })
26371 };
26372
26373 // snippets:
26374 // @foo
26375 // foo bar
26376 //
26377 // when typing:
26378 //
26379 // when typing:
26380 // - if I type a symbol "open the completions with snippets only"
26381 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26382 //
26383 // stuff we need:
26384 // - filtering logic change?
26385 // - remember how far back the completion started.
26386
26387 let test_cases: &[(&str, &[&str])] = &[
26388 (
26389 "un",
26390 &[
26391 "unsafe",
26392 "unlimit word count",
26393 "unlimited unknown",
26394 "unlimited word count",
26395 "unsnip",
26396 ],
26397 ),
26398 (
26399 "u ",
26400 &[
26401 "unlimit word count",
26402 "unlimited unknown",
26403 "unlimited word count",
26404 ],
26405 ),
26406 ("u a", &["ab aa", "unsafe"]), // unsAfe
26407 (
26408 "u u",
26409 &[
26410 "unsafe",
26411 "unlimit word count",
26412 "unlimited unknown", // ranked highest among snippets
26413 "unlimited word count",
26414 "unsnip",
26415 ],
26416 ),
26417 ("uw c", &["unlimit word count", "unlimited word count"]),
26418 (
26419 "u w",
26420 &[
26421 "unlimit word count",
26422 "unlimited word count",
26423 "unlimited unknown",
26424 ],
26425 ),
26426 ("u w ", &["unlimit word count", "unlimited word count"]),
26427 (
26428 "u ",
26429 &[
26430 "unlimit word count",
26431 "unlimited unknown",
26432 "unlimited word count",
26433 ],
26434 ),
26435 ("wor", &[]),
26436 ("uf", &["unsafe"]),
26437 ("af", &["unsafe"]),
26438 ("afu", &[]),
26439 (
26440 "ue",
26441 &["unsafe", "unlimited unknown", "unlimited word count"],
26442 ),
26443 ("@", &["@few"]),
26444 ("@few", &["@few"]),
26445 ("@ ", &[]),
26446 ("a@", &["@few"]),
26447 ("a@f", &["@few", "unsafe"]),
26448 ("a@fw", &["@few"]),
26449 ("a", &["ab aa", "unsafe"]),
26450 ("aa", &["ab aa"]),
26451 ("aaa", &["ab aa"]),
26452 ("ab", &["ab aa"]),
26453 ("ab ", &["ab aa"]),
26454 ("ab a", &["ab aa", "unsafe"]),
26455 ("ab ab", &["ab aa"]),
26456 ("ab ab aa", &["ab aa"]),
26457 ];
26458
26459 for &(input_to_simulate, expected_completions) in test_cases {
26460 cx.set_state("fn a() { ˇ }\n");
26461 for c in input_to_simulate.split("") {
26462 cx.simulate_input(c);
26463 cx.run_until_parked();
26464 }
26465 let expected_completions = expected_completions
26466 .iter()
26467 .map(|s| s.to_string())
26468 .collect_vec();
26469 assert_eq!(
26470 get_completions(&mut cx),
26471 expected_completions,
26472 "< actual / expected >, input = {input_to_simulate:?}",
26473 );
26474 }
26475}
26476
26477/// Handle completion request passing a marked string specifying where the completion
26478/// should be triggered from using '|' character, what range should be replaced, and what completions
26479/// should be returned using '<' and '>' to delimit the range.
26480///
26481/// Also see `handle_completion_request_with_insert_and_replace`.
26482#[track_caller]
26483pub fn handle_completion_request(
26484 marked_string: &str,
26485 completions: Vec<&'static str>,
26486 is_incomplete: bool,
26487 counter: Arc<AtomicUsize>,
26488 cx: &mut EditorLspTestContext,
26489) -> impl Future<Output = ()> {
26490 let complete_from_marker: TextRangeMarker = '|'.into();
26491 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26492 let (_, mut marked_ranges) = marked_text_ranges_by(
26493 marked_string,
26494 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26495 );
26496
26497 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26498 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26499 ));
26500 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26501 let replace_range =
26502 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26503
26504 let mut request =
26505 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26506 let completions = completions.clone();
26507 counter.fetch_add(1, atomic::Ordering::Release);
26508 async move {
26509 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26510 assert_eq!(
26511 params.text_document_position.position,
26512 complete_from_position
26513 );
26514 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26515 is_incomplete,
26516 item_defaults: None,
26517 items: completions
26518 .iter()
26519 .map(|completion_text| lsp::CompletionItem {
26520 label: completion_text.to_string(),
26521 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26522 range: replace_range,
26523 new_text: completion_text.to_string(),
26524 })),
26525 ..Default::default()
26526 })
26527 .collect(),
26528 })))
26529 }
26530 });
26531
26532 async move {
26533 request.next().await;
26534 }
26535}
26536
26537/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26538/// given instead, which also contains an `insert` range.
26539///
26540/// This function uses markers to define ranges:
26541/// - `|` marks the cursor position
26542/// - `<>` marks the replace range
26543/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26544pub fn handle_completion_request_with_insert_and_replace(
26545 cx: &mut EditorLspTestContext,
26546 marked_string: &str,
26547 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26548 counter: Arc<AtomicUsize>,
26549) -> impl Future<Output = ()> {
26550 let complete_from_marker: TextRangeMarker = '|'.into();
26551 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26552 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26553
26554 let (_, mut marked_ranges) = marked_text_ranges_by(
26555 marked_string,
26556 vec![
26557 complete_from_marker.clone(),
26558 replace_range_marker.clone(),
26559 insert_range_marker.clone(),
26560 ],
26561 );
26562
26563 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26564 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26565 ));
26566 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26567 let replace_range =
26568 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26569
26570 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26571 Some(ranges) if !ranges.is_empty() => {
26572 let range1 = ranges[0].clone();
26573 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26574 }
26575 _ => lsp::Range {
26576 start: replace_range.start,
26577 end: complete_from_position,
26578 },
26579 };
26580
26581 let mut request =
26582 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26583 let completions = completions.clone();
26584 counter.fetch_add(1, atomic::Ordering::Release);
26585 async move {
26586 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26587 assert_eq!(
26588 params.text_document_position.position, complete_from_position,
26589 "marker `|` position doesn't match",
26590 );
26591 Ok(Some(lsp::CompletionResponse::Array(
26592 completions
26593 .iter()
26594 .map(|(label, new_text)| lsp::CompletionItem {
26595 label: label.to_string(),
26596 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26597 lsp::InsertReplaceEdit {
26598 insert: insert_range,
26599 replace: replace_range,
26600 new_text: new_text.to_string(),
26601 },
26602 )),
26603 ..Default::default()
26604 })
26605 .collect(),
26606 )))
26607 }
26608 });
26609
26610 async move {
26611 request.next().await;
26612 }
26613}
26614
26615fn handle_resolve_completion_request(
26616 cx: &mut EditorLspTestContext,
26617 edits: Option<Vec<(&'static str, &'static str)>>,
26618) -> impl Future<Output = ()> {
26619 let edits = edits.map(|edits| {
26620 edits
26621 .iter()
26622 .map(|(marked_string, new_text)| {
26623 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26624 let replace_range = cx.to_lsp_range(
26625 MultiBufferOffset(marked_ranges[0].start)
26626 ..MultiBufferOffset(marked_ranges[0].end),
26627 );
26628 lsp::TextEdit::new(replace_range, new_text.to_string())
26629 })
26630 .collect::<Vec<_>>()
26631 });
26632
26633 let mut request =
26634 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26635 let edits = edits.clone();
26636 async move {
26637 Ok(lsp::CompletionItem {
26638 additional_text_edits: edits,
26639 ..Default::default()
26640 })
26641 }
26642 });
26643
26644 async move {
26645 request.next().await;
26646 }
26647}
26648
26649pub(crate) fn update_test_language_settings(
26650 cx: &mut TestAppContext,
26651 f: impl Fn(&mut AllLanguageSettingsContent),
26652) {
26653 cx.update(|cx| {
26654 SettingsStore::update_global(cx, |store, cx| {
26655 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26656 });
26657 });
26658}
26659
26660pub(crate) fn update_test_project_settings(
26661 cx: &mut TestAppContext,
26662 f: impl Fn(&mut ProjectSettingsContent),
26663) {
26664 cx.update(|cx| {
26665 SettingsStore::update_global(cx, |store, cx| {
26666 store.update_user_settings(cx, |settings| f(&mut settings.project));
26667 });
26668 });
26669}
26670
26671pub(crate) fn update_test_editor_settings(
26672 cx: &mut TestAppContext,
26673 f: impl Fn(&mut EditorSettingsContent),
26674) {
26675 cx.update(|cx| {
26676 SettingsStore::update_global(cx, |store, cx| {
26677 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26678 })
26679 })
26680}
26681
26682pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26683 cx.update(|cx| {
26684 assets::Assets.load_test_fonts(cx);
26685 let store = SettingsStore::test(cx);
26686 cx.set_global(store);
26687 theme::init(theme::LoadThemes::JustBase, cx);
26688 release_channel::init(semver::Version::new(0, 0, 0), cx);
26689 crate::init(cx);
26690 });
26691 zlog::init_test();
26692 update_test_language_settings(cx, f);
26693}
26694
26695#[track_caller]
26696fn assert_hunk_revert(
26697 not_reverted_text_with_selections: &str,
26698 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26699 expected_reverted_text_with_selections: &str,
26700 base_text: &str,
26701 cx: &mut EditorLspTestContext,
26702) {
26703 cx.set_state(not_reverted_text_with_selections);
26704 cx.set_head_text(base_text);
26705 cx.executor().run_until_parked();
26706
26707 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26708 let snapshot = editor.snapshot(window, cx);
26709 let reverted_hunk_statuses = snapshot
26710 .buffer_snapshot()
26711 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26712 .map(|hunk| hunk.status().kind)
26713 .collect::<Vec<_>>();
26714
26715 editor.git_restore(&Default::default(), window, cx);
26716 reverted_hunk_statuses
26717 });
26718 cx.executor().run_until_parked();
26719 cx.assert_editor_state(expected_reverted_text_with_selections);
26720 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26721}
26722
26723#[gpui::test(iterations = 10)]
26724async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26725 init_test(cx, |_| {});
26726
26727 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26728 let counter = diagnostic_requests.clone();
26729
26730 let fs = FakeFs::new(cx.executor());
26731 fs.insert_tree(
26732 path!("/a"),
26733 json!({
26734 "first.rs": "fn main() { let a = 5; }",
26735 "second.rs": "// Test file",
26736 }),
26737 )
26738 .await;
26739
26740 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26741 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26742 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26743
26744 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26745 language_registry.add(rust_lang());
26746 let mut fake_servers = language_registry.register_fake_lsp(
26747 "Rust",
26748 FakeLspAdapter {
26749 capabilities: lsp::ServerCapabilities {
26750 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26751 lsp::DiagnosticOptions {
26752 identifier: None,
26753 inter_file_dependencies: true,
26754 workspace_diagnostics: true,
26755 work_done_progress_options: Default::default(),
26756 },
26757 )),
26758 ..Default::default()
26759 },
26760 ..Default::default()
26761 },
26762 );
26763
26764 let editor = workspace
26765 .update(cx, |workspace, window, cx| {
26766 workspace.open_abs_path(
26767 PathBuf::from(path!("/a/first.rs")),
26768 OpenOptions::default(),
26769 window,
26770 cx,
26771 )
26772 })
26773 .unwrap()
26774 .await
26775 .unwrap()
26776 .downcast::<Editor>()
26777 .unwrap();
26778 let fake_server = fake_servers.next().await.unwrap();
26779 let server_id = fake_server.server.server_id();
26780 let mut first_request = fake_server
26781 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26782 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26783 let result_id = Some(new_result_id.to_string());
26784 assert_eq!(
26785 params.text_document.uri,
26786 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26787 );
26788 async move {
26789 Ok(lsp::DocumentDiagnosticReportResult::Report(
26790 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26791 related_documents: None,
26792 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26793 items: Vec::new(),
26794 result_id,
26795 },
26796 }),
26797 ))
26798 }
26799 });
26800
26801 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
26802 project.update(cx, |project, cx| {
26803 let buffer_id = editor
26804 .read(cx)
26805 .buffer()
26806 .read(cx)
26807 .as_singleton()
26808 .expect("created a singleton buffer")
26809 .read(cx)
26810 .remote_id();
26811 let buffer_result_id = project
26812 .lsp_store()
26813 .read(cx)
26814 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
26815 assert_eq!(expected, buffer_result_id);
26816 });
26817 };
26818
26819 ensure_result_id(None, cx);
26820 cx.executor().advance_clock(Duration::from_millis(60));
26821 cx.executor().run_until_parked();
26822 assert_eq!(
26823 diagnostic_requests.load(atomic::Ordering::Acquire),
26824 1,
26825 "Opening file should trigger diagnostic request"
26826 );
26827 first_request
26828 .next()
26829 .await
26830 .expect("should have sent the first diagnostics pull request");
26831 ensure_result_id(Some(SharedString::new("1")), cx);
26832
26833 // Editing should trigger diagnostics
26834 editor.update_in(cx, |editor, window, cx| {
26835 editor.handle_input("2", window, cx)
26836 });
26837 cx.executor().advance_clock(Duration::from_millis(60));
26838 cx.executor().run_until_parked();
26839 assert_eq!(
26840 diagnostic_requests.load(atomic::Ordering::Acquire),
26841 2,
26842 "Editing should trigger diagnostic request"
26843 );
26844 ensure_result_id(Some(SharedString::new("2")), cx);
26845
26846 // Moving cursor should not trigger diagnostic request
26847 editor.update_in(cx, |editor, window, cx| {
26848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26849 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26850 });
26851 });
26852 cx.executor().advance_clock(Duration::from_millis(60));
26853 cx.executor().run_until_parked();
26854 assert_eq!(
26855 diagnostic_requests.load(atomic::Ordering::Acquire),
26856 2,
26857 "Cursor movement should not trigger diagnostic request"
26858 );
26859 ensure_result_id(Some(SharedString::new("2")), cx);
26860 // Multiple rapid edits should be debounced
26861 for _ in 0..5 {
26862 editor.update_in(cx, |editor, window, cx| {
26863 editor.handle_input("x", window, cx)
26864 });
26865 }
26866 cx.executor().advance_clock(Duration::from_millis(60));
26867 cx.executor().run_until_parked();
26868
26869 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26870 assert!(
26871 final_requests <= 4,
26872 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26873 );
26874 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
26875}
26876
26877#[gpui::test]
26878async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26879 // Regression test for issue #11671
26880 // Previously, adding a cursor after moving multiple cursors would reset
26881 // the cursor count instead of adding to the existing cursors.
26882 init_test(cx, |_| {});
26883 let mut cx = EditorTestContext::new(cx).await;
26884
26885 // Create a simple buffer with cursor at start
26886 cx.set_state(indoc! {"
26887 ˇaaaa
26888 bbbb
26889 cccc
26890 dddd
26891 eeee
26892 ffff
26893 gggg
26894 hhhh"});
26895
26896 // Add 2 cursors below (so we have 3 total)
26897 cx.update_editor(|editor, window, cx| {
26898 editor.add_selection_below(&Default::default(), window, cx);
26899 editor.add_selection_below(&Default::default(), window, cx);
26900 });
26901
26902 // Verify we have 3 cursors
26903 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26904 assert_eq!(
26905 initial_count, 3,
26906 "Should have 3 cursors after adding 2 below"
26907 );
26908
26909 // Move down one line
26910 cx.update_editor(|editor, window, cx| {
26911 editor.move_down(&MoveDown, window, cx);
26912 });
26913
26914 // Add another cursor below
26915 cx.update_editor(|editor, window, cx| {
26916 editor.add_selection_below(&Default::default(), window, cx);
26917 });
26918
26919 // Should now have 4 cursors (3 original + 1 new)
26920 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26921 assert_eq!(
26922 final_count, 4,
26923 "Should have 4 cursors after moving and adding another"
26924 );
26925}
26926
26927#[gpui::test]
26928async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26929 init_test(cx, |_| {});
26930
26931 let mut cx = EditorTestContext::new(cx).await;
26932
26933 cx.set_state(indoc!(
26934 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26935 Second line here"#
26936 ));
26937
26938 cx.update_editor(|editor, window, cx| {
26939 // Enable soft wrapping with a narrow width to force soft wrapping and
26940 // confirm that more than 2 rows are being displayed.
26941 editor.set_wrap_width(Some(100.0.into()), cx);
26942 assert!(editor.display_text(cx).lines().count() > 2);
26943
26944 editor.add_selection_below(
26945 &AddSelectionBelow {
26946 skip_soft_wrap: true,
26947 },
26948 window,
26949 cx,
26950 );
26951
26952 assert_eq!(
26953 display_ranges(editor, cx),
26954 &[
26955 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26956 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26957 ]
26958 );
26959
26960 editor.add_selection_above(
26961 &AddSelectionAbove {
26962 skip_soft_wrap: true,
26963 },
26964 window,
26965 cx,
26966 );
26967
26968 assert_eq!(
26969 display_ranges(editor, cx),
26970 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26971 );
26972
26973 editor.add_selection_below(
26974 &AddSelectionBelow {
26975 skip_soft_wrap: false,
26976 },
26977 window,
26978 cx,
26979 );
26980
26981 assert_eq!(
26982 display_ranges(editor, cx),
26983 &[
26984 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26985 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26986 ]
26987 );
26988
26989 editor.add_selection_above(
26990 &AddSelectionAbove {
26991 skip_soft_wrap: false,
26992 },
26993 window,
26994 cx,
26995 );
26996
26997 assert_eq!(
26998 display_ranges(editor, cx),
26999 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27000 );
27001 });
27002}
27003
27004#[gpui::test]
27005async fn test_insert_snippet(cx: &mut TestAppContext) {
27006 init_test(cx, |_| {});
27007 let mut cx = EditorTestContext::new(cx).await;
27008
27009 cx.update_editor(|editor, _, cx| {
27010 editor.project().unwrap().update(cx, |project, cx| {
27011 project.snippets().update(cx, |snippets, _cx| {
27012 let snippet = project::snippet_provider::Snippet {
27013 prefix: vec![], // no prefix needed!
27014 body: "an Unspecified".to_string(),
27015 description: Some("shhhh it's a secret".to_string()),
27016 name: "super secret snippet".to_string(),
27017 };
27018 snippets.add_snippet_for_test(
27019 None,
27020 PathBuf::from("test_snippets.json"),
27021 vec![Arc::new(snippet)],
27022 );
27023
27024 let snippet = project::snippet_provider::Snippet {
27025 prefix: vec![], // no prefix needed!
27026 body: " Location".to_string(),
27027 description: Some("the word 'location'".to_string()),
27028 name: "location word".to_string(),
27029 };
27030 snippets.add_snippet_for_test(
27031 Some("Markdown".to_string()),
27032 PathBuf::from("test_snippets.json"),
27033 vec![Arc::new(snippet)],
27034 );
27035 });
27036 })
27037 });
27038
27039 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27040
27041 cx.update_editor(|editor, window, cx| {
27042 editor.insert_snippet_at_selections(
27043 &InsertSnippet {
27044 language: None,
27045 name: Some("super secret snippet".to_string()),
27046 snippet: None,
27047 },
27048 window,
27049 cx,
27050 );
27051
27052 // Language is specified in the action,
27053 // so the buffer language does not need to match
27054 editor.insert_snippet_at_selections(
27055 &InsertSnippet {
27056 language: Some("Markdown".to_string()),
27057 name: Some("location word".to_string()),
27058 snippet: None,
27059 },
27060 window,
27061 cx,
27062 );
27063
27064 editor.insert_snippet_at_selections(
27065 &InsertSnippet {
27066 language: None,
27067 name: None,
27068 snippet: Some("$0 after".to_string()),
27069 },
27070 window,
27071 cx,
27072 );
27073 });
27074
27075 cx.assert_editor_state(
27076 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27077 );
27078}
27079
27080#[gpui::test(iterations = 10)]
27081async fn test_document_colors(cx: &mut TestAppContext) {
27082 let expected_color = Rgba {
27083 r: 0.33,
27084 g: 0.33,
27085 b: 0.33,
27086 a: 0.33,
27087 };
27088
27089 init_test(cx, |_| {});
27090
27091 let fs = FakeFs::new(cx.executor());
27092 fs.insert_tree(
27093 path!("/a"),
27094 json!({
27095 "first.rs": "fn main() { let a = 5; }",
27096 }),
27097 )
27098 .await;
27099
27100 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27101 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27102 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27103
27104 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27105 language_registry.add(rust_lang());
27106 let mut fake_servers = language_registry.register_fake_lsp(
27107 "Rust",
27108 FakeLspAdapter {
27109 capabilities: lsp::ServerCapabilities {
27110 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27111 ..lsp::ServerCapabilities::default()
27112 },
27113 name: "rust-analyzer",
27114 ..FakeLspAdapter::default()
27115 },
27116 );
27117 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27118 "Rust",
27119 FakeLspAdapter {
27120 capabilities: lsp::ServerCapabilities {
27121 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27122 ..lsp::ServerCapabilities::default()
27123 },
27124 name: "not-rust-analyzer",
27125 ..FakeLspAdapter::default()
27126 },
27127 );
27128
27129 let editor = workspace
27130 .update(cx, |workspace, window, cx| {
27131 workspace.open_abs_path(
27132 PathBuf::from(path!("/a/first.rs")),
27133 OpenOptions::default(),
27134 window,
27135 cx,
27136 )
27137 })
27138 .unwrap()
27139 .await
27140 .unwrap()
27141 .downcast::<Editor>()
27142 .unwrap();
27143 let fake_language_server = fake_servers.next().await.unwrap();
27144 let fake_language_server_without_capabilities =
27145 fake_servers_without_capabilities.next().await.unwrap();
27146 let requests_made = Arc::new(AtomicUsize::new(0));
27147 let closure_requests_made = Arc::clone(&requests_made);
27148 let mut color_request_handle = fake_language_server
27149 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27150 let requests_made = Arc::clone(&closure_requests_made);
27151 async move {
27152 assert_eq!(
27153 params.text_document.uri,
27154 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27155 );
27156 requests_made.fetch_add(1, atomic::Ordering::Release);
27157 Ok(vec![
27158 lsp::ColorInformation {
27159 range: lsp::Range {
27160 start: lsp::Position {
27161 line: 0,
27162 character: 0,
27163 },
27164 end: lsp::Position {
27165 line: 0,
27166 character: 1,
27167 },
27168 },
27169 color: lsp::Color {
27170 red: 0.33,
27171 green: 0.33,
27172 blue: 0.33,
27173 alpha: 0.33,
27174 },
27175 },
27176 lsp::ColorInformation {
27177 range: lsp::Range {
27178 start: lsp::Position {
27179 line: 0,
27180 character: 0,
27181 },
27182 end: lsp::Position {
27183 line: 0,
27184 character: 1,
27185 },
27186 },
27187 color: lsp::Color {
27188 red: 0.33,
27189 green: 0.33,
27190 blue: 0.33,
27191 alpha: 0.33,
27192 },
27193 },
27194 ])
27195 }
27196 });
27197
27198 let _handle = fake_language_server_without_capabilities
27199 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27200 panic!("Should not be called");
27201 });
27202 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27203 color_request_handle.next().await.unwrap();
27204 cx.run_until_parked();
27205 assert_eq!(
27206 1,
27207 requests_made.load(atomic::Ordering::Acquire),
27208 "Should query for colors once per editor open"
27209 );
27210 editor.update_in(cx, |editor, _, cx| {
27211 assert_eq!(
27212 vec![expected_color],
27213 extract_color_inlays(editor, cx),
27214 "Should have an initial inlay"
27215 );
27216 });
27217
27218 // opening another file in a split should not influence the LSP query counter
27219 workspace
27220 .update(cx, |workspace, window, cx| {
27221 assert_eq!(
27222 workspace.panes().len(),
27223 1,
27224 "Should have one pane with one editor"
27225 );
27226 workspace.move_item_to_pane_in_direction(
27227 &MoveItemToPaneInDirection {
27228 direction: SplitDirection::Right,
27229 focus: false,
27230 clone: true,
27231 },
27232 window,
27233 cx,
27234 );
27235 })
27236 .unwrap();
27237 cx.run_until_parked();
27238 workspace
27239 .update(cx, |workspace, _, cx| {
27240 let panes = workspace.panes();
27241 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27242 for pane in panes {
27243 let editor = pane
27244 .read(cx)
27245 .active_item()
27246 .and_then(|item| item.downcast::<Editor>())
27247 .expect("Should have opened an editor in each split");
27248 let editor_file = editor
27249 .read(cx)
27250 .buffer()
27251 .read(cx)
27252 .as_singleton()
27253 .expect("test deals with singleton buffers")
27254 .read(cx)
27255 .file()
27256 .expect("test buffese should have a file")
27257 .path();
27258 assert_eq!(
27259 editor_file.as_ref(),
27260 rel_path("first.rs"),
27261 "Both editors should be opened for the same file"
27262 )
27263 }
27264 })
27265 .unwrap();
27266
27267 cx.executor().advance_clock(Duration::from_millis(500));
27268 let save = editor.update_in(cx, |editor, window, cx| {
27269 editor.move_to_end(&MoveToEnd, window, cx);
27270 editor.handle_input("dirty", window, cx);
27271 editor.save(
27272 SaveOptions {
27273 format: true,
27274 autosave: true,
27275 },
27276 project.clone(),
27277 window,
27278 cx,
27279 )
27280 });
27281 save.await.unwrap();
27282
27283 color_request_handle.next().await.unwrap();
27284 cx.run_until_parked();
27285 assert_eq!(
27286 2,
27287 requests_made.load(atomic::Ordering::Acquire),
27288 "Should query for colors once per save (deduplicated) and once per formatting after save"
27289 );
27290
27291 drop(editor);
27292 let close = workspace
27293 .update(cx, |workspace, window, cx| {
27294 workspace.active_pane().update(cx, |pane, cx| {
27295 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27296 })
27297 })
27298 .unwrap();
27299 close.await.unwrap();
27300 let close = workspace
27301 .update(cx, |workspace, window, cx| {
27302 workspace.active_pane().update(cx, |pane, cx| {
27303 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27304 })
27305 })
27306 .unwrap();
27307 close.await.unwrap();
27308 assert_eq!(
27309 2,
27310 requests_made.load(atomic::Ordering::Acquire),
27311 "After saving and closing all editors, no extra requests should be made"
27312 );
27313 workspace
27314 .update(cx, |workspace, _, cx| {
27315 assert!(
27316 workspace.active_item(cx).is_none(),
27317 "Should close all editors"
27318 )
27319 })
27320 .unwrap();
27321
27322 workspace
27323 .update(cx, |workspace, window, cx| {
27324 workspace.active_pane().update(cx, |pane, cx| {
27325 pane.navigate_backward(&workspace::GoBack, window, cx);
27326 })
27327 })
27328 .unwrap();
27329 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27330 cx.run_until_parked();
27331 let editor = workspace
27332 .update(cx, |workspace, _, cx| {
27333 workspace
27334 .active_item(cx)
27335 .expect("Should have reopened the editor again after navigating back")
27336 .downcast::<Editor>()
27337 .expect("Should be an editor")
27338 })
27339 .unwrap();
27340
27341 assert_eq!(
27342 2,
27343 requests_made.load(atomic::Ordering::Acquire),
27344 "Cache should be reused on buffer close and reopen"
27345 );
27346 editor.update(cx, |editor, cx| {
27347 assert_eq!(
27348 vec![expected_color],
27349 extract_color_inlays(editor, cx),
27350 "Should have an initial inlay"
27351 );
27352 });
27353
27354 drop(color_request_handle);
27355 let closure_requests_made = Arc::clone(&requests_made);
27356 let mut empty_color_request_handle = fake_language_server
27357 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27358 let requests_made = Arc::clone(&closure_requests_made);
27359 async move {
27360 assert_eq!(
27361 params.text_document.uri,
27362 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27363 );
27364 requests_made.fetch_add(1, atomic::Ordering::Release);
27365 Ok(Vec::new())
27366 }
27367 });
27368 let save = editor.update_in(cx, |editor, window, cx| {
27369 editor.move_to_end(&MoveToEnd, window, cx);
27370 editor.handle_input("dirty_again", window, cx);
27371 editor.save(
27372 SaveOptions {
27373 format: false,
27374 autosave: true,
27375 },
27376 project.clone(),
27377 window,
27378 cx,
27379 )
27380 });
27381 save.await.unwrap();
27382
27383 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27384 empty_color_request_handle.next().await.unwrap();
27385 cx.run_until_parked();
27386 assert_eq!(
27387 3,
27388 requests_made.load(atomic::Ordering::Acquire),
27389 "Should query for colors once per save only, as formatting was not requested"
27390 );
27391 editor.update(cx, |editor, cx| {
27392 assert_eq!(
27393 Vec::<Rgba>::new(),
27394 extract_color_inlays(editor, cx),
27395 "Should clear all colors when the server returns an empty response"
27396 );
27397 });
27398}
27399
27400#[gpui::test]
27401async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27402 init_test(cx, |_| {});
27403 let (editor, cx) = cx.add_window_view(Editor::single_line);
27404 editor.update_in(cx, |editor, window, cx| {
27405 editor.set_text("oops\n\nwow\n", window, cx)
27406 });
27407 cx.run_until_parked();
27408 editor.update(cx, |editor, cx| {
27409 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27410 });
27411 editor.update(cx, |editor, cx| {
27412 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27413 });
27414 cx.run_until_parked();
27415 editor.update(cx, |editor, cx| {
27416 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27417 });
27418}
27419
27420#[gpui::test]
27421async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27422 init_test(cx, |_| {});
27423
27424 cx.update(|cx| {
27425 register_project_item::<Editor>(cx);
27426 });
27427
27428 let fs = FakeFs::new(cx.executor());
27429 fs.insert_tree("/root1", json!({})).await;
27430 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27431 .await;
27432
27433 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27434 let (workspace, cx) =
27435 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27436
27437 let worktree_id = project.update(cx, |project, cx| {
27438 project.worktrees(cx).next().unwrap().read(cx).id()
27439 });
27440
27441 let handle = workspace
27442 .update_in(cx, |workspace, window, cx| {
27443 let project_path = (worktree_id, rel_path("one.pdf"));
27444 workspace.open_path(project_path, None, true, window, cx)
27445 })
27446 .await
27447 .unwrap();
27448
27449 assert_eq!(
27450 handle.to_any_view().entity_type(),
27451 TypeId::of::<InvalidItemView>()
27452 );
27453}
27454
27455#[gpui::test]
27456async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27457 init_test(cx, |_| {});
27458
27459 let language = Arc::new(Language::new(
27460 LanguageConfig::default(),
27461 Some(tree_sitter_rust::LANGUAGE.into()),
27462 ));
27463
27464 // Test hierarchical sibling navigation
27465 let text = r#"
27466 fn outer() {
27467 if condition {
27468 let a = 1;
27469 }
27470 let b = 2;
27471 }
27472
27473 fn another() {
27474 let c = 3;
27475 }
27476 "#;
27477
27478 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27479 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27480 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27481
27482 // Wait for parsing to complete
27483 editor
27484 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27485 .await;
27486
27487 editor.update_in(cx, |editor, window, cx| {
27488 // Start by selecting "let a = 1;" inside the if block
27489 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27490 s.select_display_ranges([
27491 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27492 ]);
27493 });
27494
27495 let initial_selection = editor
27496 .selections
27497 .display_ranges(&editor.display_snapshot(cx));
27498 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27499
27500 // Test select next sibling - should move up levels to find the next sibling
27501 // Since "let a = 1;" has no siblings in the if block, it should move up
27502 // to find "let b = 2;" which is a sibling of the if block
27503 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27504 let next_selection = editor
27505 .selections
27506 .display_ranges(&editor.display_snapshot(cx));
27507
27508 // Should have a selection and it should be different from the initial
27509 assert_eq!(
27510 next_selection.len(),
27511 1,
27512 "Should have one selection after next"
27513 );
27514 assert_ne!(
27515 next_selection[0], initial_selection[0],
27516 "Next sibling selection should be different"
27517 );
27518
27519 // Test hierarchical navigation by going to the end of the current function
27520 // and trying to navigate to the next function
27521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27522 s.select_display_ranges([
27523 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27524 ]);
27525 });
27526
27527 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27528 let function_next_selection = editor
27529 .selections
27530 .display_ranges(&editor.display_snapshot(cx));
27531
27532 // Should move to the next function
27533 assert_eq!(
27534 function_next_selection.len(),
27535 1,
27536 "Should have one selection after function next"
27537 );
27538
27539 // Test select previous sibling navigation
27540 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27541 let prev_selection = editor
27542 .selections
27543 .display_ranges(&editor.display_snapshot(cx));
27544
27545 // Should have a selection and it should be different
27546 assert_eq!(
27547 prev_selection.len(),
27548 1,
27549 "Should have one selection after prev"
27550 );
27551 assert_ne!(
27552 prev_selection[0], function_next_selection[0],
27553 "Previous sibling selection should be different from next"
27554 );
27555 });
27556}
27557
27558#[gpui::test]
27559async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27560 init_test(cx, |_| {});
27561
27562 let mut cx = EditorTestContext::new(cx).await;
27563 cx.set_state(
27564 "let ˇvariable = 42;
27565let another = variable + 1;
27566let result = variable * 2;",
27567 );
27568
27569 // Set up document highlights manually (simulating LSP response)
27570 cx.update_editor(|editor, _window, cx| {
27571 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27572
27573 // Create highlights for "variable" occurrences
27574 let highlight_ranges = [
27575 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27576 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27577 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27578 ];
27579
27580 let anchor_ranges: Vec<_> = highlight_ranges
27581 .iter()
27582 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27583 .collect();
27584
27585 editor.highlight_background::<DocumentHighlightRead>(
27586 &anchor_ranges,
27587 |_, theme| theme.colors().editor_document_highlight_read_background,
27588 cx,
27589 );
27590 });
27591
27592 // Go to next highlight - should move to second "variable"
27593 cx.update_editor(|editor, window, cx| {
27594 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27595 });
27596 cx.assert_editor_state(
27597 "let variable = 42;
27598let another = ˇvariable + 1;
27599let result = variable * 2;",
27600 );
27601
27602 // Go to next highlight - should move to third "variable"
27603 cx.update_editor(|editor, window, cx| {
27604 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27605 });
27606 cx.assert_editor_state(
27607 "let variable = 42;
27608let another = variable + 1;
27609let result = ˇvariable * 2;",
27610 );
27611
27612 // Go to next highlight - should stay at third "variable" (no wrap-around)
27613 cx.update_editor(|editor, window, cx| {
27614 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27615 });
27616 cx.assert_editor_state(
27617 "let variable = 42;
27618let another = variable + 1;
27619let result = ˇvariable * 2;",
27620 );
27621
27622 // Now test going backwards from third position
27623 cx.update_editor(|editor, window, cx| {
27624 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27625 });
27626 cx.assert_editor_state(
27627 "let variable = 42;
27628let another = ˇvariable + 1;
27629let result = variable * 2;",
27630 );
27631
27632 // Go to previous highlight - should move to first "variable"
27633 cx.update_editor(|editor, window, cx| {
27634 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27635 });
27636 cx.assert_editor_state(
27637 "let ˇvariable = 42;
27638let another = variable + 1;
27639let result = variable * 2;",
27640 );
27641
27642 // Go to previous highlight - should stay on first "variable"
27643 cx.update_editor(|editor, window, cx| {
27644 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27645 });
27646 cx.assert_editor_state(
27647 "let ˇvariable = 42;
27648let another = variable + 1;
27649let result = variable * 2;",
27650 );
27651}
27652
27653#[gpui::test]
27654async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27655 cx: &mut gpui::TestAppContext,
27656) {
27657 init_test(cx, |_| {});
27658
27659 let url = "https://zed.dev";
27660
27661 let markdown_language = Arc::new(Language::new(
27662 LanguageConfig {
27663 name: "Markdown".into(),
27664 ..LanguageConfig::default()
27665 },
27666 None,
27667 ));
27668
27669 let mut cx = EditorTestContext::new(cx).await;
27670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27671 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27672
27673 cx.update_editor(|editor, window, cx| {
27674 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27675 editor.paste(&Paste, window, cx);
27676 });
27677
27678 cx.assert_editor_state(&format!(
27679 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27680 ));
27681}
27682
27683#[gpui::test]
27684async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
27685 init_test(cx, |_| {});
27686
27687 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
27688 let mut cx = EditorTestContext::new(cx).await;
27689
27690 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27691
27692 // Case 1: Test if adding a character with multi cursors preserves nested list indents
27693 cx.set_state(&indoc! {"
27694 - [ ] Item 1
27695 - [ ] Item 1.a
27696 - [ˇ] Item 2
27697 - [ˇ] Item 2.a
27698 - [ˇ] Item 2.b
27699 "
27700 });
27701 cx.update_editor(|editor, window, cx| {
27702 editor.handle_input("x", window, cx);
27703 });
27704 cx.run_until_parked();
27705 cx.assert_editor_state(indoc! {"
27706 - [ ] Item 1
27707 - [ ] Item 1.a
27708 - [xˇ] Item 2
27709 - [xˇ] Item 2.a
27710 - [xˇ] Item 2.b
27711 "
27712 });
27713
27714 // Case 2: Test adding new line after nested list preserves indent of previous line
27715 cx.set_state(&indoc! {"
27716 - [ ] Item 1
27717 - [ ] Item 1.a
27718 - [x] Item 2
27719 - [x] Item 2.a
27720 - [x] Item 2.bˇ"
27721 });
27722 cx.update_editor(|editor, window, cx| {
27723 editor.newline(&Newline, window, cx);
27724 });
27725 cx.assert_editor_state(indoc! {"
27726 - [ ] Item 1
27727 - [ ] Item 1.a
27728 - [x] Item 2
27729 - [x] Item 2.a
27730 - [x] Item 2.b
27731 ˇ"
27732 });
27733
27734 // Case 3: Test adding a new nested list item preserves indent
27735 cx.set_state(&indoc! {"
27736 - [ ] Item 1
27737 - [ ] Item 1.a
27738 - [x] Item 2
27739 - [x] Item 2.a
27740 - [x] Item 2.b
27741 ˇ"
27742 });
27743 cx.update_editor(|editor, window, cx| {
27744 editor.handle_input("-", window, cx);
27745 });
27746 cx.run_until_parked();
27747 cx.assert_editor_state(indoc! {"
27748 - [ ] Item 1
27749 - [ ] Item 1.a
27750 - [x] Item 2
27751 - [x] Item 2.a
27752 - [x] Item 2.b
27753 -ˇ"
27754 });
27755 cx.update_editor(|editor, window, cx| {
27756 editor.handle_input(" [x] Item 2.c", window, cx);
27757 });
27758 cx.run_until_parked();
27759 cx.assert_editor_state(indoc! {"
27760 - [ ] Item 1
27761 - [ ] Item 1.a
27762 - [x] Item 2
27763 - [x] Item 2.a
27764 - [x] Item 2.b
27765 - [x] Item 2.cˇ"
27766 });
27767
27768 // Case 4: Test adding new line after nested ordered list preserves indent of previous line
27769 cx.set_state(indoc! {"
27770 1. Item 1
27771 1. Item 1.a
27772 2. Item 2
27773 1. Item 2.a
27774 2. Item 2.bˇ"
27775 });
27776 cx.update_editor(|editor, window, cx| {
27777 editor.newline(&Newline, window, cx);
27778 });
27779 cx.assert_editor_state(indoc! {"
27780 1. Item 1
27781 1. Item 1.a
27782 2. Item 2
27783 1. Item 2.a
27784 2. Item 2.b
27785 ˇ"
27786 });
27787
27788 // Case 5: Adding new ordered list item preserves indent
27789 cx.set_state(indoc! {"
27790 1. Item 1
27791 1. Item 1.a
27792 2. Item 2
27793 1. Item 2.a
27794 2. Item 2.b
27795 ˇ"
27796 });
27797 cx.update_editor(|editor, window, cx| {
27798 editor.handle_input("3", window, cx);
27799 });
27800 cx.run_until_parked();
27801 cx.assert_editor_state(indoc! {"
27802 1. Item 1
27803 1. Item 1.a
27804 2. Item 2
27805 1. Item 2.a
27806 2. Item 2.b
27807 3ˇ"
27808 });
27809 cx.update_editor(|editor, window, cx| {
27810 editor.handle_input(".", window, cx);
27811 });
27812 cx.run_until_parked();
27813 cx.assert_editor_state(indoc! {"
27814 1. Item 1
27815 1. Item 1.a
27816 2. Item 2
27817 1. Item 2.a
27818 2. Item 2.b
27819 3.ˇ"
27820 });
27821 cx.update_editor(|editor, window, cx| {
27822 editor.handle_input(" Item 2.c", window, cx);
27823 });
27824 cx.run_until_parked();
27825 cx.assert_editor_state(indoc! {"
27826 1. Item 1
27827 1. Item 1.a
27828 2. Item 2
27829 1. Item 2.a
27830 2. Item 2.b
27831 3. Item 2.cˇ"
27832 });
27833
27834 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
27835 cx.set_state(indoc! {"
27836 - Item 1
27837 - Item 1.a
27838 - Item 1.a
27839 ˇ"});
27840 cx.update_editor(|editor, window, cx| {
27841 editor.handle_input("-", window, cx);
27842 });
27843 cx.run_until_parked();
27844 cx.assert_editor_state(indoc! {"
27845 - Item 1
27846 - Item 1.a
27847 - Item 1.a
27848 -ˇ"});
27849
27850 // Case 7: Test blockquote newline preserves something
27851 cx.set_state(indoc! {"
27852 > Item 1ˇ"
27853 });
27854 cx.update_editor(|editor, window, cx| {
27855 editor.newline(&Newline, window, cx);
27856 });
27857 cx.assert_editor_state(indoc! {"
27858 > Item 1
27859 ˇ"
27860 });
27861}
27862
27863#[gpui::test]
27864async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
27865 cx: &mut gpui::TestAppContext,
27866) {
27867 init_test(cx, |_| {});
27868
27869 let url = "https://zed.dev";
27870
27871 let markdown_language = Arc::new(Language::new(
27872 LanguageConfig {
27873 name: "Markdown".into(),
27874 ..LanguageConfig::default()
27875 },
27876 None,
27877 ));
27878
27879 let mut cx = EditorTestContext::new(cx).await;
27880 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27881 cx.set_state(&format!(
27882 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
27883 ));
27884
27885 cx.update_editor(|editor, window, cx| {
27886 editor.copy(&Copy, window, cx);
27887 });
27888
27889 cx.set_state(&format!(
27890 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
27891 ));
27892
27893 cx.update_editor(|editor, window, cx| {
27894 editor.paste(&Paste, window, cx);
27895 });
27896
27897 cx.assert_editor_state(&format!(
27898 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
27899 ));
27900}
27901
27902#[gpui::test]
27903async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
27904 cx: &mut gpui::TestAppContext,
27905) {
27906 init_test(cx, |_| {});
27907
27908 let url = "https://zed.dev";
27909
27910 let markdown_language = Arc::new(Language::new(
27911 LanguageConfig {
27912 name: "Markdown".into(),
27913 ..LanguageConfig::default()
27914 },
27915 None,
27916 ));
27917
27918 let mut cx = EditorTestContext::new(cx).await;
27919 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27920 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
27921
27922 cx.update_editor(|editor, window, cx| {
27923 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27924 editor.paste(&Paste, window, cx);
27925 });
27926
27927 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
27928}
27929
27930#[gpui::test]
27931async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
27932 cx: &mut gpui::TestAppContext,
27933) {
27934 init_test(cx, |_| {});
27935
27936 let text = "Awesome";
27937
27938 let markdown_language = Arc::new(Language::new(
27939 LanguageConfig {
27940 name: "Markdown".into(),
27941 ..LanguageConfig::default()
27942 },
27943 None,
27944 ));
27945
27946 let mut cx = EditorTestContext::new(cx).await;
27947 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27948 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
27949
27950 cx.update_editor(|editor, window, cx| {
27951 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
27952 editor.paste(&Paste, window, cx);
27953 });
27954
27955 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
27956}
27957
27958#[gpui::test]
27959async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
27960 cx: &mut gpui::TestAppContext,
27961) {
27962 init_test(cx, |_| {});
27963
27964 let url = "https://zed.dev";
27965
27966 let markdown_language = Arc::new(Language::new(
27967 LanguageConfig {
27968 name: "Rust".into(),
27969 ..LanguageConfig::default()
27970 },
27971 None,
27972 ));
27973
27974 let mut cx = EditorTestContext::new(cx).await;
27975 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27976 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
27977
27978 cx.update_editor(|editor, window, cx| {
27979 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27980 editor.paste(&Paste, window, cx);
27981 });
27982
27983 cx.assert_editor_state(&format!(
27984 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
27985 ));
27986}
27987
27988#[gpui::test]
27989async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
27990 cx: &mut TestAppContext,
27991) {
27992 init_test(cx, |_| {});
27993
27994 let url = "https://zed.dev";
27995
27996 let markdown_language = Arc::new(Language::new(
27997 LanguageConfig {
27998 name: "Markdown".into(),
27999 ..LanguageConfig::default()
28000 },
28001 None,
28002 ));
28003
28004 let (editor, cx) = cx.add_window_view(|window, cx| {
28005 let multi_buffer = MultiBuffer::build_multi(
28006 [
28007 ("this will embed -> link", vec![Point::row_range(0..1)]),
28008 ("this will replace -> link", vec![Point::row_range(0..1)]),
28009 ],
28010 cx,
28011 );
28012 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28014 s.select_ranges(vec![
28015 Point::new(0, 19)..Point::new(0, 23),
28016 Point::new(1, 21)..Point::new(1, 25),
28017 ])
28018 });
28019 let first_buffer_id = multi_buffer
28020 .read(cx)
28021 .excerpt_buffer_ids()
28022 .into_iter()
28023 .next()
28024 .unwrap();
28025 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28026 first_buffer.update(cx, |buffer, cx| {
28027 buffer.set_language(Some(markdown_language.clone()), cx);
28028 });
28029
28030 editor
28031 });
28032 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28033
28034 cx.update_editor(|editor, window, cx| {
28035 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28036 editor.paste(&Paste, window, cx);
28037 });
28038
28039 cx.assert_editor_state(&format!(
28040 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28041 ));
28042}
28043
28044#[gpui::test]
28045async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28046 init_test(cx, |_| {});
28047
28048 let fs = FakeFs::new(cx.executor());
28049 fs.insert_tree(
28050 path!("/project"),
28051 json!({
28052 "first.rs": "# First Document\nSome content here.",
28053 "second.rs": "Plain text content for second file.",
28054 }),
28055 )
28056 .await;
28057
28058 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28059 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28060 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28061
28062 let language = rust_lang();
28063 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28064 language_registry.add(language.clone());
28065 let mut fake_servers = language_registry.register_fake_lsp(
28066 "Rust",
28067 FakeLspAdapter {
28068 ..FakeLspAdapter::default()
28069 },
28070 );
28071
28072 let buffer1 = project
28073 .update(cx, |project, cx| {
28074 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28075 })
28076 .await
28077 .unwrap();
28078 let buffer2 = project
28079 .update(cx, |project, cx| {
28080 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28081 })
28082 .await
28083 .unwrap();
28084
28085 let multi_buffer = cx.new(|cx| {
28086 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28087 multi_buffer.set_excerpts_for_path(
28088 PathKey::for_buffer(&buffer1, cx),
28089 buffer1.clone(),
28090 [Point::zero()..buffer1.read(cx).max_point()],
28091 3,
28092 cx,
28093 );
28094 multi_buffer.set_excerpts_for_path(
28095 PathKey::for_buffer(&buffer2, cx),
28096 buffer2.clone(),
28097 [Point::zero()..buffer1.read(cx).max_point()],
28098 3,
28099 cx,
28100 );
28101 multi_buffer
28102 });
28103
28104 let (editor, cx) = cx.add_window_view(|window, cx| {
28105 Editor::new(
28106 EditorMode::full(),
28107 multi_buffer,
28108 Some(project.clone()),
28109 window,
28110 cx,
28111 )
28112 });
28113
28114 let fake_language_server = fake_servers.next().await.unwrap();
28115
28116 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28117
28118 let save = editor.update_in(cx, |editor, window, cx| {
28119 assert!(editor.is_dirty(cx));
28120
28121 editor.save(
28122 SaveOptions {
28123 format: true,
28124 autosave: true,
28125 },
28126 project,
28127 window,
28128 cx,
28129 )
28130 });
28131 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28132 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28133 let mut done_edit_rx = Some(done_edit_rx);
28134 let mut start_edit_tx = Some(start_edit_tx);
28135
28136 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28137 start_edit_tx.take().unwrap().send(()).unwrap();
28138 let done_edit_rx = done_edit_rx.take().unwrap();
28139 async move {
28140 done_edit_rx.await.unwrap();
28141 Ok(None)
28142 }
28143 });
28144
28145 start_edit_rx.await.unwrap();
28146 buffer2
28147 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28148 .unwrap();
28149
28150 done_edit_tx.send(()).unwrap();
28151
28152 save.await.unwrap();
28153 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28154}
28155
28156#[track_caller]
28157fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28158 editor
28159 .all_inlays(cx)
28160 .into_iter()
28161 .filter_map(|inlay| inlay.get_color())
28162 .map(Rgba::from)
28163 .collect()
28164}
28165
28166#[gpui::test]
28167fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28168 init_test(cx, |_| {});
28169
28170 let editor = cx.add_window(|window, cx| {
28171 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28172 build_editor(buffer, window, cx)
28173 });
28174
28175 editor
28176 .update(cx, |editor, window, cx| {
28177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28178 s.select_display_ranges([
28179 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28180 ])
28181 });
28182
28183 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28184
28185 assert_eq!(
28186 editor.display_text(cx),
28187 "line1\nline2\nline2",
28188 "Duplicating last line upward should create duplicate above, not on same line"
28189 );
28190
28191 assert_eq!(
28192 editor
28193 .selections
28194 .display_ranges(&editor.display_snapshot(cx)),
28195 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28196 "Selection should move to the duplicated line"
28197 );
28198 })
28199 .unwrap();
28200}
28201
28202#[gpui::test]
28203async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28204 init_test(cx, |_| {});
28205
28206 let mut cx = EditorTestContext::new(cx).await;
28207
28208 cx.set_state("line1\nline2ˇ");
28209
28210 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28211
28212 let clipboard_text = cx
28213 .read_from_clipboard()
28214 .and_then(|item| item.text().as_deref().map(str::to_string));
28215
28216 assert_eq!(
28217 clipboard_text,
28218 Some("line2\n".to_string()),
28219 "Copying a line without trailing newline should include a newline"
28220 );
28221
28222 cx.set_state("line1\nˇ");
28223
28224 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28225
28226 cx.assert_editor_state("line1\nline2\nˇ");
28227}
28228
28229#[gpui::test]
28230async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28231 init_test(cx, |_| {});
28232
28233 let mut cx = EditorTestContext::new(cx).await;
28234
28235 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28236
28237 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28238
28239 let clipboard_text = cx
28240 .read_from_clipboard()
28241 .and_then(|item| item.text().as_deref().map(str::to_string));
28242
28243 assert_eq!(
28244 clipboard_text,
28245 Some("line1\nline2\nline3\n".to_string()),
28246 "Copying multiple lines should include a single newline between lines"
28247 );
28248
28249 cx.set_state("lineA\nˇ");
28250
28251 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28252
28253 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28254}
28255
28256#[gpui::test]
28257async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28258 init_test(cx, |_| {});
28259
28260 let mut cx = EditorTestContext::new(cx).await;
28261
28262 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28263
28264 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28265
28266 let clipboard_text = cx
28267 .read_from_clipboard()
28268 .and_then(|item| item.text().as_deref().map(str::to_string));
28269
28270 assert_eq!(
28271 clipboard_text,
28272 Some("line1\nline2\nline3\n".to_string()),
28273 "Copying multiple lines should include a single newline between lines"
28274 );
28275
28276 cx.set_state("lineA\nˇ");
28277
28278 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28279
28280 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28281}
28282
28283#[gpui::test]
28284async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28285 init_test(cx, |_| {});
28286
28287 let mut cx = EditorTestContext::new(cx).await;
28288
28289 cx.set_state("line1\nline2ˇ");
28290 cx.update_editor(|e, window, cx| {
28291 e.set_mode(EditorMode::SingleLine);
28292 assert!(e.key_context(window, cx).contains("end_of_input"));
28293 });
28294 cx.set_state("ˇline1\nline2");
28295 cx.update_editor(|e, window, cx| {
28296 assert!(!e.key_context(window, cx).contains("end_of_input"));
28297 });
28298 cx.set_state("line1ˇ\nline2");
28299 cx.update_editor(|e, window, cx| {
28300 assert!(!e.key_context(window, cx).contains("end_of_input"));
28301 });
28302}
28303
28304#[gpui::test]
28305async fn test_sticky_scroll(cx: &mut TestAppContext) {
28306 init_test(cx, |_| {});
28307 let mut cx = EditorTestContext::new(cx).await;
28308
28309 let buffer = indoc! {"
28310 ˇfn foo() {
28311 let abc = 123;
28312 }
28313 struct Bar;
28314 impl Bar {
28315 fn new() -> Self {
28316 Self
28317 }
28318 }
28319 fn baz() {
28320 }
28321 "};
28322 cx.set_state(&buffer);
28323
28324 cx.update_editor(|e, _, cx| {
28325 e.buffer()
28326 .read(cx)
28327 .as_singleton()
28328 .unwrap()
28329 .update(cx, |buffer, cx| {
28330 buffer.set_language(Some(rust_lang()), cx);
28331 })
28332 });
28333
28334 let mut sticky_headers = |offset: ScrollOffset| {
28335 cx.update_editor(|e, window, cx| {
28336 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28337 let style = e.style(cx).clone();
28338 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28339 .into_iter()
28340 .map(
28341 |StickyHeader {
28342 start_point,
28343 offset,
28344 ..
28345 }| { (start_point, offset) },
28346 )
28347 .collect::<Vec<_>>()
28348 })
28349 };
28350
28351 let fn_foo = Point { row: 0, column: 0 };
28352 let impl_bar = Point { row: 4, column: 0 };
28353 let fn_new = Point { row: 5, column: 4 };
28354
28355 assert_eq!(sticky_headers(0.0), vec![]);
28356 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28357 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28358 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28359 assert_eq!(sticky_headers(2.0), vec![]);
28360 assert_eq!(sticky_headers(2.5), vec![]);
28361 assert_eq!(sticky_headers(3.0), vec![]);
28362 assert_eq!(sticky_headers(3.5), vec![]);
28363 assert_eq!(sticky_headers(4.0), vec![]);
28364 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28365 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28366 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28367 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28368 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28369 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28370 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28371 assert_eq!(sticky_headers(8.0), vec![]);
28372 assert_eq!(sticky_headers(8.5), vec![]);
28373 assert_eq!(sticky_headers(9.0), vec![]);
28374 assert_eq!(sticky_headers(9.5), vec![]);
28375 assert_eq!(sticky_headers(10.0), vec![]);
28376}
28377
28378#[gpui::test]
28379async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
28380 init_test(cx, |_| {});
28381 cx.update(|cx| {
28382 SettingsStore::update_global(cx, |store, cx| {
28383 store.update_user_settings(cx, |settings| {
28384 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
28385 enabled: Some(true),
28386 })
28387 });
28388 });
28389 });
28390 let mut cx = EditorTestContext::new(cx).await;
28391
28392 let line_height = cx.update_editor(|editor, window, cx| {
28393 editor
28394 .style(cx)
28395 .text
28396 .line_height_in_pixels(window.rem_size())
28397 });
28398
28399 let buffer = indoc! {"
28400 ˇfn foo() {
28401 let abc = 123;
28402 }
28403 struct Bar;
28404 impl Bar {
28405 fn new() -> Self {
28406 Self
28407 }
28408 }
28409 fn baz() {
28410 }
28411 "};
28412 cx.set_state(&buffer);
28413
28414 cx.update_editor(|e, _, cx| {
28415 e.buffer()
28416 .read(cx)
28417 .as_singleton()
28418 .unwrap()
28419 .update(cx, |buffer, cx| {
28420 buffer.set_language(Some(rust_lang()), cx);
28421 })
28422 });
28423
28424 let fn_foo = || empty_range(0, 0);
28425 let impl_bar = || empty_range(4, 0);
28426 let fn_new = || empty_range(5, 4);
28427
28428 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
28429 cx.update_editor(|e, window, cx| {
28430 e.scroll(
28431 gpui::Point {
28432 x: 0.,
28433 y: scroll_offset,
28434 },
28435 None,
28436 window,
28437 cx,
28438 );
28439 });
28440 cx.simulate_click(
28441 gpui::Point {
28442 x: px(0.),
28443 y: click_offset as f32 * line_height,
28444 },
28445 Modifiers::none(),
28446 );
28447 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
28448 };
28449
28450 assert_eq!(
28451 scroll_and_click(
28452 4.5, // impl Bar is halfway off the screen
28453 0.0 // click top of screen
28454 ),
28455 // scrolled to impl Bar
28456 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28457 );
28458
28459 assert_eq!(
28460 scroll_and_click(
28461 4.5, // impl Bar is halfway off the screen
28462 0.25 // click middle of impl Bar
28463 ),
28464 // scrolled to impl Bar
28465 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28466 );
28467
28468 assert_eq!(
28469 scroll_and_click(
28470 4.5, // impl Bar is halfway off the screen
28471 1.5 // click below impl Bar (e.g. fn new())
28472 ),
28473 // scrolled to fn new() - this is below the impl Bar header which has persisted
28474 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28475 );
28476
28477 assert_eq!(
28478 scroll_and_click(
28479 5.5, // fn new is halfway underneath impl Bar
28480 0.75 // click on the overlap of impl Bar and fn new()
28481 ),
28482 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28483 );
28484
28485 assert_eq!(
28486 scroll_and_click(
28487 5.5, // fn new is halfway underneath impl Bar
28488 1.25 // click on the visible part of fn new()
28489 ),
28490 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28491 );
28492
28493 assert_eq!(
28494 scroll_and_click(
28495 1.5, // fn foo is halfway off the screen
28496 0.0 // click top of screen
28497 ),
28498 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28499 );
28500
28501 assert_eq!(
28502 scroll_and_click(
28503 1.5, // fn foo is halfway off the screen
28504 0.75 // click visible part of let abc...
28505 )
28506 .0,
28507 // no change in scroll
28508 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28509 (gpui::Point { x: 0., y: 1.5 })
28510 );
28511}
28512
28513#[gpui::test]
28514async fn test_next_prev_reference(cx: &mut TestAppContext) {
28515 const CYCLE_POSITIONS: &[&'static str] = &[
28516 indoc! {"
28517 fn foo() {
28518 let ˇabc = 123;
28519 let x = abc + 1;
28520 let y = abc + 2;
28521 let z = abc + 2;
28522 }
28523 "},
28524 indoc! {"
28525 fn foo() {
28526 let abc = 123;
28527 let x = ˇabc + 1;
28528 let y = abc + 2;
28529 let z = abc + 2;
28530 }
28531 "},
28532 indoc! {"
28533 fn foo() {
28534 let abc = 123;
28535 let x = abc + 1;
28536 let y = ˇabc + 2;
28537 let z = abc + 2;
28538 }
28539 "},
28540 indoc! {"
28541 fn foo() {
28542 let abc = 123;
28543 let x = abc + 1;
28544 let y = abc + 2;
28545 let z = ˇabc + 2;
28546 }
28547 "},
28548 ];
28549
28550 init_test(cx, |_| {});
28551
28552 let mut cx = EditorLspTestContext::new_rust(
28553 lsp::ServerCapabilities {
28554 references_provider: Some(lsp::OneOf::Left(true)),
28555 ..Default::default()
28556 },
28557 cx,
28558 )
28559 .await;
28560
28561 // importantly, the cursor is in the middle
28562 cx.set_state(indoc! {"
28563 fn foo() {
28564 let aˇbc = 123;
28565 let x = abc + 1;
28566 let y = abc + 2;
28567 let z = abc + 2;
28568 }
28569 "});
28570
28571 let reference_ranges = [
28572 lsp::Position::new(1, 8),
28573 lsp::Position::new(2, 12),
28574 lsp::Position::new(3, 12),
28575 lsp::Position::new(4, 12),
28576 ]
28577 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28578
28579 cx.lsp
28580 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
28581 Ok(Some(
28582 reference_ranges
28583 .map(|range| lsp::Location {
28584 uri: params.text_document_position.text_document.uri.clone(),
28585 range,
28586 })
28587 .to_vec(),
28588 ))
28589 });
28590
28591 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
28592 cx.update_editor(|editor, window, cx| {
28593 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
28594 })
28595 .unwrap()
28596 .await
28597 .unwrap()
28598 };
28599
28600 _move(Direction::Next, 1, &mut cx).await;
28601 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28602
28603 _move(Direction::Next, 1, &mut cx).await;
28604 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28605
28606 _move(Direction::Next, 1, &mut cx).await;
28607 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28608
28609 // loops back to the start
28610 _move(Direction::Next, 1, &mut cx).await;
28611 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28612
28613 // loops back to the end
28614 _move(Direction::Prev, 1, &mut cx).await;
28615 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28616
28617 _move(Direction::Prev, 1, &mut cx).await;
28618 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28619
28620 _move(Direction::Prev, 1, &mut cx).await;
28621 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28622
28623 _move(Direction::Prev, 1, &mut cx).await;
28624 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28625
28626 _move(Direction::Next, 3, &mut cx).await;
28627 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28628
28629 _move(Direction::Prev, 2, &mut cx).await;
28630 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28631}
28632
28633#[gpui::test]
28634async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
28635 init_test(cx, |_| {});
28636
28637 let (editor, cx) = cx.add_window_view(|window, cx| {
28638 let multi_buffer = MultiBuffer::build_multi(
28639 [
28640 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28641 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28642 ],
28643 cx,
28644 );
28645 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28646 });
28647
28648 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28649 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28650
28651 cx.assert_excerpts_with_selections(indoc! {"
28652 [EXCERPT]
28653 ˇ1
28654 2
28655 3
28656 [EXCERPT]
28657 1
28658 2
28659 3
28660 "});
28661
28662 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28663 cx.update_editor(|editor, window, cx| {
28664 editor.change_selections(None.into(), window, cx, |s| {
28665 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28666 });
28667 });
28668 cx.assert_excerpts_with_selections(indoc! {"
28669 [EXCERPT]
28670 1
28671 2ˇ
28672 3
28673 [EXCERPT]
28674 1
28675 2
28676 3
28677 "});
28678
28679 cx.update_editor(|editor, window, cx| {
28680 editor
28681 .select_all_matches(&SelectAllMatches, window, cx)
28682 .unwrap();
28683 });
28684 cx.assert_excerpts_with_selections(indoc! {"
28685 [EXCERPT]
28686 1
28687 2ˇ
28688 3
28689 [EXCERPT]
28690 1
28691 2ˇ
28692 3
28693 "});
28694
28695 cx.update_editor(|editor, window, cx| {
28696 editor.handle_input("X", window, cx);
28697 });
28698 cx.assert_excerpts_with_selections(indoc! {"
28699 [EXCERPT]
28700 1
28701 Xˇ
28702 3
28703 [EXCERPT]
28704 1
28705 Xˇ
28706 3
28707 "});
28708
28709 // Scenario 2: Select "2", then fold second buffer before insertion
28710 cx.update_multibuffer(|mb, cx| {
28711 for buffer_id in buffer_ids.iter() {
28712 let buffer = mb.buffer(*buffer_id).unwrap();
28713 buffer.update(cx, |buffer, cx| {
28714 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28715 });
28716 }
28717 });
28718
28719 // Select "2" and select all matches
28720 cx.update_editor(|editor, window, cx| {
28721 editor.change_selections(None.into(), window, cx, |s| {
28722 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28723 });
28724 editor
28725 .select_all_matches(&SelectAllMatches, window, cx)
28726 .unwrap();
28727 });
28728
28729 // Fold second buffer - should remove selections from folded buffer
28730 cx.update_editor(|editor, _, cx| {
28731 editor.fold_buffer(buffer_ids[1], cx);
28732 });
28733 cx.assert_excerpts_with_selections(indoc! {"
28734 [EXCERPT]
28735 1
28736 2ˇ
28737 3
28738 [EXCERPT]
28739 [FOLDED]
28740 "});
28741
28742 // Insert text - should only affect first buffer
28743 cx.update_editor(|editor, window, cx| {
28744 editor.handle_input("Y", window, cx);
28745 });
28746 cx.update_editor(|editor, _, cx| {
28747 editor.unfold_buffer(buffer_ids[1], cx);
28748 });
28749 cx.assert_excerpts_with_selections(indoc! {"
28750 [EXCERPT]
28751 1
28752 Yˇ
28753 3
28754 [EXCERPT]
28755 1
28756 2
28757 3
28758 "});
28759
28760 // Scenario 3: Select "2", then fold first buffer before insertion
28761 cx.update_multibuffer(|mb, cx| {
28762 for buffer_id in buffer_ids.iter() {
28763 let buffer = mb.buffer(*buffer_id).unwrap();
28764 buffer.update(cx, |buffer, cx| {
28765 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28766 });
28767 }
28768 });
28769
28770 // Select "2" and select all matches
28771 cx.update_editor(|editor, window, cx| {
28772 editor.change_selections(None.into(), window, cx, |s| {
28773 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28774 });
28775 editor
28776 .select_all_matches(&SelectAllMatches, window, cx)
28777 .unwrap();
28778 });
28779
28780 // Fold first buffer - should remove selections from folded buffer
28781 cx.update_editor(|editor, _, cx| {
28782 editor.fold_buffer(buffer_ids[0], cx);
28783 });
28784 cx.assert_excerpts_with_selections(indoc! {"
28785 [EXCERPT]
28786 [FOLDED]
28787 [EXCERPT]
28788 1
28789 2ˇ
28790 3
28791 "});
28792
28793 // Insert text - should only affect second buffer
28794 cx.update_editor(|editor, window, cx| {
28795 editor.handle_input("Z", window, cx);
28796 });
28797 cx.update_editor(|editor, _, cx| {
28798 editor.unfold_buffer(buffer_ids[0], cx);
28799 });
28800 cx.assert_excerpts_with_selections(indoc! {"
28801 [EXCERPT]
28802 1
28803 2
28804 3
28805 [EXCERPT]
28806 1
28807 Zˇ
28808 3
28809 "});
28810
28811 // Test correct folded header is selected upon fold
28812 cx.update_editor(|editor, _, cx| {
28813 editor.fold_buffer(buffer_ids[0], cx);
28814 editor.fold_buffer(buffer_ids[1], cx);
28815 });
28816 cx.assert_excerpts_with_selections(indoc! {"
28817 [EXCERPT]
28818 [FOLDED]
28819 [EXCERPT]
28820 ˇ[FOLDED]
28821 "});
28822
28823 // Test selection inside folded buffer unfolds it on type
28824 cx.update_editor(|editor, window, cx| {
28825 editor.handle_input("W", window, cx);
28826 });
28827 cx.update_editor(|editor, _, cx| {
28828 editor.unfold_buffer(buffer_ids[0], cx);
28829 });
28830 cx.assert_excerpts_with_selections(indoc! {"
28831 [EXCERPT]
28832 1
28833 2
28834 3
28835 [EXCERPT]
28836 Wˇ1
28837 Z
28838 3
28839 "});
28840}
28841
28842#[gpui::test]
28843async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
28844 init_test(cx, |_| {});
28845 let mut leader_cx = EditorTestContext::new(cx).await;
28846
28847 let diff_base = indoc!(
28848 r#"
28849 one
28850 two
28851 three
28852 four
28853 five
28854 six
28855 "#
28856 );
28857
28858 let initial_state = indoc!(
28859 r#"
28860 ˇone
28861 two
28862 THREE
28863 four
28864 five
28865 six
28866 "#
28867 );
28868
28869 leader_cx.set_state(initial_state);
28870
28871 leader_cx.set_head_text(&diff_base);
28872 leader_cx.run_until_parked();
28873
28874 let follower = leader_cx.update_multibuffer(|leader, cx| {
28875 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28876 leader.set_all_diff_hunks_expanded(cx);
28877 leader.get_or_create_follower(cx)
28878 });
28879 follower.update(cx, |follower, cx| {
28880 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28881 follower.set_all_diff_hunks_expanded(cx);
28882 });
28883
28884 let follower_editor =
28885 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
28886 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
28887
28888 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
28889 cx.run_until_parked();
28890
28891 leader_cx.assert_editor_state(initial_state);
28892 follower_cx.assert_editor_state(indoc! {
28893 r#"
28894 ˇone
28895 two
28896 three
28897 four
28898 five
28899 six
28900 "#
28901 });
28902
28903 follower_cx.editor(|editor, _window, cx| {
28904 assert!(editor.read_only(cx));
28905 });
28906
28907 leader_cx.update_editor(|editor, _window, cx| {
28908 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
28909 });
28910 cx.run_until_parked();
28911
28912 leader_cx.assert_editor_state(indoc! {
28913 r#"
28914 ˇone
28915 two
28916 THREE
28917 four
28918 FIVE
28919 six
28920 "#
28921 });
28922
28923 follower_cx.assert_editor_state(indoc! {
28924 r#"
28925 ˇone
28926 two
28927 three
28928 four
28929 five
28930 six
28931 "#
28932 });
28933
28934 leader_cx.update_editor(|editor, _window, cx| {
28935 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
28936 });
28937 cx.run_until_parked();
28938
28939 leader_cx.assert_editor_state(indoc! {
28940 r#"
28941 ˇone
28942 two
28943 THREE
28944 four
28945 FIVE
28946 six
28947 SEVEN"#
28948 });
28949
28950 follower_cx.assert_editor_state(indoc! {
28951 r#"
28952 ˇone
28953 two
28954 three
28955 four
28956 five
28957 six
28958 "#
28959 });
28960
28961 leader_cx.update_editor(|editor, window, cx| {
28962 editor.move_down(&MoveDown, window, cx);
28963 editor.refresh_selected_text_highlights(true, window, cx);
28964 });
28965 leader_cx.run_until_parked();
28966}
28967
28968#[gpui::test]
28969async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
28970 init_test(cx, |_| {});
28971 let base_text = "base\n";
28972 let buffer_text = "buffer\n";
28973
28974 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
28975 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
28976
28977 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
28978 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
28979 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
28980 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
28981
28982 let leader = cx.new(|cx| {
28983 let mut leader = MultiBuffer::new(Capability::ReadWrite);
28984 leader.set_all_diff_hunks_expanded(cx);
28985 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28986 leader
28987 });
28988 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
28989 follower.update(cx, |follower, _| {
28990 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28991 });
28992
28993 leader.update(cx, |leader, cx| {
28994 leader.insert_excerpts_after(
28995 ExcerptId::min(),
28996 extra_buffer_2.clone(),
28997 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28998 cx,
28999 );
29000 leader.add_diff(extra_diff_2.clone(), cx);
29001
29002 leader.insert_excerpts_after(
29003 ExcerptId::min(),
29004 extra_buffer_1.clone(),
29005 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29006 cx,
29007 );
29008 leader.add_diff(extra_diff_1.clone(), cx);
29009
29010 leader.insert_excerpts_after(
29011 ExcerptId::min(),
29012 buffer1.clone(),
29013 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29014 cx,
29015 );
29016 leader.add_diff(diff1.clone(), cx);
29017 });
29018
29019 cx.run_until_parked();
29020 let mut cx = cx.add_empty_window();
29021
29022 let leader_editor = cx
29023 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
29024 let follower_editor = cx.new_window_entity(|window, cx| {
29025 Editor::for_multibuffer(follower.clone(), None, window, cx)
29026 });
29027
29028 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
29029 leader_cx.assert_editor_state(indoc! {"
29030 ˇbuffer
29031
29032 dummy text 1
29033
29034 dummy text 2
29035 "});
29036 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
29037 follower_cx.assert_editor_state(indoc! {"
29038 ˇbase
29039
29040
29041 "});
29042}
29043
29044#[gpui::test]
29045async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29046 init_test(cx, |_| {});
29047
29048 let (editor, cx) = cx.add_window_view(|window, cx| {
29049 let multi_buffer = MultiBuffer::build_multi(
29050 [
29051 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29052 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29053 ],
29054 cx,
29055 );
29056 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29057 });
29058
29059 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29060
29061 cx.assert_excerpts_with_selections(indoc! {"
29062 [EXCERPT]
29063 ˇ1
29064 2
29065 3
29066 [EXCERPT]
29067 1
29068 2
29069 3
29070 4
29071 5
29072 6
29073 7
29074 8
29075 9
29076 "});
29077
29078 cx.update_editor(|editor, window, cx| {
29079 editor.change_selections(None.into(), window, cx, |s| {
29080 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29081 });
29082 });
29083
29084 cx.assert_excerpts_with_selections(indoc! {"
29085 [EXCERPT]
29086 1
29087 2
29088 3
29089 [EXCERPT]
29090 1
29091 2
29092 3
29093 4
29094 5
29095 6
29096 ˇ7
29097 8
29098 9
29099 "});
29100
29101 cx.update_editor(|editor, _window, cx| {
29102 editor.set_vertical_scroll_margin(0, cx);
29103 });
29104
29105 cx.update_editor(|editor, window, cx| {
29106 assert_eq!(editor.vertical_scroll_margin(), 0);
29107 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29108 assert_eq!(
29109 editor.snapshot(window, cx).scroll_position(),
29110 gpui::Point::new(0., 12.0)
29111 );
29112 });
29113
29114 cx.update_editor(|editor, _window, cx| {
29115 editor.set_vertical_scroll_margin(3, cx);
29116 });
29117
29118 cx.update_editor(|editor, window, cx| {
29119 assert_eq!(editor.vertical_scroll_margin(), 3);
29120 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29121 assert_eq!(
29122 editor.snapshot(window, cx).scroll_position(),
29123 gpui::Point::new(0., 9.0)
29124 );
29125 });
29126}
29127
29128#[gpui::test]
29129async fn test_find_references_single_case(cx: &mut TestAppContext) {
29130 init_test(cx, |_| {});
29131 let mut cx = EditorLspTestContext::new_rust(
29132 lsp::ServerCapabilities {
29133 references_provider: Some(lsp::OneOf::Left(true)),
29134 ..lsp::ServerCapabilities::default()
29135 },
29136 cx,
29137 )
29138 .await;
29139
29140 let before = indoc!(
29141 r#"
29142 fn main() {
29143 let aˇbc = 123;
29144 let xyz = abc;
29145 }
29146 "#
29147 );
29148 let after = indoc!(
29149 r#"
29150 fn main() {
29151 let abc = 123;
29152 let xyz = ˇabc;
29153 }
29154 "#
29155 );
29156
29157 cx.lsp
29158 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29159 Ok(Some(vec![
29160 lsp::Location {
29161 uri: params.text_document_position.text_document.uri.clone(),
29162 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29163 },
29164 lsp::Location {
29165 uri: params.text_document_position.text_document.uri,
29166 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29167 },
29168 ]))
29169 });
29170
29171 cx.set_state(before);
29172
29173 let action = FindAllReferences {
29174 always_open_multibuffer: false,
29175 };
29176
29177 let navigated = cx
29178 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29179 .expect("should have spawned a task")
29180 .await
29181 .unwrap();
29182
29183 assert_eq!(navigated, Navigated::No);
29184
29185 cx.run_until_parked();
29186
29187 cx.assert_editor_state(after);
29188}