1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, Rgba, TestAppContext, UpdateGlobal, VisualTestContext,
21 WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::markdown_lang;
36use languages::rust_lang;
37use lsp::CompletionParams;
38use multi_buffer::{
39 IndentGuide, MultiBufferFilterMode, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
40};
41use parking_lot::Mutex;
42use pretty_assertions::{assert_eq, assert_ne};
43use project::{
44 FakeFs,
45 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
46 project_settings::LspSettings,
47};
48use serde_json::{self, json};
49use settings::{
50 AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
51 IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
52};
53use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
54use std::{
55 iter,
56 sync::atomic::{self, AtomicUsize},
57};
58use test::build_editor_with_project;
59use text::ToPoint as _;
60use unindent::Unindent;
61use util::{
62 assert_set_eq, path,
63 rel_path::rel_path,
64 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
65 uri,
66};
67use workspace::{
68 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
69 OpenOptions, ViewId,
70 invalid_item_view::InvalidItemView,
71 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
72 register_project_item,
73};
74
75fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
76 editor
77 .selections
78 .display_ranges(&editor.display_snapshot(cx))
79}
80
81#[gpui::test]
82fn test_edit_events(cx: &mut TestAppContext) {
83 init_test(cx, |_| {});
84
85 let buffer = cx.new(|cx| {
86 let mut buffer = language::Buffer::local("123456", cx);
87 buffer.set_group_interval(Duration::from_secs(1));
88 buffer
89 });
90
91 let events = Rc::new(RefCell::new(Vec::new()));
92 let editor1 = cx.add_window({
93 let events = events.clone();
94 |window, cx| {
95 let entity = cx.entity();
96 cx.subscribe_in(
97 &entity,
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor1", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 let editor2 = cx.add_window({
113 let events = events.clone();
114 |window, cx| {
115 cx.subscribe_in(
116 &cx.entity(),
117 window,
118 move |_, _, event: &EditorEvent, _, _| match event {
119 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
120 EditorEvent::BufferEdited => {
121 events.borrow_mut().push(("editor2", "buffer edited"))
122 }
123 _ => {}
124 },
125 )
126 .detach();
127 Editor::for_buffer(buffer.clone(), None, window, cx)
128 }
129 });
130
131 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
132
133 // Mutating editor 1 will emit an `Edited` event only for that editor.
134 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor1", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Mutating editor 2 will emit an `Edited` event only for that editor.
145 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor2", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Undoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Redoing on editor 1 will emit an `Edited` event only for that editor.
167 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor1", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Undoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // Redoing on editor 2 will emit an `Edited` event only for that editor.
189 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
190 assert_eq!(
191 mem::take(&mut *events.borrow_mut()),
192 [
193 ("editor2", "edited"),
194 ("editor1", "buffer edited"),
195 ("editor2", "buffer edited"),
196 ]
197 );
198
199 // No event is emitted when the mutation is a no-op.
200 _ = editor2.update(cx, |editor, window, cx| {
201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
202 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
203 });
204
205 editor.backspace(&Backspace, window, cx);
206 });
207 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
208}
209
210#[gpui::test]
211fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
212 init_test(cx, |_| {});
213
214 let mut now = Instant::now();
215 let group_interval = Duration::from_millis(1);
216 let buffer = cx.new(|cx| {
217 let mut buf = language::Buffer::local("123456", cx);
218 buf.set_group_interval(group_interval);
219 buf
220 });
221 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
222 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
223
224 _ = editor.update(cx, |editor, window, cx| {
225 editor.start_transaction_at(now, window, cx);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
228 });
229
230 editor.insert("cd", window, cx);
231 editor.end_transaction_at(now, cx);
232 assert_eq!(editor.text(cx), "12cd56");
233 assert_eq!(
234 editor.selections.ranges(&editor.display_snapshot(cx)),
235 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
236 );
237
238 editor.start_transaction_at(now, window, cx);
239 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
240 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
241 });
242 editor.insert("e", window, cx);
243 editor.end_transaction_at(now, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(
246 editor.selections.ranges(&editor.display_snapshot(cx)),
247 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
248 );
249
250 now += group_interval + Duration::from_millis(1);
251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
252 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
253 });
254
255 // Simulate an edit in another editor
256 buffer.update(cx, |buffer, cx| {
257 buffer.start_transaction_at(now, cx);
258 buffer.edit(
259 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
260 None,
261 cx,
262 );
263 buffer.edit(
264 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
265 None,
266 cx,
267 );
268 buffer.end_transaction_at(now, cx);
269 });
270
271 assert_eq!(editor.text(cx), "ab2cde6");
272 assert_eq!(
273 editor.selections.ranges(&editor.display_snapshot(cx)),
274 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
275 );
276
277 // Last transaction happened past the group interval in a different editor.
278 // Undo it individually and don't restore selections.
279 editor.undo(&Undo, window, cx);
280 assert_eq!(editor.text(cx), "12cde6");
281 assert_eq!(
282 editor.selections.ranges(&editor.display_snapshot(cx)),
283 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
284 );
285
286 // First two transactions happened within the group interval in this editor.
287 // Undo them together and restore selections.
288 editor.undo(&Undo, window, cx);
289 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
290 assert_eq!(editor.text(cx), "123456");
291 assert_eq!(
292 editor.selections.ranges(&editor.display_snapshot(cx)),
293 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
294 );
295
296 // Redo the first two transactions together.
297 editor.redo(&Redo, window, cx);
298 assert_eq!(editor.text(cx), "12cde6");
299 assert_eq!(
300 editor.selections.ranges(&editor.display_snapshot(cx)),
301 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
302 );
303
304 // Redo the last transaction on its own.
305 editor.redo(&Redo, window, cx);
306 assert_eq!(editor.text(cx), "ab2cde6");
307 assert_eq!(
308 editor.selections.ranges(&editor.display_snapshot(cx)),
309 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
310 );
311
312 // Test empty transactions.
313 editor.start_transaction_at(now, window, cx);
314 editor.end_transaction_at(now, cx);
315 editor.undo(&Undo, window, cx);
316 assert_eq!(editor.text(cx), "12cde6");
317 });
318}
319
320#[gpui::test]
321fn test_ime_composition(cx: &mut TestAppContext) {
322 init_test(cx, |_| {});
323
324 let buffer = cx.new(|cx| {
325 let mut buffer = language::Buffer::local("abcde", cx);
326 // Ensure automatic grouping doesn't occur.
327 buffer.set_group_interval(Duration::ZERO);
328 buffer
329 });
330
331 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
332 cx.add_window(|window, cx| {
333 let mut editor = build_editor(buffer.clone(), window, cx);
334
335 // Start a new IME composition.
336 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
337 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
338 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
339 assert_eq!(editor.text(cx), "äbcde");
340 assert_eq!(
341 editor.marked_text_ranges(cx),
342 Some(vec![
343 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
344 ])
345 );
346
347 // Finalize IME composition.
348 editor.replace_text_in_range(None, "ā", window, cx);
349 assert_eq!(editor.text(cx), "ābcde");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 // IME composition edits are grouped and are undone/redone at once.
353 editor.undo(&Default::default(), window, cx);
354 assert_eq!(editor.text(cx), "abcde");
355 assert_eq!(editor.marked_text_ranges(cx), None);
356 editor.redo(&Default::default(), window, cx);
357 assert_eq!(editor.text(cx), "ābcde");
358 assert_eq!(editor.marked_text_ranges(cx), None);
359
360 // Start a new IME composition.
361 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
362 assert_eq!(
363 editor.marked_text_ranges(cx),
364 Some(vec![
365 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
366 ])
367 );
368
369 // Undoing during an IME composition cancels it.
370 editor.undo(&Default::default(), window, cx);
371 assert_eq!(editor.text(cx), "ābcde");
372 assert_eq!(editor.marked_text_ranges(cx), None);
373
374 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
375 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
376 assert_eq!(editor.text(cx), "ābcdè");
377 assert_eq!(
378 editor.marked_text_ranges(cx),
379 Some(vec![
380 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
381 ])
382 );
383
384 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
385 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
386 assert_eq!(editor.text(cx), "ābcdę");
387 assert_eq!(editor.marked_text_ranges(cx), None);
388
389 // Start a new IME composition with multiple cursors.
390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
391 s.select_ranges([
392 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
393 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
394 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
395 ])
396 });
397 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
398 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
399 assert_eq!(
400 editor.marked_text_ranges(cx),
401 Some(vec![
402 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
403 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
404 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
405 ])
406 );
407
408 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
409 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
410 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
411 assert_eq!(
412 editor.marked_text_ranges(cx),
413 Some(vec![
414 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
415 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
416 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
417 ])
418 );
419
420 // Finalize IME composition with multiple cursors.
421 editor.replace_text_in_range(Some(9..10), "2", window, cx);
422 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
423 assert_eq!(editor.marked_text_ranges(cx), None);
424
425 editor
426 });
427}
428
429#[gpui::test]
430fn test_selection_with_mouse(cx: &mut TestAppContext) {
431 init_test(cx, |_| {});
432
433 let editor = cx.add_window(|window, cx| {
434 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
435 build_editor(buffer, window, cx)
436 });
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
440 });
441 assert_eq!(
442 editor
443 .update(cx, |editor, _, cx| display_ranges(editor, cx))
444 .unwrap(),
445 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(3), 3),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| display_ranges(editor, cx))
461 .unwrap(),
462 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
463 );
464
465 _ = editor.update(cx, |editor, window, cx| {
466 editor.update_selection(
467 DisplayPoint::new(DisplayRow(1), 1),
468 0,
469 gpui::Point::<f32>::default(),
470 window,
471 cx,
472 );
473 });
474
475 assert_eq!(
476 editor
477 .update(cx, |editor, _, cx| display_ranges(editor, cx))
478 .unwrap(),
479 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
480 );
481
482 _ = editor.update(cx, |editor, window, cx| {
483 editor.end_selection(window, cx);
484 editor.update_selection(
485 DisplayPoint::new(DisplayRow(3), 3),
486 0,
487 gpui::Point::<f32>::default(),
488 window,
489 cx,
490 );
491 });
492
493 assert_eq!(
494 editor
495 .update(cx, |editor, _, cx| display_ranges(editor, cx))
496 .unwrap(),
497 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
498 );
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
502 editor.update_selection(
503 DisplayPoint::new(DisplayRow(0), 0),
504 0,
505 gpui::Point::<f32>::default(),
506 window,
507 cx,
508 );
509 });
510
511 assert_eq!(
512 editor
513 .update(cx, |editor, _, cx| display_ranges(editor, cx))
514 .unwrap(),
515 [
516 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
517 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
518 ]
519 );
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.end_selection(window, cx);
523 });
524
525 assert_eq!(
526 editor
527 .update(cx, |editor, _, cx| display_ranges(editor, cx))
528 .unwrap(),
529 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
530 );
531}
532
533#[gpui::test]
534fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
535 init_test(cx, |_| {});
536
537 let editor = cx.add_window(|window, cx| {
538 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
539 build_editor(buffer, window, cx)
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
544 });
545
546 _ = editor.update(cx, |editor, window, cx| {
547 editor.end_selection(window, cx);
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
552 });
553
554 _ = editor.update(cx, |editor, window, cx| {
555 editor.end_selection(window, cx);
556 });
557
558 assert_eq!(
559 editor
560 .update(cx, |editor, _, cx| display_ranges(editor, cx))
561 .unwrap(),
562 [
563 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
564 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
565 ]
566 );
567
568 _ = editor.update(cx, |editor, window, cx| {
569 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
570 });
571
572 _ = editor.update(cx, |editor, window, cx| {
573 editor.end_selection(window, cx);
574 });
575
576 assert_eq!(
577 editor
578 .update(cx, |editor, _, cx| display_ranges(editor, cx))
579 .unwrap(),
580 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
581 );
582}
583
584#[gpui::test]
585fn test_canceling_pending_selection(cx: &mut TestAppContext) {
586 init_test(cx, |_| {});
587
588 let editor = cx.add_window(|window, cx| {
589 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
590 build_editor(buffer, window, cx)
591 });
592
593 _ = editor.update(cx, |editor, window, cx| {
594 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
595 assert_eq!(
596 display_ranges(editor, cx),
597 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
598 );
599 });
600
601 _ = editor.update(cx, |editor, window, cx| {
602 editor.update_selection(
603 DisplayPoint::new(DisplayRow(3), 3),
604 0,
605 gpui::Point::<f32>::default(),
606 window,
607 cx,
608 );
609 assert_eq!(
610 display_ranges(editor, cx),
611 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
612 );
613 });
614
615 _ = editor.update(cx, |editor, window, cx| {
616 editor.cancel(&Cancel, window, cx);
617 editor.update_selection(
618 DisplayPoint::new(DisplayRow(1), 1),
619 0,
620 gpui::Point::<f32>::default(),
621 window,
622 cx,
623 );
624 assert_eq!(
625 display_ranges(editor, cx),
626 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
627 );
628 });
629}
630
631#[gpui::test]
632fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
633 init_test(cx, |_| {});
634
635 let editor = cx.add_window(|window, cx| {
636 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
637 build_editor(buffer, window, cx)
638 });
639
640 _ = editor.update(cx, |editor, window, cx| {
641 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
642 assert_eq!(
643 display_ranges(editor, cx),
644 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
645 );
646
647 editor.move_down(&Default::default(), window, cx);
648 assert_eq!(
649 display_ranges(editor, cx),
650 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
651 );
652
653 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
654 assert_eq!(
655 display_ranges(editor, cx),
656 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
657 );
658
659 editor.move_up(&Default::default(), window, cx);
660 assert_eq!(
661 display_ranges(editor, cx),
662 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
663 );
664 });
665}
666
667#[gpui::test]
668fn test_extending_selection(cx: &mut TestAppContext) {
669 init_test(cx, |_| {});
670
671 let editor = cx.add_window(|window, cx| {
672 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
673 build_editor(buffer, window, cx)
674 });
675
676 _ = editor.update(cx, |editor, window, cx| {
677 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
678 editor.end_selection(window, cx);
679 assert_eq!(
680 display_ranges(editor, cx),
681 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
682 );
683
684 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
685 editor.end_selection(window, cx);
686 assert_eq!(
687 display_ranges(editor, cx),
688 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
689 );
690
691 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
692 editor.end_selection(window, cx);
693 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
694 assert_eq!(
695 display_ranges(editor, cx),
696 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
697 );
698
699 editor.update_selection(
700 DisplayPoint::new(DisplayRow(0), 1),
701 0,
702 gpui::Point::<f32>::default(),
703 window,
704 cx,
705 );
706 editor.end_selection(window, cx);
707 assert_eq!(
708 display_ranges(editor, cx),
709 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
710 );
711
712 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
713 editor.end_selection(window, cx);
714 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
715 editor.end_selection(window, cx);
716 assert_eq!(
717 display_ranges(editor, cx),
718 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
719 );
720
721 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
722 assert_eq!(
723 display_ranges(editor, cx),
724 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
725 );
726
727 editor.update_selection(
728 DisplayPoint::new(DisplayRow(0), 6),
729 0,
730 gpui::Point::<f32>::default(),
731 window,
732 cx,
733 );
734 assert_eq!(
735 display_ranges(editor, cx),
736 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
737 );
738
739 editor.update_selection(
740 DisplayPoint::new(DisplayRow(0), 1),
741 0,
742 gpui::Point::<f32>::default(),
743 window,
744 cx,
745 );
746 editor.end_selection(window, cx);
747 assert_eq!(
748 display_ranges(editor, cx),
749 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
750 );
751 });
752}
753
754#[gpui::test]
755fn test_clone(cx: &mut TestAppContext) {
756 init_test(cx, |_| {});
757
758 let (text, selection_ranges) = marked_text_ranges(
759 indoc! {"
760 one
761 two
762 threeˇ
763 four
764 fiveˇ
765 "},
766 true,
767 );
768
769 let editor = cx.add_window(|window, cx| {
770 let buffer = MultiBuffer::build_simple(&text, cx);
771 build_editor(buffer, window, cx)
772 });
773
774 _ = editor.update(cx, |editor, window, cx| {
775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
776 s.select_ranges(
777 selection_ranges
778 .iter()
779 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
780 )
781 });
782 editor.fold_creases(
783 vec![
784 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
785 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
786 ],
787 true,
788 window,
789 cx,
790 );
791 });
792
793 let cloned_editor = editor
794 .update(cx, |editor, _, cx| {
795 cx.open_window(Default::default(), |window, cx| {
796 cx.new(|cx| editor.clone(window, cx))
797 })
798 })
799 .unwrap()
800 .unwrap();
801
802 let snapshot = editor
803 .update(cx, |e, window, cx| e.snapshot(window, cx))
804 .unwrap();
805 let cloned_snapshot = cloned_editor
806 .update(cx, |e, window, cx| e.snapshot(window, cx))
807 .unwrap();
808
809 assert_eq!(
810 cloned_editor
811 .update(cx, |e, _, cx| e.display_text(cx))
812 .unwrap(),
813 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
814 );
815 assert_eq!(
816 cloned_snapshot
817 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
818 .collect::<Vec<_>>(),
819 snapshot
820 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
821 .collect::<Vec<_>>(),
822 );
823 assert_set_eq!(
824 cloned_editor
825 .update(cx, |editor, _, cx| editor
826 .selections
827 .ranges::<Point>(&editor.display_snapshot(cx)))
828 .unwrap(),
829 editor
830 .update(cx, |editor, _, cx| editor
831 .selections
832 .ranges(&editor.display_snapshot(cx)))
833 .unwrap()
834 );
835 assert_set_eq!(
836 cloned_editor
837 .update(cx, |e, _window, cx| e
838 .selections
839 .display_ranges(&e.display_snapshot(cx)))
840 .unwrap(),
841 editor
842 .update(cx, |e, _, cx| e
843 .selections
844 .display_ranges(&e.display_snapshot(cx)))
845 .unwrap()
846 );
847}
848
849#[gpui::test]
850async fn test_navigation_history(cx: &mut TestAppContext) {
851 init_test(cx, |_| {});
852
853 use workspace::item::Item;
854
855 let fs = FakeFs::new(cx.executor());
856 let project = Project::test(fs, [], cx).await;
857 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
858 let pane = workspace
859 .update(cx, |workspace, _, _| workspace.active_pane().clone())
860 .unwrap();
861
862 _ = workspace.update(cx, |_v, window, cx| {
863 cx.new(|cx| {
864 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
865 let mut editor = build_editor(buffer, window, cx);
866 let handle = cx.entity();
867 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
868
869 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
870 editor.nav_history.as_mut().unwrap().pop_backward(cx)
871 }
872
873 // Move the cursor a small distance.
874 // Nothing is added to the navigation history.
875 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
876 s.select_display_ranges([
877 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
878 ])
879 });
880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
881 s.select_display_ranges([
882 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
883 ])
884 });
885 assert!(pop_history(&mut editor, cx).is_none());
886
887 // Move the cursor a large distance.
888 // The history can jump back to the previous position.
889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
890 s.select_display_ranges([
891 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
892 ])
893 });
894 let nav_entry = pop_history(&mut editor, cx).unwrap();
895 editor.navigate(nav_entry.data.unwrap(), window, cx);
896 assert_eq!(nav_entry.item.id(), cx.entity_id());
897 assert_eq!(
898 editor
899 .selections
900 .display_ranges(&editor.display_snapshot(cx)),
901 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
902 );
903 assert!(pop_history(&mut editor, cx).is_none());
904
905 // Move the cursor a small distance via the mouse.
906 // Nothing is added to the navigation history.
907 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
908 editor.end_selection(window, cx);
909 assert_eq!(
910 editor
911 .selections
912 .display_ranges(&editor.display_snapshot(cx)),
913 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
914 );
915 assert!(pop_history(&mut editor, cx).is_none());
916
917 // Move the cursor a large distance via the mouse.
918 // The history can jump back to the previous position.
919 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
920 editor.end_selection(window, cx);
921 assert_eq!(
922 editor
923 .selections
924 .display_ranges(&editor.display_snapshot(cx)),
925 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
926 );
927 let nav_entry = pop_history(&mut editor, cx).unwrap();
928 editor.navigate(nav_entry.data.unwrap(), window, cx);
929 assert_eq!(nav_entry.item.id(), cx.entity_id());
930 assert_eq!(
931 editor
932 .selections
933 .display_ranges(&editor.display_snapshot(cx)),
934 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
935 );
936 assert!(pop_history(&mut editor, cx).is_none());
937
938 // Set scroll position to check later
939 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
940 let original_scroll_position = editor.scroll_manager.anchor();
941
942 // Jump to the end of the document and adjust scroll
943 editor.move_to_end(&MoveToEnd, window, cx);
944 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
945 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
946
947 let nav_entry = pop_history(&mut editor, cx).unwrap();
948 editor.navigate(nav_entry.data.unwrap(), window, cx);
949 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
950
951 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
952 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
953 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
954 let invalid_point = Point::new(9999, 0);
955 editor.navigate(
956 Box::new(NavigationData {
957 cursor_anchor: invalid_anchor,
958 cursor_position: invalid_point,
959 scroll_anchor: ScrollAnchor {
960 anchor: invalid_anchor,
961 offset: Default::default(),
962 },
963 scroll_top_row: invalid_point.row,
964 }),
965 window,
966 cx,
967 );
968 assert_eq!(
969 editor
970 .selections
971 .display_ranges(&editor.display_snapshot(cx)),
972 &[editor.max_point(cx)..editor.max_point(cx)]
973 );
974 assert_eq!(
975 editor.scroll_position(cx),
976 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
977 );
978
979 editor
980 })
981 });
982}
983
984#[gpui::test]
985fn test_cancel(cx: &mut TestAppContext) {
986 init_test(cx, |_| {});
987
988 let editor = cx.add_window(|window, cx| {
989 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
990 build_editor(buffer, window, cx)
991 });
992
993 _ = editor.update(cx, |editor, window, cx| {
994 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
995 editor.update_selection(
996 DisplayPoint::new(DisplayRow(1), 1),
997 0,
998 gpui::Point::<f32>::default(),
999 window,
1000 cx,
1001 );
1002 editor.end_selection(window, cx);
1003
1004 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1005 editor.update_selection(
1006 DisplayPoint::new(DisplayRow(0), 3),
1007 0,
1008 gpui::Point::<f32>::default(),
1009 window,
1010 cx,
1011 );
1012 editor.end_selection(window, cx);
1013 assert_eq!(
1014 display_ranges(editor, cx),
1015 [
1016 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1017 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1018 ]
1019 );
1020 });
1021
1022 _ = editor.update(cx, |editor, window, cx| {
1023 editor.cancel(&Cancel, window, cx);
1024 assert_eq!(
1025 display_ranges(editor, cx),
1026 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1027 );
1028 });
1029
1030 _ = editor.update(cx, |editor, window, cx| {
1031 editor.cancel(&Cancel, window, cx);
1032 assert_eq!(
1033 display_ranges(editor, cx),
1034 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 impl Foo {
1047 // Hello!
1048
1049 fn a() {
1050 1
1051 }
1052
1053 fn b() {
1054 2
1055 }
1056
1057 fn c() {
1058 3
1059 }
1060 }
1061 "
1062 .unindent(),
1063 cx,
1064 );
1065 build_editor(buffer, window, cx)
1066 });
1067
1068 _ = editor.update(cx, |editor, window, cx| {
1069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1070 s.select_display_ranges([
1071 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1072 ]);
1073 });
1074 editor.fold(&Fold, window, cx);
1075 assert_eq!(
1076 editor.display_text(cx),
1077 "
1078 impl Foo {
1079 // Hello!
1080
1081 fn a() {
1082 1
1083 }
1084
1085 fn b() {⋯
1086 }
1087
1088 fn c() {⋯
1089 }
1090 }
1091 "
1092 .unindent(),
1093 );
1094
1095 editor.fold(&Fold, window, cx);
1096 assert_eq!(
1097 editor.display_text(cx),
1098 "
1099 impl Foo {⋯
1100 }
1101 "
1102 .unindent(),
1103 );
1104
1105 editor.unfold_lines(&UnfoldLines, window, cx);
1106 assert_eq!(
1107 editor.display_text(cx),
1108 "
1109 impl Foo {
1110 // Hello!
1111
1112 fn a() {
1113 1
1114 }
1115
1116 fn b() {⋯
1117 }
1118
1119 fn c() {⋯
1120 }
1121 }
1122 "
1123 .unindent(),
1124 );
1125
1126 editor.unfold_lines(&UnfoldLines, window, cx);
1127 assert_eq!(
1128 editor.display_text(cx),
1129 editor.buffer.read(cx).read(cx).text()
1130 );
1131 });
1132}
1133
1134#[gpui::test]
1135fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1136 init_test(cx, |_| {});
1137
1138 let editor = cx.add_window(|window, cx| {
1139 let buffer = MultiBuffer::build_simple(
1140 &"
1141 class Foo:
1142 # Hello!
1143
1144 def a():
1145 print(1)
1146
1147 def b():
1148 print(2)
1149
1150 def c():
1151 print(3)
1152 "
1153 .unindent(),
1154 cx,
1155 );
1156 build_editor(buffer, window, cx)
1157 });
1158
1159 _ = editor.update(cx, |editor, window, cx| {
1160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1161 s.select_display_ranges([
1162 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1163 ]);
1164 });
1165 editor.fold(&Fold, window, cx);
1166 assert_eq!(
1167 editor.display_text(cx),
1168 "
1169 class Foo:
1170 # Hello!
1171
1172 def a():
1173 print(1)
1174
1175 def b():⋯
1176
1177 def c():⋯
1178 "
1179 .unindent(),
1180 );
1181
1182 editor.fold(&Fold, window, cx);
1183 assert_eq!(
1184 editor.display_text(cx),
1185 "
1186 class Foo:⋯
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.unfold_lines(&UnfoldLines, window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:
1196 # Hello!
1197
1198 def a():
1199 print(1)
1200
1201 def b():⋯
1202
1203 def c():⋯
1204 "
1205 .unindent(),
1206 );
1207
1208 editor.unfold_lines(&UnfoldLines, window, cx);
1209 assert_eq!(
1210 editor.display_text(cx),
1211 editor.buffer.read(cx).read(cx).text()
1212 );
1213 });
1214}
1215
1216#[gpui::test]
1217fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1218 init_test(cx, |_| {});
1219
1220 let editor = cx.add_window(|window, cx| {
1221 let buffer = MultiBuffer::build_simple(
1222 &"
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 def c():
1234 print(3)
1235
1236
1237 "
1238 .unindent(),
1239 cx,
1240 );
1241 build_editor(buffer, window, cx)
1242 });
1243
1244 _ = editor.update(cx, |editor, window, cx| {
1245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1246 s.select_display_ranges([
1247 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1248 ]);
1249 });
1250 editor.fold(&Fold, window, cx);
1251 assert_eq!(
1252 editor.display_text(cx),
1253 "
1254 class Foo:
1255 # Hello!
1256
1257 def a():
1258 print(1)
1259
1260 def b():⋯
1261
1262
1263 def c():⋯
1264
1265
1266 "
1267 .unindent(),
1268 );
1269
1270 editor.fold(&Fold, window, cx);
1271 assert_eq!(
1272 editor.display_text(cx),
1273 "
1274 class Foo:⋯
1275
1276
1277 "
1278 .unindent(),
1279 );
1280
1281 editor.unfold_lines(&UnfoldLines, window, cx);
1282 assert_eq!(
1283 editor.display_text(cx),
1284 "
1285 class Foo:
1286 # Hello!
1287
1288 def a():
1289 print(1)
1290
1291 def b():⋯
1292
1293
1294 def c():⋯
1295
1296
1297 "
1298 .unindent(),
1299 );
1300
1301 editor.unfold_lines(&UnfoldLines, window, cx);
1302 assert_eq!(
1303 editor.display_text(cx),
1304 editor.buffer.read(cx).read(cx).text()
1305 );
1306 });
1307}
1308
1309#[gpui::test]
1310fn test_fold_at_level(cx: &mut TestAppContext) {
1311 init_test(cx, |_| {});
1312
1313 let editor = cx.add_window(|window, cx| {
1314 let buffer = MultiBuffer::build_simple(
1315 &"
1316 class Foo:
1317 # Hello!
1318
1319 def a():
1320 print(1)
1321
1322 def b():
1323 print(2)
1324
1325
1326 class Bar:
1327 # World!
1328
1329 def a():
1330 print(1)
1331
1332 def b():
1333 print(2)
1334
1335
1336 "
1337 .unindent(),
1338 cx,
1339 );
1340 build_editor(buffer, window, cx)
1341 });
1342
1343 _ = editor.update(cx, |editor, window, cx| {
1344 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1345 assert_eq!(
1346 editor.display_text(cx),
1347 "
1348 class Foo:
1349 # Hello!
1350
1351 def a():⋯
1352
1353 def b():⋯
1354
1355
1356 class Bar:
1357 # World!
1358
1359 def a():⋯
1360
1361 def b():⋯
1362
1363
1364 "
1365 .unindent(),
1366 );
1367
1368 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1369 assert_eq!(
1370 editor.display_text(cx),
1371 "
1372 class Foo:⋯
1373
1374
1375 class Bar:⋯
1376
1377
1378 "
1379 .unindent(),
1380 );
1381
1382 editor.unfold_all(&UnfoldAll, window, cx);
1383 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1384 assert_eq!(
1385 editor.display_text(cx),
1386 "
1387 class Foo:
1388 # Hello!
1389
1390 def a():
1391 print(1)
1392
1393 def b():
1394 print(2)
1395
1396
1397 class Bar:
1398 # World!
1399
1400 def a():
1401 print(1)
1402
1403 def b():
1404 print(2)
1405
1406
1407 "
1408 .unindent(),
1409 );
1410
1411 assert_eq!(
1412 editor.display_text(cx),
1413 editor.buffer.read(cx).read(cx).text()
1414 );
1415 let (_, positions) = marked_text_ranges(
1416 &"
1417 class Foo:
1418 # Hello!
1419
1420 def a():
1421 print(1)
1422
1423 def b():
1424 p«riˇ»nt(2)
1425
1426
1427 class Bar:
1428 # World!
1429
1430 def a():
1431 «ˇprint(1)
1432
1433 def b():
1434 print(2)»
1435
1436
1437 "
1438 .unindent(),
1439 true,
1440 );
1441
1442 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1443 s.select_ranges(
1444 positions
1445 .iter()
1446 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1447 )
1448 });
1449
1450 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1451 assert_eq!(
1452 editor.display_text(cx),
1453 "
1454 class Foo:
1455 # Hello!
1456
1457 def a():⋯
1458
1459 def b():
1460 print(2)
1461
1462
1463 class Bar:
1464 # World!
1465
1466 def a():
1467 print(1)
1468
1469 def b():
1470 print(2)
1471
1472
1473 "
1474 .unindent(),
1475 );
1476 });
1477}
1478
1479#[gpui::test]
1480fn test_move_cursor(cx: &mut TestAppContext) {
1481 init_test(cx, |_| {});
1482
1483 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1484 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1485
1486 buffer.update(cx, |buffer, cx| {
1487 buffer.edit(
1488 vec![
1489 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1490 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1491 ],
1492 None,
1493 cx,
1494 );
1495 });
1496 _ = editor.update(cx, |editor, window, cx| {
1497 assert_eq!(
1498 display_ranges(editor, cx),
1499 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1500 );
1501
1502 editor.move_down(&MoveDown, window, cx);
1503 assert_eq!(
1504 display_ranges(editor, cx),
1505 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1506 );
1507
1508 editor.move_right(&MoveRight, window, cx);
1509 assert_eq!(
1510 display_ranges(editor, cx),
1511 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1512 );
1513
1514 editor.move_left(&MoveLeft, window, cx);
1515 assert_eq!(
1516 display_ranges(editor, cx),
1517 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1518 );
1519
1520 editor.move_up(&MoveUp, window, cx);
1521 assert_eq!(
1522 display_ranges(editor, cx),
1523 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1524 );
1525
1526 editor.move_to_end(&MoveToEnd, window, cx);
1527 assert_eq!(
1528 display_ranges(editor, cx),
1529 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1530 );
1531
1532 editor.move_to_beginning(&MoveToBeginning, window, cx);
1533 assert_eq!(
1534 display_ranges(editor, cx),
1535 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1536 );
1537
1538 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1539 s.select_display_ranges([
1540 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1541 ]);
1542 });
1543 editor.select_to_beginning(&SelectToBeginning, window, cx);
1544 assert_eq!(
1545 display_ranges(editor, cx),
1546 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1547 );
1548
1549 editor.select_to_end(&SelectToEnd, window, cx);
1550 assert_eq!(
1551 display_ranges(editor, cx),
1552 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1553 );
1554 });
1555}
1556
1557#[gpui::test]
1558fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1559 init_test(cx, |_| {});
1560
1561 let editor = cx.add_window(|window, cx| {
1562 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1563 build_editor(buffer, window, cx)
1564 });
1565
1566 assert_eq!('🟥'.len_utf8(), 4);
1567 assert_eq!('α'.len_utf8(), 2);
1568
1569 _ = editor.update(cx, |editor, window, cx| {
1570 editor.fold_creases(
1571 vec![
1572 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1573 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1574 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1575 ],
1576 true,
1577 window,
1578 cx,
1579 );
1580 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1581
1582 editor.move_right(&MoveRight, window, cx);
1583 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1584 editor.move_right(&MoveRight, window, cx);
1585 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1586 editor.move_right(&MoveRight, window, cx);
1587 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1588
1589 editor.move_down(&MoveDown, window, cx);
1590 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1591 editor.move_left(&MoveLeft, window, cx);
1592 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1593 editor.move_left(&MoveLeft, window, cx);
1594 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1595 editor.move_left(&MoveLeft, window, cx);
1596 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1597
1598 editor.move_down(&MoveDown, window, cx);
1599 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1600 editor.move_right(&MoveRight, window, cx);
1601 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1602 editor.move_right(&MoveRight, window, cx);
1603 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1604 editor.move_right(&MoveRight, window, cx);
1605 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1606
1607 editor.move_up(&MoveUp, window, cx);
1608 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1609 editor.move_down(&MoveDown, window, cx);
1610 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1611 editor.move_up(&MoveUp, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1613
1614 editor.move_up(&MoveUp, window, cx);
1615 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1616 editor.move_left(&MoveLeft, window, cx);
1617 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1618 editor.move_left(&MoveLeft, window, cx);
1619 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1620 });
1621}
1622
1623#[gpui::test]
1624fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1625 init_test(cx, |_| {});
1626
1627 let editor = cx.add_window(|window, cx| {
1628 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1629 build_editor(buffer, window, cx)
1630 });
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1633 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1634 });
1635
1636 // moving above start of document should move selection to start of document,
1637 // but the next move down should still be at the original goal_x
1638 editor.move_up(&MoveUp, window, cx);
1639 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1640
1641 editor.move_down(&MoveDown, window, cx);
1642 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1643
1644 editor.move_down(&MoveDown, window, cx);
1645 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1646
1647 editor.move_down(&MoveDown, window, cx);
1648 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1649
1650 editor.move_down(&MoveDown, window, cx);
1651 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1652
1653 // moving past end of document should not change goal_x
1654 editor.move_down(&MoveDown, window, cx);
1655 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1656
1657 editor.move_down(&MoveDown, window, cx);
1658 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1659
1660 editor.move_up(&MoveUp, window, cx);
1661 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1662
1663 editor.move_up(&MoveUp, window, cx);
1664 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1665
1666 editor.move_up(&MoveUp, window, cx);
1667 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1668 });
1669}
1670
1671#[gpui::test]
1672fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1673 init_test(cx, |_| {});
1674 let move_to_beg = MoveToBeginningOfLine {
1675 stop_at_soft_wraps: true,
1676 stop_at_indent: true,
1677 };
1678
1679 let delete_to_beg = DeleteToBeginningOfLine {
1680 stop_at_indent: false,
1681 };
1682
1683 let move_to_end = MoveToEndOfLine {
1684 stop_at_soft_wraps: true,
1685 };
1686
1687 let editor = cx.add_window(|window, cx| {
1688 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1689 build_editor(buffer, window, cx)
1690 });
1691 _ = editor.update(cx, |editor, window, cx| {
1692 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1693 s.select_display_ranges([
1694 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1695 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1696 ]);
1697 });
1698 });
1699
1700 _ = editor.update(cx, |editor, window, cx| {
1701 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1702 assert_eq!(
1703 display_ranges(editor, cx),
1704 &[
1705 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1706 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1707 ]
1708 );
1709 });
1710
1711 _ = editor.update(cx, |editor, window, cx| {
1712 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1713 assert_eq!(
1714 display_ranges(editor, cx),
1715 &[
1716 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1717 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1718 ]
1719 );
1720 });
1721
1722 _ = editor.update(cx, |editor, window, cx| {
1723 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1724 assert_eq!(
1725 display_ranges(editor, cx),
1726 &[
1727 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1728 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1729 ]
1730 );
1731 });
1732
1733 _ = editor.update(cx, |editor, window, cx| {
1734 editor.move_to_end_of_line(&move_to_end, window, cx);
1735 assert_eq!(
1736 display_ranges(editor, cx),
1737 &[
1738 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1739 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1740 ]
1741 );
1742 });
1743
1744 // Moving to the end of line again is a no-op.
1745 _ = editor.update(cx, |editor, window, cx| {
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 display_ranges(editor, cx),
1749 &[
1750 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1751 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1752 ]
1753 );
1754 });
1755
1756 _ = editor.update(cx, |editor, window, cx| {
1757 editor.move_left(&MoveLeft, window, cx);
1758 editor.select_to_beginning_of_line(
1759 &SelectToBeginningOfLine {
1760 stop_at_soft_wraps: true,
1761 stop_at_indent: true,
1762 },
1763 window,
1764 cx,
1765 );
1766 assert_eq!(
1767 display_ranges(editor, cx),
1768 &[
1769 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1770 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1771 ]
1772 );
1773 });
1774
1775 _ = editor.update(cx, |editor, window, cx| {
1776 editor.select_to_beginning_of_line(
1777 &SelectToBeginningOfLine {
1778 stop_at_soft_wraps: true,
1779 stop_at_indent: true,
1780 },
1781 window,
1782 cx,
1783 );
1784 assert_eq!(
1785 display_ranges(editor, cx),
1786 &[
1787 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1788 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1789 ]
1790 );
1791 });
1792
1793 _ = editor.update(cx, |editor, window, cx| {
1794 editor.select_to_beginning_of_line(
1795 &SelectToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 },
1799 window,
1800 cx,
1801 );
1802 assert_eq!(
1803 display_ranges(editor, cx),
1804 &[
1805 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1806 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1807 ]
1808 );
1809 });
1810
1811 _ = editor.update(cx, |editor, window, cx| {
1812 editor.select_to_end_of_line(
1813 &SelectToEndOfLine {
1814 stop_at_soft_wraps: true,
1815 },
1816 window,
1817 cx,
1818 );
1819 assert_eq!(
1820 display_ranges(editor, cx),
1821 &[
1822 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1824 ]
1825 );
1826 });
1827
1828 _ = editor.update(cx, |editor, window, cx| {
1829 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1830 assert_eq!(editor.display_text(cx), "ab\n de");
1831 assert_eq!(
1832 display_ranges(editor, cx),
1833 &[
1834 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1835 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1836 ]
1837 );
1838 });
1839
1840 _ = editor.update(cx, |editor, window, cx| {
1841 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1842 assert_eq!(editor.display_text(cx), "\n");
1843 assert_eq!(
1844 display_ranges(editor, cx),
1845 &[
1846 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1847 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1848 ]
1849 );
1850 });
1851}
1852
1853#[gpui::test]
1854fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1855 init_test(cx, |_| {});
1856 let move_to_beg = MoveToBeginningOfLine {
1857 stop_at_soft_wraps: false,
1858 stop_at_indent: false,
1859 };
1860
1861 let move_to_end = MoveToEndOfLine {
1862 stop_at_soft_wraps: false,
1863 };
1864
1865 let editor = cx.add_window(|window, cx| {
1866 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1867 build_editor(buffer, window, cx)
1868 });
1869
1870 _ = editor.update(cx, |editor, window, cx| {
1871 editor.set_wrap_width(Some(140.0.into()), cx);
1872
1873 // We expect the following lines after wrapping
1874 // ```
1875 // thequickbrownfox
1876 // jumpedoverthelazydo
1877 // gs
1878 // ```
1879 // The final `gs` was soft-wrapped onto a new line.
1880 assert_eq!(
1881 "thequickbrownfox\njumpedoverthelaz\nydogs",
1882 editor.display_text(cx),
1883 );
1884
1885 // First, let's assert behavior on the first line, that was not soft-wrapped.
1886 // Start the cursor at the `k` on the first line
1887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1888 s.select_display_ranges([
1889 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1890 ]);
1891 });
1892
1893 // Moving to the beginning of the line should put us at the beginning of the line.
1894 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1895 assert_eq!(
1896 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1897 display_ranges(editor, cx)
1898 );
1899
1900 // Moving to the end of the line should put us at the end of the line.
1901 editor.move_to_end_of_line(&move_to_end, window, cx);
1902 assert_eq!(
1903 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1904 display_ranges(editor, cx)
1905 );
1906
1907 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1908 // Start the cursor at the last line (`y` that was wrapped to a new line)
1909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1910 s.select_display_ranges([
1911 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1912 ]);
1913 });
1914
1915 // Moving to the beginning of the line should put us at the start of the second line of
1916 // display text, i.e., the `j`.
1917 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1918 assert_eq!(
1919 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1920 display_ranges(editor, cx)
1921 );
1922
1923 // Moving to the beginning of the line again should be a no-op.
1924 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1925 assert_eq!(
1926 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1927 display_ranges(editor, cx)
1928 );
1929
1930 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1931 // next display line.
1932 editor.move_to_end_of_line(&move_to_end, window, cx);
1933 assert_eq!(
1934 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1935 display_ranges(editor, cx)
1936 );
1937
1938 // Moving to the end of the line again should be a no-op.
1939 editor.move_to_end_of_line(&move_to_end, window, cx);
1940 assert_eq!(
1941 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1942 display_ranges(editor, cx)
1943 );
1944 });
1945}
1946
1947#[gpui::test]
1948fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1949 init_test(cx, |_| {});
1950
1951 let move_to_beg = MoveToBeginningOfLine {
1952 stop_at_soft_wraps: true,
1953 stop_at_indent: true,
1954 };
1955
1956 let select_to_beg = SelectToBeginningOfLine {
1957 stop_at_soft_wraps: true,
1958 stop_at_indent: true,
1959 };
1960
1961 let delete_to_beg = DeleteToBeginningOfLine {
1962 stop_at_indent: true,
1963 };
1964
1965 let move_to_end = MoveToEndOfLine {
1966 stop_at_soft_wraps: false,
1967 };
1968
1969 let editor = cx.add_window(|window, cx| {
1970 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1971 build_editor(buffer, window, cx)
1972 });
1973
1974 _ = editor.update(cx, |editor, window, cx| {
1975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1976 s.select_display_ranges([
1977 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1978 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1979 ]);
1980 });
1981
1982 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1983 // and the second cursor at the first non-whitespace character in the line.
1984 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1985 assert_eq!(
1986 display_ranges(editor, cx),
1987 &[
1988 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1989 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1990 ]
1991 );
1992
1993 // Moving to the beginning of the line again should be a no-op for the first cursor,
1994 // and should move the second cursor to the beginning of the line.
1995 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1996 assert_eq!(
1997 display_ranges(editor, cx),
1998 &[
1999 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2000 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2001 ]
2002 );
2003
2004 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2005 // and should move the second cursor back to the first non-whitespace character in the line.
2006 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2007 assert_eq!(
2008 display_ranges(editor, cx),
2009 &[
2010 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2011 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2012 ]
2013 );
2014
2015 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2016 // and to the first non-whitespace character in the line for the second cursor.
2017 editor.move_to_end_of_line(&move_to_end, window, cx);
2018 editor.move_left(&MoveLeft, window, cx);
2019 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2020 assert_eq!(
2021 display_ranges(editor, cx),
2022 &[
2023 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2024 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2025 ]
2026 );
2027
2028 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2029 // and should select to the beginning of the line for the second cursor.
2030 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2031 assert_eq!(
2032 display_ranges(editor, cx),
2033 &[
2034 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2035 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2036 ]
2037 );
2038
2039 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2040 // and should delete to the first non-whitespace character in the line for the second cursor.
2041 editor.move_to_end_of_line(&move_to_end, window, cx);
2042 editor.move_left(&MoveLeft, window, cx);
2043 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2044 assert_eq!(editor.text(cx), "c\n f");
2045 });
2046}
2047
2048#[gpui::test]
2049fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2050 init_test(cx, |_| {});
2051
2052 let move_to_beg = MoveToBeginningOfLine {
2053 stop_at_soft_wraps: true,
2054 stop_at_indent: true,
2055 };
2056
2057 let editor = cx.add_window(|window, cx| {
2058 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2059 build_editor(buffer, window, cx)
2060 });
2061
2062 _ = editor.update(cx, |editor, window, cx| {
2063 // test cursor between line_start and indent_start
2064 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2065 s.select_display_ranges([
2066 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2067 ]);
2068 });
2069
2070 // cursor should move to line_start
2071 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2072 assert_eq!(
2073 display_ranges(editor, cx),
2074 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2075 );
2076
2077 // cursor should move to indent_start
2078 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2079 assert_eq!(
2080 display_ranges(editor, cx),
2081 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2082 );
2083
2084 // cursor should move to back to line_start
2085 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2086 assert_eq!(
2087 display_ranges(editor, cx),
2088 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2089 );
2090 });
2091}
2092
2093#[gpui::test]
2094fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2095 init_test(cx, |_| {});
2096
2097 let editor = cx.add_window(|window, cx| {
2098 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2099 build_editor(buffer, window, cx)
2100 });
2101 _ = editor.update(cx, |editor, window, cx| {
2102 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2103 s.select_display_ranges([
2104 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2105 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2106 ])
2107 });
2108 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2109 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2110
2111 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2112 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2113
2114 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2115 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2116
2117 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2118 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2119
2120 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2121 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2122
2123 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2124 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2125
2126 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2127 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2128
2129 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2130 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2131
2132 editor.move_right(&MoveRight, window, cx);
2133 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2134 assert_selection_ranges(
2135 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2136 editor,
2137 cx,
2138 );
2139
2140 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2141 assert_selection_ranges(
2142 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2143 editor,
2144 cx,
2145 );
2146
2147 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2148 assert_selection_ranges(
2149 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2150 editor,
2151 cx,
2152 );
2153 });
2154}
2155
2156#[gpui::test]
2157fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2158 init_test(cx, |_| {});
2159
2160 let editor = cx.add_window(|window, cx| {
2161 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2162 build_editor(buffer, window, cx)
2163 });
2164
2165 _ = editor.update(cx, |editor, window, cx| {
2166 editor.set_wrap_width(Some(140.0.into()), cx);
2167 assert_eq!(
2168 editor.display_text(cx),
2169 "use one::{\n two::three::\n four::five\n};"
2170 );
2171
2172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2173 s.select_display_ranges([
2174 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2175 ]);
2176 });
2177
2178 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2179 assert_eq!(
2180 display_ranges(editor, cx),
2181 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2182 );
2183
2184 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2185 assert_eq!(
2186 display_ranges(editor, cx),
2187 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2188 );
2189
2190 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2191 assert_eq!(
2192 display_ranges(editor, cx),
2193 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2194 );
2195
2196 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2197 assert_eq!(
2198 display_ranges(editor, cx),
2199 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2200 );
2201
2202 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2203 assert_eq!(
2204 display_ranges(editor, cx),
2205 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2206 );
2207
2208 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2209 assert_eq!(
2210 display_ranges(editor, cx),
2211 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2212 );
2213 });
2214}
2215
2216#[gpui::test]
2217async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2218 init_test(cx, |_| {});
2219 let mut cx = EditorTestContext::new(cx).await;
2220
2221 let line_height = cx.editor(|editor, window, _| {
2222 editor
2223 .style()
2224 .unwrap()
2225 .text
2226 .line_height_in_pixels(window.rem_size())
2227 });
2228 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2229
2230 cx.set_state(
2231 &r#"ˇone
2232 two
2233
2234 three
2235 fourˇ
2236 five
2237
2238 six"#
2239 .unindent(),
2240 );
2241
2242 cx.update_editor(|editor, window, cx| {
2243 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2244 });
2245 cx.assert_editor_state(
2246 &r#"one
2247 two
2248 ˇ
2249 three
2250 four
2251 five
2252 ˇ
2253 six"#
2254 .unindent(),
2255 );
2256
2257 cx.update_editor(|editor, window, cx| {
2258 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2259 });
2260 cx.assert_editor_state(
2261 &r#"one
2262 two
2263
2264 three
2265 four
2266 five
2267 ˇ
2268 sixˇ"#
2269 .unindent(),
2270 );
2271
2272 cx.update_editor(|editor, window, cx| {
2273 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2274 });
2275 cx.assert_editor_state(
2276 &r#"one
2277 two
2278
2279 three
2280 four
2281 five
2282
2283 sixˇ"#
2284 .unindent(),
2285 );
2286
2287 cx.update_editor(|editor, window, cx| {
2288 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2289 });
2290 cx.assert_editor_state(
2291 &r#"one
2292 two
2293
2294 three
2295 four
2296 five
2297 ˇ
2298 six"#
2299 .unindent(),
2300 );
2301
2302 cx.update_editor(|editor, window, cx| {
2303 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2304 });
2305 cx.assert_editor_state(
2306 &r#"one
2307 two
2308 ˇ
2309 three
2310 four
2311 five
2312
2313 six"#
2314 .unindent(),
2315 );
2316
2317 cx.update_editor(|editor, window, cx| {
2318 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2319 });
2320 cx.assert_editor_state(
2321 &r#"ˇone
2322 two
2323
2324 three
2325 four
2326 five
2327
2328 six"#
2329 .unindent(),
2330 );
2331}
2332
2333#[gpui::test]
2334async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2335 init_test(cx, |_| {});
2336 let mut cx = EditorTestContext::new(cx).await;
2337 let line_height = cx.editor(|editor, window, _| {
2338 editor
2339 .style()
2340 .unwrap()
2341 .text
2342 .line_height_in_pixels(window.rem_size())
2343 });
2344 let window = cx.window;
2345 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2346
2347 cx.set_state(
2348 r#"ˇone
2349 two
2350 three
2351 four
2352 five
2353 six
2354 seven
2355 eight
2356 nine
2357 ten
2358 "#,
2359 );
2360
2361 cx.update_editor(|editor, window, cx| {
2362 assert_eq!(
2363 editor.snapshot(window, cx).scroll_position(),
2364 gpui::Point::new(0., 0.)
2365 );
2366 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2367 assert_eq!(
2368 editor.snapshot(window, cx).scroll_position(),
2369 gpui::Point::new(0., 3.)
2370 );
2371 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2372 assert_eq!(
2373 editor.snapshot(window, cx).scroll_position(),
2374 gpui::Point::new(0., 6.)
2375 );
2376 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2377 assert_eq!(
2378 editor.snapshot(window, cx).scroll_position(),
2379 gpui::Point::new(0., 3.)
2380 );
2381
2382 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2383 assert_eq!(
2384 editor.snapshot(window, cx).scroll_position(),
2385 gpui::Point::new(0., 1.)
2386 );
2387 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2388 assert_eq!(
2389 editor.snapshot(window, cx).scroll_position(),
2390 gpui::Point::new(0., 3.)
2391 );
2392 });
2393}
2394
2395#[gpui::test]
2396async fn test_autoscroll(cx: &mut TestAppContext) {
2397 init_test(cx, |_| {});
2398 let mut cx = EditorTestContext::new(cx).await;
2399
2400 let line_height = cx.update_editor(|editor, window, cx| {
2401 editor.set_vertical_scroll_margin(2, cx);
2402 editor
2403 .style()
2404 .unwrap()
2405 .text
2406 .line_height_in_pixels(window.rem_size())
2407 });
2408 let window = cx.window;
2409 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2410
2411 cx.set_state(
2412 r#"ˇone
2413 two
2414 three
2415 four
2416 five
2417 six
2418 seven
2419 eight
2420 nine
2421 ten
2422 "#,
2423 );
2424 cx.update_editor(|editor, window, cx| {
2425 assert_eq!(
2426 editor.snapshot(window, cx).scroll_position(),
2427 gpui::Point::new(0., 0.0)
2428 );
2429 });
2430
2431 // Add a cursor below the visible area. Since both cursors cannot fit
2432 // on screen, the editor autoscrolls to reveal the newest cursor, and
2433 // allows the vertical scroll margin below that cursor.
2434 cx.update_editor(|editor, window, cx| {
2435 editor.change_selections(Default::default(), window, cx, |selections| {
2436 selections.select_ranges([
2437 Point::new(0, 0)..Point::new(0, 0),
2438 Point::new(6, 0)..Point::new(6, 0),
2439 ]);
2440 })
2441 });
2442 cx.update_editor(|editor, window, cx| {
2443 assert_eq!(
2444 editor.snapshot(window, cx).scroll_position(),
2445 gpui::Point::new(0., 3.0)
2446 );
2447 });
2448
2449 // Move down. The editor cursor scrolls down to track the newest cursor.
2450 cx.update_editor(|editor, window, cx| {
2451 editor.move_down(&Default::default(), window, cx);
2452 });
2453 cx.update_editor(|editor, window, cx| {
2454 assert_eq!(
2455 editor.snapshot(window, cx).scroll_position(),
2456 gpui::Point::new(0., 4.0)
2457 );
2458 });
2459
2460 // Add a cursor above the visible area. Since both cursors fit on screen,
2461 // the editor scrolls to show both.
2462 cx.update_editor(|editor, window, cx| {
2463 editor.change_selections(Default::default(), window, cx, |selections| {
2464 selections.select_ranges([
2465 Point::new(1, 0)..Point::new(1, 0),
2466 Point::new(6, 0)..Point::new(6, 0),
2467 ]);
2468 })
2469 });
2470 cx.update_editor(|editor, window, cx| {
2471 assert_eq!(
2472 editor.snapshot(window, cx).scroll_position(),
2473 gpui::Point::new(0., 1.0)
2474 );
2475 });
2476}
2477
2478#[gpui::test]
2479async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481 let mut cx = EditorTestContext::new(cx).await;
2482
2483 let line_height = cx.editor(|editor, window, _cx| {
2484 editor
2485 .style()
2486 .unwrap()
2487 .text
2488 .line_height_in_pixels(window.rem_size())
2489 });
2490 let window = cx.window;
2491 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2492 cx.set_state(
2493 &r#"
2494 ˇone
2495 two
2496 threeˇ
2497 four
2498 five
2499 six
2500 seven
2501 eight
2502 nine
2503 ten
2504 "#
2505 .unindent(),
2506 );
2507
2508 cx.update_editor(|editor, window, cx| {
2509 editor.move_page_down(&MovePageDown::default(), window, cx)
2510 });
2511 cx.assert_editor_state(
2512 &r#"
2513 one
2514 two
2515 three
2516 ˇfour
2517 five
2518 sixˇ
2519 seven
2520 eight
2521 nine
2522 ten
2523 "#
2524 .unindent(),
2525 );
2526
2527 cx.update_editor(|editor, window, cx| {
2528 editor.move_page_down(&MovePageDown::default(), window, cx)
2529 });
2530 cx.assert_editor_state(
2531 &r#"
2532 one
2533 two
2534 three
2535 four
2536 five
2537 six
2538 ˇseven
2539 eight
2540 nineˇ
2541 ten
2542 "#
2543 .unindent(),
2544 );
2545
2546 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2547 cx.assert_editor_state(
2548 &r#"
2549 one
2550 two
2551 three
2552 ˇfour
2553 five
2554 sixˇ
2555 seven
2556 eight
2557 nine
2558 ten
2559 "#
2560 .unindent(),
2561 );
2562
2563 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2564 cx.assert_editor_state(
2565 &r#"
2566 ˇone
2567 two
2568 threeˇ
2569 four
2570 five
2571 six
2572 seven
2573 eight
2574 nine
2575 ten
2576 "#
2577 .unindent(),
2578 );
2579
2580 // Test select collapsing
2581 cx.update_editor(|editor, window, cx| {
2582 editor.move_page_down(&MovePageDown::default(), window, cx);
2583 editor.move_page_down(&MovePageDown::default(), window, cx);
2584 editor.move_page_down(&MovePageDown::default(), window, cx);
2585 });
2586 cx.assert_editor_state(
2587 &r#"
2588 one
2589 two
2590 three
2591 four
2592 five
2593 six
2594 seven
2595 eight
2596 nine
2597 ˇten
2598 ˇ"#
2599 .unindent(),
2600 );
2601}
2602
2603#[gpui::test]
2604async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2605 init_test(cx, |_| {});
2606 let mut cx = EditorTestContext::new(cx).await;
2607 cx.set_state("one «two threeˇ» four");
2608 cx.update_editor(|editor, window, cx| {
2609 editor.delete_to_beginning_of_line(
2610 &DeleteToBeginningOfLine {
2611 stop_at_indent: false,
2612 },
2613 window,
2614 cx,
2615 );
2616 assert_eq!(editor.text(cx), " four");
2617 });
2618}
2619
2620#[gpui::test]
2621async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2622 init_test(cx, |_| {});
2623
2624 let mut cx = EditorTestContext::new(cx).await;
2625
2626 // For an empty selection, the preceding word fragment is deleted.
2627 // For non-empty selections, only selected characters are deleted.
2628 cx.set_state("onˇe two t«hreˇ»e four");
2629 cx.update_editor(|editor, window, cx| {
2630 editor.delete_to_previous_word_start(
2631 &DeleteToPreviousWordStart {
2632 ignore_newlines: false,
2633 ignore_brackets: false,
2634 },
2635 window,
2636 cx,
2637 );
2638 });
2639 cx.assert_editor_state("ˇe two tˇe four");
2640
2641 cx.set_state("e tˇwo te «fˇ»our");
2642 cx.update_editor(|editor, window, cx| {
2643 editor.delete_to_next_word_end(
2644 &DeleteToNextWordEnd {
2645 ignore_newlines: false,
2646 ignore_brackets: false,
2647 },
2648 window,
2649 cx,
2650 );
2651 });
2652 cx.assert_editor_state("e tˇ te ˇour");
2653}
2654
2655#[gpui::test]
2656async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2657 init_test(cx, |_| {});
2658
2659 let mut cx = EditorTestContext::new(cx).await;
2660
2661 cx.set_state("here is some text ˇwith a space");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_previous_word_start(
2664 &DeleteToPreviousWordStart {
2665 ignore_newlines: false,
2666 ignore_brackets: true,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2673 cx.assert_editor_state("here is some textˇwith a space");
2674
2675 cx.set_state("here is some text ˇwith a space");
2676 cx.update_editor(|editor, window, cx| {
2677 editor.delete_to_previous_word_start(
2678 &DeleteToPreviousWordStart {
2679 ignore_newlines: false,
2680 ignore_brackets: false,
2681 },
2682 window,
2683 cx,
2684 );
2685 });
2686 cx.assert_editor_state("here is some textˇwith a space");
2687
2688 cx.set_state("here is some textˇ with a space");
2689 cx.update_editor(|editor, window, cx| {
2690 editor.delete_to_next_word_end(
2691 &DeleteToNextWordEnd {
2692 ignore_newlines: false,
2693 ignore_brackets: true,
2694 },
2695 window,
2696 cx,
2697 );
2698 });
2699 // Same happens in the other direction.
2700 cx.assert_editor_state("here is some textˇwith a space");
2701
2702 cx.set_state("here is some textˇ with a space");
2703 cx.update_editor(|editor, window, cx| {
2704 editor.delete_to_next_word_end(
2705 &DeleteToNextWordEnd {
2706 ignore_newlines: false,
2707 ignore_brackets: false,
2708 },
2709 window,
2710 cx,
2711 );
2712 });
2713 cx.assert_editor_state("here is some textˇwith a space");
2714
2715 cx.set_state("here is some textˇ with a space");
2716 cx.update_editor(|editor, window, cx| {
2717 editor.delete_to_next_word_end(
2718 &DeleteToNextWordEnd {
2719 ignore_newlines: true,
2720 ignore_brackets: false,
2721 },
2722 window,
2723 cx,
2724 );
2725 });
2726 cx.assert_editor_state("here is some textˇwith a space");
2727 cx.update_editor(|editor, window, cx| {
2728 editor.delete_to_previous_word_start(
2729 &DeleteToPreviousWordStart {
2730 ignore_newlines: true,
2731 ignore_brackets: false,
2732 },
2733 window,
2734 cx,
2735 );
2736 });
2737 cx.assert_editor_state("here is some ˇwith a space");
2738 cx.update_editor(|editor, window, cx| {
2739 editor.delete_to_previous_word_start(
2740 &DeleteToPreviousWordStart {
2741 ignore_newlines: true,
2742 ignore_brackets: false,
2743 },
2744 window,
2745 cx,
2746 );
2747 });
2748 // Single whitespaces are removed with the word behind them.
2749 cx.assert_editor_state("here is ˇwith a space");
2750 cx.update_editor(|editor, window, cx| {
2751 editor.delete_to_previous_word_start(
2752 &DeleteToPreviousWordStart {
2753 ignore_newlines: true,
2754 ignore_brackets: false,
2755 },
2756 window,
2757 cx,
2758 );
2759 });
2760 cx.assert_editor_state("here ˇwith a space");
2761 cx.update_editor(|editor, window, cx| {
2762 editor.delete_to_previous_word_start(
2763 &DeleteToPreviousWordStart {
2764 ignore_newlines: true,
2765 ignore_brackets: false,
2766 },
2767 window,
2768 cx,
2769 );
2770 });
2771 cx.assert_editor_state("ˇwith a space");
2772 cx.update_editor(|editor, window, cx| {
2773 editor.delete_to_previous_word_start(
2774 &DeleteToPreviousWordStart {
2775 ignore_newlines: true,
2776 ignore_brackets: false,
2777 },
2778 window,
2779 cx,
2780 );
2781 });
2782 cx.assert_editor_state("ˇwith a space");
2783 cx.update_editor(|editor, window, cx| {
2784 editor.delete_to_next_word_end(
2785 &DeleteToNextWordEnd {
2786 ignore_newlines: true,
2787 ignore_brackets: false,
2788 },
2789 window,
2790 cx,
2791 );
2792 });
2793 // Same happens in the other direction.
2794 cx.assert_editor_state("ˇ a space");
2795 cx.update_editor(|editor, window, cx| {
2796 editor.delete_to_next_word_end(
2797 &DeleteToNextWordEnd {
2798 ignore_newlines: true,
2799 ignore_brackets: false,
2800 },
2801 window,
2802 cx,
2803 );
2804 });
2805 cx.assert_editor_state("ˇ space");
2806 cx.update_editor(|editor, window, cx| {
2807 editor.delete_to_next_word_end(
2808 &DeleteToNextWordEnd {
2809 ignore_newlines: true,
2810 ignore_brackets: false,
2811 },
2812 window,
2813 cx,
2814 );
2815 });
2816 cx.assert_editor_state("ˇ");
2817 cx.update_editor(|editor, window, cx| {
2818 editor.delete_to_next_word_end(
2819 &DeleteToNextWordEnd {
2820 ignore_newlines: true,
2821 ignore_brackets: false,
2822 },
2823 window,
2824 cx,
2825 );
2826 });
2827 cx.assert_editor_state("ˇ");
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_previous_word_start(
2830 &DeleteToPreviousWordStart {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state("ˇ");
2839}
2840
2841#[gpui::test]
2842async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2843 init_test(cx, |_| {});
2844
2845 let language = Arc::new(
2846 Language::new(
2847 LanguageConfig {
2848 brackets: BracketPairConfig {
2849 pairs: vec![
2850 BracketPair {
2851 start: "\"".to_string(),
2852 end: "\"".to_string(),
2853 close: true,
2854 surround: true,
2855 newline: false,
2856 },
2857 BracketPair {
2858 start: "(".to_string(),
2859 end: ")".to_string(),
2860 close: true,
2861 surround: true,
2862 newline: true,
2863 },
2864 ],
2865 ..BracketPairConfig::default()
2866 },
2867 ..LanguageConfig::default()
2868 },
2869 Some(tree_sitter_rust::LANGUAGE.into()),
2870 )
2871 .with_brackets_query(
2872 r#"
2873 ("(" @open ")" @close)
2874 ("\"" @open "\"" @close)
2875 "#,
2876 )
2877 .unwrap(),
2878 );
2879
2880 let mut cx = EditorTestContext::new(cx).await;
2881 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2882
2883 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2884 cx.update_editor(|editor, window, cx| {
2885 editor.delete_to_previous_word_start(
2886 &DeleteToPreviousWordStart {
2887 ignore_newlines: true,
2888 ignore_brackets: false,
2889 },
2890 window,
2891 cx,
2892 );
2893 });
2894 // Deletion stops before brackets if asked to not ignore them.
2895 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2896 cx.update_editor(|editor, window, cx| {
2897 editor.delete_to_previous_word_start(
2898 &DeleteToPreviousWordStart {
2899 ignore_newlines: true,
2900 ignore_brackets: false,
2901 },
2902 window,
2903 cx,
2904 );
2905 });
2906 // Deletion has to remove a single bracket and then stop again.
2907 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2908
2909 cx.update_editor(|editor, window, cx| {
2910 editor.delete_to_previous_word_start(
2911 &DeleteToPreviousWordStart {
2912 ignore_newlines: true,
2913 ignore_brackets: false,
2914 },
2915 window,
2916 cx,
2917 );
2918 });
2919 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2920
2921 cx.update_editor(|editor, window, cx| {
2922 editor.delete_to_previous_word_start(
2923 &DeleteToPreviousWordStart {
2924 ignore_newlines: true,
2925 ignore_brackets: false,
2926 },
2927 window,
2928 cx,
2929 );
2930 });
2931 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2932
2933 cx.update_editor(|editor, window, cx| {
2934 editor.delete_to_previous_word_start(
2935 &DeleteToPreviousWordStart {
2936 ignore_newlines: true,
2937 ignore_brackets: false,
2938 },
2939 window,
2940 cx,
2941 );
2942 });
2943 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2944
2945 cx.update_editor(|editor, window, cx| {
2946 editor.delete_to_next_word_end(
2947 &DeleteToNextWordEnd {
2948 ignore_newlines: true,
2949 ignore_brackets: false,
2950 },
2951 window,
2952 cx,
2953 );
2954 });
2955 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2956 cx.assert_editor_state(r#"ˇ");"#);
2957
2958 cx.update_editor(|editor, window, cx| {
2959 editor.delete_to_next_word_end(
2960 &DeleteToNextWordEnd {
2961 ignore_newlines: true,
2962 ignore_brackets: false,
2963 },
2964 window,
2965 cx,
2966 );
2967 });
2968 cx.assert_editor_state(r#"ˇ"#);
2969
2970 cx.update_editor(|editor, window, cx| {
2971 editor.delete_to_next_word_end(
2972 &DeleteToNextWordEnd {
2973 ignore_newlines: true,
2974 ignore_brackets: false,
2975 },
2976 window,
2977 cx,
2978 );
2979 });
2980 cx.assert_editor_state(r#"ˇ"#);
2981
2982 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2983 cx.update_editor(|editor, window, cx| {
2984 editor.delete_to_previous_word_start(
2985 &DeleteToPreviousWordStart {
2986 ignore_newlines: true,
2987 ignore_brackets: true,
2988 },
2989 window,
2990 cx,
2991 );
2992 });
2993 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2994}
2995
2996#[gpui::test]
2997fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2998 init_test(cx, |_| {});
2999
3000 let editor = cx.add_window(|window, cx| {
3001 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3002 build_editor(buffer, window, cx)
3003 });
3004 let del_to_prev_word_start = DeleteToPreviousWordStart {
3005 ignore_newlines: false,
3006 ignore_brackets: false,
3007 };
3008 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3009 ignore_newlines: true,
3010 ignore_brackets: false,
3011 };
3012
3013 _ = editor.update(cx, |editor, window, cx| {
3014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3015 s.select_display_ranges([
3016 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3017 ])
3018 });
3019 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3020 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3021 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3022 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3023 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3024 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3025 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3026 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3027 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3028 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3029 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3030 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3031 });
3032}
3033
3034#[gpui::test]
3035fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3036 init_test(cx, |_| {});
3037
3038 let editor = cx.add_window(|window, cx| {
3039 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3040 build_editor(buffer, window, cx)
3041 });
3042 let del_to_next_word_end = DeleteToNextWordEnd {
3043 ignore_newlines: false,
3044 ignore_brackets: false,
3045 };
3046 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3047 ignore_newlines: true,
3048 ignore_brackets: false,
3049 };
3050
3051 _ = editor.update(cx, |editor, window, cx| {
3052 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3053 s.select_display_ranges([
3054 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3055 ])
3056 });
3057 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3058 assert_eq!(
3059 editor.buffer.read(cx).read(cx).text(),
3060 "one\n two\nthree\n four"
3061 );
3062 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3063 assert_eq!(
3064 editor.buffer.read(cx).read(cx).text(),
3065 "\n two\nthree\n four"
3066 );
3067 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3068 assert_eq!(
3069 editor.buffer.read(cx).read(cx).text(),
3070 "two\nthree\n four"
3071 );
3072 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3073 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3074 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3075 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3076 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3077 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3078 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3079 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3080 });
3081}
3082
3083#[gpui::test]
3084fn test_newline(cx: &mut TestAppContext) {
3085 init_test(cx, |_| {});
3086
3087 let editor = cx.add_window(|window, cx| {
3088 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3089 build_editor(buffer, window, cx)
3090 });
3091
3092 _ = editor.update(cx, |editor, window, cx| {
3093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3094 s.select_display_ranges([
3095 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3096 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3097 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3098 ])
3099 });
3100
3101 editor.newline(&Newline, window, cx);
3102 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3103 });
3104}
3105
3106#[gpui::test]
3107async fn test_newline_yaml(cx: &mut TestAppContext) {
3108 init_test(cx, |_| {});
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3112 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3113
3114 // Object (between 2 fields)
3115 cx.set_state(indoc! {"
3116 test:ˇ
3117 hello: bye"});
3118 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3119 cx.assert_editor_state(indoc! {"
3120 test:
3121 ˇ
3122 hello: bye"});
3123
3124 // Object (first and single line)
3125 cx.set_state(indoc! {"
3126 test:ˇ"});
3127 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 test:
3130 ˇ"});
3131
3132 // Array with objects (after first element)
3133 cx.set_state(indoc! {"
3134 test:
3135 - foo: barˇ"});
3136 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3137 cx.assert_editor_state(indoc! {"
3138 test:
3139 - foo: bar
3140 ˇ"});
3141
3142 // Array with objects and comment
3143 cx.set_state(indoc! {"
3144 test:
3145 - foo: bar
3146 - bar: # testˇ"});
3147 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3148 cx.assert_editor_state(indoc! {"
3149 test:
3150 - foo: bar
3151 - bar: # test
3152 ˇ"});
3153
3154 // Array with objects (after second element)
3155 cx.set_state(indoc! {"
3156 test:
3157 - foo: bar
3158 - bar: fooˇ"});
3159 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3160 cx.assert_editor_state(indoc! {"
3161 test:
3162 - foo: bar
3163 - bar: foo
3164 ˇ"});
3165
3166 // Array with strings (after first element)
3167 cx.set_state(indoc! {"
3168 test:
3169 - fooˇ"});
3170 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3171 cx.assert_editor_state(indoc! {"
3172 test:
3173 - foo
3174 ˇ"});
3175}
3176
3177#[gpui::test]
3178fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3179 init_test(cx, |_| {});
3180
3181 let editor = cx.add_window(|window, cx| {
3182 let buffer = MultiBuffer::build_simple(
3183 "
3184 a
3185 b(
3186 X
3187 )
3188 c(
3189 X
3190 )
3191 "
3192 .unindent()
3193 .as_str(),
3194 cx,
3195 );
3196 let mut editor = build_editor(buffer, window, cx);
3197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3198 s.select_ranges([
3199 Point::new(2, 4)..Point::new(2, 5),
3200 Point::new(5, 4)..Point::new(5, 5),
3201 ])
3202 });
3203 editor
3204 });
3205
3206 _ = editor.update(cx, |editor, window, cx| {
3207 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3208 editor.buffer.update(cx, |buffer, cx| {
3209 buffer.edit(
3210 [
3211 (Point::new(1, 2)..Point::new(3, 0), ""),
3212 (Point::new(4, 2)..Point::new(6, 0), ""),
3213 ],
3214 None,
3215 cx,
3216 );
3217 assert_eq!(
3218 buffer.read(cx).text(),
3219 "
3220 a
3221 b()
3222 c()
3223 "
3224 .unindent()
3225 );
3226 });
3227 assert_eq!(
3228 editor.selections.ranges(&editor.display_snapshot(cx)),
3229 &[
3230 Point::new(1, 2)..Point::new(1, 2),
3231 Point::new(2, 2)..Point::new(2, 2),
3232 ],
3233 );
3234
3235 editor.newline(&Newline, window, cx);
3236 assert_eq!(
3237 editor.text(cx),
3238 "
3239 a
3240 b(
3241 )
3242 c(
3243 )
3244 "
3245 .unindent()
3246 );
3247
3248 // The selections are moved after the inserted newlines
3249 assert_eq!(
3250 editor.selections.ranges(&editor.display_snapshot(cx)),
3251 &[
3252 Point::new(2, 0)..Point::new(2, 0),
3253 Point::new(4, 0)..Point::new(4, 0),
3254 ],
3255 );
3256 });
3257}
3258
3259#[gpui::test]
3260async fn test_newline_above(cx: &mut TestAppContext) {
3261 init_test(cx, |settings| {
3262 settings.defaults.tab_size = NonZeroU32::new(4)
3263 });
3264
3265 let language = Arc::new(
3266 Language::new(
3267 LanguageConfig::default(),
3268 Some(tree_sitter_rust::LANGUAGE.into()),
3269 )
3270 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3271 .unwrap(),
3272 );
3273
3274 let mut cx = EditorTestContext::new(cx).await;
3275 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3276 cx.set_state(indoc! {"
3277 const a: ˇA = (
3278 (ˇ
3279 «const_functionˇ»(ˇ),
3280 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3281 )ˇ
3282 ˇ);ˇ
3283 "});
3284
3285 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 ˇ
3288 const a: A = (
3289 ˇ
3290 (
3291 ˇ
3292 ˇ
3293 const_function(),
3294 ˇ
3295 ˇ
3296 ˇ
3297 ˇ
3298 something_else,
3299 ˇ
3300 )
3301 ˇ
3302 ˇ
3303 );
3304 "});
3305}
3306
3307#[gpui::test]
3308async fn test_newline_below(cx: &mut TestAppContext) {
3309 init_test(cx, |settings| {
3310 settings.defaults.tab_size = NonZeroU32::new(4)
3311 });
3312
3313 let language = Arc::new(
3314 Language::new(
3315 LanguageConfig::default(),
3316 Some(tree_sitter_rust::LANGUAGE.into()),
3317 )
3318 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3319 .unwrap(),
3320 );
3321
3322 let mut cx = EditorTestContext::new(cx).await;
3323 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3324 cx.set_state(indoc! {"
3325 const a: ˇA = (
3326 (ˇ
3327 «const_functionˇ»(ˇ),
3328 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3329 )ˇ
3330 ˇ);ˇ
3331 "});
3332
3333 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3334 cx.assert_editor_state(indoc! {"
3335 const a: A = (
3336 ˇ
3337 (
3338 ˇ
3339 const_function(),
3340 ˇ
3341 ˇ
3342 something_else,
3343 ˇ
3344 ˇ
3345 ˇ
3346 ˇ
3347 )
3348 ˇ
3349 );
3350 ˇ
3351 ˇ
3352 "});
3353}
3354
3355#[gpui::test]
3356async fn test_newline_comments(cx: &mut TestAppContext) {
3357 init_test(cx, |settings| {
3358 settings.defaults.tab_size = NonZeroU32::new(4)
3359 });
3360
3361 let language = Arc::new(Language::new(
3362 LanguageConfig {
3363 line_comments: vec!["// ".into()],
3364 ..LanguageConfig::default()
3365 },
3366 None,
3367 ));
3368 {
3369 let mut cx = EditorTestContext::new(cx).await;
3370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3371 cx.set_state(indoc! {"
3372 // Fooˇ
3373 "});
3374
3375 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3376 cx.assert_editor_state(indoc! {"
3377 // Foo
3378 // ˇ
3379 "});
3380 // Ensure that we add comment prefix when existing line contains space
3381 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3382 cx.assert_editor_state(
3383 indoc! {"
3384 // Foo
3385 //s
3386 // ˇ
3387 "}
3388 .replace("s", " ") // s is used as space placeholder to prevent format on save
3389 .as_str(),
3390 );
3391 // Ensure that we add comment prefix when existing line does not contain space
3392 cx.set_state(indoc! {"
3393 // Foo
3394 //ˇ
3395 "});
3396 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3397 cx.assert_editor_state(indoc! {"
3398 // Foo
3399 //
3400 // ˇ
3401 "});
3402 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3403 cx.set_state(indoc! {"
3404 ˇ// Foo
3405 "});
3406 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3407 cx.assert_editor_state(indoc! {"
3408
3409 ˇ// Foo
3410 "});
3411 }
3412 // Ensure that comment continuations can be disabled.
3413 update_test_language_settings(cx, |settings| {
3414 settings.defaults.extend_comment_on_newline = Some(false);
3415 });
3416 let mut cx = EditorTestContext::new(cx).await;
3417 cx.set_state(indoc! {"
3418 // Fooˇ
3419 "});
3420 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3421 cx.assert_editor_state(indoc! {"
3422 // Foo
3423 ˇ
3424 "});
3425}
3426
3427#[gpui::test]
3428async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3429 init_test(cx, |settings| {
3430 settings.defaults.tab_size = NonZeroU32::new(4)
3431 });
3432
3433 let language = Arc::new(Language::new(
3434 LanguageConfig {
3435 line_comments: vec!["// ".into(), "/// ".into()],
3436 ..LanguageConfig::default()
3437 },
3438 None,
3439 ));
3440 {
3441 let mut cx = EditorTestContext::new(cx).await;
3442 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3443 cx.set_state(indoc! {"
3444 //ˇ
3445 "});
3446 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3447 cx.assert_editor_state(indoc! {"
3448 //
3449 // ˇ
3450 "});
3451
3452 cx.set_state(indoc! {"
3453 ///ˇ
3454 "});
3455 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 ///
3458 /// ˇ
3459 "});
3460 }
3461}
3462
3463#[gpui::test]
3464async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3465 init_test(cx, |settings| {
3466 settings.defaults.tab_size = NonZeroU32::new(4)
3467 });
3468
3469 let language = Arc::new(
3470 Language::new(
3471 LanguageConfig {
3472 documentation_comment: Some(language::BlockCommentConfig {
3473 start: "/**".into(),
3474 end: "*/".into(),
3475 prefix: "* ".into(),
3476 tab_size: 1,
3477 }),
3478
3479 ..LanguageConfig::default()
3480 },
3481 Some(tree_sitter_rust::LANGUAGE.into()),
3482 )
3483 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3484 .unwrap(),
3485 );
3486
3487 {
3488 let mut cx = EditorTestContext::new(cx).await;
3489 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3490 cx.set_state(indoc! {"
3491 /**ˇ
3492 "});
3493
3494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 /**
3497 * ˇ
3498 "});
3499 // Ensure that if cursor is before the comment start,
3500 // we do not actually insert a comment prefix.
3501 cx.set_state(indoc! {"
3502 ˇ/**
3503 "});
3504 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3505 cx.assert_editor_state(indoc! {"
3506
3507 ˇ/**
3508 "});
3509 // Ensure that if cursor is between it doesn't add comment prefix.
3510 cx.set_state(indoc! {"
3511 /*ˇ*
3512 "});
3513 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3514 cx.assert_editor_state(indoc! {"
3515 /*
3516 ˇ*
3517 "});
3518 // Ensure that if suffix exists on same line after cursor it adds new line.
3519 cx.set_state(indoc! {"
3520 /**ˇ*/
3521 "});
3522 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 /**
3525 * ˇ
3526 */
3527 "});
3528 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3529 cx.set_state(indoc! {"
3530 /**ˇ */
3531 "});
3532 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3533 cx.assert_editor_state(indoc! {"
3534 /**
3535 * ˇ
3536 */
3537 "});
3538 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3539 cx.set_state(indoc! {"
3540 /** ˇ*/
3541 "});
3542 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3543 cx.assert_editor_state(
3544 indoc! {"
3545 /**s
3546 * ˇ
3547 */
3548 "}
3549 .replace("s", " ") // s is used as space placeholder to prevent format on save
3550 .as_str(),
3551 );
3552 // Ensure that delimiter space is preserved when newline on already
3553 // spaced delimiter.
3554 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3555 cx.assert_editor_state(
3556 indoc! {"
3557 /**s
3558 *s
3559 * ˇ
3560 */
3561 "}
3562 .replace("s", " ") // s is used as space placeholder to prevent format on save
3563 .as_str(),
3564 );
3565 // Ensure that delimiter space is preserved when space is not
3566 // on existing delimiter.
3567 cx.set_state(indoc! {"
3568 /**
3569 *ˇ
3570 */
3571 "});
3572 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3573 cx.assert_editor_state(indoc! {"
3574 /**
3575 *
3576 * ˇ
3577 */
3578 "});
3579 // Ensure that if suffix exists on same line after cursor it
3580 // doesn't add extra new line if prefix is not on same line.
3581 cx.set_state(indoc! {"
3582 /**
3583 ˇ*/
3584 "});
3585 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3586 cx.assert_editor_state(indoc! {"
3587 /**
3588
3589 ˇ*/
3590 "});
3591 // Ensure that it detects suffix after existing prefix.
3592 cx.set_state(indoc! {"
3593 /**ˇ/
3594 "});
3595 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3596 cx.assert_editor_state(indoc! {"
3597 /**
3598 ˇ/
3599 "});
3600 // Ensure that if suffix exists on same line before
3601 // cursor it does not add comment prefix.
3602 cx.set_state(indoc! {"
3603 /** */ˇ
3604 "});
3605 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 /** */
3608 ˇ
3609 "});
3610 // Ensure that if suffix exists on same line before
3611 // cursor it does not add comment prefix.
3612 cx.set_state(indoc! {"
3613 /**
3614 *
3615 */ˇ
3616 "});
3617 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3618 cx.assert_editor_state(indoc! {"
3619 /**
3620 *
3621 */
3622 ˇ
3623 "});
3624
3625 // Ensure that inline comment followed by code
3626 // doesn't add comment prefix on newline
3627 cx.set_state(indoc! {"
3628 /** */ textˇ
3629 "});
3630 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 /** */ text
3633 ˇ
3634 "});
3635
3636 // Ensure that text after comment end tag
3637 // doesn't add comment prefix on newline
3638 cx.set_state(indoc! {"
3639 /**
3640 *
3641 */ˇtext
3642 "});
3643 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3644 cx.assert_editor_state(indoc! {"
3645 /**
3646 *
3647 */
3648 ˇtext
3649 "});
3650
3651 // Ensure if not comment block it doesn't
3652 // add comment prefix on newline
3653 cx.set_state(indoc! {"
3654 * textˇ
3655 "});
3656 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 * text
3659 ˇ
3660 "});
3661 }
3662 // Ensure that comment continuations can be disabled.
3663 update_test_language_settings(cx, |settings| {
3664 settings.defaults.extend_comment_on_newline = Some(false);
3665 });
3666 let mut cx = EditorTestContext::new(cx).await;
3667 cx.set_state(indoc! {"
3668 /**ˇ
3669 "});
3670 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3671 cx.assert_editor_state(indoc! {"
3672 /**
3673 ˇ
3674 "});
3675}
3676
3677#[gpui::test]
3678async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3679 init_test(cx, |settings| {
3680 settings.defaults.tab_size = NonZeroU32::new(4)
3681 });
3682
3683 let lua_language = Arc::new(Language::new(
3684 LanguageConfig {
3685 line_comments: vec!["--".into()],
3686 block_comment: Some(language::BlockCommentConfig {
3687 start: "--[[".into(),
3688 prefix: "".into(),
3689 end: "]]".into(),
3690 tab_size: 0,
3691 }),
3692 ..LanguageConfig::default()
3693 },
3694 None,
3695 ));
3696
3697 let mut cx = EditorTestContext::new(cx).await;
3698 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3699
3700 // Line with line comment should extend
3701 cx.set_state(indoc! {"
3702 --ˇ
3703 "});
3704 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3705 cx.assert_editor_state(indoc! {"
3706 --
3707 --ˇ
3708 "});
3709
3710 // Line with block comment that matches line comment should not extend
3711 cx.set_state(indoc! {"
3712 --[[ˇ
3713 "});
3714 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 --[[
3717 ˇ
3718 "});
3719}
3720
3721#[gpui::test]
3722fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3723 init_test(cx, |_| {});
3724
3725 let editor = cx.add_window(|window, cx| {
3726 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3727 let mut editor = build_editor(buffer, window, cx);
3728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3729 s.select_ranges([
3730 MultiBufferOffset(3)..MultiBufferOffset(4),
3731 MultiBufferOffset(11)..MultiBufferOffset(12),
3732 MultiBufferOffset(19)..MultiBufferOffset(20),
3733 ])
3734 });
3735 editor
3736 });
3737
3738 _ = editor.update(cx, |editor, window, cx| {
3739 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3740 editor.buffer.update(cx, |buffer, cx| {
3741 buffer.edit(
3742 [
3743 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3744 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3745 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3746 ],
3747 None,
3748 cx,
3749 );
3750 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3751 });
3752 assert_eq!(
3753 editor.selections.ranges(&editor.display_snapshot(cx)),
3754 &[
3755 MultiBufferOffset(2)..MultiBufferOffset(2),
3756 MultiBufferOffset(7)..MultiBufferOffset(7),
3757 MultiBufferOffset(12)..MultiBufferOffset(12)
3758 ],
3759 );
3760
3761 editor.insert("Z", window, cx);
3762 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3763
3764 // The selections are moved after the inserted characters
3765 assert_eq!(
3766 editor.selections.ranges(&editor.display_snapshot(cx)),
3767 &[
3768 MultiBufferOffset(3)..MultiBufferOffset(3),
3769 MultiBufferOffset(9)..MultiBufferOffset(9),
3770 MultiBufferOffset(15)..MultiBufferOffset(15)
3771 ],
3772 );
3773 });
3774}
3775
3776#[gpui::test]
3777async fn test_tab(cx: &mut TestAppContext) {
3778 init_test(cx, |settings| {
3779 settings.defaults.tab_size = NonZeroU32::new(3)
3780 });
3781
3782 let mut cx = EditorTestContext::new(cx).await;
3783 cx.set_state(indoc! {"
3784 ˇabˇc
3785 ˇ🏀ˇ🏀ˇefg
3786 dˇ
3787 "});
3788 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3789 cx.assert_editor_state(indoc! {"
3790 ˇab ˇc
3791 ˇ🏀 ˇ🏀 ˇefg
3792 d ˇ
3793 "});
3794
3795 cx.set_state(indoc! {"
3796 a
3797 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3798 "});
3799 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3800 cx.assert_editor_state(indoc! {"
3801 a
3802 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3803 "});
3804}
3805
3806#[gpui::test]
3807async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3808 init_test(cx, |_| {});
3809
3810 let mut cx = EditorTestContext::new(cx).await;
3811 let language = Arc::new(
3812 Language::new(
3813 LanguageConfig::default(),
3814 Some(tree_sitter_rust::LANGUAGE.into()),
3815 )
3816 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3817 .unwrap(),
3818 );
3819 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3820
3821 // test when all cursors are not at suggested indent
3822 // then simply move to their suggested indent location
3823 cx.set_state(indoc! {"
3824 const a: B = (
3825 c(
3826 ˇ
3827 ˇ )
3828 );
3829 "});
3830 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3831 cx.assert_editor_state(indoc! {"
3832 const a: B = (
3833 c(
3834 ˇ
3835 ˇ)
3836 );
3837 "});
3838
3839 // test cursor already at suggested indent not moving when
3840 // other cursors are yet to reach their suggested indents
3841 cx.set_state(indoc! {"
3842 ˇ
3843 const a: B = (
3844 c(
3845 d(
3846 ˇ
3847 )
3848 ˇ
3849 ˇ )
3850 );
3851 "});
3852 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3853 cx.assert_editor_state(indoc! {"
3854 ˇ
3855 const a: B = (
3856 c(
3857 d(
3858 ˇ
3859 )
3860 ˇ
3861 ˇ)
3862 );
3863 "});
3864 // test when all cursors are at suggested indent then tab is inserted
3865 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3866 cx.assert_editor_state(indoc! {"
3867 ˇ
3868 const a: B = (
3869 c(
3870 d(
3871 ˇ
3872 )
3873 ˇ
3874 ˇ)
3875 );
3876 "});
3877
3878 // test when current indent is less than suggested indent,
3879 // we adjust line to match suggested indent and move cursor to it
3880 //
3881 // when no other cursor is at word boundary, all of them should move
3882 cx.set_state(indoc! {"
3883 const a: B = (
3884 c(
3885 d(
3886 ˇ
3887 ˇ )
3888 ˇ )
3889 );
3890 "});
3891 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3892 cx.assert_editor_state(indoc! {"
3893 const a: B = (
3894 c(
3895 d(
3896 ˇ
3897 ˇ)
3898 ˇ)
3899 );
3900 "});
3901
3902 // test when current indent is less than suggested indent,
3903 // we adjust line to match suggested indent and move cursor to it
3904 //
3905 // when some other cursor is at word boundary, it should not move
3906 cx.set_state(indoc! {"
3907 const a: B = (
3908 c(
3909 d(
3910 ˇ
3911 ˇ )
3912 ˇ)
3913 );
3914 "});
3915 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3916 cx.assert_editor_state(indoc! {"
3917 const a: B = (
3918 c(
3919 d(
3920 ˇ
3921 ˇ)
3922 ˇ)
3923 );
3924 "});
3925
3926 // test when current indent is more than suggested indent,
3927 // we just move cursor to current indent instead of suggested indent
3928 //
3929 // when no other cursor is at word boundary, all of them should move
3930 cx.set_state(indoc! {"
3931 const a: B = (
3932 c(
3933 d(
3934 ˇ
3935 ˇ )
3936 ˇ )
3937 );
3938 "});
3939 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3940 cx.assert_editor_state(indoc! {"
3941 const a: B = (
3942 c(
3943 d(
3944 ˇ
3945 ˇ)
3946 ˇ)
3947 );
3948 "});
3949 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3950 cx.assert_editor_state(indoc! {"
3951 const a: B = (
3952 c(
3953 d(
3954 ˇ
3955 ˇ)
3956 ˇ)
3957 );
3958 "});
3959
3960 // test when current indent is more than suggested indent,
3961 // we just move cursor to current indent instead of suggested indent
3962 //
3963 // when some other cursor is at word boundary, it doesn't move
3964 cx.set_state(indoc! {"
3965 const a: B = (
3966 c(
3967 d(
3968 ˇ
3969 ˇ )
3970 ˇ)
3971 );
3972 "});
3973 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3974 cx.assert_editor_state(indoc! {"
3975 const a: B = (
3976 c(
3977 d(
3978 ˇ
3979 ˇ)
3980 ˇ)
3981 );
3982 "});
3983
3984 // handle auto-indent when there are multiple cursors on the same line
3985 cx.set_state(indoc! {"
3986 const a: B = (
3987 c(
3988 ˇ ˇ
3989 ˇ )
3990 );
3991 "});
3992 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 const a: B = (
3995 c(
3996 ˇ
3997 ˇ)
3998 );
3999 "});
4000}
4001
4002#[gpui::test]
4003async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4004 init_test(cx, |settings| {
4005 settings.defaults.tab_size = NonZeroU32::new(3)
4006 });
4007
4008 let mut cx = EditorTestContext::new(cx).await;
4009 cx.set_state(indoc! {"
4010 ˇ
4011 \t ˇ
4012 \t ˇ
4013 \t ˇ
4014 \t \t\t \t \t\t \t\t \t \t ˇ
4015 "});
4016
4017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 ˇ
4020 \t ˇ
4021 \t ˇ
4022 \t ˇ
4023 \t \t\t \t \t\t \t\t \t \t ˇ
4024 "});
4025}
4026
4027#[gpui::test]
4028async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4029 init_test(cx, |settings| {
4030 settings.defaults.tab_size = NonZeroU32::new(4)
4031 });
4032
4033 let language = Arc::new(
4034 Language::new(
4035 LanguageConfig::default(),
4036 Some(tree_sitter_rust::LANGUAGE.into()),
4037 )
4038 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4039 .unwrap(),
4040 );
4041
4042 let mut cx = EditorTestContext::new(cx).await;
4043 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4044 cx.set_state(indoc! {"
4045 fn a() {
4046 if b {
4047 \t ˇc
4048 }
4049 }
4050 "});
4051
4052 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4053 cx.assert_editor_state(indoc! {"
4054 fn a() {
4055 if b {
4056 ˇc
4057 }
4058 }
4059 "});
4060}
4061
4062#[gpui::test]
4063async fn test_indent_outdent(cx: &mut TestAppContext) {
4064 init_test(cx, |settings| {
4065 settings.defaults.tab_size = NonZeroU32::new(4);
4066 });
4067
4068 let mut cx = EditorTestContext::new(cx).await;
4069
4070 cx.set_state(indoc! {"
4071 «oneˇ» «twoˇ»
4072 three
4073 four
4074 "});
4075 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4076 cx.assert_editor_state(indoc! {"
4077 «oneˇ» «twoˇ»
4078 three
4079 four
4080 "});
4081
4082 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4083 cx.assert_editor_state(indoc! {"
4084 «oneˇ» «twoˇ»
4085 three
4086 four
4087 "});
4088
4089 // select across line ending
4090 cx.set_state(indoc! {"
4091 one two
4092 t«hree
4093 ˇ» four
4094 "});
4095 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4096 cx.assert_editor_state(indoc! {"
4097 one two
4098 t«hree
4099 ˇ» four
4100 "});
4101
4102 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4103 cx.assert_editor_state(indoc! {"
4104 one two
4105 t«hree
4106 ˇ» four
4107 "});
4108
4109 // Ensure that indenting/outdenting works when the cursor is at column 0.
4110 cx.set_state(indoc! {"
4111 one two
4112 ˇthree
4113 four
4114 "});
4115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4116 cx.assert_editor_state(indoc! {"
4117 one two
4118 ˇthree
4119 four
4120 "});
4121
4122 cx.set_state(indoc! {"
4123 one two
4124 ˇ three
4125 four
4126 "});
4127 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4128 cx.assert_editor_state(indoc! {"
4129 one two
4130 ˇthree
4131 four
4132 "});
4133}
4134
4135#[gpui::test]
4136async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4137 // This is a regression test for issue #33761
4138 init_test(cx, |_| {});
4139
4140 let mut cx = EditorTestContext::new(cx).await;
4141 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4142 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4143
4144 cx.set_state(
4145 r#"ˇ# ingress:
4146ˇ# api:
4147ˇ# enabled: false
4148ˇ# pathType: Prefix
4149ˇ# console:
4150ˇ# enabled: false
4151ˇ# pathType: Prefix
4152"#,
4153 );
4154
4155 // Press tab to indent all lines
4156 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4157
4158 cx.assert_editor_state(
4159 r#" ˇ# ingress:
4160 ˇ# api:
4161 ˇ# enabled: false
4162 ˇ# pathType: Prefix
4163 ˇ# console:
4164 ˇ# enabled: false
4165 ˇ# pathType: Prefix
4166"#,
4167 );
4168}
4169
4170#[gpui::test]
4171async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4172 // This is a test to make sure our fix for issue #33761 didn't break anything
4173 init_test(cx, |_| {});
4174
4175 let mut cx = EditorTestContext::new(cx).await;
4176 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4177 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4178
4179 cx.set_state(
4180 r#"ˇingress:
4181ˇ api:
4182ˇ enabled: false
4183ˇ pathType: Prefix
4184"#,
4185 );
4186
4187 // Press tab to indent all lines
4188 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4189
4190 cx.assert_editor_state(
4191 r#"ˇingress:
4192 ˇapi:
4193 ˇenabled: false
4194 ˇpathType: Prefix
4195"#,
4196 );
4197}
4198
4199#[gpui::test]
4200async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4201 init_test(cx, |settings| {
4202 settings.defaults.hard_tabs = Some(true);
4203 });
4204
4205 let mut cx = EditorTestContext::new(cx).await;
4206
4207 // select two ranges on one line
4208 cx.set_state(indoc! {"
4209 «oneˇ» «twoˇ»
4210 three
4211 four
4212 "});
4213 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4214 cx.assert_editor_state(indoc! {"
4215 \t«oneˇ» «twoˇ»
4216 three
4217 four
4218 "});
4219 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4220 cx.assert_editor_state(indoc! {"
4221 \t\t«oneˇ» «twoˇ»
4222 three
4223 four
4224 "});
4225 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4226 cx.assert_editor_state(indoc! {"
4227 \t«oneˇ» «twoˇ»
4228 three
4229 four
4230 "});
4231 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4232 cx.assert_editor_state(indoc! {"
4233 «oneˇ» «twoˇ»
4234 three
4235 four
4236 "});
4237
4238 // select across a line ending
4239 cx.set_state(indoc! {"
4240 one two
4241 t«hree
4242 ˇ»four
4243 "});
4244 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4245 cx.assert_editor_state(indoc! {"
4246 one two
4247 \tt«hree
4248 ˇ»four
4249 "});
4250 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4251 cx.assert_editor_state(indoc! {"
4252 one two
4253 \t\tt«hree
4254 ˇ»four
4255 "});
4256 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4257 cx.assert_editor_state(indoc! {"
4258 one two
4259 \tt«hree
4260 ˇ»four
4261 "});
4262 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4263 cx.assert_editor_state(indoc! {"
4264 one two
4265 t«hree
4266 ˇ»four
4267 "});
4268
4269 // Ensure that indenting/outdenting works when the cursor is at column 0.
4270 cx.set_state(indoc! {"
4271 one two
4272 ˇthree
4273 four
4274 "});
4275 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4276 cx.assert_editor_state(indoc! {"
4277 one two
4278 ˇthree
4279 four
4280 "});
4281 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4282 cx.assert_editor_state(indoc! {"
4283 one two
4284 \tˇthree
4285 four
4286 "});
4287 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4288 cx.assert_editor_state(indoc! {"
4289 one two
4290 ˇthree
4291 four
4292 "});
4293}
4294
4295#[gpui::test]
4296fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4297 init_test(cx, |settings| {
4298 settings.languages.0.extend([
4299 (
4300 "TOML".into(),
4301 LanguageSettingsContent {
4302 tab_size: NonZeroU32::new(2),
4303 ..Default::default()
4304 },
4305 ),
4306 (
4307 "Rust".into(),
4308 LanguageSettingsContent {
4309 tab_size: NonZeroU32::new(4),
4310 ..Default::default()
4311 },
4312 ),
4313 ]);
4314 });
4315
4316 let toml_language = Arc::new(Language::new(
4317 LanguageConfig {
4318 name: "TOML".into(),
4319 ..Default::default()
4320 },
4321 None,
4322 ));
4323 let rust_language = Arc::new(Language::new(
4324 LanguageConfig {
4325 name: "Rust".into(),
4326 ..Default::default()
4327 },
4328 None,
4329 ));
4330
4331 let toml_buffer =
4332 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4333 let rust_buffer =
4334 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4335 let multibuffer = cx.new(|cx| {
4336 let mut multibuffer = MultiBuffer::new(ReadWrite);
4337 multibuffer.push_excerpts(
4338 toml_buffer.clone(),
4339 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4340 cx,
4341 );
4342 multibuffer.push_excerpts(
4343 rust_buffer.clone(),
4344 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4345 cx,
4346 );
4347 multibuffer
4348 });
4349
4350 cx.add_window(|window, cx| {
4351 let mut editor = build_editor(multibuffer, window, cx);
4352
4353 assert_eq!(
4354 editor.text(cx),
4355 indoc! {"
4356 a = 1
4357 b = 2
4358
4359 const c: usize = 3;
4360 "}
4361 );
4362
4363 select_ranges(
4364 &mut editor,
4365 indoc! {"
4366 «aˇ» = 1
4367 b = 2
4368
4369 «const c:ˇ» usize = 3;
4370 "},
4371 window,
4372 cx,
4373 );
4374
4375 editor.tab(&Tab, window, cx);
4376 assert_text_with_selections(
4377 &mut editor,
4378 indoc! {"
4379 «aˇ» = 1
4380 b = 2
4381
4382 «const c:ˇ» usize = 3;
4383 "},
4384 cx,
4385 );
4386 editor.backtab(&Backtab, window, cx);
4387 assert_text_with_selections(
4388 &mut editor,
4389 indoc! {"
4390 «aˇ» = 1
4391 b = 2
4392
4393 «const c:ˇ» usize = 3;
4394 "},
4395 cx,
4396 );
4397
4398 editor
4399 });
4400}
4401
4402#[gpui::test]
4403async fn test_backspace(cx: &mut TestAppContext) {
4404 init_test(cx, |_| {});
4405
4406 let mut cx = EditorTestContext::new(cx).await;
4407
4408 // Basic backspace
4409 cx.set_state(indoc! {"
4410 onˇe two three
4411 fou«rˇ» five six
4412 seven «ˇeight nine
4413 »ten
4414 "});
4415 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4416 cx.assert_editor_state(indoc! {"
4417 oˇe two three
4418 fouˇ five six
4419 seven ˇten
4420 "});
4421
4422 // Test backspace inside and around indents
4423 cx.set_state(indoc! {"
4424 zero
4425 ˇone
4426 ˇtwo
4427 ˇ ˇ ˇ three
4428 ˇ ˇ four
4429 "});
4430 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4431 cx.assert_editor_state(indoc! {"
4432 zero
4433 ˇone
4434 ˇtwo
4435 ˇ threeˇ four
4436 "});
4437}
4438
4439#[gpui::test]
4440async fn test_delete(cx: &mut TestAppContext) {
4441 init_test(cx, |_| {});
4442
4443 let mut cx = EditorTestContext::new(cx).await;
4444 cx.set_state(indoc! {"
4445 onˇe two three
4446 fou«rˇ» five six
4447 seven «ˇeight nine
4448 »ten
4449 "});
4450 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4451 cx.assert_editor_state(indoc! {"
4452 onˇ two three
4453 fouˇ five six
4454 seven ˇten
4455 "});
4456}
4457
4458#[gpui::test]
4459fn test_delete_line(cx: &mut TestAppContext) {
4460 init_test(cx, |_| {});
4461
4462 let editor = cx.add_window(|window, cx| {
4463 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4464 build_editor(buffer, window, cx)
4465 });
4466 _ = editor.update(cx, |editor, window, cx| {
4467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4468 s.select_display_ranges([
4469 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4470 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4471 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4472 ])
4473 });
4474 editor.delete_line(&DeleteLine, window, cx);
4475 assert_eq!(editor.display_text(cx), "ghi");
4476 assert_eq!(
4477 display_ranges(editor, cx),
4478 vec![
4479 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4480 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4481 ]
4482 );
4483 });
4484
4485 let editor = cx.add_window(|window, cx| {
4486 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4487 build_editor(buffer, window, cx)
4488 });
4489 _ = editor.update(cx, |editor, window, cx| {
4490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4491 s.select_display_ranges([
4492 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4493 ])
4494 });
4495 editor.delete_line(&DeleteLine, window, cx);
4496 assert_eq!(editor.display_text(cx), "ghi\n");
4497 assert_eq!(
4498 display_ranges(editor, cx),
4499 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4500 );
4501 });
4502
4503 let editor = cx.add_window(|window, cx| {
4504 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4505 build_editor(buffer, window, cx)
4506 });
4507 _ = editor.update(cx, |editor, window, cx| {
4508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4509 s.select_display_ranges([
4510 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4511 ])
4512 });
4513 editor.delete_line(&DeleteLine, window, cx);
4514 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4515 assert_eq!(
4516 display_ranges(editor, cx),
4517 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4518 );
4519 });
4520}
4521
4522#[gpui::test]
4523fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4524 init_test(cx, |_| {});
4525
4526 cx.add_window(|window, cx| {
4527 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4528 let mut editor = build_editor(buffer.clone(), window, cx);
4529 let buffer = buffer.read(cx).as_singleton().unwrap();
4530
4531 assert_eq!(
4532 editor
4533 .selections
4534 .ranges::<Point>(&editor.display_snapshot(cx)),
4535 &[Point::new(0, 0)..Point::new(0, 0)]
4536 );
4537
4538 // When on single line, replace newline at end by space
4539 editor.join_lines(&JoinLines, window, cx);
4540 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4541 assert_eq!(
4542 editor
4543 .selections
4544 .ranges::<Point>(&editor.display_snapshot(cx)),
4545 &[Point::new(0, 3)..Point::new(0, 3)]
4546 );
4547
4548 // When multiple lines are selected, remove newlines that are spanned by the selection
4549 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4550 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4551 });
4552 editor.join_lines(&JoinLines, window, cx);
4553 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4554 assert_eq!(
4555 editor
4556 .selections
4557 .ranges::<Point>(&editor.display_snapshot(cx)),
4558 &[Point::new(0, 11)..Point::new(0, 11)]
4559 );
4560
4561 // Undo should be transactional
4562 editor.undo(&Undo, window, cx);
4563 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4564 assert_eq!(
4565 editor
4566 .selections
4567 .ranges::<Point>(&editor.display_snapshot(cx)),
4568 &[Point::new(0, 5)..Point::new(2, 2)]
4569 );
4570
4571 // When joining an empty line don't insert a space
4572 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4573 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4574 });
4575 editor.join_lines(&JoinLines, window, cx);
4576 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4577 assert_eq!(
4578 editor
4579 .selections
4580 .ranges::<Point>(&editor.display_snapshot(cx)),
4581 [Point::new(2, 3)..Point::new(2, 3)]
4582 );
4583
4584 // We can remove trailing newlines
4585 editor.join_lines(&JoinLines, window, cx);
4586 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4587 assert_eq!(
4588 editor
4589 .selections
4590 .ranges::<Point>(&editor.display_snapshot(cx)),
4591 [Point::new(2, 3)..Point::new(2, 3)]
4592 );
4593
4594 // We don't blow up on the last line
4595 editor.join_lines(&JoinLines, window, cx);
4596 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4597 assert_eq!(
4598 editor
4599 .selections
4600 .ranges::<Point>(&editor.display_snapshot(cx)),
4601 [Point::new(2, 3)..Point::new(2, 3)]
4602 );
4603
4604 // reset to test indentation
4605 editor.buffer.update(cx, |buffer, cx| {
4606 buffer.edit(
4607 [
4608 (Point::new(1, 0)..Point::new(1, 2), " "),
4609 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4610 ],
4611 None,
4612 cx,
4613 )
4614 });
4615
4616 // We remove any leading spaces
4617 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4618 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4619 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4620 });
4621 editor.join_lines(&JoinLines, window, cx);
4622 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4623
4624 // We don't insert a space for a line containing only spaces
4625 editor.join_lines(&JoinLines, window, cx);
4626 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4627
4628 // We ignore any leading tabs
4629 editor.join_lines(&JoinLines, window, cx);
4630 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4631
4632 editor
4633 });
4634}
4635
4636#[gpui::test]
4637fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4638 init_test(cx, |_| {});
4639
4640 cx.add_window(|window, cx| {
4641 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4642 let mut editor = build_editor(buffer.clone(), window, cx);
4643 let buffer = buffer.read(cx).as_singleton().unwrap();
4644
4645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4646 s.select_ranges([
4647 Point::new(0, 2)..Point::new(1, 1),
4648 Point::new(1, 2)..Point::new(1, 2),
4649 Point::new(3, 1)..Point::new(3, 2),
4650 ])
4651 });
4652
4653 editor.join_lines(&JoinLines, window, cx);
4654 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4655
4656 assert_eq!(
4657 editor
4658 .selections
4659 .ranges::<Point>(&editor.display_snapshot(cx)),
4660 [
4661 Point::new(0, 7)..Point::new(0, 7),
4662 Point::new(1, 3)..Point::new(1, 3)
4663 ]
4664 );
4665 editor
4666 });
4667}
4668
4669#[gpui::test]
4670async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4671 init_test(cx, |_| {});
4672
4673 let mut cx = EditorTestContext::new(cx).await;
4674
4675 let diff_base = r#"
4676 Line 0
4677 Line 1
4678 Line 2
4679 Line 3
4680 "#
4681 .unindent();
4682
4683 cx.set_state(
4684 &r#"
4685 ˇLine 0
4686 Line 1
4687 Line 2
4688 Line 3
4689 "#
4690 .unindent(),
4691 );
4692
4693 cx.set_head_text(&diff_base);
4694 executor.run_until_parked();
4695
4696 // Join lines
4697 cx.update_editor(|editor, window, cx| {
4698 editor.join_lines(&JoinLines, window, cx);
4699 });
4700 executor.run_until_parked();
4701
4702 cx.assert_editor_state(
4703 &r#"
4704 Line 0ˇ Line 1
4705 Line 2
4706 Line 3
4707 "#
4708 .unindent(),
4709 );
4710 // Join again
4711 cx.update_editor(|editor, window, cx| {
4712 editor.join_lines(&JoinLines, window, cx);
4713 });
4714 executor.run_until_parked();
4715
4716 cx.assert_editor_state(
4717 &r#"
4718 Line 0 Line 1ˇ Line 2
4719 Line 3
4720 "#
4721 .unindent(),
4722 );
4723}
4724
4725#[gpui::test]
4726async fn test_custom_newlines_cause_no_false_positive_diffs(
4727 executor: BackgroundExecutor,
4728 cx: &mut TestAppContext,
4729) {
4730 init_test(cx, |_| {});
4731 let mut cx = EditorTestContext::new(cx).await;
4732 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4733 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4734 executor.run_until_parked();
4735
4736 cx.update_editor(|editor, window, cx| {
4737 let snapshot = editor.snapshot(window, cx);
4738 assert_eq!(
4739 snapshot
4740 .buffer_snapshot()
4741 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4742 .collect::<Vec<_>>(),
4743 Vec::new(),
4744 "Should not have any diffs for files with custom newlines"
4745 );
4746 });
4747}
4748
4749#[gpui::test]
4750async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4751 init_test(cx, |_| {});
4752
4753 let mut cx = EditorTestContext::new(cx).await;
4754
4755 // Test sort_lines_case_insensitive()
4756 cx.set_state(indoc! {"
4757 «z
4758 y
4759 x
4760 Z
4761 Y
4762 Xˇ»
4763 "});
4764 cx.update_editor(|e, window, cx| {
4765 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4766 });
4767 cx.assert_editor_state(indoc! {"
4768 «x
4769 X
4770 y
4771 Y
4772 z
4773 Zˇ»
4774 "});
4775
4776 // Test sort_lines_by_length()
4777 //
4778 // Demonstrates:
4779 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4780 // - sort is stable
4781 cx.set_state(indoc! {"
4782 «123
4783 æ
4784 12
4785 ∞
4786 1
4787 æˇ»
4788 "});
4789 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4790 cx.assert_editor_state(indoc! {"
4791 «æ
4792 ∞
4793 1
4794 æ
4795 12
4796 123ˇ»
4797 "});
4798
4799 // Test reverse_lines()
4800 cx.set_state(indoc! {"
4801 «5
4802 4
4803 3
4804 2
4805 1ˇ»
4806 "});
4807 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4808 cx.assert_editor_state(indoc! {"
4809 «1
4810 2
4811 3
4812 4
4813 5ˇ»
4814 "});
4815
4816 // Skip testing shuffle_line()
4817
4818 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4819 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4820
4821 // Don't manipulate when cursor is on single line, but expand the selection
4822 cx.set_state(indoc! {"
4823 ddˇdd
4824 ccc
4825 bb
4826 a
4827 "});
4828 cx.update_editor(|e, window, cx| {
4829 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4830 });
4831 cx.assert_editor_state(indoc! {"
4832 «ddddˇ»
4833 ccc
4834 bb
4835 a
4836 "});
4837
4838 // Basic manipulate case
4839 // Start selection moves to column 0
4840 // End of selection shrinks to fit shorter line
4841 cx.set_state(indoc! {"
4842 dd«d
4843 ccc
4844 bb
4845 aaaaaˇ»
4846 "});
4847 cx.update_editor(|e, window, cx| {
4848 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4849 });
4850 cx.assert_editor_state(indoc! {"
4851 «aaaaa
4852 bb
4853 ccc
4854 dddˇ»
4855 "});
4856
4857 // Manipulate case with newlines
4858 cx.set_state(indoc! {"
4859 dd«d
4860 ccc
4861
4862 bb
4863 aaaaa
4864
4865 ˇ»
4866 "});
4867 cx.update_editor(|e, window, cx| {
4868 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4869 });
4870 cx.assert_editor_state(indoc! {"
4871 «
4872
4873 aaaaa
4874 bb
4875 ccc
4876 dddˇ»
4877
4878 "});
4879
4880 // Adding new line
4881 cx.set_state(indoc! {"
4882 aa«a
4883 bbˇ»b
4884 "});
4885 cx.update_editor(|e, window, cx| {
4886 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4887 });
4888 cx.assert_editor_state(indoc! {"
4889 «aaa
4890 bbb
4891 added_lineˇ»
4892 "});
4893
4894 // Removing line
4895 cx.set_state(indoc! {"
4896 aa«a
4897 bbbˇ»
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.manipulate_immutable_lines(window, cx, |lines| {
4901 lines.pop();
4902 })
4903 });
4904 cx.assert_editor_state(indoc! {"
4905 «aaaˇ»
4906 "});
4907
4908 // Removing all lines
4909 cx.set_state(indoc! {"
4910 aa«a
4911 bbbˇ»
4912 "});
4913 cx.update_editor(|e, window, cx| {
4914 e.manipulate_immutable_lines(window, cx, |lines| {
4915 lines.drain(..);
4916 })
4917 });
4918 cx.assert_editor_state(indoc! {"
4919 ˇ
4920 "});
4921}
4922
4923#[gpui::test]
4924async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4925 init_test(cx, |_| {});
4926
4927 let mut cx = EditorTestContext::new(cx).await;
4928
4929 // Consider continuous selection as single selection
4930 cx.set_state(indoc! {"
4931 Aaa«aa
4932 cˇ»c«c
4933 bb
4934 aaaˇ»aa
4935 "});
4936 cx.update_editor(|e, window, cx| {
4937 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4938 });
4939 cx.assert_editor_state(indoc! {"
4940 «Aaaaa
4941 ccc
4942 bb
4943 aaaaaˇ»
4944 "});
4945
4946 cx.set_state(indoc! {"
4947 Aaa«aa
4948 cˇ»c«c
4949 bb
4950 aaaˇ»aa
4951 "});
4952 cx.update_editor(|e, window, cx| {
4953 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4954 });
4955 cx.assert_editor_state(indoc! {"
4956 «Aaaaa
4957 ccc
4958 bbˇ»
4959 "});
4960
4961 // Consider non continuous selection as distinct dedup operations
4962 cx.set_state(indoc! {"
4963 «aaaaa
4964 bb
4965 aaaaa
4966 aaaaaˇ»
4967
4968 aaa«aaˇ»
4969 "});
4970 cx.update_editor(|e, window, cx| {
4971 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4972 });
4973 cx.assert_editor_state(indoc! {"
4974 «aaaaa
4975 bbˇ»
4976
4977 «aaaaaˇ»
4978 "});
4979}
4980
4981#[gpui::test]
4982async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4983 init_test(cx, |_| {});
4984
4985 let mut cx = EditorTestContext::new(cx).await;
4986
4987 cx.set_state(indoc! {"
4988 «Aaa
4989 aAa
4990 Aaaˇ»
4991 "});
4992 cx.update_editor(|e, window, cx| {
4993 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4994 });
4995 cx.assert_editor_state(indoc! {"
4996 «Aaa
4997 aAaˇ»
4998 "});
4999
5000 cx.set_state(indoc! {"
5001 «Aaa
5002 aAa
5003 aaAˇ»
5004 "});
5005 cx.update_editor(|e, window, cx| {
5006 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5007 });
5008 cx.assert_editor_state(indoc! {"
5009 «Aaaˇ»
5010 "});
5011}
5012
5013#[gpui::test]
5014async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5015 init_test(cx, |_| {});
5016
5017 let mut cx = EditorTestContext::new(cx).await;
5018
5019 let js_language = Arc::new(Language::new(
5020 LanguageConfig {
5021 name: "JavaScript".into(),
5022 wrap_characters: Some(language::WrapCharactersConfig {
5023 start_prefix: "<".into(),
5024 start_suffix: ">".into(),
5025 end_prefix: "</".into(),
5026 end_suffix: ">".into(),
5027 }),
5028 ..LanguageConfig::default()
5029 },
5030 None,
5031 ));
5032
5033 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5034
5035 cx.set_state(indoc! {"
5036 «testˇ»
5037 "});
5038 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5039 cx.assert_editor_state(indoc! {"
5040 <«ˇ»>test</«ˇ»>
5041 "});
5042
5043 cx.set_state(indoc! {"
5044 «test
5045 testˇ»
5046 "});
5047 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5048 cx.assert_editor_state(indoc! {"
5049 <«ˇ»>test
5050 test</«ˇ»>
5051 "});
5052
5053 cx.set_state(indoc! {"
5054 teˇst
5055 "});
5056 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5057 cx.assert_editor_state(indoc! {"
5058 te<«ˇ»></«ˇ»>st
5059 "});
5060}
5061
5062#[gpui::test]
5063async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5064 init_test(cx, |_| {});
5065
5066 let mut cx = EditorTestContext::new(cx).await;
5067
5068 let js_language = Arc::new(Language::new(
5069 LanguageConfig {
5070 name: "JavaScript".into(),
5071 wrap_characters: Some(language::WrapCharactersConfig {
5072 start_prefix: "<".into(),
5073 start_suffix: ">".into(),
5074 end_prefix: "</".into(),
5075 end_suffix: ">".into(),
5076 }),
5077 ..LanguageConfig::default()
5078 },
5079 None,
5080 ));
5081
5082 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5083
5084 cx.set_state(indoc! {"
5085 «testˇ»
5086 «testˇ» «testˇ»
5087 «testˇ»
5088 "});
5089 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5090 cx.assert_editor_state(indoc! {"
5091 <«ˇ»>test</«ˇ»>
5092 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5093 <«ˇ»>test</«ˇ»>
5094 "});
5095
5096 cx.set_state(indoc! {"
5097 «test
5098 testˇ»
5099 «test
5100 testˇ»
5101 "});
5102 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5103 cx.assert_editor_state(indoc! {"
5104 <«ˇ»>test
5105 test</«ˇ»>
5106 <«ˇ»>test
5107 test</«ˇ»>
5108 "});
5109}
5110
5111#[gpui::test]
5112async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5113 init_test(cx, |_| {});
5114
5115 let mut cx = EditorTestContext::new(cx).await;
5116
5117 let plaintext_language = Arc::new(Language::new(
5118 LanguageConfig {
5119 name: "Plain Text".into(),
5120 ..LanguageConfig::default()
5121 },
5122 None,
5123 ));
5124
5125 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5126
5127 cx.set_state(indoc! {"
5128 «testˇ»
5129 "});
5130 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5131 cx.assert_editor_state(indoc! {"
5132 «testˇ»
5133 "});
5134}
5135
5136#[gpui::test]
5137async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5138 init_test(cx, |_| {});
5139
5140 let mut cx = EditorTestContext::new(cx).await;
5141
5142 // Manipulate with multiple selections on a single line
5143 cx.set_state(indoc! {"
5144 dd«dd
5145 cˇ»c«c
5146 bb
5147 aaaˇ»aa
5148 "});
5149 cx.update_editor(|e, window, cx| {
5150 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5151 });
5152 cx.assert_editor_state(indoc! {"
5153 «aaaaa
5154 bb
5155 ccc
5156 ddddˇ»
5157 "});
5158
5159 // Manipulate with multiple disjoin selections
5160 cx.set_state(indoc! {"
5161 5«
5162 4
5163 3
5164 2
5165 1ˇ»
5166
5167 dd«dd
5168 ccc
5169 bb
5170 aaaˇ»aa
5171 "});
5172 cx.update_editor(|e, window, cx| {
5173 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5174 });
5175 cx.assert_editor_state(indoc! {"
5176 «1
5177 2
5178 3
5179 4
5180 5ˇ»
5181
5182 «aaaaa
5183 bb
5184 ccc
5185 ddddˇ»
5186 "});
5187
5188 // Adding lines on each selection
5189 cx.set_state(indoc! {"
5190 2«
5191 1ˇ»
5192
5193 bb«bb
5194 aaaˇ»aa
5195 "});
5196 cx.update_editor(|e, window, cx| {
5197 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5198 });
5199 cx.assert_editor_state(indoc! {"
5200 «2
5201 1
5202 added lineˇ»
5203
5204 «bbbb
5205 aaaaa
5206 added lineˇ»
5207 "});
5208
5209 // Removing lines on each selection
5210 cx.set_state(indoc! {"
5211 2«
5212 1ˇ»
5213
5214 bb«bb
5215 aaaˇ»aa
5216 "});
5217 cx.update_editor(|e, window, cx| {
5218 e.manipulate_immutable_lines(window, cx, |lines| {
5219 lines.pop();
5220 })
5221 });
5222 cx.assert_editor_state(indoc! {"
5223 «2ˇ»
5224
5225 «bbbbˇ»
5226 "});
5227}
5228
5229#[gpui::test]
5230async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5231 init_test(cx, |settings| {
5232 settings.defaults.tab_size = NonZeroU32::new(3)
5233 });
5234
5235 let mut cx = EditorTestContext::new(cx).await;
5236
5237 // MULTI SELECTION
5238 // Ln.1 "«" tests empty lines
5239 // Ln.9 tests just leading whitespace
5240 cx.set_state(indoc! {"
5241 «
5242 abc // No indentationˇ»
5243 «\tabc // 1 tabˇ»
5244 \t\tabc « ˇ» // 2 tabs
5245 \t ab«c // Tab followed by space
5246 \tabc // Space followed by tab (3 spaces should be the result)
5247 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5248 abˇ»ˇc ˇ ˇ // Already space indented«
5249 \t
5250 \tabc\tdef // Only the leading tab is manipulatedˇ»
5251 "});
5252 cx.update_editor(|e, window, cx| {
5253 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5254 });
5255 cx.assert_editor_state(
5256 indoc! {"
5257 «
5258 abc // No indentation
5259 abc // 1 tab
5260 abc // 2 tabs
5261 abc // Tab followed by space
5262 abc // Space followed by tab (3 spaces should be the result)
5263 abc // Mixed indentation (tab conversion depends on the column)
5264 abc // Already space indented
5265 ·
5266 abc\tdef // Only the leading tab is manipulatedˇ»
5267 "}
5268 .replace("·", "")
5269 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5270 );
5271
5272 // Test on just a few lines, the others should remain unchanged
5273 // Only lines (3, 5, 10, 11) should change
5274 cx.set_state(
5275 indoc! {"
5276 ·
5277 abc // No indentation
5278 \tabcˇ // 1 tab
5279 \t\tabc // 2 tabs
5280 \t abcˇ // Tab followed by space
5281 \tabc // Space followed by tab (3 spaces should be the result)
5282 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5283 abc // Already space indented
5284 «\t
5285 \tabc\tdef // Only the leading tab is manipulatedˇ»
5286 "}
5287 .replace("·", "")
5288 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5289 );
5290 cx.update_editor(|e, window, cx| {
5291 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5292 });
5293 cx.assert_editor_state(
5294 indoc! {"
5295 ·
5296 abc // No indentation
5297 « abc // 1 tabˇ»
5298 \t\tabc // 2 tabs
5299 « abc // Tab followed by spaceˇ»
5300 \tabc // Space followed by tab (3 spaces should be the result)
5301 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5302 abc // Already space indented
5303 « ·
5304 abc\tdef // Only the leading tab is manipulatedˇ»
5305 "}
5306 .replace("·", "")
5307 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5308 );
5309
5310 // SINGLE SELECTION
5311 // Ln.1 "«" tests empty lines
5312 // Ln.9 tests just leading whitespace
5313 cx.set_state(indoc! {"
5314 «
5315 abc // No indentation
5316 \tabc // 1 tab
5317 \t\tabc // 2 tabs
5318 \t abc // Tab followed by space
5319 \tabc // Space followed by tab (3 spaces should be the result)
5320 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5321 abc // Already space indented
5322 \t
5323 \tabc\tdef // Only the leading tab is manipulatedˇ»
5324 "});
5325 cx.update_editor(|e, window, cx| {
5326 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5327 });
5328 cx.assert_editor_state(
5329 indoc! {"
5330 «
5331 abc // No indentation
5332 abc // 1 tab
5333 abc // 2 tabs
5334 abc // Tab followed by space
5335 abc // Space followed by tab (3 spaces should be the result)
5336 abc // Mixed indentation (tab conversion depends on the column)
5337 abc // Already space indented
5338 ·
5339 abc\tdef // Only the leading tab is manipulatedˇ»
5340 "}
5341 .replace("·", "")
5342 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5343 );
5344}
5345
5346#[gpui::test]
5347async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5348 init_test(cx, |settings| {
5349 settings.defaults.tab_size = NonZeroU32::new(3)
5350 });
5351
5352 let mut cx = EditorTestContext::new(cx).await;
5353
5354 // MULTI SELECTION
5355 // Ln.1 "«" tests empty lines
5356 // Ln.11 tests just leading whitespace
5357 cx.set_state(indoc! {"
5358 «
5359 abˇ»ˇc // No indentation
5360 abc ˇ ˇ // 1 space (< 3 so dont convert)
5361 abc « // 2 spaces (< 3 so dont convert)
5362 abc // 3 spaces (convert)
5363 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5364 «\tˇ»\t«\tˇ»abc // Already tab indented
5365 «\t abc // Tab followed by space
5366 \tabc // Space followed by tab (should be consumed due to tab)
5367 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5368 \tˇ» «\t
5369 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5370 "});
5371 cx.update_editor(|e, window, cx| {
5372 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5373 });
5374 cx.assert_editor_state(indoc! {"
5375 «
5376 abc // No indentation
5377 abc // 1 space (< 3 so dont convert)
5378 abc // 2 spaces (< 3 so dont convert)
5379 \tabc // 3 spaces (convert)
5380 \t abc // 5 spaces (1 tab + 2 spaces)
5381 \t\t\tabc // Already tab indented
5382 \t abc // Tab followed by space
5383 \tabc // Space followed by tab (should be consumed due to tab)
5384 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5385 \t\t\t
5386 \tabc \t // Only the leading spaces should be convertedˇ»
5387 "});
5388
5389 // Test on just a few lines, the other should remain unchanged
5390 // Only lines (4, 8, 11, 12) should change
5391 cx.set_state(
5392 indoc! {"
5393 ·
5394 abc // No indentation
5395 abc // 1 space (< 3 so dont convert)
5396 abc // 2 spaces (< 3 so dont convert)
5397 « abc // 3 spaces (convert)ˇ»
5398 abc // 5 spaces (1 tab + 2 spaces)
5399 \t\t\tabc // Already tab indented
5400 \t abc // Tab followed by space
5401 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5402 \t\t \tabc // Mixed indentation
5403 \t \t \t \tabc // Mixed indentation
5404 \t \tˇ
5405 « abc \t // Only the leading spaces should be convertedˇ»
5406 "}
5407 .replace("·", "")
5408 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5409 );
5410 cx.update_editor(|e, window, cx| {
5411 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5412 });
5413 cx.assert_editor_state(
5414 indoc! {"
5415 ·
5416 abc // No indentation
5417 abc // 1 space (< 3 so dont convert)
5418 abc // 2 spaces (< 3 so dont convert)
5419 «\tabc // 3 spaces (convert)ˇ»
5420 abc // 5 spaces (1 tab + 2 spaces)
5421 \t\t\tabc // Already tab indented
5422 \t abc // Tab followed by space
5423 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5424 \t\t \tabc // Mixed indentation
5425 \t \t \t \tabc // Mixed indentation
5426 «\t\t\t
5427 \tabc \t // Only the leading spaces should be convertedˇ»
5428 "}
5429 .replace("·", "")
5430 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5431 );
5432
5433 // SINGLE SELECTION
5434 // Ln.1 "«" tests empty lines
5435 // Ln.11 tests just leading whitespace
5436 cx.set_state(indoc! {"
5437 «
5438 abc // No indentation
5439 abc // 1 space (< 3 so dont convert)
5440 abc // 2 spaces (< 3 so dont convert)
5441 abc // 3 spaces (convert)
5442 abc // 5 spaces (1 tab + 2 spaces)
5443 \t\t\tabc // Already tab indented
5444 \t abc // Tab followed by space
5445 \tabc // Space followed by tab (should be consumed due to tab)
5446 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5447 \t \t
5448 abc \t // Only the leading spaces should be convertedˇ»
5449 "});
5450 cx.update_editor(|e, window, cx| {
5451 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5452 });
5453 cx.assert_editor_state(indoc! {"
5454 «
5455 abc // No indentation
5456 abc // 1 space (< 3 so dont convert)
5457 abc // 2 spaces (< 3 so dont convert)
5458 \tabc // 3 spaces (convert)
5459 \t abc // 5 spaces (1 tab + 2 spaces)
5460 \t\t\tabc // Already tab indented
5461 \t abc // Tab followed by space
5462 \tabc // Space followed by tab (should be consumed due to tab)
5463 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5464 \t\t\t
5465 \tabc \t // Only the leading spaces should be convertedˇ»
5466 "});
5467}
5468
5469#[gpui::test]
5470async fn test_toggle_case(cx: &mut TestAppContext) {
5471 init_test(cx, |_| {});
5472
5473 let mut cx = EditorTestContext::new(cx).await;
5474
5475 // If all lower case -> upper case
5476 cx.set_state(indoc! {"
5477 «hello worldˇ»
5478 "});
5479 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5480 cx.assert_editor_state(indoc! {"
5481 «HELLO WORLDˇ»
5482 "});
5483
5484 // If all upper case -> lower case
5485 cx.set_state(indoc! {"
5486 «HELLO WORLDˇ»
5487 "});
5488 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5489 cx.assert_editor_state(indoc! {"
5490 «hello worldˇ»
5491 "});
5492
5493 // If any upper case characters are identified -> lower case
5494 // This matches JetBrains IDEs
5495 cx.set_state(indoc! {"
5496 «hEllo worldˇ»
5497 "});
5498 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5499 cx.assert_editor_state(indoc! {"
5500 «hello worldˇ»
5501 "});
5502}
5503
5504#[gpui::test]
5505async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5506 init_test(cx, |_| {});
5507
5508 let mut cx = EditorTestContext::new(cx).await;
5509
5510 cx.set_state(indoc! {"
5511 «implement-windows-supportˇ»
5512 "});
5513 cx.update_editor(|e, window, cx| {
5514 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5515 });
5516 cx.assert_editor_state(indoc! {"
5517 «Implement windows supportˇ»
5518 "});
5519}
5520
5521#[gpui::test]
5522async fn test_manipulate_text(cx: &mut TestAppContext) {
5523 init_test(cx, |_| {});
5524
5525 let mut cx = EditorTestContext::new(cx).await;
5526
5527 // Test convert_to_upper_case()
5528 cx.set_state(indoc! {"
5529 «hello worldˇ»
5530 "});
5531 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5532 cx.assert_editor_state(indoc! {"
5533 «HELLO WORLDˇ»
5534 "});
5535
5536 // Test convert_to_lower_case()
5537 cx.set_state(indoc! {"
5538 «HELLO WORLDˇ»
5539 "});
5540 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5541 cx.assert_editor_state(indoc! {"
5542 «hello worldˇ»
5543 "});
5544
5545 // Test multiple line, single selection case
5546 cx.set_state(indoc! {"
5547 «The quick brown
5548 fox jumps over
5549 the lazy dogˇ»
5550 "});
5551 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5552 cx.assert_editor_state(indoc! {"
5553 «The Quick Brown
5554 Fox Jumps Over
5555 The Lazy Dogˇ»
5556 "});
5557
5558 // Test multiple line, single selection case
5559 cx.set_state(indoc! {"
5560 «The quick brown
5561 fox jumps over
5562 the lazy dogˇ»
5563 "});
5564 cx.update_editor(|e, window, cx| {
5565 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5566 });
5567 cx.assert_editor_state(indoc! {"
5568 «TheQuickBrown
5569 FoxJumpsOver
5570 TheLazyDogˇ»
5571 "});
5572
5573 // From here on out, test more complex cases of manipulate_text()
5574
5575 // Test no selection case - should affect words cursors are in
5576 // Cursor at beginning, middle, and end of word
5577 cx.set_state(indoc! {"
5578 ˇhello big beauˇtiful worldˇ
5579 "});
5580 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5581 cx.assert_editor_state(indoc! {"
5582 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5583 "});
5584
5585 // Test multiple selections on a single line and across multiple lines
5586 cx.set_state(indoc! {"
5587 «Theˇ» quick «brown
5588 foxˇ» jumps «overˇ»
5589 the «lazyˇ» dog
5590 "});
5591 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5592 cx.assert_editor_state(indoc! {"
5593 «THEˇ» quick «BROWN
5594 FOXˇ» jumps «OVERˇ»
5595 the «LAZYˇ» dog
5596 "});
5597
5598 // Test case where text length grows
5599 cx.set_state(indoc! {"
5600 «tschüߡ»
5601 "});
5602 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5603 cx.assert_editor_state(indoc! {"
5604 «TSCHÜSSˇ»
5605 "});
5606
5607 // Test to make sure we don't crash when text shrinks
5608 cx.set_state(indoc! {"
5609 aaa_bbbˇ
5610 "});
5611 cx.update_editor(|e, window, cx| {
5612 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5613 });
5614 cx.assert_editor_state(indoc! {"
5615 «aaaBbbˇ»
5616 "});
5617
5618 // Test to make sure we all aware of the fact that each word can grow and shrink
5619 // Final selections should be aware of this fact
5620 cx.set_state(indoc! {"
5621 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5622 "});
5623 cx.update_editor(|e, window, cx| {
5624 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5625 });
5626 cx.assert_editor_state(indoc! {"
5627 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5628 "});
5629
5630 cx.set_state(indoc! {"
5631 «hElLo, WoRld!ˇ»
5632 "});
5633 cx.update_editor(|e, window, cx| {
5634 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5635 });
5636 cx.assert_editor_state(indoc! {"
5637 «HeLlO, wOrLD!ˇ»
5638 "});
5639
5640 // Test selections with `line_mode() = true`.
5641 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5642 cx.set_state(indoc! {"
5643 «The quick brown
5644 fox jumps over
5645 tˇ»he lazy dog
5646 "});
5647 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5648 cx.assert_editor_state(indoc! {"
5649 «THE QUICK BROWN
5650 FOX JUMPS OVER
5651 THE LAZY DOGˇ»
5652 "});
5653}
5654
5655#[gpui::test]
5656fn test_duplicate_line(cx: &mut TestAppContext) {
5657 init_test(cx, |_| {});
5658
5659 let editor = cx.add_window(|window, cx| {
5660 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5661 build_editor(buffer, window, cx)
5662 });
5663 _ = editor.update(cx, |editor, window, cx| {
5664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5665 s.select_display_ranges([
5666 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5668 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5669 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5670 ])
5671 });
5672 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5673 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5674 assert_eq!(
5675 display_ranges(editor, cx),
5676 vec![
5677 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5678 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5679 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5680 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5681 ]
5682 );
5683 });
5684
5685 let editor = cx.add_window(|window, cx| {
5686 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5687 build_editor(buffer, window, cx)
5688 });
5689 _ = editor.update(cx, |editor, window, cx| {
5690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5691 s.select_display_ranges([
5692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5693 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5694 ])
5695 });
5696 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5697 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5698 assert_eq!(
5699 display_ranges(editor, cx),
5700 vec![
5701 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5702 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5703 ]
5704 );
5705 });
5706
5707 // With `duplicate_line_up` the selections move to the duplicated lines,
5708 // which are inserted above the original lines
5709 let editor = cx.add_window(|window, cx| {
5710 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5711 build_editor(buffer, window, cx)
5712 });
5713 _ = editor.update(cx, |editor, window, cx| {
5714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5715 s.select_display_ranges([
5716 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5717 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5718 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5719 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5720 ])
5721 });
5722 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5723 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5724 assert_eq!(
5725 display_ranges(editor, cx),
5726 vec![
5727 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5728 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5729 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5730 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5731 ]
5732 );
5733 });
5734
5735 let editor = cx.add_window(|window, cx| {
5736 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5737 build_editor(buffer, window, cx)
5738 });
5739 _ = editor.update(cx, |editor, window, cx| {
5740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5741 s.select_display_ranges([
5742 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5743 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5744 ])
5745 });
5746 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5747 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5748 assert_eq!(
5749 display_ranges(editor, cx),
5750 vec![
5751 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5752 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5753 ]
5754 );
5755 });
5756
5757 let editor = cx.add_window(|window, cx| {
5758 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5759 build_editor(buffer, window, cx)
5760 });
5761 _ = editor.update(cx, |editor, window, cx| {
5762 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5763 s.select_display_ranges([
5764 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5765 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5766 ])
5767 });
5768 editor.duplicate_selection(&DuplicateSelection, window, cx);
5769 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5770 assert_eq!(
5771 display_ranges(editor, cx),
5772 vec![
5773 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5774 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5775 ]
5776 );
5777 });
5778}
5779
5780#[gpui::test]
5781fn test_move_line_up_down(cx: &mut TestAppContext) {
5782 init_test(cx, |_| {});
5783
5784 let editor = cx.add_window(|window, cx| {
5785 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5786 build_editor(buffer, window, cx)
5787 });
5788 _ = editor.update(cx, |editor, window, cx| {
5789 editor.fold_creases(
5790 vec![
5791 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5792 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5793 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5794 ],
5795 true,
5796 window,
5797 cx,
5798 );
5799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5800 s.select_display_ranges([
5801 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5802 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5803 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5804 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5805 ])
5806 });
5807 assert_eq!(
5808 editor.display_text(cx),
5809 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5810 );
5811
5812 editor.move_line_up(&MoveLineUp, window, cx);
5813 assert_eq!(
5814 editor.display_text(cx),
5815 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5816 );
5817 assert_eq!(
5818 display_ranges(editor, cx),
5819 vec![
5820 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5821 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5822 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5823 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5824 ]
5825 );
5826 });
5827
5828 _ = editor.update(cx, |editor, window, cx| {
5829 editor.move_line_down(&MoveLineDown, window, cx);
5830 assert_eq!(
5831 editor.display_text(cx),
5832 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5833 );
5834 assert_eq!(
5835 display_ranges(editor, cx),
5836 vec![
5837 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5838 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5839 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5840 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5841 ]
5842 );
5843 });
5844
5845 _ = editor.update(cx, |editor, window, cx| {
5846 editor.move_line_down(&MoveLineDown, window, cx);
5847 assert_eq!(
5848 editor.display_text(cx),
5849 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5850 );
5851 assert_eq!(
5852 display_ranges(editor, cx),
5853 vec![
5854 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5855 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5856 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5857 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5858 ]
5859 );
5860 });
5861
5862 _ = editor.update(cx, |editor, window, cx| {
5863 editor.move_line_up(&MoveLineUp, window, cx);
5864 assert_eq!(
5865 editor.display_text(cx),
5866 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5867 );
5868 assert_eq!(
5869 display_ranges(editor, cx),
5870 vec![
5871 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5872 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5873 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5874 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5875 ]
5876 );
5877 });
5878}
5879
5880#[gpui::test]
5881fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5882 init_test(cx, |_| {});
5883 let editor = cx.add_window(|window, cx| {
5884 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5885 build_editor(buffer, window, cx)
5886 });
5887 _ = editor.update(cx, |editor, window, cx| {
5888 editor.fold_creases(
5889 vec![Crease::simple(
5890 Point::new(6, 4)..Point::new(7, 4),
5891 FoldPlaceholder::test(),
5892 )],
5893 true,
5894 window,
5895 cx,
5896 );
5897 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5898 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5899 });
5900 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5901 editor.move_line_up(&MoveLineUp, window, cx);
5902 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5903 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5904 });
5905}
5906
5907#[gpui::test]
5908fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5909 init_test(cx, |_| {});
5910
5911 let editor = cx.add_window(|window, cx| {
5912 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5913 build_editor(buffer, window, cx)
5914 });
5915 _ = editor.update(cx, |editor, window, cx| {
5916 let snapshot = editor.buffer.read(cx).snapshot(cx);
5917 editor.insert_blocks(
5918 [BlockProperties {
5919 style: BlockStyle::Fixed,
5920 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5921 height: Some(1),
5922 render: Arc::new(|_| div().into_any()),
5923 priority: 0,
5924 }],
5925 Some(Autoscroll::fit()),
5926 cx,
5927 );
5928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5929 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5930 });
5931 editor.move_line_down(&MoveLineDown, window, cx);
5932 });
5933}
5934
5935#[gpui::test]
5936async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5937 init_test(cx, |_| {});
5938
5939 let mut cx = EditorTestContext::new(cx).await;
5940 cx.set_state(
5941 &"
5942 ˇzero
5943 one
5944 two
5945 three
5946 four
5947 five
5948 "
5949 .unindent(),
5950 );
5951
5952 // Create a four-line block that replaces three lines of text.
5953 cx.update_editor(|editor, window, cx| {
5954 let snapshot = editor.snapshot(window, cx);
5955 let snapshot = &snapshot.buffer_snapshot();
5956 let placement = BlockPlacement::Replace(
5957 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5958 );
5959 editor.insert_blocks(
5960 [BlockProperties {
5961 placement,
5962 height: Some(4),
5963 style: BlockStyle::Sticky,
5964 render: Arc::new(|_| gpui::div().into_any_element()),
5965 priority: 0,
5966 }],
5967 None,
5968 cx,
5969 );
5970 });
5971
5972 // Move down so that the cursor touches the block.
5973 cx.update_editor(|editor, window, cx| {
5974 editor.move_down(&Default::default(), window, cx);
5975 });
5976 cx.assert_editor_state(
5977 &"
5978 zero
5979 «one
5980 two
5981 threeˇ»
5982 four
5983 five
5984 "
5985 .unindent(),
5986 );
5987
5988 // Move down past the block.
5989 cx.update_editor(|editor, window, cx| {
5990 editor.move_down(&Default::default(), window, cx);
5991 });
5992 cx.assert_editor_state(
5993 &"
5994 zero
5995 one
5996 two
5997 three
5998 ˇfour
5999 five
6000 "
6001 .unindent(),
6002 );
6003}
6004
6005#[gpui::test]
6006fn test_transpose(cx: &mut TestAppContext) {
6007 init_test(cx, |_| {});
6008
6009 _ = cx.add_window(|window, cx| {
6010 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6011 editor.set_style(EditorStyle::default(), window, cx);
6012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6013 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6014 });
6015 editor.transpose(&Default::default(), window, cx);
6016 assert_eq!(editor.text(cx), "bac");
6017 assert_eq!(
6018 editor.selections.ranges(&editor.display_snapshot(cx)),
6019 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6020 );
6021
6022 editor.transpose(&Default::default(), window, cx);
6023 assert_eq!(editor.text(cx), "bca");
6024 assert_eq!(
6025 editor.selections.ranges(&editor.display_snapshot(cx)),
6026 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6027 );
6028
6029 editor.transpose(&Default::default(), window, cx);
6030 assert_eq!(editor.text(cx), "bac");
6031 assert_eq!(
6032 editor.selections.ranges(&editor.display_snapshot(cx)),
6033 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6034 );
6035
6036 editor
6037 });
6038
6039 _ = cx.add_window(|window, cx| {
6040 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6041 editor.set_style(EditorStyle::default(), window, cx);
6042 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6043 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6044 });
6045 editor.transpose(&Default::default(), window, cx);
6046 assert_eq!(editor.text(cx), "acb\nde");
6047 assert_eq!(
6048 editor.selections.ranges(&editor.display_snapshot(cx)),
6049 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6050 );
6051
6052 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6053 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6054 });
6055 editor.transpose(&Default::default(), window, cx);
6056 assert_eq!(editor.text(cx), "acbd\ne");
6057 assert_eq!(
6058 editor.selections.ranges(&editor.display_snapshot(cx)),
6059 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6060 );
6061
6062 editor.transpose(&Default::default(), window, cx);
6063 assert_eq!(editor.text(cx), "acbde\n");
6064 assert_eq!(
6065 editor.selections.ranges(&editor.display_snapshot(cx)),
6066 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6067 );
6068
6069 editor.transpose(&Default::default(), window, cx);
6070 assert_eq!(editor.text(cx), "acbd\ne");
6071 assert_eq!(
6072 editor.selections.ranges(&editor.display_snapshot(cx)),
6073 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6074 );
6075
6076 editor
6077 });
6078
6079 _ = cx.add_window(|window, cx| {
6080 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6081 editor.set_style(EditorStyle::default(), window, cx);
6082 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6083 s.select_ranges([
6084 MultiBufferOffset(1)..MultiBufferOffset(1),
6085 MultiBufferOffset(2)..MultiBufferOffset(2),
6086 MultiBufferOffset(4)..MultiBufferOffset(4),
6087 ])
6088 });
6089 editor.transpose(&Default::default(), window, cx);
6090 assert_eq!(editor.text(cx), "bacd\ne");
6091 assert_eq!(
6092 editor.selections.ranges(&editor.display_snapshot(cx)),
6093 [
6094 MultiBufferOffset(2)..MultiBufferOffset(2),
6095 MultiBufferOffset(3)..MultiBufferOffset(3),
6096 MultiBufferOffset(5)..MultiBufferOffset(5)
6097 ]
6098 );
6099
6100 editor.transpose(&Default::default(), window, cx);
6101 assert_eq!(editor.text(cx), "bcade\n");
6102 assert_eq!(
6103 editor.selections.ranges(&editor.display_snapshot(cx)),
6104 [
6105 MultiBufferOffset(3)..MultiBufferOffset(3),
6106 MultiBufferOffset(4)..MultiBufferOffset(4),
6107 MultiBufferOffset(6)..MultiBufferOffset(6)
6108 ]
6109 );
6110
6111 editor.transpose(&Default::default(), window, cx);
6112 assert_eq!(editor.text(cx), "bcda\ne");
6113 assert_eq!(
6114 editor.selections.ranges(&editor.display_snapshot(cx)),
6115 [
6116 MultiBufferOffset(4)..MultiBufferOffset(4),
6117 MultiBufferOffset(6)..MultiBufferOffset(6)
6118 ]
6119 );
6120
6121 editor.transpose(&Default::default(), window, cx);
6122 assert_eq!(editor.text(cx), "bcade\n");
6123 assert_eq!(
6124 editor.selections.ranges(&editor.display_snapshot(cx)),
6125 [
6126 MultiBufferOffset(4)..MultiBufferOffset(4),
6127 MultiBufferOffset(6)..MultiBufferOffset(6)
6128 ]
6129 );
6130
6131 editor.transpose(&Default::default(), window, cx);
6132 assert_eq!(editor.text(cx), "bcaed\n");
6133 assert_eq!(
6134 editor.selections.ranges(&editor.display_snapshot(cx)),
6135 [
6136 MultiBufferOffset(5)..MultiBufferOffset(5),
6137 MultiBufferOffset(6)..MultiBufferOffset(6)
6138 ]
6139 );
6140
6141 editor
6142 });
6143
6144 _ = cx.add_window(|window, cx| {
6145 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6146 editor.set_style(EditorStyle::default(), window, cx);
6147 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6148 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6149 });
6150 editor.transpose(&Default::default(), window, cx);
6151 assert_eq!(editor.text(cx), "🏀🍐✋");
6152 assert_eq!(
6153 editor.selections.ranges(&editor.display_snapshot(cx)),
6154 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6155 );
6156
6157 editor.transpose(&Default::default(), window, cx);
6158 assert_eq!(editor.text(cx), "🏀✋🍐");
6159 assert_eq!(
6160 editor.selections.ranges(&editor.display_snapshot(cx)),
6161 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6162 );
6163
6164 editor.transpose(&Default::default(), window, cx);
6165 assert_eq!(editor.text(cx), "🏀🍐✋");
6166 assert_eq!(
6167 editor.selections.ranges(&editor.display_snapshot(cx)),
6168 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6169 );
6170
6171 editor
6172 });
6173}
6174
6175#[gpui::test]
6176async fn test_rewrap(cx: &mut TestAppContext) {
6177 init_test(cx, |settings| {
6178 settings.languages.0.extend([
6179 (
6180 "Markdown".into(),
6181 LanguageSettingsContent {
6182 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6183 preferred_line_length: Some(40),
6184 ..Default::default()
6185 },
6186 ),
6187 (
6188 "Plain Text".into(),
6189 LanguageSettingsContent {
6190 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6191 preferred_line_length: Some(40),
6192 ..Default::default()
6193 },
6194 ),
6195 (
6196 "C++".into(),
6197 LanguageSettingsContent {
6198 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6199 preferred_line_length: Some(40),
6200 ..Default::default()
6201 },
6202 ),
6203 (
6204 "Python".into(),
6205 LanguageSettingsContent {
6206 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6207 preferred_line_length: Some(40),
6208 ..Default::default()
6209 },
6210 ),
6211 (
6212 "Rust".into(),
6213 LanguageSettingsContent {
6214 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6215 preferred_line_length: Some(40),
6216 ..Default::default()
6217 },
6218 ),
6219 ])
6220 });
6221
6222 let mut cx = EditorTestContext::new(cx).await;
6223
6224 let cpp_language = Arc::new(Language::new(
6225 LanguageConfig {
6226 name: "C++".into(),
6227 line_comments: vec!["// ".into()],
6228 ..LanguageConfig::default()
6229 },
6230 None,
6231 ));
6232 let python_language = Arc::new(Language::new(
6233 LanguageConfig {
6234 name: "Python".into(),
6235 line_comments: vec!["# ".into()],
6236 ..LanguageConfig::default()
6237 },
6238 None,
6239 ));
6240 let markdown_language = Arc::new(Language::new(
6241 LanguageConfig {
6242 name: "Markdown".into(),
6243 rewrap_prefixes: vec![
6244 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6245 regex::Regex::new("[-*+]\\s+").unwrap(),
6246 ],
6247 ..LanguageConfig::default()
6248 },
6249 None,
6250 ));
6251 let rust_language = Arc::new(
6252 Language::new(
6253 LanguageConfig {
6254 name: "Rust".into(),
6255 line_comments: vec!["// ".into(), "/// ".into()],
6256 ..LanguageConfig::default()
6257 },
6258 Some(tree_sitter_rust::LANGUAGE.into()),
6259 )
6260 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6261 .unwrap(),
6262 );
6263
6264 let plaintext_language = Arc::new(Language::new(
6265 LanguageConfig {
6266 name: "Plain Text".into(),
6267 ..LanguageConfig::default()
6268 },
6269 None,
6270 ));
6271
6272 // Test basic rewrapping of a long line with a cursor
6273 assert_rewrap(
6274 indoc! {"
6275 // ˇThis is a long comment that needs to be wrapped.
6276 "},
6277 indoc! {"
6278 // ˇThis is a long comment that needs to
6279 // be wrapped.
6280 "},
6281 cpp_language.clone(),
6282 &mut cx,
6283 );
6284
6285 // Test rewrapping a full selection
6286 assert_rewrap(
6287 indoc! {"
6288 «// This selected long comment needs to be wrapped.ˇ»"
6289 },
6290 indoc! {"
6291 «// This selected long comment needs to
6292 // be wrapped.ˇ»"
6293 },
6294 cpp_language.clone(),
6295 &mut cx,
6296 );
6297
6298 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6299 assert_rewrap(
6300 indoc! {"
6301 // ˇThis is the first line.
6302 // Thisˇ is the second line.
6303 // This is the thirdˇ line, all part of one paragraph.
6304 "},
6305 indoc! {"
6306 // ˇThis is the first line. Thisˇ is the
6307 // second line. This is the thirdˇ line,
6308 // all part of one paragraph.
6309 "},
6310 cpp_language.clone(),
6311 &mut cx,
6312 );
6313
6314 // Test multiple cursors in different paragraphs trigger separate rewraps
6315 assert_rewrap(
6316 indoc! {"
6317 // ˇThis is the first paragraph, first line.
6318 // ˇThis is the first paragraph, second line.
6319
6320 // ˇThis is the second paragraph, first line.
6321 // ˇThis is the second paragraph, second line.
6322 "},
6323 indoc! {"
6324 // ˇThis is the first paragraph, first
6325 // line. ˇThis is the first paragraph,
6326 // second line.
6327
6328 // ˇThis is the second paragraph, first
6329 // line. ˇThis is the second paragraph,
6330 // second line.
6331 "},
6332 cpp_language.clone(),
6333 &mut cx,
6334 );
6335
6336 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6337 assert_rewrap(
6338 indoc! {"
6339 «// A regular long long comment to be wrapped.
6340 /// A documentation long comment to be wrapped.ˇ»
6341 "},
6342 indoc! {"
6343 «// A regular long long comment to be
6344 // wrapped.
6345 /// A documentation long comment to be
6346 /// wrapped.ˇ»
6347 "},
6348 rust_language.clone(),
6349 &mut cx,
6350 );
6351
6352 // Test that change in indentation level trigger seperate rewraps
6353 assert_rewrap(
6354 indoc! {"
6355 fn foo() {
6356 «// This is a long comment at the base indent.
6357 // This is a long comment at the next indent.ˇ»
6358 }
6359 "},
6360 indoc! {"
6361 fn foo() {
6362 «// This is a long comment at the
6363 // base indent.
6364 // This is a long comment at the
6365 // next indent.ˇ»
6366 }
6367 "},
6368 rust_language.clone(),
6369 &mut cx,
6370 );
6371
6372 // Test that different comment prefix characters (e.g., '#') are handled correctly
6373 assert_rewrap(
6374 indoc! {"
6375 # ˇThis is a long comment using a pound sign.
6376 "},
6377 indoc! {"
6378 # ˇThis is a long comment using a pound
6379 # sign.
6380 "},
6381 python_language,
6382 &mut cx,
6383 );
6384
6385 // Test rewrapping only affects comments, not code even when selected
6386 assert_rewrap(
6387 indoc! {"
6388 «/// This doc comment is long and should be wrapped.
6389 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6390 "},
6391 indoc! {"
6392 «/// This doc comment is long and should
6393 /// be wrapped.
6394 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6395 "},
6396 rust_language.clone(),
6397 &mut cx,
6398 );
6399
6400 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6401 assert_rewrap(
6402 indoc! {"
6403 # Header
6404
6405 A long long long line of markdown text to wrap.ˇ
6406 "},
6407 indoc! {"
6408 # Header
6409
6410 A long long long line of markdown text
6411 to wrap.ˇ
6412 "},
6413 markdown_language.clone(),
6414 &mut cx,
6415 );
6416
6417 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6418 assert_rewrap(
6419 indoc! {"
6420 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6421 2. This is a numbered list item that is very long and needs to be wrapped properly.
6422 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6423 "},
6424 indoc! {"
6425 «1. This is a numbered list item that is
6426 very long and needs to be wrapped
6427 properly.
6428 2. This is a numbered list item that is
6429 very long and needs to be wrapped
6430 properly.
6431 - This is an unordered list item that is
6432 also very long and should not merge
6433 with the numbered item.ˇ»
6434 "},
6435 markdown_language.clone(),
6436 &mut cx,
6437 );
6438
6439 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6440 assert_rewrap(
6441 indoc! {"
6442 «1. This is a numbered list item that is
6443 very long and needs to be wrapped
6444 properly.
6445 2. This is a numbered list item that is
6446 very long and needs to be wrapped
6447 properly.
6448 - This is an unordered list item that is
6449 also very long and should not merge with
6450 the numbered item.ˇ»
6451 "},
6452 indoc! {"
6453 «1. This is a numbered list item that is
6454 very long and needs to be wrapped
6455 properly.
6456 2. This is a numbered list item that is
6457 very long and needs to be wrapped
6458 properly.
6459 - This is an unordered list item that is
6460 also very long and should not merge
6461 with the numbered item.ˇ»
6462 "},
6463 markdown_language.clone(),
6464 &mut cx,
6465 );
6466
6467 // Test that rewrapping maintain indents even when they already exists.
6468 assert_rewrap(
6469 indoc! {"
6470 «1. This is a numbered list
6471 item that is very long and needs to be wrapped properly.
6472 2. This is a numbered list
6473 item that is very long and needs to be wrapped properly.
6474 - This is an unordered list item that is also very long and
6475 should not merge with the numbered item.ˇ»
6476 "},
6477 indoc! {"
6478 «1. This is a numbered list item that is
6479 very long and needs to be wrapped
6480 properly.
6481 2. This is a numbered list item that is
6482 very long and needs to be wrapped
6483 properly.
6484 - This is an unordered list item that is
6485 also very long and should not merge
6486 with the numbered item.ˇ»
6487 "},
6488 markdown_language,
6489 &mut cx,
6490 );
6491
6492 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6493 assert_rewrap(
6494 indoc! {"
6495 ˇThis is a very long line of plain text that will be wrapped.
6496 "},
6497 indoc! {"
6498 ˇThis is a very long line of plain text
6499 that will be wrapped.
6500 "},
6501 plaintext_language.clone(),
6502 &mut cx,
6503 );
6504
6505 // Test that non-commented code acts as a paragraph boundary within a selection
6506 assert_rewrap(
6507 indoc! {"
6508 «// This is the first long comment block to be wrapped.
6509 fn my_func(a: u32);
6510 // This is the second long comment block to be wrapped.ˇ»
6511 "},
6512 indoc! {"
6513 «// This is the first long comment block
6514 // to be wrapped.
6515 fn my_func(a: u32);
6516 // This is the second long comment block
6517 // to be wrapped.ˇ»
6518 "},
6519 rust_language,
6520 &mut cx,
6521 );
6522
6523 // Test rewrapping multiple selections, including ones with blank lines or tabs
6524 assert_rewrap(
6525 indoc! {"
6526 «ˇThis is a very long line that will be wrapped.
6527
6528 This is another paragraph in the same selection.»
6529
6530 «\tThis is a very long indented line that will be wrapped.ˇ»
6531 "},
6532 indoc! {"
6533 «ˇThis is a very long line that will be
6534 wrapped.
6535
6536 This is another paragraph in the same
6537 selection.»
6538
6539 «\tThis is a very long indented line
6540 \tthat will be wrapped.ˇ»
6541 "},
6542 plaintext_language,
6543 &mut cx,
6544 );
6545
6546 // Test that an empty comment line acts as a paragraph boundary
6547 assert_rewrap(
6548 indoc! {"
6549 // ˇThis is a long comment that will be wrapped.
6550 //
6551 // And this is another long comment that will also be wrapped.ˇ
6552 "},
6553 indoc! {"
6554 // ˇThis is a long comment that will be
6555 // wrapped.
6556 //
6557 // And this is another long comment that
6558 // will also be wrapped.ˇ
6559 "},
6560 cpp_language,
6561 &mut cx,
6562 );
6563
6564 #[track_caller]
6565 fn assert_rewrap(
6566 unwrapped_text: &str,
6567 wrapped_text: &str,
6568 language: Arc<Language>,
6569 cx: &mut EditorTestContext,
6570 ) {
6571 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6572 cx.set_state(unwrapped_text);
6573 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6574 cx.assert_editor_state(wrapped_text);
6575 }
6576}
6577
6578#[gpui::test]
6579async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6580 init_test(cx, |settings| {
6581 settings.languages.0.extend([(
6582 "Rust".into(),
6583 LanguageSettingsContent {
6584 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6585 preferred_line_length: Some(40),
6586 ..Default::default()
6587 },
6588 )])
6589 });
6590
6591 let mut cx = EditorTestContext::new(cx).await;
6592
6593 let rust_lang = Arc::new(
6594 Language::new(
6595 LanguageConfig {
6596 name: "Rust".into(),
6597 line_comments: vec!["// ".into()],
6598 block_comment: Some(BlockCommentConfig {
6599 start: "/*".into(),
6600 end: "*/".into(),
6601 prefix: "* ".into(),
6602 tab_size: 1,
6603 }),
6604 documentation_comment: Some(BlockCommentConfig {
6605 start: "/**".into(),
6606 end: "*/".into(),
6607 prefix: "* ".into(),
6608 tab_size: 1,
6609 }),
6610
6611 ..LanguageConfig::default()
6612 },
6613 Some(tree_sitter_rust::LANGUAGE.into()),
6614 )
6615 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6616 .unwrap(),
6617 );
6618
6619 // regular block comment
6620 assert_rewrap(
6621 indoc! {"
6622 /*
6623 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6624 */
6625 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6626 "},
6627 indoc! {"
6628 /*
6629 *ˇ Lorem ipsum dolor sit amet,
6630 * consectetur adipiscing elit.
6631 */
6632 /*
6633 *ˇ Lorem ipsum dolor sit amet,
6634 * consectetur adipiscing elit.
6635 */
6636 "},
6637 rust_lang.clone(),
6638 &mut cx,
6639 );
6640
6641 // indent is respected
6642 assert_rewrap(
6643 indoc! {"
6644 {}
6645 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6646 "},
6647 indoc! {"
6648 {}
6649 /*
6650 *ˇ Lorem ipsum dolor sit amet,
6651 * consectetur adipiscing elit.
6652 */
6653 "},
6654 rust_lang.clone(),
6655 &mut cx,
6656 );
6657
6658 // short block comments with inline delimiters
6659 assert_rewrap(
6660 indoc! {"
6661 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6662 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6663 */
6664 /*
6665 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6666 "},
6667 indoc! {"
6668 /*
6669 *ˇ Lorem ipsum dolor sit amet,
6670 * consectetur adipiscing elit.
6671 */
6672 /*
6673 *ˇ Lorem ipsum dolor sit amet,
6674 * consectetur adipiscing elit.
6675 */
6676 /*
6677 *ˇ Lorem ipsum dolor sit amet,
6678 * consectetur adipiscing elit.
6679 */
6680 "},
6681 rust_lang.clone(),
6682 &mut cx,
6683 );
6684
6685 // multiline block comment with inline start/end delimiters
6686 assert_rewrap(
6687 indoc! {"
6688 /*ˇ Lorem ipsum dolor sit amet,
6689 * consectetur adipiscing elit. */
6690 "},
6691 indoc! {"
6692 /*
6693 *ˇ Lorem ipsum dolor sit amet,
6694 * consectetur adipiscing elit.
6695 */
6696 "},
6697 rust_lang.clone(),
6698 &mut cx,
6699 );
6700
6701 // block comment rewrap still respects paragraph bounds
6702 assert_rewrap(
6703 indoc! {"
6704 /*
6705 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6706 *
6707 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6708 */
6709 "},
6710 indoc! {"
6711 /*
6712 *ˇ Lorem ipsum dolor sit amet,
6713 * consectetur adipiscing elit.
6714 *
6715 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6716 */
6717 "},
6718 rust_lang.clone(),
6719 &mut cx,
6720 );
6721
6722 // documentation comments
6723 assert_rewrap(
6724 indoc! {"
6725 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6726 /**
6727 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6728 */
6729 "},
6730 indoc! {"
6731 /**
6732 *ˇ Lorem ipsum dolor sit amet,
6733 * consectetur adipiscing elit.
6734 */
6735 /**
6736 *ˇ Lorem ipsum dolor sit amet,
6737 * consectetur adipiscing elit.
6738 */
6739 "},
6740 rust_lang.clone(),
6741 &mut cx,
6742 );
6743
6744 // different, adjacent comments
6745 assert_rewrap(
6746 indoc! {"
6747 /**
6748 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6749 */
6750 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6751 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6752 "},
6753 indoc! {"
6754 /**
6755 *ˇ Lorem ipsum dolor sit amet,
6756 * consectetur adipiscing elit.
6757 */
6758 /*
6759 *ˇ Lorem ipsum dolor sit amet,
6760 * consectetur adipiscing elit.
6761 */
6762 //ˇ Lorem ipsum dolor sit amet,
6763 // consectetur adipiscing elit.
6764 "},
6765 rust_lang.clone(),
6766 &mut cx,
6767 );
6768
6769 // selection w/ single short block comment
6770 assert_rewrap(
6771 indoc! {"
6772 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6773 "},
6774 indoc! {"
6775 «/*
6776 * Lorem ipsum dolor sit amet,
6777 * consectetur adipiscing elit.
6778 */ˇ»
6779 "},
6780 rust_lang.clone(),
6781 &mut cx,
6782 );
6783
6784 // rewrapping a single comment w/ abutting comments
6785 assert_rewrap(
6786 indoc! {"
6787 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6788 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6789 "},
6790 indoc! {"
6791 /*
6792 * ˇLorem ipsum dolor sit amet,
6793 * consectetur adipiscing elit.
6794 */
6795 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6796 "},
6797 rust_lang.clone(),
6798 &mut cx,
6799 );
6800
6801 // selection w/ non-abutting short block comments
6802 assert_rewrap(
6803 indoc! {"
6804 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6805
6806 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6807 "},
6808 indoc! {"
6809 «/*
6810 * Lorem ipsum dolor sit amet,
6811 * consectetur adipiscing elit.
6812 */
6813
6814 /*
6815 * Lorem ipsum dolor sit amet,
6816 * consectetur adipiscing elit.
6817 */ˇ»
6818 "},
6819 rust_lang.clone(),
6820 &mut cx,
6821 );
6822
6823 // selection of multiline block comments
6824 assert_rewrap(
6825 indoc! {"
6826 «/* Lorem ipsum dolor sit amet,
6827 * consectetur adipiscing elit. */ˇ»
6828 "},
6829 indoc! {"
6830 «/*
6831 * Lorem ipsum dolor sit amet,
6832 * consectetur adipiscing elit.
6833 */ˇ»
6834 "},
6835 rust_lang.clone(),
6836 &mut cx,
6837 );
6838
6839 // partial selection of multiline block comments
6840 assert_rewrap(
6841 indoc! {"
6842 «/* Lorem ipsum dolor sit amet,ˇ»
6843 * consectetur adipiscing elit. */
6844 /* Lorem ipsum dolor sit amet,
6845 «* consectetur adipiscing elit. */ˇ»
6846 "},
6847 indoc! {"
6848 «/*
6849 * Lorem ipsum dolor sit amet,ˇ»
6850 * consectetur adipiscing elit. */
6851 /* Lorem ipsum dolor sit amet,
6852 «* consectetur adipiscing elit.
6853 */ˇ»
6854 "},
6855 rust_lang.clone(),
6856 &mut cx,
6857 );
6858
6859 // selection w/ abutting short block comments
6860 // TODO: should not be combined; should rewrap as 2 comments
6861 assert_rewrap(
6862 indoc! {"
6863 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6864 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6865 "},
6866 // desired behavior:
6867 // indoc! {"
6868 // «/*
6869 // * Lorem ipsum dolor sit amet,
6870 // * consectetur adipiscing elit.
6871 // */
6872 // /*
6873 // * Lorem ipsum dolor sit amet,
6874 // * consectetur adipiscing elit.
6875 // */ˇ»
6876 // "},
6877 // actual behaviour:
6878 indoc! {"
6879 «/*
6880 * Lorem ipsum dolor sit amet,
6881 * consectetur adipiscing elit. Lorem
6882 * ipsum dolor sit amet, consectetur
6883 * adipiscing elit.
6884 */ˇ»
6885 "},
6886 rust_lang.clone(),
6887 &mut cx,
6888 );
6889
6890 // TODO: same as above, but with delimiters on separate line
6891 // assert_rewrap(
6892 // indoc! {"
6893 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6894 // */
6895 // /*
6896 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6897 // "},
6898 // // desired:
6899 // // indoc! {"
6900 // // «/*
6901 // // * Lorem ipsum dolor sit amet,
6902 // // * consectetur adipiscing elit.
6903 // // */
6904 // // /*
6905 // // * Lorem ipsum dolor sit amet,
6906 // // * consectetur adipiscing elit.
6907 // // */ˇ»
6908 // // "},
6909 // // actual: (but with trailing w/s on the empty lines)
6910 // indoc! {"
6911 // «/*
6912 // * Lorem ipsum dolor sit amet,
6913 // * consectetur adipiscing elit.
6914 // *
6915 // */
6916 // /*
6917 // *
6918 // * Lorem ipsum dolor sit amet,
6919 // * consectetur adipiscing elit.
6920 // */ˇ»
6921 // "},
6922 // rust_lang.clone(),
6923 // &mut cx,
6924 // );
6925
6926 // TODO these are unhandled edge cases; not correct, just documenting known issues
6927 assert_rewrap(
6928 indoc! {"
6929 /*
6930 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6931 */
6932 /*
6933 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6934 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6935 "},
6936 // desired:
6937 // indoc! {"
6938 // /*
6939 // *ˇ Lorem ipsum dolor sit amet,
6940 // * consectetur adipiscing elit.
6941 // */
6942 // /*
6943 // *ˇ Lorem ipsum dolor sit amet,
6944 // * consectetur adipiscing elit.
6945 // */
6946 // /*
6947 // *ˇ Lorem ipsum dolor sit amet
6948 // */ /* consectetur adipiscing elit. */
6949 // "},
6950 // actual:
6951 indoc! {"
6952 /*
6953 //ˇ Lorem ipsum dolor sit amet,
6954 // consectetur adipiscing elit.
6955 */
6956 /*
6957 * //ˇ Lorem ipsum dolor sit amet,
6958 * consectetur adipiscing elit.
6959 */
6960 /*
6961 *ˇ Lorem ipsum dolor sit amet */ /*
6962 * consectetur adipiscing elit.
6963 */
6964 "},
6965 rust_lang,
6966 &mut cx,
6967 );
6968
6969 #[track_caller]
6970 fn assert_rewrap(
6971 unwrapped_text: &str,
6972 wrapped_text: &str,
6973 language: Arc<Language>,
6974 cx: &mut EditorTestContext,
6975 ) {
6976 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6977 cx.set_state(unwrapped_text);
6978 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6979 cx.assert_editor_state(wrapped_text);
6980 }
6981}
6982
6983#[gpui::test]
6984async fn test_hard_wrap(cx: &mut TestAppContext) {
6985 init_test(cx, |_| {});
6986 let mut cx = EditorTestContext::new(cx).await;
6987
6988 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6989 cx.update_editor(|editor, _, cx| {
6990 editor.set_hard_wrap(Some(14), cx);
6991 });
6992
6993 cx.set_state(indoc!(
6994 "
6995 one two three ˇ
6996 "
6997 ));
6998 cx.simulate_input("four");
6999 cx.run_until_parked();
7000
7001 cx.assert_editor_state(indoc!(
7002 "
7003 one two three
7004 fourˇ
7005 "
7006 ));
7007
7008 cx.update_editor(|editor, window, cx| {
7009 editor.newline(&Default::default(), window, cx);
7010 });
7011 cx.run_until_parked();
7012 cx.assert_editor_state(indoc!(
7013 "
7014 one two three
7015 four
7016 ˇ
7017 "
7018 ));
7019
7020 cx.simulate_input("five");
7021 cx.run_until_parked();
7022 cx.assert_editor_state(indoc!(
7023 "
7024 one two three
7025 four
7026 fiveˇ
7027 "
7028 ));
7029
7030 cx.update_editor(|editor, window, cx| {
7031 editor.newline(&Default::default(), window, cx);
7032 });
7033 cx.run_until_parked();
7034 cx.simulate_input("# ");
7035 cx.run_until_parked();
7036 cx.assert_editor_state(indoc!(
7037 "
7038 one two three
7039 four
7040 five
7041 # ˇ
7042 "
7043 ));
7044
7045 cx.update_editor(|editor, window, cx| {
7046 editor.newline(&Default::default(), window, cx);
7047 });
7048 cx.run_until_parked();
7049 cx.assert_editor_state(indoc!(
7050 "
7051 one two three
7052 four
7053 five
7054 #\x20
7055 #ˇ
7056 "
7057 ));
7058
7059 cx.simulate_input(" 6");
7060 cx.run_until_parked();
7061 cx.assert_editor_state(indoc!(
7062 "
7063 one two three
7064 four
7065 five
7066 #
7067 # 6ˇ
7068 "
7069 ));
7070}
7071
7072#[gpui::test]
7073async fn test_cut_line_ends(cx: &mut TestAppContext) {
7074 init_test(cx, |_| {});
7075
7076 let mut cx = EditorTestContext::new(cx).await;
7077
7078 cx.set_state(indoc! {"The quick brownˇ"});
7079 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7080 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7081
7082 cx.set_state(indoc! {"The emacs foxˇ"});
7083 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7084 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7085
7086 cx.set_state(indoc! {"
7087 The quick« brownˇ»
7088 fox jumps overˇ
7089 the lazy dog"});
7090 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7091 cx.assert_editor_state(indoc! {"
7092 The quickˇ
7093 ˇthe lazy dog"});
7094
7095 cx.set_state(indoc! {"
7096 The quick« brownˇ»
7097 fox jumps overˇ
7098 the lazy dog"});
7099 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7100 cx.assert_editor_state(indoc! {"
7101 The quickˇ
7102 fox jumps overˇthe lazy dog"});
7103
7104 cx.set_state(indoc! {"
7105 The quick« brownˇ»
7106 fox jumps overˇ
7107 the lazy dog"});
7108 cx.update_editor(|e, window, cx| {
7109 e.cut_to_end_of_line(
7110 &CutToEndOfLine {
7111 stop_at_newlines: true,
7112 },
7113 window,
7114 cx,
7115 )
7116 });
7117 cx.assert_editor_state(indoc! {"
7118 The quickˇ
7119 fox jumps overˇ
7120 the lazy dog"});
7121
7122 cx.set_state(indoc! {"
7123 The quick« brownˇ»
7124 fox jumps overˇ
7125 the lazy dog"});
7126 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7127 cx.assert_editor_state(indoc! {"
7128 The quickˇ
7129 fox jumps overˇthe lazy dog"});
7130}
7131
7132#[gpui::test]
7133async fn test_clipboard(cx: &mut TestAppContext) {
7134 init_test(cx, |_| {});
7135
7136 let mut cx = EditorTestContext::new(cx).await;
7137
7138 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7139 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7140 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7141
7142 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7143 cx.set_state("two ˇfour ˇsix ˇ");
7144 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7145 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7146
7147 // Paste again but with only two cursors. Since the number of cursors doesn't
7148 // match the number of slices in the clipboard, the entire clipboard text
7149 // is pasted at each cursor.
7150 cx.set_state("ˇtwo one✅ four three six five ˇ");
7151 cx.update_editor(|e, window, cx| {
7152 e.handle_input("( ", window, cx);
7153 e.paste(&Paste, window, cx);
7154 e.handle_input(") ", window, cx);
7155 });
7156 cx.assert_editor_state(
7157 &([
7158 "( one✅ ",
7159 "three ",
7160 "five ) ˇtwo one✅ four three six five ( one✅ ",
7161 "three ",
7162 "five ) ˇ",
7163 ]
7164 .join("\n")),
7165 );
7166
7167 // Cut with three selections, one of which is full-line.
7168 cx.set_state(indoc! {"
7169 1«2ˇ»3
7170 4ˇ567
7171 «8ˇ»9"});
7172 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7173 cx.assert_editor_state(indoc! {"
7174 1ˇ3
7175 ˇ9"});
7176
7177 // Paste with three selections, noticing how the copied selection that was full-line
7178 // gets inserted before the second cursor.
7179 cx.set_state(indoc! {"
7180 1ˇ3
7181 9ˇ
7182 «oˇ»ne"});
7183 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7184 cx.assert_editor_state(indoc! {"
7185 12ˇ3
7186 4567
7187 9ˇ
7188 8ˇne"});
7189
7190 // Copy with a single cursor only, which writes the whole line into the clipboard.
7191 cx.set_state(indoc! {"
7192 The quick brown
7193 fox juˇmps over
7194 the lazy dog"});
7195 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7196 assert_eq!(
7197 cx.read_from_clipboard()
7198 .and_then(|item| item.text().as_deref().map(str::to_string)),
7199 Some("fox jumps over\n".to_string())
7200 );
7201
7202 // Paste with three selections, noticing how the copied full-line selection is inserted
7203 // before the empty selections but replaces the selection that is non-empty.
7204 cx.set_state(indoc! {"
7205 Tˇhe quick brown
7206 «foˇ»x jumps over
7207 tˇhe lazy dog"});
7208 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7209 cx.assert_editor_state(indoc! {"
7210 fox jumps over
7211 Tˇhe quick brown
7212 fox jumps over
7213 ˇx jumps over
7214 fox jumps over
7215 tˇhe lazy dog"});
7216}
7217
7218#[gpui::test]
7219async fn test_copy_trim(cx: &mut TestAppContext) {
7220 init_test(cx, |_| {});
7221
7222 let mut cx = EditorTestContext::new(cx).await;
7223 cx.set_state(
7224 r#" «for selection in selections.iter() {
7225 let mut start = selection.start;
7226 let mut end = selection.end;
7227 let is_entire_line = selection.is_empty();
7228 if is_entire_line {
7229 start = Point::new(start.row, 0);ˇ»
7230 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7231 }
7232 "#,
7233 );
7234 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7235 assert_eq!(
7236 cx.read_from_clipboard()
7237 .and_then(|item| item.text().as_deref().map(str::to_string)),
7238 Some(
7239 "for selection in selections.iter() {
7240 let mut start = selection.start;
7241 let mut end = selection.end;
7242 let is_entire_line = selection.is_empty();
7243 if is_entire_line {
7244 start = Point::new(start.row, 0);"
7245 .to_string()
7246 ),
7247 "Regular copying preserves all indentation selected",
7248 );
7249 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7250 assert_eq!(
7251 cx.read_from_clipboard()
7252 .and_then(|item| item.text().as_deref().map(str::to_string)),
7253 Some(
7254 "for selection in selections.iter() {
7255let mut start = selection.start;
7256let mut end = selection.end;
7257let is_entire_line = selection.is_empty();
7258if is_entire_line {
7259 start = Point::new(start.row, 0);"
7260 .to_string()
7261 ),
7262 "Copying with stripping should strip all leading whitespaces"
7263 );
7264
7265 cx.set_state(
7266 r#" « for selection in selections.iter() {
7267 let mut start = selection.start;
7268 let mut end = selection.end;
7269 let is_entire_line = selection.is_empty();
7270 if is_entire_line {
7271 start = Point::new(start.row, 0);ˇ»
7272 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7273 }
7274 "#,
7275 );
7276 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7277 assert_eq!(
7278 cx.read_from_clipboard()
7279 .and_then(|item| item.text().as_deref().map(str::to_string)),
7280 Some(
7281 " for selection in selections.iter() {
7282 let mut start = selection.start;
7283 let mut end = selection.end;
7284 let is_entire_line = selection.is_empty();
7285 if is_entire_line {
7286 start = Point::new(start.row, 0);"
7287 .to_string()
7288 ),
7289 "Regular copying preserves all indentation selected",
7290 );
7291 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7292 assert_eq!(
7293 cx.read_from_clipboard()
7294 .and_then(|item| item.text().as_deref().map(str::to_string)),
7295 Some(
7296 "for selection in selections.iter() {
7297let mut start = selection.start;
7298let mut end = selection.end;
7299let is_entire_line = selection.is_empty();
7300if is_entire_line {
7301 start = Point::new(start.row, 0);"
7302 .to_string()
7303 ),
7304 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7305 );
7306
7307 cx.set_state(
7308 r#" «ˇ for selection in selections.iter() {
7309 let mut start = selection.start;
7310 let mut end = selection.end;
7311 let is_entire_line = selection.is_empty();
7312 if is_entire_line {
7313 start = Point::new(start.row, 0);»
7314 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7315 }
7316 "#,
7317 );
7318 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7319 assert_eq!(
7320 cx.read_from_clipboard()
7321 .and_then(|item| item.text().as_deref().map(str::to_string)),
7322 Some(
7323 " for selection in selections.iter() {
7324 let mut start = selection.start;
7325 let mut end = selection.end;
7326 let is_entire_line = selection.is_empty();
7327 if is_entire_line {
7328 start = Point::new(start.row, 0);"
7329 .to_string()
7330 ),
7331 "Regular copying for reverse selection works the same",
7332 );
7333 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7334 assert_eq!(
7335 cx.read_from_clipboard()
7336 .and_then(|item| item.text().as_deref().map(str::to_string)),
7337 Some(
7338 "for selection in selections.iter() {
7339let mut start = selection.start;
7340let mut end = selection.end;
7341let is_entire_line = selection.is_empty();
7342if is_entire_line {
7343 start = Point::new(start.row, 0);"
7344 .to_string()
7345 ),
7346 "Copying with stripping for reverse selection works the same"
7347 );
7348
7349 cx.set_state(
7350 r#" for selection «in selections.iter() {
7351 let mut start = selection.start;
7352 let mut end = selection.end;
7353 let is_entire_line = selection.is_empty();
7354 if is_entire_line {
7355 start = Point::new(start.row, 0);ˇ»
7356 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7357 }
7358 "#,
7359 );
7360 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7361 assert_eq!(
7362 cx.read_from_clipboard()
7363 .and_then(|item| item.text().as_deref().map(str::to_string)),
7364 Some(
7365 "in selections.iter() {
7366 let mut start = selection.start;
7367 let mut end = selection.end;
7368 let is_entire_line = selection.is_empty();
7369 if is_entire_line {
7370 start = Point::new(start.row, 0);"
7371 .to_string()
7372 ),
7373 "When selecting past the indent, the copying works as usual",
7374 );
7375 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7376 assert_eq!(
7377 cx.read_from_clipboard()
7378 .and_then(|item| item.text().as_deref().map(str::to_string)),
7379 Some(
7380 "in selections.iter() {
7381 let mut start = selection.start;
7382 let mut end = selection.end;
7383 let is_entire_line = selection.is_empty();
7384 if is_entire_line {
7385 start = Point::new(start.row, 0);"
7386 .to_string()
7387 ),
7388 "When selecting past the indent, nothing is trimmed"
7389 );
7390
7391 cx.set_state(
7392 r#" «for selection in selections.iter() {
7393 let mut start = selection.start;
7394
7395 let mut end = selection.end;
7396 let is_entire_line = selection.is_empty();
7397 if is_entire_line {
7398 start = Point::new(start.row, 0);
7399ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7400 }
7401 "#,
7402 );
7403 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7404 assert_eq!(
7405 cx.read_from_clipboard()
7406 .and_then(|item| item.text().as_deref().map(str::to_string)),
7407 Some(
7408 "for selection in selections.iter() {
7409let mut start = selection.start;
7410
7411let mut end = selection.end;
7412let is_entire_line = selection.is_empty();
7413if is_entire_line {
7414 start = Point::new(start.row, 0);
7415"
7416 .to_string()
7417 ),
7418 "Copying with stripping should ignore empty lines"
7419 );
7420}
7421
7422#[gpui::test]
7423async fn test_paste_multiline(cx: &mut TestAppContext) {
7424 init_test(cx, |_| {});
7425
7426 let mut cx = EditorTestContext::new(cx).await;
7427 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7428
7429 // Cut an indented block, without the leading whitespace.
7430 cx.set_state(indoc! {"
7431 const a: B = (
7432 c(),
7433 «d(
7434 e,
7435 f
7436 )ˇ»
7437 );
7438 "});
7439 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7440 cx.assert_editor_state(indoc! {"
7441 const a: B = (
7442 c(),
7443 ˇ
7444 );
7445 "});
7446
7447 // Paste it at the same position.
7448 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7449 cx.assert_editor_state(indoc! {"
7450 const a: B = (
7451 c(),
7452 d(
7453 e,
7454 f
7455 )ˇ
7456 );
7457 "});
7458
7459 // Paste it at a line with a lower indent level.
7460 cx.set_state(indoc! {"
7461 ˇ
7462 const a: B = (
7463 c(),
7464 );
7465 "});
7466 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7467 cx.assert_editor_state(indoc! {"
7468 d(
7469 e,
7470 f
7471 )ˇ
7472 const a: B = (
7473 c(),
7474 );
7475 "});
7476
7477 // Cut an indented block, with the leading whitespace.
7478 cx.set_state(indoc! {"
7479 const a: B = (
7480 c(),
7481 « d(
7482 e,
7483 f
7484 )
7485 ˇ»);
7486 "});
7487 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7488 cx.assert_editor_state(indoc! {"
7489 const a: B = (
7490 c(),
7491 ˇ);
7492 "});
7493
7494 // Paste it at the same position.
7495 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7496 cx.assert_editor_state(indoc! {"
7497 const a: B = (
7498 c(),
7499 d(
7500 e,
7501 f
7502 )
7503 ˇ);
7504 "});
7505
7506 // Paste it at a line with a higher indent level.
7507 cx.set_state(indoc! {"
7508 const a: B = (
7509 c(),
7510 d(
7511 e,
7512 fˇ
7513 )
7514 );
7515 "});
7516 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7517 cx.assert_editor_state(indoc! {"
7518 const a: B = (
7519 c(),
7520 d(
7521 e,
7522 f d(
7523 e,
7524 f
7525 )
7526 ˇ
7527 )
7528 );
7529 "});
7530
7531 // Copy an indented block, starting mid-line
7532 cx.set_state(indoc! {"
7533 const a: B = (
7534 c(),
7535 somethin«g(
7536 e,
7537 f
7538 )ˇ»
7539 );
7540 "});
7541 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7542
7543 // Paste it on a line with a lower indent level
7544 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7545 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7546 cx.assert_editor_state(indoc! {"
7547 const a: B = (
7548 c(),
7549 something(
7550 e,
7551 f
7552 )
7553 );
7554 g(
7555 e,
7556 f
7557 )ˇ"});
7558}
7559
7560#[gpui::test]
7561async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7562 init_test(cx, |_| {});
7563
7564 cx.write_to_clipboard(ClipboardItem::new_string(
7565 " d(\n e\n );\n".into(),
7566 ));
7567
7568 let mut cx = EditorTestContext::new(cx).await;
7569 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7570
7571 cx.set_state(indoc! {"
7572 fn a() {
7573 b();
7574 if c() {
7575 ˇ
7576 }
7577 }
7578 "});
7579
7580 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7581 cx.assert_editor_state(indoc! {"
7582 fn a() {
7583 b();
7584 if c() {
7585 d(
7586 e
7587 );
7588 ˇ
7589 }
7590 }
7591 "});
7592
7593 cx.set_state(indoc! {"
7594 fn a() {
7595 b();
7596 ˇ
7597 }
7598 "});
7599
7600 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7601 cx.assert_editor_state(indoc! {"
7602 fn a() {
7603 b();
7604 d(
7605 e
7606 );
7607 ˇ
7608 }
7609 "});
7610}
7611
7612#[gpui::test]
7613fn test_select_all(cx: &mut TestAppContext) {
7614 init_test(cx, |_| {});
7615
7616 let editor = cx.add_window(|window, cx| {
7617 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7618 build_editor(buffer, window, cx)
7619 });
7620 _ = editor.update(cx, |editor, window, cx| {
7621 editor.select_all(&SelectAll, window, cx);
7622 assert_eq!(
7623 display_ranges(editor, cx),
7624 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7625 );
7626 });
7627}
7628
7629#[gpui::test]
7630fn test_select_line(cx: &mut TestAppContext) {
7631 init_test(cx, |_| {});
7632
7633 let editor = cx.add_window(|window, cx| {
7634 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7635 build_editor(buffer, window, cx)
7636 });
7637 _ = editor.update(cx, |editor, window, cx| {
7638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7639 s.select_display_ranges([
7640 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7641 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7642 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7643 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7644 ])
7645 });
7646 editor.select_line(&SelectLine, window, cx);
7647 assert_eq!(
7648 display_ranges(editor, cx),
7649 vec![
7650 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7651 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7652 ]
7653 );
7654 });
7655
7656 _ = editor.update(cx, |editor, window, cx| {
7657 editor.select_line(&SelectLine, window, cx);
7658 assert_eq!(
7659 display_ranges(editor, cx),
7660 vec![
7661 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7662 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7663 ]
7664 );
7665 });
7666
7667 _ = editor.update(cx, |editor, window, cx| {
7668 editor.select_line(&SelectLine, window, cx);
7669 assert_eq!(
7670 display_ranges(editor, cx),
7671 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7672 );
7673 });
7674}
7675
7676#[gpui::test]
7677async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7678 init_test(cx, |_| {});
7679 let mut cx = EditorTestContext::new(cx).await;
7680
7681 #[track_caller]
7682 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7683 cx.set_state(initial_state);
7684 cx.update_editor(|e, window, cx| {
7685 e.split_selection_into_lines(&Default::default(), window, cx)
7686 });
7687 cx.assert_editor_state(expected_state);
7688 }
7689
7690 // Selection starts and ends at the middle of lines, left-to-right
7691 test(
7692 &mut cx,
7693 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7694 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7695 );
7696 // Same thing, right-to-left
7697 test(
7698 &mut cx,
7699 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7700 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7701 );
7702
7703 // Whole buffer, left-to-right, last line *doesn't* end with newline
7704 test(
7705 &mut cx,
7706 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7707 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7708 );
7709 // Same thing, right-to-left
7710 test(
7711 &mut cx,
7712 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7713 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7714 );
7715
7716 // Whole buffer, left-to-right, last line ends with newline
7717 test(
7718 &mut cx,
7719 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7720 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7721 );
7722 // Same thing, right-to-left
7723 test(
7724 &mut cx,
7725 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7726 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7727 );
7728
7729 // Starts at the end of a line, ends at the start of another
7730 test(
7731 &mut cx,
7732 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7733 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7734 );
7735}
7736
7737#[gpui::test]
7738async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7739 init_test(cx, |_| {});
7740
7741 let editor = cx.add_window(|window, cx| {
7742 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7743 build_editor(buffer, window, cx)
7744 });
7745
7746 // setup
7747 _ = editor.update(cx, |editor, window, cx| {
7748 editor.fold_creases(
7749 vec![
7750 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7751 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7752 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7753 ],
7754 true,
7755 window,
7756 cx,
7757 );
7758 assert_eq!(
7759 editor.display_text(cx),
7760 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7761 );
7762 });
7763
7764 _ = editor.update(cx, |editor, window, cx| {
7765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7766 s.select_display_ranges([
7767 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7768 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7769 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7770 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7771 ])
7772 });
7773 editor.split_selection_into_lines(&Default::default(), window, cx);
7774 assert_eq!(
7775 editor.display_text(cx),
7776 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7777 );
7778 });
7779 EditorTestContext::for_editor(editor, cx)
7780 .await
7781 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7782
7783 _ = editor.update(cx, |editor, window, cx| {
7784 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7785 s.select_display_ranges([
7786 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7787 ])
7788 });
7789 editor.split_selection_into_lines(&Default::default(), window, cx);
7790 assert_eq!(
7791 editor.display_text(cx),
7792 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7793 );
7794 assert_eq!(
7795 display_ranges(editor, cx),
7796 [
7797 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7798 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7799 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7800 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7801 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7802 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7803 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7804 ]
7805 );
7806 });
7807 EditorTestContext::for_editor(editor, cx)
7808 .await
7809 .assert_editor_state(
7810 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7811 );
7812}
7813
7814#[gpui::test]
7815async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7816 init_test(cx, |_| {});
7817
7818 let mut cx = EditorTestContext::new(cx).await;
7819
7820 cx.set_state(indoc!(
7821 r#"abc
7822 defˇghi
7823
7824 jk
7825 nlmo
7826 "#
7827 ));
7828
7829 cx.update_editor(|editor, window, cx| {
7830 editor.add_selection_above(&Default::default(), window, cx);
7831 });
7832
7833 cx.assert_editor_state(indoc!(
7834 r#"abcˇ
7835 defˇghi
7836
7837 jk
7838 nlmo
7839 "#
7840 ));
7841
7842 cx.update_editor(|editor, window, cx| {
7843 editor.add_selection_above(&Default::default(), window, cx);
7844 });
7845
7846 cx.assert_editor_state(indoc!(
7847 r#"abcˇ
7848 defˇghi
7849
7850 jk
7851 nlmo
7852 "#
7853 ));
7854
7855 cx.update_editor(|editor, window, cx| {
7856 editor.add_selection_below(&Default::default(), window, cx);
7857 });
7858
7859 cx.assert_editor_state(indoc!(
7860 r#"abc
7861 defˇghi
7862
7863 jk
7864 nlmo
7865 "#
7866 ));
7867
7868 cx.update_editor(|editor, window, cx| {
7869 editor.undo_selection(&Default::default(), window, cx);
7870 });
7871
7872 cx.assert_editor_state(indoc!(
7873 r#"abcˇ
7874 defˇghi
7875
7876 jk
7877 nlmo
7878 "#
7879 ));
7880
7881 cx.update_editor(|editor, window, cx| {
7882 editor.redo_selection(&Default::default(), window, cx);
7883 });
7884
7885 cx.assert_editor_state(indoc!(
7886 r#"abc
7887 defˇghi
7888
7889 jk
7890 nlmo
7891 "#
7892 ));
7893
7894 cx.update_editor(|editor, window, cx| {
7895 editor.add_selection_below(&Default::default(), window, cx);
7896 });
7897
7898 cx.assert_editor_state(indoc!(
7899 r#"abc
7900 defˇghi
7901 ˇ
7902 jk
7903 nlmo
7904 "#
7905 ));
7906
7907 cx.update_editor(|editor, window, cx| {
7908 editor.add_selection_below(&Default::default(), window, cx);
7909 });
7910
7911 cx.assert_editor_state(indoc!(
7912 r#"abc
7913 defˇghi
7914 ˇ
7915 jkˇ
7916 nlmo
7917 "#
7918 ));
7919
7920 cx.update_editor(|editor, window, cx| {
7921 editor.add_selection_below(&Default::default(), window, cx);
7922 });
7923
7924 cx.assert_editor_state(indoc!(
7925 r#"abc
7926 defˇghi
7927 ˇ
7928 jkˇ
7929 nlmˇo
7930 "#
7931 ));
7932
7933 cx.update_editor(|editor, window, cx| {
7934 editor.add_selection_below(&Default::default(), window, cx);
7935 });
7936
7937 cx.assert_editor_state(indoc!(
7938 r#"abc
7939 defˇghi
7940 ˇ
7941 jkˇ
7942 nlmˇo
7943 ˇ"#
7944 ));
7945
7946 // change selections
7947 cx.set_state(indoc!(
7948 r#"abc
7949 def«ˇg»hi
7950
7951 jk
7952 nlmo
7953 "#
7954 ));
7955
7956 cx.update_editor(|editor, window, cx| {
7957 editor.add_selection_below(&Default::default(), window, cx);
7958 });
7959
7960 cx.assert_editor_state(indoc!(
7961 r#"abc
7962 def«ˇg»hi
7963
7964 jk
7965 nlm«ˇo»
7966 "#
7967 ));
7968
7969 cx.update_editor(|editor, window, cx| {
7970 editor.add_selection_below(&Default::default(), window, cx);
7971 });
7972
7973 cx.assert_editor_state(indoc!(
7974 r#"abc
7975 def«ˇg»hi
7976
7977 jk
7978 nlm«ˇo»
7979 "#
7980 ));
7981
7982 cx.update_editor(|editor, window, cx| {
7983 editor.add_selection_above(&Default::default(), window, cx);
7984 });
7985
7986 cx.assert_editor_state(indoc!(
7987 r#"abc
7988 def«ˇg»hi
7989
7990 jk
7991 nlmo
7992 "#
7993 ));
7994
7995 cx.update_editor(|editor, window, cx| {
7996 editor.add_selection_above(&Default::default(), window, cx);
7997 });
7998
7999 cx.assert_editor_state(indoc!(
8000 r#"abc
8001 def«ˇg»hi
8002
8003 jk
8004 nlmo
8005 "#
8006 ));
8007
8008 // Change selections again
8009 cx.set_state(indoc!(
8010 r#"a«bc
8011 defgˇ»hi
8012
8013 jk
8014 nlmo
8015 "#
8016 ));
8017
8018 cx.update_editor(|editor, window, cx| {
8019 editor.add_selection_below(&Default::default(), window, cx);
8020 });
8021
8022 cx.assert_editor_state(indoc!(
8023 r#"a«bcˇ»
8024 d«efgˇ»hi
8025
8026 j«kˇ»
8027 nlmo
8028 "#
8029 ));
8030
8031 cx.update_editor(|editor, window, cx| {
8032 editor.add_selection_below(&Default::default(), window, cx);
8033 });
8034 cx.assert_editor_state(indoc!(
8035 r#"a«bcˇ»
8036 d«efgˇ»hi
8037
8038 j«kˇ»
8039 n«lmoˇ»
8040 "#
8041 ));
8042 cx.update_editor(|editor, window, cx| {
8043 editor.add_selection_above(&Default::default(), window, cx);
8044 });
8045
8046 cx.assert_editor_state(indoc!(
8047 r#"a«bcˇ»
8048 d«efgˇ»hi
8049
8050 j«kˇ»
8051 nlmo
8052 "#
8053 ));
8054
8055 // Change selections again
8056 cx.set_state(indoc!(
8057 r#"abc
8058 d«ˇefghi
8059
8060 jk
8061 nlm»o
8062 "#
8063 ));
8064
8065 cx.update_editor(|editor, window, cx| {
8066 editor.add_selection_above(&Default::default(), window, cx);
8067 });
8068
8069 cx.assert_editor_state(indoc!(
8070 r#"a«ˇbc»
8071 d«ˇef»ghi
8072
8073 j«ˇk»
8074 n«ˇlm»o
8075 "#
8076 ));
8077
8078 cx.update_editor(|editor, window, cx| {
8079 editor.add_selection_below(&Default::default(), window, cx);
8080 });
8081
8082 cx.assert_editor_state(indoc!(
8083 r#"abc
8084 d«ˇef»ghi
8085
8086 j«ˇk»
8087 n«ˇlm»o
8088 "#
8089 ));
8090}
8091
8092#[gpui::test]
8093async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8094 init_test(cx, |_| {});
8095 let mut cx = EditorTestContext::new(cx).await;
8096
8097 cx.set_state(indoc!(
8098 r#"line onˇe
8099 liˇne two
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.add_selection_below(&Default::default(), window, cx);
8106 });
8107
8108 // test multiple cursors expand in the same direction
8109 cx.assert_editor_state(indoc!(
8110 r#"line onˇe
8111 liˇne twˇo
8112 liˇne three
8113 line four"#
8114 ));
8115
8116 cx.update_editor(|editor, window, cx| {
8117 editor.add_selection_below(&Default::default(), window, cx);
8118 });
8119
8120 cx.update_editor(|editor, window, cx| {
8121 editor.add_selection_below(&Default::default(), window, cx);
8122 });
8123
8124 // test multiple cursors expand below overflow
8125 cx.assert_editor_state(indoc!(
8126 r#"line onˇe
8127 liˇne twˇo
8128 liˇne thˇree
8129 liˇne foˇur"#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_above(&Default::default(), window, cx);
8134 });
8135
8136 // test multiple cursors retrieves back correctly
8137 cx.assert_editor_state(indoc!(
8138 r#"line onˇe
8139 liˇne twˇo
8140 liˇne thˇree
8141 line four"#
8142 ));
8143
8144 cx.update_editor(|editor, window, cx| {
8145 editor.add_selection_above(&Default::default(), window, cx);
8146 });
8147
8148 cx.update_editor(|editor, window, cx| {
8149 editor.add_selection_above(&Default::default(), window, cx);
8150 });
8151
8152 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8153 cx.assert_editor_state(indoc!(
8154 r#"liˇne onˇe
8155 liˇne two
8156 line three
8157 line four"#
8158 ));
8159
8160 cx.update_editor(|editor, window, cx| {
8161 editor.undo_selection(&Default::default(), window, cx);
8162 });
8163
8164 // test undo
8165 cx.assert_editor_state(indoc!(
8166 r#"line onˇe
8167 liˇne twˇo
8168 line three
8169 line four"#
8170 ));
8171
8172 cx.update_editor(|editor, window, cx| {
8173 editor.redo_selection(&Default::default(), window, cx);
8174 });
8175
8176 // test redo
8177 cx.assert_editor_state(indoc!(
8178 r#"liˇne onˇe
8179 liˇne two
8180 line three
8181 line four"#
8182 ));
8183
8184 cx.set_state(indoc!(
8185 r#"abcd
8186 ef«ghˇ»
8187 ijkl
8188 «mˇ»nop"#
8189 ));
8190
8191 cx.update_editor(|editor, window, cx| {
8192 editor.add_selection_above(&Default::default(), window, cx);
8193 });
8194
8195 // test multiple selections expand in the same direction
8196 cx.assert_editor_state(indoc!(
8197 r#"ab«cdˇ»
8198 ef«ghˇ»
8199 «iˇ»jkl
8200 «mˇ»nop"#
8201 ));
8202
8203 cx.update_editor(|editor, window, cx| {
8204 editor.add_selection_above(&Default::default(), window, cx);
8205 });
8206
8207 // test multiple selection upward overflow
8208 cx.assert_editor_state(indoc!(
8209 r#"ab«cdˇ»
8210 «eˇ»f«ghˇ»
8211 «iˇ»jkl
8212 «mˇ»nop"#
8213 ));
8214
8215 cx.update_editor(|editor, window, cx| {
8216 editor.add_selection_below(&Default::default(), window, cx);
8217 });
8218
8219 // test multiple selection retrieves back correctly
8220 cx.assert_editor_state(indoc!(
8221 r#"abcd
8222 ef«ghˇ»
8223 «iˇ»jkl
8224 «mˇ»nop"#
8225 ));
8226
8227 cx.update_editor(|editor, window, cx| {
8228 editor.add_selection_below(&Default::default(), window, cx);
8229 });
8230
8231 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8232 cx.assert_editor_state(indoc!(
8233 r#"abcd
8234 ef«ghˇ»
8235 ij«klˇ»
8236 «mˇ»nop"#
8237 ));
8238
8239 cx.update_editor(|editor, window, cx| {
8240 editor.undo_selection(&Default::default(), window, cx);
8241 });
8242
8243 // test undo
8244 cx.assert_editor_state(indoc!(
8245 r#"abcd
8246 ef«ghˇ»
8247 «iˇ»jkl
8248 «mˇ»nop"#
8249 ));
8250
8251 cx.update_editor(|editor, window, cx| {
8252 editor.redo_selection(&Default::default(), window, cx);
8253 });
8254
8255 // test redo
8256 cx.assert_editor_state(indoc!(
8257 r#"abcd
8258 ef«ghˇ»
8259 ij«klˇ»
8260 «mˇ»nop"#
8261 ));
8262}
8263
8264#[gpui::test]
8265async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8266 init_test(cx, |_| {});
8267 let mut cx = EditorTestContext::new(cx).await;
8268
8269 cx.set_state(indoc!(
8270 r#"line onˇe
8271 liˇne two
8272 line three
8273 line four"#
8274 ));
8275
8276 cx.update_editor(|editor, window, cx| {
8277 editor.add_selection_below(&Default::default(), window, cx);
8278 editor.add_selection_below(&Default::default(), window, cx);
8279 editor.add_selection_below(&Default::default(), window, cx);
8280 });
8281
8282 // initial state with two multi cursor groups
8283 cx.assert_editor_state(indoc!(
8284 r#"line onˇe
8285 liˇne twˇo
8286 liˇne thˇree
8287 liˇne foˇur"#
8288 ));
8289
8290 // add single cursor in middle - simulate opt click
8291 cx.update_editor(|editor, window, cx| {
8292 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8293 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8294 editor.end_selection(window, cx);
8295 });
8296
8297 cx.assert_editor_state(indoc!(
8298 r#"line onˇe
8299 liˇne twˇo
8300 liˇneˇ thˇree
8301 liˇne foˇur"#
8302 ));
8303
8304 cx.update_editor(|editor, window, cx| {
8305 editor.add_selection_above(&Default::default(), window, cx);
8306 });
8307
8308 // test new added selection expands above and existing selection shrinks
8309 cx.assert_editor_state(indoc!(
8310 r#"line onˇe
8311 liˇneˇ twˇo
8312 liˇneˇ thˇree
8313 line four"#
8314 ));
8315
8316 cx.update_editor(|editor, window, cx| {
8317 editor.add_selection_above(&Default::default(), window, cx);
8318 });
8319
8320 // test new added selection expands above and existing selection shrinks
8321 cx.assert_editor_state(indoc!(
8322 r#"lineˇ onˇe
8323 liˇneˇ twˇo
8324 lineˇ three
8325 line four"#
8326 ));
8327
8328 // intial state with two selection groups
8329 cx.set_state(indoc!(
8330 r#"abcd
8331 ef«ghˇ»
8332 ijkl
8333 «mˇ»nop"#
8334 ));
8335
8336 cx.update_editor(|editor, window, cx| {
8337 editor.add_selection_above(&Default::default(), window, cx);
8338 editor.add_selection_above(&Default::default(), window, cx);
8339 });
8340
8341 cx.assert_editor_state(indoc!(
8342 r#"ab«cdˇ»
8343 «eˇ»f«ghˇ»
8344 «iˇ»jkl
8345 «mˇ»nop"#
8346 ));
8347
8348 // add single selection in middle - simulate opt drag
8349 cx.update_editor(|editor, window, cx| {
8350 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8351 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8352 editor.update_selection(
8353 DisplayPoint::new(DisplayRow(2), 4),
8354 0,
8355 gpui::Point::<f32>::default(),
8356 window,
8357 cx,
8358 );
8359 editor.end_selection(window, cx);
8360 });
8361
8362 cx.assert_editor_state(indoc!(
8363 r#"ab«cdˇ»
8364 «eˇ»f«ghˇ»
8365 «iˇ»jk«lˇ»
8366 «mˇ»nop"#
8367 ));
8368
8369 cx.update_editor(|editor, window, cx| {
8370 editor.add_selection_below(&Default::default(), window, cx);
8371 });
8372
8373 // test new added selection expands below, others shrinks from above
8374 cx.assert_editor_state(indoc!(
8375 r#"abcd
8376 ef«ghˇ»
8377 «iˇ»jk«lˇ»
8378 «mˇ»no«pˇ»"#
8379 ));
8380}
8381
8382#[gpui::test]
8383async fn test_select_next(cx: &mut TestAppContext) {
8384 init_test(cx, |_| {});
8385 let mut cx = EditorTestContext::new(cx).await;
8386
8387 // Enable case sensitive search.
8388 update_test_editor_settings(&mut cx, |settings| {
8389 let mut search_settings = SearchSettingsContent::default();
8390 search_settings.case_sensitive = Some(true);
8391 settings.search = Some(search_settings);
8392 });
8393
8394 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8395
8396 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8397 .unwrap();
8398 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8399
8400 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8401 .unwrap();
8402 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8403
8404 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8405 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8406
8407 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8408 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8409
8410 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8411 .unwrap();
8412 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8413
8414 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8415 .unwrap();
8416 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8417
8418 // Test selection direction should be preserved
8419 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8420
8421 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8422 .unwrap();
8423 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8424
8425 // Test case sensitivity
8426 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8427 cx.update_editor(|e, window, cx| {
8428 e.select_next(&SelectNext::default(), window, cx).unwrap();
8429 });
8430 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8431
8432 // Disable case sensitive search.
8433 update_test_editor_settings(&mut cx, |settings| {
8434 let mut search_settings = SearchSettingsContent::default();
8435 search_settings.case_sensitive = Some(false);
8436 settings.search = Some(search_settings);
8437 });
8438
8439 cx.set_state("«ˇfoo»\nFOO\nFoo");
8440 cx.update_editor(|e, window, cx| {
8441 e.select_next(&SelectNext::default(), window, cx).unwrap();
8442 e.select_next(&SelectNext::default(), window, cx).unwrap();
8443 });
8444 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8445}
8446
8447#[gpui::test]
8448async fn test_select_all_matches(cx: &mut TestAppContext) {
8449 init_test(cx, |_| {});
8450 let mut cx = EditorTestContext::new(cx).await;
8451
8452 // Enable case sensitive search.
8453 update_test_editor_settings(&mut cx, |settings| {
8454 let mut search_settings = SearchSettingsContent::default();
8455 search_settings.case_sensitive = Some(true);
8456 settings.search = Some(search_settings);
8457 });
8458
8459 // Test caret-only selections
8460 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8461 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8462 .unwrap();
8463 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8464
8465 // Test left-to-right selections
8466 cx.set_state("abc\n«abcˇ»\nabc");
8467 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8468 .unwrap();
8469 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8470
8471 // Test right-to-left selections
8472 cx.set_state("abc\n«ˇabc»\nabc");
8473 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8474 .unwrap();
8475 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8476
8477 // Test selecting whitespace with caret selection
8478 cx.set_state("abc\nˇ abc\nabc");
8479 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8480 .unwrap();
8481 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8482
8483 // Test selecting whitespace with left-to-right selection
8484 cx.set_state("abc\n«ˇ »abc\nabc");
8485 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8486 .unwrap();
8487 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8488
8489 // Test no matches with right-to-left selection
8490 cx.set_state("abc\n« ˇ»abc\nabc");
8491 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8492 .unwrap();
8493 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8494
8495 // Test with a single word and clip_at_line_ends=true (#29823)
8496 cx.set_state("aˇbc");
8497 cx.update_editor(|e, window, cx| {
8498 e.set_clip_at_line_ends(true, cx);
8499 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8500 e.set_clip_at_line_ends(false, cx);
8501 });
8502 cx.assert_editor_state("«abcˇ»");
8503
8504 // Test case sensitivity
8505 cx.set_state("fˇoo\nFOO\nFoo");
8506 cx.update_editor(|e, window, cx| {
8507 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8508 });
8509 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8510
8511 // Disable case sensitive search.
8512 update_test_editor_settings(&mut cx, |settings| {
8513 let mut search_settings = SearchSettingsContent::default();
8514 search_settings.case_sensitive = Some(false);
8515 settings.search = Some(search_settings);
8516 });
8517
8518 cx.set_state("fˇoo\nFOO\nFoo");
8519 cx.update_editor(|e, window, cx| {
8520 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8521 });
8522 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8523}
8524
8525#[gpui::test]
8526async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8527 init_test(cx, |_| {});
8528
8529 let mut cx = EditorTestContext::new(cx).await;
8530
8531 let large_body_1 = "\nd".repeat(200);
8532 let large_body_2 = "\ne".repeat(200);
8533
8534 cx.set_state(&format!(
8535 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8536 ));
8537 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8538 let scroll_position = editor.scroll_position(cx);
8539 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8540 scroll_position
8541 });
8542
8543 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8544 .unwrap();
8545 cx.assert_editor_state(&format!(
8546 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8547 ));
8548 let scroll_position_after_selection =
8549 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8550 assert_eq!(
8551 initial_scroll_position, scroll_position_after_selection,
8552 "Scroll position should not change after selecting all matches"
8553 );
8554}
8555
8556#[gpui::test]
8557async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8558 init_test(cx, |_| {});
8559
8560 let mut cx = EditorLspTestContext::new_rust(
8561 lsp::ServerCapabilities {
8562 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8563 ..Default::default()
8564 },
8565 cx,
8566 )
8567 .await;
8568
8569 cx.set_state(indoc! {"
8570 line 1
8571 line 2
8572 linˇe 3
8573 line 4
8574 line 5
8575 "});
8576
8577 // Make an edit
8578 cx.update_editor(|editor, window, cx| {
8579 editor.handle_input("X", window, cx);
8580 });
8581
8582 // Move cursor to a different position
8583 cx.update_editor(|editor, window, cx| {
8584 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8585 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8586 });
8587 });
8588
8589 cx.assert_editor_state(indoc! {"
8590 line 1
8591 line 2
8592 linXe 3
8593 line 4
8594 liˇne 5
8595 "});
8596
8597 cx.lsp
8598 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8599 Ok(Some(vec![lsp::TextEdit::new(
8600 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8601 "PREFIX ".to_string(),
8602 )]))
8603 });
8604
8605 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8606 .unwrap()
8607 .await
8608 .unwrap();
8609
8610 cx.assert_editor_state(indoc! {"
8611 PREFIX line 1
8612 line 2
8613 linXe 3
8614 line 4
8615 liˇne 5
8616 "});
8617
8618 // Undo formatting
8619 cx.update_editor(|editor, window, cx| {
8620 editor.undo(&Default::default(), window, cx);
8621 });
8622
8623 // Verify cursor moved back to position after edit
8624 cx.assert_editor_state(indoc! {"
8625 line 1
8626 line 2
8627 linXˇe 3
8628 line 4
8629 line 5
8630 "});
8631}
8632
8633#[gpui::test]
8634async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8635 init_test(cx, |_| {});
8636
8637 let mut cx = EditorTestContext::new(cx).await;
8638
8639 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8640 cx.update_editor(|editor, window, cx| {
8641 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8642 });
8643
8644 cx.set_state(indoc! {"
8645 line 1
8646 line 2
8647 linˇe 3
8648 line 4
8649 line 5
8650 line 6
8651 line 7
8652 line 8
8653 line 9
8654 line 10
8655 "});
8656
8657 let snapshot = cx.buffer_snapshot();
8658 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8659
8660 cx.update(|_, cx| {
8661 provider.update(cx, |provider, _| {
8662 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8663 id: None,
8664 edits: vec![(edit_position..edit_position, "X".into())],
8665 edit_preview: None,
8666 }))
8667 })
8668 });
8669
8670 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8671 cx.update_editor(|editor, window, cx| {
8672 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8673 });
8674
8675 cx.assert_editor_state(indoc! {"
8676 line 1
8677 line 2
8678 lineXˇ 3
8679 line 4
8680 line 5
8681 line 6
8682 line 7
8683 line 8
8684 line 9
8685 line 10
8686 "});
8687
8688 cx.update_editor(|editor, window, cx| {
8689 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8690 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8691 });
8692 });
8693
8694 cx.assert_editor_state(indoc! {"
8695 line 1
8696 line 2
8697 lineX 3
8698 line 4
8699 line 5
8700 line 6
8701 line 7
8702 line 8
8703 line 9
8704 liˇne 10
8705 "});
8706
8707 cx.update_editor(|editor, window, cx| {
8708 editor.undo(&Default::default(), window, cx);
8709 });
8710
8711 cx.assert_editor_state(indoc! {"
8712 line 1
8713 line 2
8714 lineˇ 3
8715 line 4
8716 line 5
8717 line 6
8718 line 7
8719 line 8
8720 line 9
8721 line 10
8722 "});
8723}
8724
8725#[gpui::test]
8726async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8727 init_test(cx, |_| {});
8728
8729 let mut cx = EditorTestContext::new(cx).await;
8730 cx.set_state(
8731 r#"let foo = 2;
8732lˇet foo = 2;
8733let fooˇ = 2;
8734let foo = 2;
8735let foo = ˇ2;"#,
8736 );
8737
8738 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8739 .unwrap();
8740 cx.assert_editor_state(
8741 r#"let foo = 2;
8742«letˇ» foo = 2;
8743let «fooˇ» = 2;
8744let foo = 2;
8745let foo = «2ˇ»;"#,
8746 );
8747
8748 // noop for multiple selections with different contents
8749 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8750 .unwrap();
8751 cx.assert_editor_state(
8752 r#"let foo = 2;
8753«letˇ» foo = 2;
8754let «fooˇ» = 2;
8755let foo = 2;
8756let foo = «2ˇ»;"#,
8757 );
8758
8759 // Test last selection direction should be preserved
8760 cx.set_state(
8761 r#"let foo = 2;
8762let foo = 2;
8763let «fooˇ» = 2;
8764let «ˇfoo» = 2;
8765let foo = 2;"#,
8766 );
8767
8768 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8769 .unwrap();
8770 cx.assert_editor_state(
8771 r#"let foo = 2;
8772let foo = 2;
8773let «fooˇ» = 2;
8774let «ˇfoo» = 2;
8775let «ˇfoo» = 2;"#,
8776 );
8777}
8778
8779#[gpui::test]
8780async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8781 init_test(cx, |_| {});
8782
8783 let mut cx =
8784 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8785
8786 cx.assert_editor_state(indoc! {"
8787 ˇbbb
8788 ccc
8789
8790 bbb
8791 ccc
8792 "});
8793 cx.dispatch_action(SelectPrevious::default());
8794 cx.assert_editor_state(indoc! {"
8795 «bbbˇ»
8796 ccc
8797
8798 bbb
8799 ccc
8800 "});
8801 cx.dispatch_action(SelectPrevious::default());
8802 cx.assert_editor_state(indoc! {"
8803 «bbbˇ»
8804 ccc
8805
8806 «bbbˇ»
8807 ccc
8808 "});
8809}
8810
8811#[gpui::test]
8812async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8813 init_test(cx, |_| {});
8814
8815 let mut cx = EditorTestContext::new(cx).await;
8816 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8817
8818 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8819 .unwrap();
8820 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8821
8822 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8823 .unwrap();
8824 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8825
8826 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8827 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8828
8829 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8830 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8831
8832 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8833 .unwrap();
8834 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8835
8836 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8837 .unwrap();
8838 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8839}
8840
8841#[gpui::test]
8842async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8843 init_test(cx, |_| {});
8844
8845 let mut cx = EditorTestContext::new(cx).await;
8846 cx.set_state("aˇ");
8847
8848 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8849 .unwrap();
8850 cx.assert_editor_state("«aˇ»");
8851 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8852 .unwrap();
8853 cx.assert_editor_state("«aˇ»");
8854}
8855
8856#[gpui::test]
8857async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8858 init_test(cx, |_| {});
8859
8860 let mut cx = EditorTestContext::new(cx).await;
8861 cx.set_state(
8862 r#"let foo = 2;
8863lˇet foo = 2;
8864let fooˇ = 2;
8865let foo = 2;
8866let foo = ˇ2;"#,
8867 );
8868
8869 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8870 .unwrap();
8871 cx.assert_editor_state(
8872 r#"let foo = 2;
8873«letˇ» foo = 2;
8874let «fooˇ» = 2;
8875let foo = 2;
8876let foo = «2ˇ»;"#,
8877 );
8878
8879 // noop for multiple selections with different contents
8880 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8881 .unwrap();
8882 cx.assert_editor_state(
8883 r#"let foo = 2;
8884«letˇ» foo = 2;
8885let «fooˇ» = 2;
8886let foo = 2;
8887let foo = «2ˇ»;"#,
8888 );
8889}
8890
8891#[gpui::test]
8892async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8893 init_test(cx, |_| {});
8894 let mut cx = EditorTestContext::new(cx).await;
8895
8896 // Enable case sensitive search.
8897 update_test_editor_settings(&mut cx, |settings| {
8898 let mut search_settings = SearchSettingsContent::default();
8899 search_settings.case_sensitive = Some(true);
8900 settings.search = Some(search_settings);
8901 });
8902
8903 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8904
8905 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8906 .unwrap();
8907 // selection direction is preserved
8908 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8909
8910 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8911 .unwrap();
8912 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8913
8914 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8915 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8916
8917 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8918 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8919
8920 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8921 .unwrap();
8922 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8923
8924 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8925 .unwrap();
8926 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8927
8928 // Test case sensitivity
8929 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
8930 cx.update_editor(|e, window, cx| {
8931 e.select_previous(&SelectPrevious::default(), window, cx)
8932 .unwrap();
8933 e.select_previous(&SelectPrevious::default(), window, cx)
8934 .unwrap();
8935 });
8936 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8937
8938 // Disable case sensitive search.
8939 update_test_editor_settings(&mut cx, |settings| {
8940 let mut search_settings = SearchSettingsContent::default();
8941 search_settings.case_sensitive = Some(false);
8942 settings.search = Some(search_settings);
8943 });
8944
8945 cx.set_state("foo\nFOO\n«ˇFoo»");
8946 cx.update_editor(|e, window, cx| {
8947 e.select_previous(&SelectPrevious::default(), window, cx)
8948 .unwrap();
8949 e.select_previous(&SelectPrevious::default(), window, cx)
8950 .unwrap();
8951 });
8952 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8953}
8954
8955#[gpui::test]
8956async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8957 init_test(cx, |_| {});
8958
8959 let language = Arc::new(Language::new(
8960 LanguageConfig::default(),
8961 Some(tree_sitter_rust::LANGUAGE.into()),
8962 ));
8963
8964 let text = r#"
8965 use mod1::mod2::{mod3, mod4};
8966
8967 fn fn_1(param1: bool, param2: &str) {
8968 let var1 = "text";
8969 }
8970 "#
8971 .unindent();
8972
8973 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8974 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8975 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8976
8977 editor
8978 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8979 .await;
8980
8981 editor.update_in(cx, |editor, window, cx| {
8982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8983 s.select_display_ranges([
8984 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8985 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8986 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8987 ]);
8988 });
8989 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8990 });
8991 editor.update(cx, |editor, cx| {
8992 assert_text_with_selections(
8993 editor,
8994 indoc! {r#"
8995 use mod1::mod2::{mod3, «mod4ˇ»};
8996
8997 fn fn_1«ˇ(param1: bool, param2: &str)» {
8998 let var1 = "«ˇtext»";
8999 }
9000 "#},
9001 cx,
9002 );
9003 });
9004
9005 editor.update_in(cx, |editor, window, cx| {
9006 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9007 });
9008 editor.update(cx, |editor, cx| {
9009 assert_text_with_selections(
9010 editor,
9011 indoc! {r#"
9012 use mod1::mod2::«{mod3, mod4}ˇ»;
9013
9014 «ˇfn fn_1(param1: bool, param2: &str) {
9015 let var1 = "text";
9016 }»
9017 "#},
9018 cx,
9019 );
9020 });
9021
9022 editor.update_in(cx, |editor, window, cx| {
9023 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9024 });
9025 assert_eq!(
9026 editor.update(cx, |editor, cx| editor
9027 .selections
9028 .display_ranges(&editor.display_snapshot(cx))),
9029 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9030 );
9031
9032 // Trying to expand the selected syntax node one more time has no effect.
9033 editor.update_in(cx, |editor, window, cx| {
9034 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9035 });
9036 assert_eq!(
9037 editor.update(cx, |editor, cx| editor
9038 .selections
9039 .display_ranges(&editor.display_snapshot(cx))),
9040 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9041 );
9042
9043 editor.update_in(cx, |editor, window, cx| {
9044 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9045 });
9046 editor.update(cx, |editor, cx| {
9047 assert_text_with_selections(
9048 editor,
9049 indoc! {r#"
9050 use mod1::mod2::«{mod3, mod4}ˇ»;
9051
9052 «ˇfn fn_1(param1: bool, param2: &str) {
9053 let var1 = "text";
9054 }»
9055 "#},
9056 cx,
9057 );
9058 });
9059
9060 editor.update_in(cx, |editor, window, cx| {
9061 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9062 });
9063 editor.update(cx, |editor, cx| {
9064 assert_text_with_selections(
9065 editor,
9066 indoc! {r#"
9067 use mod1::mod2::{mod3, «mod4ˇ»};
9068
9069 fn fn_1«ˇ(param1: bool, param2: &str)» {
9070 let var1 = "«ˇtext»";
9071 }
9072 "#},
9073 cx,
9074 );
9075 });
9076
9077 editor.update_in(cx, |editor, window, cx| {
9078 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9079 });
9080 editor.update(cx, |editor, cx| {
9081 assert_text_with_selections(
9082 editor,
9083 indoc! {r#"
9084 use mod1::mod2::{mod3, moˇd4};
9085
9086 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9087 let var1 = "teˇxt";
9088 }
9089 "#},
9090 cx,
9091 );
9092 });
9093
9094 // Trying to shrink the selected syntax node one more time has no effect.
9095 editor.update_in(cx, |editor, window, cx| {
9096 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9097 });
9098 editor.update_in(cx, |editor, _, cx| {
9099 assert_text_with_selections(
9100 editor,
9101 indoc! {r#"
9102 use mod1::mod2::{mod3, moˇd4};
9103
9104 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9105 let var1 = "teˇxt";
9106 }
9107 "#},
9108 cx,
9109 );
9110 });
9111
9112 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9113 // a fold.
9114 editor.update_in(cx, |editor, window, cx| {
9115 editor.fold_creases(
9116 vec![
9117 Crease::simple(
9118 Point::new(0, 21)..Point::new(0, 24),
9119 FoldPlaceholder::test(),
9120 ),
9121 Crease::simple(
9122 Point::new(3, 20)..Point::new(3, 22),
9123 FoldPlaceholder::test(),
9124 ),
9125 ],
9126 true,
9127 window,
9128 cx,
9129 );
9130 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9131 });
9132 editor.update(cx, |editor, cx| {
9133 assert_text_with_selections(
9134 editor,
9135 indoc! {r#"
9136 use mod1::mod2::«{mod3, mod4}ˇ»;
9137
9138 fn fn_1«ˇ(param1: bool, param2: &str)» {
9139 let var1 = "«ˇtext»";
9140 }
9141 "#},
9142 cx,
9143 );
9144 });
9145}
9146
9147#[gpui::test]
9148async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9149 init_test(cx, |_| {});
9150
9151 let language = Arc::new(Language::new(
9152 LanguageConfig::default(),
9153 Some(tree_sitter_rust::LANGUAGE.into()),
9154 ));
9155
9156 let text = "let a = 2;";
9157
9158 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9159 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9160 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9161
9162 editor
9163 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9164 .await;
9165
9166 // Test case 1: Cursor at end of word
9167 editor.update_in(cx, |editor, window, cx| {
9168 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9169 s.select_display_ranges([
9170 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9171 ]);
9172 });
9173 });
9174 editor.update(cx, |editor, cx| {
9175 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9176 });
9177 editor.update_in(cx, |editor, window, cx| {
9178 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9179 });
9180 editor.update(cx, |editor, cx| {
9181 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9182 });
9183 editor.update_in(cx, |editor, window, cx| {
9184 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9185 });
9186 editor.update(cx, |editor, cx| {
9187 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9188 });
9189
9190 // Test case 2: Cursor at end of statement
9191 editor.update_in(cx, |editor, window, cx| {
9192 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9193 s.select_display_ranges([
9194 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9195 ]);
9196 });
9197 });
9198 editor.update(cx, |editor, cx| {
9199 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9200 });
9201 editor.update_in(cx, |editor, window, cx| {
9202 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9203 });
9204 editor.update(cx, |editor, cx| {
9205 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9206 });
9207}
9208
9209#[gpui::test]
9210async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9211 init_test(cx, |_| {});
9212
9213 let language = Arc::new(Language::new(
9214 LanguageConfig {
9215 name: "JavaScript".into(),
9216 ..Default::default()
9217 },
9218 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9219 ));
9220
9221 let text = r#"
9222 let a = {
9223 key: "value",
9224 };
9225 "#
9226 .unindent();
9227
9228 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9229 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9230 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9231
9232 editor
9233 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9234 .await;
9235
9236 // Test case 1: Cursor after '{'
9237 editor.update_in(cx, |editor, window, cx| {
9238 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9239 s.select_display_ranges([
9240 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9241 ]);
9242 });
9243 });
9244 editor.update(cx, |editor, cx| {
9245 assert_text_with_selections(
9246 editor,
9247 indoc! {r#"
9248 let a = {ˇ
9249 key: "value",
9250 };
9251 "#},
9252 cx,
9253 );
9254 });
9255 editor.update_in(cx, |editor, window, cx| {
9256 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9257 });
9258 editor.update(cx, |editor, cx| {
9259 assert_text_with_selections(
9260 editor,
9261 indoc! {r#"
9262 let a = «ˇ{
9263 key: "value",
9264 }»;
9265 "#},
9266 cx,
9267 );
9268 });
9269
9270 // Test case 2: Cursor after ':'
9271 editor.update_in(cx, |editor, window, cx| {
9272 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9273 s.select_display_ranges([
9274 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9275 ]);
9276 });
9277 });
9278 editor.update(cx, |editor, cx| {
9279 assert_text_with_selections(
9280 editor,
9281 indoc! {r#"
9282 let a = {
9283 key:ˇ "value",
9284 };
9285 "#},
9286 cx,
9287 );
9288 });
9289 editor.update_in(cx, |editor, window, cx| {
9290 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9291 });
9292 editor.update(cx, |editor, cx| {
9293 assert_text_with_selections(
9294 editor,
9295 indoc! {r#"
9296 let a = {
9297 «ˇkey: "value"»,
9298 };
9299 "#},
9300 cx,
9301 );
9302 });
9303 editor.update_in(cx, |editor, window, cx| {
9304 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9305 });
9306 editor.update(cx, |editor, cx| {
9307 assert_text_with_selections(
9308 editor,
9309 indoc! {r#"
9310 let a = «ˇ{
9311 key: "value",
9312 }»;
9313 "#},
9314 cx,
9315 );
9316 });
9317
9318 // Test case 3: Cursor after ','
9319 editor.update_in(cx, |editor, window, cx| {
9320 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9321 s.select_display_ranges([
9322 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9323 ]);
9324 });
9325 });
9326 editor.update(cx, |editor, cx| {
9327 assert_text_with_selections(
9328 editor,
9329 indoc! {r#"
9330 let a = {
9331 key: "value",ˇ
9332 };
9333 "#},
9334 cx,
9335 );
9336 });
9337 editor.update_in(cx, |editor, window, cx| {
9338 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9339 });
9340 editor.update(cx, |editor, cx| {
9341 assert_text_with_selections(
9342 editor,
9343 indoc! {r#"
9344 let a = «ˇ{
9345 key: "value",
9346 }»;
9347 "#},
9348 cx,
9349 );
9350 });
9351
9352 // Test case 4: Cursor after ';'
9353 editor.update_in(cx, |editor, window, cx| {
9354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9355 s.select_display_ranges([
9356 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9357 ]);
9358 });
9359 });
9360 editor.update(cx, |editor, cx| {
9361 assert_text_with_selections(
9362 editor,
9363 indoc! {r#"
9364 let a = {
9365 key: "value",
9366 };ˇ
9367 "#},
9368 cx,
9369 );
9370 });
9371 editor.update_in(cx, |editor, window, cx| {
9372 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9373 });
9374 editor.update(cx, |editor, cx| {
9375 assert_text_with_selections(
9376 editor,
9377 indoc! {r#"
9378 «ˇlet a = {
9379 key: "value",
9380 };
9381 »"#},
9382 cx,
9383 );
9384 });
9385}
9386
9387#[gpui::test]
9388async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9389 init_test(cx, |_| {});
9390
9391 let language = Arc::new(Language::new(
9392 LanguageConfig::default(),
9393 Some(tree_sitter_rust::LANGUAGE.into()),
9394 ));
9395
9396 let text = r#"
9397 use mod1::mod2::{mod3, mod4};
9398
9399 fn fn_1(param1: bool, param2: &str) {
9400 let var1 = "hello world";
9401 }
9402 "#
9403 .unindent();
9404
9405 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9406 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9407 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9408
9409 editor
9410 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9411 .await;
9412
9413 // Test 1: Cursor on a letter of a string word
9414 editor.update_in(cx, |editor, window, cx| {
9415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9416 s.select_display_ranges([
9417 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9418 ]);
9419 });
9420 });
9421 editor.update_in(cx, |editor, window, cx| {
9422 assert_text_with_selections(
9423 editor,
9424 indoc! {r#"
9425 use mod1::mod2::{mod3, mod4};
9426
9427 fn fn_1(param1: bool, param2: &str) {
9428 let var1 = "hˇello world";
9429 }
9430 "#},
9431 cx,
9432 );
9433 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9434 assert_text_with_selections(
9435 editor,
9436 indoc! {r#"
9437 use mod1::mod2::{mod3, mod4};
9438
9439 fn fn_1(param1: bool, param2: &str) {
9440 let var1 = "«ˇhello» world";
9441 }
9442 "#},
9443 cx,
9444 );
9445 });
9446
9447 // Test 2: Partial selection within a word
9448 editor.update_in(cx, |editor, window, cx| {
9449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9450 s.select_display_ranges([
9451 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9452 ]);
9453 });
9454 });
9455 editor.update_in(cx, |editor, window, cx| {
9456 assert_text_with_selections(
9457 editor,
9458 indoc! {r#"
9459 use mod1::mod2::{mod3, mod4};
9460
9461 fn fn_1(param1: bool, param2: &str) {
9462 let var1 = "h«elˇ»lo world";
9463 }
9464 "#},
9465 cx,
9466 );
9467 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9468 assert_text_with_selections(
9469 editor,
9470 indoc! {r#"
9471 use mod1::mod2::{mod3, mod4};
9472
9473 fn fn_1(param1: bool, param2: &str) {
9474 let var1 = "«ˇhello» world";
9475 }
9476 "#},
9477 cx,
9478 );
9479 });
9480
9481 // Test 3: Complete word already selected
9482 editor.update_in(cx, |editor, window, cx| {
9483 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9484 s.select_display_ranges([
9485 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9486 ]);
9487 });
9488 });
9489 editor.update_in(cx, |editor, window, cx| {
9490 assert_text_with_selections(
9491 editor,
9492 indoc! {r#"
9493 use mod1::mod2::{mod3, mod4};
9494
9495 fn fn_1(param1: bool, param2: &str) {
9496 let var1 = "«helloˇ» world";
9497 }
9498 "#},
9499 cx,
9500 );
9501 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9502 assert_text_with_selections(
9503 editor,
9504 indoc! {r#"
9505 use mod1::mod2::{mod3, mod4};
9506
9507 fn fn_1(param1: bool, param2: &str) {
9508 let var1 = "«hello worldˇ»";
9509 }
9510 "#},
9511 cx,
9512 );
9513 });
9514
9515 // Test 4: Selection spanning across words
9516 editor.update_in(cx, |editor, window, cx| {
9517 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9518 s.select_display_ranges([
9519 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9520 ]);
9521 });
9522 });
9523 editor.update_in(cx, |editor, window, cx| {
9524 assert_text_with_selections(
9525 editor,
9526 indoc! {r#"
9527 use mod1::mod2::{mod3, mod4};
9528
9529 fn fn_1(param1: bool, param2: &str) {
9530 let var1 = "hel«lo woˇ»rld";
9531 }
9532 "#},
9533 cx,
9534 );
9535 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9536 assert_text_with_selections(
9537 editor,
9538 indoc! {r#"
9539 use mod1::mod2::{mod3, mod4};
9540
9541 fn fn_1(param1: bool, param2: &str) {
9542 let var1 = "«ˇhello world»";
9543 }
9544 "#},
9545 cx,
9546 );
9547 });
9548
9549 // Test 5: Expansion beyond string
9550 editor.update_in(cx, |editor, window, cx| {
9551 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9552 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9553 assert_text_with_selections(
9554 editor,
9555 indoc! {r#"
9556 use mod1::mod2::{mod3, mod4};
9557
9558 fn fn_1(param1: bool, param2: &str) {
9559 «ˇlet var1 = "hello world";»
9560 }
9561 "#},
9562 cx,
9563 );
9564 });
9565}
9566
9567#[gpui::test]
9568async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9569 init_test(cx, |_| {});
9570
9571 let mut cx = EditorTestContext::new(cx).await;
9572
9573 let language = Arc::new(Language::new(
9574 LanguageConfig::default(),
9575 Some(tree_sitter_rust::LANGUAGE.into()),
9576 ));
9577
9578 cx.update_buffer(|buffer, cx| {
9579 buffer.set_language(Some(language), cx);
9580 });
9581
9582 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9583 cx.update_editor(|editor, window, cx| {
9584 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9585 });
9586
9587 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9588
9589 cx.set_state(indoc! { r#"fn a() {
9590 // what
9591 // a
9592 // ˇlong
9593 // method
9594 // I
9595 // sure
9596 // hope
9597 // it
9598 // works
9599 }"# });
9600
9601 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9602 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9603 cx.update(|_, cx| {
9604 multi_buffer.update(cx, |multi_buffer, cx| {
9605 multi_buffer.set_excerpts_for_path(
9606 PathKey::for_buffer(&buffer, cx),
9607 buffer,
9608 [Point::new(1, 0)..Point::new(1, 0)],
9609 3,
9610 cx,
9611 );
9612 });
9613 });
9614
9615 let editor2 = cx.new_window_entity(|window, cx| {
9616 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9617 });
9618
9619 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9620 cx.update_editor(|editor, window, cx| {
9621 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9622 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9623 })
9624 });
9625
9626 cx.assert_editor_state(indoc! { "
9627 fn a() {
9628 // what
9629 // a
9630 ˇ // long
9631 // method"});
9632
9633 cx.update_editor(|editor, window, cx| {
9634 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9635 });
9636
9637 // Although we could potentially make the action work when the syntax node
9638 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9639 // did. Maybe we could also expand the excerpt to contain the range?
9640 cx.assert_editor_state(indoc! { "
9641 fn a() {
9642 // what
9643 // a
9644 ˇ // long
9645 // method"});
9646}
9647
9648#[gpui::test]
9649async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9650 init_test(cx, |_| {});
9651
9652 let base_text = r#"
9653 impl A {
9654 // this is an uncommitted comment
9655
9656 fn b() {
9657 c();
9658 }
9659
9660 // this is another uncommitted comment
9661
9662 fn d() {
9663 // e
9664 // f
9665 }
9666 }
9667
9668 fn g() {
9669 // h
9670 }
9671 "#
9672 .unindent();
9673
9674 let text = r#"
9675 ˇimpl A {
9676
9677 fn b() {
9678 c();
9679 }
9680
9681 fn d() {
9682 // e
9683 // f
9684 }
9685 }
9686
9687 fn g() {
9688 // h
9689 }
9690 "#
9691 .unindent();
9692
9693 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9694 cx.set_state(&text);
9695 cx.set_head_text(&base_text);
9696 cx.update_editor(|editor, window, cx| {
9697 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9698 });
9699
9700 cx.assert_state_with_diff(
9701 "
9702 ˇimpl A {
9703 - // this is an uncommitted comment
9704
9705 fn b() {
9706 c();
9707 }
9708
9709 - // this is another uncommitted comment
9710 -
9711 fn d() {
9712 // e
9713 // f
9714 }
9715 }
9716
9717 fn g() {
9718 // h
9719 }
9720 "
9721 .unindent(),
9722 );
9723
9724 let expected_display_text = "
9725 impl A {
9726 // this is an uncommitted comment
9727
9728 fn b() {
9729 ⋯
9730 }
9731
9732 // this is another uncommitted comment
9733
9734 fn d() {
9735 ⋯
9736 }
9737 }
9738
9739 fn g() {
9740 ⋯
9741 }
9742 "
9743 .unindent();
9744
9745 cx.update_editor(|editor, window, cx| {
9746 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9747 assert_eq!(editor.display_text(cx), expected_display_text);
9748 });
9749}
9750
9751#[gpui::test]
9752async fn test_autoindent(cx: &mut TestAppContext) {
9753 init_test(cx, |_| {});
9754
9755 let language = Arc::new(
9756 Language::new(
9757 LanguageConfig {
9758 brackets: BracketPairConfig {
9759 pairs: vec![
9760 BracketPair {
9761 start: "{".to_string(),
9762 end: "}".to_string(),
9763 close: false,
9764 surround: false,
9765 newline: true,
9766 },
9767 BracketPair {
9768 start: "(".to_string(),
9769 end: ")".to_string(),
9770 close: false,
9771 surround: false,
9772 newline: true,
9773 },
9774 ],
9775 ..Default::default()
9776 },
9777 ..Default::default()
9778 },
9779 Some(tree_sitter_rust::LANGUAGE.into()),
9780 )
9781 .with_indents_query(
9782 r#"
9783 (_ "(" ")" @end) @indent
9784 (_ "{" "}" @end) @indent
9785 "#,
9786 )
9787 .unwrap(),
9788 );
9789
9790 let text = "fn a() {}";
9791
9792 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9793 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9794 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9795 editor
9796 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9797 .await;
9798
9799 editor.update_in(cx, |editor, window, cx| {
9800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9801 s.select_ranges([
9802 MultiBufferOffset(5)..MultiBufferOffset(5),
9803 MultiBufferOffset(8)..MultiBufferOffset(8),
9804 MultiBufferOffset(9)..MultiBufferOffset(9),
9805 ])
9806 });
9807 editor.newline(&Newline, window, cx);
9808 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9809 assert_eq!(
9810 editor.selections.ranges(&editor.display_snapshot(cx)),
9811 &[
9812 Point::new(1, 4)..Point::new(1, 4),
9813 Point::new(3, 4)..Point::new(3, 4),
9814 Point::new(5, 0)..Point::new(5, 0)
9815 ]
9816 );
9817 });
9818}
9819
9820#[gpui::test]
9821async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9822 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9823
9824 let language = Arc::new(
9825 Language::new(
9826 LanguageConfig {
9827 brackets: BracketPairConfig {
9828 pairs: vec![
9829 BracketPair {
9830 start: "{".to_string(),
9831 end: "}".to_string(),
9832 close: false,
9833 surround: false,
9834 newline: true,
9835 },
9836 BracketPair {
9837 start: "(".to_string(),
9838 end: ")".to_string(),
9839 close: false,
9840 surround: false,
9841 newline: true,
9842 },
9843 ],
9844 ..Default::default()
9845 },
9846 ..Default::default()
9847 },
9848 Some(tree_sitter_rust::LANGUAGE.into()),
9849 )
9850 .with_indents_query(
9851 r#"
9852 (_ "(" ")" @end) @indent
9853 (_ "{" "}" @end) @indent
9854 "#,
9855 )
9856 .unwrap(),
9857 );
9858
9859 let text = "fn a() {}";
9860
9861 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9862 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9863 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9864 editor
9865 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9866 .await;
9867
9868 editor.update_in(cx, |editor, window, cx| {
9869 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9870 s.select_ranges([
9871 MultiBufferOffset(5)..MultiBufferOffset(5),
9872 MultiBufferOffset(8)..MultiBufferOffset(8),
9873 MultiBufferOffset(9)..MultiBufferOffset(9),
9874 ])
9875 });
9876 editor.newline(&Newline, window, cx);
9877 assert_eq!(
9878 editor.text(cx),
9879 indoc!(
9880 "
9881 fn a(
9882
9883 ) {
9884
9885 }
9886 "
9887 )
9888 );
9889 assert_eq!(
9890 editor.selections.ranges(&editor.display_snapshot(cx)),
9891 &[
9892 Point::new(1, 0)..Point::new(1, 0),
9893 Point::new(3, 0)..Point::new(3, 0),
9894 Point::new(5, 0)..Point::new(5, 0)
9895 ]
9896 );
9897 });
9898}
9899
9900#[gpui::test]
9901async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9902 init_test(cx, |settings| {
9903 settings.defaults.auto_indent = Some(true);
9904 settings.languages.0.insert(
9905 "python".into(),
9906 LanguageSettingsContent {
9907 auto_indent: Some(false),
9908 ..Default::default()
9909 },
9910 );
9911 });
9912
9913 let mut cx = EditorTestContext::new(cx).await;
9914
9915 let injected_language = Arc::new(
9916 Language::new(
9917 LanguageConfig {
9918 brackets: BracketPairConfig {
9919 pairs: vec![
9920 BracketPair {
9921 start: "{".to_string(),
9922 end: "}".to_string(),
9923 close: false,
9924 surround: false,
9925 newline: true,
9926 },
9927 BracketPair {
9928 start: "(".to_string(),
9929 end: ")".to_string(),
9930 close: true,
9931 surround: false,
9932 newline: true,
9933 },
9934 ],
9935 ..Default::default()
9936 },
9937 name: "python".into(),
9938 ..Default::default()
9939 },
9940 Some(tree_sitter_python::LANGUAGE.into()),
9941 )
9942 .with_indents_query(
9943 r#"
9944 (_ "(" ")" @end) @indent
9945 (_ "{" "}" @end) @indent
9946 "#,
9947 )
9948 .unwrap(),
9949 );
9950
9951 let language = Arc::new(
9952 Language::new(
9953 LanguageConfig {
9954 brackets: BracketPairConfig {
9955 pairs: vec![
9956 BracketPair {
9957 start: "{".to_string(),
9958 end: "}".to_string(),
9959 close: false,
9960 surround: false,
9961 newline: true,
9962 },
9963 BracketPair {
9964 start: "(".to_string(),
9965 end: ")".to_string(),
9966 close: true,
9967 surround: false,
9968 newline: true,
9969 },
9970 ],
9971 ..Default::default()
9972 },
9973 name: LanguageName::new("rust"),
9974 ..Default::default()
9975 },
9976 Some(tree_sitter_rust::LANGUAGE.into()),
9977 )
9978 .with_indents_query(
9979 r#"
9980 (_ "(" ")" @end) @indent
9981 (_ "{" "}" @end) @indent
9982 "#,
9983 )
9984 .unwrap()
9985 .with_injection_query(
9986 r#"
9987 (macro_invocation
9988 macro: (identifier) @_macro_name
9989 (token_tree) @injection.content
9990 (#set! injection.language "python"))
9991 "#,
9992 )
9993 .unwrap(),
9994 );
9995
9996 cx.language_registry().add(injected_language);
9997 cx.language_registry().add(language.clone());
9998
9999 cx.update_buffer(|buffer, cx| {
10000 buffer.set_language(Some(language), cx);
10001 });
10002
10003 cx.set_state(r#"struct A {ˇ}"#);
10004
10005 cx.update_editor(|editor, window, cx| {
10006 editor.newline(&Default::default(), window, cx);
10007 });
10008
10009 cx.assert_editor_state(indoc!(
10010 "struct A {
10011 ˇ
10012 }"
10013 ));
10014
10015 cx.set_state(r#"select_biased!(ˇ)"#);
10016
10017 cx.update_editor(|editor, window, cx| {
10018 editor.newline(&Default::default(), window, cx);
10019 editor.handle_input("def ", window, cx);
10020 editor.handle_input("(", window, cx);
10021 editor.newline(&Default::default(), window, cx);
10022 editor.handle_input("a", window, cx);
10023 });
10024
10025 cx.assert_editor_state(indoc!(
10026 "select_biased!(
10027 def (
10028 aˇ
10029 )
10030 )"
10031 ));
10032}
10033
10034#[gpui::test]
10035async fn test_autoindent_selections(cx: &mut TestAppContext) {
10036 init_test(cx, |_| {});
10037
10038 {
10039 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10040 cx.set_state(indoc! {"
10041 impl A {
10042
10043 fn b() {}
10044
10045 «fn c() {
10046
10047 }ˇ»
10048 }
10049 "});
10050
10051 cx.update_editor(|editor, window, cx| {
10052 editor.autoindent(&Default::default(), window, cx);
10053 });
10054
10055 cx.assert_editor_state(indoc! {"
10056 impl A {
10057
10058 fn b() {}
10059
10060 «fn c() {
10061
10062 }ˇ»
10063 }
10064 "});
10065 }
10066
10067 {
10068 let mut cx = EditorTestContext::new_multibuffer(
10069 cx,
10070 [indoc! { "
10071 impl A {
10072 «
10073 // a
10074 fn b(){}
10075 »
10076 «
10077 }
10078 fn c(){}
10079 »
10080 "}],
10081 );
10082
10083 let buffer = cx.update_editor(|editor, _, cx| {
10084 let buffer = editor.buffer().update(cx, |buffer, _| {
10085 buffer.all_buffers().iter().next().unwrap().clone()
10086 });
10087 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10088 buffer
10089 });
10090
10091 cx.run_until_parked();
10092 cx.update_editor(|editor, window, cx| {
10093 editor.select_all(&Default::default(), window, cx);
10094 editor.autoindent(&Default::default(), window, cx)
10095 });
10096 cx.run_until_parked();
10097
10098 cx.update(|_, cx| {
10099 assert_eq!(
10100 buffer.read(cx).text(),
10101 indoc! { "
10102 impl A {
10103
10104 // a
10105 fn b(){}
10106
10107
10108 }
10109 fn c(){}
10110
10111 " }
10112 )
10113 });
10114 }
10115}
10116
10117#[gpui::test]
10118async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10119 init_test(cx, |_| {});
10120
10121 let mut cx = EditorTestContext::new(cx).await;
10122
10123 let language = Arc::new(Language::new(
10124 LanguageConfig {
10125 brackets: BracketPairConfig {
10126 pairs: vec![
10127 BracketPair {
10128 start: "{".to_string(),
10129 end: "}".to_string(),
10130 close: true,
10131 surround: true,
10132 newline: true,
10133 },
10134 BracketPair {
10135 start: "(".to_string(),
10136 end: ")".to_string(),
10137 close: true,
10138 surround: true,
10139 newline: true,
10140 },
10141 BracketPair {
10142 start: "/*".to_string(),
10143 end: " */".to_string(),
10144 close: true,
10145 surround: true,
10146 newline: true,
10147 },
10148 BracketPair {
10149 start: "[".to_string(),
10150 end: "]".to_string(),
10151 close: false,
10152 surround: false,
10153 newline: true,
10154 },
10155 BracketPair {
10156 start: "\"".to_string(),
10157 end: "\"".to_string(),
10158 close: true,
10159 surround: true,
10160 newline: false,
10161 },
10162 BracketPair {
10163 start: "<".to_string(),
10164 end: ">".to_string(),
10165 close: false,
10166 surround: true,
10167 newline: true,
10168 },
10169 ],
10170 ..Default::default()
10171 },
10172 autoclose_before: "})]".to_string(),
10173 ..Default::default()
10174 },
10175 Some(tree_sitter_rust::LANGUAGE.into()),
10176 ));
10177
10178 cx.language_registry().add(language.clone());
10179 cx.update_buffer(|buffer, cx| {
10180 buffer.set_language(Some(language), cx);
10181 });
10182
10183 cx.set_state(
10184 &r#"
10185 🏀ˇ
10186 εˇ
10187 ❤️ˇ
10188 "#
10189 .unindent(),
10190 );
10191
10192 // autoclose multiple nested brackets at multiple cursors
10193 cx.update_editor(|editor, window, cx| {
10194 editor.handle_input("{", window, cx);
10195 editor.handle_input("{", window, cx);
10196 editor.handle_input("{", window, cx);
10197 });
10198 cx.assert_editor_state(
10199 &"
10200 🏀{{{ˇ}}}
10201 ε{{{ˇ}}}
10202 ❤️{{{ˇ}}}
10203 "
10204 .unindent(),
10205 );
10206
10207 // insert a different closing bracket
10208 cx.update_editor(|editor, window, cx| {
10209 editor.handle_input(")", window, cx);
10210 });
10211 cx.assert_editor_state(
10212 &"
10213 🏀{{{)ˇ}}}
10214 ε{{{)ˇ}}}
10215 ❤️{{{)ˇ}}}
10216 "
10217 .unindent(),
10218 );
10219
10220 // skip over the auto-closed brackets when typing a closing bracket
10221 cx.update_editor(|editor, window, cx| {
10222 editor.move_right(&MoveRight, window, cx);
10223 editor.handle_input("}", window, cx);
10224 editor.handle_input("}", window, cx);
10225 editor.handle_input("}", window, cx);
10226 });
10227 cx.assert_editor_state(
10228 &"
10229 🏀{{{)}}}}ˇ
10230 ε{{{)}}}}ˇ
10231 ❤️{{{)}}}}ˇ
10232 "
10233 .unindent(),
10234 );
10235
10236 // autoclose multi-character pairs
10237 cx.set_state(
10238 &"
10239 ˇ
10240 ˇ
10241 "
10242 .unindent(),
10243 );
10244 cx.update_editor(|editor, window, cx| {
10245 editor.handle_input("/", window, cx);
10246 editor.handle_input("*", window, cx);
10247 });
10248 cx.assert_editor_state(
10249 &"
10250 /*ˇ */
10251 /*ˇ */
10252 "
10253 .unindent(),
10254 );
10255
10256 // one cursor autocloses a multi-character pair, one cursor
10257 // does not autoclose.
10258 cx.set_state(
10259 &"
10260 /ˇ
10261 ˇ
10262 "
10263 .unindent(),
10264 );
10265 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10266 cx.assert_editor_state(
10267 &"
10268 /*ˇ */
10269 *ˇ
10270 "
10271 .unindent(),
10272 );
10273
10274 // Don't autoclose if the next character isn't whitespace and isn't
10275 // listed in the language's "autoclose_before" section.
10276 cx.set_state("ˇa b");
10277 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10278 cx.assert_editor_state("{ˇa b");
10279
10280 // Don't autoclose if `close` is false for the bracket pair
10281 cx.set_state("ˇ");
10282 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10283 cx.assert_editor_state("[ˇ");
10284
10285 // Surround with brackets if text is selected
10286 cx.set_state("«aˇ» b");
10287 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10288 cx.assert_editor_state("{«aˇ»} b");
10289
10290 // Autoclose when not immediately after a word character
10291 cx.set_state("a ˇ");
10292 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10293 cx.assert_editor_state("a \"ˇ\"");
10294
10295 // Autoclose pair where the start and end characters are the same
10296 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10297 cx.assert_editor_state("a \"\"ˇ");
10298
10299 // Don't autoclose when immediately after a word character
10300 cx.set_state("aˇ");
10301 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10302 cx.assert_editor_state("a\"ˇ");
10303
10304 // Do autoclose when after a non-word character
10305 cx.set_state("{ˇ");
10306 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10307 cx.assert_editor_state("{\"ˇ\"");
10308
10309 // Non identical pairs autoclose regardless of preceding character
10310 cx.set_state("aˇ");
10311 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10312 cx.assert_editor_state("a{ˇ}");
10313
10314 // Don't autoclose pair if autoclose is disabled
10315 cx.set_state("ˇ");
10316 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10317 cx.assert_editor_state("<ˇ");
10318
10319 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10320 cx.set_state("«aˇ» b");
10321 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10322 cx.assert_editor_state("<«aˇ»> b");
10323}
10324
10325#[gpui::test]
10326async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10327 init_test(cx, |settings| {
10328 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10329 });
10330
10331 let mut cx = EditorTestContext::new(cx).await;
10332
10333 let language = Arc::new(Language::new(
10334 LanguageConfig {
10335 brackets: BracketPairConfig {
10336 pairs: vec![
10337 BracketPair {
10338 start: "{".to_string(),
10339 end: "}".to_string(),
10340 close: true,
10341 surround: true,
10342 newline: true,
10343 },
10344 BracketPair {
10345 start: "(".to_string(),
10346 end: ")".to_string(),
10347 close: true,
10348 surround: true,
10349 newline: true,
10350 },
10351 BracketPair {
10352 start: "[".to_string(),
10353 end: "]".to_string(),
10354 close: false,
10355 surround: false,
10356 newline: true,
10357 },
10358 ],
10359 ..Default::default()
10360 },
10361 autoclose_before: "})]".to_string(),
10362 ..Default::default()
10363 },
10364 Some(tree_sitter_rust::LANGUAGE.into()),
10365 ));
10366
10367 cx.language_registry().add(language.clone());
10368 cx.update_buffer(|buffer, cx| {
10369 buffer.set_language(Some(language), cx);
10370 });
10371
10372 cx.set_state(
10373 &"
10374 ˇ
10375 ˇ
10376 ˇ
10377 "
10378 .unindent(),
10379 );
10380
10381 // ensure only matching closing brackets are skipped over
10382 cx.update_editor(|editor, window, cx| {
10383 editor.handle_input("}", window, cx);
10384 editor.move_left(&MoveLeft, window, cx);
10385 editor.handle_input(")", window, cx);
10386 editor.move_left(&MoveLeft, window, cx);
10387 });
10388 cx.assert_editor_state(
10389 &"
10390 ˇ)}
10391 ˇ)}
10392 ˇ)}
10393 "
10394 .unindent(),
10395 );
10396
10397 // skip-over closing brackets at multiple cursors
10398 cx.update_editor(|editor, window, cx| {
10399 editor.handle_input(")", window, cx);
10400 editor.handle_input("}", window, cx);
10401 });
10402 cx.assert_editor_state(
10403 &"
10404 )}ˇ
10405 )}ˇ
10406 )}ˇ
10407 "
10408 .unindent(),
10409 );
10410
10411 // ignore non-close brackets
10412 cx.update_editor(|editor, window, cx| {
10413 editor.handle_input("]", window, cx);
10414 editor.move_left(&MoveLeft, window, cx);
10415 editor.handle_input("]", window, cx);
10416 });
10417 cx.assert_editor_state(
10418 &"
10419 )}]ˇ]
10420 )}]ˇ]
10421 )}]ˇ]
10422 "
10423 .unindent(),
10424 );
10425}
10426
10427#[gpui::test]
10428async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10429 init_test(cx, |_| {});
10430
10431 let mut cx = EditorTestContext::new(cx).await;
10432
10433 let html_language = Arc::new(
10434 Language::new(
10435 LanguageConfig {
10436 name: "HTML".into(),
10437 brackets: BracketPairConfig {
10438 pairs: vec![
10439 BracketPair {
10440 start: "<".into(),
10441 end: ">".into(),
10442 close: true,
10443 ..Default::default()
10444 },
10445 BracketPair {
10446 start: "{".into(),
10447 end: "}".into(),
10448 close: true,
10449 ..Default::default()
10450 },
10451 BracketPair {
10452 start: "(".into(),
10453 end: ")".into(),
10454 close: true,
10455 ..Default::default()
10456 },
10457 ],
10458 ..Default::default()
10459 },
10460 autoclose_before: "})]>".into(),
10461 ..Default::default()
10462 },
10463 Some(tree_sitter_html::LANGUAGE.into()),
10464 )
10465 .with_injection_query(
10466 r#"
10467 (script_element
10468 (raw_text) @injection.content
10469 (#set! injection.language "javascript"))
10470 "#,
10471 )
10472 .unwrap(),
10473 );
10474
10475 let javascript_language = Arc::new(Language::new(
10476 LanguageConfig {
10477 name: "JavaScript".into(),
10478 brackets: BracketPairConfig {
10479 pairs: vec![
10480 BracketPair {
10481 start: "/*".into(),
10482 end: " */".into(),
10483 close: true,
10484 ..Default::default()
10485 },
10486 BracketPair {
10487 start: "{".into(),
10488 end: "}".into(),
10489 close: true,
10490 ..Default::default()
10491 },
10492 BracketPair {
10493 start: "(".into(),
10494 end: ")".into(),
10495 close: true,
10496 ..Default::default()
10497 },
10498 ],
10499 ..Default::default()
10500 },
10501 autoclose_before: "})]>".into(),
10502 ..Default::default()
10503 },
10504 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10505 ));
10506
10507 cx.language_registry().add(html_language.clone());
10508 cx.language_registry().add(javascript_language);
10509 cx.executor().run_until_parked();
10510
10511 cx.update_buffer(|buffer, cx| {
10512 buffer.set_language(Some(html_language), cx);
10513 });
10514
10515 cx.set_state(
10516 &r#"
10517 <body>ˇ
10518 <script>
10519 var x = 1;ˇ
10520 </script>
10521 </body>ˇ
10522 "#
10523 .unindent(),
10524 );
10525
10526 // Precondition: different languages are active at different locations.
10527 cx.update_editor(|editor, window, cx| {
10528 let snapshot = editor.snapshot(window, cx);
10529 let cursors = editor
10530 .selections
10531 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10532 let languages = cursors
10533 .iter()
10534 .map(|c| snapshot.language_at(c.start).unwrap().name())
10535 .collect::<Vec<_>>();
10536 assert_eq!(
10537 languages,
10538 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10539 );
10540 });
10541
10542 // Angle brackets autoclose in HTML, but not JavaScript.
10543 cx.update_editor(|editor, window, cx| {
10544 editor.handle_input("<", window, cx);
10545 editor.handle_input("a", window, cx);
10546 });
10547 cx.assert_editor_state(
10548 &r#"
10549 <body><aˇ>
10550 <script>
10551 var x = 1;<aˇ
10552 </script>
10553 </body><aˇ>
10554 "#
10555 .unindent(),
10556 );
10557
10558 // Curly braces and parens autoclose in both HTML and JavaScript.
10559 cx.update_editor(|editor, window, cx| {
10560 editor.handle_input(" b=", window, cx);
10561 editor.handle_input("{", window, cx);
10562 editor.handle_input("c", window, cx);
10563 editor.handle_input("(", window, cx);
10564 });
10565 cx.assert_editor_state(
10566 &r#"
10567 <body><a b={c(ˇ)}>
10568 <script>
10569 var x = 1;<a b={c(ˇ)}
10570 </script>
10571 </body><a b={c(ˇ)}>
10572 "#
10573 .unindent(),
10574 );
10575
10576 // Brackets that were already autoclosed are skipped.
10577 cx.update_editor(|editor, window, cx| {
10578 editor.handle_input(")", window, cx);
10579 editor.handle_input("d", window, cx);
10580 editor.handle_input("}", window, cx);
10581 });
10582 cx.assert_editor_state(
10583 &r#"
10584 <body><a b={c()d}ˇ>
10585 <script>
10586 var x = 1;<a b={c()d}ˇ
10587 </script>
10588 </body><a b={c()d}ˇ>
10589 "#
10590 .unindent(),
10591 );
10592 cx.update_editor(|editor, window, cx| {
10593 editor.handle_input(">", window, cx);
10594 });
10595 cx.assert_editor_state(
10596 &r#"
10597 <body><a b={c()d}>ˇ
10598 <script>
10599 var x = 1;<a b={c()d}>ˇ
10600 </script>
10601 </body><a b={c()d}>ˇ
10602 "#
10603 .unindent(),
10604 );
10605
10606 // Reset
10607 cx.set_state(
10608 &r#"
10609 <body>ˇ
10610 <script>
10611 var x = 1;ˇ
10612 </script>
10613 </body>ˇ
10614 "#
10615 .unindent(),
10616 );
10617
10618 cx.update_editor(|editor, window, cx| {
10619 editor.handle_input("<", window, cx);
10620 });
10621 cx.assert_editor_state(
10622 &r#"
10623 <body><ˇ>
10624 <script>
10625 var x = 1;<ˇ
10626 </script>
10627 </body><ˇ>
10628 "#
10629 .unindent(),
10630 );
10631
10632 // When backspacing, the closing angle brackets are removed.
10633 cx.update_editor(|editor, window, cx| {
10634 editor.backspace(&Backspace, window, cx);
10635 });
10636 cx.assert_editor_state(
10637 &r#"
10638 <body>ˇ
10639 <script>
10640 var x = 1;ˇ
10641 </script>
10642 </body>ˇ
10643 "#
10644 .unindent(),
10645 );
10646
10647 // Block comments autoclose in JavaScript, but not HTML.
10648 cx.update_editor(|editor, window, cx| {
10649 editor.handle_input("/", window, cx);
10650 editor.handle_input("*", window, cx);
10651 });
10652 cx.assert_editor_state(
10653 &r#"
10654 <body>/*ˇ
10655 <script>
10656 var x = 1;/*ˇ */
10657 </script>
10658 </body>/*ˇ
10659 "#
10660 .unindent(),
10661 );
10662}
10663
10664#[gpui::test]
10665async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10666 init_test(cx, |_| {});
10667
10668 let mut cx = EditorTestContext::new(cx).await;
10669
10670 let rust_language = Arc::new(
10671 Language::new(
10672 LanguageConfig {
10673 name: "Rust".into(),
10674 brackets: serde_json::from_value(json!([
10675 { "start": "{", "end": "}", "close": true, "newline": true },
10676 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10677 ]))
10678 .unwrap(),
10679 autoclose_before: "})]>".into(),
10680 ..Default::default()
10681 },
10682 Some(tree_sitter_rust::LANGUAGE.into()),
10683 )
10684 .with_override_query("(string_literal) @string")
10685 .unwrap(),
10686 );
10687
10688 cx.language_registry().add(rust_language.clone());
10689 cx.update_buffer(|buffer, cx| {
10690 buffer.set_language(Some(rust_language), cx);
10691 });
10692
10693 cx.set_state(
10694 &r#"
10695 let x = ˇ
10696 "#
10697 .unindent(),
10698 );
10699
10700 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10701 cx.update_editor(|editor, window, cx| {
10702 editor.handle_input("\"", window, cx);
10703 });
10704 cx.assert_editor_state(
10705 &r#"
10706 let x = "ˇ"
10707 "#
10708 .unindent(),
10709 );
10710
10711 // Inserting another quotation mark. The cursor moves across the existing
10712 // automatically-inserted quotation mark.
10713 cx.update_editor(|editor, window, cx| {
10714 editor.handle_input("\"", window, cx);
10715 });
10716 cx.assert_editor_state(
10717 &r#"
10718 let x = ""ˇ
10719 "#
10720 .unindent(),
10721 );
10722
10723 // Reset
10724 cx.set_state(
10725 &r#"
10726 let x = ˇ
10727 "#
10728 .unindent(),
10729 );
10730
10731 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10732 cx.update_editor(|editor, window, cx| {
10733 editor.handle_input("\"", window, cx);
10734 editor.handle_input(" ", window, cx);
10735 editor.move_left(&Default::default(), window, cx);
10736 editor.handle_input("\\", window, cx);
10737 editor.handle_input("\"", window, cx);
10738 });
10739 cx.assert_editor_state(
10740 &r#"
10741 let x = "\"ˇ "
10742 "#
10743 .unindent(),
10744 );
10745
10746 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10747 // mark. Nothing is inserted.
10748 cx.update_editor(|editor, window, cx| {
10749 editor.move_right(&Default::default(), window, cx);
10750 editor.handle_input("\"", window, cx);
10751 });
10752 cx.assert_editor_state(
10753 &r#"
10754 let x = "\" "ˇ
10755 "#
10756 .unindent(),
10757 );
10758}
10759
10760#[gpui::test]
10761async fn test_surround_with_pair(cx: &mut TestAppContext) {
10762 init_test(cx, |_| {});
10763
10764 let language = Arc::new(Language::new(
10765 LanguageConfig {
10766 brackets: BracketPairConfig {
10767 pairs: vec![
10768 BracketPair {
10769 start: "{".to_string(),
10770 end: "}".to_string(),
10771 close: true,
10772 surround: true,
10773 newline: true,
10774 },
10775 BracketPair {
10776 start: "/* ".to_string(),
10777 end: "*/".to_string(),
10778 close: true,
10779 surround: true,
10780 ..Default::default()
10781 },
10782 ],
10783 ..Default::default()
10784 },
10785 ..Default::default()
10786 },
10787 Some(tree_sitter_rust::LANGUAGE.into()),
10788 ));
10789
10790 let text = r#"
10791 a
10792 b
10793 c
10794 "#
10795 .unindent();
10796
10797 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10798 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10799 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10800 editor
10801 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10802 .await;
10803
10804 editor.update_in(cx, |editor, window, cx| {
10805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10806 s.select_display_ranges([
10807 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10808 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10809 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10810 ])
10811 });
10812
10813 editor.handle_input("{", window, cx);
10814 editor.handle_input("{", window, cx);
10815 editor.handle_input("{", window, cx);
10816 assert_eq!(
10817 editor.text(cx),
10818 "
10819 {{{a}}}
10820 {{{b}}}
10821 {{{c}}}
10822 "
10823 .unindent()
10824 );
10825 assert_eq!(
10826 display_ranges(editor, cx),
10827 [
10828 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10829 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10830 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10831 ]
10832 );
10833
10834 editor.undo(&Undo, window, cx);
10835 editor.undo(&Undo, window, cx);
10836 editor.undo(&Undo, window, cx);
10837 assert_eq!(
10838 editor.text(cx),
10839 "
10840 a
10841 b
10842 c
10843 "
10844 .unindent()
10845 );
10846 assert_eq!(
10847 display_ranges(editor, cx),
10848 [
10849 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10850 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10851 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10852 ]
10853 );
10854
10855 // Ensure inserting the first character of a multi-byte bracket pair
10856 // doesn't surround the selections with the bracket.
10857 editor.handle_input("/", window, cx);
10858 assert_eq!(
10859 editor.text(cx),
10860 "
10861 /
10862 /
10863 /
10864 "
10865 .unindent()
10866 );
10867 assert_eq!(
10868 display_ranges(editor, cx),
10869 [
10870 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10871 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10872 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10873 ]
10874 );
10875
10876 editor.undo(&Undo, window, cx);
10877 assert_eq!(
10878 editor.text(cx),
10879 "
10880 a
10881 b
10882 c
10883 "
10884 .unindent()
10885 );
10886 assert_eq!(
10887 display_ranges(editor, cx),
10888 [
10889 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10890 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10891 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10892 ]
10893 );
10894
10895 // Ensure inserting the last character of a multi-byte bracket pair
10896 // doesn't surround the selections with the bracket.
10897 editor.handle_input("*", window, cx);
10898 assert_eq!(
10899 editor.text(cx),
10900 "
10901 *
10902 *
10903 *
10904 "
10905 .unindent()
10906 );
10907 assert_eq!(
10908 display_ranges(editor, cx),
10909 [
10910 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10911 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10912 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10913 ]
10914 );
10915 });
10916}
10917
10918#[gpui::test]
10919async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10920 init_test(cx, |_| {});
10921
10922 let language = Arc::new(Language::new(
10923 LanguageConfig {
10924 brackets: BracketPairConfig {
10925 pairs: vec![BracketPair {
10926 start: "{".to_string(),
10927 end: "}".to_string(),
10928 close: true,
10929 surround: true,
10930 newline: true,
10931 }],
10932 ..Default::default()
10933 },
10934 autoclose_before: "}".to_string(),
10935 ..Default::default()
10936 },
10937 Some(tree_sitter_rust::LANGUAGE.into()),
10938 ));
10939
10940 let text = r#"
10941 a
10942 b
10943 c
10944 "#
10945 .unindent();
10946
10947 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10948 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10949 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10950 editor
10951 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10952 .await;
10953
10954 editor.update_in(cx, |editor, window, cx| {
10955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10956 s.select_ranges([
10957 Point::new(0, 1)..Point::new(0, 1),
10958 Point::new(1, 1)..Point::new(1, 1),
10959 Point::new(2, 1)..Point::new(2, 1),
10960 ])
10961 });
10962
10963 editor.handle_input("{", window, cx);
10964 editor.handle_input("{", window, cx);
10965 editor.handle_input("_", window, cx);
10966 assert_eq!(
10967 editor.text(cx),
10968 "
10969 a{{_}}
10970 b{{_}}
10971 c{{_}}
10972 "
10973 .unindent()
10974 );
10975 assert_eq!(
10976 editor
10977 .selections
10978 .ranges::<Point>(&editor.display_snapshot(cx)),
10979 [
10980 Point::new(0, 4)..Point::new(0, 4),
10981 Point::new(1, 4)..Point::new(1, 4),
10982 Point::new(2, 4)..Point::new(2, 4)
10983 ]
10984 );
10985
10986 editor.backspace(&Default::default(), window, cx);
10987 editor.backspace(&Default::default(), window, cx);
10988 assert_eq!(
10989 editor.text(cx),
10990 "
10991 a{}
10992 b{}
10993 c{}
10994 "
10995 .unindent()
10996 );
10997 assert_eq!(
10998 editor
10999 .selections
11000 .ranges::<Point>(&editor.display_snapshot(cx)),
11001 [
11002 Point::new(0, 2)..Point::new(0, 2),
11003 Point::new(1, 2)..Point::new(1, 2),
11004 Point::new(2, 2)..Point::new(2, 2)
11005 ]
11006 );
11007
11008 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11009 assert_eq!(
11010 editor.text(cx),
11011 "
11012 a
11013 b
11014 c
11015 "
11016 .unindent()
11017 );
11018 assert_eq!(
11019 editor
11020 .selections
11021 .ranges::<Point>(&editor.display_snapshot(cx)),
11022 [
11023 Point::new(0, 1)..Point::new(0, 1),
11024 Point::new(1, 1)..Point::new(1, 1),
11025 Point::new(2, 1)..Point::new(2, 1)
11026 ]
11027 );
11028 });
11029}
11030
11031#[gpui::test]
11032async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11033 init_test(cx, |settings| {
11034 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11035 });
11036
11037 let mut cx = EditorTestContext::new(cx).await;
11038
11039 let language = Arc::new(Language::new(
11040 LanguageConfig {
11041 brackets: BracketPairConfig {
11042 pairs: vec![
11043 BracketPair {
11044 start: "{".to_string(),
11045 end: "}".to_string(),
11046 close: true,
11047 surround: true,
11048 newline: true,
11049 },
11050 BracketPair {
11051 start: "(".to_string(),
11052 end: ")".to_string(),
11053 close: true,
11054 surround: true,
11055 newline: true,
11056 },
11057 BracketPair {
11058 start: "[".to_string(),
11059 end: "]".to_string(),
11060 close: false,
11061 surround: true,
11062 newline: true,
11063 },
11064 ],
11065 ..Default::default()
11066 },
11067 autoclose_before: "})]".to_string(),
11068 ..Default::default()
11069 },
11070 Some(tree_sitter_rust::LANGUAGE.into()),
11071 ));
11072
11073 cx.language_registry().add(language.clone());
11074 cx.update_buffer(|buffer, cx| {
11075 buffer.set_language(Some(language), cx);
11076 });
11077
11078 cx.set_state(
11079 &"
11080 {(ˇ)}
11081 [[ˇ]]
11082 {(ˇ)}
11083 "
11084 .unindent(),
11085 );
11086
11087 cx.update_editor(|editor, window, cx| {
11088 editor.backspace(&Default::default(), window, cx);
11089 editor.backspace(&Default::default(), window, cx);
11090 });
11091
11092 cx.assert_editor_state(
11093 &"
11094 ˇ
11095 ˇ]]
11096 ˇ
11097 "
11098 .unindent(),
11099 );
11100
11101 cx.update_editor(|editor, window, cx| {
11102 editor.handle_input("{", window, cx);
11103 editor.handle_input("{", window, cx);
11104 editor.move_right(&MoveRight, window, cx);
11105 editor.move_right(&MoveRight, window, cx);
11106 editor.move_left(&MoveLeft, window, cx);
11107 editor.move_left(&MoveLeft, window, cx);
11108 editor.backspace(&Default::default(), window, cx);
11109 });
11110
11111 cx.assert_editor_state(
11112 &"
11113 {ˇ}
11114 {ˇ}]]
11115 {ˇ}
11116 "
11117 .unindent(),
11118 );
11119
11120 cx.update_editor(|editor, window, cx| {
11121 editor.backspace(&Default::default(), window, cx);
11122 });
11123
11124 cx.assert_editor_state(
11125 &"
11126 ˇ
11127 ˇ]]
11128 ˇ
11129 "
11130 .unindent(),
11131 );
11132}
11133
11134#[gpui::test]
11135async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11136 init_test(cx, |_| {});
11137
11138 let language = Arc::new(Language::new(
11139 LanguageConfig::default(),
11140 Some(tree_sitter_rust::LANGUAGE.into()),
11141 ));
11142
11143 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11144 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11145 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11146 editor
11147 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11148 .await;
11149
11150 editor.update_in(cx, |editor, window, cx| {
11151 editor.set_auto_replace_emoji_shortcode(true);
11152
11153 editor.handle_input("Hello ", window, cx);
11154 editor.handle_input(":wave", window, cx);
11155 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11156
11157 editor.handle_input(":", window, cx);
11158 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11159
11160 editor.handle_input(" :smile", window, cx);
11161 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11162
11163 editor.handle_input(":", window, cx);
11164 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11165
11166 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11167 editor.handle_input(":wave", window, cx);
11168 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11169
11170 editor.handle_input(":", window, cx);
11171 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11172
11173 editor.handle_input(":1", window, cx);
11174 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11175
11176 editor.handle_input(":", window, cx);
11177 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11178
11179 // Ensure shortcode does not get replaced when it is part of a word
11180 editor.handle_input(" Test:wave", window, cx);
11181 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11182
11183 editor.handle_input(":", window, cx);
11184 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11185
11186 editor.set_auto_replace_emoji_shortcode(false);
11187
11188 // Ensure shortcode does not get replaced when auto replace is off
11189 editor.handle_input(" :wave", window, cx);
11190 assert_eq!(
11191 editor.text(cx),
11192 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11193 );
11194
11195 editor.handle_input(":", window, cx);
11196 assert_eq!(
11197 editor.text(cx),
11198 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11199 );
11200 });
11201}
11202
11203#[gpui::test]
11204async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11205 init_test(cx, |_| {});
11206
11207 let (text, insertion_ranges) = marked_text_ranges(
11208 indoc! {"
11209 ˇ
11210 "},
11211 false,
11212 );
11213
11214 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11215 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11216
11217 _ = editor.update_in(cx, |editor, window, cx| {
11218 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11219
11220 editor
11221 .insert_snippet(
11222 &insertion_ranges
11223 .iter()
11224 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11225 .collect::<Vec<_>>(),
11226 snippet,
11227 window,
11228 cx,
11229 )
11230 .unwrap();
11231
11232 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11233 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11234 assert_eq!(editor.text(cx), expected_text);
11235 assert_eq!(
11236 editor.selections.ranges(&editor.display_snapshot(cx)),
11237 selection_ranges
11238 .iter()
11239 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11240 .collect::<Vec<_>>()
11241 );
11242 }
11243
11244 assert(
11245 editor,
11246 cx,
11247 indoc! {"
11248 type «» =•
11249 "},
11250 );
11251
11252 assert!(editor.context_menu_visible(), "There should be a matches");
11253 });
11254}
11255
11256#[gpui::test]
11257async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11258 init_test(cx, |_| {});
11259
11260 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11261 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11262 assert_eq!(editor.text(cx), expected_text);
11263 assert_eq!(
11264 editor.selections.ranges(&editor.display_snapshot(cx)),
11265 selection_ranges
11266 .iter()
11267 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11268 .collect::<Vec<_>>()
11269 );
11270 }
11271
11272 let (text, insertion_ranges) = marked_text_ranges(
11273 indoc! {"
11274 ˇ
11275 "},
11276 false,
11277 );
11278
11279 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11280 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11281
11282 _ = editor.update_in(cx, |editor, window, cx| {
11283 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11284
11285 editor
11286 .insert_snippet(
11287 &insertion_ranges
11288 .iter()
11289 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11290 .collect::<Vec<_>>(),
11291 snippet,
11292 window,
11293 cx,
11294 )
11295 .unwrap();
11296
11297 assert_state(
11298 editor,
11299 cx,
11300 indoc! {"
11301 type «» = ;•
11302 "},
11303 );
11304
11305 assert!(
11306 editor.context_menu_visible(),
11307 "Context menu should be visible for placeholder choices"
11308 );
11309
11310 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11311
11312 assert_state(
11313 editor,
11314 cx,
11315 indoc! {"
11316 type = «»;•
11317 "},
11318 );
11319
11320 assert!(
11321 !editor.context_menu_visible(),
11322 "Context menu should be hidden after moving to next tabstop"
11323 );
11324
11325 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11326
11327 assert_state(
11328 editor,
11329 cx,
11330 indoc! {"
11331 type = ; ˇ
11332 "},
11333 );
11334
11335 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11336
11337 assert_state(
11338 editor,
11339 cx,
11340 indoc! {"
11341 type = ; ˇ
11342 "},
11343 );
11344 });
11345
11346 _ = editor.update_in(cx, |editor, window, cx| {
11347 editor.select_all(&SelectAll, window, cx);
11348 editor.backspace(&Backspace, window, cx);
11349
11350 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11351 let insertion_ranges = editor
11352 .selections
11353 .all(&editor.display_snapshot(cx))
11354 .iter()
11355 .map(|s| s.range())
11356 .collect::<Vec<_>>();
11357
11358 editor
11359 .insert_snippet(&insertion_ranges, snippet, window, cx)
11360 .unwrap();
11361
11362 assert_state(editor, cx, "fn «» = value;•");
11363
11364 assert!(
11365 editor.context_menu_visible(),
11366 "Context menu should be visible for placeholder choices"
11367 );
11368
11369 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11370
11371 assert_state(editor, cx, "fn = «valueˇ»;•");
11372
11373 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11374
11375 assert_state(editor, cx, "fn «» = value;•");
11376
11377 assert!(
11378 editor.context_menu_visible(),
11379 "Context menu should be visible again after returning to first tabstop"
11380 );
11381
11382 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11383
11384 assert_state(editor, cx, "fn «» = value;•");
11385 });
11386}
11387
11388#[gpui::test]
11389async fn test_snippets(cx: &mut TestAppContext) {
11390 init_test(cx, |_| {});
11391
11392 let mut cx = EditorTestContext::new(cx).await;
11393
11394 cx.set_state(indoc! {"
11395 a.ˇ b
11396 a.ˇ b
11397 a.ˇ b
11398 "});
11399
11400 cx.update_editor(|editor, window, cx| {
11401 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11402 let insertion_ranges = editor
11403 .selections
11404 .all(&editor.display_snapshot(cx))
11405 .iter()
11406 .map(|s| s.range())
11407 .collect::<Vec<_>>();
11408 editor
11409 .insert_snippet(&insertion_ranges, snippet, window, cx)
11410 .unwrap();
11411 });
11412
11413 cx.assert_editor_state(indoc! {"
11414 a.f(«oneˇ», two, «threeˇ») b
11415 a.f(«oneˇ», two, «threeˇ») b
11416 a.f(«oneˇ», two, «threeˇ») b
11417 "});
11418
11419 // Can't move earlier than the first tab stop
11420 cx.update_editor(|editor, window, cx| {
11421 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11422 });
11423 cx.assert_editor_state(indoc! {"
11424 a.f(«oneˇ», two, «threeˇ») b
11425 a.f(«oneˇ», two, «threeˇ») b
11426 a.f(«oneˇ», two, «threeˇ») b
11427 "});
11428
11429 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11430 cx.assert_editor_state(indoc! {"
11431 a.f(one, «twoˇ», three) b
11432 a.f(one, «twoˇ», three) b
11433 a.f(one, «twoˇ», three) b
11434 "});
11435
11436 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11437 cx.assert_editor_state(indoc! {"
11438 a.f(«oneˇ», two, «threeˇ») b
11439 a.f(«oneˇ», two, «threeˇ») b
11440 a.f(«oneˇ», two, «threeˇ») b
11441 "});
11442
11443 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11444 cx.assert_editor_state(indoc! {"
11445 a.f(one, «twoˇ», three) b
11446 a.f(one, «twoˇ», three) b
11447 a.f(one, «twoˇ», three) b
11448 "});
11449 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11450 cx.assert_editor_state(indoc! {"
11451 a.f(one, two, three)ˇ b
11452 a.f(one, two, three)ˇ b
11453 a.f(one, two, three)ˇ b
11454 "});
11455
11456 // As soon as the last tab stop is reached, snippet state is gone
11457 cx.update_editor(|editor, window, cx| {
11458 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11459 });
11460 cx.assert_editor_state(indoc! {"
11461 a.f(one, two, three)ˇ b
11462 a.f(one, two, three)ˇ b
11463 a.f(one, two, three)ˇ b
11464 "});
11465}
11466
11467#[gpui::test]
11468async fn test_snippet_indentation(cx: &mut TestAppContext) {
11469 init_test(cx, |_| {});
11470
11471 let mut cx = EditorTestContext::new(cx).await;
11472
11473 cx.update_editor(|editor, window, cx| {
11474 let snippet = Snippet::parse(indoc! {"
11475 /*
11476 * Multiline comment with leading indentation
11477 *
11478 * $1
11479 */
11480 $0"})
11481 .unwrap();
11482 let insertion_ranges = editor
11483 .selections
11484 .all(&editor.display_snapshot(cx))
11485 .iter()
11486 .map(|s| s.range())
11487 .collect::<Vec<_>>();
11488 editor
11489 .insert_snippet(&insertion_ranges, snippet, window, cx)
11490 .unwrap();
11491 });
11492
11493 cx.assert_editor_state(indoc! {"
11494 /*
11495 * Multiline comment with leading indentation
11496 *
11497 * ˇ
11498 */
11499 "});
11500
11501 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11502 cx.assert_editor_state(indoc! {"
11503 /*
11504 * Multiline comment with leading indentation
11505 *
11506 *•
11507 */
11508 ˇ"});
11509}
11510
11511#[gpui::test]
11512async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11513 init_test(cx, |_| {});
11514
11515 let mut cx = EditorTestContext::new(cx).await;
11516 cx.update_editor(|editor, _, cx| {
11517 editor.project().unwrap().update(cx, |project, cx| {
11518 project.snippets().update(cx, |snippets, _cx| {
11519 let snippet = project::snippet_provider::Snippet {
11520 prefix: vec!["multi word".to_string()],
11521 body: "this is many words".to_string(),
11522 description: Some("description".to_string()),
11523 name: "multi-word snippet test".to_string(),
11524 };
11525 snippets.add_snippet_for_test(
11526 None,
11527 PathBuf::from("test_snippets.json"),
11528 vec![Arc::new(snippet)],
11529 );
11530 });
11531 })
11532 });
11533
11534 for (input_to_simulate, should_match_snippet) in [
11535 ("m", true),
11536 ("m ", true),
11537 ("m w", true),
11538 ("aa m w", true),
11539 ("aa m g", false),
11540 ] {
11541 cx.set_state("ˇ");
11542 cx.simulate_input(input_to_simulate); // fails correctly
11543
11544 cx.update_editor(|editor, _, _| {
11545 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11546 else {
11547 assert!(!should_match_snippet); // no completions! don't even show the menu
11548 return;
11549 };
11550 assert!(context_menu.visible());
11551 let completions = context_menu.completions.borrow();
11552
11553 assert_eq!(!completions.is_empty(), should_match_snippet);
11554 });
11555 }
11556}
11557
11558#[gpui::test]
11559async fn test_document_format_during_save(cx: &mut TestAppContext) {
11560 init_test(cx, |_| {});
11561
11562 let fs = FakeFs::new(cx.executor());
11563 fs.insert_file(path!("/file.rs"), Default::default()).await;
11564
11565 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11566
11567 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11568 language_registry.add(rust_lang());
11569 let mut fake_servers = language_registry.register_fake_lsp(
11570 "Rust",
11571 FakeLspAdapter {
11572 capabilities: lsp::ServerCapabilities {
11573 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11574 ..Default::default()
11575 },
11576 ..Default::default()
11577 },
11578 );
11579
11580 let buffer = project
11581 .update(cx, |project, cx| {
11582 project.open_local_buffer(path!("/file.rs"), cx)
11583 })
11584 .await
11585 .unwrap();
11586
11587 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11588 let (editor, cx) = cx.add_window_view(|window, cx| {
11589 build_editor_with_project(project.clone(), buffer, window, cx)
11590 });
11591 editor.update_in(cx, |editor, window, cx| {
11592 editor.set_text("one\ntwo\nthree\n", window, cx)
11593 });
11594 assert!(cx.read(|cx| editor.is_dirty(cx)));
11595
11596 cx.executor().start_waiting();
11597 let fake_server = fake_servers.next().await.unwrap();
11598
11599 {
11600 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11601 move |params, _| async move {
11602 assert_eq!(
11603 params.text_document.uri,
11604 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11605 );
11606 assert_eq!(params.options.tab_size, 4);
11607 Ok(Some(vec![lsp::TextEdit::new(
11608 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11609 ", ".to_string(),
11610 )]))
11611 },
11612 );
11613 let save = editor
11614 .update_in(cx, |editor, window, cx| {
11615 editor.save(
11616 SaveOptions {
11617 format: true,
11618 autosave: false,
11619 },
11620 project.clone(),
11621 window,
11622 cx,
11623 )
11624 })
11625 .unwrap();
11626 cx.executor().start_waiting();
11627 save.await;
11628
11629 assert_eq!(
11630 editor.update(cx, |editor, cx| editor.text(cx)),
11631 "one, two\nthree\n"
11632 );
11633 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11634 }
11635
11636 {
11637 editor.update_in(cx, |editor, window, cx| {
11638 editor.set_text("one\ntwo\nthree\n", window, cx)
11639 });
11640 assert!(cx.read(|cx| editor.is_dirty(cx)));
11641
11642 // Ensure we can still save even if formatting hangs.
11643 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11644 move |params, _| async move {
11645 assert_eq!(
11646 params.text_document.uri,
11647 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11648 );
11649 futures::future::pending::<()>().await;
11650 unreachable!()
11651 },
11652 );
11653 let save = editor
11654 .update_in(cx, |editor, window, cx| {
11655 editor.save(
11656 SaveOptions {
11657 format: true,
11658 autosave: false,
11659 },
11660 project.clone(),
11661 window,
11662 cx,
11663 )
11664 })
11665 .unwrap();
11666 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11667 cx.executor().start_waiting();
11668 save.await;
11669 assert_eq!(
11670 editor.update(cx, |editor, cx| editor.text(cx)),
11671 "one\ntwo\nthree\n"
11672 );
11673 }
11674
11675 // Set rust language override and assert overridden tabsize is sent to language server
11676 update_test_language_settings(cx, |settings| {
11677 settings.languages.0.insert(
11678 "Rust".into(),
11679 LanguageSettingsContent {
11680 tab_size: NonZeroU32::new(8),
11681 ..Default::default()
11682 },
11683 );
11684 });
11685
11686 {
11687 editor.update_in(cx, |editor, window, cx| {
11688 editor.set_text("somehting_new\n", window, cx)
11689 });
11690 assert!(cx.read(|cx| editor.is_dirty(cx)));
11691 let _formatting_request_signal = fake_server
11692 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11693 assert_eq!(
11694 params.text_document.uri,
11695 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11696 );
11697 assert_eq!(params.options.tab_size, 8);
11698 Ok(Some(vec![]))
11699 });
11700 let save = editor
11701 .update_in(cx, |editor, window, cx| {
11702 editor.save(
11703 SaveOptions {
11704 format: true,
11705 autosave: false,
11706 },
11707 project.clone(),
11708 window,
11709 cx,
11710 )
11711 })
11712 .unwrap();
11713 cx.executor().start_waiting();
11714 save.await;
11715 }
11716}
11717
11718#[gpui::test]
11719async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11720 init_test(cx, |settings| {
11721 settings.defaults.ensure_final_newline_on_save = Some(false);
11722 });
11723
11724 let fs = FakeFs::new(cx.executor());
11725 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11726
11727 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11728
11729 let buffer = project
11730 .update(cx, |project, cx| {
11731 project.open_local_buffer(path!("/file.txt"), cx)
11732 })
11733 .await
11734 .unwrap();
11735
11736 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11737 let (editor, cx) = cx.add_window_view(|window, cx| {
11738 build_editor_with_project(project.clone(), buffer, window, cx)
11739 });
11740 editor.update_in(cx, |editor, window, cx| {
11741 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11742 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11743 });
11744 });
11745 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11746
11747 editor.update_in(cx, |editor, window, cx| {
11748 editor.handle_input("\n", window, cx)
11749 });
11750 cx.run_until_parked();
11751 save(&editor, &project, cx).await;
11752 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11753
11754 editor.update_in(cx, |editor, window, cx| {
11755 editor.undo(&Default::default(), window, cx);
11756 });
11757 save(&editor, &project, cx).await;
11758 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11759
11760 editor.update_in(cx, |editor, window, cx| {
11761 editor.redo(&Default::default(), window, cx);
11762 });
11763 cx.run_until_parked();
11764 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11765
11766 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11767 let save = editor
11768 .update_in(cx, |editor, window, cx| {
11769 editor.save(
11770 SaveOptions {
11771 format: true,
11772 autosave: false,
11773 },
11774 project.clone(),
11775 window,
11776 cx,
11777 )
11778 })
11779 .unwrap();
11780 cx.executor().start_waiting();
11781 save.await;
11782 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11783 }
11784}
11785
11786#[gpui::test]
11787async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11788 init_test(cx, |_| {});
11789
11790 let cols = 4;
11791 let rows = 10;
11792 let sample_text_1 = sample_text(rows, cols, 'a');
11793 assert_eq!(
11794 sample_text_1,
11795 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11796 );
11797 let sample_text_2 = sample_text(rows, cols, 'l');
11798 assert_eq!(
11799 sample_text_2,
11800 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11801 );
11802 let sample_text_3 = sample_text(rows, cols, 'v');
11803 assert_eq!(
11804 sample_text_3,
11805 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11806 );
11807
11808 let fs = FakeFs::new(cx.executor());
11809 fs.insert_tree(
11810 path!("/a"),
11811 json!({
11812 "main.rs": sample_text_1,
11813 "other.rs": sample_text_2,
11814 "lib.rs": sample_text_3,
11815 }),
11816 )
11817 .await;
11818
11819 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11820 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11821 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11822
11823 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11824 language_registry.add(rust_lang());
11825 let mut fake_servers = language_registry.register_fake_lsp(
11826 "Rust",
11827 FakeLspAdapter {
11828 capabilities: lsp::ServerCapabilities {
11829 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11830 ..Default::default()
11831 },
11832 ..Default::default()
11833 },
11834 );
11835
11836 let worktree = project.update(cx, |project, cx| {
11837 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11838 assert_eq!(worktrees.len(), 1);
11839 worktrees.pop().unwrap()
11840 });
11841 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11842
11843 let buffer_1 = project
11844 .update(cx, |project, cx| {
11845 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11846 })
11847 .await
11848 .unwrap();
11849 let buffer_2 = project
11850 .update(cx, |project, cx| {
11851 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11852 })
11853 .await
11854 .unwrap();
11855 let buffer_3 = project
11856 .update(cx, |project, cx| {
11857 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11858 })
11859 .await
11860 .unwrap();
11861
11862 let multi_buffer = cx.new(|cx| {
11863 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11864 multi_buffer.push_excerpts(
11865 buffer_1.clone(),
11866 [
11867 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11868 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11869 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11870 ],
11871 cx,
11872 );
11873 multi_buffer.push_excerpts(
11874 buffer_2.clone(),
11875 [
11876 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11877 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11878 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11879 ],
11880 cx,
11881 );
11882 multi_buffer.push_excerpts(
11883 buffer_3.clone(),
11884 [
11885 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11886 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11887 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11888 ],
11889 cx,
11890 );
11891 multi_buffer
11892 });
11893 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11894 Editor::new(
11895 EditorMode::full(),
11896 multi_buffer,
11897 Some(project.clone()),
11898 window,
11899 cx,
11900 )
11901 });
11902
11903 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11904 editor.change_selections(
11905 SelectionEffects::scroll(Autoscroll::Next),
11906 window,
11907 cx,
11908 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
11909 );
11910 editor.insert("|one|two|three|", window, cx);
11911 });
11912 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11913 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11914 editor.change_selections(
11915 SelectionEffects::scroll(Autoscroll::Next),
11916 window,
11917 cx,
11918 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
11919 );
11920 editor.insert("|four|five|six|", window, cx);
11921 });
11922 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11923
11924 // First two buffers should be edited, but not the third one.
11925 assert_eq!(
11926 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11927 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
11928 );
11929 buffer_1.update(cx, |buffer, _| {
11930 assert!(buffer.is_dirty());
11931 assert_eq!(
11932 buffer.text(),
11933 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11934 )
11935 });
11936 buffer_2.update(cx, |buffer, _| {
11937 assert!(buffer.is_dirty());
11938 assert_eq!(
11939 buffer.text(),
11940 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11941 )
11942 });
11943 buffer_3.update(cx, |buffer, _| {
11944 assert!(!buffer.is_dirty());
11945 assert_eq!(buffer.text(), sample_text_3,)
11946 });
11947 cx.executor().run_until_parked();
11948
11949 cx.executor().start_waiting();
11950 let save = multi_buffer_editor
11951 .update_in(cx, |editor, window, cx| {
11952 editor.save(
11953 SaveOptions {
11954 format: true,
11955 autosave: false,
11956 },
11957 project.clone(),
11958 window,
11959 cx,
11960 )
11961 })
11962 .unwrap();
11963
11964 let fake_server = fake_servers.next().await.unwrap();
11965 fake_server
11966 .server
11967 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11968 Ok(Some(vec![lsp::TextEdit::new(
11969 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11970 format!("[{} formatted]", params.text_document.uri),
11971 )]))
11972 })
11973 .detach();
11974 save.await;
11975
11976 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11977 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11978 assert_eq!(
11979 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11980 uri!(
11981 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
11982 ),
11983 );
11984 buffer_1.update(cx, |buffer, _| {
11985 assert!(!buffer.is_dirty());
11986 assert_eq!(
11987 buffer.text(),
11988 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11989 )
11990 });
11991 buffer_2.update(cx, |buffer, _| {
11992 assert!(!buffer.is_dirty());
11993 assert_eq!(
11994 buffer.text(),
11995 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11996 )
11997 });
11998 buffer_3.update(cx, |buffer, _| {
11999 assert!(!buffer.is_dirty());
12000 assert_eq!(buffer.text(), sample_text_3,)
12001 });
12002}
12003
12004#[gpui::test]
12005async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12006 init_test(cx, |_| {});
12007
12008 let fs = FakeFs::new(cx.executor());
12009 fs.insert_tree(
12010 path!("/dir"),
12011 json!({
12012 "file1.rs": "fn main() { println!(\"hello\"); }",
12013 "file2.rs": "fn test() { println!(\"test\"); }",
12014 "file3.rs": "fn other() { println!(\"other\"); }\n",
12015 }),
12016 )
12017 .await;
12018
12019 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12020 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12021 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12022
12023 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12024 language_registry.add(rust_lang());
12025
12026 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12027 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12028
12029 // Open three buffers
12030 let buffer_1 = project
12031 .update(cx, |project, cx| {
12032 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12033 })
12034 .await
12035 .unwrap();
12036 let buffer_2 = project
12037 .update(cx, |project, cx| {
12038 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12039 })
12040 .await
12041 .unwrap();
12042 let buffer_3 = project
12043 .update(cx, |project, cx| {
12044 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12045 })
12046 .await
12047 .unwrap();
12048
12049 // Create a multi-buffer with all three buffers
12050 let multi_buffer = cx.new(|cx| {
12051 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12052 multi_buffer.push_excerpts(
12053 buffer_1.clone(),
12054 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12055 cx,
12056 );
12057 multi_buffer.push_excerpts(
12058 buffer_2.clone(),
12059 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12060 cx,
12061 );
12062 multi_buffer.push_excerpts(
12063 buffer_3.clone(),
12064 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12065 cx,
12066 );
12067 multi_buffer
12068 });
12069
12070 let editor = cx.new_window_entity(|window, cx| {
12071 Editor::new(
12072 EditorMode::full(),
12073 multi_buffer,
12074 Some(project.clone()),
12075 window,
12076 cx,
12077 )
12078 });
12079
12080 // Edit only the first buffer
12081 editor.update_in(cx, |editor, window, cx| {
12082 editor.change_selections(
12083 SelectionEffects::scroll(Autoscroll::Next),
12084 window,
12085 cx,
12086 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12087 );
12088 editor.insert("// edited", window, cx);
12089 });
12090
12091 // Verify that only buffer 1 is dirty
12092 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12093 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12094 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12095
12096 // Get write counts after file creation (files were created with initial content)
12097 // We expect each file to have been written once during creation
12098 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12099 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12100 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12101
12102 // Perform autosave
12103 let save_task = editor.update_in(cx, |editor, window, cx| {
12104 editor.save(
12105 SaveOptions {
12106 format: true,
12107 autosave: true,
12108 },
12109 project.clone(),
12110 window,
12111 cx,
12112 )
12113 });
12114 save_task.await.unwrap();
12115
12116 // Only the dirty buffer should have been saved
12117 assert_eq!(
12118 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12119 1,
12120 "Buffer 1 was dirty, so it should have been written once during autosave"
12121 );
12122 assert_eq!(
12123 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12124 0,
12125 "Buffer 2 was clean, so it should not have been written during autosave"
12126 );
12127 assert_eq!(
12128 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12129 0,
12130 "Buffer 3 was clean, so it should not have been written during autosave"
12131 );
12132
12133 // Verify buffer states after autosave
12134 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12135 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12136 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12137
12138 // Now perform a manual save (format = true)
12139 let save_task = editor.update_in(cx, |editor, window, cx| {
12140 editor.save(
12141 SaveOptions {
12142 format: true,
12143 autosave: false,
12144 },
12145 project.clone(),
12146 window,
12147 cx,
12148 )
12149 });
12150 save_task.await.unwrap();
12151
12152 // During manual save, clean buffers don't get written to disk
12153 // They just get did_save called for language server notifications
12154 assert_eq!(
12155 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12156 1,
12157 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12158 );
12159 assert_eq!(
12160 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12161 0,
12162 "Buffer 2 should not have been written at all"
12163 );
12164 assert_eq!(
12165 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12166 0,
12167 "Buffer 3 should not have been written at all"
12168 );
12169}
12170
12171async fn setup_range_format_test(
12172 cx: &mut TestAppContext,
12173) -> (
12174 Entity<Project>,
12175 Entity<Editor>,
12176 &mut gpui::VisualTestContext,
12177 lsp::FakeLanguageServer,
12178) {
12179 init_test(cx, |_| {});
12180
12181 let fs = FakeFs::new(cx.executor());
12182 fs.insert_file(path!("/file.rs"), Default::default()).await;
12183
12184 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12185
12186 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12187 language_registry.add(rust_lang());
12188 let mut fake_servers = language_registry.register_fake_lsp(
12189 "Rust",
12190 FakeLspAdapter {
12191 capabilities: lsp::ServerCapabilities {
12192 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12193 ..lsp::ServerCapabilities::default()
12194 },
12195 ..FakeLspAdapter::default()
12196 },
12197 );
12198
12199 let buffer = project
12200 .update(cx, |project, cx| {
12201 project.open_local_buffer(path!("/file.rs"), cx)
12202 })
12203 .await
12204 .unwrap();
12205
12206 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12207 let (editor, cx) = cx.add_window_view(|window, cx| {
12208 build_editor_with_project(project.clone(), buffer, window, cx)
12209 });
12210
12211 cx.executor().start_waiting();
12212 let fake_server = fake_servers.next().await.unwrap();
12213
12214 (project, editor, cx, fake_server)
12215}
12216
12217#[gpui::test]
12218async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12219 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12220
12221 editor.update_in(cx, |editor, window, cx| {
12222 editor.set_text("one\ntwo\nthree\n", window, cx)
12223 });
12224 assert!(cx.read(|cx| editor.is_dirty(cx)));
12225
12226 let save = editor
12227 .update_in(cx, |editor, window, cx| {
12228 editor.save(
12229 SaveOptions {
12230 format: true,
12231 autosave: false,
12232 },
12233 project.clone(),
12234 window,
12235 cx,
12236 )
12237 })
12238 .unwrap();
12239 fake_server
12240 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12241 assert_eq!(
12242 params.text_document.uri,
12243 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12244 );
12245 assert_eq!(params.options.tab_size, 4);
12246 Ok(Some(vec![lsp::TextEdit::new(
12247 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12248 ", ".to_string(),
12249 )]))
12250 })
12251 .next()
12252 .await;
12253 cx.executor().start_waiting();
12254 save.await;
12255 assert_eq!(
12256 editor.update(cx, |editor, cx| editor.text(cx)),
12257 "one, two\nthree\n"
12258 );
12259 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12260}
12261
12262#[gpui::test]
12263async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12264 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12265
12266 editor.update_in(cx, |editor, window, cx| {
12267 editor.set_text("one\ntwo\nthree\n", window, cx)
12268 });
12269 assert!(cx.read(|cx| editor.is_dirty(cx)));
12270
12271 // Test that save still works when formatting hangs
12272 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12273 move |params, _| async move {
12274 assert_eq!(
12275 params.text_document.uri,
12276 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12277 );
12278 futures::future::pending::<()>().await;
12279 unreachable!()
12280 },
12281 );
12282 let save = editor
12283 .update_in(cx, |editor, window, cx| {
12284 editor.save(
12285 SaveOptions {
12286 format: true,
12287 autosave: false,
12288 },
12289 project.clone(),
12290 window,
12291 cx,
12292 )
12293 })
12294 .unwrap();
12295 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12296 cx.executor().start_waiting();
12297 save.await;
12298 assert_eq!(
12299 editor.update(cx, |editor, cx| editor.text(cx)),
12300 "one\ntwo\nthree\n"
12301 );
12302 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12303}
12304
12305#[gpui::test]
12306async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12307 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12308
12309 // Buffer starts clean, no formatting should be requested
12310 let save = editor
12311 .update_in(cx, |editor, window, cx| {
12312 editor.save(
12313 SaveOptions {
12314 format: false,
12315 autosave: false,
12316 },
12317 project.clone(),
12318 window,
12319 cx,
12320 )
12321 })
12322 .unwrap();
12323 let _pending_format_request = fake_server
12324 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12325 panic!("Should not be invoked");
12326 })
12327 .next();
12328 cx.executor().start_waiting();
12329 save.await;
12330 cx.run_until_parked();
12331}
12332
12333#[gpui::test]
12334async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12335 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12336
12337 // Set Rust language override and assert overridden tabsize is sent to language server
12338 update_test_language_settings(cx, |settings| {
12339 settings.languages.0.insert(
12340 "Rust".into(),
12341 LanguageSettingsContent {
12342 tab_size: NonZeroU32::new(8),
12343 ..Default::default()
12344 },
12345 );
12346 });
12347
12348 editor.update_in(cx, |editor, window, cx| {
12349 editor.set_text("something_new\n", window, cx)
12350 });
12351 assert!(cx.read(|cx| editor.is_dirty(cx)));
12352 let save = editor
12353 .update_in(cx, |editor, window, cx| {
12354 editor.save(
12355 SaveOptions {
12356 format: true,
12357 autosave: false,
12358 },
12359 project.clone(),
12360 window,
12361 cx,
12362 )
12363 })
12364 .unwrap();
12365 fake_server
12366 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12367 assert_eq!(
12368 params.text_document.uri,
12369 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12370 );
12371 assert_eq!(params.options.tab_size, 8);
12372 Ok(Some(Vec::new()))
12373 })
12374 .next()
12375 .await;
12376 save.await;
12377}
12378
12379#[gpui::test]
12380async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12381 init_test(cx, |settings| {
12382 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12383 settings::LanguageServerFormatterSpecifier::Current,
12384 )))
12385 });
12386
12387 let fs = FakeFs::new(cx.executor());
12388 fs.insert_file(path!("/file.rs"), Default::default()).await;
12389
12390 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12391
12392 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12393 language_registry.add(Arc::new(Language::new(
12394 LanguageConfig {
12395 name: "Rust".into(),
12396 matcher: LanguageMatcher {
12397 path_suffixes: vec!["rs".to_string()],
12398 ..Default::default()
12399 },
12400 ..LanguageConfig::default()
12401 },
12402 Some(tree_sitter_rust::LANGUAGE.into()),
12403 )));
12404 update_test_language_settings(cx, |settings| {
12405 // Enable Prettier formatting for the same buffer, and ensure
12406 // LSP is called instead of Prettier.
12407 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12408 });
12409 let mut fake_servers = language_registry.register_fake_lsp(
12410 "Rust",
12411 FakeLspAdapter {
12412 capabilities: lsp::ServerCapabilities {
12413 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12414 ..Default::default()
12415 },
12416 ..Default::default()
12417 },
12418 );
12419
12420 let buffer = project
12421 .update(cx, |project, cx| {
12422 project.open_local_buffer(path!("/file.rs"), cx)
12423 })
12424 .await
12425 .unwrap();
12426
12427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12428 let (editor, cx) = cx.add_window_view(|window, cx| {
12429 build_editor_with_project(project.clone(), buffer, window, cx)
12430 });
12431 editor.update_in(cx, |editor, window, cx| {
12432 editor.set_text("one\ntwo\nthree\n", window, cx)
12433 });
12434
12435 cx.executor().start_waiting();
12436 let fake_server = fake_servers.next().await.unwrap();
12437
12438 let format = editor
12439 .update_in(cx, |editor, window, cx| {
12440 editor.perform_format(
12441 project.clone(),
12442 FormatTrigger::Manual,
12443 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12444 window,
12445 cx,
12446 )
12447 })
12448 .unwrap();
12449 fake_server
12450 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12451 assert_eq!(
12452 params.text_document.uri,
12453 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12454 );
12455 assert_eq!(params.options.tab_size, 4);
12456 Ok(Some(vec![lsp::TextEdit::new(
12457 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12458 ", ".to_string(),
12459 )]))
12460 })
12461 .next()
12462 .await;
12463 cx.executor().start_waiting();
12464 format.await;
12465 assert_eq!(
12466 editor.update(cx, |editor, cx| editor.text(cx)),
12467 "one, two\nthree\n"
12468 );
12469
12470 editor.update_in(cx, |editor, window, cx| {
12471 editor.set_text("one\ntwo\nthree\n", window, cx)
12472 });
12473 // Ensure we don't lock if formatting hangs.
12474 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12475 move |params, _| async move {
12476 assert_eq!(
12477 params.text_document.uri,
12478 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12479 );
12480 futures::future::pending::<()>().await;
12481 unreachable!()
12482 },
12483 );
12484 let format = editor
12485 .update_in(cx, |editor, window, cx| {
12486 editor.perform_format(
12487 project,
12488 FormatTrigger::Manual,
12489 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12490 window,
12491 cx,
12492 )
12493 })
12494 .unwrap();
12495 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12496 cx.executor().start_waiting();
12497 format.await;
12498 assert_eq!(
12499 editor.update(cx, |editor, cx| editor.text(cx)),
12500 "one\ntwo\nthree\n"
12501 );
12502}
12503
12504#[gpui::test]
12505async fn test_multiple_formatters(cx: &mut TestAppContext) {
12506 init_test(cx, |settings| {
12507 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12508 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12509 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12510 Formatter::CodeAction("code-action-1".into()),
12511 Formatter::CodeAction("code-action-2".into()),
12512 ]))
12513 });
12514
12515 let fs = FakeFs::new(cx.executor());
12516 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12517 .await;
12518
12519 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12520 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12521 language_registry.add(rust_lang());
12522
12523 let mut fake_servers = language_registry.register_fake_lsp(
12524 "Rust",
12525 FakeLspAdapter {
12526 capabilities: lsp::ServerCapabilities {
12527 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12528 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12529 commands: vec!["the-command-for-code-action-1".into()],
12530 ..Default::default()
12531 }),
12532 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12533 ..Default::default()
12534 },
12535 ..Default::default()
12536 },
12537 );
12538
12539 let buffer = project
12540 .update(cx, |project, cx| {
12541 project.open_local_buffer(path!("/file.rs"), cx)
12542 })
12543 .await
12544 .unwrap();
12545
12546 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12547 let (editor, cx) = cx.add_window_view(|window, cx| {
12548 build_editor_with_project(project.clone(), buffer, window, cx)
12549 });
12550
12551 cx.executor().start_waiting();
12552
12553 let fake_server = fake_servers.next().await.unwrap();
12554 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12555 move |_params, _| async move {
12556 Ok(Some(vec![lsp::TextEdit::new(
12557 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12558 "applied-formatting\n".to_string(),
12559 )]))
12560 },
12561 );
12562 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12563 move |params, _| async move {
12564 let requested_code_actions = params.context.only.expect("Expected code action request");
12565 assert_eq!(requested_code_actions.len(), 1);
12566
12567 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12568 let code_action = match requested_code_actions[0].as_str() {
12569 "code-action-1" => lsp::CodeAction {
12570 kind: Some("code-action-1".into()),
12571 edit: Some(lsp::WorkspaceEdit::new(
12572 [(
12573 uri,
12574 vec![lsp::TextEdit::new(
12575 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12576 "applied-code-action-1-edit\n".to_string(),
12577 )],
12578 )]
12579 .into_iter()
12580 .collect(),
12581 )),
12582 command: Some(lsp::Command {
12583 command: "the-command-for-code-action-1".into(),
12584 ..Default::default()
12585 }),
12586 ..Default::default()
12587 },
12588 "code-action-2" => lsp::CodeAction {
12589 kind: Some("code-action-2".into()),
12590 edit: Some(lsp::WorkspaceEdit::new(
12591 [(
12592 uri,
12593 vec![lsp::TextEdit::new(
12594 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12595 "applied-code-action-2-edit\n".to_string(),
12596 )],
12597 )]
12598 .into_iter()
12599 .collect(),
12600 )),
12601 ..Default::default()
12602 },
12603 req => panic!("Unexpected code action request: {:?}", req),
12604 };
12605 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12606 code_action,
12607 )]))
12608 },
12609 );
12610
12611 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12612 move |params, _| async move { Ok(params) }
12613 });
12614
12615 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12616 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12617 let fake = fake_server.clone();
12618 let lock = command_lock.clone();
12619 move |params, _| {
12620 assert_eq!(params.command, "the-command-for-code-action-1");
12621 let fake = fake.clone();
12622 let lock = lock.clone();
12623 async move {
12624 lock.lock().await;
12625 fake.server
12626 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12627 label: None,
12628 edit: lsp::WorkspaceEdit {
12629 changes: Some(
12630 [(
12631 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12632 vec![lsp::TextEdit {
12633 range: lsp::Range::new(
12634 lsp::Position::new(0, 0),
12635 lsp::Position::new(0, 0),
12636 ),
12637 new_text: "applied-code-action-1-command\n".into(),
12638 }],
12639 )]
12640 .into_iter()
12641 .collect(),
12642 ),
12643 ..Default::default()
12644 },
12645 })
12646 .await
12647 .into_response()
12648 .unwrap();
12649 Ok(Some(json!(null)))
12650 }
12651 }
12652 });
12653
12654 cx.executor().start_waiting();
12655 editor
12656 .update_in(cx, |editor, window, cx| {
12657 editor.perform_format(
12658 project.clone(),
12659 FormatTrigger::Manual,
12660 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12661 window,
12662 cx,
12663 )
12664 })
12665 .unwrap()
12666 .await;
12667 editor.update(cx, |editor, cx| {
12668 assert_eq!(
12669 editor.text(cx),
12670 r#"
12671 applied-code-action-2-edit
12672 applied-code-action-1-command
12673 applied-code-action-1-edit
12674 applied-formatting
12675 one
12676 two
12677 three
12678 "#
12679 .unindent()
12680 );
12681 });
12682
12683 editor.update_in(cx, |editor, window, cx| {
12684 editor.undo(&Default::default(), window, cx);
12685 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12686 });
12687
12688 // Perform a manual edit while waiting for an LSP command
12689 // that's being run as part of a formatting code action.
12690 let lock_guard = command_lock.lock().await;
12691 let format = editor
12692 .update_in(cx, |editor, window, cx| {
12693 editor.perform_format(
12694 project.clone(),
12695 FormatTrigger::Manual,
12696 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12697 window,
12698 cx,
12699 )
12700 })
12701 .unwrap();
12702 cx.run_until_parked();
12703 editor.update(cx, |editor, cx| {
12704 assert_eq!(
12705 editor.text(cx),
12706 r#"
12707 applied-code-action-1-edit
12708 applied-formatting
12709 one
12710 two
12711 three
12712 "#
12713 .unindent()
12714 );
12715
12716 editor.buffer.update(cx, |buffer, cx| {
12717 let ix = buffer.len(cx);
12718 buffer.edit([(ix..ix, "edited\n")], None, cx);
12719 });
12720 });
12721
12722 // Allow the LSP command to proceed. Because the buffer was edited,
12723 // the second code action will not be run.
12724 drop(lock_guard);
12725 format.await;
12726 editor.update_in(cx, |editor, window, cx| {
12727 assert_eq!(
12728 editor.text(cx),
12729 r#"
12730 applied-code-action-1-command
12731 applied-code-action-1-edit
12732 applied-formatting
12733 one
12734 two
12735 three
12736 edited
12737 "#
12738 .unindent()
12739 );
12740
12741 // The manual edit is undone first, because it is the last thing the user did
12742 // (even though the command completed afterwards).
12743 editor.undo(&Default::default(), window, cx);
12744 assert_eq!(
12745 editor.text(cx),
12746 r#"
12747 applied-code-action-1-command
12748 applied-code-action-1-edit
12749 applied-formatting
12750 one
12751 two
12752 three
12753 "#
12754 .unindent()
12755 );
12756
12757 // All the formatting (including the command, which completed after the manual edit)
12758 // is undone together.
12759 editor.undo(&Default::default(), window, cx);
12760 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12761 });
12762}
12763
12764#[gpui::test]
12765async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12766 init_test(cx, |settings| {
12767 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12768 settings::LanguageServerFormatterSpecifier::Current,
12769 )]))
12770 });
12771
12772 let fs = FakeFs::new(cx.executor());
12773 fs.insert_file(path!("/file.ts"), Default::default()).await;
12774
12775 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12776
12777 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12778 language_registry.add(Arc::new(Language::new(
12779 LanguageConfig {
12780 name: "TypeScript".into(),
12781 matcher: LanguageMatcher {
12782 path_suffixes: vec!["ts".to_string()],
12783 ..Default::default()
12784 },
12785 ..LanguageConfig::default()
12786 },
12787 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12788 )));
12789 update_test_language_settings(cx, |settings| {
12790 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12791 });
12792 let mut fake_servers = language_registry.register_fake_lsp(
12793 "TypeScript",
12794 FakeLspAdapter {
12795 capabilities: lsp::ServerCapabilities {
12796 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12797 ..Default::default()
12798 },
12799 ..Default::default()
12800 },
12801 );
12802
12803 let buffer = project
12804 .update(cx, |project, cx| {
12805 project.open_local_buffer(path!("/file.ts"), cx)
12806 })
12807 .await
12808 .unwrap();
12809
12810 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12811 let (editor, cx) = cx.add_window_view(|window, cx| {
12812 build_editor_with_project(project.clone(), buffer, window, cx)
12813 });
12814 editor.update_in(cx, |editor, window, cx| {
12815 editor.set_text(
12816 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12817 window,
12818 cx,
12819 )
12820 });
12821
12822 cx.executor().start_waiting();
12823 let fake_server = fake_servers.next().await.unwrap();
12824
12825 let format = editor
12826 .update_in(cx, |editor, window, cx| {
12827 editor.perform_code_action_kind(
12828 project.clone(),
12829 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12830 window,
12831 cx,
12832 )
12833 })
12834 .unwrap();
12835 fake_server
12836 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12837 assert_eq!(
12838 params.text_document.uri,
12839 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12840 );
12841 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12842 lsp::CodeAction {
12843 title: "Organize Imports".to_string(),
12844 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12845 edit: Some(lsp::WorkspaceEdit {
12846 changes: Some(
12847 [(
12848 params.text_document.uri.clone(),
12849 vec![lsp::TextEdit::new(
12850 lsp::Range::new(
12851 lsp::Position::new(1, 0),
12852 lsp::Position::new(2, 0),
12853 ),
12854 "".to_string(),
12855 )],
12856 )]
12857 .into_iter()
12858 .collect(),
12859 ),
12860 ..Default::default()
12861 }),
12862 ..Default::default()
12863 },
12864 )]))
12865 })
12866 .next()
12867 .await;
12868 cx.executor().start_waiting();
12869 format.await;
12870 assert_eq!(
12871 editor.update(cx, |editor, cx| editor.text(cx)),
12872 "import { a } from 'module';\n\nconst x = a;\n"
12873 );
12874
12875 editor.update_in(cx, |editor, window, cx| {
12876 editor.set_text(
12877 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12878 window,
12879 cx,
12880 )
12881 });
12882 // Ensure we don't lock if code action hangs.
12883 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12884 move |params, _| async move {
12885 assert_eq!(
12886 params.text_document.uri,
12887 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12888 );
12889 futures::future::pending::<()>().await;
12890 unreachable!()
12891 },
12892 );
12893 let format = editor
12894 .update_in(cx, |editor, window, cx| {
12895 editor.perform_code_action_kind(
12896 project,
12897 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12898 window,
12899 cx,
12900 )
12901 })
12902 .unwrap();
12903 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12904 cx.executor().start_waiting();
12905 format.await;
12906 assert_eq!(
12907 editor.update(cx, |editor, cx| editor.text(cx)),
12908 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12909 );
12910}
12911
12912#[gpui::test]
12913async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12914 init_test(cx, |_| {});
12915
12916 let mut cx = EditorLspTestContext::new_rust(
12917 lsp::ServerCapabilities {
12918 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12919 ..Default::default()
12920 },
12921 cx,
12922 )
12923 .await;
12924
12925 cx.set_state(indoc! {"
12926 one.twoˇ
12927 "});
12928
12929 // The format request takes a long time. When it completes, it inserts
12930 // a newline and an indent before the `.`
12931 cx.lsp
12932 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12933 let executor = cx.background_executor().clone();
12934 async move {
12935 executor.timer(Duration::from_millis(100)).await;
12936 Ok(Some(vec![lsp::TextEdit {
12937 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12938 new_text: "\n ".into(),
12939 }]))
12940 }
12941 });
12942
12943 // Submit a format request.
12944 let format_1 = cx
12945 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12946 .unwrap();
12947 cx.executor().run_until_parked();
12948
12949 // Submit a second format request.
12950 let format_2 = cx
12951 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12952 .unwrap();
12953 cx.executor().run_until_parked();
12954
12955 // Wait for both format requests to complete
12956 cx.executor().advance_clock(Duration::from_millis(200));
12957 cx.executor().start_waiting();
12958 format_1.await.unwrap();
12959 cx.executor().start_waiting();
12960 format_2.await.unwrap();
12961
12962 // The formatting edits only happens once.
12963 cx.assert_editor_state(indoc! {"
12964 one
12965 .twoˇ
12966 "});
12967}
12968
12969#[gpui::test]
12970async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12971 init_test(cx, |settings| {
12972 settings.defaults.formatter = Some(FormatterList::default())
12973 });
12974
12975 let mut cx = EditorLspTestContext::new_rust(
12976 lsp::ServerCapabilities {
12977 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12978 ..Default::default()
12979 },
12980 cx,
12981 )
12982 .await;
12983
12984 // Record which buffer changes have been sent to the language server
12985 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12986 cx.lsp
12987 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12988 let buffer_changes = buffer_changes.clone();
12989 move |params, _| {
12990 buffer_changes.lock().extend(
12991 params
12992 .content_changes
12993 .into_iter()
12994 .map(|e| (e.range.unwrap(), e.text)),
12995 );
12996 }
12997 });
12998 // Handle formatting requests to the language server.
12999 cx.lsp
13000 .set_request_handler::<lsp::request::Formatting, _, _>({
13001 let buffer_changes = buffer_changes.clone();
13002 move |_, _| {
13003 let buffer_changes = buffer_changes.clone();
13004 // Insert blank lines between each line of the buffer.
13005 async move {
13006 // When formatting is requested, trailing whitespace has already been stripped,
13007 // and the trailing newline has already been added.
13008 assert_eq!(
13009 &buffer_changes.lock()[1..],
13010 &[
13011 (
13012 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13013 "".into()
13014 ),
13015 (
13016 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13017 "".into()
13018 ),
13019 (
13020 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13021 "\n".into()
13022 ),
13023 ]
13024 );
13025
13026 Ok(Some(vec![
13027 lsp::TextEdit {
13028 range: lsp::Range::new(
13029 lsp::Position::new(1, 0),
13030 lsp::Position::new(1, 0),
13031 ),
13032 new_text: "\n".into(),
13033 },
13034 lsp::TextEdit {
13035 range: lsp::Range::new(
13036 lsp::Position::new(2, 0),
13037 lsp::Position::new(2, 0),
13038 ),
13039 new_text: "\n".into(),
13040 },
13041 ]))
13042 }
13043 }
13044 });
13045
13046 // Set up a buffer white some trailing whitespace and no trailing newline.
13047 cx.set_state(
13048 &[
13049 "one ", //
13050 "twoˇ", //
13051 "three ", //
13052 "four", //
13053 ]
13054 .join("\n"),
13055 );
13056 cx.run_until_parked();
13057
13058 // Submit a format request.
13059 let format = cx
13060 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13061 .unwrap();
13062
13063 cx.run_until_parked();
13064 // After formatting the buffer, the trailing whitespace is stripped,
13065 // a newline is appended, and the edits provided by the language server
13066 // have been applied.
13067 format.await.unwrap();
13068
13069 cx.assert_editor_state(
13070 &[
13071 "one", //
13072 "", //
13073 "twoˇ", //
13074 "", //
13075 "three", //
13076 "four", //
13077 "", //
13078 ]
13079 .join("\n"),
13080 );
13081
13082 // Undoing the formatting undoes the trailing whitespace removal, the
13083 // trailing newline, and the LSP edits.
13084 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13085 cx.assert_editor_state(
13086 &[
13087 "one ", //
13088 "twoˇ", //
13089 "three ", //
13090 "four", //
13091 ]
13092 .join("\n"),
13093 );
13094}
13095
13096#[gpui::test]
13097async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13098 cx: &mut TestAppContext,
13099) {
13100 init_test(cx, |_| {});
13101
13102 cx.update(|cx| {
13103 cx.update_global::<SettingsStore, _>(|settings, cx| {
13104 settings.update_user_settings(cx, |settings| {
13105 settings.editor.auto_signature_help = Some(true);
13106 });
13107 });
13108 });
13109
13110 let mut cx = EditorLspTestContext::new_rust(
13111 lsp::ServerCapabilities {
13112 signature_help_provider: Some(lsp::SignatureHelpOptions {
13113 ..Default::default()
13114 }),
13115 ..Default::default()
13116 },
13117 cx,
13118 )
13119 .await;
13120
13121 let language = Language::new(
13122 LanguageConfig {
13123 name: "Rust".into(),
13124 brackets: BracketPairConfig {
13125 pairs: vec![
13126 BracketPair {
13127 start: "{".to_string(),
13128 end: "}".to_string(),
13129 close: true,
13130 surround: true,
13131 newline: true,
13132 },
13133 BracketPair {
13134 start: "(".to_string(),
13135 end: ")".to_string(),
13136 close: true,
13137 surround: true,
13138 newline: true,
13139 },
13140 BracketPair {
13141 start: "/*".to_string(),
13142 end: " */".to_string(),
13143 close: true,
13144 surround: true,
13145 newline: true,
13146 },
13147 BracketPair {
13148 start: "[".to_string(),
13149 end: "]".to_string(),
13150 close: false,
13151 surround: false,
13152 newline: true,
13153 },
13154 BracketPair {
13155 start: "\"".to_string(),
13156 end: "\"".to_string(),
13157 close: true,
13158 surround: true,
13159 newline: false,
13160 },
13161 BracketPair {
13162 start: "<".to_string(),
13163 end: ">".to_string(),
13164 close: false,
13165 surround: true,
13166 newline: true,
13167 },
13168 ],
13169 ..Default::default()
13170 },
13171 autoclose_before: "})]".to_string(),
13172 ..Default::default()
13173 },
13174 Some(tree_sitter_rust::LANGUAGE.into()),
13175 );
13176 let language = Arc::new(language);
13177
13178 cx.language_registry().add(language.clone());
13179 cx.update_buffer(|buffer, cx| {
13180 buffer.set_language(Some(language), cx);
13181 });
13182
13183 cx.set_state(
13184 &r#"
13185 fn main() {
13186 sampleˇ
13187 }
13188 "#
13189 .unindent(),
13190 );
13191
13192 cx.update_editor(|editor, window, cx| {
13193 editor.handle_input("(", window, cx);
13194 });
13195 cx.assert_editor_state(
13196 &"
13197 fn main() {
13198 sample(ˇ)
13199 }
13200 "
13201 .unindent(),
13202 );
13203
13204 let mocked_response = lsp::SignatureHelp {
13205 signatures: vec![lsp::SignatureInformation {
13206 label: "fn sample(param1: u8, param2: u8)".to_string(),
13207 documentation: None,
13208 parameters: Some(vec![
13209 lsp::ParameterInformation {
13210 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13211 documentation: None,
13212 },
13213 lsp::ParameterInformation {
13214 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13215 documentation: None,
13216 },
13217 ]),
13218 active_parameter: None,
13219 }],
13220 active_signature: Some(0),
13221 active_parameter: Some(0),
13222 };
13223 handle_signature_help_request(&mut cx, mocked_response).await;
13224
13225 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13226 .await;
13227
13228 cx.editor(|editor, _, _| {
13229 let signature_help_state = editor.signature_help_state.popover().cloned();
13230 let signature = signature_help_state.unwrap();
13231 assert_eq!(
13232 signature.signatures[signature.current_signature].label,
13233 "fn sample(param1: u8, param2: u8)"
13234 );
13235 });
13236}
13237
13238#[gpui::test]
13239async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13240 init_test(cx, |_| {});
13241
13242 cx.update(|cx| {
13243 cx.update_global::<SettingsStore, _>(|settings, cx| {
13244 settings.update_user_settings(cx, |settings| {
13245 settings.editor.auto_signature_help = Some(false);
13246 settings.editor.show_signature_help_after_edits = Some(false);
13247 });
13248 });
13249 });
13250
13251 let mut cx = EditorLspTestContext::new_rust(
13252 lsp::ServerCapabilities {
13253 signature_help_provider: Some(lsp::SignatureHelpOptions {
13254 ..Default::default()
13255 }),
13256 ..Default::default()
13257 },
13258 cx,
13259 )
13260 .await;
13261
13262 let language = Language::new(
13263 LanguageConfig {
13264 name: "Rust".into(),
13265 brackets: BracketPairConfig {
13266 pairs: vec![
13267 BracketPair {
13268 start: "{".to_string(),
13269 end: "}".to_string(),
13270 close: true,
13271 surround: true,
13272 newline: true,
13273 },
13274 BracketPair {
13275 start: "(".to_string(),
13276 end: ")".to_string(),
13277 close: true,
13278 surround: true,
13279 newline: true,
13280 },
13281 BracketPair {
13282 start: "/*".to_string(),
13283 end: " */".to_string(),
13284 close: true,
13285 surround: true,
13286 newline: true,
13287 },
13288 BracketPair {
13289 start: "[".to_string(),
13290 end: "]".to_string(),
13291 close: false,
13292 surround: false,
13293 newline: true,
13294 },
13295 BracketPair {
13296 start: "\"".to_string(),
13297 end: "\"".to_string(),
13298 close: true,
13299 surround: true,
13300 newline: false,
13301 },
13302 BracketPair {
13303 start: "<".to_string(),
13304 end: ">".to_string(),
13305 close: false,
13306 surround: true,
13307 newline: true,
13308 },
13309 ],
13310 ..Default::default()
13311 },
13312 autoclose_before: "})]".to_string(),
13313 ..Default::default()
13314 },
13315 Some(tree_sitter_rust::LANGUAGE.into()),
13316 );
13317 let language = Arc::new(language);
13318
13319 cx.language_registry().add(language.clone());
13320 cx.update_buffer(|buffer, cx| {
13321 buffer.set_language(Some(language), cx);
13322 });
13323
13324 // Ensure that signature_help is not called when no signature help is enabled.
13325 cx.set_state(
13326 &r#"
13327 fn main() {
13328 sampleˇ
13329 }
13330 "#
13331 .unindent(),
13332 );
13333 cx.update_editor(|editor, window, cx| {
13334 editor.handle_input("(", window, cx);
13335 });
13336 cx.assert_editor_state(
13337 &"
13338 fn main() {
13339 sample(ˇ)
13340 }
13341 "
13342 .unindent(),
13343 );
13344 cx.editor(|editor, _, _| {
13345 assert!(editor.signature_help_state.task().is_none());
13346 });
13347
13348 let mocked_response = lsp::SignatureHelp {
13349 signatures: vec![lsp::SignatureInformation {
13350 label: "fn sample(param1: u8, param2: u8)".to_string(),
13351 documentation: None,
13352 parameters: Some(vec![
13353 lsp::ParameterInformation {
13354 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13355 documentation: None,
13356 },
13357 lsp::ParameterInformation {
13358 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13359 documentation: None,
13360 },
13361 ]),
13362 active_parameter: None,
13363 }],
13364 active_signature: Some(0),
13365 active_parameter: Some(0),
13366 };
13367
13368 // Ensure that signature_help is called when enabled afte edits
13369 cx.update(|_, cx| {
13370 cx.update_global::<SettingsStore, _>(|settings, cx| {
13371 settings.update_user_settings(cx, |settings| {
13372 settings.editor.auto_signature_help = Some(false);
13373 settings.editor.show_signature_help_after_edits = Some(true);
13374 });
13375 });
13376 });
13377 cx.set_state(
13378 &r#"
13379 fn main() {
13380 sampleˇ
13381 }
13382 "#
13383 .unindent(),
13384 );
13385 cx.update_editor(|editor, window, cx| {
13386 editor.handle_input("(", window, cx);
13387 });
13388 cx.assert_editor_state(
13389 &"
13390 fn main() {
13391 sample(ˇ)
13392 }
13393 "
13394 .unindent(),
13395 );
13396 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13397 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13398 .await;
13399 cx.update_editor(|editor, _, _| {
13400 let signature_help_state = editor.signature_help_state.popover().cloned();
13401 assert!(signature_help_state.is_some());
13402 let signature = signature_help_state.unwrap();
13403 assert_eq!(
13404 signature.signatures[signature.current_signature].label,
13405 "fn sample(param1: u8, param2: u8)"
13406 );
13407 editor.signature_help_state = SignatureHelpState::default();
13408 });
13409
13410 // Ensure that signature_help is called when auto signature help override is enabled
13411 cx.update(|_, cx| {
13412 cx.update_global::<SettingsStore, _>(|settings, cx| {
13413 settings.update_user_settings(cx, |settings| {
13414 settings.editor.auto_signature_help = Some(true);
13415 settings.editor.show_signature_help_after_edits = Some(false);
13416 });
13417 });
13418 });
13419 cx.set_state(
13420 &r#"
13421 fn main() {
13422 sampleˇ
13423 }
13424 "#
13425 .unindent(),
13426 );
13427 cx.update_editor(|editor, window, cx| {
13428 editor.handle_input("(", window, cx);
13429 });
13430 cx.assert_editor_state(
13431 &"
13432 fn main() {
13433 sample(ˇ)
13434 }
13435 "
13436 .unindent(),
13437 );
13438 handle_signature_help_request(&mut cx, mocked_response).await;
13439 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13440 .await;
13441 cx.editor(|editor, _, _| {
13442 let signature_help_state = editor.signature_help_state.popover().cloned();
13443 assert!(signature_help_state.is_some());
13444 let signature = signature_help_state.unwrap();
13445 assert_eq!(
13446 signature.signatures[signature.current_signature].label,
13447 "fn sample(param1: u8, param2: u8)"
13448 );
13449 });
13450}
13451
13452#[gpui::test]
13453async fn test_signature_help(cx: &mut TestAppContext) {
13454 init_test(cx, |_| {});
13455 cx.update(|cx| {
13456 cx.update_global::<SettingsStore, _>(|settings, cx| {
13457 settings.update_user_settings(cx, |settings| {
13458 settings.editor.auto_signature_help = Some(true);
13459 });
13460 });
13461 });
13462
13463 let mut cx = EditorLspTestContext::new_rust(
13464 lsp::ServerCapabilities {
13465 signature_help_provider: Some(lsp::SignatureHelpOptions {
13466 ..Default::default()
13467 }),
13468 ..Default::default()
13469 },
13470 cx,
13471 )
13472 .await;
13473
13474 // A test that directly calls `show_signature_help`
13475 cx.update_editor(|editor, window, cx| {
13476 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13477 });
13478
13479 let mocked_response = lsp::SignatureHelp {
13480 signatures: vec![lsp::SignatureInformation {
13481 label: "fn sample(param1: u8, param2: u8)".to_string(),
13482 documentation: None,
13483 parameters: Some(vec![
13484 lsp::ParameterInformation {
13485 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13486 documentation: None,
13487 },
13488 lsp::ParameterInformation {
13489 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13490 documentation: None,
13491 },
13492 ]),
13493 active_parameter: None,
13494 }],
13495 active_signature: Some(0),
13496 active_parameter: Some(0),
13497 };
13498 handle_signature_help_request(&mut cx, mocked_response).await;
13499
13500 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13501 .await;
13502
13503 cx.editor(|editor, _, _| {
13504 let signature_help_state = editor.signature_help_state.popover().cloned();
13505 assert!(signature_help_state.is_some());
13506 let signature = signature_help_state.unwrap();
13507 assert_eq!(
13508 signature.signatures[signature.current_signature].label,
13509 "fn sample(param1: u8, param2: u8)"
13510 );
13511 });
13512
13513 // When exiting outside from inside the brackets, `signature_help` is closed.
13514 cx.set_state(indoc! {"
13515 fn main() {
13516 sample(ˇ);
13517 }
13518
13519 fn sample(param1: u8, param2: u8) {}
13520 "});
13521
13522 cx.update_editor(|editor, window, cx| {
13523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13524 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13525 });
13526 });
13527
13528 let mocked_response = lsp::SignatureHelp {
13529 signatures: Vec::new(),
13530 active_signature: None,
13531 active_parameter: None,
13532 };
13533 handle_signature_help_request(&mut cx, mocked_response).await;
13534
13535 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13536 .await;
13537
13538 cx.editor(|editor, _, _| {
13539 assert!(!editor.signature_help_state.is_shown());
13540 });
13541
13542 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13543 cx.set_state(indoc! {"
13544 fn main() {
13545 sample(ˇ);
13546 }
13547
13548 fn sample(param1: u8, param2: u8) {}
13549 "});
13550
13551 let mocked_response = lsp::SignatureHelp {
13552 signatures: vec![lsp::SignatureInformation {
13553 label: "fn sample(param1: u8, param2: u8)".to_string(),
13554 documentation: None,
13555 parameters: Some(vec![
13556 lsp::ParameterInformation {
13557 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13558 documentation: None,
13559 },
13560 lsp::ParameterInformation {
13561 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13562 documentation: None,
13563 },
13564 ]),
13565 active_parameter: None,
13566 }],
13567 active_signature: Some(0),
13568 active_parameter: Some(0),
13569 };
13570 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13571 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13572 .await;
13573 cx.editor(|editor, _, _| {
13574 assert!(editor.signature_help_state.is_shown());
13575 });
13576
13577 // Restore the popover with more parameter input
13578 cx.set_state(indoc! {"
13579 fn main() {
13580 sample(param1, param2ˇ);
13581 }
13582
13583 fn sample(param1: u8, param2: u8) {}
13584 "});
13585
13586 let mocked_response = lsp::SignatureHelp {
13587 signatures: vec![lsp::SignatureInformation {
13588 label: "fn sample(param1: u8, param2: u8)".to_string(),
13589 documentation: None,
13590 parameters: Some(vec![
13591 lsp::ParameterInformation {
13592 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13593 documentation: None,
13594 },
13595 lsp::ParameterInformation {
13596 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13597 documentation: None,
13598 },
13599 ]),
13600 active_parameter: None,
13601 }],
13602 active_signature: Some(0),
13603 active_parameter: Some(1),
13604 };
13605 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13606 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13607 .await;
13608
13609 // When selecting a range, the popover is gone.
13610 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13611 cx.update_editor(|editor, window, cx| {
13612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13613 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13614 })
13615 });
13616 cx.assert_editor_state(indoc! {"
13617 fn main() {
13618 sample(param1, «ˇparam2»);
13619 }
13620
13621 fn sample(param1: u8, param2: u8) {}
13622 "});
13623 cx.editor(|editor, _, _| {
13624 assert!(!editor.signature_help_state.is_shown());
13625 });
13626
13627 // When unselecting again, the popover is back if within the brackets.
13628 cx.update_editor(|editor, window, cx| {
13629 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13630 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13631 })
13632 });
13633 cx.assert_editor_state(indoc! {"
13634 fn main() {
13635 sample(param1, ˇparam2);
13636 }
13637
13638 fn sample(param1: u8, param2: u8) {}
13639 "});
13640 handle_signature_help_request(&mut cx, mocked_response).await;
13641 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13642 .await;
13643 cx.editor(|editor, _, _| {
13644 assert!(editor.signature_help_state.is_shown());
13645 });
13646
13647 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13648 cx.update_editor(|editor, window, cx| {
13649 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13650 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13651 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13652 })
13653 });
13654 cx.assert_editor_state(indoc! {"
13655 fn main() {
13656 sample(param1, ˇparam2);
13657 }
13658
13659 fn sample(param1: u8, param2: u8) {}
13660 "});
13661
13662 let mocked_response = lsp::SignatureHelp {
13663 signatures: vec![lsp::SignatureInformation {
13664 label: "fn sample(param1: u8, param2: u8)".to_string(),
13665 documentation: None,
13666 parameters: Some(vec![
13667 lsp::ParameterInformation {
13668 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13669 documentation: None,
13670 },
13671 lsp::ParameterInformation {
13672 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13673 documentation: None,
13674 },
13675 ]),
13676 active_parameter: None,
13677 }],
13678 active_signature: Some(0),
13679 active_parameter: Some(1),
13680 };
13681 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13682 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13683 .await;
13684 cx.update_editor(|editor, _, cx| {
13685 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13686 });
13687 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13688 .await;
13689 cx.update_editor(|editor, window, cx| {
13690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13691 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13692 })
13693 });
13694 cx.assert_editor_state(indoc! {"
13695 fn main() {
13696 sample(param1, «ˇparam2»);
13697 }
13698
13699 fn sample(param1: u8, param2: u8) {}
13700 "});
13701 cx.update_editor(|editor, window, cx| {
13702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13703 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13704 })
13705 });
13706 cx.assert_editor_state(indoc! {"
13707 fn main() {
13708 sample(param1, ˇparam2);
13709 }
13710
13711 fn sample(param1: u8, param2: u8) {}
13712 "});
13713 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13714 .await;
13715}
13716
13717#[gpui::test]
13718async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13719 init_test(cx, |_| {});
13720
13721 let mut cx = EditorLspTestContext::new_rust(
13722 lsp::ServerCapabilities {
13723 signature_help_provider: Some(lsp::SignatureHelpOptions {
13724 ..Default::default()
13725 }),
13726 ..Default::default()
13727 },
13728 cx,
13729 )
13730 .await;
13731
13732 cx.set_state(indoc! {"
13733 fn main() {
13734 overloadedˇ
13735 }
13736 "});
13737
13738 cx.update_editor(|editor, window, cx| {
13739 editor.handle_input("(", window, cx);
13740 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13741 });
13742
13743 // Mock response with 3 signatures
13744 let mocked_response = lsp::SignatureHelp {
13745 signatures: vec![
13746 lsp::SignatureInformation {
13747 label: "fn overloaded(x: i32)".to_string(),
13748 documentation: None,
13749 parameters: Some(vec![lsp::ParameterInformation {
13750 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13751 documentation: None,
13752 }]),
13753 active_parameter: None,
13754 },
13755 lsp::SignatureInformation {
13756 label: "fn overloaded(x: i32, y: i32)".to_string(),
13757 documentation: None,
13758 parameters: Some(vec![
13759 lsp::ParameterInformation {
13760 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13761 documentation: None,
13762 },
13763 lsp::ParameterInformation {
13764 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13765 documentation: None,
13766 },
13767 ]),
13768 active_parameter: None,
13769 },
13770 lsp::SignatureInformation {
13771 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13772 documentation: None,
13773 parameters: Some(vec![
13774 lsp::ParameterInformation {
13775 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13776 documentation: None,
13777 },
13778 lsp::ParameterInformation {
13779 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13780 documentation: None,
13781 },
13782 lsp::ParameterInformation {
13783 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13784 documentation: None,
13785 },
13786 ]),
13787 active_parameter: None,
13788 },
13789 ],
13790 active_signature: Some(1),
13791 active_parameter: Some(0),
13792 };
13793 handle_signature_help_request(&mut cx, mocked_response).await;
13794
13795 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13796 .await;
13797
13798 // Verify we have multiple signatures and the right one is selected
13799 cx.editor(|editor, _, _| {
13800 let popover = editor.signature_help_state.popover().cloned().unwrap();
13801 assert_eq!(popover.signatures.len(), 3);
13802 // active_signature was 1, so that should be the current
13803 assert_eq!(popover.current_signature, 1);
13804 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13805 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13806 assert_eq!(
13807 popover.signatures[2].label,
13808 "fn overloaded(x: i32, y: i32, z: i32)"
13809 );
13810 });
13811
13812 // Test navigation functionality
13813 cx.update_editor(|editor, window, cx| {
13814 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13815 });
13816
13817 cx.editor(|editor, _, _| {
13818 let popover = editor.signature_help_state.popover().cloned().unwrap();
13819 assert_eq!(popover.current_signature, 2);
13820 });
13821
13822 // Test wrap around
13823 cx.update_editor(|editor, window, cx| {
13824 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13825 });
13826
13827 cx.editor(|editor, _, _| {
13828 let popover = editor.signature_help_state.popover().cloned().unwrap();
13829 assert_eq!(popover.current_signature, 0);
13830 });
13831
13832 // Test previous navigation
13833 cx.update_editor(|editor, window, cx| {
13834 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13835 });
13836
13837 cx.editor(|editor, _, _| {
13838 let popover = editor.signature_help_state.popover().cloned().unwrap();
13839 assert_eq!(popover.current_signature, 2);
13840 });
13841}
13842
13843#[gpui::test]
13844async fn test_completion_mode(cx: &mut TestAppContext) {
13845 init_test(cx, |_| {});
13846 let mut cx = EditorLspTestContext::new_rust(
13847 lsp::ServerCapabilities {
13848 completion_provider: Some(lsp::CompletionOptions {
13849 resolve_provider: Some(true),
13850 ..Default::default()
13851 }),
13852 ..Default::default()
13853 },
13854 cx,
13855 )
13856 .await;
13857
13858 struct Run {
13859 run_description: &'static str,
13860 initial_state: String,
13861 buffer_marked_text: String,
13862 completion_label: &'static str,
13863 completion_text: &'static str,
13864 expected_with_insert_mode: String,
13865 expected_with_replace_mode: String,
13866 expected_with_replace_subsequence_mode: String,
13867 expected_with_replace_suffix_mode: String,
13868 }
13869
13870 let runs = [
13871 Run {
13872 run_description: "Start of word matches completion text",
13873 initial_state: "before ediˇ after".into(),
13874 buffer_marked_text: "before <edi|> after".into(),
13875 completion_label: "editor",
13876 completion_text: "editor",
13877 expected_with_insert_mode: "before editorˇ after".into(),
13878 expected_with_replace_mode: "before editorˇ after".into(),
13879 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13880 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13881 },
13882 Run {
13883 run_description: "Accept same text at the middle of the word",
13884 initial_state: "before ediˇtor after".into(),
13885 buffer_marked_text: "before <edi|tor> after".into(),
13886 completion_label: "editor",
13887 completion_text: "editor",
13888 expected_with_insert_mode: "before editorˇtor after".into(),
13889 expected_with_replace_mode: "before editorˇ after".into(),
13890 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13891 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13892 },
13893 Run {
13894 run_description: "End of word matches completion text -- cursor at end",
13895 initial_state: "before torˇ after".into(),
13896 buffer_marked_text: "before <tor|> after".into(),
13897 completion_label: "editor",
13898 completion_text: "editor",
13899 expected_with_insert_mode: "before editorˇ after".into(),
13900 expected_with_replace_mode: "before editorˇ after".into(),
13901 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13902 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13903 },
13904 Run {
13905 run_description: "End of word matches completion text -- cursor at start",
13906 initial_state: "before ˇtor after".into(),
13907 buffer_marked_text: "before <|tor> after".into(),
13908 completion_label: "editor",
13909 completion_text: "editor",
13910 expected_with_insert_mode: "before editorˇtor after".into(),
13911 expected_with_replace_mode: "before editorˇ after".into(),
13912 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13913 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13914 },
13915 Run {
13916 run_description: "Prepend text containing whitespace",
13917 initial_state: "pˇfield: bool".into(),
13918 buffer_marked_text: "<p|field>: bool".into(),
13919 completion_label: "pub ",
13920 completion_text: "pub ",
13921 expected_with_insert_mode: "pub ˇfield: bool".into(),
13922 expected_with_replace_mode: "pub ˇ: bool".into(),
13923 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13924 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13925 },
13926 Run {
13927 run_description: "Add element to start of list",
13928 initial_state: "[element_ˇelement_2]".into(),
13929 buffer_marked_text: "[<element_|element_2>]".into(),
13930 completion_label: "element_1",
13931 completion_text: "element_1",
13932 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13933 expected_with_replace_mode: "[element_1ˇ]".into(),
13934 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13935 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13936 },
13937 Run {
13938 run_description: "Add element to start of list -- first and second elements are equal",
13939 initial_state: "[elˇelement]".into(),
13940 buffer_marked_text: "[<el|element>]".into(),
13941 completion_label: "element",
13942 completion_text: "element",
13943 expected_with_insert_mode: "[elementˇelement]".into(),
13944 expected_with_replace_mode: "[elementˇ]".into(),
13945 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13946 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13947 },
13948 Run {
13949 run_description: "Ends with matching suffix",
13950 initial_state: "SubˇError".into(),
13951 buffer_marked_text: "<Sub|Error>".into(),
13952 completion_label: "SubscriptionError",
13953 completion_text: "SubscriptionError",
13954 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13955 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13956 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13957 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13958 },
13959 Run {
13960 run_description: "Suffix is a subsequence -- contiguous",
13961 initial_state: "SubˇErr".into(),
13962 buffer_marked_text: "<Sub|Err>".into(),
13963 completion_label: "SubscriptionError",
13964 completion_text: "SubscriptionError",
13965 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13966 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13967 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13968 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13969 },
13970 Run {
13971 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13972 initial_state: "Suˇscrirr".into(),
13973 buffer_marked_text: "<Su|scrirr>".into(),
13974 completion_label: "SubscriptionError",
13975 completion_text: "SubscriptionError",
13976 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13977 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13978 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13979 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13980 },
13981 Run {
13982 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13983 initial_state: "foo(indˇix)".into(),
13984 buffer_marked_text: "foo(<ind|ix>)".into(),
13985 completion_label: "node_index",
13986 completion_text: "node_index",
13987 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13988 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13989 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13990 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13991 },
13992 Run {
13993 run_description: "Replace range ends before cursor - should extend to cursor",
13994 initial_state: "before editˇo after".into(),
13995 buffer_marked_text: "before <{ed}>it|o after".into(),
13996 completion_label: "editor",
13997 completion_text: "editor",
13998 expected_with_insert_mode: "before editorˇo after".into(),
13999 expected_with_replace_mode: "before editorˇo after".into(),
14000 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14001 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14002 },
14003 Run {
14004 run_description: "Uses label for suffix matching",
14005 initial_state: "before ediˇtor after".into(),
14006 buffer_marked_text: "before <edi|tor> after".into(),
14007 completion_label: "editor",
14008 completion_text: "editor()",
14009 expected_with_insert_mode: "before editor()ˇtor after".into(),
14010 expected_with_replace_mode: "before editor()ˇ after".into(),
14011 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14012 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14013 },
14014 Run {
14015 run_description: "Case insensitive subsequence and suffix matching",
14016 initial_state: "before EDiˇtoR after".into(),
14017 buffer_marked_text: "before <EDi|toR> after".into(),
14018 completion_label: "editor",
14019 completion_text: "editor",
14020 expected_with_insert_mode: "before editorˇtoR after".into(),
14021 expected_with_replace_mode: "before editorˇ after".into(),
14022 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14023 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14024 },
14025 ];
14026
14027 for run in runs {
14028 let run_variations = [
14029 (LspInsertMode::Insert, run.expected_with_insert_mode),
14030 (LspInsertMode::Replace, run.expected_with_replace_mode),
14031 (
14032 LspInsertMode::ReplaceSubsequence,
14033 run.expected_with_replace_subsequence_mode,
14034 ),
14035 (
14036 LspInsertMode::ReplaceSuffix,
14037 run.expected_with_replace_suffix_mode,
14038 ),
14039 ];
14040
14041 for (lsp_insert_mode, expected_text) in run_variations {
14042 eprintln!(
14043 "run = {:?}, mode = {lsp_insert_mode:.?}",
14044 run.run_description,
14045 );
14046
14047 update_test_language_settings(&mut cx, |settings| {
14048 settings.defaults.completions = Some(CompletionSettingsContent {
14049 lsp_insert_mode: Some(lsp_insert_mode),
14050 words: Some(WordsCompletionMode::Disabled),
14051 words_min_length: Some(0),
14052 ..Default::default()
14053 });
14054 });
14055
14056 cx.set_state(&run.initial_state);
14057 cx.update_editor(|editor, window, cx| {
14058 editor.show_completions(&ShowCompletions, window, cx);
14059 });
14060
14061 let counter = Arc::new(AtomicUsize::new(0));
14062 handle_completion_request_with_insert_and_replace(
14063 &mut cx,
14064 &run.buffer_marked_text,
14065 vec![(run.completion_label, run.completion_text)],
14066 counter.clone(),
14067 )
14068 .await;
14069 cx.condition(|editor, _| editor.context_menu_visible())
14070 .await;
14071 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14072
14073 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14074 editor
14075 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14076 .unwrap()
14077 });
14078 cx.assert_editor_state(&expected_text);
14079 handle_resolve_completion_request(&mut cx, None).await;
14080 apply_additional_edits.await.unwrap();
14081 }
14082 }
14083}
14084
14085#[gpui::test]
14086async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14087 init_test(cx, |_| {});
14088 let mut cx = EditorLspTestContext::new_rust(
14089 lsp::ServerCapabilities {
14090 completion_provider: Some(lsp::CompletionOptions {
14091 resolve_provider: Some(true),
14092 ..Default::default()
14093 }),
14094 ..Default::default()
14095 },
14096 cx,
14097 )
14098 .await;
14099
14100 let initial_state = "SubˇError";
14101 let buffer_marked_text = "<Sub|Error>";
14102 let completion_text = "SubscriptionError";
14103 let expected_with_insert_mode = "SubscriptionErrorˇError";
14104 let expected_with_replace_mode = "SubscriptionErrorˇ";
14105
14106 update_test_language_settings(&mut cx, |settings| {
14107 settings.defaults.completions = Some(CompletionSettingsContent {
14108 words: Some(WordsCompletionMode::Disabled),
14109 words_min_length: Some(0),
14110 // set the opposite here to ensure that the action is overriding the default behavior
14111 lsp_insert_mode: Some(LspInsertMode::Insert),
14112 ..Default::default()
14113 });
14114 });
14115
14116 cx.set_state(initial_state);
14117 cx.update_editor(|editor, window, cx| {
14118 editor.show_completions(&ShowCompletions, window, cx);
14119 });
14120
14121 let counter = Arc::new(AtomicUsize::new(0));
14122 handle_completion_request_with_insert_and_replace(
14123 &mut cx,
14124 buffer_marked_text,
14125 vec![(completion_text, completion_text)],
14126 counter.clone(),
14127 )
14128 .await;
14129 cx.condition(|editor, _| editor.context_menu_visible())
14130 .await;
14131 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14132
14133 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14134 editor
14135 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14136 .unwrap()
14137 });
14138 cx.assert_editor_state(expected_with_replace_mode);
14139 handle_resolve_completion_request(&mut cx, None).await;
14140 apply_additional_edits.await.unwrap();
14141
14142 update_test_language_settings(&mut cx, |settings| {
14143 settings.defaults.completions = Some(CompletionSettingsContent {
14144 words: Some(WordsCompletionMode::Disabled),
14145 words_min_length: Some(0),
14146 // set the opposite here to ensure that the action is overriding the default behavior
14147 lsp_insert_mode: Some(LspInsertMode::Replace),
14148 ..Default::default()
14149 });
14150 });
14151
14152 cx.set_state(initial_state);
14153 cx.update_editor(|editor, window, cx| {
14154 editor.show_completions(&ShowCompletions, window, cx);
14155 });
14156 handle_completion_request_with_insert_and_replace(
14157 &mut cx,
14158 buffer_marked_text,
14159 vec![(completion_text, completion_text)],
14160 counter.clone(),
14161 )
14162 .await;
14163 cx.condition(|editor, _| editor.context_menu_visible())
14164 .await;
14165 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14166
14167 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14168 editor
14169 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14170 .unwrap()
14171 });
14172 cx.assert_editor_state(expected_with_insert_mode);
14173 handle_resolve_completion_request(&mut cx, None).await;
14174 apply_additional_edits.await.unwrap();
14175}
14176
14177#[gpui::test]
14178async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14179 init_test(cx, |_| {});
14180 let mut cx = EditorLspTestContext::new_rust(
14181 lsp::ServerCapabilities {
14182 completion_provider: Some(lsp::CompletionOptions {
14183 resolve_provider: Some(true),
14184 ..Default::default()
14185 }),
14186 ..Default::default()
14187 },
14188 cx,
14189 )
14190 .await;
14191
14192 // scenario: surrounding text matches completion text
14193 let completion_text = "to_offset";
14194 let initial_state = indoc! {"
14195 1. buf.to_offˇsuffix
14196 2. buf.to_offˇsuf
14197 3. buf.to_offˇfix
14198 4. buf.to_offˇ
14199 5. into_offˇensive
14200 6. ˇsuffix
14201 7. let ˇ //
14202 8. aaˇzz
14203 9. buf.to_off«zzzzzˇ»suffix
14204 10. buf.«ˇzzzzz»suffix
14205 11. to_off«ˇzzzzz»
14206
14207 buf.to_offˇsuffix // newest cursor
14208 "};
14209 let completion_marked_buffer = indoc! {"
14210 1. buf.to_offsuffix
14211 2. buf.to_offsuf
14212 3. buf.to_offfix
14213 4. buf.to_off
14214 5. into_offensive
14215 6. suffix
14216 7. let //
14217 8. aazz
14218 9. buf.to_offzzzzzsuffix
14219 10. buf.zzzzzsuffix
14220 11. to_offzzzzz
14221
14222 buf.<to_off|suffix> // newest cursor
14223 "};
14224 let expected = indoc! {"
14225 1. buf.to_offsetˇ
14226 2. buf.to_offsetˇsuf
14227 3. buf.to_offsetˇfix
14228 4. buf.to_offsetˇ
14229 5. into_offsetˇensive
14230 6. to_offsetˇsuffix
14231 7. let to_offsetˇ //
14232 8. aato_offsetˇzz
14233 9. buf.to_offsetˇ
14234 10. buf.to_offsetˇsuffix
14235 11. to_offsetˇ
14236
14237 buf.to_offsetˇ // newest cursor
14238 "};
14239 cx.set_state(initial_state);
14240 cx.update_editor(|editor, window, cx| {
14241 editor.show_completions(&ShowCompletions, window, cx);
14242 });
14243 handle_completion_request_with_insert_and_replace(
14244 &mut cx,
14245 completion_marked_buffer,
14246 vec![(completion_text, completion_text)],
14247 Arc::new(AtomicUsize::new(0)),
14248 )
14249 .await;
14250 cx.condition(|editor, _| editor.context_menu_visible())
14251 .await;
14252 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14253 editor
14254 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14255 .unwrap()
14256 });
14257 cx.assert_editor_state(expected);
14258 handle_resolve_completion_request(&mut cx, None).await;
14259 apply_additional_edits.await.unwrap();
14260
14261 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14262 let completion_text = "foo_and_bar";
14263 let initial_state = indoc! {"
14264 1. ooanbˇ
14265 2. zooanbˇ
14266 3. ooanbˇz
14267 4. zooanbˇz
14268 5. ooanˇ
14269 6. oanbˇ
14270
14271 ooanbˇ
14272 "};
14273 let completion_marked_buffer = indoc! {"
14274 1. ooanb
14275 2. zooanb
14276 3. ooanbz
14277 4. zooanbz
14278 5. ooan
14279 6. oanb
14280
14281 <ooanb|>
14282 "};
14283 let expected = indoc! {"
14284 1. foo_and_barˇ
14285 2. zfoo_and_barˇ
14286 3. foo_and_barˇz
14287 4. zfoo_and_barˇz
14288 5. ooanfoo_and_barˇ
14289 6. oanbfoo_and_barˇ
14290
14291 foo_and_barˇ
14292 "};
14293 cx.set_state(initial_state);
14294 cx.update_editor(|editor, window, cx| {
14295 editor.show_completions(&ShowCompletions, window, cx);
14296 });
14297 handle_completion_request_with_insert_and_replace(
14298 &mut cx,
14299 completion_marked_buffer,
14300 vec![(completion_text, completion_text)],
14301 Arc::new(AtomicUsize::new(0)),
14302 )
14303 .await;
14304 cx.condition(|editor, _| editor.context_menu_visible())
14305 .await;
14306 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14307 editor
14308 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14309 .unwrap()
14310 });
14311 cx.assert_editor_state(expected);
14312 handle_resolve_completion_request(&mut cx, None).await;
14313 apply_additional_edits.await.unwrap();
14314
14315 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14316 // (expects the same as if it was inserted at the end)
14317 let completion_text = "foo_and_bar";
14318 let initial_state = indoc! {"
14319 1. ooˇanb
14320 2. zooˇanb
14321 3. ooˇanbz
14322 4. zooˇanbz
14323
14324 ooˇanb
14325 "};
14326 let completion_marked_buffer = indoc! {"
14327 1. ooanb
14328 2. zooanb
14329 3. ooanbz
14330 4. zooanbz
14331
14332 <oo|anb>
14333 "};
14334 let expected = indoc! {"
14335 1. foo_and_barˇ
14336 2. zfoo_and_barˇ
14337 3. foo_and_barˇz
14338 4. zfoo_and_barˇz
14339
14340 foo_and_barˇ
14341 "};
14342 cx.set_state(initial_state);
14343 cx.update_editor(|editor, window, cx| {
14344 editor.show_completions(&ShowCompletions, window, cx);
14345 });
14346 handle_completion_request_with_insert_and_replace(
14347 &mut cx,
14348 completion_marked_buffer,
14349 vec![(completion_text, completion_text)],
14350 Arc::new(AtomicUsize::new(0)),
14351 )
14352 .await;
14353 cx.condition(|editor, _| editor.context_menu_visible())
14354 .await;
14355 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14356 editor
14357 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14358 .unwrap()
14359 });
14360 cx.assert_editor_state(expected);
14361 handle_resolve_completion_request(&mut cx, None).await;
14362 apply_additional_edits.await.unwrap();
14363}
14364
14365// This used to crash
14366#[gpui::test]
14367async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14368 init_test(cx, |_| {});
14369
14370 let buffer_text = indoc! {"
14371 fn main() {
14372 10.satu;
14373
14374 //
14375 // separate cursors so they open in different excerpts (manually reproducible)
14376 //
14377
14378 10.satu20;
14379 }
14380 "};
14381 let multibuffer_text_with_selections = indoc! {"
14382 fn main() {
14383 10.satuˇ;
14384
14385 //
14386
14387 //
14388
14389 10.satuˇ20;
14390 }
14391 "};
14392 let expected_multibuffer = indoc! {"
14393 fn main() {
14394 10.saturating_sub()ˇ;
14395
14396 //
14397
14398 //
14399
14400 10.saturating_sub()ˇ;
14401 }
14402 "};
14403
14404 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14405 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14406
14407 let fs = FakeFs::new(cx.executor());
14408 fs.insert_tree(
14409 path!("/a"),
14410 json!({
14411 "main.rs": buffer_text,
14412 }),
14413 )
14414 .await;
14415
14416 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14417 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14418 language_registry.add(rust_lang());
14419 let mut fake_servers = language_registry.register_fake_lsp(
14420 "Rust",
14421 FakeLspAdapter {
14422 capabilities: lsp::ServerCapabilities {
14423 completion_provider: Some(lsp::CompletionOptions {
14424 resolve_provider: None,
14425 ..lsp::CompletionOptions::default()
14426 }),
14427 ..lsp::ServerCapabilities::default()
14428 },
14429 ..FakeLspAdapter::default()
14430 },
14431 );
14432 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14433 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14434 let buffer = project
14435 .update(cx, |project, cx| {
14436 project.open_local_buffer(path!("/a/main.rs"), cx)
14437 })
14438 .await
14439 .unwrap();
14440
14441 let multi_buffer = cx.new(|cx| {
14442 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14443 multi_buffer.push_excerpts(
14444 buffer.clone(),
14445 [ExcerptRange::new(0..first_excerpt_end)],
14446 cx,
14447 );
14448 multi_buffer.push_excerpts(
14449 buffer.clone(),
14450 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14451 cx,
14452 );
14453 multi_buffer
14454 });
14455
14456 let editor = workspace
14457 .update(cx, |_, window, cx| {
14458 cx.new(|cx| {
14459 Editor::new(
14460 EditorMode::Full {
14461 scale_ui_elements_with_buffer_font_size: false,
14462 show_active_line_background: false,
14463 sizing_behavior: SizingBehavior::Default,
14464 },
14465 multi_buffer.clone(),
14466 Some(project.clone()),
14467 window,
14468 cx,
14469 )
14470 })
14471 })
14472 .unwrap();
14473
14474 let pane = workspace
14475 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14476 .unwrap();
14477 pane.update_in(cx, |pane, window, cx| {
14478 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14479 });
14480
14481 let fake_server = fake_servers.next().await.unwrap();
14482
14483 editor.update_in(cx, |editor, window, cx| {
14484 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14485 s.select_ranges([
14486 Point::new(1, 11)..Point::new(1, 11),
14487 Point::new(7, 11)..Point::new(7, 11),
14488 ])
14489 });
14490
14491 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14492 });
14493
14494 editor.update_in(cx, |editor, window, cx| {
14495 editor.show_completions(&ShowCompletions, window, cx);
14496 });
14497
14498 fake_server
14499 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14500 let completion_item = lsp::CompletionItem {
14501 label: "saturating_sub()".into(),
14502 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14503 lsp::InsertReplaceEdit {
14504 new_text: "saturating_sub()".to_owned(),
14505 insert: lsp::Range::new(
14506 lsp::Position::new(7, 7),
14507 lsp::Position::new(7, 11),
14508 ),
14509 replace: lsp::Range::new(
14510 lsp::Position::new(7, 7),
14511 lsp::Position::new(7, 13),
14512 ),
14513 },
14514 )),
14515 ..lsp::CompletionItem::default()
14516 };
14517
14518 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14519 })
14520 .next()
14521 .await
14522 .unwrap();
14523
14524 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14525 .await;
14526
14527 editor
14528 .update_in(cx, |editor, window, cx| {
14529 editor
14530 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14531 .unwrap()
14532 })
14533 .await
14534 .unwrap();
14535
14536 editor.update(cx, |editor, cx| {
14537 assert_text_with_selections(editor, expected_multibuffer, cx);
14538 })
14539}
14540
14541#[gpui::test]
14542async fn test_completion(cx: &mut TestAppContext) {
14543 init_test(cx, |_| {});
14544
14545 let mut cx = EditorLspTestContext::new_rust(
14546 lsp::ServerCapabilities {
14547 completion_provider: Some(lsp::CompletionOptions {
14548 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14549 resolve_provider: Some(true),
14550 ..Default::default()
14551 }),
14552 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14553 ..Default::default()
14554 },
14555 cx,
14556 )
14557 .await;
14558 let counter = Arc::new(AtomicUsize::new(0));
14559
14560 cx.set_state(indoc! {"
14561 oneˇ
14562 two
14563 three
14564 "});
14565 cx.simulate_keystroke(".");
14566 handle_completion_request(
14567 indoc! {"
14568 one.|<>
14569 two
14570 three
14571 "},
14572 vec!["first_completion", "second_completion"],
14573 true,
14574 counter.clone(),
14575 &mut cx,
14576 )
14577 .await;
14578 cx.condition(|editor, _| editor.context_menu_visible())
14579 .await;
14580 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14581
14582 let _handler = handle_signature_help_request(
14583 &mut cx,
14584 lsp::SignatureHelp {
14585 signatures: vec![lsp::SignatureInformation {
14586 label: "test signature".to_string(),
14587 documentation: None,
14588 parameters: Some(vec![lsp::ParameterInformation {
14589 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14590 documentation: None,
14591 }]),
14592 active_parameter: None,
14593 }],
14594 active_signature: None,
14595 active_parameter: None,
14596 },
14597 );
14598 cx.update_editor(|editor, window, cx| {
14599 assert!(
14600 !editor.signature_help_state.is_shown(),
14601 "No signature help was called for"
14602 );
14603 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14604 });
14605 cx.run_until_parked();
14606 cx.update_editor(|editor, _, _| {
14607 assert!(
14608 !editor.signature_help_state.is_shown(),
14609 "No signature help should be shown when completions menu is open"
14610 );
14611 });
14612
14613 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14614 editor.context_menu_next(&Default::default(), window, cx);
14615 editor
14616 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14617 .unwrap()
14618 });
14619 cx.assert_editor_state(indoc! {"
14620 one.second_completionˇ
14621 two
14622 three
14623 "});
14624
14625 handle_resolve_completion_request(
14626 &mut cx,
14627 Some(vec![
14628 (
14629 //This overlaps with the primary completion edit which is
14630 //misbehavior from the LSP spec, test that we filter it out
14631 indoc! {"
14632 one.second_ˇcompletion
14633 two
14634 threeˇ
14635 "},
14636 "overlapping additional edit",
14637 ),
14638 (
14639 indoc! {"
14640 one.second_completion
14641 two
14642 threeˇ
14643 "},
14644 "\nadditional edit",
14645 ),
14646 ]),
14647 )
14648 .await;
14649 apply_additional_edits.await.unwrap();
14650 cx.assert_editor_state(indoc! {"
14651 one.second_completionˇ
14652 two
14653 three
14654 additional edit
14655 "});
14656
14657 cx.set_state(indoc! {"
14658 one.second_completion
14659 twoˇ
14660 threeˇ
14661 additional edit
14662 "});
14663 cx.simulate_keystroke(" ");
14664 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14665 cx.simulate_keystroke("s");
14666 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14667
14668 cx.assert_editor_state(indoc! {"
14669 one.second_completion
14670 two sˇ
14671 three sˇ
14672 additional edit
14673 "});
14674 handle_completion_request(
14675 indoc! {"
14676 one.second_completion
14677 two s
14678 three <s|>
14679 additional edit
14680 "},
14681 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14682 true,
14683 counter.clone(),
14684 &mut cx,
14685 )
14686 .await;
14687 cx.condition(|editor, _| editor.context_menu_visible())
14688 .await;
14689 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14690
14691 cx.simulate_keystroke("i");
14692
14693 handle_completion_request(
14694 indoc! {"
14695 one.second_completion
14696 two si
14697 three <si|>
14698 additional edit
14699 "},
14700 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14701 true,
14702 counter.clone(),
14703 &mut cx,
14704 )
14705 .await;
14706 cx.condition(|editor, _| editor.context_menu_visible())
14707 .await;
14708 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14709
14710 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14711 editor
14712 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14713 .unwrap()
14714 });
14715 cx.assert_editor_state(indoc! {"
14716 one.second_completion
14717 two sixth_completionˇ
14718 three sixth_completionˇ
14719 additional edit
14720 "});
14721
14722 apply_additional_edits.await.unwrap();
14723
14724 update_test_language_settings(&mut cx, |settings| {
14725 settings.defaults.show_completions_on_input = Some(false);
14726 });
14727 cx.set_state("editorˇ");
14728 cx.simulate_keystroke(".");
14729 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14730 cx.simulate_keystrokes("c l o");
14731 cx.assert_editor_state("editor.cloˇ");
14732 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14733 cx.update_editor(|editor, window, cx| {
14734 editor.show_completions(&ShowCompletions, window, cx);
14735 });
14736 handle_completion_request(
14737 "editor.<clo|>",
14738 vec!["close", "clobber"],
14739 true,
14740 counter.clone(),
14741 &mut cx,
14742 )
14743 .await;
14744 cx.condition(|editor, _| editor.context_menu_visible())
14745 .await;
14746 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14747
14748 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14749 editor
14750 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14751 .unwrap()
14752 });
14753 cx.assert_editor_state("editor.clobberˇ");
14754 handle_resolve_completion_request(&mut cx, None).await;
14755 apply_additional_edits.await.unwrap();
14756}
14757
14758#[gpui::test]
14759async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
14760 init_test(cx, |_| {});
14761
14762 let fs = FakeFs::new(cx.executor());
14763 fs.insert_tree(
14764 path!("/a"),
14765 json!({
14766 "main.rs": "",
14767 }),
14768 )
14769 .await;
14770
14771 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14772 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14773 language_registry.add(rust_lang());
14774 let command_calls = Arc::new(AtomicUsize::new(0));
14775 let registered_command = "_the/command";
14776
14777 let closure_command_calls = command_calls.clone();
14778 let mut fake_servers = language_registry.register_fake_lsp(
14779 "Rust",
14780 FakeLspAdapter {
14781 capabilities: lsp::ServerCapabilities {
14782 completion_provider: Some(lsp::CompletionOptions {
14783 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14784 ..lsp::CompletionOptions::default()
14785 }),
14786 execute_command_provider: Some(lsp::ExecuteCommandOptions {
14787 commands: vec![registered_command.to_owned()],
14788 ..lsp::ExecuteCommandOptions::default()
14789 }),
14790 ..lsp::ServerCapabilities::default()
14791 },
14792 initializer: Some(Box::new(move |fake_server| {
14793 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14794 move |params, _| async move {
14795 Ok(Some(lsp::CompletionResponse::Array(vec![
14796 lsp::CompletionItem {
14797 label: "registered_command".to_owned(),
14798 text_edit: gen_text_edit(¶ms, ""),
14799 command: Some(lsp::Command {
14800 title: registered_command.to_owned(),
14801 command: "_the/command".to_owned(),
14802 arguments: Some(vec![serde_json::Value::Bool(true)]),
14803 }),
14804 ..lsp::CompletionItem::default()
14805 },
14806 lsp::CompletionItem {
14807 label: "unregistered_command".to_owned(),
14808 text_edit: gen_text_edit(¶ms, ""),
14809 command: Some(lsp::Command {
14810 title: "????????????".to_owned(),
14811 command: "????????????".to_owned(),
14812 arguments: Some(vec![serde_json::Value::Null]),
14813 }),
14814 ..lsp::CompletionItem::default()
14815 },
14816 ])))
14817 },
14818 );
14819 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
14820 let command_calls = closure_command_calls.clone();
14821 move |params, _| {
14822 assert_eq!(params.command, registered_command);
14823 let command_calls = command_calls.clone();
14824 async move {
14825 command_calls.fetch_add(1, atomic::Ordering::Release);
14826 Ok(Some(json!(null)))
14827 }
14828 }
14829 });
14830 })),
14831 ..FakeLspAdapter::default()
14832 },
14833 );
14834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14835 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14836 let editor = workspace
14837 .update(cx, |workspace, window, cx| {
14838 workspace.open_abs_path(
14839 PathBuf::from(path!("/a/main.rs")),
14840 OpenOptions::default(),
14841 window,
14842 cx,
14843 )
14844 })
14845 .unwrap()
14846 .await
14847 .unwrap()
14848 .downcast::<Editor>()
14849 .unwrap();
14850 let _fake_server = fake_servers.next().await.unwrap();
14851
14852 editor.update_in(cx, |editor, window, cx| {
14853 cx.focus_self(window);
14854 editor.move_to_end(&MoveToEnd, window, cx);
14855 editor.handle_input(".", window, cx);
14856 });
14857 cx.run_until_parked();
14858 editor.update(cx, |editor, _| {
14859 assert!(editor.context_menu_visible());
14860 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14861 {
14862 let completion_labels = menu
14863 .completions
14864 .borrow()
14865 .iter()
14866 .map(|c| c.label.text.clone())
14867 .collect::<Vec<_>>();
14868 assert_eq!(
14869 completion_labels,
14870 &["registered_command", "unregistered_command",],
14871 );
14872 } else {
14873 panic!("expected completion menu to be open");
14874 }
14875 });
14876
14877 editor
14878 .update_in(cx, |editor, window, cx| {
14879 editor
14880 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14881 .unwrap()
14882 })
14883 .await
14884 .unwrap();
14885 cx.run_until_parked();
14886 assert_eq!(
14887 command_calls.load(atomic::Ordering::Acquire),
14888 1,
14889 "For completion with a registered command, Zed should send a command execution request",
14890 );
14891
14892 editor.update_in(cx, |editor, window, cx| {
14893 cx.focus_self(window);
14894 editor.handle_input(".", window, cx);
14895 });
14896 cx.run_until_parked();
14897 editor.update(cx, |editor, _| {
14898 assert!(editor.context_menu_visible());
14899 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14900 {
14901 let completion_labels = menu
14902 .completions
14903 .borrow()
14904 .iter()
14905 .map(|c| c.label.text.clone())
14906 .collect::<Vec<_>>();
14907 assert_eq!(
14908 completion_labels,
14909 &["registered_command", "unregistered_command",],
14910 );
14911 } else {
14912 panic!("expected completion menu to be open");
14913 }
14914 });
14915 editor
14916 .update_in(cx, |editor, window, cx| {
14917 editor.context_menu_next(&Default::default(), window, cx);
14918 editor
14919 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14920 .unwrap()
14921 })
14922 .await
14923 .unwrap();
14924 cx.run_until_parked();
14925 assert_eq!(
14926 command_calls.load(atomic::Ordering::Acquire),
14927 1,
14928 "For completion with an unregistered command, Zed should not send a command execution request",
14929 );
14930}
14931
14932#[gpui::test]
14933async fn test_completion_reuse(cx: &mut TestAppContext) {
14934 init_test(cx, |_| {});
14935
14936 let mut cx = EditorLspTestContext::new_rust(
14937 lsp::ServerCapabilities {
14938 completion_provider: Some(lsp::CompletionOptions {
14939 trigger_characters: Some(vec![".".to_string()]),
14940 ..Default::default()
14941 }),
14942 ..Default::default()
14943 },
14944 cx,
14945 )
14946 .await;
14947
14948 let counter = Arc::new(AtomicUsize::new(0));
14949 cx.set_state("objˇ");
14950 cx.simulate_keystroke(".");
14951
14952 // Initial completion request returns complete results
14953 let is_incomplete = false;
14954 handle_completion_request(
14955 "obj.|<>",
14956 vec!["a", "ab", "abc"],
14957 is_incomplete,
14958 counter.clone(),
14959 &mut cx,
14960 )
14961 .await;
14962 cx.run_until_parked();
14963 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14964 cx.assert_editor_state("obj.ˇ");
14965 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14966
14967 // Type "a" - filters existing completions
14968 cx.simulate_keystroke("a");
14969 cx.run_until_parked();
14970 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14971 cx.assert_editor_state("obj.aˇ");
14972 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14973
14974 // Type "b" - filters existing completions
14975 cx.simulate_keystroke("b");
14976 cx.run_until_parked();
14977 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14978 cx.assert_editor_state("obj.abˇ");
14979 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14980
14981 // Type "c" - filters existing completions
14982 cx.simulate_keystroke("c");
14983 cx.run_until_parked();
14984 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14985 cx.assert_editor_state("obj.abcˇ");
14986 check_displayed_completions(vec!["abc"], &mut cx);
14987
14988 // Backspace to delete "c" - filters existing completions
14989 cx.update_editor(|editor, window, cx| {
14990 editor.backspace(&Backspace, window, cx);
14991 });
14992 cx.run_until_parked();
14993 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14994 cx.assert_editor_state("obj.abˇ");
14995 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14996
14997 // Moving cursor to the left dismisses menu.
14998 cx.update_editor(|editor, window, cx| {
14999 editor.move_left(&MoveLeft, window, cx);
15000 });
15001 cx.run_until_parked();
15002 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15003 cx.assert_editor_state("obj.aˇb");
15004 cx.update_editor(|editor, _, _| {
15005 assert_eq!(editor.context_menu_visible(), false);
15006 });
15007
15008 // Type "b" - new request
15009 cx.simulate_keystroke("b");
15010 let is_incomplete = false;
15011 handle_completion_request(
15012 "obj.<ab|>a",
15013 vec!["ab", "abc"],
15014 is_incomplete,
15015 counter.clone(),
15016 &mut cx,
15017 )
15018 .await;
15019 cx.run_until_parked();
15020 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15021 cx.assert_editor_state("obj.abˇb");
15022 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15023
15024 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15025 cx.update_editor(|editor, window, cx| {
15026 editor.backspace(&Backspace, window, cx);
15027 });
15028 let is_incomplete = false;
15029 handle_completion_request(
15030 "obj.<a|>b",
15031 vec!["a", "ab", "abc"],
15032 is_incomplete,
15033 counter.clone(),
15034 &mut cx,
15035 )
15036 .await;
15037 cx.run_until_parked();
15038 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15039 cx.assert_editor_state("obj.aˇb");
15040 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15041
15042 // Backspace to delete "a" - dismisses menu.
15043 cx.update_editor(|editor, window, cx| {
15044 editor.backspace(&Backspace, window, cx);
15045 });
15046 cx.run_until_parked();
15047 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15048 cx.assert_editor_state("obj.ˇb");
15049 cx.update_editor(|editor, _, _| {
15050 assert_eq!(editor.context_menu_visible(), false);
15051 });
15052}
15053
15054#[gpui::test]
15055async fn test_word_completion(cx: &mut TestAppContext) {
15056 let lsp_fetch_timeout_ms = 10;
15057 init_test(cx, |language_settings| {
15058 language_settings.defaults.completions = Some(CompletionSettingsContent {
15059 words_min_length: Some(0),
15060 lsp_fetch_timeout_ms: Some(10),
15061 lsp_insert_mode: Some(LspInsertMode::Insert),
15062 ..Default::default()
15063 });
15064 });
15065
15066 let mut cx = EditorLspTestContext::new_rust(
15067 lsp::ServerCapabilities {
15068 completion_provider: Some(lsp::CompletionOptions {
15069 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15070 ..lsp::CompletionOptions::default()
15071 }),
15072 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15073 ..lsp::ServerCapabilities::default()
15074 },
15075 cx,
15076 )
15077 .await;
15078
15079 let throttle_completions = Arc::new(AtomicBool::new(false));
15080
15081 let lsp_throttle_completions = throttle_completions.clone();
15082 let _completion_requests_handler =
15083 cx.lsp
15084 .server
15085 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15086 let lsp_throttle_completions = lsp_throttle_completions.clone();
15087 let cx = cx.clone();
15088 async move {
15089 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15090 cx.background_executor()
15091 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15092 .await;
15093 }
15094 Ok(Some(lsp::CompletionResponse::Array(vec![
15095 lsp::CompletionItem {
15096 label: "first".into(),
15097 ..lsp::CompletionItem::default()
15098 },
15099 lsp::CompletionItem {
15100 label: "last".into(),
15101 ..lsp::CompletionItem::default()
15102 },
15103 ])))
15104 }
15105 });
15106
15107 cx.set_state(indoc! {"
15108 oneˇ
15109 two
15110 three
15111 "});
15112 cx.simulate_keystroke(".");
15113 cx.executor().run_until_parked();
15114 cx.condition(|editor, _| editor.context_menu_visible())
15115 .await;
15116 cx.update_editor(|editor, window, cx| {
15117 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15118 {
15119 assert_eq!(
15120 completion_menu_entries(menu),
15121 &["first", "last"],
15122 "When LSP server is fast to reply, no fallback word completions are used"
15123 );
15124 } else {
15125 panic!("expected completion menu to be open");
15126 }
15127 editor.cancel(&Cancel, window, cx);
15128 });
15129 cx.executor().run_until_parked();
15130 cx.condition(|editor, _| !editor.context_menu_visible())
15131 .await;
15132
15133 throttle_completions.store(true, atomic::Ordering::Release);
15134 cx.simulate_keystroke(".");
15135 cx.executor()
15136 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15137 cx.executor().run_until_parked();
15138 cx.condition(|editor, _| editor.context_menu_visible())
15139 .await;
15140 cx.update_editor(|editor, _, _| {
15141 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15142 {
15143 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15144 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15145 } else {
15146 panic!("expected completion menu to be open");
15147 }
15148 });
15149}
15150
15151#[gpui::test]
15152async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15153 init_test(cx, |language_settings| {
15154 language_settings.defaults.completions = Some(CompletionSettingsContent {
15155 words: Some(WordsCompletionMode::Enabled),
15156 words_min_length: Some(0),
15157 lsp_insert_mode: Some(LspInsertMode::Insert),
15158 ..Default::default()
15159 });
15160 });
15161
15162 let mut cx = EditorLspTestContext::new_rust(
15163 lsp::ServerCapabilities {
15164 completion_provider: Some(lsp::CompletionOptions {
15165 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15166 ..lsp::CompletionOptions::default()
15167 }),
15168 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15169 ..lsp::ServerCapabilities::default()
15170 },
15171 cx,
15172 )
15173 .await;
15174
15175 let _completion_requests_handler =
15176 cx.lsp
15177 .server
15178 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15179 Ok(Some(lsp::CompletionResponse::Array(vec![
15180 lsp::CompletionItem {
15181 label: "first".into(),
15182 ..lsp::CompletionItem::default()
15183 },
15184 lsp::CompletionItem {
15185 label: "last".into(),
15186 ..lsp::CompletionItem::default()
15187 },
15188 ])))
15189 });
15190
15191 cx.set_state(indoc! {"ˇ
15192 first
15193 last
15194 second
15195 "});
15196 cx.simulate_keystroke(".");
15197 cx.executor().run_until_parked();
15198 cx.condition(|editor, _| editor.context_menu_visible())
15199 .await;
15200 cx.update_editor(|editor, _, _| {
15201 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15202 {
15203 assert_eq!(
15204 completion_menu_entries(menu),
15205 &["first", "last", "second"],
15206 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15207 );
15208 } else {
15209 panic!("expected completion menu to be open");
15210 }
15211 });
15212}
15213
15214#[gpui::test]
15215async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15216 init_test(cx, |language_settings| {
15217 language_settings.defaults.completions = Some(CompletionSettingsContent {
15218 words: Some(WordsCompletionMode::Disabled),
15219 words_min_length: Some(0),
15220 lsp_insert_mode: Some(LspInsertMode::Insert),
15221 ..Default::default()
15222 });
15223 });
15224
15225 let mut cx = EditorLspTestContext::new_rust(
15226 lsp::ServerCapabilities {
15227 completion_provider: Some(lsp::CompletionOptions {
15228 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15229 ..lsp::CompletionOptions::default()
15230 }),
15231 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15232 ..lsp::ServerCapabilities::default()
15233 },
15234 cx,
15235 )
15236 .await;
15237
15238 let _completion_requests_handler =
15239 cx.lsp
15240 .server
15241 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15242 panic!("LSP completions should not be queried when dealing with word completions")
15243 });
15244
15245 cx.set_state(indoc! {"ˇ
15246 first
15247 last
15248 second
15249 "});
15250 cx.update_editor(|editor, window, cx| {
15251 editor.show_word_completions(&ShowWordCompletions, window, cx);
15252 });
15253 cx.executor().run_until_parked();
15254 cx.condition(|editor, _| editor.context_menu_visible())
15255 .await;
15256 cx.update_editor(|editor, _, _| {
15257 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15258 {
15259 assert_eq!(
15260 completion_menu_entries(menu),
15261 &["first", "last", "second"],
15262 "`ShowWordCompletions` action should show word completions"
15263 );
15264 } else {
15265 panic!("expected completion menu to be open");
15266 }
15267 });
15268
15269 cx.simulate_keystroke("l");
15270 cx.executor().run_until_parked();
15271 cx.condition(|editor, _| editor.context_menu_visible())
15272 .await;
15273 cx.update_editor(|editor, _, _| {
15274 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15275 {
15276 assert_eq!(
15277 completion_menu_entries(menu),
15278 &["last"],
15279 "After showing word completions, further editing should filter them and not query the LSP"
15280 );
15281 } else {
15282 panic!("expected completion menu to be open");
15283 }
15284 });
15285}
15286
15287#[gpui::test]
15288async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15289 init_test(cx, |language_settings| {
15290 language_settings.defaults.completions = Some(CompletionSettingsContent {
15291 words_min_length: Some(0),
15292 lsp: Some(false),
15293 lsp_insert_mode: Some(LspInsertMode::Insert),
15294 ..Default::default()
15295 });
15296 });
15297
15298 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15299
15300 cx.set_state(indoc! {"ˇ
15301 0_usize
15302 let
15303 33
15304 4.5f32
15305 "});
15306 cx.update_editor(|editor, window, cx| {
15307 editor.show_completions(&ShowCompletions, window, cx);
15308 });
15309 cx.executor().run_until_parked();
15310 cx.condition(|editor, _| editor.context_menu_visible())
15311 .await;
15312 cx.update_editor(|editor, window, cx| {
15313 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15314 {
15315 assert_eq!(
15316 completion_menu_entries(menu),
15317 &["let"],
15318 "With no digits in the completion query, no digits should be in the word completions"
15319 );
15320 } else {
15321 panic!("expected completion menu to be open");
15322 }
15323 editor.cancel(&Cancel, window, cx);
15324 });
15325
15326 cx.set_state(indoc! {"3ˇ
15327 0_usize
15328 let
15329 3
15330 33.35f32
15331 "});
15332 cx.update_editor(|editor, window, cx| {
15333 editor.show_completions(&ShowCompletions, window, cx);
15334 });
15335 cx.executor().run_until_parked();
15336 cx.condition(|editor, _| editor.context_menu_visible())
15337 .await;
15338 cx.update_editor(|editor, _, _| {
15339 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15340 {
15341 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15342 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15343 } else {
15344 panic!("expected completion menu to be open");
15345 }
15346 });
15347}
15348
15349#[gpui::test]
15350async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15351 init_test(cx, |language_settings| {
15352 language_settings.defaults.completions = Some(CompletionSettingsContent {
15353 words: Some(WordsCompletionMode::Enabled),
15354 words_min_length: Some(3),
15355 lsp_insert_mode: Some(LspInsertMode::Insert),
15356 ..Default::default()
15357 });
15358 });
15359
15360 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15361 cx.set_state(indoc! {"ˇ
15362 wow
15363 wowen
15364 wowser
15365 "});
15366 cx.simulate_keystroke("w");
15367 cx.executor().run_until_parked();
15368 cx.update_editor(|editor, _, _| {
15369 if editor.context_menu.borrow_mut().is_some() {
15370 panic!(
15371 "expected completion menu to be hidden, as words completion threshold is not met"
15372 );
15373 }
15374 });
15375
15376 cx.update_editor(|editor, window, cx| {
15377 editor.show_word_completions(&ShowWordCompletions, window, cx);
15378 });
15379 cx.executor().run_until_parked();
15380 cx.update_editor(|editor, window, cx| {
15381 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15382 {
15383 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");
15384 } else {
15385 panic!("expected completion menu to be open after the word completions are called with an action");
15386 }
15387
15388 editor.cancel(&Cancel, window, cx);
15389 });
15390 cx.update_editor(|editor, _, _| {
15391 if editor.context_menu.borrow_mut().is_some() {
15392 panic!("expected completion menu to be hidden after canceling");
15393 }
15394 });
15395
15396 cx.simulate_keystroke("o");
15397 cx.executor().run_until_parked();
15398 cx.update_editor(|editor, _, _| {
15399 if editor.context_menu.borrow_mut().is_some() {
15400 panic!(
15401 "expected completion menu to be hidden, as words completion threshold is not met still"
15402 );
15403 }
15404 });
15405
15406 cx.simulate_keystroke("w");
15407 cx.executor().run_until_parked();
15408 cx.update_editor(|editor, _, _| {
15409 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15410 {
15411 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15412 } else {
15413 panic!("expected completion menu to be open after the word completions threshold is met");
15414 }
15415 });
15416}
15417
15418#[gpui::test]
15419async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15420 init_test(cx, |language_settings| {
15421 language_settings.defaults.completions = Some(CompletionSettingsContent {
15422 words: Some(WordsCompletionMode::Enabled),
15423 words_min_length: Some(0),
15424 lsp_insert_mode: Some(LspInsertMode::Insert),
15425 ..Default::default()
15426 });
15427 });
15428
15429 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15430 cx.update_editor(|editor, _, _| {
15431 editor.disable_word_completions();
15432 });
15433 cx.set_state(indoc! {"ˇ
15434 wow
15435 wowen
15436 wowser
15437 "});
15438 cx.simulate_keystroke("w");
15439 cx.executor().run_until_parked();
15440 cx.update_editor(|editor, _, _| {
15441 if editor.context_menu.borrow_mut().is_some() {
15442 panic!(
15443 "expected completion menu to be hidden, as words completion are disabled for this editor"
15444 );
15445 }
15446 });
15447
15448 cx.update_editor(|editor, window, cx| {
15449 editor.show_word_completions(&ShowWordCompletions, window, cx);
15450 });
15451 cx.executor().run_until_parked();
15452 cx.update_editor(|editor, _, _| {
15453 if editor.context_menu.borrow_mut().is_some() {
15454 panic!(
15455 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15456 );
15457 }
15458 });
15459}
15460
15461#[gpui::test]
15462async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15463 init_test(cx, |language_settings| {
15464 language_settings.defaults.completions = Some(CompletionSettingsContent {
15465 words: Some(WordsCompletionMode::Disabled),
15466 words_min_length: Some(0),
15467 lsp_insert_mode: Some(LspInsertMode::Insert),
15468 ..Default::default()
15469 });
15470 });
15471
15472 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15473 cx.update_editor(|editor, _, _| {
15474 editor.set_completion_provider(None);
15475 });
15476 cx.set_state(indoc! {"ˇ
15477 wow
15478 wowen
15479 wowser
15480 "});
15481 cx.simulate_keystroke("w");
15482 cx.executor().run_until_parked();
15483 cx.update_editor(|editor, _, _| {
15484 if editor.context_menu.borrow_mut().is_some() {
15485 panic!("expected completion menu to be hidden, as disabled in settings");
15486 }
15487 });
15488}
15489
15490fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15491 let position = || lsp::Position {
15492 line: params.text_document_position.position.line,
15493 character: params.text_document_position.position.character,
15494 };
15495 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15496 range: lsp::Range {
15497 start: position(),
15498 end: position(),
15499 },
15500 new_text: text.to_string(),
15501 }))
15502}
15503
15504#[gpui::test]
15505async fn test_multiline_completion(cx: &mut TestAppContext) {
15506 init_test(cx, |_| {});
15507
15508 let fs = FakeFs::new(cx.executor());
15509 fs.insert_tree(
15510 path!("/a"),
15511 json!({
15512 "main.ts": "a",
15513 }),
15514 )
15515 .await;
15516
15517 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15518 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15519 let typescript_language = Arc::new(Language::new(
15520 LanguageConfig {
15521 name: "TypeScript".into(),
15522 matcher: LanguageMatcher {
15523 path_suffixes: vec!["ts".to_string()],
15524 ..LanguageMatcher::default()
15525 },
15526 line_comments: vec!["// ".into()],
15527 ..LanguageConfig::default()
15528 },
15529 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15530 ));
15531 language_registry.add(typescript_language.clone());
15532 let mut fake_servers = language_registry.register_fake_lsp(
15533 "TypeScript",
15534 FakeLspAdapter {
15535 capabilities: lsp::ServerCapabilities {
15536 completion_provider: Some(lsp::CompletionOptions {
15537 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15538 ..lsp::CompletionOptions::default()
15539 }),
15540 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15541 ..lsp::ServerCapabilities::default()
15542 },
15543 // Emulate vtsls label generation
15544 label_for_completion: Some(Box::new(|item, _| {
15545 let text = if let Some(description) = item
15546 .label_details
15547 .as_ref()
15548 .and_then(|label_details| label_details.description.as_ref())
15549 {
15550 format!("{} {}", item.label, description)
15551 } else if let Some(detail) = &item.detail {
15552 format!("{} {}", item.label, detail)
15553 } else {
15554 item.label.clone()
15555 };
15556 Some(language::CodeLabel::plain(text, None))
15557 })),
15558 ..FakeLspAdapter::default()
15559 },
15560 );
15561 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15562 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15563 let worktree_id = workspace
15564 .update(cx, |workspace, _window, cx| {
15565 workspace.project().update(cx, |project, cx| {
15566 project.worktrees(cx).next().unwrap().read(cx).id()
15567 })
15568 })
15569 .unwrap();
15570 let _buffer = project
15571 .update(cx, |project, cx| {
15572 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15573 })
15574 .await
15575 .unwrap();
15576 let editor = workspace
15577 .update(cx, |workspace, window, cx| {
15578 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15579 })
15580 .unwrap()
15581 .await
15582 .unwrap()
15583 .downcast::<Editor>()
15584 .unwrap();
15585 let fake_server = fake_servers.next().await.unwrap();
15586
15587 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15588 let multiline_label_2 = "a\nb\nc\n";
15589 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15590 let multiline_description = "d\ne\nf\n";
15591 let multiline_detail_2 = "g\nh\ni\n";
15592
15593 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15594 move |params, _| async move {
15595 Ok(Some(lsp::CompletionResponse::Array(vec![
15596 lsp::CompletionItem {
15597 label: multiline_label.to_string(),
15598 text_edit: gen_text_edit(¶ms, "new_text_1"),
15599 ..lsp::CompletionItem::default()
15600 },
15601 lsp::CompletionItem {
15602 label: "single line label 1".to_string(),
15603 detail: Some(multiline_detail.to_string()),
15604 text_edit: gen_text_edit(¶ms, "new_text_2"),
15605 ..lsp::CompletionItem::default()
15606 },
15607 lsp::CompletionItem {
15608 label: "single line label 2".to_string(),
15609 label_details: Some(lsp::CompletionItemLabelDetails {
15610 description: Some(multiline_description.to_string()),
15611 detail: None,
15612 }),
15613 text_edit: gen_text_edit(¶ms, "new_text_2"),
15614 ..lsp::CompletionItem::default()
15615 },
15616 lsp::CompletionItem {
15617 label: multiline_label_2.to_string(),
15618 detail: Some(multiline_detail_2.to_string()),
15619 text_edit: gen_text_edit(¶ms, "new_text_3"),
15620 ..lsp::CompletionItem::default()
15621 },
15622 lsp::CompletionItem {
15623 label: "Label with many spaces and \t but without newlines".to_string(),
15624 detail: Some(
15625 "Details with many spaces and \t but without newlines".to_string(),
15626 ),
15627 text_edit: gen_text_edit(¶ms, "new_text_4"),
15628 ..lsp::CompletionItem::default()
15629 },
15630 ])))
15631 },
15632 );
15633
15634 editor.update_in(cx, |editor, window, cx| {
15635 cx.focus_self(window);
15636 editor.move_to_end(&MoveToEnd, window, cx);
15637 editor.handle_input(".", window, cx);
15638 });
15639 cx.run_until_parked();
15640 completion_handle.next().await.unwrap();
15641
15642 editor.update(cx, |editor, _| {
15643 assert!(editor.context_menu_visible());
15644 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15645 {
15646 let completion_labels = menu
15647 .completions
15648 .borrow()
15649 .iter()
15650 .map(|c| c.label.text.clone())
15651 .collect::<Vec<_>>();
15652 assert_eq!(
15653 completion_labels,
15654 &[
15655 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15656 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15657 "single line label 2 d e f ",
15658 "a b c g h i ",
15659 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15660 ],
15661 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15662 );
15663
15664 for completion in menu
15665 .completions
15666 .borrow()
15667 .iter() {
15668 assert_eq!(
15669 completion.label.filter_range,
15670 0..completion.label.text.len(),
15671 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15672 );
15673 }
15674 } else {
15675 panic!("expected completion menu to be open");
15676 }
15677 });
15678}
15679
15680#[gpui::test]
15681async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15682 init_test(cx, |_| {});
15683 let mut cx = EditorLspTestContext::new_rust(
15684 lsp::ServerCapabilities {
15685 completion_provider: Some(lsp::CompletionOptions {
15686 trigger_characters: Some(vec![".".to_string()]),
15687 ..Default::default()
15688 }),
15689 ..Default::default()
15690 },
15691 cx,
15692 )
15693 .await;
15694 cx.lsp
15695 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15696 Ok(Some(lsp::CompletionResponse::Array(vec![
15697 lsp::CompletionItem {
15698 label: "first".into(),
15699 ..Default::default()
15700 },
15701 lsp::CompletionItem {
15702 label: "last".into(),
15703 ..Default::default()
15704 },
15705 ])))
15706 });
15707 cx.set_state("variableˇ");
15708 cx.simulate_keystroke(".");
15709 cx.executor().run_until_parked();
15710
15711 cx.update_editor(|editor, _, _| {
15712 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15713 {
15714 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15715 } else {
15716 panic!("expected completion menu to be open");
15717 }
15718 });
15719
15720 cx.update_editor(|editor, window, cx| {
15721 editor.move_page_down(&MovePageDown::default(), window, cx);
15722 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15723 {
15724 assert!(
15725 menu.selected_item == 1,
15726 "expected PageDown to select the last item from the context menu"
15727 );
15728 } else {
15729 panic!("expected completion menu to stay open after PageDown");
15730 }
15731 });
15732
15733 cx.update_editor(|editor, window, cx| {
15734 editor.move_page_up(&MovePageUp::default(), window, cx);
15735 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15736 {
15737 assert!(
15738 menu.selected_item == 0,
15739 "expected PageUp to select the first item from the context menu"
15740 );
15741 } else {
15742 panic!("expected completion menu to stay open after PageUp");
15743 }
15744 });
15745}
15746
15747#[gpui::test]
15748async fn test_as_is_completions(cx: &mut TestAppContext) {
15749 init_test(cx, |_| {});
15750 let mut cx = EditorLspTestContext::new_rust(
15751 lsp::ServerCapabilities {
15752 completion_provider: Some(lsp::CompletionOptions {
15753 ..Default::default()
15754 }),
15755 ..Default::default()
15756 },
15757 cx,
15758 )
15759 .await;
15760 cx.lsp
15761 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15762 Ok(Some(lsp::CompletionResponse::Array(vec![
15763 lsp::CompletionItem {
15764 label: "unsafe".into(),
15765 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15766 range: lsp::Range {
15767 start: lsp::Position {
15768 line: 1,
15769 character: 2,
15770 },
15771 end: lsp::Position {
15772 line: 1,
15773 character: 3,
15774 },
15775 },
15776 new_text: "unsafe".to_string(),
15777 })),
15778 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15779 ..Default::default()
15780 },
15781 ])))
15782 });
15783 cx.set_state("fn a() {}\n nˇ");
15784 cx.executor().run_until_parked();
15785 cx.update_editor(|editor, window, cx| {
15786 editor.trigger_completion_on_input("n", true, window, cx)
15787 });
15788 cx.executor().run_until_parked();
15789
15790 cx.update_editor(|editor, window, cx| {
15791 editor.confirm_completion(&Default::default(), window, cx)
15792 });
15793 cx.executor().run_until_parked();
15794 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15795}
15796
15797#[gpui::test]
15798async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15799 init_test(cx, |_| {});
15800 let language =
15801 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15802 let mut cx = EditorLspTestContext::new(
15803 language,
15804 lsp::ServerCapabilities {
15805 completion_provider: Some(lsp::CompletionOptions {
15806 ..lsp::CompletionOptions::default()
15807 }),
15808 ..lsp::ServerCapabilities::default()
15809 },
15810 cx,
15811 )
15812 .await;
15813
15814 cx.set_state(
15815 "#ifndef BAR_H
15816#define BAR_H
15817
15818#include <stdbool.h>
15819
15820int fn_branch(bool do_branch1, bool do_branch2);
15821
15822#endif // BAR_H
15823ˇ",
15824 );
15825 cx.executor().run_until_parked();
15826 cx.update_editor(|editor, window, cx| {
15827 editor.handle_input("#", window, cx);
15828 });
15829 cx.executor().run_until_parked();
15830 cx.update_editor(|editor, window, cx| {
15831 editor.handle_input("i", window, cx);
15832 });
15833 cx.executor().run_until_parked();
15834 cx.update_editor(|editor, window, cx| {
15835 editor.handle_input("n", window, cx);
15836 });
15837 cx.executor().run_until_parked();
15838 cx.assert_editor_state(
15839 "#ifndef BAR_H
15840#define BAR_H
15841
15842#include <stdbool.h>
15843
15844int fn_branch(bool do_branch1, bool do_branch2);
15845
15846#endif // BAR_H
15847#inˇ",
15848 );
15849
15850 cx.lsp
15851 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15852 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15853 is_incomplete: false,
15854 item_defaults: None,
15855 items: vec![lsp::CompletionItem {
15856 kind: Some(lsp::CompletionItemKind::SNIPPET),
15857 label_details: Some(lsp::CompletionItemLabelDetails {
15858 detail: Some("header".to_string()),
15859 description: None,
15860 }),
15861 label: " include".to_string(),
15862 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15863 range: lsp::Range {
15864 start: lsp::Position {
15865 line: 8,
15866 character: 1,
15867 },
15868 end: lsp::Position {
15869 line: 8,
15870 character: 1,
15871 },
15872 },
15873 new_text: "include \"$0\"".to_string(),
15874 })),
15875 sort_text: Some("40b67681include".to_string()),
15876 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15877 filter_text: Some("include".to_string()),
15878 insert_text: Some("include \"$0\"".to_string()),
15879 ..lsp::CompletionItem::default()
15880 }],
15881 })))
15882 });
15883 cx.update_editor(|editor, window, cx| {
15884 editor.show_completions(&ShowCompletions, window, cx);
15885 });
15886 cx.executor().run_until_parked();
15887 cx.update_editor(|editor, window, cx| {
15888 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15889 });
15890 cx.executor().run_until_parked();
15891 cx.assert_editor_state(
15892 "#ifndef BAR_H
15893#define BAR_H
15894
15895#include <stdbool.h>
15896
15897int fn_branch(bool do_branch1, bool do_branch2);
15898
15899#endif // BAR_H
15900#include \"ˇ\"",
15901 );
15902
15903 cx.lsp
15904 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15905 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15906 is_incomplete: true,
15907 item_defaults: None,
15908 items: vec![lsp::CompletionItem {
15909 kind: Some(lsp::CompletionItemKind::FILE),
15910 label: "AGL/".to_string(),
15911 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15912 range: lsp::Range {
15913 start: lsp::Position {
15914 line: 8,
15915 character: 10,
15916 },
15917 end: lsp::Position {
15918 line: 8,
15919 character: 11,
15920 },
15921 },
15922 new_text: "AGL/".to_string(),
15923 })),
15924 sort_text: Some("40b67681AGL/".to_string()),
15925 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15926 filter_text: Some("AGL/".to_string()),
15927 insert_text: Some("AGL/".to_string()),
15928 ..lsp::CompletionItem::default()
15929 }],
15930 })))
15931 });
15932 cx.update_editor(|editor, window, cx| {
15933 editor.show_completions(&ShowCompletions, window, cx);
15934 });
15935 cx.executor().run_until_parked();
15936 cx.update_editor(|editor, window, cx| {
15937 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15938 });
15939 cx.executor().run_until_parked();
15940 cx.assert_editor_state(
15941 r##"#ifndef BAR_H
15942#define BAR_H
15943
15944#include <stdbool.h>
15945
15946int fn_branch(bool do_branch1, bool do_branch2);
15947
15948#endif // BAR_H
15949#include "AGL/ˇ"##,
15950 );
15951
15952 cx.update_editor(|editor, window, cx| {
15953 editor.handle_input("\"", window, cx);
15954 });
15955 cx.executor().run_until_parked();
15956 cx.assert_editor_state(
15957 r##"#ifndef BAR_H
15958#define BAR_H
15959
15960#include <stdbool.h>
15961
15962int fn_branch(bool do_branch1, bool do_branch2);
15963
15964#endif // BAR_H
15965#include "AGL/"ˇ"##,
15966 );
15967}
15968
15969#[gpui::test]
15970async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15971 init_test(cx, |_| {});
15972
15973 let mut cx = EditorLspTestContext::new_rust(
15974 lsp::ServerCapabilities {
15975 completion_provider: Some(lsp::CompletionOptions {
15976 trigger_characters: Some(vec![".".to_string()]),
15977 resolve_provider: Some(true),
15978 ..Default::default()
15979 }),
15980 ..Default::default()
15981 },
15982 cx,
15983 )
15984 .await;
15985
15986 cx.set_state("fn main() { let a = 2ˇ; }");
15987 cx.simulate_keystroke(".");
15988 let completion_item = lsp::CompletionItem {
15989 label: "Some".into(),
15990 kind: Some(lsp::CompletionItemKind::SNIPPET),
15991 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15992 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15993 kind: lsp::MarkupKind::Markdown,
15994 value: "```rust\nSome(2)\n```".to_string(),
15995 })),
15996 deprecated: Some(false),
15997 sort_text: Some("Some".to_string()),
15998 filter_text: Some("Some".to_string()),
15999 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16000 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16001 range: lsp::Range {
16002 start: lsp::Position {
16003 line: 0,
16004 character: 22,
16005 },
16006 end: lsp::Position {
16007 line: 0,
16008 character: 22,
16009 },
16010 },
16011 new_text: "Some(2)".to_string(),
16012 })),
16013 additional_text_edits: Some(vec![lsp::TextEdit {
16014 range: lsp::Range {
16015 start: lsp::Position {
16016 line: 0,
16017 character: 20,
16018 },
16019 end: lsp::Position {
16020 line: 0,
16021 character: 22,
16022 },
16023 },
16024 new_text: "".to_string(),
16025 }]),
16026 ..Default::default()
16027 };
16028
16029 let closure_completion_item = completion_item.clone();
16030 let counter = Arc::new(AtomicUsize::new(0));
16031 let counter_clone = counter.clone();
16032 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16033 let task_completion_item = closure_completion_item.clone();
16034 counter_clone.fetch_add(1, atomic::Ordering::Release);
16035 async move {
16036 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16037 is_incomplete: true,
16038 item_defaults: None,
16039 items: vec![task_completion_item],
16040 })))
16041 }
16042 });
16043
16044 cx.condition(|editor, _| editor.context_menu_visible())
16045 .await;
16046 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16047 assert!(request.next().await.is_some());
16048 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16049
16050 cx.simulate_keystrokes("S o m");
16051 cx.condition(|editor, _| editor.context_menu_visible())
16052 .await;
16053 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16054 assert!(request.next().await.is_some());
16055 assert!(request.next().await.is_some());
16056 assert!(request.next().await.is_some());
16057 request.close();
16058 assert!(request.next().await.is_none());
16059 assert_eq!(
16060 counter.load(atomic::Ordering::Acquire),
16061 4,
16062 "With the completions menu open, only one LSP request should happen per input"
16063 );
16064}
16065
16066#[gpui::test]
16067async fn test_toggle_comment(cx: &mut TestAppContext) {
16068 init_test(cx, |_| {});
16069 let mut cx = EditorTestContext::new(cx).await;
16070 let language = Arc::new(Language::new(
16071 LanguageConfig {
16072 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16073 ..Default::default()
16074 },
16075 Some(tree_sitter_rust::LANGUAGE.into()),
16076 ));
16077 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16078
16079 // If multiple selections intersect a line, the line is only toggled once.
16080 cx.set_state(indoc! {"
16081 fn a() {
16082 «//b();
16083 ˇ»// «c();
16084 //ˇ» d();
16085 }
16086 "});
16087
16088 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16089
16090 cx.assert_editor_state(indoc! {"
16091 fn a() {
16092 «b();
16093 c();
16094 ˇ» d();
16095 }
16096 "});
16097
16098 // The comment prefix is inserted at the same column for every line in a
16099 // selection.
16100 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16101
16102 cx.assert_editor_state(indoc! {"
16103 fn a() {
16104 // «b();
16105 // c();
16106 ˇ»// d();
16107 }
16108 "});
16109
16110 // If a selection ends at the beginning of a line, that line is not toggled.
16111 cx.set_selections_state(indoc! {"
16112 fn a() {
16113 // b();
16114 «// c();
16115 ˇ» // d();
16116 }
16117 "});
16118
16119 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16120
16121 cx.assert_editor_state(indoc! {"
16122 fn a() {
16123 // b();
16124 «c();
16125 ˇ» // d();
16126 }
16127 "});
16128
16129 // If a selection span a single line and is empty, the line is toggled.
16130 cx.set_state(indoc! {"
16131 fn a() {
16132 a();
16133 b();
16134 ˇ
16135 }
16136 "});
16137
16138 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16139
16140 cx.assert_editor_state(indoc! {"
16141 fn a() {
16142 a();
16143 b();
16144 //•ˇ
16145 }
16146 "});
16147
16148 // If a selection span multiple lines, empty lines are not toggled.
16149 cx.set_state(indoc! {"
16150 fn a() {
16151 «a();
16152
16153 c();ˇ»
16154 }
16155 "});
16156
16157 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16158
16159 cx.assert_editor_state(indoc! {"
16160 fn a() {
16161 // «a();
16162
16163 // c();ˇ»
16164 }
16165 "});
16166
16167 // If a selection includes multiple comment prefixes, all lines are uncommented.
16168 cx.set_state(indoc! {"
16169 fn a() {
16170 «// a();
16171 /// b();
16172 //! c();ˇ»
16173 }
16174 "});
16175
16176 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16177
16178 cx.assert_editor_state(indoc! {"
16179 fn a() {
16180 «a();
16181 b();
16182 c();ˇ»
16183 }
16184 "});
16185}
16186
16187#[gpui::test]
16188async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16189 init_test(cx, |_| {});
16190 let mut cx = EditorTestContext::new(cx).await;
16191 let language = Arc::new(Language::new(
16192 LanguageConfig {
16193 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16194 ..Default::default()
16195 },
16196 Some(tree_sitter_rust::LANGUAGE.into()),
16197 ));
16198 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16199
16200 let toggle_comments = &ToggleComments {
16201 advance_downwards: false,
16202 ignore_indent: true,
16203 };
16204
16205 // If multiple selections intersect a line, the line is only toggled once.
16206 cx.set_state(indoc! {"
16207 fn a() {
16208 // «b();
16209 // c();
16210 // ˇ» d();
16211 }
16212 "});
16213
16214 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16215
16216 cx.assert_editor_state(indoc! {"
16217 fn a() {
16218 «b();
16219 c();
16220 ˇ» d();
16221 }
16222 "});
16223
16224 // The comment prefix is inserted at the beginning of each line
16225 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, 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 ends at the beginning of a line, that line is not toggled.
16236 cx.set_selections_state(indoc! {"
16237 fn a() {
16238 // b();
16239 // «c();
16240 ˇ»// d();
16241 }
16242 "});
16243
16244 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16245
16246 cx.assert_editor_state(indoc! {"
16247 fn a() {
16248 // b();
16249 «c();
16250 ˇ»// d();
16251 }
16252 "});
16253
16254 // If a selection span a single line and is empty, the line is toggled.
16255 cx.set_state(indoc! {"
16256 fn a() {
16257 a();
16258 b();
16259 ˇ
16260 }
16261 "});
16262
16263 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16264
16265 cx.assert_editor_state(indoc! {"
16266 fn a() {
16267 a();
16268 b();
16269 //ˇ
16270 }
16271 "});
16272
16273 // If a selection span multiple lines, empty lines are not toggled.
16274 cx.set_state(indoc! {"
16275 fn a() {
16276 «a();
16277
16278 c();ˇ»
16279 }
16280 "});
16281
16282 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16283
16284 cx.assert_editor_state(indoc! {"
16285 fn a() {
16286 // «a();
16287
16288 // c();ˇ»
16289 }
16290 "});
16291
16292 // If a selection includes multiple comment prefixes, all lines are uncommented.
16293 cx.set_state(indoc! {"
16294 fn a() {
16295 // «a();
16296 /// b();
16297 //! c();ˇ»
16298 }
16299 "});
16300
16301 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16302
16303 cx.assert_editor_state(indoc! {"
16304 fn a() {
16305 «a();
16306 b();
16307 c();ˇ»
16308 }
16309 "});
16310}
16311
16312#[gpui::test]
16313async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16314 init_test(cx, |_| {});
16315
16316 let language = Arc::new(Language::new(
16317 LanguageConfig {
16318 line_comments: vec!["// ".into()],
16319 ..Default::default()
16320 },
16321 Some(tree_sitter_rust::LANGUAGE.into()),
16322 ));
16323
16324 let mut cx = EditorTestContext::new(cx).await;
16325
16326 cx.language_registry().add(language.clone());
16327 cx.update_buffer(|buffer, cx| {
16328 buffer.set_language(Some(language), cx);
16329 });
16330
16331 let toggle_comments = &ToggleComments {
16332 advance_downwards: true,
16333 ignore_indent: false,
16334 };
16335
16336 // Single cursor on one line -> advance
16337 // Cursor moves horizontally 3 characters as well on non-blank line
16338 cx.set_state(indoc!(
16339 "fn a() {
16340 ˇdog();
16341 cat();
16342 }"
16343 ));
16344 cx.update_editor(|editor, window, cx| {
16345 editor.toggle_comments(toggle_comments, window, cx);
16346 });
16347 cx.assert_editor_state(indoc!(
16348 "fn a() {
16349 // dog();
16350 catˇ();
16351 }"
16352 ));
16353
16354 // Single selection on one line -> don't advance
16355 cx.set_state(indoc!(
16356 "fn a() {
16357 «dog()ˇ»;
16358 cat();
16359 }"
16360 ));
16361 cx.update_editor(|editor, window, cx| {
16362 editor.toggle_comments(toggle_comments, window, cx);
16363 });
16364 cx.assert_editor_state(indoc!(
16365 "fn a() {
16366 // «dog()ˇ»;
16367 cat();
16368 }"
16369 ));
16370
16371 // Multiple cursors on one line -> advance
16372 cx.set_state(indoc!(
16373 "fn a() {
16374 ˇdˇog();
16375 cat();
16376 }"
16377 ));
16378 cx.update_editor(|editor, window, cx| {
16379 editor.toggle_comments(toggle_comments, window, cx);
16380 });
16381 cx.assert_editor_state(indoc!(
16382 "fn a() {
16383 // dog();
16384 catˇ(ˇ);
16385 }"
16386 ));
16387
16388 // Multiple cursors on one line, with selection -> don't advance
16389 cx.set_state(indoc!(
16390 "fn a() {
16391 ˇdˇog«()ˇ»;
16392 cat();
16393 }"
16394 ));
16395 cx.update_editor(|editor, window, cx| {
16396 editor.toggle_comments(toggle_comments, window, cx);
16397 });
16398 cx.assert_editor_state(indoc!(
16399 "fn a() {
16400 // ˇdˇog«()ˇ»;
16401 cat();
16402 }"
16403 ));
16404
16405 // Single cursor on one line -> advance
16406 // Cursor moves to column 0 on blank line
16407 cx.set_state(indoc!(
16408 "fn a() {
16409 ˇdog();
16410
16411 cat();
16412 }"
16413 ));
16414 cx.update_editor(|editor, window, cx| {
16415 editor.toggle_comments(toggle_comments, window, cx);
16416 });
16417 cx.assert_editor_state(indoc!(
16418 "fn a() {
16419 // dog();
16420 ˇ
16421 cat();
16422 }"
16423 ));
16424
16425 // Single cursor on one line -> advance
16426 // Cursor starts and ends at column 0
16427 cx.set_state(indoc!(
16428 "fn a() {
16429 ˇ dog();
16430 cat();
16431 }"
16432 ));
16433 cx.update_editor(|editor, window, cx| {
16434 editor.toggle_comments(toggle_comments, window, cx);
16435 });
16436 cx.assert_editor_state(indoc!(
16437 "fn a() {
16438 // dog();
16439 ˇ cat();
16440 }"
16441 ));
16442}
16443
16444#[gpui::test]
16445async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16446 init_test(cx, |_| {});
16447
16448 let mut cx = EditorTestContext::new(cx).await;
16449
16450 let html_language = Arc::new(
16451 Language::new(
16452 LanguageConfig {
16453 name: "HTML".into(),
16454 block_comment: Some(BlockCommentConfig {
16455 start: "<!-- ".into(),
16456 prefix: "".into(),
16457 end: " -->".into(),
16458 tab_size: 0,
16459 }),
16460 ..Default::default()
16461 },
16462 Some(tree_sitter_html::LANGUAGE.into()),
16463 )
16464 .with_injection_query(
16465 r#"
16466 (script_element
16467 (raw_text) @injection.content
16468 (#set! injection.language "javascript"))
16469 "#,
16470 )
16471 .unwrap(),
16472 );
16473
16474 let javascript_language = Arc::new(Language::new(
16475 LanguageConfig {
16476 name: "JavaScript".into(),
16477 line_comments: vec!["// ".into()],
16478 ..Default::default()
16479 },
16480 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16481 ));
16482
16483 cx.language_registry().add(html_language.clone());
16484 cx.language_registry().add(javascript_language);
16485 cx.update_buffer(|buffer, cx| {
16486 buffer.set_language(Some(html_language), cx);
16487 });
16488
16489 // Toggle comments for empty selections
16490 cx.set_state(
16491 &r#"
16492 <p>A</p>ˇ
16493 <p>B</p>ˇ
16494 <p>C</p>ˇ
16495 "#
16496 .unindent(),
16497 );
16498 cx.update_editor(|editor, window, cx| {
16499 editor.toggle_comments(&ToggleComments::default(), window, cx)
16500 });
16501 cx.assert_editor_state(
16502 &r#"
16503 <!-- <p>A</p>ˇ -->
16504 <!-- <p>B</p>ˇ -->
16505 <!-- <p>C</p>ˇ -->
16506 "#
16507 .unindent(),
16508 );
16509 cx.update_editor(|editor, window, cx| {
16510 editor.toggle_comments(&ToggleComments::default(), window, cx)
16511 });
16512 cx.assert_editor_state(
16513 &r#"
16514 <p>A</p>ˇ
16515 <p>B</p>ˇ
16516 <p>C</p>ˇ
16517 "#
16518 .unindent(),
16519 );
16520
16521 // Toggle comments for mixture of empty and non-empty selections, where
16522 // multiple selections occupy a given line.
16523 cx.set_state(
16524 &r#"
16525 <p>A«</p>
16526 <p>ˇ»B</p>ˇ
16527 <p>C«</p>
16528 <p>ˇ»D</p>ˇ
16529 "#
16530 .unindent(),
16531 );
16532
16533 cx.update_editor(|editor, window, cx| {
16534 editor.toggle_comments(&ToggleComments::default(), window, cx)
16535 });
16536 cx.assert_editor_state(
16537 &r#"
16538 <!-- <p>A«</p>
16539 <p>ˇ»B</p>ˇ -->
16540 <!-- <p>C«</p>
16541 <p>ˇ»D</p>ˇ -->
16542 "#
16543 .unindent(),
16544 );
16545 cx.update_editor(|editor, window, cx| {
16546 editor.toggle_comments(&ToggleComments::default(), window, cx)
16547 });
16548 cx.assert_editor_state(
16549 &r#"
16550 <p>A«</p>
16551 <p>ˇ»B</p>ˇ
16552 <p>C«</p>
16553 <p>ˇ»D</p>ˇ
16554 "#
16555 .unindent(),
16556 );
16557
16558 // Toggle comments when different languages are active for different
16559 // selections.
16560 cx.set_state(
16561 &r#"
16562 ˇ<script>
16563 ˇvar x = new Y();
16564 ˇ</script>
16565 "#
16566 .unindent(),
16567 );
16568 cx.executor().run_until_parked();
16569 cx.update_editor(|editor, window, cx| {
16570 editor.toggle_comments(&ToggleComments::default(), window, cx)
16571 });
16572 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16573 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16574 cx.assert_editor_state(
16575 &r#"
16576 <!-- ˇ<script> -->
16577 // ˇvar x = new Y();
16578 <!-- ˇ</script> -->
16579 "#
16580 .unindent(),
16581 );
16582}
16583
16584#[gpui::test]
16585fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16586 init_test(cx, |_| {});
16587
16588 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16589 let multibuffer = cx.new(|cx| {
16590 let mut multibuffer = MultiBuffer::new(ReadWrite);
16591 multibuffer.push_excerpts(
16592 buffer.clone(),
16593 [
16594 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16595 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16596 ],
16597 cx,
16598 );
16599 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16600 multibuffer
16601 });
16602
16603 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16604 editor.update_in(cx, |editor, window, cx| {
16605 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16606 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16607 s.select_ranges([
16608 Point::new(0, 0)..Point::new(0, 0),
16609 Point::new(1, 0)..Point::new(1, 0),
16610 ])
16611 });
16612
16613 editor.handle_input("X", window, cx);
16614 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16615 assert_eq!(
16616 editor.selections.ranges(&editor.display_snapshot(cx)),
16617 [
16618 Point::new(0, 1)..Point::new(0, 1),
16619 Point::new(1, 1)..Point::new(1, 1),
16620 ]
16621 );
16622
16623 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16624 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16625 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16626 });
16627 editor.backspace(&Default::default(), window, cx);
16628 assert_eq!(editor.text(cx), "Xa\nbbb");
16629 assert_eq!(
16630 editor.selections.ranges(&editor.display_snapshot(cx)),
16631 [Point::new(1, 0)..Point::new(1, 0)]
16632 );
16633
16634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16635 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16636 });
16637 editor.backspace(&Default::default(), window, cx);
16638 assert_eq!(editor.text(cx), "X\nbb");
16639 assert_eq!(
16640 editor.selections.ranges(&editor.display_snapshot(cx)),
16641 [Point::new(0, 1)..Point::new(0, 1)]
16642 );
16643 });
16644}
16645
16646#[gpui::test]
16647fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16648 init_test(cx, |_| {});
16649
16650 let markers = vec![('[', ']').into(), ('(', ')').into()];
16651 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16652 indoc! {"
16653 [aaaa
16654 (bbbb]
16655 cccc)",
16656 },
16657 markers.clone(),
16658 );
16659 let excerpt_ranges = markers.into_iter().map(|marker| {
16660 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16661 ExcerptRange::new(context)
16662 });
16663 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16664 let multibuffer = cx.new(|cx| {
16665 let mut multibuffer = MultiBuffer::new(ReadWrite);
16666 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16667 multibuffer
16668 });
16669
16670 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16671 editor.update_in(cx, |editor, window, cx| {
16672 let (expected_text, selection_ranges) = marked_text_ranges(
16673 indoc! {"
16674 aaaa
16675 bˇbbb
16676 bˇbbˇb
16677 cccc"
16678 },
16679 true,
16680 );
16681 assert_eq!(editor.text(cx), expected_text);
16682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16683 s.select_ranges(
16684 selection_ranges
16685 .iter()
16686 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16687 )
16688 });
16689
16690 editor.handle_input("X", window, cx);
16691
16692 let (expected_text, expected_selections) = marked_text_ranges(
16693 indoc! {"
16694 aaaa
16695 bXˇbbXb
16696 bXˇbbXˇb
16697 cccc"
16698 },
16699 false,
16700 );
16701 assert_eq!(editor.text(cx), expected_text);
16702 assert_eq!(
16703 editor.selections.ranges(&editor.display_snapshot(cx)),
16704 expected_selections
16705 .iter()
16706 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16707 .collect::<Vec<_>>()
16708 );
16709
16710 editor.newline(&Newline, window, cx);
16711 let (expected_text, expected_selections) = marked_text_ranges(
16712 indoc! {"
16713 aaaa
16714 bX
16715 ˇbbX
16716 b
16717 bX
16718 ˇbbX
16719 ˇb
16720 cccc"
16721 },
16722 false,
16723 );
16724 assert_eq!(editor.text(cx), expected_text);
16725 assert_eq!(
16726 editor.selections.ranges(&editor.display_snapshot(cx)),
16727 expected_selections
16728 .iter()
16729 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16730 .collect::<Vec<_>>()
16731 );
16732 });
16733}
16734
16735#[gpui::test]
16736fn test_refresh_selections(cx: &mut TestAppContext) {
16737 init_test(cx, |_| {});
16738
16739 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16740 let mut excerpt1_id = None;
16741 let multibuffer = cx.new(|cx| {
16742 let mut multibuffer = MultiBuffer::new(ReadWrite);
16743 excerpt1_id = multibuffer
16744 .push_excerpts(
16745 buffer.clone(),
16746 [
16747 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16748 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16749 ],
16750 cx,
16751 )
16752 .into_iter()
16753 .next();
16754 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16755 multibuffer
16756 });
16757
16758 let editor = cx.add_window(|window, cx| {
16759 let mut editor = build_editor(multibuffer.clone(), window, cx);
16760 let snapshot = editor.snapshot(window, cx);
16761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16762 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16763 });
16764 editor.begin_selection(
16765 Point::new(2, 1).to_display_point(&snapshot),
16766 true,
16767 1,
16768 window,
16769 cx,
16770 );
16771 assert_eq!(
16772 editor.selections.ranges(&editor.display_snapshot(cx)),
16773 [
16774 Point::new(1, 3)..Point::new(1, 3),
16775 Point::new(2, 1)..Point::new(2, 1),
16776 ]
16777 );
16778 editor
16779 });
16780
16781 // Refreshing selections is a no-op when excerpts haven't changed.
16782 _ = editor.update(cx, |editor, window, cx| {
16783 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16784 assert_eq!(
16785 editor.selections.ranges(&editor.display_snapshot(cx)),
16786 [
16787 Point::new(1, 3)..Point::new(1, 3),
16788 Point::new(2, 1)..Point::new(2, 1),
16789 ]
16790 );
16791 });
16792
16793 multibuffer.update(cx, |multibuffer, cx| {
16794 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16795 });
16796 _ = editor.update(cx, |editor, window, cx| {
16797 // Removing an excerpt causes the first selection to become degenerate.
16798 assert_eq!(
16799 editor.selections.ranges(&editor.display_snapshot(cx)),
16800 [
16801 Point::new(0, 0)..Point::new(0, 0),
16802 Point::new(0, 1)..Point::new(0, 1)
16803 ]
16804 );
16805
16806 // Refreshing selections will relocate the first selection to the original buffer
16807 // location.
16808 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16809 assert_eq!(
16810 editor.selections.ranges(&editor.display_snapshot(cx)),
16811 [
16812 Point::new(0, 1)..Point::new(0, 1),
16813 Point::new(0, 3)..Point::new(0, 3)
16814 ]
16815 );
16816 assert!(editor.selections.pending_anchor().is_some());
16817 });
16818}
16819
16820#[gpui::test]
16821fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16822 init_test(cx, |_| {});
16823
16824 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16825 let mut excerpt1_id = None;
16826 let multibuffer = cx.new(|cx| {
16827 let mut multibuffer = MultiBuffer::new(ReadWrite);
16828 excerpt1_id = multibuffer
16829 .push_excerpts(
16830 buffer.clone(),
16831 [
16832 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16833 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16834 ],
16835 cx,
16836 )
16837 .into_iter()
16838 .next();
16839 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16840 multibuffer
16841 });
16842
16843 let editor = cx.add_window(|window, cx| {
16844 let mut editor = build_editor(multibuffer.clone(), window, cx);
16845 let snapshot = editor.snapshot(window, cx);
16846 editor.begin_selection(
16847 Point::new(1, 3).to_display_point(&snapshot),
16848 false,
16849 1,
16850 window,
16851 cx,
16852 );
16853 assert_eq!(
16854 editor.selections.ranges(&editor.display_snapshot(cx)),
16855 [Point::new(1, 3)..Point::new(1, 3)]
16856 );
16857 editor
16858 });
16859
16860 multibuffer.update(cx, |multibuffer, cx| {
16861 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16862 });
16863 _ = editor.update(cx, |editor, window, cx| {
16864 assert_eq!(
16865 editor.selections.ranges(&editor.display_snapshot(cx)),
16866 [Point::new(0, 0)..Point::new(0, 0)]
16867 );
16868
16869 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16871 assert_eq!(
16872 editor.selections.ranges(&editor.display_snapshot(cx)),
16873 [Point::new(0, 3)..Point::new(0, 3)]
16874 );
16875 assert!(editor.selections.pending_anchor().is_some());
16876 });
16877}
16878
16879#[gpui::test]
16880async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16881 init_test(cx, |_| {});
16882
16883 let language = Arc::new(
16884 Language::new(
16885 LanguageConfig {
16886 brackets: BracketPairConfig {
16887 pairs: vec![
16888 BracketPair {
16889 start: "{".to_string(),
16890 end: "}".to_string(),
16891 close: true,
16892 surround: true,
16893 newline: true,
16894 },
16895 BracketPair {
16896 start: "/* ".to_string(),
16897 end: " */".to_string(),
16898 close: true,
16899 surround: true,
16900 newline: true,
16901 },
16902 ],
16903 ..Default::default()
16904 },
16905 ..Default::default()
16906 },
16907 Some(tree_sitter_rust::LANGUAGE.into()),
16908 )
16909 .with_indents_query("")
16910 .unwrap(),
16911 );
16912
16913 let text = concat!(
16914 "{ }\n", //
16915 " x\n", //
16916 " /* */\n", //
16917 "x\n", //
16918 "{{} }\n", //
16919 );
16920
16921 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16922 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16923 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16924 editor
16925 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16926 .await;
16927
16928 editor.update_in(cx, |editor, window, cx| {
16929 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16930 s.select_display_ranges([
16931 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16932 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16933 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16934 ])
16935 });
16936 editor.newline(&Newline, window, cx);
16937
16938 assert_eq!(
16939 editor.buffer().read(cx).read(cx).text(),
16940 concat!(
16941 "{ \n", // Suppress rustfmt
16942 "\n", //
16943 "}\n", //
16944 " x\n", //
16945 " /* \n", //
16946 " \n", //
16947 " */\n", //
16948 "x\n", //
16949 "{{} \n", //
16950 "}\n", //
16951 )
16952 );
16953 });
16954}
16955
16956#[gpui::test]
16957fn test_highlighted_ranges(cx: &mut TestAppContext) {
16958 init_test(cx, |_| {});
16959
16960 let editor = cx.add_window(|window, cx| {
16961 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16962 build_editor(buffer, window, cx)
16963 });
16964
16965 _ = editor.update(cx, |editor, window, cx| {
16966 struct Type1;
16967 struct Type2;
16968
16969 let buffer = editor.buffer.read(cx).snapshot(cx);
16970
16971 let anchor_range =
16972 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16973
16974 editor.highlight_background::<Type1>(
16975 &[
16976 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16977 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16978 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16979 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16980 ],
16981 |_| Hsla::red(),
16982 cx,
16983 );
16984 editor.highlight_background::<Type2>(
16985 &[
16986 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16987 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16988 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16989 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16990 ],
16991 |_| Hsla::green(),
16992 cx,
16993 );
16994
16995 let snapshot = editor.snapshot(window, cx);
16996 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16997 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16998 &snapshot,
16999 cx.theme(),
17000 );
17001 assert_eq!(
17002 highlighted_ranges,
17003 &[
17004 (
17005 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17006 Hsla::green(),
17007 ),
17008 (
17009 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17010 Hsla::red(),
17011 ),
17012 (
17013 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17014 Hsla::green(),
17015 ),
17016 (
17017 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17018 Hsla::red(),
17019 ),
17020 ]
17021 );
17022 assert_eq!(
17023 editor.sorted_background_highlights_in_range(
17024 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17025 &snapshot,
17026 cx.theme(),
17027 ),
17028 &[(
17029 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17030 Hsla::red(),
17031 )]
17032 );
17033 });
17034}
17035
17036#[gpui::test]
17037async fn test_following(cx: &mut TestAppContext) {
17038 init_test(cx, |_| {});
17039
17040 let fs = FakeFs::new(cx.executor());
17041 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17042
17043 let buffer = project.update(cx, |project, cx| {
17044 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17045 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17046 });
17047 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17048 let follower = cx.update(|cx| {
17049 cx.open_window(
17050 WindowOptions {
17051 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17052 gpui::Point::new(px(0.), px(0.)),
17053 gpui::Point::new(px(10.), px(80.)),
17054 ))),
17055 ..Default::default()
17056 },
17057 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17058 )
17059 .unwrap()
17060 });
17061
17062 let is_still_following = Rc::new(RefCell::new(true));
17063 let follower_edit_event_count = Rc::new(RefCell::new(0));
17064 let pending_update = Rc::new(RefCell::new(None));
17065 let leader_entity = leader.root(cx).unwrap();
17066 let follower_entity = follower.root(cx).unwrap();
17067 _ = follower.update(cx, {
17068 let update = pending_update.clone();
17069 let is_still_following = is_still_following.clone();
17070 let follower_edit_event_count = follower_edit_event_count.clone();
17071 |_, window, cx| {
17072 cx.subscribe_in(
17073 &leader_entity,
17074 window,
17075 move |_, leader, event, window, cx| {
17076 leader.read(cx).add_event_to_update_proto(
17077 event,
17078 &mut update.borrow_mut(),
17079 window,
17080 cx,
17081 );
17082 },
17083 )
17084 .detach();
17085
17086 cx.subscribe_in(
17087 &follower_entity,
17088 window,
17089 move |_, _, event: &EditorEvent, _window, _cx| {
17090 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17091 *is_still_following.borrow_mut() = false;
17092 }
17093
17094 if let EditorEvent::BufferEdited = event {
17095 *follower_edit_event_count.borrow_mut() += 1;
17096 }
17097 },
17098 )
17099 .detach();
17100 }
17101 });
17102
17103 // Update the selections only
17104 _ = leader.update(cx, |leader, window, cx| {
17105 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17106 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17107 });
17108 });
17109 follower
17110 .update(cx, |follower, window, cx| {
17111 follower.apply_update_proto(
17112 &project,
17113 pending_update.borrow_mut().take().unwrap(),
17114 window,
17115 cx,
17116 )
17117 })
17118 .unwrap()
17119 .await
17120 .unwrap();
17121 _ = follower.update(cx, |follower, _, cx| {
17122 assert_eq!(
17123 follower.selections.ranges(&follower.display_snapshot(cx)),
17124 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17125 );
17126 });
17127 assert!(*is_still_following.borrow());
17128 assert_eq!(*follower_edit_event_count.borrow(), 0);
17129
17130 // Update the scroll position only
17131 _ = leader.update(cx, |leader, window, cx| {
17132 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17133 });
17134 follower
17135 .update(cx, |follower, window, cx| {
17136 follower.apply_update_proto(
17137 &project,
17138 pending_update.borrow_mut().take().unwrap(),
17139 window,
17140 cx,
17141 )
17142 })
17143 .unwrap()
17144 .await
17145 .unwrap();
17146 assert_eq!(
17147 follower
17148 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17149 .unwrap(),
17150 gpui::Point::new(1.5, 3.5)
17151 );
17152 assert!(*is_still_following.borrow());
17153 assert_eq!(*follower_edit_event_count.borrow(), 0);
17154
17155 // Update the selections and scroll position. The follower's scroll position is updated
17156 // via autoscroll, not via the leader's exact scroll position.
17157 _ = leader.update(cx, |leader, window, cx| {
17158 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17159 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17160 });
17161 leader.request_autoscroll(Autoscroll::newest(), cx);
17162 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17163 });
17164 follower
17165 .update(cx, |follower, window, cx| {
17166 follower.apply_update_proto(
17167 &project,
17168 pending_update.borrow_mut().take().unwrap(),
17169 window,
17170 cx,
17171 )
17172 })
17173 .unwrap()
17174 .await
17175 .unwrap();
17176 _ = follower.update(cx, |follower, _, cx| {
17177 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17178 assert_eq!(
17179 follower.selections.ranges(&follower.display_snapshot(cx)),
17180 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17181 );
17182 });
17183 assert!(*is_still_following.borrow());
17184
17185 // Creating a pending selection that precedes another selection
17186 _ = leader.update(cx, |leader, window, cx| {
17187 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17188 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17189 });
17190 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17191 });
17192 follower
17193 .update(cx, |follower, window, cx| {
17194 follower.apply_update_proto(
17195 &project,
17196 pending_update.borrow_mut().take().unwrap(),
17197 window,
17198 cx,
17199 )
17200 })
17201 .unwrap()
17202 .await
17203 .unwrap();
17204 _ = follower.update(cx, |follower, _, cx| {
17205 assert_eq!(
17206 follower.selections.ranges(&follower.display_snapshot(cx)),
17207 vec![
17208 MultiBufferOffset(0)..MultiBufferOffset(0),
17209 MultiBufferOffset(1)..MultiBufferOffset(1)
17210 ]
17211 );
17212 });
17213 assert!(*is_still_following.borrow());
17214
17215 // Extend the pending selection so that it surrounds another selection
17216 _ = leader.update(cx, |leader, window, cx| {
17217 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17218 });
17219 follower
17220 .update(cx, |follower, window, cx| {
17221 follower.apply_update_proto(
17222 &project,
17223 pending_update.borrow_mut().take().unwrap(),
17224 window,
17225 cx,
17226 )
17227 })
17228 .unwrap()
17229 .await
17230 .unwrap();
17231 _ = follower.update(cx, |follower, _, cx| {
17232 assert_eq!(
17233 follower.selections.ranges(&follower.display_snapshot(cx)),
17234 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17235 );
17236 });
17237
17238 // Scrolling locally breaks the follow
17239 _ = follower.update(cx, |follower, window, cx| {
17240 let top_anchor = follower
17241 .buffer()
17242 .read(cx)
17243 .read(cx)
17244 .anchor_after(MultiBufferOffset(0));
17245 follower.set_scroll_anchor(
17246 ScrollAnchor {
17247 anchor: top_anchor,
17248 offset: gpui::Point::new(0.0, 0.5),
17249 },
17250 window,
17251 cx,
17252 );
17253 });
17254 assert!(!(*is_still_following.borrow()));
17255}
17256
17257#[gpui::test]
17258async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17259 init_test(cx, |_| {});
17260
17261 let fs = FakeFs::new(cx.executor());
17262 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17263 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17264 let pane = workspace
17265 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17266 .unwrap();
17267
17268 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17269
17270 let leader = pane.update_in(cx, |_, window, cx| {
17271 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17272 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17273 });
17274
17275 // Start following the editor when it has no excerpts.
17276 let mut state_message =
17277 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17278 let workspace_entity = workspace.root(cx).unwrap();
17279 let follower_1 = cx
17280 .update_window(*workspace.deref(), |_, window, cx| {
17281 Editor::from_state_proto(
17282 workspace_entity,
17283 ViewId {
17284 creator: CollaboratorId::PeerId(PeerId::default()),
17285 id: 0,
17286 },
17287 &mut state_message,
17288 window,
17289 cx,
17290 )
17291 })
17292 .unwrap()
17293 .unwrap()
17294 .await
17295 .unwrap();
17296
17297 let update_message = Rc::new(RefCell::new(None));
17298 follower_1.update_in(cx, {
17299 let update = update_message.clone();
17300 |_, window, cx| {
17301 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17302 leader.read(cx).add_event_to_update_proto(
17303 event,
17304 &mut update.borrow_mut(),
17305 window,
17306 cx,
17307 );
17308 })
17309 .detach();
17310 }
17311 });
17312
17313 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17314 (
17315 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17316 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17317 )
17318 });
17319
17320 // Insert some excerpts.
17321 leader.update(cx, |leader, cx| {
17322 leader.buffer.update(cx, |multibuffer, cx| {
17323 multibuffer.set_excerpts_for_path(
17324 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17325 buffer_1.clone(),
17326 vec![
17327 Point::row_range(0..3),
17328 Point::row_range(1..6),
17329 Point::row_range(12..15),
17330 ],
17331 0,
17332 cx,
17333 );
17334 multibuffer.set_excerpts_for_path(
17335 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17336 buffer_2.clone(),
17337 vec![Point::row_range(0..6), Point::row_range(8..12)],
17338 0,
17339 cx,
17340 );
17341 });
17342 });
17343
17344 // Apply the update of adding the excerpts.
17345 follower_1
17346 .update_in(cx, |follower, window, cx| {
17347 follower.apply_update_proto(
17348 &project,
17349 update_message.borrow().clone().unwrap(),
17350 window,
17351 cx,
17352 )
17353 })
17354 .await
17355 .unwrap();
17356 assert_eq!(
17357 follower_1.update(cx, |editor, cx| editor.text(cx)),
17358 leader.update(cx, |editor, cx| editor.text(cx))
17359 );
17360 update_message.borrow_mut().take();
17361
17362 // Start following separately after it already has excerpts.
17363 let mut state_message =
17364 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17365 let workspace_entity = workspace.root(cx).unwrap();
17366 let follower_2 = cx
17367 .update_window(*workspace.deref(), |_, window, cx| {
17368 Editor::from_state_proto(
17369 workspace_entity,
17370 ViewId {
17371 creator: CollaboratorId::PeerId(PeerId::default()),
17372 id: 0,
17373 },
17374 &mut state_message,
17375 window,
17376 cx,
17377 )
17378 })
17379 .unwrap()
17380 .unwrap()
17381 .await
17382 .unwrap();
17383 assert_eq!(
17384 follower_2.update(cx, |editor, cx| editor.text(cx)),
17385 leader.update(cx, |editor, cx| editor.text(cx))
17386 );
17387
17388 // Remove some excerpts.
17389 leader.update(cx, |leader, cx| {
17390 leader.buffer.update(cx, |multibuffer, cx| {
17391 let excerpt_ids = multibuffer.excerpt_ids();
17392 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17393 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17394 });
17395 });
17396
17397 // Apply the update of removing the excerpts.
17398 follower_1
17399 .update_in(cx, |follower, window, cx| {
17400 follower.apply_update_proto(
17401 &project,
17402 update_message.borrow().clone().unwrap(),
17403 window,
17404 cx,
17405 )
17406 })
17407 .await
17408 .unwrap();
17409 follower_2
17410 .update_in(cx, |follower, window, cx| {
17411 follower.apply_update_proto(
17412 &project,
17413 update_message.borrow().clone().unwrap(),
17414 window,
17415 cx,
17416 )
17417 })
17418 .await
17419 .unwrap();
17420 update_message.borrow_mut().take();
17421 assert_eq!(
17422 follower_1.update(cx, |editor, cx| editor.text(cx)),
17423 leader.update(cx, |editor, cx| editor.text(cx))
17424 );
17425}
17426
17427#[gpui::test]
17428async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17429 init_test(cx, |_| {});
17430
17431 let mut cx = EditorTestContext::new(cx).await;
17432 let lsp_store =
17433 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17434
17435 cx.set_state(indoc! {"
17436 ˇfn func(abc def: i32) -> u32 {
17437 }
17438 "});
17439
17440 cx.update(|_, cx| {
17441 lsp_store.update(cx, |lsp_store, cx| {
17442 lsp_store
17443 .update_diagnostics(
17444 LanguageServerId(0),
17445 lsp::PublishDiagnosticsParams {
17446 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17447 version: None,
17448 diagnostics: vec![
17449 lsp::Diagnostic {
17450 range: lsp::Range::new(
17451 lsp::Position::new(0, 11),
17452 lsp::Position::new(0, 12),
17453 ),
17454 severity: Some(lsp::DiagnosticSeverity::ERROR),
17455 ..Default::default()
17456 },
17457 lsp::Diagnostic {
17458 range: lsp::Range::new(
17459 lsp::Position::new(0, 12),
17460 lsp::Position::new(0, 15),
17461 ),
17462 severity: Some(lsp::DiagnosticSeverity::ERROR),
17463 ..Default::default()
17464 },
17465 lsp::Diagnostic {
17466 range: lsp::Range::new(
17467 lsp::Position::new(0, 25),
17468 lsp::Position::new(0, 28),
17469 ),
17470 severity: Some(lsp::DiagnosticSeverity::ERROR),
17471 ..Default::default()
17472 },
17473 ],
17474 },
17475 None,
17476 DiagnosticSourceKind::Pushed,
17477 &[],
17478 cx,
17479 )
17480 .unwrap()
17481 });
17482 });
17483
17484 executor.run_until_parked();
17485
17486 cx.update_editor(|editor, window, cx| {
17487 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17488 });
17489
17490 cx.assert_editor_state(indoc! {"
17491 fn func(abc def: i32) -> ˇu32 {
17492 }
17493 "});
17494
17495 cx.update_editor(|editor, window, cx| {
17496 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17497 });
17498
17499 cx.assert_editor_state(indoc! {"
17500 fn func(abc ˇdef: i32) -> u32 {
17501 }
17502 "});
17503
17504 cx.update_editor(|editor, window, cx| {
17505 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17506 });
17507
17508 cx.assert_editor_state(indoc! {"
17509 fn func(abcˇ def: i32) -> u32 {
17510 }
17511 "});
17512
17513 cx.update_editor(|editor, window, cx| {
17514 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17515 });
17516
17517 cx.assert_editor_state(indoc! {"
17518 fn func(abc def: i32) -> ˇu32 {
17519 }
17520 "});
17521}
17522
17523#[gpui::test]
17524async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17525 init_test(cx, |_| {});
17526
17527 let mut cx = EditorTestContext::new(cx).await;
17528
17529 let diff_base = r#"
17530 use some::mod;
17531
17532 const A: u32 = 42;
17533
17534 fn main() {
17535 println!("hello");
17536
17537 println!("world");
17538 }
17539 "#
17540 .unindent();
17541
17542 // Edits are modified, removed, modified, added
17543 cx.set_state(
17544 &r#"
17545 use some::modified;
17546
17547 ˇ
17548 fn main() {
17549 println!("hello there");
17550
17551 println!("around the");
17552 println!("world");
17553 }
17554 "#
17555 .unindent(),
17556 );
17557
17558 cx.set_head_text(&diff_base);
17559 executor.run_until_parked();
17560
17561 cx.update_editor(|editor, window, cx| {
17562 //Wrap around the bottom of the buffer
17563 for _ in 0..3 {
17564 editor.go_to_next_hunk(&GoToHunk, window, cx);
17565 }
17566 });
17567
17568 cx.assert_editor_state(
17569 &r#"
17570 ˇuse some::modified;
17571
17572
17573 fn main() {
17574 println!("hello there");
17575
17576 println!("around the");
17577 println!("world");
17578 }
17579 "#
17580 .unindent(),
17581 );
17582
17583 cx.update_editor(|editor, window, cx| {
17584 //Wrap around the top of the buffer
17585 for _ in 0..2 {
17586 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17587 }
17588 });
17589
17590 cx.assert_editor_state(
17591 &r#"
17592 use some::modified;
17593
17594
17595 fn main() {
17596 ˇ println!("hello there");
17597
17598 println!("around the");
17599 println!("world");
17600 }
17601 "#
17602 .unindent(),
17603 );
17604
17605 cx.update_editor(|editor, window, cx| {
17606 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17607 });
17608
17609 cx.assert_editor_state(
17610 &r#"
17611 use some::modified;
17612
17613 ˇ
17614 fn main() {
17615 println!("hello there");
17616
17617 println!("around the");
17618 println!("world");
17619 }
17620 "#
17621 .unindent(),
17622 );
17623
17624 cx.update_editor(|editor, window, cx| {
17625 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17626 });
17627
17628 cx.assert_editor_state(
17629 &r#"
17630 ˇuse some::modified;
17631
17632
17633 fn main() {
17634 println!("hello there");
17635
17636 println!("around the");
17637 println!("world");
17638 }
17639 "#
17640 .unindent(),
17641 );
17642
17643 cx.update_editor(|editor, window, cx| {
17644 for _ in 0..2 {
17645 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17646 }
17647 });
17648
17649 cx.assert_editor_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.update_editor(|editor, window, cx| {
17665 editor.fold(&Fold, window, cx);
17666 });
17667
17668 cx.update_editor(|editor, window, cx| {
17669 editor.go_to_next_hunk(&GoToHunk, window, cx);
17670 });
17671
17672 cx.assert_editor_state(
17673 &r#"
17674 ˇuse some::modified;
17675
17676
17677 fn main() {
17678 println!("hello there");
17679
17680 println!("around the");
17681 println!("world");
17682 }
17683 "#
17684 .unindent(),
17685 );
17686}
17687
17688#[test]
17689fn test_split_words() {
17690 fn split(text: &str) -> Vec<&str> {
17691 split_words(text).collect()
17692 }
17693
17694 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17695 assert_eq!(split("hello_world"), &["hello_", "world"]);
17696 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17697 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17698 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17699 assert_eq!(split("helloworld"), &["helloworld"]);
17700
17701 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17702}
17703
17704#[test]
17705fn test_split_words_for_snippet_prefix() {
17706 fn split(text: &str) -> Vec<&str> {
17707 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17708 }
17709
17710 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17711 assert_eq!(split("hello_world"), &["hello_world"]);
17712 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17713 assert_eq!(split("Hello_World"), &["Hello_World"]);
17714 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17715 assert_eq!(split("helloworld"), &["helloworld"]);
17716 assert_eq!(
17717 split("this@is!@#$^many . symbols"),
17718 &[
17719 "symbols",
17720 " symbols",
17721 ". symbols",
17722 " . symbols",
17723 " . symbols",
17724 " . symbols",
17725 "many . symbols",
17726 "^many . symbols",
17727 "$^many . symbols",
17728 "#$^many . symbols",
17729 "@#$^many . symbols",
17730 "!@#$^many . symbols",
17731 "is!@#$^many . symbols",
17732 "@is!@#$^many . symbols",
17733 "this@is!@#$^many . symbols",
17734 ],
17735 );
17736 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17737}
17738
17739#[gpui::test]
17740async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17741 init_test(cx, |_| {});
17742
17743 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17744
17745 #[track_caller]
17746 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17747 let _state_context = cx.set_state(before);
17748 cx.run_until_parked();
17749 cx.update_editor(|editor, window, cx| {
17750 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17751 });
17752 cx.run_until_parked();
17753 cx.assert_editor_state(after);
17754 }
17755
17756 // Outside bracket jumps to outside of matching bracket
17757 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17758 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17759
17760 // Inside bracket jumps to inside of matching bracket
17761 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17762 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17763
17764 // When outside a bracket and inside, favor jumping to the inside bracket
17765 assert(
17766 "console.log('foo', [1, 2, 3]ˇ);",
17767 "console.log('foo', ˇ[1, 2, 3]);",
17768 &mut cx,
17769 );
17770 assert(
17771 "console.log(ˇ'foo', [1, 2, 3]);",
17772 "console.log('foo'ˇ, [1, 2, 3]);",
17773 &mut cx,
17774 );
17775
17776 // Bias forward if two options are equally likely
17777 assert(
17778 "let result = curried_fun()ˇ();",
17779 "let result = curried_fun()()ˇ;",
17780 &mut cx,
17781 );
17782
17783 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17784 assert(
17785 indoc! {"
17786 function test() {
17787 console.log('test')ˇ
17788 }"},
17789 indoc! {"
17790 function test() {
17791 console.logˇ('test')
17792 }"},
17793 &mut cx,
17794 );
17795}
17796
17797#[gpui::test]
17798async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
17799 init_test(cx, |_| {});
17800 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
17801 language_registry.add(markdown_lang());
17802 language_registry.add(rust_lang());
17803 let buffer = cx.new(|cx| {
17804 let mut buffer = language::Buffer::local(
17805 indoc! {"
17806 ```rs
17807 impl Worktree {
17808 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17809 }
17810 }
17811 ```
17812 "},
17813 cx,
17814 );
17815 buffer.set_language_registry(language_registry.clone());
17816 buffer.set_language(Some(markdown_lang()), cx);
17817 buffer
17818 });
17819 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17820 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17821 cx.executor().run_until_parked();
17822 _ = editor.update(cx, |editor, window, cx| {
17823 // Case 1: Test outer enclosing brackets
17824 select_ranges(
17825 editor,
17826 &indoc! {"
17827 ```rs
17828 impl Worktree {
17829 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17830 }
17831 }ˇ
17832 ```
17833 "},
17834 window,
17835 cx,
17836 );
17837 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17838 assert_text_with_selections(
17839 editor,
17840 &indoc! {"
17841 ```rs
17842 impl Worktree ˇ{
17843 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17844 }
17845 }
17846 ```
17847 "},
17848 cx,
17849 );
17850 // Case 2: Test inner enclosing brackets
17851 select_ranges(
17852 editor,
17853 &indoc! {"
17854 ```rs
17855 impl Worktree {
17856 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17857 }ˇ
17858 }
17859 ```
17860 "},
17861 window,
17862 cx,
17863 );
17864 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17865 assert_text_with_selections(
17866 editor,
17867 &indoc! {"
17868 ```rs
17869 impl Worktree {
17870 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
17871 }
17872 }
17873 ```
17874 "},
17875 cx,
17876 );
17877 });
17878}
17879
17880#[gpui::test]
17881async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17882 init_test(cx, |_| {});
17883
17884 let fs = FakeFs::new(cx.executor());
17885 fs.insert_tree(
17886 path!("/a"),
17887 json!({
17888 "main.rs": "fn main() { let a = 5; }",
17889 "other.rs": "// Test file",
17890 }),
17891 )
17892 .await;
17893 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17894
17895 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17896 language_registry.add(Arc::new(Language::new(
17897 LanguageConfig {
17898 name: "Rust".into(),
17899 matcher: LanguageMatcher {
17900 path_suffixes: vec!["rs".to_string()],
17901 ..Default::default()
17902 },
17903 brackets: BracketPairConfig {
17904 pairs: vec![BracketPair {
17905 start: "{".to_string(),
17906 end: "}".to_string(),
17907 close: true,
17908 surround: true,
17909 newline: true,
17910 }],
17911 disabled_scopes_by_bracket_ix: Vec::new(),
17912 },
17913 ..Default::default()
17914 },
17915 Some(tree_sitter_rust::LANGUAGE.into()),
17916 )));
17917 let mut fake_servers = language_registry.register_fake_lsp(
17918 "Rust",
17919 FakeLspAdapter {
17920 capabilities: lsp::ServerCapabilities {
17921 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17922 first_trigger_character: "{".to_string(),
17923 more_trigger_character: None,
17924 }),
17925 ..Default::default()
17926 },
17927 ..Default::default()
17928 },
17929 );
17930
17931 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17932
17933 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17934
17935 let worktree_id = workspace
17936 .update(cx, |workspace, _, cx| {
17937 workspace.project().update(cx, |project, cx| {
17938 project.worktrees(cx).next().unwrap().read(cx).id()
17939 })
17940 })
17941 .unwrap();
17942
17943 let buffer = project
17944 .update(cx, |project, cx| {
17945 project.open_local_buffer(path!("/a/main.rs"), cx)
17946 })
17947 .await
17948 .unwrap();
17949 let editor_handle = workspace
17950 .update(cx, |workspace, window, cx| {
17951 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17952 })
17953 .unwrap()
17954 .await
17955 .unwrap()
17956 .downcast::<Editor>()
17957 .unwrap();
17958
17959 cx.executor().start_waiting();
17960 let fake_server = fake_servers.next().await.unwrap();
17961
17962 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17963 |params, _| async move {
17964 assert_eq!(
17965 params.text_document_position.text_document.uri,
17966 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17967 );
17968 assert_eq!(
17969 params.text_document_position.position,
17970 lsp::Position::new(0, 21),
17971 );
17972
17973 Ok(Some(vec![lsp::TextEdit {
17974 new_text: "]".to_string(),
17975 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17976 }]))
17977 },
17978 );
17979
17980 editor_handle.update_in(cx, |editor, window, cx| {
17981 window.focus(&editor.focus_handle(cx));
17982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17983 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17984 });
17985 editor.handle_input("{", window, cx);
17986 });
17987
17988 cx.executor().run_until_parked();
17989
17990 buffer.update(cx, |buffer, _| {
17991 assert_eq!(
17992 buffer.text(),
17993 "fn main() { let a = {5}; }",
17994 "No extra braces from on type formatting should appear in the buffer"
17995 )
17996 });
17997}
17998
17999#[gpui::test(iterations = 20, seeds(31))]
18000async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18001 init_test(cx, |_| {});
18002
18003 let mut cx = EditorLspTestContext::new_rust(
18004 lsp::ServerCapabilities {
18005 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18006 first_trigger_character: ".".to_string(),
18007 more_trigger_character: None,
18008 }),
18009 ..Default::default()
18010 },
18011 cx,
18012 )
18013 .await;
18014
18015 cx.update_buffer(|buffer, _| {
18016 // This causes autoindent to be async.
18017 buffer.set_sync_parse_timeout(Duration::ZERO)
18018 });
18019
18020 cx.set_state("fn c() {\n d()ˇ\n}\n");
18021 cx.simulate_keystroke("\n");
18022 cx.run_until_parked();
18023
18024 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18025 let mut request =
18026 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18027 let buffer_cloned = buffer_cloned.clone();
18028 async move {
18029 buffer_cloned.update(&mut cx, |buffer, _| {
18030 assert_eq!(
18031 buffer.text(),
18032 "fn c() {\n d()\n .\n}\n",
18033 "OnTypeFormatting should triggered after autoindent applied"
18034 )
18035 })?;
18036
18037 Ok(Some(vec![]))
18038 }
18039 });
18040
18041 cx.simulate_keystroke(".");
18042 cx.run_until_parked();
18043
18044 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18045 assert!(request.next().await.is_some());
18046 request.close();
18047 assert!(request.next().await.is_none());
18048}
18049
18050#[gpui::test]
18051async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18052 init_test(cx, |_| {});
18053
18054 let fs = FakeFs::new(cx.executor());
18055 fs.insert_tree(
18056 path!("/a"),
18057 json!({
18058 "main.rs": "fn main() { let a = 5; }",
18059 "other.rs": "// Test file",
18060 }),
18061 )
18062 .await;
18063
18064 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18065
18066 let server_restarts = Arc::new(AtomicUsize::new(0));
18067 let closure_restarts = Arc::clone(&server_restarts);
18068 let language_server_name = "test language server";
18069 let language_name: LanguageName = "Rust".into();
18070
18071 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18072 language_registry.add(Arc::new(Language::new(
18073 LanguageConfig {
18074 name: language_name.clone(),
18075 matcher: LanguageMatcher {
18076 path_suffixes: vec!["rs".to_string()],
18077 ..Default::default()
18078 },
18079 ..Default::default()
18080 },
18081 Some(tree_sitter_rust::LANGUAGE.into()),
18082 )));
18083 let mut fake_servers = language_registry.register_fake_lsp(
18084 "Rust",
18085 FakeLspAdapter {
18086 name: language_server_name,
18087 initialization_options: Some(json!({
18088 "testOptionValue": true
18089 })),
18090 initializer: Some(Box::new(move |fake_server| {
18091 let task_restarts = Arc::clone(&closure_restarts);
18092 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18093 task_restarts.fetch_add(1, atomic::Ordering::Release);
18094 futures::future::ready(Ok(()))
18095 });
18096 })),
18097 ..Default::default()
18098 },
18099 );
18100
18101 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18102 let _buffer = project
18103 .update(cx, |project, cx| {
18104 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18105 })
18106 .await
18107 .unwrap();
18108 let _fake_server = fake_servers.next().await.unwrap();
18109 update_test_language_settings(cx, |language_settings| {
18110 language_settings.languages.0.insert(
18111 language_name.clone().0,
18112 LanguageSettingsContent {
18113 tab_size: NonZeroU32::new(8),
18114 ..Default::default()
18115 },
18116 );
18117 });
18118 cx.executor().run_until_parked();
18119 assert_eq!(
18120 server_restarts.load(atomic::Ordering::Acquire),
18121 0,
18122 "Should not restart LSP server on an unrelated change"
18123 );
18124
18125 update_test_project_settings(cx, |project_settings| {
18126 project_settings.lsp.insert(
18127 "Some other server name".into(),
18128 LspSettings {
18129 binary: None,
18130 settings: None,
18131 initialization_options: Some(json!({
18132 "some other init value": false
18133 })),
18134 enable_lsp_tasks: false,
18135 fetch: None,
18136 },
18137 );
18138 });
18139 cx.executor().run_until_parked();
18140 assert_eq!(
18141 server_restarts.load(atomic::Ordering::Acquire),
18142 0,
18143 "Should not restart LSP server on an unrelated LSP settings change"
18144 );
18145
18146 update_test_project_settings(cx, |project_settings| {
18147 project_settings.lsp.insert(
18148 language_server_name.into(),
18149 LspSettings {
18150 binary: None,
18151 settings: None,
18152 initialization_options: Some(json!({
18153 "anotherInitValue": false
18154 })),
18155 enable_lsp_tasks: false,
18156 fetch: None,
18157 },
18158 );
18159 });
18160 cx.executor().run_until_parked();
18161 assert_eq!(
18162 server_restarts.load(atomic::Ordering::Acquire),
18163 1,
18164 "Should restart LSP server on a related LSP settings change"
18165 );
18166
18167 update_test_project_settings(cx, |project_settings| {
18168 project_settings.lsp.insert(
18169 language_server_name.into(),
18170 LspSettings {
18171 binary: None,
18172 settings: None,
18173 initialization_options: Some(json!({
18174 "anotherInitValue": false
18175 })),
18176 enable_lsp_tasks: false,
18177 fetch: None,
18178 },
18179 );
18180 });
18181 cx.executor().run_until_parked();
18182 assert_eq!(
18183 server_restarts.load(atomic::Ordering::Acquire),
18184 1,
18185 "Should not restart LSP server on a related LSP settings change that is the same"
18186 );
18187
18188 update_test_project_settings(cx, |project_settings| {
18189 project_settings.lsp.insert(
18190 language_server_name.into(),
18191 LspSettings {
18192 binary: None,
18193 settings: None,
18194 initialization_options: None,
18195 enable_lsp_tasks: false,
18196 fetch: None,
18197 },
18198 );
18199 });
18200 cx.executor().run_until_parked();
18201 assert_eq!(
18202 server_restarts.load(atomic::Ordering::Acquire),
18203 2,
18204 "Should restart LSP server on another related LSP settings change"
18205 );
18206}
18207
18208#[gpui::test]
18209async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18210 init_test(cx, |_| {});
18211
18212 let mut cx = EditorLspTestContext::new_rust(
18213 lsp::ServerCapabilities {
18214 completion_provider: Some(lsp::CompletionOptions {
18215 trigger_characters: Some(vec![".".to_string()]),
18216 resolve_provider: Some(true),
18217 ..Default::default()
18218 }),
18219 ..Default::default()
18220 },
18221 cx,
18222 )
18223 .await;
18224
18225 cx.set_state("fn main() { let a = 2ˇ; }");
18226 cx.simulate_keystroke(".");
18227 let completion_item = lsp::CompletionItem {
18228 label: "some".into(),
18229 kind: Some(lsp::CompletionItemKind::SNIPPET),
18230 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18231 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18232 kind: lsp::MarkupKind::Markdown,
18233 value: "```rust\nSome(2)\n```".to_string(),
18234 })),
18235 deprecated: Some(false),
18236 sort_text: Some("fffffff2".to_string()),
18237 filter_text: Some("some".to_string()),
18238 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18239 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18240 range: lsp::Range {
18241 start: lsp::Position {
18242 line: 0,
18243 character: 22,
18244 },
18245 end: lsp::Position {
18246 line: 0,
18247 character: 22,
18248 },
18249 },
18250 new_text: "Some(2)".to_string(),
18251 })),
18252 additional_text_edits: Some(vec![lsp::TextEdit {
18253 range: lsp::Range {
18254 start: lsp::Position {
18255 line: 0,
18256 character: 20,
18257 },
18258 end: lsp::Position {
18259 line: 0,
18260 character: 22,
18261 },
18262 },
18263 new_text: "".to_string(),
18264 }]),
18265 ..Default::default()
18266 };
18267
18268 let closure_completion_item = completion_item.clone();
18269 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18270 let task_completion_item = closure_completion_item.clone();
18271 async move {
18272 Ok(Some(lsp::CompletionResponse::Array(vec![
18273 task_completion_item,
18274 ])))
18275 }
18276 });
18277
18278 request.next().await;
18279
18280 cx.condition(|editor, _| editor.context_menu_visible())
18281 .await;
18282 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18283 editor
18284 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18285 .unwrap()
18286 });
18287 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18288
18289 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18290 let task_completion_item = completion_item.clone();
18291 async move { Ok(task_completion_item) }
18292 })
18293 .next()
18294 .await
18295 .unwrap();
18296 apply_additional_edits.await.unwrap();
18297 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18298}
18299
18300#[gpui::test]
18301async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18302 init_test(cx, |_| {});
18303
18304 let mut cx = EditorLspTestContext::new_rust(
18305 lsp::ServerCapabilities {
18306 completion_provider: Some(lsp::CompletionOptions {
18307 trigger_characters: Some(vec![".".to_string()]),
18308 resolve_provider: Some(true),
18309 ..Default::default()
18310 }),
18311 ..Default::default()
18312 },
18313 cx,
18314 )
18315 .await;
18316
18317 cx.set_state("fn main() { let a = 2ˇ; }");
18318 cx.simulate_keystroke(".");
18319
18320 let item1 = lsp::CompletionItem {
18321 label: "method id()".to_string(),
18322 filter_text: Some("id".to_string()),
18323 detail: None,
18324 documentation: None,
18325 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18326 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18327 new_text: ".id".to_string(),
18328 })),
18329 ..lsp::CompletionItem::default()
18330 };
18331
18332 let item2 = lsp::CompletionItem {
18333 label: "other".to_string(),
18334 filter_text: Some("other".to_string()),
18335 detail: None,
18336 documentation: None,
18337 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18338 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18339 new_text: ".other".to_string(),
18340 })),
18341 ..lsp::CompletionItem::default()
18342 };
18343
18344 let item1 = item1.clone();
18345 cx.set_request_handler::<lsp::request::Completion, _, _>({
18346 let item1 = item1.clone();
18347 move |_, _, _| {
18348 let item1 = item1.clone();
18349 let item2 = item2.clone();
18350 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18351 }
18352 })
18353 .next()
18354 .await;
18355
18356 cx.condition(|editor, _| editor.context_menu_visible())
18357 .await;
18358 cx.update_editor(|editor, _, _| {
18359 let context_menu = editor.context_menu.borrow_mut();
18360 let context_menu = context_menu
18361 .as_ref()
18362 .expect("Should have the context menu deployed");
18363 match context_menu {
18364 CodeContextMenu::Completions(completions_menu) => {
18365 let completions = completions_menu.completions.borrow_mut();
18366 assert_eq!(
18367 completions
18368 .iter()
18369 .map(|completion| &completion.label.text)
18370 .collect::<Vec<_>>(),
18371 vec!["method id()", "other"]
18372 )
18373 }
18374 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18375 }
18376 });
18377
18378 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18379 let item1 = item1.clone();
18380 move |_, item_to_resolve, _| {
18381 let item1 = item1.clone();
18382 async move {
18383 if item1 == item_to_resolve {
18384 Ok(lsp::CompletionItem {
18385 label: "method id()".to_string(),
18386 filter_text: Some("id".to_string()),
18387 detail: Some("Now resolved!".to_string()),
18388 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18389 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18390 range: lsp::Range::new(
18391 lsp::Position::new(0, 22),
18392 lsp::Position::new(0, 22),
18393 ),
18394 new_text: ".id".to_string(),
18395 })),
18396 ..lsp::CompletionItem::default()
18397 })
18398 } else {
18399 Ok(item_to_resolve)
18400 }
18401 }
18402 }
18403 })
18404 .next()
18405 .await
18406 .unwrap();
18407 cx.run_until_parked();
18408
18409 cx.update_editor(|editor, window, cx| {
18410 editor.context_menu_next(&Default::default(), window, cx);
18411 });
18412
18413 cx.update_editor(|editor, _, _| {
18414 let context_menu = editor.context_menu.borrow_mut();
18415 let context_menu = context_menu
18416 .as_ref()
18417 .expect("Should have the context menu deployed");
18418 match context_menu {
18419 CodeContextMenu::Completions(completions_menu) => {
18420 let completions = completions_menu.completions.borrow_mut();
18421 assert_eq!(
18422 completions
18423 .iter()
18424 .map(|completion| &completion.label.text)
18425 .collect::<Vec<_>>(),
18426 vec!["method id() Now resolved!", "other"],
18427 "Should update first completion label, but not second as the filter text did not match."
18428 );
18429 }
18430 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18431 }
18432 });
18433}
18434
18435#[gpui::test]
18436async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18437 init_test(cx, |_| {});
18438 let mut cx = EditorLspTestContext::new_rust(
18439 lsp::ServerCapabilities {
18440 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18441 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18442 completion_provider: Some(lsp::CompletionOptions {
18443 resolve_provider: Some(true),
18444 ..Default::default()
18445 }),
18446 ..Default::default()
18447 },
18448 cx,
18449 )
18450 .await;
18451 cx.set_state(indoc! {"
18452 struct TestStruct {
18453 field: i32
18454 }
18455
18456 fn mainˇ() {
18457 let unused_var = 42;
18458 let test_struct = TestStruct { field: 42 };
18459 }
18460 "});
18461 let symbol_range = cx.lsp_range(indoc! {"
18462 struct TestStruct {
18463 field: i32
18464 }
18465
18466 «fn main»() {
18467 let unused_var = 42;
18468 let test_struct = TestStruct { field: 42 };
18469 }
18470 "});
18471 let mut hover_requests =
18472 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18473 Ok(Some(lsp::Hover {
18474 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18475 kind: lsp::MarkupKind::Markdown,
18476 value: "Function documentation".to_string(),
18477 }),
18478 range: Some(symbol_range),
18479 }))
18480 });
18481
18482 // Case 1: Test that code action menu hide hover popover
18483 cx.dispatch_action(Hover);
18484 hover_requests.next().await;
18485 cx.condition(|editor, _| editor.hover_state.visible()).await;
18486 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18487 move |_, _, _| async move {
18488 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18489 lsp::CodeAction {
18490 title: "Remove unused variable".to_string(),
18491 kind: Some(CodeActionKind::QUICKFIX),
18492 edit: Some(lsp::WorkspaceEdit {
18493 changes: Some(
18494 [(
18495 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18496 vec![lsp::TextEdit {
18497 range: lsp::Range::new(
18498 lsp::Position::new(5, 4),
18499 lsp::Position::new(5, 27),
18500 ),
18501 new_text: "".to_string(),
18502 }],
18503 )]
18504 .into_iter()
18505 .collect(),
18506 ),
18507 ..Default::default()
18508 }),
18509 ..Default::default()
18510 },
18511 )]))
18512 },
18513 );
18514 cx.update_editor(|editor, window, cx| {
18515 editor.toggle_code_actions(
18516 &ToggleCodeActions {
18517 deployed_from: None,
18518 quick_launch: false,
18519 },
18520 window,
18521 cx,
18522 );
18523 });
18524 code_action_requests.next().await;
18525 cx.run_until_parked();
18526 cx.condition(|editor, _| editor.context_menu_visible())
18527 .await;
18528 cx.update_editor(|editor, _, _| {
18529 assert!(
18530 !editor.hover_state.visible(),
18531 "Hover popover should be hidden when code action menu is shown"
18532 );
18533 // Hide code actions
18534 editor.context_menu.take();
18535 });
18536
18537 // Case 2: Test that code completions hide hover popover
18538 cx.dispatch_action(Hover);
18539 hover_requests.next().await;
18540 cx.condition(|editor, _| editor.hover_state.visible()).await;
18541 let counter = Arc::new(AtomicUsize::new(0));
18542 let mut completion_requests =
18543 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18544 let counter = counter.clone();
18545 async move {
18546 counter.fetch_add(1, atomic::Ordering::Release);
18547 Ok(Some(lsp::CompletionResponse::Array(vec![
18548 lsp::CompletionItem {
18549 label: "main".into(),
18550 kind: Some(lsp::CompletionItemKind::FUNCTION),
18551 detail: Some("() -> ()".to_string()),
18552 ..Default::default()
18553 },
18554 lsp::CompletionItem {
18555 label: "TestStruct".into(),
18556 kind: Some(lsp::CompletionItemKind::STRUCT),
18557 detail: Some("struct TestStruct".to_string()),
18558 ..Default::default()
18559 },
18560 ])))
18561 }
18562 });
18563 cx.update_editor(|editor, window, cx| {
18564 editor.show_completions(&ShowCompletions, window, cx);
18565 });
18566 completion_requests.next().await;
18567 cx.condition(|editor, _| editor.context_menu_visible())
18568 .await;
18569 cx.update_editor(|editor, _, _| {
18570 assert!(
18571 !editor.hover_state.visible(),
18572 "Hover popover should be hidden when completion menu is shown"
18573 );
18574 });
18575}
18576
18577#[gpui::test]
18578async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18579 init_test(cx, |_| {});
18580
18581 let mut cx = EditorLspTestContext::new_rust(
18582 lsp::ServerCapabilities {
18583 completion_provider: Some(lsp::CompletionOptions {
18584 trigger_characters: Some(vec![".".to_string()]),
18585 resolve_provider: Some(true),
18586 ..Default::default()
18587 }),
18588 ..Default::default()
18589 },
18590 cx,
18591 )
18592 .await;
18593
18594 cx.set_state("fn main() { let a = 2ˇ; }");
18595 cx.simulate_keystroke(".");
18596
18597 let unresolved_item_1 = lsp::CompletionItem {
18598 label: "id".to_string(),
18599 filter_text: Some("id".to_string()),
18600 detail: None,
18601 documentation: None,
18602 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18603 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18604 new_text: ".id".to_string(),
18605 })),
18606 ..lsp::CompletionItem::default()
18607 };
18608 let resolved_item_1 = lsp::CompletionItem {
18609 additional_text_edits: Some(vec![lsp::TextEdit {
18610 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18611 new_text: "!!".to_string(),
18612 }]),
18613 ..unresolved_item_1.clone()
18614 };
18615 let unresolved_item_2 = lsp::CompletionItem {
18616 label: "other".to_string(),
18617 filter_text: Some("other".to_string()),
18618 detail: None,
18619 documentation: None,
18620 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18621 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18622 new_text: ".other".to_string(),
18623 })),
18624 ..lsp::CompletionItem::default()
18625 };
18626 let resolved_item_2 = lsp::CompletionItem {
18627 additional_text_edits: Some(vec![lsp::TextEdit {
18628 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18629 new_text: "??".to_string(),
18630 }]),
18631 ..unresolved_item_2.clone()
18632 };
18633
18634 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18635 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18636 cx.lsp
18637 .server
18638 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18639 let unresolved_item_1 = unresolved_item_1.clone();
18640 let resolved_item_1 = resolved_item_1.clone();
18641 let unresolved_item_2 = unresolved_item_2.clone();
18642 let resolved_item_2 = resolved_item_2.clone();
18643 let resolve_requests_1 = resolve_requests_1.clone();
18644 let resolve_requests_2 = resolve_requests_2.clone();
18645 move |unresolved_request, _| {
18646 let unresolved_item_1 = unresolved_item_1.clone();
18647 let resolved_item_1 = resolved_item_1.clone();
18648 let unresolved_item_2 = unresolved_item_2.clone();
18649 let resolved_item_2 = resolved_item_2.clone();
18650 let resolve_requests_1 = resolve_requests_1.clone();
18651 let resolve_requests_2 = resolve_requests_2.clone();
18652 async move {
18653 if unresolved_request == unresolved_item_1 {
18654 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18655 Ok(resolved_item_1.clone())
18656 } else if unresolved_request == unresolved_item_2 {
18657 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18658 Ok(resolved_item_2.clone())
18659 } else {
18660 panic!("Unexpected completion item {unresolved_request:?}")
18661 }
18662 }
18663 }
18664 })
18665 .detach();
18666
18667 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18668 let unresolved_item_1 = unresolved_item_1.clone();
18669 let unresolved_item_2 = unresolved_item_2.clone();
18670 async move {
18671 Ok(Some(lsp::CompletionResponse::Array(vec![
18672 unresolved_item_1,
18673 unresolved_item_2,
18674 ])))
18675 }
18676 })
18677 .next()
18678 .await;
18679
18680 cx.condition(|editor, _| editor.context_menu_visible())
18681 .await;
18682 cx.update_editor(|editor, _, _| {
18683 let context_menu = editor.context_menu.borrow_mut();
18684 let context_menu = context_menu
18685 .as_ref()
18686 .expect("Should have the context menu deployed");
18687 match context_menu {
18688 CodeContextMenu::Completions(completions_menu) => {
18689 let completions = completions_menu.completions.borrow_mut();
18690 assert_eq!(
18691 completions
18692 .iter()
18693 .map(|completion| &completion.label.text)
18694 .collect::<Vec<_>>(),
18695 vec!["id", "other"]
18696 )
18697 }
18698 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18699 }
18700 });
18701 cx.run_until_parked();
18702
18703 cx.update_editor(|editor, window, cx| {
18704 editor.context_menu_next(&ContextMenuNext, window, cx);
18705 });
18706 cx.run_until_parked();
18707 cx.update_editor(|editor, window, cx| {
18708 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18709 });
18710 cx.run_until_parked();
18711 cx.update_editor(|editor, window, cx| {
18712 editor.context_menu_next(&ContextMenuNext, window, cx);
18713 });
18714 cx.run_until_parked();
18715 cx.update_editor(|editor, window, cx| {
18716 editor
18717 .compose_completion(&ComposeCompletion::default(), window, cx)
18718 .expect("No task returned")
18719 })
18720 .await
18721 .expect("Completion failed");
18722 cx.run_until_parked();
18723
18724 cx.update_editor(|editor, _, cx| {
18725 assert_eq!(
18726 resolve_requests_1.load(atomic::Ordering::Acquire),
18727 1,
18728 "Should always resolve once despite multiple selections"
18729 );
18730 assert_eq!(
18731 resolve_requests_2.load(atomic::Ordering::Acquire),
18732 1,
18733 "Should always resolve once after multiple selections and applying the completion"
18734 );
18735 assert_eq!(
18736 editor.text(cx),
18737 "fn main() { let a = ??.other; }",
18738 "Should use resolved data when applying the completion"
18739 );
18740 });
18741}
18742
18743#[gpui::test]
18744async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18745 init_test(cx, |_| {});
18746
18747 let item_0 = lsp::CompletionItem {
18748 label: "abs".into(),
18749 insert_text: Some("abs".into()),
18750 data: Some(json!({ "very": "special"})),
18751 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18752 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18753 lsp::InsertReplaceEdit {
18754 new_text: "abs".to_string(),
18755 insert: lsp::Range::default(),
18756 replace: lsp::Range::default(),
18757 },
18758 )),
18759 ..lsp::CompletionItem::default()
18760 };
18761 let items = iter::once(item_0.clone())
18762 .chain((11..51).map(|i| lsp::CompletionItem {
18763 label: format!("item_{}", i),
18764 insert_text: Some(format!("item_{}", i)),
18765 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18766 ..lsp::CompletionItem::default()
18767 }))
18768 .collect::<Vec<_>>();
18769
18770 let default_commit_characters = vec!["?".to_string()];
18771 let default_data = json!({ "default": "data"});
18772 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18773 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18774 let default_edit_range = lsp::Range {
18775 start: lsp::Position {
18776 line: 0,
18777 character: 5,
18778 },
18779 end: lsp::Position {
18780 line: 0,
18781 character: 5,
18782 },
18783 };
18784
18785 let mut cx = EditorLspTestContext::new_rust(
18786 lsp::ServerCapabilities {
18787 completion_provider: Some(lsp::CompletionOptions {
18788 trigger_characters: Some(vec![".".to_string()]),
18789 resolve_provider: Some(true),
18790 ..Default::default()
18791 }),
18792 ..Default::default()
18793 },
18794 cx,
18795 )
18796 .await;
18797
18798 cx.set_state("fn main() { let a = 2ˇ; }");
18799 cx.simulate_keystroke(".");
18800
18801 let completion_data = default_data.clone();
18802 let completion_characters = default_commit_characters.clone();
18803 let completion_items = items.clone();
18804 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18805 let default_data = completion_data.clone();
18806 let default_commit_characters = completion_characters.clone();
18807 let items = completion_items.clone();
18808 async move {
18809 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18810 items,
18811 item_defaults: Some(lsp::CompletionListItemDefaults {
18812 data: Some(default_data.clone()),
18813 commit_characters: Some(default_commit_characters.clone()),
18814 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18815 default_edit_range,
18816 )),
18817 insert_text_format: Some(default_insert_text_format),
18818 insert_text_mode: Some(default_insert_text_mode),
18819 }),
18820 ..lsp::CompletionList::default()
18821 })))
18822 }
18823 })
18824 .next()
18825 .await;
18826
18827 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18828 cx.lsp
18829 .server
18830 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18831 let closure_resolved_items = resolved_items.clone();
18832 move |item_to_resolve, _| {
18833 let closure_resolved_items = closure_resolved_items.clone();
18834 async move {
18835 closure_resolved_items.lock().push(item_to_resolve.clone());
18836 Ok(item_to_resolve)
18837 }
18838 }
18839 })
18840 .detach();
18841
18842 cx.condition(|editor, _| editor.context_menu_visible())
18843 .await;
18844 cx.run_until_parked();
18845 cx.update_editor(|editor, _, _| {
18846 let menu = editor.context_menu.borrow_mut();
18847 match menu.as_ref().expect("should have the completions menu") {
18848 CodeContextMenu::Completions(completions_menu) => {
18849 assert_eq!(
18850 completions_menu
18851 .entries
18852 .borrow()
18853 .iter()
18854 .map(|mat| mat.string.clone())
18855 .collect::<Vec<String>>(),
18856 items
18857 .iter()
18858 .map(|completion| completion.label.clone())
18859 .collect::<Vec<String>>()
18860 );
18861 }
18862 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18863 }
18864 });
18865 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18866 // with 4 from the end.
18867 assert_eq!(
18868 *resolved_items.lock(),
18869 [&items[0..16], &items[items.len() - 4..items.len()]]
18870 .concat()
18871 .iter()
18872 .cloned()
18873 .map(|mut item| {
18874 if item.data.is_none() {
18875 item.data = Some(default_data.clone());
18876 }
18877 item
18878 })
18879 .collect::<Vec<lsp::CompletionItem>>(),
18880 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18881 );
18882 resolved_items.lock().clear();
18883
18884 cx.update_editor(|editor, window, cx| {
18885 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18886 });
18887 cx.run_until_parked();
18888 // Completions that have already been resolved are skipped.
18889 assert_eq!(
18890 *resolved_items.lock(),
18891 items[items.len() - 17..items.len() - 4]
18892 .iter()
18893 .cloned()
18894 .map(|mut item| {
18895 if item.data.is_none() {
18896 item.data = Some(default_data.clone());
18897 }
18898 item
18899 })
18900 .collect::<Vec<lsp::CompletionItem>>()
18901 );
18902 resolved_items.lock().clear();
18903}
18904
18905#[gpui::test]
18906async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18907 init_test(cx, |_| {});
18908
18909 let mut cx = EditorLspTestContext::new(
18910 Language::new(
18911 LanguageConfig {
18912 matcher: LanguageMatcher {
18913 path_suffixes: vec!["jsx".into()],
18914 ..Default::default()
18915 },
18916 overrides: [(
18917 "element".into(),
18918 LanguageConfigOverride {
18919 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18920 ..Default::default()
18921 },
18922 )]
18923 .into_iter()
18924 .collect(),
18925 ..Default::default()
18926 },
18927 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18928 )
18929 .with_override_query("(jsx_self_closing_element) @element")
18930 .unwrap(),
18931 lsp::ServerCapabilities {
18932 completion_provider: Some(lsp::CompletionOptions {
18933 trigger_characters: Some(vec![":".to_string()]),
18934 ..Default::default()
18935 }),
18936 ..Default::default()
18937 },
18938 cx,
18939 )
18940 .await;
18941
18942 cx.lsp
18943 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18944 Ok(Some(lsp::CompletionResponse::Array(vec![
18945 lsp::CompletionItem {
18946 label: "bg-blue".into(),
18947 ..Default::default()
18948 },
18949 lsp::CompletionItem {
18950 label: "bg-red".into(),
18951 ..Default::default()
18952 },
18953 lsp::CompletionItem {
18954 label: "bg-yellow".into(),
18955 ..Default::default()
18956 },
18957 ])))
18958 });
18959
18960 cx.set_state(r#"<p class="bgˇ" />"#);
18961
18962 // Trigger completion when typing a dash, because the dash is an extra
18963 // word character in the 'element' scope, which contains the cursor.
18964 cx.simulate_keystroke("-");
18965 cx.executor().run_until_parked();
18966 cx.update_editor(|editor, _, _| {
18967 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18968 {
18969 assert_eq!(
18970 completion_menu_entries(menu),
18971 &["bg-blue", "bg-red", "bg-yellow"]
18972 );
18973 } else {
18974 panic!("expected completion menu to be open");
18975 }
18976 });
18977
18978 cx.simulate_keystroke("l");
18979 cx.executor().run_until_parked();
18980 cx.update_editor(|editor, _, _| {
18981 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18982 {
18983 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18984 } else {
18985 panic!("expected completion menu to be open");
18986 }
18987 });
18988
18989 // When filtering completions, consider the character after the '-' to
18990 // be the start of a subword.
18991 cx.set_state(r#"<p class="yelˇ" />"#);
18992 cx.simulate_keystroke("l");
18993 cx.executor().run_until_parked();
18994 cx.update_editor(|editor, _, _| {
18995 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18996 {
18997 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18998 } else {
18999 panic!("expected completion menu to be open");
19000 }
19001 });
19002}
19003
19004fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19005 let entries = menu.entries.borrow();
19006 entries.iter().map(|mat| mat.string.clone()).collect()
19007}
19008
19009#[gpui::test]
19010async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19011 init_test(cx, |settings| {
19012 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19013 });
19014
19015 let fs = FakeFs::new(cx.executor());
19016 fs.insert_file(path!("/file.ts"), Default::default()).await;
19017
19018 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19019 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19020
19021 language_registry.add(Arc::new(Language::new(
19022 LanguageConfig {
19023 name: "TypeScript".into(),
19024 matcher: LanguageMatcher {
19025 path_suffixes: vec!["ts".to_string()],
19026 ..Default::default()
19027 },
19028 ..Default::default()
19029 },
19030 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19031 )));
19032 update_test_language_settings(cx, |settings| {
19033 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19034 });
19035
19036 let test_plugin = "test_plugin";
19037 let _ = language_registry.register_fake_lsp(
19038 "TypeScript",
19039 FakeLspAdapter {
19040 prettier_plugins: vec![test_plugin],
19041 ..Default::default()
19042 },
19043 );
19044
19045 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19046 let buffer = project
19047 .update(cx, |project, cx| {
19048 project.open_local_buffer(path!("/file.ts"), cx)
19049 })
19050 .await
19051 .unwrap();
19052
19053 let buffer_text = "one\ntwo\nthree\n";
19054 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19055 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19056 editor.update_in(cx, |editor, window, cx| {
19057 editor.set_text(buffer_text, window, cx)
19058 });
19059
19060 editor
19061 .update_in(cx, |editor, window, cx| {
19062 editor.perform_format(
19063 project.clone(),
19064 FormatTrigger::Manual,
19065 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19066 window,
19067 cx,
19068 )
19069 })
19070 .unwrap()
19071 .await;
19072 assert_eq!(
19073 editor.update(cx, |editor, cx| editor.text(cx)),
19074 buffer_text.to_string() + prettier_format_suffix,
19075 "Test prettier formatting was not applied to the original buffer text",
19076 );
19077
19078 update_test_language_settings(cx, |settings| {
19079 settings.defaults.formatter = Some(FormatterList::default())
19080 });
19081 let format = editor.update_in(cx, |editor, window, cx| {
19082 editor.perform_format(
19083 project.clone(),
19084 FormatTrigger::Manual,
19085 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19086 window,
19087 cx,
19088 )
19089 });
19090 format.await.unwrap();
19091 assert_eq!(
19092 editor.update(cx, |editor, cx| editor.text(cx)),
19093 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19094 "Autoformatting (via test prettier) was not applied to the original buffer text",
19095 );
19096}
19097
19098#[gpui::test]
19099async fn test_addition_reverts(cx: &mut TestAppContext) {
19100 init_test(cx, |_| {});
19101 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19102 let base_text = indoc! {r#"
19103 struct Row;
19104 struct Row1;
19105 struct Row2;
19106
19107 struct Row4;
19108 struct Row5;
19109 struct Row6;
19110
19111 struct Row8;
19112 struct Row9;
19113 struct Row10;"#};
19114
19115 // When addition hunks are not adjacent to carets, no hunk revert is performed
19116 assert_hunk_revert(
19117 indoc! {r#"struct Row;
19118 struct Row1;
19119 struct Row1.1;
19120 struct Row1.2;
19121 struct Row2;ˇ
19122
19123 struct Row4;
19124 struct Row5;
19125 struct Row6;
19126
19127 struct Row8;
19128 ˇstruct Row9;
19129 struct Row9.1;
19130 struct Row9.2;
19131 struct Row9.3;
19132 struct Row10;"#},
19133 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19134 indoc! {r#"struct Row;
19135 struct Row1;
19136 struct Row1.1;
19137 struct Row1.2;
19138 struct Row2;ˇ
19139
19140 struct Row4;
19141 struct Row5;
19142 struct Row6;
19143
19144 struct Row8;
19145 ˇstruct Row9;
19146 struct Row9.1;
19147 struct Row9.2;
19148 struct Row9.3;
19149 struct Row10;"#},
19150 base_text,
19151 &mut cx,
19152 );
19153 // Same for selections
19154 assert_hunk_revert(
19155 indoc! {r#"struct Row;
19156 struct Row1;
19157 struct Row2;
19158 struct Row2.1;
19159 struct Row2.2;
19160 «ˇ
19161 struct Row4;
19162 struct» Row5;
19163 «struct Row6;
19164 ˇ»
19165 struct Row9.1;
19166 struct Row9.2;
19167 struct Row9.3;
19168 struct Row8;
19169 struct Row9;
19170 struct Row10;"#},
19171 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19172 indoc! {r#"struct Row;
19173 struct Row1;
19174 struct Row2;
19175 struct Row2.1;
19176 struct Row2.2;
19177 «ˇ
19178 struct Row4;
19179 struct» Row5;
19180 «struct Row6;
19181 ˇ»
19182 struct Row9.1;
19183 struct Row9.2;
19184 struct Row9.3;
19185 struct Row8;
19186 struct Row9;
19187 struct Row10;"#},
19188 base_text,
19189 &mut cx,
19190 );
19191
19192 // When carets and selections intersect the addition hunks, those are reverted.
19193 // Adjacent carets got merged.
19194 assert_hunk_revert(
19195 indoc! {r#"struct Row;
19196 ˇ// something on the top
19197 struct Row1;
19198 struct Row2;
19199 struct Roˇw3.1;
19200 struct Row2.2;
19201 struct Row2.3;ˇ
19202
19203 struct Row4;
19204 struct ˇRow5.1;
19205 struct Row5.2;
19206 struct «Rowˇ»5.3;
19207 struct Row5;
19208 struct Row6;
19209 ˇ
19210 struct Row9.1;
19211 struct «Rowˇ»9.2;
19212 struct «ˇRow»9.3;
19213 struct Row8;
19214 struct Row9;
19215 «ˇ// something on bottom»
19216 struct Row10;"#},
19217 vec![
19218 DiffHunkStatusKind::Added,
19219 DiffHunkStatusKind::Added,
19220 DiffHunkStatusKind::Added,
19221 DiffHunkStatusKind::Added,
19222 DiffHunkStatusKind::Added,
19223 ],
19224 indoc! {r#"struct Row;
19225 ˇstruct Row1;
19226 struct Row2;
19227 ˇ
19228 struct Row4;
19229 ˇstruct Row5;
19230 struct Row6;
19231 ˇ
19232 ˇstruct Row8;
19233 struct Row9;
19234 ˇstruct Row10;"#},
19235 base_text,
19236 &mut cx,
19237 );
19238}
19239
19240#[gpui::test]
19241async fn test_modification_reverts(cx: &mut TestAppContext) {
19242 init_test(cx, |_| {});
19243 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19244 let base_text = indoc! {r#"
19245 struct Row;
19246 struct Row1;
19247 struct Row2;
19248
19249 struct Row4;
19250 struct Row5;
19251 struct Row6;
19252
19253 struct Row8;
19254 struct Row9;
19255 struct Row10;"#};
19256
19257 // Modification hunks behave the same as the addition ones.
19258 assert_hunk_revert(
19259 indoc! {r#"struct Row;
19260 struct Row1;
19261 struct Row33;
19262 ˇ
19263 struct Row4;
19264 struct Row5;
19265 struct Row6;
19266 ˇ
19267 struct Row99;
19268 struct Row9;
19269 struct Row10;"#},
19270 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19271 indoc! {r#"struct Row;
19272 struct Row1;
19273 struct Row33;
19274 ˇ
19275 struct Row4;
19276 struct Row5;
19277 struct Row6;
19278 ˇ
19279 struct Row99;
19280 struct Row9;
19281 struct Row10;"#},
19282 base_text,
19283 &mut cx,
19284 );
19285 assert_hunk_revert(
19286 indoc! {r#"struct Row;
19287 struct Row1;
19288 struct Row33;
19289 «ˇ
19290 struct Row4;
19291 struct» Row5;
19292 «struct Row6;
19293 ˇ»
19294 struct Row99;
19295 struct Row9;
19296 struct Row10;"#},
19297 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19298 indoc! {r#"struct Row;
19299 struct Row1;
19300 struct Row33;
19301 «ˇ
19302 struct Row4;
19303 struct» Row5;
19304 «struct Row6;
19305 ˇ»
19306 struct Row99;
19307 struct Row9;
19308 struct Row10;"#},
19309 base_text,
19310 &mut cx,
19311 );
19312
19313 assert_hunk_revert(
19314 indoc! {r#"ˇstruct Row1.1;
19315 struct Row1;
19316 «ˇstr»uct Row22;
19317
19318 struct ˇRow44;
19319 struct Row5;
19320 struct «Rˇ»ow66;ˇ
19321
19322 «struˇ»ct Row88;
19323 struct Row9;
19324 struct Row1011;ˇ"#},
19325 vec![
19326 DiffHunkStatusKind::Modified,
19327 DiffHunkStatusKind::Modified,
19328 DiffHunkStatusKind::Modified,
19329 DiffHunkStatusKind::Modified,
19330 DiffHunkStatusKind::Modified,
19331 DiffHunkStatusKind::Modified,
19332 ],
19333 indoc! {r#"struct Row;
19334 ˇstruct Row1;
19335 struct Row2;
19336 ˇ
19337 struct Row4;
19338 ˇstruct Row5;
19339 struct Row6;
19340 ˇ
19341 struct Row8;
19342 ˇstruct Row9;
19343 struct Row10;ˇ"#},
19344 base_text,
19345 &mut cx,
19346 );
19347}
19348
19349#[gpui::test]
19350async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19351 init_test(cx, |_| {});
19352 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19353 let base_text = indoc! {r#"
19354 one
19355
19356 two
19357 three
19358 "#};
19359
19360 cx.set_head_text(base_text);
19361 cx.set_state("\nˇ\n");
19362 cx.executor().run_until_parked();
19363 cx.update_editor(|editor, _window, cx| {
19364 editor.expand_selected_diff_hunks(cx);
19365 });
19366 cx.executor().run_until_parked();
19367 cx.update_editor(|editor, window, cx| {
19368 editor.backspace(&Default::default(), window, cx);
19369 });
19370 cx.run_until_parked();
19371 cx.assert_state_with_diff(
19372 indoc! {r#"
19373
19374 - two
19375 - threeˇ
19376 +
19377 "#}
19378 .to_string(),
19379 );
19380}
19381
19382#[gpui::test]
19383async fn test_deletion_reverts(cx: &mut TestAppContext) {
19384 init_test(cx, |_| {});
19385 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19386 let base_text = indoc! {r#"struct Row;
19387struct Row1;
19388struct Row2;
19389
19390struct Row4;
19391struct Row5;
19392struct Row6;
19393
19394struct Row8;
19395struct Row9;
19396struct Row10;"#};
19397
19398 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19399 assert_hunk_revert(
19400 indoc! {r#"struct Row;
19401 struct Row2;
19402
19403 ˇstruct Row4;
19404 struct Row5;
19405 struct Row6;
19406 ˇ
19407 struct Row8;
19408 struct Row10;"#},
19409 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19410 indoc! {r#"struct Row;
19411 struct Row2;
19412
19413 ˇstruct Row4;
19414 struct Row5;
19415 struct Row6;
19416 ˇ
19417 struct Row8;
19418 struct Row10;"#},
19419 base_text,
19420 &mut cx,
19421 );
19422 assert_hunk_revert(
19423 indoc! {r#"struct Row;
19424 struct Row2;
19425
19426 «ˇstruct Row4;
19427 struct» Row5;
19428 «struct Row6;
19429 ˇ»
19430 struct Row8;
19431 struct Row10;"#},
19432 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19433 indoc! {r#"struct Row;
19434 struct Row2;
19435
19436 «ˇstruct Row4;
19437 struct» Row5;
19438 «struct Row6;
19439 ˇ»
19440 struct Row8;
19441 struct Row10;"#},
19442 base_text,
19443 &mut cx,
19444 );
19445
19446 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19447 assert_hunk_revert(
19448 indoc! {r#"struct Row;
19449 ˇstruct Row2;
19450
19451 struct Row4;
19452 struct Row5;
19453 struct Row6;
19454
19455 struct Row8;ˇ
19456 struct Row10;"#},
19457 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19458 indoc! {r#"struct Row;
19459 struct Row1;
19460 ˇstruct Row2;
19461
19462 struct Row4;
19463 struct Row5;
19464 struct Row6;
19465
19466 struct Row8;ˇ
19467 struct Row9;
19468 struct Row10;"#},
19469 base_text,
19470 &mut cx,
19471 );
19472 assert_hunk_revert(
19473 indoc! {r#"struct Row;
19474 struct Row2«ˇ;
19475 struct Row4;
19476 struct» Row5;
19477 «struct Row6;
19478
19479 struct Row8;ˇ»
19480 struct Row10;"#},
19481 vec![
19482 DiffHunkStatusKind::Deleted,
19483 DiffHunkStatusKind::Deleted,
19484 DiffHunkStatusKind::Deleted,
19485 ],
19486 indoc! {r#"struct Row;
19487 struct Row1;
19488 struct Row2«ˇ;
19489
19490 struct Row4;
19491 struct» Row5;
19492 «struct Row6;
19493
19494 struct Row8;ˇ»
19495 struct Row9;
19496 struct Row10;"#},
19497 base_text,
19498 &mut cx,
19499 );
19500}
19501
19502#[gpui::test]
19503async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19504 init_test(cx, |_| {});
19505
19506 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19507 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19508 let base_text_3 =
19509 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19510
19511 let text_1 = edit_first_char_of_every_line(base_text_1);
19512 let text_2 = edit_first_char_of_every_line(base_text_2);
19513 let text_3 = edit_first_char_of_every_line(base_text_3);
19514
19515 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19516 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19517 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19518
19519 let multibuffer = cx.new(|cx| {
19520 let mut multibuffer = MultiBuffer::new(ReadWrite);
19521 multibuffer.push_excerpts(
19522 buffer_1.clone(),
19523 [
19524 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19525 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19526 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19527 ],
19528 cx,
19529 );
19530 multibuffer.push_excerpts(
19531 buffer_2.clone(),
19532 [
19533 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19534 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19535 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19536 ],
19537 cx,
19538 );
19539 multibuffer.push_excerpts(
19540 buffer_3.clone(),
19541 [
19542 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19543 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19544 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19545 ],
19546 cx,
19547 );
19548 multibuffer
19549 });
19550
19551 let fs = FakeFs::new(cx.executor());
19552 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19553 let (editor, cx) = cx
19554 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19555 editor.update_in(cx, |editor, _window, cx| {
19556 for (buffer, diff_base) in [
19557 (buffer_1.clone(), base_text_1),
19558 (buffer_2.clone(), base_text_2),
19559 (buffer_3.clone(), base_text_3),
19560 ] {
19561 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19562 editor
19563 .buffer
19564 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19565 }
19566 });
19567 cx.executor().run_until_parked();
19568
19569 editor.update_in(cx, |editor, window, cx| {
19570 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}");
19571 editor.select_all(&SelectAll, window, cx);
19572 editor.git_restore(&Default::default(), window, cx);
19573 });
19574 cx.executor().run_until_parked();
19575
19576 // When all ranges are selected, all buffer hunks are reverted.
19577 editor.update(cx, |editor, cx| {
19578 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");
19579 });
19580 buffer_1.update(cx, |buffer, _| {
19581 assert_eq!(buffer.text(), base_text_1);
19582 });
19583 buffer_2.update(cx, |buffer, _| {
19584 assert_eq!(buffer.text(), base_text_2);
19585 });
19586 buffer_3.update(cx, |buffer, _| {
19587 assert_eq!(buffer.text(), base_text_3);
19588 });
19589
19590 editor.update_in(cx, |editor, window, cx| {
19591 editor.undo(&Default::default(), window, cx);
19592 });
19593
19594 editor.update_in(cx, |editor, window, cx| {
19595 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19596 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19597 });
19598 editor.git_restore(&Default::default(), window, cx);
19599 });
19600
19601 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19602 // but not affect buffer_2 and its related excerpts.
19603 editor.update(cx, |editor, cx| {
19604 assert_eq!(
19605 editor.text(cx),
19606 "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}"
19607 );
19608 });
19609 buffer_1.update(cx, |buffer, _| {
19610 assert_eq!(buffer.text(), base_text_1);
19611 });
19612 buffer_2.update(cx, |buffer, _| {
19613 assert_eq!(
19614 buffer.text(),
19615 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19616 );
19617 });
19618 buffer_3.update(cx, |buffer, _| {
19619 assert_eq!(
19620 buffer.text(),
19621 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19622 );
19623 });
19624
19625 fn edit_first_char_of_every_line(text: &str) -> String {
19626 text.split('\n')
19627 .map(|line| format!("X{}", &line[1..]))
19628 .collect::<Vec<_>>()
19629 .join("\n")
19630 }
19631}
19632
19633#[gpui::test]
19634async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19635 init_test(cx, |_| {});
19636
19637 let cols = 4;
19638 let rows = 10;
19639 let sample_text_1 = sample_text(rows, cols, 'a');
19640 assert_eq!(
19641 sample_text_1,
19642 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19643 );
19644 let sample_text_2 = sample_text(rows, cols, 'l');
19645 assert_eq!(
19646 sample_text_2,
19647 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19648 );
19649 let sample_text_3 = sample_text(rows, cols, 'v');
19650 assert_eq!(
19651 sample_text_3,
19652 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19653 );
19654
19655 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19656 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19657 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19658
19659 let multi_buffer = cx.new(|cx| {
19660 let mut multibuffer = MultiBuffer::new(ReadWrite);
19661 multibuffer.push_excerpts(
19662 buffer_1.clone(),
19663 [
19664 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19665 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19666 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19667 ],
19668 cx,
19669 );
19670 multibuffer.push_excerpts(
19671 buffer_2.clone(),
19672 [
19673 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19674 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19675 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19676 ],
19677 cx,
19678 );
19679 multibuffer.push_excerpts(
19680 buffer_3.clone(),
19681 [
19682 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19683 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19684 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19685 ],
19686 cx,
19687 );
19688 multibuffer
19689 });
19690
19691 let fs = FakeFs::new(cx.executor());
19692 fs.insert_tree(
19693 "/a",
19694 json!({
19695 "main.rs": sample_text_1,
19696 "other.rs": sample_text_2,
19697 "lib.rs": sample_text_3,
19698 }),
19699 )
19700 .await;
19701 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19702 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19703 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19704 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19705 Editor::new(
19706 EditorMode::full(),
19707 multi_buffer,
19708 Some(project.clone()),
19709 window,
19710 cx,
19711 )
19712 });
19713 let multibuffer_item_id = workspace
19714 .update(cx, |workspace, window, cx| {
19715 assert!(
19716 workspace.active_item(cx).is_none(),
19717 "active item should be None before the first item is added"
19718 );
19719 workspace.add_item_to_active_pane(
19720 Box::new(multi_buffer_editor.clone()),
19721 None,
19722 true,
19723 window,
19724 cx,
19725 );
19726 let active_item = workspace
19727 .active_item(cx)
19728 .expect("should have an active item after adding the multi buffer");
19729 assert_eq!(
19730 active_item.buffer_kind(cx),
19731 ItemBufferKind::Multibuffer,
19732 "A multi buffer was expected to active after adding"
19733 );
19734 active_item.item_id()
19735 })
19736 .unwrap();
19737 cx.executor().run_until_parked();
19738
19739 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19740 editor.change_selections(
19741 SelectionEffects::scroll(Autoscroll::Next),
19742 window,
19743 cx,
19744 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
19745 );
19746 editor.open_excerpts(&OpenExcerpts, window, cx);
19747 });
19748 cx.executor().run_until_parked();
19749 let first_item_id = workspace
19750 .update(cx, |workspace, window, cx| {
19751 let active_item = workspace
19752 .active_item(cx)
19753 .expect("should have an active item after navigating into the 1st buffer");
19754 let first_item_id = active_item.item_id();
19755 assert_ne!(
19756 first_item_id, multibuffer_item_id,
19757 "Should navigate into the 1st buffer and activate it"
19758 );
19759 assert_eq!(
19760 active_item.buffer_kind(cx),
19761 ItemBufferKind::Singleton,
19762 "New active item should be a singleton buffer"
19763 );
19764 assert_eq!(
19765 active_item
19766 .act_as::<Editor>(cx)
19767 .expect("should have navigated into an editor for the 1st buffer")
19768 .read(cx)
19769 .text(cx),
19770 sample_text_1
19771 );
19772
19773 workspace
19774 .go_back(workspace.active_pane().downgrade(), window, cx)
19775 .detach_and_log_err(cx);
19776
19777 first_item_id
19778 })
19779 .unwrap();
19780 cx.executor().run_until_parked();
19781 workspace
19782 .update(cx, |workspace, _, cx| {
19783 let active_item = workspace
19784 .active_item(cx)
19785 .expect("should have an active item after navigating back");
19786 assert_eq!(
19787 active_item.item_id(),
19788 multibuffer_item_id,
19789 "Should navigate back to the multi buffer"
19790 );
19791 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19792 })
19793 .unwrap();
19794
19795 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19796 editor.change_selections(
19797 SelectionEffects::scroll(Autoscroll::Next),
19798 window,
19799 cx,
19800 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
19801 );
19802 editor.open_excerpts(&OpenExcerpts, window, cx);
19803 });
19804 cx.executor().run_until_parked();
19805 let second_item_id = workspace
19806 .update(cx, |workspace, window, cx| {
19807 let active_item = workspace
19808 .active_item(cx)
19809 .expect("should have an active item after navigating into the 2nd buffer");
19810 let second_item_id = active_item.item_id();
19811 assert_ne!(
19812 second_item_id, multibuffer_item_id,
19813 "Should navigate away from the multibuffer"
19814 );
19815 assert_ne!(
19816 second_item_id, first_item_id,
19817 "Should navigate into the 2nd buffer and activate it"
19818 );
19819 assert_eq!(
19820 active_item.buffer_kind(cx),
19821 ItemBufferKind::Singleton,
19822 "New active item should be a singleton buffer"
19823 );
19824 assert_eq!(
19825 active_item
19826 .act_as::<Editor>(cx)
19827 .expect("should have navigated into an editor")
19828 .read(cx)
19829 .text(cx),
19830 sample_text_2
19831 );
19832
19833 workspace
19834 .go_back(workspace.active_pane().downgrade(), window, cx)
19835 .detach_and_log_err(cx);
19836
19837 second_item_id
19838 })
19839 .unwrap();
19840 cx.executor().run_until_parked();
19841 workspace
19842 .update(cx, |workspace, _, cx| {
19843 let active_item = workspace
19844 .active_item(cx)
19845 .expect("should have an active item after navigating back from the 2nd buffer");
19846 assert_eq!(
19847 active_item.item_id(),
19848 multibuffer_item_id,
19849 "Should navigate back from the 2nd buffer to the multi buffer"
19850 );
19851 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19852 })
19853 .unwrap();
19854
19855 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19856 editor.change_selections(
19857 SelectionEffects::scroll(Autoscroll::Next),
19858 window,
19859 cx,
19860 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
19861 );
19862 editor.open_excerpts(&OpenExcerpts, window, cx);
19863 });
19864 cx.executor().run_until_parked();
19865 workspace
19866 .update(cx, |workspace, window, cx| {
19867 let active_item = workspace
19868 .active_item(cx)
19869 .expect("should have an active item after navigating into the 3rd buffer");
19870 let third_item_id = active_item.item_id();
19871 assert_ne!(
19872 third_item_id, multibuffer_item_id,
19873 "Should navigate into the 3rd buffer and activate it"
19874 );
19875 assert_ne!(third_item_id, first_item_id);
19876 assert_ne!(third_item_id, second_item_id);
19877 assert_eq!(
19878 active_item.buffer_kind(cx),
19879 ItemBufferKind::Singleton,
19880 "New active item should be a singleton buffer"
19881 );
19882 assert_eq!(
19883 active_item
19884 .act_as::<Editor>(cx)
19885 .expect("should have navigated into an editor")
19886 .read(cx)
19887 .text(cx),
19888 sample_text_3
19889 );
19890
19891 workspace
19892 .go_back(workspace.active_pane().downgrade(), window, cx)
19893 .detach_and_log_err(cx);
19894 })
19895 .unwrap();
19896 cx.executor().run_until_parked();
19897 workspace
19898 .update(cx, |workspace, _, cx| {
19899 let active_item = workspace
19900 .active_item(cx)
19901 .expect("should have an active item after navigating back from the 3rd buffer");
19902 assert_eq!(
19903 active_item.item_id(),
19904 multibuffer_item_id,
19905 "Should navigate back from the 3rd buffer to the multi buffer"
19906 );
19907 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19908 })
19909 .unwrap();
19910}
19911
19912#[gpui::test]
19913async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19914 init_test(cx, |_| {});
19915
19916 let mut cx = EditorTestContext::new(cx).await;
19917
19918 let diff_base = r#"
19919 use some::mod;
19920
19921 const A: u32 = 42;
19922
19923 fn main() {
19924 println!("hello");
19925
19926 println!("world");
19927 }
19928 "#
19929 .unindent();
19930
19931 cx.set_state(
19932 &r#"
19933 use some::modified;
19934
19935 ˇ
19936 fn main() {
19937 println!("hello there");
19938
19939 println!("around the");
19940 println!("world");
19941 }
19942 "#
19943 .unindent(),
19944 );
19945
19946 cx.set_head_text(&diff_base);
19947 executor.run_until_parked();
19948
19949 cx.update_editor(|editor, window, cx| {
19950 editor.go_to_next_hunk(&GoToHunk, window, cx);
19951 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19952 });
19953 executor.run_until_parked();
19954 cx.assert_state_with_diff(
19955 r#"
19956 use some::modified;
19957
19958
19959 fn main() {
19960 - println!("hello");
19961 + ˇ println!("hello there");
19962
19963 println!("around the");
19964 println!("world");
19965 }
19966 "#
19967 .unindent(),
19968 );
19969
19970 cx.update_editor(|editor, window, cx| {
19971 for _ in 0..2 {
19972 editor.go_to_next_hunk(&GoToHunk, window, cx);
19973 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19974 }
19975 });
19976 executor.run_until_parked();
19977 cx.assert_state_with_diff(
19978 r#"
19979 - use some::mod;
19980 + ˇuse some::modified;
19981
19982
19983 fn main() {
19984 - println!("hello");
19985 + println!("hello there");
19986
19987 + println!("around the");
19988 println!("world");
19989 }
19990 "#
19991 .unindent(),
19992 );
19993
19994 cx.update_editor(|editor, window, cx| {
19995 editor.go_to_next_hunk(&GoToHunk, window, cx);
19996 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19997 });
19998 executor.run_until_parked();
19999 cx.assert_state_with_diff(
20000 r#"
20001 - use some::mod;
20002 + use some::modified;
20003
20004 - const A: u32 = 42;
20005 ˇ
20006 fn main() {
20007 - println!("hello");
20008 + println!("hello there");
20009
20010 + println!("around the");
20011 println!("world");
20012 }
20013 "#
20014 .unindent(),
20015 );
20016
20017 cx.update_editor(|editor, window, cx| {
20018 editor.cancel(&Cancel, window, cx);
20019 });
20020
20021 cx.assert_state_with_diff(
20022 r#"
20023 use some::modified;
20024
20025 ˇ
20026 fn main() {
20027 println!("hello there");
20028
20029 println!("around the");
20030 println!("world");
20031 }
20032 "#
20033 .unindent(),
20034 );
20035}
20036
20037#[gpui::test]
20038async fn test_diff_base_change_with_expanded_diff_hunks(
20039 executor: BackgroundExecutor,
20040 cx: &mut TestAppContext,
20041) {
20042 init_test(cx, |_| {});
20043
20044 let mut cx = EditorTestContext::new(cx).await;
20045
20046 let diff_base = r#"
20047 use some::mod1;
20048 use some::mod2;
20049
20050 const A: u32 = 42;
20051 const B: u32 = 42;
20052 const C: u32 = 42;
20053
20054 fn main() {
20055 println!("hello");
20056
20057 println!("world");
20058 }
20059 "#
20060 .unindent();
20061
20062 cx.set_state(
20063 &r#"
20064 use some::mod2;
20065
20066 const A: u32 = 42;
20067 const C: u32 = 42;
20068
20069 fn main(ˇ) {
20070 //println!("hello");
20071
20072 println!("world");
20073 //
20074 //
20075 }
20076 "#
20077 .unindent(),
20078 );
20079
20080 cx.set_head_text(&diff_base);
20081 executor.run_until_parked();
20082
20083 cx.update_editor(|editor, window, cx| {
20084 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20085 });
20086 executor.run_until_parked();
20087 cx.assert_state_with_diff(
20088 r#"
20089 - use some::mod1;
20090 use some::mod2;
20091
20092 const A: u32 = 42;
20093 - const B: u32 = 42;
20094 const C: u32 = 42;
20095
20096 fn main(ˇ) {
20097 - println!("hello");
20098 + //println!("hello");
20099
20100 println!("world");
20101 + //
20102 + //
20103 }
20104 "#
20105 .unindent(),
20106 );
20107
20108 cx.set_head_text("new diff base!");
20109 executor.run_until_parked();
20110 cx.assert_state_with_diff(
20111 r#"
20112 - new diff base!
20113 + use some::mod2;
20114 +
20115 + const A: u32 = 42;
20116 + const C: u32 = 42;
20117 +
20118 + fn main(ˇ) {
20119 + //println!("hello");
20120 +
20121 + println!("world");
20122 + //
20123 + //
20124 + }
20125 "#
20126 .unindent(),
20127 );
20128}
20129
20130#[gpui::test]
20131async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20132 init_test(cx, |_| {});
20133
20134 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20135 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20136 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20137 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20138 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20139 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20140
20141 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20142 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20143 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20144
20145 let multi_buffer = cx.new(|cx| {
20146 let mut multibuffer = MultiBuffer::new(ReadWrite);
20147 multibuffer.push_excerpts(
20148 buffer_1.clone(),
20149 [
20150 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20151 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20152 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20153 ],
20154 cx,
20155 );
20156 multibuffer.push_excerpts(
20157 buffer_2.clone(),
20158 [
20159 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20160 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20161 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20162 ],
20163 cx,
20164 );
20165 multibuffer.push_excerpts(
20166 buffer_3.clone(),
20167 [
20168 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20169 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20170 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20171 ],
20172 cx,
20173 );
20174 multibuffer
20175 });
20176
20177 let editor =
20178 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20179 editor
20180 .update(cx, |editor, _window, cx| {
20181 for (buffer, diff_base) in [
20182 (buffer_1.clone(), file_1_old),
20183 (buffer_2.clone(), file_2_old),
20184 (buffer_3.clone(), file_3_old),
20185 ] {
20186 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20187 editor
20188 .buffer
20189 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20190 }
20191 })
20192 .unwrap();
20193
20194 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20195 cx.run_until_parked();
20196
20197 cx.assert_editor_state(
20198 &"
20199 ˇaaa
20200 ccc
20201 ddd
20202
20203 ggg
20204 hhh
20205
20206
20207 lll
20208 mmm
20209 NNN
20210
20211 qqq
20212 rrr
20213
20214 uuu
20215 111
20216 222
20217 333
20218
20219 666
20220 777
20221
20222 000
20223 !!!"
20224 .unindent(),
20225 );
20226
20227 cx.update_editor(|editor, window, cx| {
20228 editor.select_all(&SelectAll, window, cx);
20229 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20230 });
20231 cx.executor().run_until_parked();
20232
20233 cx.assert_state_with_diff(
20234 "
20235 «aaa
20236 - bbb
20237 ccc
20238 ddd
20239
20240 ggg
20241 hhh
20242
20243
20244 lll
20245 mmm
20246 - nnn
20247 + NNN
20248
20249 qqq
20250 rrr
20251
20252 uuu
20253 111
20254 222
20255 333
20256
20257 + 666
20258 777
20259
20260 000
20261 !!!ˇ»"
20262 .unindent(),
20263 );
20264}
20265
20266#[gpui::test]
20267async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20268 init_test(cx, |_| {});
20269
20270 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20271 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20272
20273 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20274 let multi_buffer = cx.new(|cx| {
20275 let mut multibuffer = MultiBuffer::new(ReadWrite);
20276 multibuffer.push_excerpts(
20277 buffer.clone(),
20278 [
20279 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20280 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20281 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20282 ],
20283 cx,
20284 );
20285 multibuffer
20286 });
20287
20288 let editor =
20289 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20290 editor
20291 .update(cx, |editor, _window, cx| {
20292 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20293 editor
20294 .buffer
20295 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20296 })
20297 .unwrap();
20298
20299 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20300 cx.run_until_parked();
20301
20302 cx.update_editor(|editor, window, cx| {
20303 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20304 });
20305 cx.executor().run_until_parked();
20306
20307 // When the start of a hunk coincides with the start of its excerpt,
20308 // the hunk is expanded. When the start of a hunk is earlier than
20309 // the start of its excerpt, the hunk is not expanded.
20310 cx.assert_state_with_diff(
20311 "
20312 ˇaaa
20313 - bbb
20314 + BBB
20315
20316 - ddd
20317 - eee
20318 + DDD
20319 + EEE
20320 fff
20321
20322 iii
20323 "
20324 .unindent(),
20325 );
20326}
20327
20328#[gpui::test]
20329async fn test_edits_around_expanded_insertion_hunks(
20330 executor: BackgroundExecutor,
20331 cx: &mut TestAppContext,
20332) {
20333 init_test(cx, |_| {});
20334
20335 let mut cx = EditorTestContext::new(cx).await;
20336
20337 let diff_base = r#"
20338 use some::mod1;
20339 use some::mod2;
20340
20341 const A: u32 = 42;
20342
20343 fn main() {
20344 println!("hello");
20345
20346 println!("world");
20347 }
20348 "#
20349 .unindent();
20350 executor.run_until_parked();
20351 cx.set_state(
20352 &r#"
20353 use some::mod1;
20354 use some::mod2;
20355
20356 const A: u32 = 42;
20357 const B: u32 = 42;
20358 const C: u32 = 42;
20359 ˇ
20360
20361 fn main() {
20362 println!("hello");
20363
20364 println!("world");
20365 }
20366 "#
20367 .unindent(),
20368 );
20369
20370 cx.set_head_text(&diff_base);
20371 executor.run_until_parked();
20372
20373 cx.update_editor(|editor, window, cx| {
20374 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20375 });
20376 executor.run_until_parked();
20377
20378 cx.assert_state_with_diff(
20379 r#"
20380 use some::mod1;
20381 use some::mod2;
20382
20383 const A: u32 = 42;
20384 + const B: u32 = 42;
20385 + const C: u32 = 42;
20386 + ˇ
20387
20388 fn main() {
20389 println!("hello");
20390
20391 println!("world");
20392 }
20393 "#
20394 .unindent(),
20395 );
20396
20397 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20398 executor.run_until_parked();
20399
20400 cx.assert_state_with_diff(
20401 r#"
20402 use some::mod1;
20403 use some::mod2;
20404
20405 const A: u32 = 42;
20406 + const B: u32 = 42;
20407 + const C: u32 = 42;
20408 + const D: u32 = 42;
20409 + ˇ
20410
20411 fn main() {
20412 println!("hello");
20413
20414 println!("world");
20415 }
20416 "#
20417 .unindent(),
20418 );
20419
20420 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20421 executor.run_until_parked();
20422
20423 cx.assert_state_with_diff(
20424 r#"
20425 use some::mod1;
20426 use some::mod2;
20427
20428 const A: u32 = 42;
20429 + const B: u32 = 42;
20430 + const C: u32 = 42;
20431 + const D: u32 = 42;
20432 + const E: u32 = 42;
20433 + ˇ
20434
20435 fn main() {
20436 println!("hello");
20437
20438 println!("world");
20439 }
20440 "#
20441 .unindent(),
20442 );
20443
20444 cx.update_editor(|editor, window, cx| {
20445 editor.delete_line(&DeleteLine, window, cx);
20446 });
20447 executor.run_until_parked();
20448
20449 cx.assert_state_with_diff(
20450 r#"
20451 use some::mod1;
20452 use some::mod2;
20453
20454 const A: u32 = 42;
20455 + const B: u32 = 42;
20456 + const C: u32 = 42;
20457 + const D: u32 = 42;
20458 + const E: u32 = 42;
20459 ˇ
20460 fn main() {
20461 println!("hello");
20462
20463 println!("world");
20464 }
20465 "#
20466 .unindent(),
20467 );
20468
20469 cx.update_editor(|editor, window, cx| {
20470 editor.move_up(&MoveUp, window, cx);
20471 editor.delete_line(&DeleteLine, window, cx);
20472 editor.move_up(&MoveUp, window, cx);
20473 editor.delete_line(&DeleteLine, window, cx);
20474 editor.move_up(&MoveUp, window, cx);
20475 editor.delete_line(&DeleteLine, window, cx);
20476 });
20477 executor.run_until_parked();
20478 cx.assert_state_with_diff(
20479 r#"
20480 use some::mod1;
20481 use some::mod2;
20482
20483 const A: u32 = 42;
20484 + const B: u32 = 42;
20485 ˇ
20486 fn main() {
20487 println!("hello");
20488
20489 println!("world");
20490 }
20491 "#
20492 .unindent(),
20493 );
20494
20495 cx.update_editor(|editor, window, cx| {
20496 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20497 editor.delete_line(&DeleteLine, window, cx);
20498 });
20499 executor.run_until_parked();
20500 cx.assert_state_with_diff(
20501 r#"
20502 ˇ
20503 fn main() {
20504 println!("hello");
20505
20506 println!("world");
20507 }
20508 "#
20509 .unindent(),
20510 );
20511}
20512
20513#[gpui::test]
20514async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20515 init_test(cx, |_| {});
20516
20517 let mut cx = EditorTestContext::new(cx).await;
20518 cx.set_head_text(indoc! { "
20519 one
20520 two
20521 three
20522 four
20523 five
20524 "
20525 });
20526 cx.set_state(indoc! { "
20527 one
20528 ˇthree
20529 five
20530 "});
20531 cx.run_until_parked();
20532 cx.update_editor(|editor, window, cx| {
20533 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20534 });
20535 cx.assert_state_with_diff(
20536 indoc! { "
20537 one
20538 - two
20539 ˇthree
20540 - four
20541 five
20542 "}
20543 .to_string(),
20544 );
20545 cx.update_editor(|editor, window, cx| {
20546 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20547 });
20548
20549 cx.assert_state_with_diff(
20550 indoc! { "
20551 one
20552 ˇthree
20553 five
20554 "}
20555 .to_string(),
20556 );
20557
20558 cx.set_state(indoc! { "
20559 one
20560 ˇTWO
20561 three
20562 four
20563 five
20564 "});
20565 cx.run_until_parked();
20566 cx.update_editor(|editor, window, cx| {
20567 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20568 });
20569
20570 cx.assert_state_with_diff(
20571 indoc! { "
20572 one
20573 - two
20574 + ˇTWO
20575 three
20576 four
20577 five
20578 "}
20579 .to_string(),
20580 );
20581 cx.update_editor(|editor, window, cx| {
20582 editor.move_up(&Default::default(), window, cx);
20583 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20584 });
20585 cx.assert_state_with_diff(
20586 indoc! { "
20587 one
20588 ˇTWO
20589 three
20590 four
20591 five
20592 "}
20593 .to_string(),
20594 );
20595}
20596
20597#[gpui::test]
20598async fn test_edits_around_expanded_deletion_hunks(
20599 executor: BackgroundExecutor,
20600 cx: &mut TestAppContext,
20601) {
20602 init_test(cx, |_| {});
20603
20604 let mut cx = EditorTestContext::new(cx).await;
20605
20606 let diff_base = r#"
20607 use some::mod1;
20608 use some::mod2;
20609
20610 const A: u32 = 42;
20611 const B: u32 = 42;
20612 const C: u32 = 42;
20613
20614
20615 fn main() {
20616 println!("hello");
20617
20618 println!("world");
20619 }
20620 "#
20621 .unindent();
20622 executor.run_until_parked();
20623 cx.set_state(
20624 &r#"
20625 use some::mod1;
20626 use some::mod2;
20627
20628 ˇconst B: u32 = 42;
20629 const C: u32 = 42;
20630
20631
20632 fn main() {
20633 println!("hello");
20634
20635 println!("world");
20636 }
20637 "#
20638 .unindent(),
20639 );
20640
20641 cx.set_head_text(&diff_base);
20642 executor.run_until_parked();
20643
20644 cx.update_editor(|editor, window, cx| {
20645 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20646 });
20647 executor.run_until_parked();
20648
20649 cx.assert_state_with_diff(
20650 r#"
20651 use some::mod1;
20652 use some::mod2;
20653
20654 - const A: u32 = 42;
20655 ˇconst B: u32 = 42;
20656 const C: u32 = 42;
20657
20658
20659 fn main() {
20660 println!("hello");
20661
20662 println!("world");
20663 }
20664 "#
20665 .unindent(),
20666 );
20667
20668 cx.update_editor(|editor, window, cx| {
20669 editor.delete_line(&DeleteLine, window, cx);
20670 });
20671 executor.run_until_parked();
20672 cx.assert_state_with_diff(
20673 r#"
20674 use some::mod1;
20675 use some::mod2;
20676
20677 - const A: u32 = 42;
20678 - const B: u32 = 42;
20679 ˇconst C: u32 = 42;
20680
20681
20682 fn main() {
20683 println!("hello");
20684
20685 println!("world");
20686 }
20687 "#
20688 .unindent(),
20689 );
20690
20691 cx.update_editor(|editor, window, cx| {
20692 editor.delete_line(&DeleteLine, window, cx);
20693 });
20694 executor.run_until_parked();
20695 cx.assert_state_with_diff(
20696 r#"
20697 use some::mod1;
20698 use some::mod2;
20699
20700 - const A: u32 = 42;
20701 - const B: u32 = 42;
20702 - const C: u32 = 42;
20703 ˇ
20704
20705 fn main() {
20706 println!("hello");
20707
20708 println!("world");
20709 }
20710 "#
20711 .unindent(),
20712 );
20713
20714 cx.update_editor(|editor, window, cx| {
20715 editor.handle_input("replacement", window, cx);
20716 });
20717 executor.run_until_parked();
20718 cx.assert_state_with_diff(
20719 r#"
20720 use some::mod1;
20721 use some::mod2;
20722
20723 - const A: u32 = 42;
20724 - const B: u32 = 42;
20725 - const C: u32 = 42;
20726 -
20727 + replacementˇ
20728
20729 fn main() {
20730 println!("hello");
20731
20732 println!("world");
20733 }
20734 "#
20735 .unindent(),
20736 );
20737}
20738
20739#[gpui::test]
20740async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20741 init_test(cx, |_| {});
20742
20743 let mut cx = EditorTestContext::new(cx).await;
20744
20745 let base_text = r#"
20746 one
20747 two
20748 three
20749 four
20750 five
20751 "#
20752 .unindent();
20753 executor.run_until_parked();
20754 cx.set_state(
20755 &r#"
20756 one
20757 two
20758 fˇour
20759 five
20760 "#
20761 .unindent(),
20762 );
20763
20764 cx.set_head_text(&base_text);
20765 executor.run_until_parked();
20766
20767 cx.update_editor(|editor, window, cx| {
20768 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20769 });
20770 executor.run_until_parked();
20771
20772 cx.assert_state_with_diff(
20773 r#"
20774 one
20775 two
20776 - three
20777 fˇour
20778 five
20779 "#
20780 .unindent(),
20781 );
20782
20783 cx.update_editor(|editor, window, cx| {
20784 editor.backspace(&Backspace, window, cx);
20785 editor.backspace(&Backspace, window, cx);
20786 });
20787 executor.run_until_parked();
20788 cx.assert_state_with_diff(
20789 r#"
20790 one
20791 two
20792 - threeˇ
20793 - four
20794 + our
20795 five
20796 "#
20797 .unindent(),
20798 );
20799}
20800
20801#[gpui::test]
20802async fn test_edit_after_expanded_modification_hunk(
20803 executor: BackgroundExecutor,
20804 cx: &mut TestAppContext,
20805) {
20806 init_test(cx, |_| {});
20807
20808 let mut cx = EditorTestContext::new(cx).await;
20809
20810 let diff_base = r#"
20811 use some::mod1;
20812 use some::mod2;
20813
20814 const A: u32 = 42;
20815 const B: u32 = 42;
20816 const C: u32 = 42;
20817 const D: u32 = 42;
20818
20819
20820 fn main() {
20821 println!("hello");
20822
20823 println!("world");
20824 }"#
20825 .unindent();
20826
20827 cx.set_state(
20828 &r#"
20829 use some::mod1;
20830 use some::mod2;
20831
20832 const A: u32 = 42;
20833 const B: u32 = 42;
20834 const C: u32 = 43ˇ
20835 const D: u32 = 42;
20836
20837
20838 fn main() {
20839 println!("hello");
20840
20841 println!("world");
20842 }"#
20843 .unindent(),
20844 );
20845
20846 cx.set_head_text(&diff_base);
20847 executor.run_until_parked();
20848 cx.update_editor(|editor, window, cx| {
20849 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20850 });
20851 executor.run_until_parked();
20852
20853 cx.assert_state_with_diff(
20854 r#"
20855 use some::mod1;
20856 use some::mod2;
20857
20858 const A: u32 = 42;
20859 const B: u32 = 42;
20860 - const C: u32 = 42;
20861 + const C: u32 = 43ˇ
20862 const D: u32 = 42;
20863
20864
20865 fn main() {
20866 println!("hello");
20867
20868 println!("world");
20869 }"#
20870 .unindent(),
20871 );
20872
20873 cx.update_editor(|editor, window, cx| {
20874 editor.handle_input("\nnew_line\n", window, cx);
20875 });
20876 executor.run_until_parked();
20877
20878 cx.assert_state_with_diff(
20879 r#"
20880 use some::mod1;
20881 use some::mod2;
20882
20883 const A: u32 = 42;
20884 const B: u32 = 42;
20885 - const C: u32 = 42;
20886 + const C: u32 = 43
20887 + new_line
20888 + ˇ
20889 const D: u32 = 42;
20890
20891
20892 fn main() {
20893 println!("hello");
20894
20895 println!("world");
20896 }"#
20897 .unindent(),
20898 );
20899}
20900
20901#[gpui::test]
20902async fn test_stage_and_unstage_added_file_hunk(
20903 executor: BackgroundExecutor,
20904 cx: &mut TestAppContext,
20905) {
20906 init_test(cx, |_| {});
20907
20908 let mut cx = EditorTestContext::new(cx).await;
20909 cx.update_editor(|editor, _, cx| {
20910 editor.set_expand_all_diff_hunks(cx);
20911 });
20912
20913 let working_copy = r#"
20914 ˇfn main() {
20915 println!("hello, world!");
20916 }
20917 "#
20918 .unindent();
20919
20920 cx.set_state(&working_copy);
20921 executor.run_until_parked();
20922
20923 cx.assert_state_with_diff(
20924 r#"
20925 + ˇfn main() {
20926 + println!("hello, world!");
20927 + }
20928 "#
20929 .unindent(),
20930 );
20931 cx.assert_index_text(None);
20932
20933 cx.update_editor(|editor, window, cx| {
20934 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20935 });
20936 executor.run_until_parked();
20937 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20938 cx.assert_state_with_diff(
20939 r#"
20940 + ˇfn main() {
20941 + println!("hello, world!");
20942 + }
20943 "#
20944 .unindent(),
20945 );
20946
20947 cx.update_editor(|editor, window, cx| {
20948 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20949 });
20950 executor.run_until_parked();
20951 cx.assert_index_text(None);
20952}
20953
20954async fn setup_indent_guides_editor(
20955 text: &str,
20956 cx: &mut TestAppContext,
20957) -> (BufferId, EditorTestContext) {
20958 init_test(cx, |_| {});
20959
20960 let mut cx = EditorTestContext::new(cx).await;
20961
20962 let buffer_id = cx.update_editor(|editor, window, cx| {
20963 editor.set_text(text, window, cx);
20964 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20965
20966 buffer_ids[0]
20967 });
20968
20969 (buffer_id, cx)
20970}
20971
20972fn assert_indent_guides(
20973 range: Range<u32>,
20974 expected: Vec<IndentGuide>,
20975 active_indices: Option<Vec<usize>>,
20976 cx: &mut EditorTestContext,
20977) {
20978 let indent_guides = cx.update_editor(|editor, window, cx| {
20979 let snapshot = editor.snapshot(window, cx).display_snapshot;
20980 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20981 editor,
20982 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20983 true,
20984 &snapshot,
20985 cx,
20986 );
20987
20988 indent_guides.sort_by(|a, b| {
20989 a.depth.cmp(&b.depth).then(
20990 a.start_row
20991 .cmp(&b.start_row)
20992 .then(a.end_row.cmp(&b.end_row)),
20993 )
20994 });
20995 indent_guides
20996 });
20997
20998 if let Some(expected) = active_indices {
20999 let active_indices = cx.update_editor(|editor, window, cx| {
21000 let snapshot = editor.snapshot(window, cx).display_snapshot;
21001 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21002 });
21003
21004 assert_eq!(
21005 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21006 expected,
21007 "Active indent guide indices do not match"
21008 );
21009 }
21010
21011 assert_eq!(indent_guides, expected, "Indent guides do not match");
21012}
21013
21014fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21015 IndentGuide {
21016 buffer_id,
21017 start_row: MultiBufferRow(start_row),
21018 end_row: MultiBufferRow(end_row),
21019 depth,
21020 tab_size: 4,
21021 settings: IndentGuideSettings {
21022 enabled: true,
21023 line_width: 1,
21024 active_line_width: 1,
21025 coloring: IndentGuideColoring::default(),
21026 background_coloring: IndentGuideBackgroundColoring::default(),
21027 },
21028 }
21029}
21030
21031#[gpui::test]
21032async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21033 let (buffer_id, mut cx) = setup_indent_guides_editor(
21034 &"
21035 fn main() {
21036 let a = 1;
21037 }"
21038 .unindent(),
21039 cx,
21040 )
21041 .await;
21042
21043 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21044}
21045
21046#[gpui::test]
21047async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21048 let (buffer_id, mut cx) = setup_indent_guides_editor(
21049 &"
21050 fn main() {
21051 let a = 1;
21052 let b = 2;
21053 }"
21054 .unindent(),
21055 cx,
21056 )
21057 .await;
21058
21059 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21060}
21061
21062#[gpui::test]
21063async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21064 let (buffer_id, mut cx) = setup_indent_guides_editor(
21065 &"
21066 fn main() {
21067 let a = 1;
21068 if a == 3 {
21069 let b = 2;
21070 } else {
21071 let c = 3;
21072 }
21073 }"
21074 .unindent(),
21075 cx,
21076 )
21077 .await;
21078
21079 assert_indent_guides(
21080 0..8,
21081 vec![
21082 indent_guide(buffer_id, 1, 6, 0),
21083 indent_guide(buffer_id, 3, 3, 1),
21084 indent_guide(buffer_id, 5, 5, 1),
21085 ],
21086 None,
21087 &mut cx,
21088 );
21089}
21090
21091#[gpui::test]
21092async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21093 let (buffer_id, mut cx) = setup_indent_guides_editor(
21094 &"
21095 fn main() {
21096 let a = 1;
21097 let b = 2;
21098 let c = 3;
21099 }"
21100 .unindent(),
21101 cx,
21102 )
21103 .await;
21104
21105 assert_indent_guides(
21106 0..5,
21107 vec![
21108 indent_guide(buffer_id, 1, 3, 0),
21109 indent_guide(buffer_id, 2, 2, 1),
21110 ],
21111 None,
21112 &mut cx,
21113 );
21114}
21115
21116#[gpui::test]
21117async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21118 let (buffer_id, mut cx) = setup_indent_guides_editor(
21119 &"
21120 fn main() {
21121 let a = 1;
21122
21123 let c = 3;
21124 }"
21125 .unindent(),
21126 cx,
21127 )
21128 .await;
21129
21130 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21131}
21132
21133#[gpui::test]
21134async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21135 let (buffer_id, mut cx) = setup_indent_guides_editor(
21136 &"
21137 fn main() {
21138 let a = 1;
21139
21140 let c = 3;
21141
21142 if a == 3 {
21143 let b = 2;
21144 } else {
21145 let c = 3;
21146 }
21147 }"
21148 .unindent(),
21149 cx,
21150 )
21151 .await;
21152
21153 assert_indent_guides(
21154 0..11,
21155 vec![
21156 indent_guide(buffer_id, 1, 9, 0),
21157 indent_guide(buffer_id, 6, 6, 1),
21158 indent_guide(buffer_id, 8, 8, 1),
21159 ],
21160 None,
21161 &mut cx,
21162 );
21163}
21164
21165#[gpui::test]
21166async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21167 let (buffer_id, mut cx) = setup_indent_guides_editor(
21168 &"
21169 fn main() {
21170 let a = 1;
21171
21172 let c = 3;
21173
21174 if a == 3 {
21175 let b = 2;
21176 } else {
21177 let c = 3;
21178 }
21179 }"
21180 .unindent(),
21181 cx,
21182 )
21183 .await;
21184
21185 assert_indent_guides(
21186 1..11,
21187 vec![
21188 indent_guide(buffer_id, 1, 9, 0),
21189 indent_guide(buffer_id, 6, 6, 1),
21190 indent_guide(buffer_id, 8, 8, 1),
21191 ],
21192 None,
21193 &mut cx,
21194 );
21195}
21196
21197#[gpui::test]
21198async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21199 let (buffer_id, mut cx) = setup_indent_guides_editor(
21200 &"
21201 fn main() {
21202 let a = 1;
21203
21204 let c = 3;
21205
21206 if a == 3 {
21207 let b = 2;
21208 } else {
21209 let c = 3;
21210 }
21211 }"
21212 .unindent(),
21213 cx,
21214 )
21215 .await;
21216
21217 assert_indent_guides(
21218 1..10,
21219 vec![
21220 indent_guide(buffer_id, 1, 9, 0),
21221 indent_guide(buffer_id, 6, 6, 1),
21222 indent_guide(buffer_id, 8, 8, 1),
21223 ],
21224 None,
21225 &mut cx,
21226 );
21227}
21228
21229#[gpui::test]
21230async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21231 let (buffer_id, mut cx) = setup_indent_guides_editor(
21232 &"
21233 fn main() {
21234 if a {
21235 b(
21236 c,
21237 d,
21238 )
21239 } else {
21240 e(
21241 f
21242 )
21243 }
21244 }"
21245 .unindent(),
21246 cx,
21247 )
21248 .await;
21249
21250 assert_indent_guides(
21251 0..11,
21252 vec![
21253 indent_guide(buffer_id, 1, 10, 0),
21254 indent_guide(buffer_id, 2, 5, 1),
21255 indent_guide(buffer_id, 7, 9, 1),
21256 indent_guide(buffer_id, 3, 4, 2),
21257 indent_guide(buffer_id, 8, 8, 2),
21258 ],
21259 None,
21260 &mut cx,
21261 );
21262
21263 cx.update_editor(|editor, window, cx| {
21264 editor.fold_at(MultiBufferRow(2), window, cx);
21265 assert_eq!(
21266 editor.display_text(cx),
21267 "
21268 fn main() {
21269 if a {
21270 b(⋯
21271 )
21272 } else {
21273 e(
21274 f
21275 )
21276 }
21277 }"
21278 .unindent()
21279 );
21280 });
21281
21282 assert_indent_guides(
21283 0..11,
21284 vec![
21285 indent_guide(buffer_id, 1, 10, 0),
21286 indent_guide(buffer_id, 2, 5, 1),
21287 indent_guide(buffer_id, 7, 9, 1),
21288 indent_guide(buffer_id, 8, 8, 2),
21289 ],
21290 None,
21291 &mut cx,
21292 );
21293}
21294
21295#[gpui::test]
21296async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21297 let (buffer_id, mut cx) = setup_indent_guides_editor(
21298 &"
21299 block1
21300 block2
21301 block3
21302 block4
21303 block2
21304 block1
21305 block1"
21306 .unindent(),
21307 cx,
21308 )
21309 .await;
21310
21311 assert_indent_guides(
21312 1..10,
21313 vec![
21314 indent_guide(buffer_id, 1, 4, 0),
21315 indent_guide(buffer_id, 2, 3, 1),
21316 indent_guide(buffer_id, 3, 3, 2),
21317 ],
21318 None,
21319 &mut cx,
21320 );
21321}
21322
21323#[gpui::test]
21324async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21325 let (buffer_id, mut cx) = setup_indent_guides_editor(
21326 &"
21327 block1
21328 block2
21329 block3
21330
21331 block1
21332 block1"
21333 .unindent(),
21334 cx,
21335 )
21336 .await;
21337
21338 assert_indent_guides(
21339 0..6,
21340 vec![
21341 indent_guide(buffer_id, 1, 2, 0),
21342 indent_guide(buffer_id, 2, 2, 1),
21343 ],
21344 None,
21345 &mut cx,
21346 );
21347}
21348
21349#[gpui::test]
21350async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21351 let (buffer_id, mut cx) = setup_indent_guides_editor(
21352 &"
21353 function component() {
21354 \treturn (
21355 \t\t\t
21356 \t\t<div>
21357 \t\t\t<abc></abc>
21358 \t\t</div>
21359 \t)
21360 }"
21361 .unindent(),
21362 cx,
21363 )
21364 .await;
21365
21366 assert_indent_guides(
21367 0..8,
21368 vec![
21369 indent_guide(buffer_id, 1, 6, 0),
21370 indent_guide(buffer_id, 2, 5, 1),
21371 indent_guide(buffer_id, 4, 4, 2),
21372 ],
21373 None,
21374 &mut cx,
21375 );
21376}
21377
21378#[gpui::test]
21379async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21380 let (buffer_id, mut cx) = setup_indent_guides_editor(
21381 &"
21382 function component() {
21383 \treturn (
21384 \t
21385 \t\t<div>
21386 \t\t\t<abc></abc>
21387 \t\t</div>
21388 \t)
21389 }"
21390 .unindent(),
21391 cx,
21392 )
21393 .await;
21394
21395 assert_indent_guides(
21396 0..8,
21397 vec![
21398 indent_guide(buffer_id, 1, 6, 0),
21399 indent_guide(buffer_id, 2, 5, 1),
21400 indent_guide(buffer_id, 4, 4, 2),
21401 ],
21402 None,
21403 &mut cx,
21404 );
21405}
21406
21407#[gpui::test]
21408async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21409 let (buffer_id, mut cx) = setup_indent_guides_editor(
21410 &"
21411 block1
21412
21413
21414
21415 block2
21416 "
21417 .unindent(),
21418 cx,
21419 )
21420 .await;
21421
21422 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21423}
21424
21425#[gpui::test]
21426async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21427 let (buffer_id, mut cx) = setup_indent_guides_editor(
21428 &"
21429 def a:
21430 \tb = 3
21431 \tif True:
21432 \t\tc = 4
21433 \t\td = 5
21434 \tprint(b)
21435 "
21436 .unindent(),
21437 cx,
21438 )
21439 .await;
21440
21441 assert_indent_guides(
21442 0..6,
21443 vec![
21444 indent_guide(buffer_id, 1, 5, 0),
21445 indent_guide(buffer_id, 3, 4, 1),
21446 ],
21447 None,
21448 &mut cx,
21449 );
21450}
21451
21452#[gpui::test]
21453async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21454 let (buffer_id, mut cx) = setup_indent_guides_editor(
21455 &"
21456 fn main() {
21457 let a = 1;
21458 }"
21459 .unindent(),
21460 cx,
21461 )
21462 .await;
21463
21464 cx.update_editor(|editor, window, cx| {
21465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21466 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21467 });
21468 });
21469
21470 assert_indent_guides(
21471 0..3,
21472 vec![indent_guide(buffer_id, 1, 1, 0)],
21473 Some(vec![0]),
21474 &mut cx,
21475 );
21476}
21477
21478#[gpui::test]
21479async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21480 let (buffer_id, mut cx) = setup_indent_guides_editor(
21481 &"
21482 fn main() {
21483 if 1 == 2 {
21484 let a = 1;
21485 }
21486 }"
21487 .unindent(),
21488 cx,
21489 )
21490 .await;
21491
21492 cx.update_editor(|editor, window, cx| {
21493 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21494 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21495 });
21496 });
21497
21498 assert_indent_guides(
21499 0..4,
21500 vec![
21501 indent_guide(buffer_id, 1, 3, 0),
21502 indent_guide(buffer_id, 2, 2, 1),
21503 ],
21504 Some(vec![1]),
21505 &mut cx,
21506 );
21507
21508 cx.update_editor(|editor, window, cx| {
21509 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21510 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21511 });
21512 });
21513
21514 assert_indent_guides(
21515 0..4,
21516 vec![
21517 indent_guide(buffer_id, 1, 3, 0),
21518 indent_guide(buffer_id, 2, 2, 1),
21519 ],
21520 Some(vec![1]),
21521 &mut cx,
21522 );
21523
21524 cx.update_editor(|editor, window, cx| {
21525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21526 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21527 });
21528 });
21529
21530 assert_indent_guides(
21531 0..4,
21532 vec![
21533 indent_guide(buffer_id, 1, 3, 0),
21534 indent_guide(buffer_id, 2, 2, 1),
21535 ],
21536 Some(vec![0]),
21537 &mut cx,
21538 );
21539}
21540
21541#[gpui::test]
21542async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21543 let (buffer_id, mut cx) = setup_indent_guides_editor(
21544 &"
21545 fn main() {
21546 let a = 1;
21547
21548 let b = 2;
21549 }"
21550 .unindent(),
21551 cx,
21552 )
21553 .await;
21554
21555 cx.update_editor(|editor, window, cx| {
21556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21557 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21558 });
21559 });
21560
21561 assert_indent_guides(
21562 0..5,
21563 vec![indent_guide(buffer_id, 1, 3, 0)],
21564 Some(vec![0]),
21565 &mut cx,
21566 );
21567}
21568
21569#[gpui::test]
21570async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21571 let (buffer_id, mut cx) = setup_indent_guides_editor(
21572 &"
21573 def m:
21574 a = 1
21575 pass"
21576 .unindent(),
21577 cx,
21578 )
21579 .await;
21580
21581 cx.update_editor(|editor, window, cx| {
21582 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21583 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21584 });
21585 });
21586
21587 assert_indent_guides(
21588 0..3,
21589 vec![indent_guide(buffer_id, 1, 2, 0)],
21590 Some(vec![0]),
21591 &mut cx,
21592 );
21593}
21594
21595#[gpui::test]
21596async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21597 init_test(cx, |_| {});
21598 let mut cx = EditorTestContext::new(cx).await;
21599 let text = indoc! {
21600 "
21601 impl A {
21602 fn b() {
21603 0;
21604 3;
21605 5;
21606 6;
21607 7;
21608 }
21609 }
21610 "
21611 };
21612 let base_text = indoc! {
21613 "
21614 impl A {
21615 fn b() {
21616 0;
21617 1;
21618 2;
21619 3;
21620 4;
21621 }
21622 fn c() {
21623 5;
21624 6;
21625 7;
21626 }
21627 }
21628 "
21629 };
21630
21631 cx.update_editor(|editor, window, cx| {
21632 editor.set_text(text, window, cx);
21633
21634 editor.buffer().update(cx, |multibuffer, cx| {
21635 let buffer = multibuffer.as_singleton().unwrap();
21636 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21637
21638 multibuffer.set_all_diff_hunks_expanded(cx);
21639 multibuffer.add_diff(diff, cx);
21640
21641 buffer.read(cx).remote_id()
21642 })
21643 });
21644 cx.run_until_parked();
21645
21646 cx.assert_state_with_diff(
21647 indoc! { "
21648 impl A {
21649 fn b() {
21650 0;
21651 - 1;
21652 - 2;
21653 3;
21654 - 4;
21655 - }
21656 - fn c() {
21657 5;
21658 6;
21659 7;
21660 }
21661 }
21662 ˇ"
21663 }
21664 .to_string(),
21665 );
21666
21667 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21668 editor
21669 .snapshot(window, cx)
21670 .buffer_snapshot()
21671 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21672 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21673 .collect::<Vec<_>>()
21674 });
21675 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21676 assert_eq!(
21677 actual_guides,
21678 vec![
21679 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21680 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21681 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21682 ]
21683 );
21684}
21685
21686#[gpui::test]
21687async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21688 init_test(cx, |_| {});
21689 let mut cx = EditorTestContext::new(cx).await;
21690
21691 let diff_base = r#"
21692 a
21693 b
21694 c
21695 "#
21696 .unindent();
21697
21698 cx.set_state(
21699 &r#"
21700 ˇA
21701 b
21702 C
21703 "#
21704 .unindent(),
21705 );
21706 cx.set_head_text(&diff_base);
21707 cx.update_editor(|editor, window, cx| {
21708 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21709 });
21710 executor.run_until_parked();
21711
21712 let both_hunks_expanded = r#"
21713 - a
21714 + ˇA
21715 b
21716 - c
21717 + C
21718 "#
21719 .unindent();
21720
21721 cx.assert_state_with_diff(both_hunks_expanded.clone());
21722
21723 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21724 let snapshot = editor.snapshot(window, cx);
21725 let hunks = editor
21726 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21727 .collect::<Vec<_>>();
21728 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21729 hunks
21730 .into_iter()
21731 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21732 .collect::<Vec<_>>()
21733 });
21734 assert_eq!(hunk_ranges.len(), 2);
21735
21736 cx.update_editor(|editor, _, cx| {
21737 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21738 });
21739 executor.run_until_parked();
21740
21741 let second_hunk_expanded = r#"
21742 ˇA
21743 b
21744 - c
21745 + C
21746 "#
21747 .unindent();
21748
21749 cx.assert_state_with_diff(second_hunk_expanded);
21750
21751 cx.update_editor(|editor, _, cx| {
21752 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21753 });
21754 executor.run_until_parked();
21755
21756 cx.assert_state_with_diff(both_hunks_expanded.clone());
21757
21758 cx.update_editor(|editor, _, cx| {
21759 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21760 });
21761 executor.run_until_parked();
21762
21763 let first_hunk_expanded = r#"
21764 - a
21765 + ˇA
21766 b
21767 C
21768 "#
21769 .unindent();
21770
21771 cx.assert_state_with_diff(first_hunk_expanded);
21772
21773 cx.update_editor(|editor, _, cx| {
21774 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21775 });
21776 executor.run_until_parked();
21777
21778 cx.assert_state_with_diff(both_hunks_expanded);
21779
21780 cx.set_state(
21781 &r#"
21782 ˇA
21783 b
21784 "#
21785 .unindent(),
21786 );
21787 cx.run_until_parked();
21788
21789 // TODO this cursor position seems bad
21790 cx.assert_state_with_diff(
21791 r#"
21792 - ˇa
21793 + A
21794 b
21795 "#
21796 .unindent(),
21797 );
21798
21799 cx.update_editor(|editor, window, cx| {
21800 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21801 });
21802
21803 cx.assert_state_with_diff(
21804 r#"
21805 - ˇa
21806 + A
21807 b
21808 - c
21809 "#
21810 .unindent(),
21811 );
21812
21813 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21814 let snapshot = editor.snapshot(window, cx);
21815 let hunks = editor
21816 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21817 .collect::<Vec<_>>();
21818 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21819 hunks
21820 .into_iter()
21821 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21822 .collect::<Vec<_>>()
21823 });
21824 assert_eq!(hunk_ranges.len(), 2);
21825
21826 cx.update_editor(|editor, _, cx| {
21827 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21828 });
21829 executor.run_until_parked();
21830
21831 cx.assert_state_with_diff(
21832 r#"
21833 - ˇa
21834 + A
21835 b
21836 "#
21837 .unindent(),
21838 );
21839}
21840
21841#[gpui::test]
21842async fn test_toggle_deletion_hunk_at_start_of_file(
21843 executor: BackgroundExecutor,
21844 cx: &mut TestAppContext,
21845) {
21846 init_test(cx, |_| {});
21847 let mut cx = EditorTestContext::new(cx).await;
21848
21849 let diff_base = r#"
21850 a
21851 b
21852 c
21853 "#
21854 .unindent();
21855
21856 cx.set_state(
21857 &r#"
21858 ˇb
21859 c
21860 "#
21861 .unindent(),
21862 );
21863 cx.set_head_text(&diff_base);
21864 cx.update_editor(|editor, window, cx| {
21865 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21866 });
21867 executor.run_until_parked();
21868
21869 let hunk_expanded = r#"
21870 - a
21871 ˇb
21872 c
21873 "#
21874 .unindent();
21875
21876 cx.assert_state_with_diff(hunk_expanded.clone());
21877
21878 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21879 let snapshot = editor.snapshot(window, cx);
21880 let hunks = editor
21881 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21882 .collect::<Vec<_>>();
21883 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21884 hunks
21885 .into_iter()
21886 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21887 .collect::<Vec<_>>()
21888 });
21889 assert_eq!(hunk_ranges.len(), 1);
21890
21891 cx.update_editor(|editor, _, cx| {
21892 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21893 });
21894 executor.run_until_parked();
21895
21896 let hunk_collapsed = r#"
21897 ˇb
21898 c
21899 "#
21900 .unindent();
21901
21902 cx.assert_state_with_diff(hunk_collapsed);
21903
21904 cx.update_editor(|editor, _, cx| {
21905 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21906 });
21907 executor.run_until_parked();
21908
21909 cx.assert_state_with_diff(hunk_expanded);
21910}
21911
21912#[gpui::test]
21913async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21914 init_test(cx, |_| {});
21915
21916 let fs = FakeFs::new(cx.executor());
21917 fs.insert_tree(
21918 path!("/test"),
21919 json!({
21920 ".git": {},
21921 "file-1": "ONE\n",
21922 "file-2": "TWO\n",
21923 "file-3": "THREE\n",
21924 }),
21925 )
21926 .await;
21927
21928 fs.set_head_for_repo(
21929 path!("/test/.git").as_ref(),
21930 &[
21931 ("file-1", "one\n".into()),
21932 ("file-2", "two\n".into()),
21933 ("file-3", "three\n".into()),
21934 ],
21935 "deadbeef",
21936 );
21937
21938 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21939 let mut buffers = vec![];
21940 for i in 1..=3 {
21941 let buffer = project
21942 .update(cx, |project, cx| {
21943 let path = format!(path!("/test/file-{}"), i);
21944 project.open_local_buffer(path, cx)
21945 })
21946 .await
21947 .unwrap();
21948 buffers.push(buffer);
21949 }
21950
21951 let multibuffer = cx.new(|cx| {
21952 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21953 multibuffer.set_all_diff_hunks_expanded(cx);
21954 for buffer in &buffers {
21955 let snapshot = buffer.read(cx).snapshot();
21956 multibuffer.set_excerpts_for_path(
21957 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21958 buffer.clone(),
21959 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21960 2,
21961 cx,
21962 );
21963 }
21964 multibuffer
21965 });
21966
21967 let editor = cx.add_window(|window, cx| {
21968 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21969 });
21970 cx.run_until_parked();
21971
21972 let snapshot = editor
21973 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21974 .unwrap();
21975 let hunks = snapshot
21976 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21977 .map(|hunk| match hunk {
21978 DisplayDiffHunk::Unfolded {
21979 display_row_range, ..
21980 } => display_row_range,
21981 DisplayDiffHunk::Folded { .. } => unreachable!(),
21982 })
21983 .collect::<Vec<_>>();
21984 assert_eq!(
21985 hunks,
21986 [
21987 DisplayRow(2)..DisplayRow(4),
21988 DisplayRow(7)..DisplayRow(9),
21989 DisplayRow(12)..DisplayRow(14),
21990 ]
21991 );
21992}
21993
21994#[gpui::test]
21995async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21996 init_test(cx, |_| {});
21997
21998 let mut cx = EditorTestContext::new(cx).await;
21999 cx.set_head_text(indoc! { "
22000 one
22001 two
22002 three
22003 four
22004 five
22005 "
22006 });
22007 cx.set_index_text(indoc! { "
22008 one
22009 two
22010 three
22011 four
22012 five
22013 "
22014 });
22015 cx.set_state(indoc! {"
22016 one
22017 TWO
22018 ˇTHREE
22019 FOUR
22020 five
22021 "});
22022 cx.run_until_parked();
22023 cx.update_editor(|editor, window, cx| {
22024 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22025 });
22026 cx.run_until_parked();
22027 cx.assert_index_text(Some(indoc! {"
22028 one
22029 TWO
22030 THREE
22031 FOUR
22032 five
22033 "}));
22034 cx.set_state(indoc! { "
22035 one
22036 TWO
22037 ˇTHREE-HUNDRED
22038 FOUR
22039 five
22040 "});
22041 cx.run_until_parked();
22042 cx.update_editor(|editor, window, cx| {
22043 let snapshot = editor.snapshot(window, cx);
22044 let hunks = editor
22045 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22046 .collect::<Vec<_>>();
22047 assert_eq!(hunks.len(), 1);
22048 assert_eq!(
22049 hunks[0].status(),
22050 DiffHunkStatus {
22051 kind: DiffHunkStatusKind::Modified,
22052 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22053 }
22054 );
22055
22056 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22057 });
22058 cx.run_until_parked();
22059 cx.assert_index_text(Some(indoc! {"
22060 one
22061 TWO
22062 THREE-HUNDRED
22063 FOUR
22064 five
22065 "}));
22066}
22067
22068#[gpui::test]
22069fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22070 init_test(cx, |_| {});
22071
22072 let editor = cx.add_window(|window, cx| {
22073 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22074 build_editor(buffer, window, cx)
22075 });
22076
22077 let render_args = Arc::new(Mutex::new(None));
22078 let snapshot = editor
22079 .update(cx, |editor, window, cx| {
22080 let snapshot = editor.buffer().read(cx).snapshot(cx);
22081 let range =
22082 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22083
22084 struct RenderArgs {
22085 row: MultiBufferRow,
22086 folded: bool,
22087 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22088 }
22089
22090 let crease = Crease::inline(
22091 range,
22092 FoldPlaceholder::test(),
22093 {
22094 let toggle_callback = render_args.clone();
22095 move |row, folded, callback, _window, _cx| {
22096 *toggle_callback.lock() = Some(RenderArgs {
22097 row,
22098 folded,
22099 callback,
22100 });
22101 div()
22102 }
22103 },
22104 |_row, _folded, _window, _cx| div(),
22105 );
22106
22107 editor.insert_creases(Some(crease), cx);
22108 let snapshot = editor.snapshot(window, cx);
22109 let _div =
22110 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22111 snapshot
22112 })
22113 .unwrap();
22114
22115 let render_args = render_args.lock().take().unwrap();
22116 assert_eq!(render_args.row, MultiBufferRow(1));
22117 assert!(!render_args.folded);
22118 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22119
22120 cx.update_window(*editor, |_, window, cx| {
22121 (render_args.callback)(true, window, cx)
22122 })
22123 .unwrap();
22124 let snapshot = editor
22125 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22126 .unwrap();
22127 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22128
22129 cx.update_window(*editor, |_, window, cx| {
22130 (render_args.callback)(false, window, cx)
22131 })
22132 .unwrap();
22133 let snapshot = editor
22134 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22135 .unwrap();
22136 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22137}
22138
22139#[gpui::test]
22140async fn test_input_text(cx: &mut TestAppContext) {
22141 init_test(cx, |_| {});
22142 let mut cx = EditorTestContext::new(cx).await;
22143
22144 cx.set_state(
22145 &r#"ˇone
22146 two
22147
22148 three
22149 fourˇ
22150 five
22151
22152 siˇx"#
22153 .unindent(),
22154 );
22155
22156 cx.dispatch_action(HandleInput(String::new()));
22157 cx.assert_editor_state(
22158 &r#"ˇone
22159 two
22160
22161 three
22162 fourˇ
22163 five
22164
22165 siˇx"#
22166 .unindent(),
22167 );
22168
22169 cx.dispatch_action(HandleInput("AAAA".to_string()));
22170 cx.assert_editor_state(
22171 &r#"AAAAˇone
22172 two
22173
22174 three
22175 fourAAAAˇ
22176 five
22177
22178 siAAAAˇx"#
22179 .unindent(),
22180 );
22181}
22182
22183#[gpui::test]
22184async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22185 init_test(cx, |_| {});
22186
22187 let mut cx = EditorTestContext::new(cx).await;
22188 cx.set_state(
22189 r#"let foo = 1;
22190let foo = 2;
22191let foo = 3;
22192let fooˇ = 4;
22193let foo = 5;
22194let foo = 6;
22195let foo = 7;
22196let foo = 8;
22197let foo = 9;
22198let foo = 10;
22199let foo = 11;
22200let foo = 12;
22201let foo = 13;
22202let foo = 14;
22203let foo = 15;"#,
22204 );
22205
22206 cx.update_editor(|e, window, cx| {
22207 assert_eq!(
22208 e.next_scroll_position,
22209 NextScrollCursorCenterTopBottom::Center,
22210 "Default next scroll direction is center",
22211 );
22212
22213 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22214 assert_eq!(
22215 e.next_scroll_position,
22216 NextScrollCursorCenterTopBottom::Top,
22217 "After center, next scroll direction should be top",
22218 );
22219
22220 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22221 assert_eq!(
22222 e.next_scroll_position,
22223 NextScrollCursorCenterTopBottom::Bottom,
22224 "After top, next scroll direction should be bottom",
22225 );
22226
22227 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22228 assert_eq!(
22229 e.next_scroll_position,
22230 NextScrollCursorCenterTopBottom::Center,
22231 "After bottom, scrolling should start over",
22232 );
22233
22234 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22235 assert_eq!(
22236 e.next_scroll_position,
22237 NextScrollCursorCenterTopBottom::Top,
22238 "Scrolling continues if retriggered fast enough"
22239 );
22240 });
22241
22242 cx.executor()
22243 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22244 cx.executor().run_until_parked();
22245 cx.update_editor(|e, _, _| {
22246 assert_eq!(
22247 e.next_scroll_position,
22248 NextScrollCursorCenterTopBottom::Center,
22249 "If scrolling is not triggered fast enough, it should reset"
22250 );
22251 });
22252}
22253
22254#[gpui::test]
22255async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22256 init_test(cx, |_| {});
22257 let mut cx = EditorLspTestContext::new_rust(
22258 lsp::ServerCapabilities {
22259 definition_provider: Some(lsp::OneOf::Left(true)),
22260 references_provider: Some(lsp::OneOf::Left(true)),
22261 ..lsp::ServerCapabilities::default()
22262 },
22263 cx,
22264 )
22265 .await;
22266
22267 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22268 let go_to_definition = cx
22269 .lsp
22270 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22271 move |params, _| async move {
22272 if empty_go_to_definition {
22273 Ok(None)
22274 } else {
22275 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22276 uri: params.text_document_position_params.text_document.uri,
22277 range: lsp::Range::new(
22278 lsp::Position::new(4, 3),
22279 lsp::Position::new(4, 6),
22280 ),
22281 })))
22282 }
22283 },
22284 );
22285 let references = cx
22286 .lsp
22287 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22288 Ok(Some(vec![lsp::Location {
22289 uri: params.text_document_position.text_document.uri,
22290 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22291 }]))
22292 });
22293 (go_to_definition, references)
22294 };
22295
22296 cx.set_state(
22297 &r#"fn one() {
22298 let mut a = ˇtwo();
22299 }
22300
22301 fn two() {}"#
22302 .unindent(),
22303 );
22304 set_up_lsp_handlers(false, &mut cx);
22305 let navigated = cx
22306 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22307 .await
22308 .expect("Failed to navigate to definition");
22309 assert_eq!(
22310 navigated,
22311 Navigated::Yes,
22312 "Should have navigated to definition from the GetDefinition response"
22313 );
22314 cx.assert_editor_state(
22315 &r#"fn one() {
22316 let mut a = two();
22317 }
22318
22319 fn «twoˇ»() {}"#
22320 .unindent(),
22321 );
22322
22323 let editors = cx.update_workspace(|workspace, _, cx| {
22324 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22325 });
22326 cx.update_editor(|_, _, test_editor_cx| {
22327 assert_eq!(
22328 editors.len(),
22329 1,
22330 "Initially, only one, test, editor should be open in the workspace"
22331 );
22332 assert_eq!(
22333 test_editor_cx.entity(),
22334 editors.last().expect("Asserted len is 1").clone()
22335 );
22336 });
22337
22338 set_up_lsp_handlers(true, &mut cx);
22339 let navigated = cx
22340 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22341 .await
22342 .expect("Failed to navigate to lookup references");
22343 assert_eq!(
22344 navigated,
22345 Navigated::Yes,
22346 "Should have navigated to references as a fallback after empty GoToDefinition response"
22347 );
22348 // We should not change the selections in the existing file,
22349 // if opening another milti buffer with the references
22350 cx.assert_editor_state(
22351 &r#"fn one() {
22352 let mut a = two();
22353 }
22354
22355 fn «twoˇ»() {}"#
22356 .unindent(),
22357 );
22358 let editors = cx.update_workspace(|workspace, _, cx| {
22359 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22360 });
22361 cx.update_editor(|_, _, test_editor_cx| {
22362 assert_eq!(
22363 editors.len(),
22364 2,
22365 "After falling back to references search, we open a new editor with the results"
22366 );
22367 let references_fallback_text = editors
22368 .into_iter()
22369 .find(|new_editor| *new_editor != test_editor_cx.entity())
22370 .expect("Should have one non-test editor now")
22371 .read(test_editor_cx)
22372 .text(test_editor_cx);
22373 assert_eq!(
22374 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22375 "Should use the range from the references response and not the GoToDefinition one"
22376 );
22377 });
22378}
22379
22380#[gpui::test]
22381async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22382 init_test(cx, |_| {});
22383 cx.update(|cx| {
22384 let mut editor_settings = EditorSettings::get_global(cx).clone();
22385 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22386 EditorSettings::override_global(editor_settings, cx);
22387 });
22388 let mut cx = EditorLspTestContext::new_rust(
22389 lsp::ServerCapabilities {
22390 definition_provider: Some(lsp::OneOf::Left(true)),
22391 references_provider: Some(lsp::OneOf::Left(true)),
22392 ..lsp::ServerCapabilities::default()
22393 },
22394 cx,
22395 )
22396 .await;
22397 let original_state = r#"fn one() {
22398 let mut a = ˇtwo();
22399 }
22400
22401 fn two() {}"#
22402 .unindent();
22403 cx.set_state(&original_state);
22404
22405 let mut go_to_definition = cx
22406 .lsp
22407 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22408 move |_, _| async move { Ok(None) },
22409 );
22410 let _references = cx
22411 .lsp
22412 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22413 panic!("Should not call for references with no go to definition fallback")
22414 });
22415
22416 let navigated = cx
22417 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22418 .await
22419 .expect("Failed to navigate to lookup references");
22420 go_to_definition
22421 .next()
22422 .await
22423 .expect("Should have called the go_to_definition handler");
22424
22425 assert_eq!(
22426 navigated,
22427 Navigated::No,
22428 "Should have navigated to references as a fallback after empty GoToDefinition response"
22429 );
22430 cx.assert_editor_state(&original_state);
22431 let editors = cx.update_workspace(|workspace, _, cx| {
22432 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22433 });
22434 cx.update_editor(|_, _, _| {
22435 assert_eq!(
22436 editors.len(),
22437 1,
22438 "After unsuccessful fallback, no other editor should have been opened"
22439 );
22440 });
22441}
22442
22443#[gpui::test]
22444async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22445 init_test(cx, |_| {});
22446 let mut cx = EditorLspTestContext::new_rust(
22447 lsp::ServerCapabilities {
22448 references_provider: Some(lsp::OneOf::Left(true)),
22449 ..lsp::ServerCapabilities::default()
22450 },
22451 cx,
22452 )
22453 .await;
22454
22455 cx.set_state(
22456 &r#"
22457 fn one() {
22458 let mut a = two();
22459 }
22460
22461 fn ˇtwo() {}"#
22462 .unindent(),
22463 );
22464 cx.lsp
22465 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22466 Ok(Some(vec![
22467 lsp::Location {
22468 uri: params.text_document_position.text_document.uri.clone(),
22469 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22470 },
22471 lsp::Location {
22472 uri: params.text_document_position.text_document.uri,
22473 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22474 },
22475 ]))
22476 });
22477 let navigated = cx
22478 .update_editor(|editor, window, cx| {
22479 editor.find_all_references(&FindAllReferences, window, cx)
22480 })
22481 .unwrap()
22482 .await
22483 .expect("Failed to navigate to references");
22484 assert_eq!(
22485 navigated,
22486 Navigated::Yes,
22487 "Should have navigated to references from the FindAllReferences response"
22488 );
22489 cx.assert_editor_state(
22490 &r#"fn one() {
22491 let mut a = two();
22492 }
22493
22494 fn ˇtwo() {}"#
22495 .unindent(),
22496 );
22497
22498 let editors = cx.update_workspace(|workspace, _, cx| {
22499 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22500 });
22501 cx.update_editor(|_, _, _| {
22502 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22503 });
22504
22505 cx.set_state(
22506 &r#"fn one() {
22507 let mut a = ˇtwo();
22508 }
22509
22510 fn two() {}"#
22511 .unindent(),
22512 );
22513 let navigated = cx
22514 .update_editor(|editor, window, cx| {
22515 editor.find_all_references(&FindAllReferences, window, cx)
22516 })
22517 .unwrap()
22518 .await
22519 .expect("Failed to navigate to references");
22520 assert_eq!(
22521 navigated,
22522 Navigated::Yes,
22523 "Should have navigated to references from the FindAllReferences response"
22524 );
22525 cx.assert_editor_state(
22526 &r#"fn one() {
22527 let mut a = ˇtwo();
22528 }
22529
22530 fn two() {}"#
22531 .unindent(),
22532 );
22533 let editors = cx.update_workspace(|workspace, _, cx| {
22534 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22535 });
22536 cx.update_editor(|_, _, _| {
22537 assert_eq!(
22538 editors.len(),
22539 2,
22540 "should have re-used the previous multibuffer"
22541 );
22542 });
22543
22544 cx.set_state(
22545 &r#"fn one() {
22546 let mut a = ˇtwo();
22547 }
22548 fn three() {}
22549 fn two() {}"#
22550 .unindent(),
22551 );
22552 cx.lsp
22553 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22554 Ok(Some(vec![
22555 lsp::Location {
22556 uri: params.text_document_position.text_document.uri.clone(),
22557 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22558 },
22559 lsp::Location {
22560 uri: params.text_document_position.text_document.uri,
22561 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22562 },
22563 ]))
22564 });
22565 let navigated = cx
22566 .update_editor(|editor, window, cx| {
22567 editor.find_all_references(&FindAllReferences, window, cx)
22568 })
22569 .unwrap()
22570 .await
22571 .expect("Failed to navigate to references");
22572 assert_eq!(
22573 navigated,
22574 Navigated::Yes,
22575 "Should have navigated to references from the FindAllReferences response"
22576 );
22577 cx.assert_editor_state(
22578 &r#"fn one() {
22579 let mut a = ˇtwo();
22580 }
22581 fn three() {}
22582 fn two() {}"#
22583 .unindent(),
22584 );
22585 let editors = cx.update_workspace(|workspace, _, cx| {
22586 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22587 });
22588 cx.update_editor(|_, _, _| {
22589 assert_eq!(
22590 editors.len(),
22591 3,
22592 "should have used a new multibuffer as offsets changed"
22593 );
22594 });
22595}
22596#[gpui::test]
22597async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22598 init_test(cx, |_| {});
22599
22600 let language = Arc::new(Language::new(
22601 LanguageConfig::default(),
22602 Some(tree_sitter_rust::LANGUAGE.into()),
22603 ));
22604
22605 let text = r#"
22606 #[cfg(test)]
22607 mod tests() {
22608 #[test]
22609 fn runnable_1() {
22610 let a = 1;
22611 }
22612
22613 #[test]
22614 fn runnable_2() {
22615 let a = 1;
22616 let b = 2;
22617 }
22618 }
22619 "#
22620 .unindent();
22621
22622 let fs = FakeFs::new(cx.executor());
22623 fs.insert_file("/file.rs", Default::default()).await;
22624
22625 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22626 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22627 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22628 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22629 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22630
22631 let editor = cx.new_window_entity(|window, cx| {
22632 Editor::new(
22633 EditorMode::full(),
22634 multi_buffer,
22635 Some(project.clone()),
22636 window,
22637 cx,
22638 )
22639 });
22640
22641 editor.update_in(cx, |editor, window, cx| {
22642 let snapshot = editor.buffer().read(cx).snapshot(cx);
22643 editor.tasks.insert(
22644 (buffer.read(cx).remote_id(), 3),
22645 RunnableTasks {
22646 templates: vec![],
22647 offset: snapshot.anchor_before(MultiBufferOffset(43)),
22648 column: 0,
22649 extra_variables: HashMap::default(),
22650 context_range: BufferOffset(43)..BufferOffset(85),
22651 },
22652 );
22653 editor.tasks.insert(
22654 (buffer.read(cx).remote_id(), 8),
22655 RunnableTasks {
22656 templates: vec![],
22657 offset: snapshot.anchor_before(MultiBufferOffset(86)),
22658 column: 0,
22659 extra_variables: HashMap::default(),
22660 context_range: BufferOffset(86)..BufferOffset(191),
22661 },
22662 );
22663
22664 // Test finding task when cursor is inside function body
22665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22666 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22667 });
22668 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22669 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22670
22671 // Test finding task when cursor is on function name
22672 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22673 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22674 });
22675 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22676 assert_eq!(row, 8, "Should find task when cursor is on function name");
22677 });
22678}
22679
22680#[gpui::test]
22681async fn test_folding_buffers(cx: &mut TestAppContext) {
22682 init_test(cx, |_| {});
22683
22684 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22685 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22686 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22687
22688 let fs = FakeFs::new(cx.executor());
22689 fs.insert_tree(
22690 path!("/a"),
22691 json!({
22692 "first.rs": sample_text_1,
22693 "second.rs": sample_text_2,
22694 "third.rs": sample_text_3,
22695 }),
22696 )
22697 .await;
22698 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22699 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22700 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22701 let worktree = project.update(cx, |project, cx| {
22702 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22703 assert_eq!(worktrees.len(), 1);
22704 worktrees.pop().unwrap()
22705 });
22706 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22707
22708 let buffer_1 = project
22709 .update(cx, |project, cx| {
22710 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22711 })
22712 .await
22713 .unwrap();
22714 let buffer_2 = project
22715 .update(cx, |project, cx| {
22716 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22717 })
22718 .await
22719 .unwrap();
22720 let buffer_3 = project
22721 .update(cx, |project, cx| {
22722 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22723 })
22724 .await
22725 .unwrap();
22726
22727 let multi_buffer = cx.new(|cx| {
22728 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22729 multi_buffer.push_excerpts(
22730 buffer_1.clone(),
22731 [
22732 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22733 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22734 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22735 ],
22736 cx,
22737 );
22738 multi_buffer.push_excerpts(
22739 buffer_2.clone(),
22740 [
22741 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22742 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22743 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22744 ],
22745 cx,
22746 );
22747 multi_buffer.push_excerpts(
22748 buffer_3.clone(),
22749 [
22750 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22751 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22752 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22753 ],
22754 cx,
22755 );
22756 multi_buffer
22757 });
22758 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22759 Editor::new(
22760 EditorMode::full(),
22761 multi_buffer.clone(),
22762 Some(project.clone()),
22763 window,
22764 cx,
22765 )
22766 });
22767
22768 assert_eq!(
22769 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22770 "\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",
22771 );
22772
22773 multi_buffer_editor.update(cx, |editor, cx| {
22774 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22775 });
22776 assert_eq!(
22777 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22778 "\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",
22779 "After folding the first buffer, its text should not be displayed"
22780 );
22781
22782 multi_buffer_editor.update(cx, |editor, cx| {
22783 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22784 });
22785 assert_eq!(
22786 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22787 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22788 "After folding the second buffer, its text should not be displayed"
22789 );
22790
22791 multi_buffer_editor.update(cx, |editor, cx| {
22792 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22793 });
22794 assert_eq!(
22795 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22796 "\n\n\n\n\n",
22797 "After folding the third buffer, its text should not be displayed"
22798 );
22799
22800 // Emulate selection inside the fold logic, that should work
22801 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22802 editor
22803 .snapshot(window, cx)
22804 .next_line_boundary(Point::new(0, 4));
22805 });
22806
22807 multi_buffer_editor.update(cx, |editor, cx| {
22808 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22809 });
22810 assert_eq!(
22811 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22812 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22813 "After unfolding the second buffer, its text should be displayed"
22814 );
22815
22816 // Typing inside of buffer 1 causes that buffer to be unfolded.
22817 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22818 assert_eq!(
22819 multi_buffer
22820 .read(cx)
22821 .snapshot(cx)
22822 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22823 .collect::<String>(),
22824 "bbbb"
22825 );
22826 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22827 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22828 });
22829 editor.handle_input("B", window, cx);
22830 });
22831
22832 assert_eq!(
22833 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22834 "\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",
22835 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22836 );
22837
22838 multi_buffer_editor.update(cx, |editor, cx| {
22839 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22840 });
22841 assert_eq!(
22842 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22843 "\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",
22844 "After unfolding the all buffers, all original text should be displayed"
22845 );
22846}
22847
22848#[gpui::test]
22849async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22850 init_test(cx, |_| {});
22851
22852 let sample_text_1 = "1111\n2222\n3333".to_string();
22853 let sample_text_2 = "4444\n5555\n6666".to_string();
22854 let sample_text_3 = "7777\n8888\n9999".to_string();
22855
22856 let fs = FakeFs::new(cx.executor());
22857 fs.insert_tree(
22858 path!("/a"),
22859 json!({
22860 "first.rs": sample_text_1,
22861 "second.rs": sample_text_2,
22862 "third.rs": sample_text_3,
22863 }),
22864 )
22865 .await;
22866 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22867 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22868 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22869 let worktree = project.update(cx, |project, cx| {
22870 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22871 assert_eq!(worktrees.len(), 1);
22872 worktrees.pop().unwrap()
22873 });
22874 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22875
22876 let buffer_1 = project
22877 .update(cx, |project, cx| {
22878 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22879 })
22880 .await
22881 .unwrap();
22882 let buffer_2 = project
22883 .update(cx, |project, cx| {
22884 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22885 })
22886 .await
22887 .unwrap();
22888 let buffer_3 = project
22889 .update(cx, |project, cx| {
22890 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22891 })
22892 .await
22893 .unwrap();
22894
22895 let multi_buffer = cx.new(|cx| {
22896 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22897 multi_buffer.push_excerpts(
22898 buffer_1.clone(),
22899 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22900 cx,
22901 );
22902 multi_buffer.push_excerpts(
22903 buffer_2.clone(),
22904 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22905 cx,
22906 );
22907 multi_buffer.push_excerpts(
22908 buffer_3.clone(),
22909 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22910 cx,
22911 );
22912 multi_buffer
22913 });
22914
22915 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22916 Editor::new(
22917 EditorMode::full(),
22918 multi_buffer,
22919 Some(project.clone()),
22920 window,
22921 cx,
22922 )
22923 });
22924
22925 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22926 assert_eq!(
22927 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22928 full_text,
22929 );
22930
22931 multi_buffer_editor.update(cx, |editor, cx| {
22932 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22933 });
22934 assert_eq!(
22935 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22936 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22937 "After folding the first buffer, its text should not be displayed"
22938 );
22939
22940 multi_buffer_editor.update(cx, |editor, cx| {
22941 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22942 });
22943
22944 assert_eq!(
22945 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22946 "\n\n\n\n\n\n7777\n8888\n9999",
22947 "After folding the second buffer, its text should not be displayed"
22948 );
22949
22950 multi_buffer_editor.update(cx, |editor, cx| {
22951 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22952 });
22953 assert_eq!(
22954 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22955 "\n\n\n\n\n",
22956 "After folding the third buffer, its text should not be displayed"
22957 );
22958
22959 multi_buffer_editor.update(cx, |editor, cx| {
22960 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22961 });
22962 assert_eq!(
22963 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22964 "\n\n\n\n4444\n5555\n6666\n\n",
22965 "After unfolding the second buffer, its text should be displayed"
22966 );
22967
22968 multi_buffer_editor.update(cx, |editor, cx| {
22969 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22970 });
22971 assert_eq!(
22972 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22973 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22974 "After unfolding the first buffer, its text should be displayed"
22975 );
22976
22977 multi_buffer_editor.update(cx, |editor, cx| {
22978 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22979 });
22980 assert_eq!(
22981 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22982 full_text,
22983 "After unfolding all buffers, all original text should be displayed"
22984 );
22985}
22986
22987#[gpui::test]
22988async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22989 init_test(cx, |_| {});
22990
22991 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22992
22993 let fs = FakeFs::new(cx.executor());
22994 fs.insert_tree(
22995 path!("/a"),
22996 json!({
22997 "main.rs": sample_text,
22998 }),
22999 )
23000 .await;
23001 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23002 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23003 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23004 let worktree = project.update(cx, |project, cx| {
23005 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23006 assert_eq!(worktrees.len(), 1);
23007 worktrees.pop().unwrap()
23008 });
23009 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23010
23011 let buffer_1 = project
23012 .update(cx, |project, cx| {
23013 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23014 })
23015 .await
23016 .unwrap();
23017
23018 let multi_buffer = cx.new(|cx| {
23019 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23020 multi_buffer.push_excerpts(
23021 buffer_1.clone(),
23022 [ExcerptRange::new(
23023 Point::new(0, 0)
23024 ..Point::new(
23025 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23026 0,
23027 ),
23028 )],
23029 cx,
23030 );
23031 multi_buffer
23032 });
23033 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23034 Editor::new(
23035 EditorMode::full(),
23036 multi_buffer,
23037 Some(project.clone()),
23038 window,
23039 cx,
23040 )
23041 });
23042
23043 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23044 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23045 enum TestHighlight {}
23046 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23047 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23048 editor.highlight_text::<TestHighlight>(
23049 vec![highlight_range.clone()],
23050 HighlightStyle::color(Hsla::green()),
23051 cx,
23052 );
23053 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23054 s.select_ranges(Some(highlight_range))
23055 });
23056 });
23057
23058 let full_text = format!("\n\n{sample_text}");
23059 assert_eq!(
23060 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23061 full_text,
23062 );
23063}
23064
23065#[gpui::test]
23066async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23067 init_test(cx, |_| {});
23068 cx.update(|cx| {
23069 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23070 "keymaps/default-linux.json",
23071 cx,
23072 )
23073 .unwrap();
23074 cx.bind_keys(default_key_bindings);
23075 });
23076
23077 let (editor, cx) = cx.add_window_view(|window, cx| {
23078 let multi_buffer = MultiBuffer::build_multi(
23079 [
23080 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23081 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23082 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23083 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23084 ],
23085 cx,
23086 );
23087 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23088
23089 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23090 // fold all but the second buffer, so that we test navigating between two
23091 // adjacent folded buffers, as well as folded buffers at the start and
23092 // end the multibuffer
23093 editor.fold_buffer(buffer_ids[0], cx);
23094 editor.fold_buffer(buffer_ids[2], cx);
23095 editor.fold_buffer(buffer_ids[3], cx);
23096
23097 editor
23098 });
23099 cx.simulate_resize(size(px(1000.), px(1000.)));
23100
23101 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23102 cx.assert_excerpts_with_selections(indoc! {"
23103 [EXCERPT]
23104 ˇ[FOLDED]
23105 [EXCERPT]
23106 a1
23107 b1
23108 [EXCERPT]
23109 [FOLDED]
23110 [EXCERPT]
23111 [FOLDED]
23112 "
23113 });
23114 cx.simulate_keystroke("down");
23115 cx.assert_excerpts_with_selections(indoc! {"
23116 [EXCERPT]
23117 [FOLDED]
23118 [EXCERPT]
23119 ˇa1
23120 b1
23121 [EXCERPT]
23122 [FOLDED]
23123 [EXCERPT]
23124 [FOLDED]
23125 "
23126 });
23127 cx.simulate_keystroke("down");
23128 cx.assert_excerpts_with_selections(indoc! {"
23129 [EXCERPT]
23130 [FOLDED]
23131 [EXCERPT]
23132 a1
23133 ˇb1
23134 [EXCERPT]
23135 [FOLDED]
23136 [EXCERPT]
23137 [FOLDED]
23138 "
23139 });
23140 cx.simulate_keystroke("down");
23141 cx.assert_excerpts_with_selections(indoc! {"
23142 [EXCERPT]
23143 [FOLDED]
23144 [EXCERPT]
23145 a1
23146 b1
23147 ˇ[EXCERPT]
23148 [FOLDED]
23149 [EXCERPT]
23150 [FOLDED]
23151 "
23152 });
23153 cx.simulate_keystroke("down");
23154 cx.assert_excerpts_with_selections(indoc! {"
23155 [EXCERPT]
23156 [FOLDED]
23157 [EXCERPT]
23158 a1
23159 b1
23160 [EXCERPT]
23161 ˇ[FOLDED]
23162 [EXCERPT]
23163 [FOLDED]
23164 "
23165 });
23166 for _ in 0..5 {
23167 cx.simulate_keystroke("down");
23168 cx.assert_excerpts_with_selections(indoc! {"
23169 [EXCERPT]
23170 [FOLDED]
23171 [EXCERPT]
23172 a1
23173 b1
23174 [EXCERPT]
23175 [FOLDED]
23176 [EXCERPT]
23177 ˇ[FOLDED]
23178 "
23179 });
23180 }
23181
23182 cx.simulate_keystroke("up");
23183 cx.assert_excerpts_with_selections(indoc! {"
23184 [EXCERPT]
23185 [FOLDED]
23186 [EXCERPT]
23187 a1
23188 b1
23189 [EXCERPT]
23190 ˇ[FOLDED]
23191 [EXCERPT]
23192 [FOLDED]
23193 "
23194 });
23195 cx.simulate_keystroke("up");
23196 cx.assert_excerpts_with_selections(indoc! {"
23197 [EXCERPT]
23198 [FOLDED]
23199 [EXCERPT]
23200 a1
23201 b1
23202 ˇ[EXCERPT]
23203 [FOLDED]
23204 [EXCERPT]
23205 [FOLDED]
23206 "
23207 });
23208 cx.simulate_keystroke("up");
23209 cx.assert_excerpts_with_selections(indoc! {"
23210 [EXCERPT]
23211 [FOLDED]
23212 [EXCERPT]
23213 a1
23214 ˇb1
23215 [EXCERPT]
23216 [FOLDED]
23217 [EXCERPT]
23218 [FOLDED]
23219 "
23220 });
23221 cx.simulate_keystroke("up");
23222 cx.assert_excerpts_with_selections(indoc! {"
23223 [EXCERPT]
23224 [FOLDED]
23225 [EXCERPT]
23226 ˇa1
23227 b1
23228 [EXCERPT]
23229 [FOLDED]
23230 [EXCERPT]
23231 [FOLDED]
23232 "
23233 });
23234 for _ in 0..5 {
23235 cx.simulate_keystroke("up");
23236 cx.assert_excerpts_with_selections(indoc! {"
23237 [EXCERPT]
23238 ˇ[FOLDED]
23239 [EXCERPT]
23240 a1
23241 b1
23242 [EXCERPT]
23243 [FOLDED]
23244 [EXCERPT]
23245 [FOLDED]
23246 "
23247 });
23248 }
23249}
23250
23251#[gpui::test]
23252async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23253 init_test(cx, |_| {});
23254
23255 // Simple insertion
23256 assert_highlighted_edits(
23257 "Hello, world!",
23258 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23259 true,
23260 cx,
23261 |highlighted_edits, cx| {
23262 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23263 assert_eq!(highlighted_edits.highlights.len(), 1);
23264 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23265 assert_eq!(
23266 highlighted_edits.highlights[0].1.background_color,
23267 Some(cx.theme().status().created_background)
23268 );
23269 },
23270 )
23271 .await;
23272
23273 // Replacement
23274 assert_highlighted_edits(
23275 "This is a test.",
23276 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23277 false,
23278 cx,
23279 |highlighted_edits, cx| {
23280 assert_eq!(highlighted_edits.text, "That is a test.");
23281 assert_eq!(highlighted_edits.highlights.len(), 1);
23282 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23283 assert_eq!(
23284 highlighted_edits.highlights[0].1.background_color,
23285 Some(cx.theme().status().created_background)
23286 );
23287 },
23288 )
23289 .await;
23290
23291 // Multiple edits
23292 assert_highlighted_edits(
23293 "Hello, world!",
23294 vec![
23295 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23296 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23297 ],
23298 false,
23299 cx,
23300 |highlighted_edits, cx| {
23301 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23302 assert_eq!(highlighted_edits.highlights.len(), 2);
23303 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23304 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23305 assert_eq!(
23306 highlighted_edits.highlights[0].1.background_color,
23307 Some(cx.theme().status().created_background)
23308 );
23309 assert_eq!(
23310 highlighted_edits.highlights[1].1.background_color,
23311 Some(cx.theme().status().created_background)
23312 );
23313 },
23314 )
23315 .await;
23316
23317 // Multiple lines with edits
23318 assert_highlighted_edits(
23319 "First line\nSecond line\nThird line\nFourth line",
23320 vec![
23321 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23322 (
23323 Point::new(2, 0)..Point::new(2, 10),
23324 "New third line".to_string(),
23325 ),
23326 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23327 ],
23328 false,
23329 cx,
23330 |highlighted_edits, cx| {
23331 assert_eq!(
23332 highlighted_edits.text,
23333 "Second modified\nNew third line\nFourth updated line"
23334 );
23335 assert_eq!(highlighted_edits.highlights.len(), 3);
23336 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23337 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23338 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23339 for highlight in &highlighted_edits.highlights {
23340 assert_eq!(
23341 highlight.1.background_color,
23342 Some(cx.theme().status().created_background)
23343 );
23344 }
23345 },
23346 )
23347 .await;
23348}
23349
23350#[gpui::test]
23351async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23352 init_test(cx, |_| {});
23353
23354 // Deletion
23355 assert_highlighted_edits(
23356 "Hello, world!",
23357 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23358 true,
23359 cx,
23360 |highlighted_edits, cx| {
23361 assert_eq!(highlighted_edits.text, "Hello, world!");
23362 assert_eq!(highlighted_edits.highlights.len(), 1);
23363 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23364 assert_eq!(
23365 highlighted_edits.highlights[0].1.background_color,
23366 Some(cx.theme().status().deleted_background)
23367 );
23368 },
23369 )
23370 .await;
23371
23372 // Insertion
23373 assert_highlighted_edits(
23374 "Hello, world!",
23375 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23376 true,
23377 cx,
23378 |highlighted_edits, cx| {
23379 assert_eq!(highlighted_edits.highlights.len(), 1);
23380 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23381 assert_eq!(
23382 highlighted_edits.highlights[0].1.background_color,
23383 Some(cx.theme().status().created_background)
23384 );
23385 },
23386 )
23387 .await;
23388}
23389
23390async fn assert_highlighted_edits(
23391 text: &str,
23392 edits: Vec<(Range<Point>, String)>,
23393 include_deletions: bool,
23394 cx: &mut TestAppContext,
23395 assertion_fn: impl Fn(HighlightedText, &App),
23396) {
23397 let window = cx.add_window(|window, cx| {
23398 let buffer = MultiBuffer::build_simple(text, cx);
23399 Editor::new(EditorMode::full(), buffer, None, window, cx)
23400 });
23401 let cx = &mut VisualTestContext::from_window(*window, cx);
23402
23403 let (buffer, snapshot) = window
23404 .update(cx, |editor, _window, cx| {
23405 (
23406 editor.buffer().clone(),
23407 editor.buffer().read(cx).snapshot(cx),
23408 )
23409 })
23410 .unwrap();
23411
23412 let edits = edits
23413 .into_iter()
23414 .map(|(range, edit)| {
23415 (
23416 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23417 edit,
23418 )
23419 })
23420 .collect::<Vec<_>>();
23421
23422 let text_anchor_edits = edits
23423 .clone()
23424 .into_iter()
23425 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23426 .collect::<Vec<_>>();
23427
23428 let edit_preview = window
23429 .update(cx, |_, _window, cx| {
23430 buffer
23431 .read(cx)
23432 .as_singleton()
23433 .unwrap()
23434 .read(cx)
23435 .preview_edits(text_anchor_edits.into(), cx)
23436 })
23437 .unwrap()
23438 .await;
23439
23440 cx.update(|_window, cx| {
23441 let highlighted_edits = edit_prediction_edit_text(
23442 snapshot.as_singleton().unwrap().2,
23443 &edits,
23444 &edit_preview,
23445 include_deletions,
23446 cx,
23447 );
23448 assertion_fn(highlighted_edits, cx)
23449 });
23450}
23451
23452#[track_caller]
23453fn assert_breakpoint(
23454 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23455 path: &Arc<Path>,
23456 expected: Vec<(u32, Breakpoint)>,
23457) {
23458 if expected.is_empty() {
23459 assert!(!breakpoints.contains_key(path), "{}", path.display());
23460 } else {
23461 let mut breakpoint = breakpoints
23462 .get(path)
23463 .unwrap()
23464 .iter()
23465 .map(|breakpoint| {
23466 (
23467 breakpoint.row,
23468 Breakpoint {
23469 message: breakpoint.message.clone(),
23470 state: breakpoint.state,
23471 condition: breakpoint.condition.clone(),
23472 hit_condition: breakpoint.hit_condition.clone(),
23473 },
23474 )
23475 })
23476 .collect::<Vec<_>>();
23477
23478 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23479
23480 assert_eq!(expected, breakpoint);
23481 }
23482}
23483
23484fn add_log_breakpoint_at_cursor(
23485 editor: &mut Editor,
23486 log_message: &str,
23487 window: &mut Window,
23488 cx: &mut Context<Editor>,
23489) {
23490 let (anchor, bp) = editor
23491 .breakpoints_at_cursors(window, cx)
23492 .first()
23493 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23494 .unwrap_or_else(|| {
23495 let snapshot = editor.snapshot(window, cx);
23496 let cursor_position: Point =
23497 editor.selections.newest(&snapshot.display_snapshot).head();
23498
23499 let breakpoint_position = snapshot
23500 .buffer_snapshot()
23501 .anchor_before(Point::new(cursor_position.row, 0));
23502
23503 (breakpoint_position, Breakpoint::new_log(log_message))
23504 });
23505
23506 editor.edit_breakpoint_at_anchor(
23507 anchor,
23508 bp,
23509 BreakpointEditAction::EditLogMessage(log_message.into()),
23510 cx,
23511 );
23512}
23513
23514#[gpui::test]
23515async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23516 init_test(cx, |_| {});
23517
23518 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23519 let fs = FakeFs::new(cx.executor());
23520 fs.insert_tree(
23521 path!("/a"),
23522 json!({
23523 "main.rs": sample_text,
23524 }),
23525 )
23526 .await;
23527 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23528 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23529 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23530
23531 let fs = FakeFs::new(cx.executor());
23532 fs.insert_tree(
23533 path!("/a"),
23534 json!({
23535 "main.rs": sample_text,
23536 }),
23537 )
23538 .await;
23539 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23540 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23541 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23542 let worktree_id = workspace
23543 .update(cx, |workspace, _window, cx| {
23544 workspace.project().update(cx, |project, cx| {
23545 project.worktrees(cx).next().unwrap().read(cx).id()
23546 })
23547 })
23548 .unwrap();
23549
23550 let buffer = project
23551 .update(cx, |project, cx| {
23552 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23553 })
23554 .await
23555 .unwrap();
23556
23557 let (editor, cx) = cx.add_window_view(|window, cx| {
23558 Editor::new(
23559 EditorMode::full(),
23560 MultiBuffer::build_from_buffer(buffer, cx),
23561 Some(project.clone()),
23562 window,
23563 cx,
23564 )
23565 });
23566
23567 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23568 let abs_path = project.read_with(cx, |project, cx| {
23569 project
23570 .absolute_path(&project_path, cx)
23571 .map(Arc::from)
23572 .unwrap()
23573 });
23574
23575 // assert we can add breakpoint on the first line
23576 editor.update_in(cx, |editor, window, cx| {
23577 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23578 editor.move_to_end(&MoveToEnd, window, cx);
23579 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23580 });
23581
23582 let breakpoints = editor.update(cx, |editor, cx| {
23583 editor
23584 .breakpoint_store()
23585 .as_ref()
23586 .unwrap()
23587 .read(cx)
23588 .all_source_breakpoints(cx)
23589 });
23590
23591 assert_eq!(1, breakpoints.len());
23592 assert_breakpoint(
23593 &breakpoints,
23594 &abs_path,
23595 vec![
23596 (0, Breakpoint::new_standard()),
23597 (3, Breakpoint::new_standard()),
23598 ],
23599 );
23600
23601 editor.update_in(cx, |editor, window, cx| {
23602 editor.move_to_beginning(&MoveToBeginning, window, cx);
23603 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23604 });
23605
23606 let breakpoints = editor.update(cx, |editor, cx| {
23607 editor
23608 .breakpoint_store()
23609 .as_ref()
23610 .unwrap()
23611 .read(cx)
23612 .all_source_breakpoints(cx)
23613 });
23614
23615 assert_eq!(1, breakpoints.len());
23616 assert_breakpoint(
23617 &breakpoints,
23618 &abs_path,
23619 vec![(3, Breakpoint::new_standard())],
23620 );
23621
23622 editor.update_in(cx, |editor, window, cx| {
23623 editor.move_to_end(&MoveToEnd, window, cx);
23624 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23625 });
23626
23627 let breakpoints = editor.update(cx, |editor, cx| {
23628 editor
23629 .breakpoint_store()
23630 .as_ref()
23631 .unwrap()
23632 .read(cx)
23633 .all_source_breakpoints(cx)
23634 });
23635
23636 assert_eq!(0, breakpoints.len());
23637 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23638}
23639
23640#[gpui::test]
23641async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23642 init_test(cx, |_| {});
23643
23644 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23645
23646 let fs = FakeFs::new(cx.executor());
23647 fs.insert_tree(
23648 path!("/a"),
23649 json!({
23650 "main.rs": sample_text,
23651 }),
23652 )
23653 .await;
23654 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23655 let (workspace, cx) =
23656 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23657
23658 let worktree_id = workspace.update(cx, |workspace, cx| {
23659 workspace.project().update(cx, |project, cx| {
23660 project.worktrees(cx).next().unwrap().read(cx).id()
23661 })
23662 });
23663
23664 let buffer = project
23665 .update(cx, |project, cx| {
23666 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23667 })
23668 .await
23669 .unwrap();
23670
23671 let (editor, cx) = cx.add_window_view(|window, cx| {
23672 Editor::new(
23673 EditorMode::full(),
23674 MultiBuffer::build_from_buffer(buffer, cx),
23675 Some(project.clone()),
23676 window,
23677 cx,
23678 )
23679 });
23680
23681 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23682 let abs_path = project.read_with(cx, |project, cx| {
23683 project
23684 .absolute_path(&project_path, cx)
23685 .map(Arc::from)
23686 .unwrap()
23687 });
23688
23689 editor.update_in(cx, |editor, window, cx| {
23690 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23691 });
23692
23693 let breakpoints = editor.update(cx, |editor, cx| {
23694 editor
23695 .breakpoint_store()
23696 .as_ref()
23697 .unwrap()
23698 .read(cx)
23699 .all_source_breakpoints(cx)
23700 });
23701
23702 assert_breakpoint(
23703 &breakpoints,
23704 &abs_path,
23705 vec![(0, Breakpoint::new_log("hello world"))],
23706 );
23707
23708 // Removing a log message from a log breakpoint should remove it
23709 editor.update_in(cx, |editor, window, cx| {
23710 add_log_breakpoint_at_cursor(editor, "", window, cx);
23711 });
23712
23713 let breakpoints = editor.update(cx, |editor, cx| {
23714 editor
23715 .breakpoint_store()
23716 .as_ref()
23717 .unwrap()
23718 .read(cx)
23719 .all_source_breakpoints(cx)
23720 });
23721
23722 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23723
23724 editor.update_in(cx, |editor, window, cx| {
23725 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23726 editor.move_to_end(&MoveToEnd, window, cx);
23727 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23728 // Not adding a log message to a standard breakpoint shouldn't remove it
23729 add_log_breakpoint_at_cursor(editor, "", window, cx);
23730 });
23731
23732 let breakpoints = editor.update(cx, |editor, cx| {
23733 editor
23734 .breakpoint_store()
23735 .as_ref()
23736 .unwrap()
23737 .read(cx)
23738 .all_source_breakpoints(cx)
23739 });
23740
23741 assert_breakpoint(
23742 &breakpoints,
23743 &abs_path,
23744 vec![
23745 (0, Breakpoint::new_standard()),
23746 (3, Breakpoint::new_standard()),
23747 ],
23748 );
23749
23750 editor.update_in(cx, |editor, window, cx| {
23751 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23752 });
23753
23754 let breakpoints = editor.update(cx, |editor, cx| {
23755 editor
23756 .breakpoint_store()
23757 .as_ref()
23758 .unwrap()
23759 .read(cx)
23760 .all_source_breakpoints(cx)
23761 });
23762
23763 assert_breakpoint(
23764 &breakpoints,
23765 &abs_path,
23766 vec![
23767 (0, Breakpoint::new_standard()),
23768 (3, Breakpoint::new_log("hello world")),
23769 ],
23770 );
23771
23772 editor.update_in(cx, |editor, window, cx| {
23773 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23774 });
23775
23776 let breakpoints = editor.update(cx, |editor, cx| {
23777 editor
23778 .breakpoint_store()
23779 .as_ref()
23780 .unwrap()
23781 .read(cx)
23782 .all_source_breakpoints(cx)
23783 });
23784
23785 assert_breakpoint(
23786 &breakpoints,
23787 &abs_path,
23788 vec![
23789 (0, Breakpoint::new_standard()),
23790 (3, Breakpoint::new_log("hello Earth!!")),
23791 ],
23792 );
23793}
23794
23795/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23796/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23797/// or when breakpoints were placed out of order. This tests for a regression too
23798#[gpui::test]
23799async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23800 init_test(cx, |_| {});
23801
23802 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23803 let fs = FakeFs::new(cx.executor());
23804 fs.insert_tree(
23805 path!("/a"),
23806 json!({
23807 "main.rs": sample_text,
23808 }),
23809 )
23810 .await;
23811 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23812 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23813 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23814
23815 let fs = FakeFs::new(cx.executor());
23816 fs.insert_tree(
23817 path!("/a"),
23818 json!({
23819 "main.rs": sample_text,
23820 }),
23821 )
23822 .await;
23823 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23824 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23825 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23826 let worktree_id = workspace
23827 .update(cx, |workspace, _window, cx| {
23828 workspace.project().update(cx, |project, cx| {
23829 project.worktrees(cx).next().unwrap().read(cx).id()
23830 })
23831 })
23832 .unwrap();
23833
23834 let buffer = project
23835 .update(cx, |project, cx| {
23836 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23837 })
23838 .await
23839 .unwrap();
23840
23841 let (editor, cx) = cx.add_window_view(|window, cx| {
23842 Editor::new(
23843 EditorMode::full(),
23844 MultiBuffer::build_from_buffer(buffer, cx),
23845 Some(project.clone()),
23846 window,
23847 cx,
23848 )
23849 });
23850
23851 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23852 let abs_path = project.read_with(cx, |project, cx| {
23853 project
23854 .absolute_path(&project_path, cx)
23855 .map(Arc::from)
23856 .unwrap()
23857 });
23858
23859 // assert we can add breakpoint on the first line
23860 editor.update_in(cx, |editor, window, cx| {
23861 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23862 editor.move_to_end(&MoveToEnd, window, cx);
23863 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23864 editor.move_up(&MoveUp, window, cx);
23865 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23866 });
23867
23868 let breakpoints = editor.update(cx, |editor, cx| {
23869 editor
23870 .breakpoint_store()
23871 .as_ref()
23872 .unwrap()
23873 .read(cx)
23874 .all_source_breakpoints(cx)
23875 });
23876
23877 assert_eq!(1, breakpoints.len());
23878 assert_breakpoint(
23879 &breakpoints,
23880 &abs_path,
23881 vec![
23882 (0, Breakpoint::new_standard()),
23883 (2, Breakpoint::new_standard()),
23884 (3, Breakpoint::new_standard()),
23885 ],
23886 );
23887
23888 editor.update_in(cx, |editor, window, cx| {
23889 editor.move_to_beginning(&MoveToBeginning, window, cx);
23890 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23891 editor.move_to_end(&MoveToEnd, window, cx);
23892 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23893 // Disabling a breakpoint that doesn't exist should do nothing
23894 editor.move_up(&MoveUp, window, cx);
23895 editor.move_up(&MoveUp, window, cx);
23896 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23897 });
23898
23899 let breakpoints = editor.update(cx, |editor, cx| {
23900 editor
23901 .breakpoint_store()
23902 .as_ref()
23903 .unwrap()
23904 .read(cx)
23905 .all_source_breakpoints(cx)
23906 });
23907
23908 let disable_breakpoint = {
23909 let mut bp = Breakpoint::new_standard();
23910 bp.state = BreakpointState::Disabled;
23911 bp
23912 };
23913
23914 assert_eq!(1, breakpoints.len());
23915 assert_breakpoint(
23916 &breakpoints,
23917 &abs_path,
23918 vec![
23919 (0, disable_breakpoint.clone()),
23920 (2, Breakpoint::new_standard()),
23921 (3, disable_breakpoint.clone()),
23922 ],
23923 );
23924
23925 editor.update_in(cx, |editor, window, cx| {
23926 editor.move_to_beginning(&MoveToBeginning, window, cx);
23927 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23928 editor.move_to_end(&MoveToEnd, window, cx);
23929 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23930 editor.move_up(&MoveUp, window, cx);
23931 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23932 });
23933
23934 let breakpoints = editor.update(cx, |editor, cx| {
23935 editor
23936 .breakpoint_store()
23937 .as_ref()
23938 .unwrap()
23939 .read(cx)
23940 .all_source_breakpoints(cx)
23941 });
23942
23943 assert_eq!(1, breakpoints.len());
23944 assert_breakpoint(
23945 &breakpoints,
23946 &abs_path,
23947 vec![
23948 (0, Breakpoint::new_standard()),
23949 (2, disable_breakpoint),
23950 (3, Breakpoint::new_standard()),
23951 ],
23952 );
23953}
23954
23955#[gpui::test]
23956async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23957 init_test(cx, |_| {});
23958 let capabilities = lsp::ServerCapabilities {
23959 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23960 prepare_provider: Some(true),
23961 work_done_progress_options: Default::default(),
23962 })),
23963 ..Default::default()
23964 };
23965 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23966
23967 cx.set_state(indoc! {"
23968 struct Fˇoo {}
23969 "});
23970
23971 cx.update_editor(|editor, _, cx| {
23972 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23973 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23974 editor.highlight_background::<DocumentHighlightRead>(
23975 &[highlight_range],
23976 |theme| theme.colors().editor_document_highlight_read_background,
23977 cx,
23978 );
23979 });
23980
23981 let mut prepare_rename_handler = cx
23982 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23983 move |_, _, _| async move {
23984 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23985 start: lsp::Position {
23986 line: 0,
23987 character: 7,
23988 },
23989 end: lsp::Position {
23990 line: 0,
23991 character: 10,
23992 },
23993 })))
23994 },
23995 );
23996 let prepare_rename_task = cx
23997 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23998 .expect("Prepare rename was not started");
23999 prepare_rename_handler.next().await.unwrap();
24000 prepare_rename_task.await.expect("Prepare rename failed");
24001
24002 let mut rename_handler =
24003 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24004 let edit = lsp::TextEdit {
24005 range: lsp::Range {
24006 start: lsp::Position {
24007 line: 0,
24008 character: 7,
24009 },
24010 end: lsp::Position {
24011 line: 0,
24012 character: 10,
24013 },
24014 },
24015 new_text: "FooRenamed".to_string(),
24016 };
24017 Ok(Some(lsp::WorkspaceEdit::new(
24018 // Specify the same edit twice
24019 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24020 )))
24021 });
24022 let rename_task = cx
24023 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24024 .expect("Confirm rename was not started");
24025 rename_handler.next().await.unwrap();
24026 rename_task.await.expect("Confirm rename failed");
24027 cx.run_until_parked();
24028
24029 // Despite two edits, only one is actually applied as those are identical
24030 cx.assert_editor_state(indoc! {"
24031 struct FooRenamedˇ {}
24032 "});
24033}
24034
24035#[gpui::test]
24036async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24037 init_test(cx, |_| {});
24038 // These capabilities indicate that the server does not support prepare rename.
24039 let capabilities = lsp::ServerCapabilities {
24040 rename_provider: Some(lsp::OneOf::Left(true)),
24041 ..Default::default()
24042 };
24043 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24044
24045 cx.set_state(indoc! {"
24046 struct Fˇoo {}
24047 "});
24048
24049 cx.update_editor(|editor, _window, cx| {
24050 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24051 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24052 editor.highlight_background::<DocumentHighlightRead>(
24053 &[highlight_range],
24054 |theme| theme.colors().editor_document_highlight_read_background,
24055 cx,
24056 );
24057 });
24058
24059 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24060 .expect("Prepare rename was not started")
24061 .await
24062 .expect("Prepare rename failed");
24063
24064 let mut rename_handler =
24065 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24066 let edit = lsp::TextEdit {
24067 range: lsp::Range {
24068 start: lsp::Position {
24069 line: 0,
24070 character: 7,
24071 },
24072 end: lsp::Position {
24073 line: 0,
24074 character: 10,
24075 },
24076 },
24077 new_text: "FooRenamed".to_string(),
24078 };
24079 Ok(Some(lsp::WorkspaceEdit::new(
24080 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24081 )))
24082 });
24083 let rename_task = cx
24084 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24085 .expect("Confirm rename was not started");
24086 rename_handler.next().await.unwrap();
24087 rename_task.await.expect("Confirm rename failed");
24088 cx.run_until_parked();
24089
24090 // Correct range is renamed, as `surrounding_word` is used to find it.
24091 cx.assert_editor_state(indoc! {"
24092 struct FooRenamedˇ {}
24093 "});
24094}
24095
24096#[gpui::test]
24097async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24098 init_test(cx, |_| {});
24099 let mut cx = EditorTestContext::new(cx).await;
24100
24101 let language = Arc::new(
24102 Language::new(
24103 LanguageConfig::default(),
24104 Some(tree_sitter_html::LANGUAGE.into()),
24105 )
24106 .with_brackets_query(
24107 r#"
24108 ("<" @open "/>" @close)
24109 ("</" @open ">" @close)
24110 ("<" @open ">" @close)
24111 ("\"" @open "\"" @close)
24112 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24113 "#,
24114 )
24115 .unwrap(),
24116 );
24117 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24118
24119 cx.set_state(indoc! {"
24120 <span>ˇ</span>
24121 "});
24122 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24123 cx.assert_editor_state(indoc! {"
24124 <span>
24125 ˇ
24126 </span>
24127 "});
24128
24129 cx.set_state(indoc! {"
24130 <span><span></span>ˇ</span>
24131 "});
24132 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24133 cx.assert_editor_state(indoc! {"
24134 <span><span></span>
24135 ˇ</span>
24136 "});
24137
24138 cx.set_state(indoc! {"
24139 <span>ˇ
24140 </span>
24141 "});
24142 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24143 cx.assert_editor_state(indoc! {"
24144 <span>
24145 ˇ
24146 </span>
24147 "});
24148}
24149
24150#[gpui::test(iterations = 10)]
24151async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24152 init_test(cx, |_| {});
24153
24154 let fs = FakeFs::new(cx.executor());
24155 fs.insert_tree(
24156 path!("/dir"),
24157 json!({
24158 "a.ts": "a",
24159 }),
24160 )
24161 .await;
24162
24163 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24164 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24165 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24166
24167 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24168 language_registry.add(Arc::new(Language::new(
24169 LanguageConfig {
24170 name: "TypeScript".into(),
24171 matcher: LanguageMatcher {
24172 path_suffixes: vec!["ts".to_string()],
24173 ..Default::default()
24174 },
24175 ..Default::default()
24176 },
24177 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24178 )));
24179 let mut fake_language_servers = language_registry.register_fake_lsp(
24180 "TypeScript",
24181 FakeLspAdapter {
24182 capabilities: lsp::ServerCapabilities {
24183 code_lens_provider: Some(lsp::CodeLensOptions {
24184 resolve_provider: Some(true),
24185 }),
24186 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24187 commands: vec!["_the/command".to_string()],
24188 ..lsp::ExecuteCommandOptions::default()
24189 }),
24190 ..lsp::ServerCapabilities::default()
24191 },
24192 ..FakeLspAdapter::default()
24193 },
24194 );
24195
24196 let editor = workspace
24197 .update(cx, |workspace, window, cx| {
24198 workspace.open_abs_path(
24199 PathBuf::from(path!("/dir/a.ts")),
24200 OpenOptions::default(),
24201 window,
24202 cx,
24203 )
24204 })
24205 .unwrap()
24206 .await
24207 .unwrap()
24208 .downcast::<Editor>()
24209 .unwrap();
24210 cx.executor().run_until_parked();
24211
24212 let fake_server = fake_language_servers.next().await.unwrap();
24213
24214 let buffer = editor.update(cx, |editor, cx| {
24215 editor
24216 .buffer()
24217 .read(cx)
24218 .as_singleton()
24219 .expect("have opened a single file by path")
24220 });
24221
24222 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24223 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24224 drop(buffer_snapshot);
24225 let actions = cx
24226 .update_window(*workspace, |_, window, cx| {
24227 project.code_actions(&buffer, anchor..anchor, window, cx)
24228 })
24229 .unwrap();
24230
24231 fake_server
24232 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24233 Ok(Some(vec![
24234 lsp::CodeLens {
24235 range: lsp::Range::default(),
24236 command: Some(lsp::Command {
24237 title: "Code lens command".to_owned(),
24238 command: "_the/command".to_owned(),
24239 arguments: None,
24240 }),
24241 data: None,
24242 },
24243 lsp::CodeLens {
24244 range: lsp::Range::default(),
24245 command: Some(lsp::Command {
24246 title: "Command not in capabilities".to_owned(),
24247 command: "not in capabilities".to_owned(),
24248 arguments: None,
24249 }),
24250 data: None,
24251 },
24252 lsp::CodeLens {
24253 range: lsp::Range {
24254 start: lsp::Position {
24255 line: 1,
24256 character: 1,
24257 },
24258 end: lsp::Position {
24259 line: 1,
24260 character: 1,
24261 },
24262 },
24263 command: Some(lsp::Command {
24264 title: "Command not in range".to_owned(),
24265 command: "_the/command".to_owned(),
24266 arguments: None,
24267 }),
24268 data: None,
24269 },
24270 ]))
24271 })
24272 .next()
24273 .await;
24274
24275 let actions = actions.await.unwrap();
24276 assert_eq!(
24277 actions.len(),
24278 1,
24279 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24280 );
24281 let action = actions[0].clone();
24282 let apply = project.update(cx, |project, cx| {
24283 project.apply_code_action(buffer.clone(), action, true, cx)
24284 });
24285
24286 // Resolving the code action does not populate its edits. In absence of
24287 // edits, we must execute the given command.
24288 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24289 |mut lens, _| async move {
24290 let lens_command = lens.command.as_mut().expect("should have a command");
24291 assert_eq!(lens_command.title, "Code lens command");
24292 lens_command.arguments = Some(vec![json!("the-argument")]);
24293 Ok(lens)
24294 },
24295 );
24296
24297 // While executing the command, the language server sends the editor
24298 // a `workspaceEdit` request.
24299 fake_server
24300 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24301 let fake = fake_server.clone();
24302 move |params, _| {
24303 assert_eq!(params.command, "_the/command");
24304 let fake = fake.clone();
24305 async move {
24306 fake.server
24307 .request::<lsp::request::ApplyWorkspaceEdit>(
24308 lsp::ApplyWorkspaceEditParams {
24309 label: None,
24310 edit: lsp::WorkspaceEdit {
24311 changes: Some(
24312 [(
24313 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24314 vec![lsp::TextEdit {
24315 range: lsp::Range::new(
24316 lsp::Position::new(0, 0),
24317 lsp::Position::new(0, 0),
24318 ),
24319 new_text: "X".into(),
24320 }],
24321 )]
24322 .into_iter()
24323 .collect(),
24324 ),
24325 ..lsp::WorkspaceEdit::default()
24326 },
24327 },
24328 )
24329 .await
24330 .into_response()
24331 .unwrap();
24332 Ok(Some(json!(null)))
24333 }
24334 }
24335 })
24336 .next()
24337 .await;
24338
24339 // Applying the code lens command returns a project transaction containing the edits
24340 // sent by the language server in its `workspaceEdit` request.
24341 let transaction = apply.await.unwrap();
24342 assert!(transaction.0.contains_key(&buffer));
24343 buffer.update(cx, |buffer, cx| {
24344 assert_eq!(buffer.text(), "Xa");
24345 buffer.undo(cx);
24346 assert_eq!(buffer.text(), "a");
24347 });
24348
24349 let actions_after_edits = cx
24350 .update_window(*workspace, |_, window, cx| {
24351 project.code_actions(&buffer, anchor..anchor, window, cx)
24352 })
24353 .unwrap()
24354 .await
24355 .unwrap();
24356 assert_eq!(
24357 actions, actions_after_edits,
24358 "For the same selection, same code lens actions should be returned"
24359 );
24360
24361 let _responses =
24362 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24363 panic!("No more code lens requests are expected");
24364 });
24365 editor.update_in(cx, |editor, window, cx| {
24366 editor.select_all(&SelectAll, window, cx);
24367 });
24368 cx.executor().run_until_parked();
24369 let new_actions = cx
24370 .update_window(*workspace, |_, window, cx| {
24371 project.code_actions(&buffer, anchor..anchor, window, cx)
24372 })
24373 .unwrap()
24374 .await
24375 .unwrap();
24376 assert_eq!(
24377 actions, new_actions,
24378 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24379 );
24380}
24381
24382#[gpui::test]
24383async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24384 init_test(cx, |_| {});
24385
24386 let fs = FakeFs::new(cx.executor());
24387 let main_text = r#"fn main() {
24388println!("1");
24389println!("2");
24390println!("3");
24391println!("4");
24392println!("5");
24393}"#;
24394 let lib_text = "mod foo {}";
24395 fs.insert_tree(
24396 path!("/a"),
24397 json!({
24398 "lib.rs": lib_text,
24399 "main.rs": main_text,
24400 }),
24401 )
24402 .await;
24403
24404 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24405 let (workspace, cx) =
24406 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24407 let worktree_id = workspace.update(cx, |workspace, cx| {
24408 workspace.project().update(cx, |project, cx| {
24409 project.worktrees(cx).next().unwrap().read(cx).id()
24410 })
24411 });
24412
24413 let expected_ranges = vec![
24414 Point::new(0, 0)..Point::new(0, 0),
24415 Point::new(1, 0)..Point::new(1, 1),
24416 Point::new(2, 0)..Point::new(2, 2),
24417 Point::new(3, 0)..Point::new(3, 3),
24418 ];
24419
24420 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24421 let editor_1 = workspace
24422 .update_in(cx, |workspace, window, cx| {
24423 workspace.open_path(
24424 (worktree_id, rel_path("main.rs")),
24425 Some(pane_1.downgrade()),
24426 true,
24427 window,
24428 cx,
24429 )
24430 })
24431 .unwrap()
24432 .await
24433 .downcast::<Editor>()
24434 .unwrap();
24435 pane_1.update(cx, |pane, cx| {
24436 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24437 open_editor.update(cx, |editor, cx| {
24438 assert_eq!(
24439 editor.display_text(cx),
24440 main_text,
24441 "Original main.rs text on initial open",
24442 );
24443 assert_eq!(
24444 editor
24445 .selections
24446 .all::<Point>(&editor.display_snapshot(cx))
24447 .into_iter()
24448 .map(|s| s.range())
24449 .collect::<Vec<_>>(),
24450 vec![Point::zero()..Point::zero()],
24451 "Default selections on initial open",
24452 );
24453 })
24454 });
24455 editor_1.update_in(cx, |editor, window, cx| {
24456 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24457 s.select_ranges(expected_ranges.clone());
24458 });
24459 });
24460
24461 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24462 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24463 });
24464 let editor_2 = workspace
24465 .update_in(cx, |workspace, window, cx| {
24466 workspace.open_path(
24467 (worktree_id, rel_path("main.rs")),
24468 Some(pane_2.downgrade()),
24469 true,
24470 window,
24471 cx,
24472 )
24473 })
24474 .unwrap()
24475 .await
24476 .downcast::<Editor>()
24477 .unwrap();
24478 pane_2.update(cx, |pane, cx| {
24479 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24480 open_editor.update(cx, |editor, cx| {
24481 assert_eq!(
24482 editor.display_text(cx),
24483 main_text,
24484 "Original main.rs text on initial open in another panel",
24485 );
24486 assert_eq!(
24487 editor
24488 .selections
24489 .all::<Point>(&editor.display_snapshot(cx))
24490 .into_iter()
24491 .map(|s| s.range())
24492 .collect::<Vec<_>>(),
24493 vec![Point::zero()..Point::zero()],
24494 "Default selections on initial open in another panel",
24495 );
24496 })
24497 });
24498
24499 editor_2.update_in(cx, |editor, window, cx| {
24500 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24501 });
24502
24503 let _other_editor_1 = workspace
24504 .update_in(cx, |workspace, window, cx| {
24505 workspace.open_path(
24506 (worktree_id, rel_path("lib.rs")),
24507 Some(pane_1.downgrade()),
24508 true,
24509 window,
24510 cx,
24511 )
24512 })
24513 .unwrap()
24514 .await
24515 .downcast::<Editor>()
24516 .unwrap();
24517 pane_1
24518 .update_in(cx, |pane, window, cx| {
24519 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24520 })
24521 .await
24522 .unwrap();
24523 drop(editor_1);
24524 pane_1.update(cx, |pane, cx| {
24525 pane.active_item()
24526 .unwrap()
24527 .downcast::<Editor>()
24528 .unwrap()
24529 .update(cx, |editor, cx| {
24530 assert_eq!(
24531 editor.display_text(cx),
24532 lib_text,
24533 "Other file should be open and active",
24534 );
24535 });
24536 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24537 });
24538
24539 let _other_editor_2 = workspace
24540 .update_in(cx, |workspace, window, cx| {
24541 workspace.open_path(
24542 (worktree_id, rel_path("lib.rs")),
24543 Some(pane_2.downgrade()),
24544 true,
24545 window,
24546 cx,
24547 )
24548 })
24549 .unwrap()
24550 .await
24551 .downcast::<Editor>()
24552 .unwrap();
24553 pane_2
24554 .update_in(cx, |pane, window, cx| {
24555 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24556 })
24557 .await
24558 .unwrap();
24559 drop(editor_2);
24560 pane_2.update(cx, |pane, cx| {
24561 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24562 open_editor.update(cx, |editor, cx| {
24563 assert_eq!(
24564 editor.display_text(cx),
24565 lib_text,
24566 "Other file should be open and active in another panel too",
24567 );
24568 });
24569 assert_eq!(
24570 pane.items().count(),
24571 1,
24572 "No other editors should be open in another pane",
24573 );
24574 });
24575
24576 let _editor_1_reopened = workspace
24577 .update_in(cx, |workspace, window, cx| {
24578 workspace.open_path(
24579 (worktree_id, rel_path("main.rs")),
24580 Some(pane_1.downgrade()),
24581 true,
24582 window,
24583 cx,
24584 )
24585 })
24586 .unwrap()
24587 .await
24588 .downcast::<Editor>()
24589 .unwrap();
24590 let _editor_2_reopened = workspace
24591 .update_in(cx, |workspace, window, cx| {
24592 workspace.open_path(
24593 (worktree_id, rel_path("main.rs")),
24594 Some(pane_2.downgrade()),
24595 true,
24596 window,
24597 cx,
24598 )
24599 })
24600 .unwrap()
24601 .await
24602 .downcast::<Editor>()
24603 .unwrap();
24604 pane_1.update(cx, |pane, cx| {
24605 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24606 open_editor.update(cx, |editor, cx| {
24607 assert_eq!(
24608 editor.display_text(cx),
24609 main_text,
24610 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24611 );
24612 assert_eq!(
24613 editor
24614 .selections
24615 .all::<Point>(&editor.display_snapshot(cx))
24616 .into_iter()
24617 .map(|s| s.range())
24618 .collect::<Vec<_>>(),
24619 expected_ranges,
24620 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24621 );
24622 })
24623 });
24624 pane_2.update(cx, |pane, cx| {
24625 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24626 open_editor.update(cx, |editor, cx| {
24627 assert_eq!(
24628 editor.display_text(cx),
24629 r#"fn main() {
24630⋯rintln!("1");
24631⋯intln!("2");
24632⋯ntln!("3");
24633println!("4");
24634println!("5");
24635}"#,
24636 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24637 );
24638 assert_eq!(
24639 editor
24640 .selections
24641 .all::<Point>(&editor.display_snapshot(cx))
24642 .into_iter()
24643 .map(|s| s.range())
24644 .collect::<Vec<_>>(),
24645 vec![Point::zero()..Point::zero()],
24646 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24647 );
24648 })
24649 });
24650}
24651
24652#[gpui::test]
24653async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24654 init_test(cx, |_| {});
24655
24656 let fs = FakeFs::new(cx.executor());
24657 let main_text = r#"fn main() {
24658println!("1");
24659println!("2");
24660println!("3");
24661println!("4");
24662println!("5");
24663}"#;
24664 let lib_text = "mod foo {}";
24665 fs.insert_tree(
24666 path!("/a"),
24667 json!({
24668 "lib.rs": lib_text,
24669 "main.rs": main_text,
24670 }),
24671 )
24672 .await;
24673
24674 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24675 let (workspace, cx) =
24676 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24677 let worktree_id = workspace.update(cx, |workspace, cx| {
24678 workspace.project().update(cx, |project, cx| {
24679 project.worktrees(cx).next().unwrap().read(cx).id()
24680 })
24681 });
24682
24683 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24684 let editor = workspace
24685 .update_in(cx, |workspace, window, cx| {
24686 workspace.open_path(
24687 (worktree_id, rel_path("main.rs")),
24688 Some(pane.downgrade()),
24689 true,
24690 window,
24691 cx,
24692 )
24693 })
24694 .unwrap()
24695 .await
24696 .downcast::<Editor>()
24697 .unwrap();
24698 pane.update(cx, |pane, cx| {
24699 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24700 open_editor.update(cx, |editor, cx| {
24701 assert_eq!(
24702 editor.display_text(cx),
24703 main_text,
24704 "Original main.rs text on initial open",
24705 );
24706 })
24707 });
24708 editor.update_in(cx, |editor, window, cx| {
24709 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24710 });
24711
24712 cx.update_global(|store: &mut SettingsStore, cx| {
24713 store.update_user_settings(cx, |s| {
24714 s.workspace.restore_on_file_reopen = Some(false);
24715 });
24716 });
24717 editor.update_in(cx, |editor, window, cx| {
24718 editor.fold_ranges(
24719 vec![
24720 Point::new(1, 0)..Point::new(1, 1),
24721 Point::new(2, 0)..Point::new(2, 2),
24722 Point::new(3, 0)..Point::new(3, 3),
24723 ],
24724 false,
24725 window,
24726 cx,
24727 );
24728 });
24729 pane.update_in(cx, |pane, window, cx| {
24730 pane.close_all_items(&CloseAllItems::default(), window, cx)
24731 })
24732 .await
24733 .unwrap();
24734 pane.update(cx, |pane, _| {
24735 assert!(pane.active_item().is_none());
24736 });
24737 cx.update_global(|store: &mut SettingsStore, cx| {
24738 store.update_user_settings(cx, |s| {
24739 s.workspace.restore_on_file_reopen = Some(true);
24740 });
24741 });
24742
24743 let _editor_reopened = workspace
24744 .update_in(cx, |workspace, window, cx| {
24745 workspace.open_path(
24746 (worktree_id, rel_path("main.rs")),
24747 Some(pane.downgrade()),
24748 true,
24749 window,
24750 cx,
24751 )
24752 })
24753 .unwrap()
24754 .await
24755 .downcast::<Editor>()
24756 .unwrap();
24757 pane.update(cx, |pane, cx| {
24758 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24759 open_editor.update(cx, |editor, cx| {
24760 assert_eq!(
24761 editor.display_text(cx),
24762 main_text,
24763 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24764 );
24765 })
24766 });
24767}
24768
24769#[gpui::test]
24770async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24771 struct EmptyModalView {
24772 focus_handle: gpui::FocusHandle,
24773 }
24774 impl EventEmitter<DismissEvent> for EmptyModalView {}
24775 impl Render for EmptyModalView {
24776 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24777 div()
24778 }
24779 }
24780 impl Focusable for EmptyModalView {
24781 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24782 self.focus_handle.clone()
24783 }
24784 }
24785 impl workspace::ModalView for EmptyModalView {}
24786 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24787 EmptyModalView {
24788 focus_handle: cx.focus_handle(),
24789 }
24790 }
24791
24792 init_test(cx, |_| {});
24793
24794 let fs = FakeFs::new(cx.executor());
24795 let project = Project::test(fs, [], cx).await;
24796 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24797 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24798 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24799 let editor = cx.new_window_entity(|window, cx| {
24800 Editor::new(
24801 EditorMode::full(),
24802 buffer,
24803 Some(project.clone()),
24804 window,
24805 cx,
24806 )
24807 });
24808 workspace
24809 .update(cx, |workspace, window, cx| {
24810 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24811 })
24812 .unwrap();
24813 editor.update_in(cx, |editor, window, cx| {
24814 editor.open_context_menu(&OpenContextMenu, window, cx);
24815 assert!(editor.mouse_context_menu.is_some());
24816 });
24817 workspace
24818 .update(cx, |workspace, window, cx| {
24819 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24820 })
24821 .unwrap();
24822 cx.read(|cx| {
24823 assert!(editor.read(cx).mouse_context_menu.is_none());
24824 });
24825}
24826
24827fn set_linked_edit_ranges(
24828 opening: (Point, Point),
24829 closing: (Point, Point),
24830 editor: &mut Editor,
24831 cx: &mut Context<Editor>,
24832) {
24833 let Some((buffer, _)) = editor
24834 .buffer
24835 .read(cx)
24836 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24837 else {
24838 panic!("Failed to get buffer for selection position");
24839 };
24840 let buffer = buffer.read(cx);
24841 let buffer_id = buffer.remote_id();
24842 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24843 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24844 let mut linked_ranges = HashMap::default();
24845 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24846 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24847}
24848
24849#[gpui::test]
24850async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24851 init_test(cx, |_| {});
24852
24853 let fs = FakeFs::new(cx.executor());
24854 fs.insert_file(path!("/file.html"), Default::default())
24855 .await;
24856
24857 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24858
24859 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24860 let html_language = Arc::new(Language::new(
24861 LanguageConfig {
24862 name: "HTML".into(),
24863 matcher: LanguageMatcher {
24864 path_suffixes: vec!["html".to_string()],
24865 ..LanguageMatcher::default()
24866 },
24867 brackets: BracketPairConfig {
24868 pairs: vec![BracketPair {
24869 start: "<".into(),
24870 end: ">".into(),
24871 close: true,
24872 ..Default::default()
24873 }],
24874 ..Default::default()
24875 },
24876 ..Default::default()
24877 },
24878 Some(tree_sitter_html::LANGUAGE.into()),
24879 ));
24880 language_registry.add(html_language);
24881 let mut fake_servers = language_registry.register_fake_lsp(
24882 "HTML",
24883 FakeLspAdapter {
24884 capabilities: lsp::ServerCapabilities {
24885 completion_provider: Some(lsp::CompletionOptions {
24886 resolve_provider: Some(true),
24887 ..Default::default()
24888 }),
24889 ..Default::default()
24890 },
24891 ..Default::default()
24892 },
24893 );
24894
24895 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24896 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24897
24898 let worktree_id = workspace
24899 .update(cx, |workspace, _window, cx| {
24900 workspace.project().update(cx, |project, cx| {
24901 project.worktrees(cx).next().unwrap().read(cx).id()
24902 })
24903 })
24904 .unwrap();
24905 project
24906 .update(cx, |project, cx| {
24907 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24908 })
24909 .await
24910 .unwrap();
24911 let editor = workspace
24912 .update(cx, |workspace, window, cx| {
24913 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24914 })
24915 .unwrap()
24916 .await
24917 .unwrap()
24918 .downcast::<Editor>()
24919 .unwrap();
24920
24921 let fake_server = fake_servers.next().await.unwrap();
24922 editor.update_in(cx, |editor, window, cx| {
24923 editor.set_text("<ad></ad>", window, cx);
24924 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24925 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24926 });
24927 set_linked_edit_ranges(
24928 (Point::new(0, 1), Point::new(0, 3)),
24929 (Point::new(0, 6), Point::new(0, 8)),
24930 editor,
24931 cx,
24932 );
24933 });
24934 let mut completion_handle =
24935 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24936 Ok(Some(lsp::CompletionResponse::Array(vec![
24937 lsp::CompletionItem {
24938 label: "head".to_string(),
24939 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24940 lsp::InsertReplaceEdit {
24941 new_text: "head".to_string(),
24942 insert: lsp::Range::new(
24943 lsp::Position::new(0, 1),
24944 lsp::Position::new(0, 3),
24945 ),
24946 replace: lsp::Range::new(
24947 lsp::Position::new(0, 1),
24948 lsp::Position::new(0, 3),
24949 ),
24950 },
24951 )),
24952 ..Default::default()
24953 },
24954 ])))
24955 });
24956 editor.update_in(cx, |editor, window, cx| {
24957 editor.show_completions(&ShowCompletions, window, cx);
24958 });
24959 cx.run_until_parked();
24960 completion_handle.next().await.unwrap();
24961 editor.update(cx, |editor, _| {
24962 assert!(
24963 editor.context_menu_visible(),
24964 "Completion menu should be visible"
24965 );
24966 });
24967 editor.update_in(cx, |editor, window, cx| {
24968 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24969 });
24970 cx.executor().run_until_parked();
24971 editor.update(cx, |editor, cx| {
24972 assert_eq!(editor.text(cx), "<head></head>");
24973 });
24974}
24975
24976#[gpui::test]
24977async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24978 init_test(cx, |_| {});
24979
24980 let mut cx = EditorTestContext::new(cx).await;
24981 let language = Arc::new(Language::new(
24982 LanguageConfig {
24983 name: "TSX".into(),
24984 matcher: LanguageMatcher {
24985 path_suffixes: vec!["tsx".to_string()],
24986 ..LanguageMatcher::default()
24987 },
24988 brackets: BracketPairConfig {
24989 pairs: vec![BracketPair {
24990 start: "<".into(),
24991 end: ">".into(),
24992 close: true,
24993 ..Default::default()
24994 }],
24995 ..Default::default()
24996 },
24997 linked_edit_characters: HashSet::from_iter(['.']),
24998 ..Default::default()
24999 },
25000 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25001 ));
25002 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25003
25004 // Test typing > does not extend linked pair
25005 cx.set_state("<divˇ<div></div>");
25006 cx.update_editor(|editor, _, cx| {
25007 set_linked_edit_ranges(
25008 (Point::new(0, 1), Point::new(0, 4)),
25009 (Point::new(0, 11), Point::new(0, 14)),
25010 editor,
25011 cx,
25012 );
25013 });
25014 cx.update_editor(|editor, window, cx| {
25015 editor.handle_input(">", window, cx);
25016 });
25017 cx.assert_editor_state("<div>ˇ<div></div>");
25018
25019 // Test typing . do extend linked pair
25020 cx.set_state("<Animatedˇ></Animated>");
25021 cx.update_editor(|editor, _, cx| {
25022 set_linked_edit_ranges(
25023 (Point::new(0, 1), Point::new(0, 9)),
25024 (Point::new(0, 12), Point::new(0, 20)),
25025 editor,
25026 cx,
25027 );
25028 });
25029 cx.update_editor(|editor, window, cx| {
25030 editor.handle_input(".", window, cx);
25031 });
25032 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25033 cx.update_editor(|editor, _, cx| {
25034 set_linked_edit_ranges(
25035 (Point::new(0, 1), Point::new(0, 10)),
25036 (Point::new(0, 13), Point::new(0, 21)),
25037 editor,
25038 cx,
25039 );
25040 });
25041 cx.update_editor(|editor, window, cx| {
25042 editor.handle_input("V", window, cx);
25043 });
25044 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25045}
25046
25047#[gpui::test]
25048async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25049 init_test(cx, |_| {});
25050
25051 let fs = FakeFs::new(cx.executor());
25052 fs.insert_tree(
25053 path!("/root"),
25054 json!({
25055 "a": {
25056 "main.rs": "fn main() {}",
25057 },
25058 "foo": {
25059 "bar": {
25060 "external_file.rs": "pub mod external {}",
25061 }
25062 }
25063 }),
25064 )
25065 .await;
25066
25067 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25068 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25069 language_registry.add(rust_lang());
25070 let _fake_servers = language_registry.register_fake_lsp(
25071 "Rust",
25072 FakeLspAdapter {
25073 ..FakeLspAdapter::default()
25074 },
25075 );
25076 let (workspace, cx) =
25077 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25078 let worktree_id = workspace.update(cx, |workspace, cx| {
25079 workspace.project().update(cx, |project, cx| {
25080 project.worktrees(cx).next().unwrap().read(cx).id()
25081 })
25082 });
25083
25084 let assert_language_servers_count =
25085 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25086 project.update(cx, |project, cx| {
25087 let current = project
25088 .lsp_store()
25089 .read(cx)
25090 .as_local()
25091 .unwrap()
25092 .language_servers
25093 .len();
25094 assert_eq!(expected, current, "{context}");
25095 });
25096 };
25097
25098 assert_language_servers_count(
25099 0,
25100 "No servers should be running before any file is open",
25101 cx,
25102 );
25103 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25104 let main_editor = workspace
25105 .update_in(cx, |workspace, window, cx| {
25106 workspace.open_path(
25107 (worktree_id, rel_path("main.rs")),
25108 Some(pane.downgrade()),
25109 true,
25110 window,
25111 cx,
25112 )
25113 })
25114 .unwrap()
25115 .await
25116 .downcast::<Editor>()
25117 .unwrap();
25118 pane.update(cx, |pane, cx| {
25119 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25120 open_editor.update(cx, |editor, cx| {
25121 assert_eq!(
25122 editor.display_text(cx),
25123 "fn main() {}",
25124 "Original main.rs text on initial open",
25125 );
25126 });
25127 assert_eq!(open_editor, main_editor);
25128 });
25129 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25130
25131 let external_editor = workspace
25132 .update_in(cx, |workspace, window, cx| {
25133 workspace.open_abs_path(
25134 PathBuf::from("/root/foo/bar/external_file.rs"),
25135 OpenOptions::default(),
25136 window,
25137 cx,
25138 )
25139 })
25140 .await
25141 .expect("opening external file")
25142 .downcast::<Editor>()
25143 .expect("downcasted external file's open element to editor");
25144 pane.update(cx, |pane, cx| {
25145 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25146 open_editor.update(cx, |editor, cx| {
25147 assert_eq!(
25148 editor.display_text(cx),
25149 "pub mod external {}",
25150 "External file is open now",
25151 );
25152 });
25153 assert_eq!(open_editor, external_editor);
25154 });
25155 assert_language_servers_count(
25156 1,
25157 "Second, external, *.rs file should join the existing server",
25158 cx,
25159 );
25160
25161 pane.update_in(cx, |pane, window, cx| {
25162 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25163 })
25164 .await
25165 .unwrap();
25166 pane.update_in(cx, |pane, window, cx| {
25167 pane.navigate_backward(&Default::default(), window, cx);
25168 });
25169 cx.run_until_parked();
25170 pane.update(cx, |pane, cx| {
25171 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25172 open_editor.update(cx, |editor, cx| {
25173 assert_eq!(
25174 editor.display_text(cx),
25175 "pub mod external {}",
25176 "External file is open now",
25177 );
25178 });
25179 });
25180 assert_language_servers_count(
25181 1,
25182 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25183 cx,
25184 );
25185
25186 cx.update(|_, cx| {
25187 workspace::reload(cx);
25188 });
25189 assert_language_servers_count(
25190 1,
25191 "After reloading the worktree with local and external files opened, only one project should be started",
25192 cx,
25193 );
25194}
25195
25196#[gpui::test]
25197async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25198 init_test(cx, |_| {});
25199
25200 let mut cx = EditorTestContext::new(cx).await;
25201 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25202 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25203
25204 // test cursor move to start of each line on tab
25205 // for `if`, `elif`, `else`, `while`, `with` and `for`
25206 cx.set_state(indoc! {"
25207 def main():
25208 ˇ for item in items:
25209 ˇ while item.active:
25210 ˇ if item.value > 10:
25211 ˇ continue
25212 ˇ elif item.value < 0:
25213 ˇ break
25214 ˇ else:
25215 ˇ with item.context() as ctx:
25216 ˇ yield count
25217 ˇ else:
25218 ˇ log('while else')
25219 ˇ else:
25220 ˇ log('for else')
25221 "});
25222 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25223 cx.assert_editor_state(indoc! {"
25224 def main():
25225 ˇfor item in items:
25226 ˇwhile item.active:
25227 ˇif item.value > 10:
25228 ˇcontinue
25229 ˇelif item.value < 0:
25230 ˇbreak
25231 ˇelse:
25232 ˇwith item.context() as ctx:
25233 ˇyield count
25234 ˇelse:
25235 ˇlog('while else')
25236 ˇelse:
25237 ˇlog('for else')
25238 "});
25239 // test relative indent is preserved when tab
25240 // for `if`, `elif`, `else`, `while`, `with` and `for`
25241 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25242 cx.assert_editor_state(indoc! {"
25243 def main():
25244 ˇfor item in items:
25245 ˇwhile item.active:
25246 ˇif item.value > 10:
25247 ˇcontinue
25248 ˇelif item.value < 0:
25249 ˇbreak
25250 ˇelse:
25251 ˇwith item.context() as ctx:
25252 ˇyield count
25253 ˇelse:
25254 ˇlog('while else')
25255 ˇelse:
25256 ˇlog('for else')
25257 "});
25258
25259 // test cursor move to start of each line on tab
25260 // for `try`, `except`, `else`, `finally`, `match` and `def`
25261 cx.set_state(indoc! {"
25262 def main():
25263 ˇ try:
25264 ˇ fetch()
25265 ˇ except ValueError:
25266 ˇ handle_error()
25267 ˇ else:
25268 ˇ match value:
25269 ˇ case _:
25270 ˇ finally:
25271 ˇ def status():
25272 ˇ return 0
25273 "});
25274 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25275 cx.assert_editor_state(indoc! {"
25276 def main():
25277 ˇtry:
25278 ˇfetch()
25279 ˇexcept ValueError:
25280 ˇhandle_error()
25281 ˇelse:
25282 ˇmatch value:
25283 ˇcase _:
25284 ˇfinally:
25285 ˇdef status():
25286 ˇreturn 0
25287 "});
25288 // test relative indent is preserved when tab
25289 // for `try`, `except`, `else`, `finally`, `match` and `def`
25290 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25291 cx.assert_editor_state(indoc! {"
25292 def main():
25293 ˇtry:
25294 ˇfetch()
25295 ˇexcept ValueError:
25296 ˇhandle_error()
25297 ˇelse:
25298 ˇmatch value:
25299 ˇcase _:
25300 ˇfinally:
25301 ˇdef status():
25302 ˇreturn 0
25303 "});
25304}
25305
25306#[gpui::test]
25307async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25308 init_test(cx, |_| {});
25309
25310 let mut cx = EditorTestContext::new(cx).await;
25311 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25312 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25313
25314 // test `else` auto outdents when typed inside `if` block
25315 cx.set_state(indoc! {"
25316 def main():
25317 if i == 2:
25318 return
25319 ˇ
25320 "});
25321 cx.update_editor(|editor, window, cx| {
25322 editor.handle_input("else:", window, cx);
25323 });
25324 cx.assert_editor_state(indoc! {"
25325 def main():
25326 if i == 2:
25327 return
25328 else:ˇ
25329 "});
25330
25331 // test `except` auto outdents when typed inside `try` block
25332 cx.set_state(indoc! {"
25333 def main():
25334 try:
25335 i = 2
25336 ˇ
25337 "});
25338 cx.update_editor(|editor, window, cx| {
25339 editor.handle_input("except:", window, cx);
25340 });
25341 cx.assert_editor_state(indoc! {"
25342 def main():
25343 try:
25344 i = 2
25345 except:ˇ
25346 "});
25347
25348 // test `else` auto outdents when typed inside `except` block
25349 cx.set_state(indoc! {"
25350 def main():
25351 try:
25352 i = 2
25353 except:
25354 j = 2
25355 ˇ
25356 "});
25357 cx.update_editor(|editor, window, cx| {
25358 editor.handle_input("else:", window, cx);
25359 });
25360 cx.assert_editor_state(indoc! {"
25361 def main():
25362 try:
25363 i = 2
25364 except:
25365 j = 2
25366 else:ˇ
25367 "});
25368
25369 // test `finally` auto outdents when typed inside `else` block
25370 cx.set_state(indoc! {"
25371 def main():
25372 try:
25373 i = 2
25374 except:
25375 j = 2
25376 else:
25377 k = 2
25378 ˇ
25379 "});
25380 cx.update_editor(|editor, window, cx| {
25381 editor.handle_input("finally:", window, cx);
25382 });
25383 cx.assert_editor_state(indoc! {"
25384 def main():
25385 try:
25386 i = 2
25387 except:
25388 j = 2
25389 else:
25390 k = 2
25391 finally:ˇ
25392 "});
25393
25394 // test `else` does not outdents when typed inside `except` block right after for block
25395 cx.set_state(indoc! {"
25396 def main():
25397 try:
25398 i = 2
25399 except:
25400 for i in range(n):
25401 pass
25402 ˇ
25403 "});
25404 cx.update_editor(|editor, window, cx| {
25405 editor.handle_input("else:", window, cx);
25406 });
25407 cx.assert_editor_state(indoc! {"
25408 def main():
25409 try:
25410 i = 2
25411 except:
25412 for i in range(n):
25413 pass
25414 else:ˇ
25415 "});
25416
25417 // test `finally` auto outdents when typed inside `else` block right after for block
25418 cx.set_state(indoc! {"
25419 def main():
25420 try:
25421 i = 2
25422 except:
25423 j = 2
25424 else:
25425 for i in range(n):
25426 pass
25427 ˇ
25428 "});
25429 cx.update_editor(|editor, window, cx| {
25430 editor.handle_input("finally:", window, cx);
25431 });
25432 cx.assert_editor_state(indoc! {"
25433 def main():
25434 try:
25435 i = 2
25436 except:
25437 j = 2
25438 else:
25439 for i in range(n):
25440 pass
25441 finally:ˇ
25442 "});
25443
25444 // test `except` outdents to inner "try" block
25445 cx.set_state(indoc! {"
25446 def main():
25447 try:
25448 i = 2
25449 if i == 2:
25450 try:
25451 i = 3
25452 ˇ
25453 "});
25454 cx.update_editor(|editor, window, cx| {
25455 editor.handle_input("except:", window, cx);
25456 });
25457 cx.assert_editor_state(indoc! {"
25458 def main():
25459 try:
25460 i = 2
25461 if i == 2:
25462 try:
25463 i = 3
25464 except:ˇ
25465 "});
25466
25467 // test `except` outdents to outer "try" block
25468 cx.set_state(indoc! {"
25469 def main():
25470 try:
25471 i = 2
25472 if i == 2:
25473 try:
25474 i = 3
25475 ˇ
25476 "});
25477 cx.update_editor(|editor, window, cx| {
25478 editor.handle_input("except:", window, cx);
25479 });
25480 cx.assert_editor_state(indoc! {"
25481 def main():
25482 try:
25483 i = 2
25484 if i == 2:
25485 try:
25486 i = 3
25487 except:ˇ
25488 "});
25489
25490 // test `else` stays at correct indent when typed after `for` block
25491 cx.set_state(indoc! {"
25492 def main():
25493 for i in range(10):
25494 if i == 3:
25495 break
25496 ˇ
25497 "});
25498 cx.update_editor(|editor, window, cx| {
25499 editor.handle_input("else:", window, cx);
25500 });
25501 cx.assert_editor_state(indoc! {"
25502 def main():
25503 for i in range(10):
25504 if i == 3:
25505 break
25506 else:ˇ
25507 "});
25508
25509 // test does not outdent on typing after line with square brackets
25510 cx.set_state(indoc! {"
25511 def f() -> list[str]:
25512 ˇ
25513 "});
25514 cx.update_editor(|editor, window, cx| {
25515 editor.handle_input("a", window, cx);
25516 });
25517 cx.assert_editor_state(indoc! {"
25518 def f() -> list[str]:
25519 aˇ
25520 "});
25521
25522 // test does not outdent on typing : after case keyword
25523 cx.set_state(indoc! {"
25524 match 1:
25525 caseˇ
25526 "});
25527 cx.update_editor(|editor, window, cx| {
25528 editor.handle_input(":", window, cx);
25529 });
25530 cx.assert_editor_state(indoc! {"
25531 match 1:
25532 case:ˇ
25533 "});
25534}
25535
25536#[gpui::test]
25537async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25538 init_test(cx, |_| {});
25539 update_test_language_settings(cx, |settings| {
25540 settings.defaults.extend_comment_on_newline = Some(false);
25541 });
25542 let mut cx = EditorTestContext::new(cx).await;
25543 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25544 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25545
25546 // test correct indent after newline on comment
25547 cx.set_state(indoc! {"
25548 # COMMENT:ˇ
25549 "});
25550 cx.update_editor(|editor, window, cx| {
25551 editor.newline(&Newline, window, cx);
25552 });
25553 cx.assert_editor_state(indoc! {"
25554 # COMMENT:
25555 ˇ
25556 "});
25557
25558 // test correct indent after newline in brackets
25559 cx.set_state(indoc! {"
25560 {ˇ}
25561 "});
25562 cx.update_editor(|editor, window, cx| {
25563 editor.newline(&Newline, window, cx);
25564 });
25565 cx.run_until_parked();
25566 cx.assert_editor_state(indoc! {"
25567 {
25568 ˇ
25569 }
25570 "});
25571
25572 cx.set_state(indoc! {"
25573 (ˇ)
25574 "});
25575 cx.update_editor(|editor, window, cx| {
25576 editor.newline(&Newline, window, cx);
25577 });
25578 cx.run_until_parked();
25579 cx.assert_editor_state(indoc! {"
25580 (
25581 ˇ
25582 )
25583 "});
25584
25585 // do not indent after empty lists or dictionaries
25586 cx.set_state(indoc! {"
25587 a = []ˇ
25588 "});
25589 cx.update_editor(|editor, window, cx| {
25590 editor.newline(&Newline, window, cx);
25591 });
25592 cx.run_until_parked();
25593 cx.assert_editor_state(indoc! {"
25594 a = []
25595 ˇ
25596 "});
25597}
25598
25599#[gpui::test]
25600async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25601 init_test(cx, |_| {});
25602
25603 let mut cx = EditorTestContext::new(cx).await;
25604 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25605 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25606
25607 // test cursor move to start of each line on tab
25608 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25609 cx.set_state(indoc! {"
25610 function main() {
25611 ˇ for item in $items; do
25612 ˇ while [ -n \"$item\" ]; do
25613 ˇ if [ \"$value\" -gt 10 ]; then
25614 ˇ continue
25615 ˇ elif [ \"$value\" -lt 0 ]; then
25616 ˇ break
25617 ˇ else
25618 ˇ echo \"$item\"
25619 ˇ fi
25620 ˇ done
25621 ˇ done
25622 ˇ}
25623 "});
25624 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25625 cx.assert_editor_state(indoc! {"
25626 function main() {
25627 ˇfor item in $items; do
25628 ˇwhile [ -n \"$item\" ]; do
25629 ˇif [ \"$value\" -gt 10 ]; then
25630 ˇcontinue
25631 ˇelif [ \"$value\" -lt 0 ]; then
25632 ˇbreak
25633 ˇelse
25634 ˇecho \"$item\"
25635 ˇfi
25636 ˇdone
25637 ˇdone
25638 ˇ}
25639 "});
25640 // test relative indent is preserved when tab
25641 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25642 cx.assert_editor_state(indoc! {"
25643 function main() {
25644 ˇfor item in $items; do
25645 ˇwhile [ -n \"$item\" ]; do
25646 ˇif [ \"$value\" -gt 10 ]; then
25647 ˇcontinue
25648 ˇelif [ \"$value\" -lt 0 ]; then
25649 ˇbreak
25650 ˇelse
25651 ˇecho \"$item\"
25652 ˇfi
25653 ˇdone
25654 ˇdone
25655 ˇ}
25656 "});
25657
25658 // test cursor move to start of each line on tab
25659 // for `case` statement with patterns
25660 cx.set_state(indoc! {"
25661 function handle() {
25662 ˇ case \"$1\" in
25663 ˇ start)
25664 ˇ echo \"a\"
25665 ˇ ;;
25666 ˇ stop)
25667 ˇ echo \"b\"
25668 ˇ ;;
25669 ˇ *)
25670 ˇ echo \"c\"
25671 ˇ ;;
25672 ˇ esac
25673 ˇ}
25674 "});
25675 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25676 cx.assert_editor_state(indoc! {"
25677 function handle() {
25678 ˇcase \"$1\" in
25679 ˇstart)
25680 ˇecho \"a\"
25681 ˇ;;
25682 ˇstop)
25683 ˇecho \"b\"
25684 ˇ;;
25685 ˇ*)
25686 ˇecho \"c\"
25687 ˇ;;
25688 ˇesac
25689 ˇ}
25690 "});
25691}
25692
25693#[gpui::test]
25694async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25695 init_test(cx, |_| {});
25696
25697 let mut cx = EditorTestContext::new(cx).await;
25698 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25699 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25700
25701 // test indents on comment insert
25702 cx.set_state(indoc! {"
25703 function main() {
25704 ˇ for item in $items; do
25705 ˇ while [ -n \"$item\" ]; do
25706 ˇ if [ \"$value\" -gt 10 ]; then
25707 ˇ continue
25708 ˇ elif [ \"$value\" -lt 0 ]; then
25709 ˇ break
25710 ˇ else
25711 ˇ echo \"$item\"
25712 ˇ fi
25713 ˇ done
25714 ˇ done
25715 ˇ}
25716 "});
25717 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25718 cx.assert_editor_state(indoc! {"
25719 function main() {
25720 #ˇ for item in $items; do
25721 #ˇ while [ -n \"$item\" ]; do
25722 #ˇ if [ \"$value\" -gt 10 ]; then
25723 #ˇ continue
25724 #ˇ elif [ \"$value\" -lt 0 ]; then
25725 #ˇ break
25726 #ˇ else
25727 #ˇ echo \"$item\"
25728 #ˇ fi
25729 #ˇ done
25730 #ˇ done
25731 #ˇ}
25732 "});
25733}
25734
25735#[gpui::test]
25736async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25737 init_test(cx, |_| {});
25738
25739 let mut cx = EditorTestContext::new(cx).await;
25740 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25741 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25742
25743 // test `else` auto outdents when typed inside `if` block
25744 cx.set_state(indoc! {"
25745 if [ \"$1\" = \"test\" ]; then
25746 echo \"foo bar\"
25747 ˇ
25748 "});
25749 cx.update_editor(|editor, window, cx| {
25750 editor.handle_input("else", window, cx);
25751 });
25752 cx.assert_editor_state(indoc! {"
25753 if [ \"$1\" = \"test\" ]; then
25754 echo \"foo bar\"
25755 elseˇ
25756 "});
25757
25758 // test `elif` auto outdents when typed inside `if` block
25759 cx.set_state(indoc! {"
25760 if [ \"$1\" = \"test\" ]; then
25761 echo \"foo bar\"
25762 ˇ
25763 "});
25764 cx.update_editor(|editor, window, cx| {
25765 editor.handle_input("elif", window, cx);
25766 });
25767 cx.assert_editor_state(indoc! {"
25768 if [ \"$1\" = \"test\" ]; then
25769 echo \"foo bar\"
25770 elifˇ
25771 "});
25772
25773 // test `fi` auto outdents when typed inside `else` block
25774 cx.set_state(indoc! {"
25775 if [ \"$1\" = \"test\" ]; then
25776 echo \"foo bar\"
25777 else
25778 echo \"bar baz\"
25779 ˇ
25780 "});
25781 cx.update_editor(|editor, window, cx| {
25782 editor.handle_input("fi", window, cx);
25783 });
25784 cx.assert_editor_state(indoc! {"
25785 if [ \"$1\" = \"test\" ]; then
25786 echo \"foo bar\"
25787 else
25788 echo \"bar baz\"
25789 fiˇ
25790 "});
25791
25792 // test `done` auto outdents when typed inside `while` block
25793 cx.set_state(indoc! {"
25794 while read line; do
25795 echo \"$line\"
25796 ˇ
25797 "});
25798 cx.update_editor(|editor, window, cx| {
25799 editor.handle_input("done", window, cx);
25800 });
25801 cx.assert_editor_state(indoc! {"
25802 while read line; do
25803 echo \"$line\"
25804 doneˇ
25805 "});
25806
25807 // test `done` auto outdents when typed inside `for` block
25808 cx.set_state(indoc! {"
25809 for file in *.txt; do
25810 cat \"$file\"
25811 ˇ
25812 "});
25813 cx.update_editor(|editor, window, cx| {
25814 editor.handle_input("done", window, cx);
25815 });
25816 cx.assert_editor_state(indoc! {"
25817 for file in *.txt; do
25818 cat \"$file\"
25819 doneˇ
25820 "});
25821
25822 // test `esac` auto outdents when typed inside `case` block
25823 cx.set_state(indoc! {"
25824 case \"$1\" in
25825 start)
25826 echo \"foo bar\"
25827 ;;
25828 stop)
25829 echo \"bar baz\"
25830 ;;
25831 ˇ
25832 "});
25833 cx.update_editor(|editor, window, cx| {
25834 editor.handle_input("esac", window, cx);
25835 });
25836 cx.assert_editor_state(indoc! {"
25837 case \"$1\" in
25838 start)
25839 echo \"foo bar\"
25840 ;;
25841 stop)
25842 echo \"bar baz\"
25843 ;;
25844 esacˇ
25845 "});
25846
25847 // test `*)` auto outdents when typed inside `case` block
25848 cx.set_state(indoc! {"
25849 case \"$1\" in
25850 start)
25851 echo \"foo bar\"
25852 ;;
25853 ˇ
25854 "});
25855 cx.update_editor(|editor, window, cx| {
25856 editor.handle_input("*)", window, cx);
25857 });
25858 cx.assert_editor_state(indoc! {"
25859 case \"$1\" in
25860 start)
25861 echo \"foo bar\"
25862 ;;
25863 *)ˇ
25864 "});
25865
25866 // test `fi` outdents to correct level with nested if blocks
25867 cx.set_state(indoc! {"
25868 if [ \"$1\" = \"test\" ]; then
25869 echo \"outer if\"
25870 if [ \"$2\" = \"debug\" ]; then
25871 echo \"inner if\"
25872 ˇ
25873 "});
25874 cx.update_editor(|editor, window, cx| {
25875 editor.handle_input("fi", window, cx);
25876 });
25877 cx.assert_editor_state(indoc! {"
25878 if [ \"$1\" = \"test\" ]; then
25879 echo \"outer if\"
25880 if [ \"$2\" = \"debug\" ]; then
25881 echo \"inner if\"
25882 fiˇ
25883 "});
25884}
25885
25886#[gpui::test]
25887async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25888 init_test(cx, |_| {});
25889 update_test_language_settings(cx, |settings| {
25890 settings.defaults.extend_comment_on_newline = Some(false);
25891 });
25892 let mut cx = EditorTestContext::new(cx).await;
25893 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25894 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25895
25896 // test correct indent after newline on comment
25897 cx.set_state(indoc! {"
25898 # COMMENT:ˇ
25899 "});
25900 cx.update_editor(|editor, window, cx| {
25901 editor.newline(&Newline, window, cx);
25902 });
25903 cx.assert_editor_state(indoc! {"
25904 # COMMENT:
25905 ˇ
25906 "});
25907
25908 // test correct indent after newline after `then`
25909 cx.set_state(indoc! {"
25910
25911 if [ \"$1\" = \"test\" ]; thenˇ
25912 "});
25913 cx.update_editor(|editor, window, cx| {
25914 editor.newline(&Newline, window, cx);
25915 });
25916 cx.run_until_parked();
25917 cx.assert_editor_state(indoc! {"
25918
25919 if [ \"$1\" = \"test\" ]; then
25920 ˇ
25921 "});
25922
25923 // test correct indent after newline after `else`
25924 cx.set_state(indoc! {"
25925 if [ \"$1\" = \"test\" ]; then
25926 elseˇ
25927 "});
25928 cx.update_editor(|editor, window, cx| {
25929 editor.newline(&Newline, window, cx);
25930 });
25931 cx.run_until_parked();
25932 cx.assert_editor_state(indoc! {"
25933 if [ \"$1\" = \"test\" ]; then
25934 else
25935 ˇ
25936 "});
25937
25938 // test correct indent after newline after `elif`
25939 cx.set_state(indoc! {"
25940 if [ \"$1\" = \"test\" ]; then
25941 elifˇ
25942 "});
25943 cx.update_editor(|editor, window, cx| {
25944 editor.newline(&Newline, window, cx);
25945 });
25946 cx.run_until_parked();
25947 cx.assert_editor_state(indoc! {"
25948 if [ \"$1\" = \"test\" ]; then
25949 elif
25950 ˇ
25951 "});
25952
25953 // test correct indent after newline after `do`
25954 cx.set_state(indoc! {"
25955 for file in *.txt; doˇ
25956 "});
25957 cx.update_editor(|editor, window, cx| {
25958 editor.newline(&Newline, window, cx);
25959 });
25960 cx.run_until_parked();
25961 cx.assert_editor_state(indoc! {"
25962 for file in *.txt; do
25963 ˇ
25964 "});
25965
25966 // test correct indent after newline after case pattern
25967 cx.set_state(indoc! {"
25968 case \"$1\" in
25969 start)ˇ
25970 "});
25971 cx.update_editor(|editor, window, cx| {
25972 editor.newline(&Newline, window, cx);
25973 });
25974 cx.run_until_parked();
25975 cx.assert_editor_state(indoc! {"
25976 case \"$1\" in
25977 start)
25978 ˇ
25979 "});
25980
25981 // test correct indent after newline after case pattern
25982 cx.set_state(indoc! {"
25983 case \"$1\" in
25984 start)
25985 ;;
25986 *)ˇ
25987 "});
25988 cx.update_editor(|editor, window, cx| {
25989 editor.newline(&Newline, window, cx);
25990 });
25991 cx.run_until_parked();
25992 cx.assert_editor_state(indoc! {"
25993 case \"$1\" in
25994 start)
25995 ;;
25996 *)
25997 ˇ
25998 "});
25999
26000 // test correct indent after newline after function opening brace
26001 cx.set_state(indoc! {"
26002 function test() {ˇ}
26003 "});
26004 cx.update_editor(|editor, window, cx| {
26005 editor.newline(&Newline, window, cx);
26006 });
26007 cx.run_until_parked();
26008 cx.assert_editor_state(indoc! {"
26009 function test() {
26010 ˇ
26011 }
26012 "});
26013
26014 // test no extra indent after semicolon on same line
26015 cx.set_state(indoc! {"
26016 echo \"test\";ˇ
26017 "});
26018 cx.update_editor(|editor, window, cx| {
26019 editor.newline(&Newline, window, cx);
26020 });
26021 cx.run_until_parked();
26022 cx.assert_editor_state(indoc! {"
26023 echo \"test\";
26024 ˇ
26025 "});
26026}
26027
26028fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26029 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26030 point..point
26031}
26032
26033#[track_caller]
26034fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26035 let (text, ranges) = marked_text_ranges(marked_text, true);
26036 assert_eq!(editor.text(cx), text);
26037 assert_eq!(
26038 editor.selections.ranges(&editor.display_snapshot(cx)),
26039 ranges
26040 .iter()
26041 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26042 .collect::<Vec<_>>(),
26043 "Assert selections are {}",
26044 marked_text
26045 );
26046}
26047
26048pub fn handle_signature_help_request(
26049 cx: &mut EditorLspTestContext,
26050 mocked_response: lsp::SignatureHelp,
26051) -> impl Future<Output = ()> + use<> {
26052 let mut request =
26053 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26054 let mocked_response = mocked_response.clone();
26055 async move { Ok(Some(mocked_response)) }
26056 });
26057
26058 async move {
26059 request.next().await;
26060 }
26061}
26062
26063#[track_caller]
26064pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26065 cx.update_editor(|editor, _, _| {
26066 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26067 let entries = menu.entries.borrow();
26068 let entries = entries
26069 .iter()
26070 .map(|entry| entry.string.as_str())
26071 .collect::<Vec<_>>();
26072 assert_eq!(entries, expected);
26073 } else {
26074 panic!("Expected completions menu");
26075 }
26076 });
26077}
26078
26079#[gpui::test]
26080async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26081 init_test(cx, |_| {});
26082 let mut cx = EditorLspTestContext::new_rust(
26083 lsp::ServerCapabilities {
26084 completion_provider: Some(lsp::CompletionOptions {
26085 ..Default::default()
26086 }),
26087 ..Default::default()
26088 },
26089 cx,
26090 )
26091 .await;
26092 cx.lsp
26093 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26094 Ok(Some(lsp::CompletionResponse::Array(vec![
26095 lsp::CompletionItem {
26096 label: "unsafe".into(),
26097 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26098 range: lsp::Range {
26099 start: lsp::Position {
26100 line: 0,
26101 character: 9,
26102 },
26103 end: lsp::Position {
26104 line: 0,
26105 character: 11,
26106 },
26107 },
26108 new_text: "unsafe".to_string(),
26109 })),
26110 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26111 ..Default::default()
26112 },
26113 ])))
26114 });
26115
26116 cx.update_editor(|editor, _, cx| {
26117 editor.project().unwrap().update(cx, |project, cx| {
26118 project.snippets().update(cx, |snippets, _cx| {
26119 snippets.add_snippet_for_test(
26120 None,
26121 PathBuf::from("test_snippets.json"),
26122 vec![
26123 Arc::new(project::snippet_provider::Snippet {
26124 prefix: vec![
26125 "unlimited word count".to_string(),
26126 "unlimit word count".to_string(),
26127 "unlimited unknown".to_string(),
26128 ],
26129 body: "this is many words".to_string(),
26130 description: Some("description".to_string()),
26131 name: "multi-word snippet test".to_string(),
26132 }),
26133 Arc::new(project::snippet_provider::Snippet {
26134 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26135 body: "fewer words".to_string(),
26136 description: Some("alt description".to_string()),
26137 name: "other name".to_string(),
26138 }),
26139 Arc::new(project::snippet_provider::Snippet {
26140 prefix: vec!["ab aa".to_string()],
26141 body: "abcd".to_string(),
26142 description: None,
26143 name: "alphabet".to_string(),
26144 }),
26145 ],
26146 );
26147 });
26148 })
26149 });
26150
26151 let get_completions = |cx: &mut EditorLspTestContext| {
26152 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26153 Some(CodeContextMenu::Completions(context_menu)) => {
26154 let entries = context_menu.entries.borrow();
26155 entries
26156 .iter()
26157 .map(|entry| entry.string.clone())
26158 .collect_vec()
26159 }
26160 _ => vec![],
26161 })
26162 };
26163
26164 // snippets:
26165 // @foo
26166 // foo bar
26167 //
26168 // when typing:
26169 //
26170 // when typing:
26171 // - if I type a symbol "open the completions with snippets only"
26172 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26173 //
26174 // stuff we need:
26175 // - filtering logic change?
26176 // - remember how far back the completion started.
26177
26178 let test_cases: &[(&str, &[&str])] = &[
26179 (
26180 "un",
26181 &[
26182 "unsafe",
26183 "unlimit word count",
26184 "unlimited unknown",
26185 "unlimited word count",
26186 "unsnip",
26187 ],
26188 ),
26189 (
26190 "u ",
26191 &[
26192 "unlimit word count",
26193 "unlimited unknown",
26194 "unlimited word count",
26195 ],
26196 ),
26197 ("u a", &["ab aa", "unsafe"]), // unsAfe
26198 (
26199 "u u",
26200 &[
26201 "unsafe",
26202 "unlimit word count",
26203 "unlimited unknown", // ranked highest among snippets
26204 "unlimited word count",
26205 "unsnip",
26206 ],
26207 ),
26208 ("uw c", &["unlimit word count", "unlimited word count"]),
26209 (
26210 "u w",
26211 &[
26212 "unlimit word count",
26213 "unlimited word count",
26214 "unlimited unknown",
26215 ],
26216 ),
26217 ("u w ", &["unlimit word count", "unlimited word count"]),
26218 (
26219 "u ",
26220 &[
26221 "unlimit word count",
26222 "unlimited unknown",
26223 "unlimited word count",
26224 ],
26225 ),
26226 ("wor", &[]),
26227 ("uf", &["unsafe"]),
26228 ("af", &["unsafe"]),
26229 ("afu", &[]),
26230 (
26231 "ue",
26232 &["unsafe", "unlimited unknown", "unlimited word count"],
26233 ),
26234 ("@", &["@few"]),
26235 ("@few", &["@few"]),
26236 ("@ ", &[]),
26237 ("a@", &["@few"]),
26238 ("a@f", &["@few", "unsafe"]),
26239 ("a@fw", &["@few"]),
26240 ("a", &["ab aa", "unsafe"]),
26241 ("aa", &["ab aa"]),
26242 ("aaa", &["ab aa"]),
26243 ("ab", &["ab aa"]),
26244 ("ab ", &["ab aa"]),
26245 ("ab a", &["ab aa", "unsafe"]),
26246 ("ab ab", &["ab aa"]),
26247 ("ab ab aa", &["ab aa"]),
26248 ];
26249
26250 for &(input_to_simulate, expected_completions) in test_cases {
26251 cx.set_state("fn a() { ˇ }\n");
26252 for c in input_to_simulate.split("") {
26253 cx.simulate_input(c);
26254 cx.run_until_parked();
26255 }
26256 let expected_completions = expected_completions
26257 .iter()
26258 .map(|s| s.to_string())
26259 .collect_vec();
26260 assert_eq!(
26261 get_completions(&mut cx),
26262 expected_completions,
26263 "< actual / expected >, input = {input_to_simulate:?}",
26264 );
26265 }
26266}
26267
26268/// Handle completion request passing a marked string specifying where the completion
26269/// should be triggered from using '|' character, what range should be replaced, and what completions
26270/// should be returned using '<' and '>' to delimit the range.
26271///
26272/// Also see `handle_completion_request_with_insert_and_replace`.
26273#[track_caller]
26274pub fn handle_completion_request(
26275 marked_string: &str,
26276 completions: Vec<&'static str>,
26277 is_incomplete: bool,
26278 counter: Arc<AtomicUsize>,
26279 cx: &mut EditorLspTestContext,
26280) -> impl Future<Output = ()> {
26281 let complete_from_marker: TextRangeMarker = '|'.into();
26282 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26283 let (_, mut marked_ranges) = marked_text_ranges_by(
26284 marked_string,
26285 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26286 );
26287
26288 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26289 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26290 ));
26291 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26292 let replace_range =
26293 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26294
26295 let mut request =
26296 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26297 let completions = completions.clone();
26298 counter.fetch_add(1, atomic::Ordering::Release);
26299 async move {
26300 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26301 assert_eq!(
26302 params.text_document_position.position,
26303 complete_from_position
26304 );
26305 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26306 is_incomplete,
26307 item_defaults: None,
26308 items: completions
26309 .iter()
26310 .map(|completion_text| lsp::CompletionItem {
26311 label: completion_text.to_string(),
26312 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26313 range: replace_range,
26314 new_text: completion_text.to_string(),
26315 })),
26316 ..Default::default()
26317 })
26318 .collect(),
26319 })))
26320 }
26321 });
26322
26323 async move {
26324 request.next().await;
26325 }
26326}
26327
26328/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26329/// given instead, which also contains an `insert` range.
26330///
26331/// This function uses markers to define ranges:
26332/// - `|` marks the cursor position
26333/// - `<>` marks the replace range
26334/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26335pub fn handle_completion_request_with_insert_and_replace(
26336 cx: &mut EditorLspTestContext,
26337 marked_string: &str,
26338 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26339 counter: Arc<AtomicUsize>,
26340) -> impl Future<Output = ()> {
26341 let complete_from_marker: TextRangeMarker = '|'.into();
26342 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26343 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26344
26345 let (_, mut marked_ranges) = marked_text_ranges_by(
26346 marked_string,
26347 vec![
26348 complete_from_marker.clone(),
26349 replace_range_marker.clone(),
26350 insert_range_marker.clone(),
26351 ],
26352 );
26353
26354 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26355 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26356 ));
26357 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26358 let replace_range =
26359 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26360
26361 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26362 Some(ranges) if !ranges.is_empty() => {
26363 let range1 = ranges[0].clone();
26364 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26365 }
26366 _ => lsp::Range {
26367 start: replace_range.start,
26368 end: complete_from_position,
26369 },
26370 };
26371
26372 let mut request =
26373 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26374 let completions = completions.clone();
26375 counter.fetch_add(1, atomic::Ordering::Release);
26376 async move {
26377 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26378 assert_eq!(
26379 params.text_document_position.position, complete_from_position,
26380 "marker `|` position doesn't match",
26381 );
26382 Ok(Some(lsp::CompletionResponse::Array(
26383 completions
26384 .iter()
26385 .map(|(label, new_text)| lsp::CompletionItem {
26386 label: label.to_string(),
26387 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26388 lsp::InsertReplaceEdit {
26389 insert: insert_range,
26390 replace: replace_range,
26391 new_text: new_text.to_string(),
26392 },
26393 )),
26394 ..Default::default()
26395 })
26396 .collect(),
26397 )))
26398 }
26399 });
26400
26401 async move {
26402 request.next().await;
26403 }
26404}
26405
26406fn handle_resolve_completion_request(
26407 cx: &mut EditorLspTestContext,
26408 edits: Option<Vec<(&'static str, &'static str)>>,
26409) -> impl Future<Output = ()> {
26410 let edits = edits.map(|edits| {
26411 edits
26412 .iter()
26413 .map(|(marked_string, new_text)| {
26414 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26415 let replace_range = cx.to_lsp_range(
26416 MultiBufferOffset(marked_ranges[0].start)
26417 ..MultiBufferOffset(marked_ranges[0].end),
26418 );
26419 lsp::TextEdit::new(replace_range, new_text.to_string())
26420 })
26421 .collect::<Vec<_>>()
26422 });
26423
26424 let mut request =
26425 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26426 let edits = edits.clone();
26427 async move {
26428 Ok(lsp::CompletionItem {
26429 additional_text_edits: edits,
26430 ..Default::default()
26431 })
26432 }
26433 });
26434
26435 async move {
26436 request.next().await;
26437 }
26438}
26439
26440pub(crate) fn update_test_language_settings(
26441 cx: &mut TestAppContext,
26442 f: impl Fn(&mut AllLanguageSettingsContent),
26443) {
26444 cx.update(|cx| {
26445 SettingsStore::update_global(cx, |store, cx| {
26446 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26447 });
26448 });
26449}
26450
26451pub(crate) fn update_test_project_settings(
26452 cx: &mut TestAppContext,
26453 f: impl Fn(&mut ProjectSettingsContent),
26454) {
26455 cx.update(|cx| {
26456 SettingsStore::update_global(cx, |store, cx| {
26457 store.update_user_settings(cx, |settings| f(&mut settings.project));
26458 });
26459 });
26460}
26461
26462pub(crate) fn update_test_editor_settings(
26463 cx: &mut TestAppContext,
26464 f: impl Fn(&mut EditorSettingsContent),
26465) {
26466 cx.update(|cx| {
26467 SettingsStore::update_global(cx, |store, cx| {
26468 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26469 })
26470 })
26471}
26472
26473pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26474 cx.update(|cx| {
26475 assets::Assets.load_test_fonts(cx);
26476 let store = SettingsStore::test(cx);
26477 cx.set_global(store);
26478 theme::init(theme::LoadThemes::JustBase, cx);
26479 release_channel::init(semver::Version::new(0, 0, 0), cx);
26480 crate::init(cx);
26481 });
26482 zlog::init_test();
26483 update_test_language_settings(cx, f);
26484}
26485
26486#[track_caller]
26487fn assert_hunk_revert(
26488 not_reverted_text_with_selections: &str,
26489 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26490 expected_reverted_text_with_selections: &str,
26491 base_text: &str,
26492 cx: &mut EditorLspTestContext,
26493) {
26494 cx.set_state(not_reverted_text_with_selections);
26495 cx.set_head_text(base_text);
26496 cx.executor().run_until_parked();
26497
26498 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26499 let snapshot = editor.snapshot(window, cx);
26500 let reverted_hunk_statuses = snapshot
26501 .buffer_snapshot()
26502 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26503 .map(|hunk| hunk.status().kind)
26504 .collect::<Vec<_>>();
26505
26506 editor.git_restore(&Default::default(), window, cx);
26507 reverted_hunk_statuses
26508 });
26509 cx.executor().run_until_parked();
26510 cx.assert_editor_state(expected_reverted_text_with_selections);
26511 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26512}
26513
26514#[gpui::test(iterations = 10)]
26515async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26516 init_test(cx, |_| {});
26517
26518 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26519 let counter = diagnostic_requests.clone();
26520
26521 let fs = FakeFs::new(cx.executor());
26522 fs.insert_tree(
26523 path!("/a"),
26524 json!({
26525 "first.rs": "fn main() { let a = 5; }",
26526 "second.rs": "// Test file",
26527 }),
26528 )
26529 .await;
26530
26531 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26532 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26533 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26534
26535 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26536 language_registry.add(rust_lang());
26537 let mut fake_servers = language_registry.register_fake_lsp(
26538 "Rust",
26539 FakeLspAdapter {
26540 capabilities: lsp::ServerCapabilities {
26541 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26542 lsp::DiagnosticOptions {
26543 identifier: None,
26544 inter_file_dependencies: true,
26545 workspace_diagnostics: true,
26546 work_done_progress_options: Default::default(),
26547 },
26548 )),
26549 ..Default::default()
26550 },
26551 ..Default::default()
26552 },
26553 );
26554
26555 let editor = workspace
26556 .update(cx, |workspace, window, cx| {
26557 workspace.open_abs_path(
26558 PathBuf::from(path!("/a/first.rs")),
26559 OpenOptions::default(),
26560 window,
26561 cx,
26562 )
26563 })
26564 .unwrap()
26565 .await
26566 .unwrap()
26567 .downcast::<Editor>()
26568 .unwrap();
26569 let fake_server = fake_servers.next().await.unwrap();
26570 let server_id = fake_server.server.server_id();
26571 let mut first_request = fake_server
26572 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26573 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26574 let result_id = Some(new_result_id.to_string());
26575 assert_eq!(
26576 params.text_document.uri,
26577 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26578 );
26579 async move {
26580 Ok(lsp::DocumentDiagnosticReportResult::Report(
26581 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26582 related_documents: None,
26583 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26584 items: Vec::new(),
26585 result_id,
26586 },
26587 }),
26588 ))
26589 }
26590 });
26591
26592 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
26593 project.update(cx, |project, cx| {
26594 let buffer_id = editor
26595 .read(cx)
26596 .buffer()
26597 .read(cx)
26598 .as_singleton()
26599 .expect("created a singleton buffer")
26600 .read(cx)
26601 .remote_id();
26602 let buffer_result_id = project
26603 .lsp_store()
26604 .read(cx)
26605 .result_id(server_id, buffer_id, cx);
26606 assert_eq!(expected, buffer_result_id);
26607 });
26608 };
26609
26610 ensure_result_id(None, cx);
26611 cx.executor().advance_clock(Duration::from_millis(60));
26612 cx.executor().run_until_parked();
26613 assert_eq!(
26614 diagnostic_requests.load(atomic::Ordering::Acquire),
26615 1,
26616 "Opening file should trigger diagnostic request"
26617 );
26618 first_request
26619 .next()
26620 .await
26621 .expect("should have sent the first diagnostics pull request");
26622 ensure_result_id(Some("1".to_string()), cx);
26623
26624 // Editing should trigger diagnostics
26625 editor.update_in(cx, |editor, window, cx| {
26626 editor.handle_input("2", window, cx)
26627 });
26628 cx.executor().advance_clock(Duration::from_millis(60));
26629 cx.executor().run_until_parked();
26630 assert_eq!(
26631 diagnostic_requests.load(atomic::Ordering::Acquire),
26632 2,
26633 "Editing should trigger diagnostic request"
26634 );
26635 ensure_result_id(Some("2".to_string()), cx);
26636
26637 // Moving cursor should not trigger diagnostic request
26638 editor.update_in(cx, |editor, window, cx| {
26639 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26640 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26641 });
26642 });
26643 cx.executor().advance_clock(Duration::from_millis(60));
26644 cx.executor().run_until_parked();
26645 assert_eq!(
26646 diagnostic_requests.load(atomic::Ordering::Acquire),
26647 2,
26648 "Cursor movement should not trigger diagnostic request"
26649 );
26650 ensure_result_id(Some("2".to_string()), cx);
26651 // Multiple rapid edits should be debounced
26652 for _ in 0..5 {
26653 editor.update_in(cx, |editor, window, cx| {
26654 editor.handle_input("x", window, cx)
26655 });
26656 }
26657 cx.executor().advance_clock(Duration::from_millis(60));
26658 cx.executor().run_until_parked();
26659
26660 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26661 assert!(
26662 final_requests <= 4,
26663 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26664 );
26665 ensure_result_id(Some(final_requests.to_string()), cx);
26666}
26667
26668#[gpui::test]
26669async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26670 // Regression test for issue #11671
26671 // Previously, adding a cursor after moving multiple cursors would reset
26672 // the cursor count instead of adding to the existing cursors.
26673 init_test(cx, |_| {});
26674 let mut cx = EditorTestContext::new(cx).await;
26675
26676 // Create a simple buffer with cursor at start
26677 cx.set_state(indoc! {"
26678 ˇaaaa
26679 bbbb
26680 cccc
26681 dddd
26682 eeee
26683 ffff
26684 gggg
26685 hhhh"});
26686
26687 // Add 2 cursors below (so we have 3 total)
26688 cx.update_editor(|editor, window, cx| {
26689 editor.add_selection_below(&Default::default(), window, cx);
26690 editor.add_selection_below(&Default::default(), window, cx);
26691 });
26692
26693 // Verify we have 3 cursors
26694 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26695 assert_eq!(
26696 initial_count, 3,
26697 "Should have 3 cursors after adding 2 below"
26698 );
26699
26700 // Move down one line
26701 cx.update_editor(|editor, window, cx| {
26702 editor.move_down(&MoveDown, window, cx);
26703 });
26704
26705 // Add another cursor below
26706 cx.update_editor(|editor, window, cx| {
26707 editor.add_selection_below(&Default::default(), window, cx);
26708 });
26709
26710 // Should now have 4 cursors (3 original + 1 new)
26711 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26712 assert_eq!(
26713 final_count, 4,
26714 "Should have 4 cursors after moving and adding another"
26715 );
26716}
26717
26718#[gpui::test]
26719async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26720 init_test(cx, |_| {});
26721
26722 let mut cx = EditorTestContext::new(cx).await;
26723
26724 cx.set_state(indoc!(
26725 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26726 Second line here"#
26727 ));
26728
26729 cx.update_editor(|editor, window, cx| {
26730 // Enable soft wrapping with a narrow width to force soft wrapping and
26731 // confirm that more than 2 rows are being displayed.
26732 editor.set_wrap_width(Some(100.0.into()), cx);
26733 assert!(editor.display_text(cx).lines().count() > 2);
26734
26735 editor.add_selection_below(
26736 &AddSelectionBelow {
26737 skip_soft_wrap: true,
26738 },
26739 window,
26740 cx,
26741 );
26742
26743 assert_eq!(
26744 display_ranges(editor, cx),
26745 &[
26746 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26747 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26748 ]
26749 );
26750
26751 editor.add_selection_above(
26752 &AddSelectionAbove {
26753 skip_soft_wrap: true,
26754 },
26755 window,
26756 cx,
26757 );
26758
26759 assert_eq!(
26760 display_ranges(editor, cx),
26761 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26762 );
26763
26764 editor.add_selection_below(
26765 &AddSelectionBelow {
26766 skip_soft_wrap: false,
26767 },
26768 window,
26769 cx,
26770 );
26771
26772 assert_eq!(
26773 display_ranges(editor, cx),
26774 &[
26775 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26776 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26777 ]
26778 );
26779
26780 editor.add_selection_above(
26781 &AddSelectionAbove {
26782 skip_soft_wrap: false,
26783 },
26784 window,
26785 cx,
26786 );
26787
26788 assert_eq!(
26789 display_ranges(editor, cx),
26790 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26791 );
26792 });
26793}
26794
26795#[gpui::test(iterations = 10)]
26796async fn test_document_colors(cx: &mut TestAppContext) {
26797 let expected_color = Rgba {
26798 r: 0.33,
26799 g: 0.33,
26800 b: 0.33,
26801 a: 0.33,
26802 };
26803
26804 init_test(cx, |_| {});
26805
26806 let fs = FakeFs::new(cx.executor());
26807 fs.insert_tree(
26808 path!("/a"),
26809 json!({
26810 "first.rs": "fn main() { let a = 5; }",
26811 }),
26812 )
26813 .await;
26814
26815 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26816 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26817 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26818
26819 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26820 language_registry.add(rust_lang());
26821 let mut fake_servers = language_registry.register_fake_lsp(
26822 "Rust",
26823 FakeLspAdapter {
26824 capabilities: lsp::ServerCapabilities {
26825 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26826 ..lsp::ServerCapabilities::default()
26827 },
26828 name: "rust-analyzer",
26829 ..FakeLspAdapter::default()
26830 },
26831 );
26832 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26833 "Rust",
26834 FakeLspAdapter {
26835 capabilities: lsp::ServerCapabilities {
26836 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26837 ..lsp::ServerCapabilities::default()
26838 },
26839 name: "not-rust-analyzer",
26840 ..FakeLspAdapter::default()
26841 },
26842 );
26843
26844 let editor = workspace
26845 .update(cx, |workspace, window, cx| {
26846 workspace.open_abs_path(
26847 PathBuf::from(path!("/a/first.rs")),
26848 OpenOptions::default(),
26849 window,
26850 cx,
26851 )
26852 })
26853 .unwrap()
26854 .await
26855 .unwrap()
26856 .downcast::<Editor>()
26857 .unwrap();
26858 let fake_language_server = fake_servers.next().await.unwrap();
26859 let fake_language_server_without_capabilities =
26860 fake_servers_without_capabilities.next().await.unwrap();
26861 let requests_made = Arc::new(AtomicUsize::new(0));
26862 let closure_requests_made = Arc::clone(&requests_made);
26863 let mut color_request_handle = fake_language_server
26864 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26865 let requests_made = Arc::clone(&closure_requests_made);
26866 async move {
26867 assert_eq!(
26868 params.text_document.uri,
26869 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26870 );
26871 requests_made.fetch_add(1, atomic::Ordering::Release);
26872 Ok(vec![
26873 lsp::ColorInformation {
26874 range: lsp::Range {
26875 start: lsp::Position {
26876 line: 0,
26877 character: 0,
26878 },
26879 end: lsp::Position {
26880 line: 0,
26881 character: 1,
26882 },
26883 },
26884 color: lsp::Color {
26885 red: 0.33,
26886 green: 0.33,
26887 blue: 0.33,
26888 alpha: 0.33,
26889 },
26890 },
26891 lsp::ColorInformation {
26892 range: lsp::Range {
26893 start: lsp::Position {
26894 line: 0,
26895 character: 0,
26896 },
26897 end: lsp::Position {
26898 line: 0,
26899 character: 1,
26900 },
26901 },
26902 color: lsp::Color {
26903 red: 0.33,
26904 green: 0.33,
26905 blue: 0.33,
26906 alpha: 0.33,
26907 },
26908 },
26909 ])
26910 }
26911 });
26912
26913 let _handle = fake_language_server_without_capabilities
26914 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26915 panic!("Should not be called");
26916 });
26917 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26918 color_request_handle.next().await.unwrap();
26919 cx.run_until_parked();
26920 assert_eq!(
26921 1,
26922 requests_made.load(atomic::Ordering::Acquire),
26923 "Should query for colors once per editor open"
26924 );
26925 editor.update_in(cx, |editor, _, cx| {
26926 assert_eq!(
26927 vec![expected_color],
26928 extract_color_inlays(editor, cx),
26929 "Should have an initial inlay"
26930 );
26931 });
26932
26933 // opening another file in a split should not influence the LSP query counter
26934 workspace
26935 .update(cx, |workspace, window, cx| {
26936 assert_eq!(
26937 workspace.panes().len(),
26938 1,
26939 "Should have one pane with one editor"
26940 );
26941 workspace.move_item_to_pane_in_direction(
26942 &MoveItemToPaneInDirection {
26943 direction: SplitDirection::Right,
26944 focus: false,
26945 clone: true,
26946 },
26947 window,
26948 cx,
26949 );
26950 })
26951 .unwrap();
26952 cx.run_until_parked();
26953 workspace
26954 .update(cx, |workspace, _, cx| {
26955 let panes = workspace.panes();
26956 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26957 for pane in panes {
26958 let editor = pane
26959 .read(cx)
26960 .active_item()
26961 .and_then(|item| item.downcast::<Editor>())
26962 .expect("Should have opened an editor in each split");
26963 let editor_file = editor
26964 .read(cx)
26965 .buffer()
26966 .read(cx)
26967 .as_singleton()
26968 .expect("test deals with singleton buffers")
26969 .read(cx)
26970 .file()
26971 .expect("test buffese should have a file")
26972 .path();
26973 assert_eq!(
26974 editor_file.as_ref(),
26975 rel_path("first.rs"),
26976 "Both editors should be opened for the same file"
26977 )
26978 }
26979 })
26980 .unwrap();
26981
26982 cx.executor().advance_clock(Duration::from_millis(500));
26983 let save = editor.update_in(cx, |editor, window, cx| {
26984 editor.move_to_end(&MoveToEnd, window, cx);
26985 editor.handle_input("dirty", window, cx);
26986 editor.save(
26987 SaveOptions {
26988 format: true,
26989 autosave: true,
26990 },
26991 project.clone(),
26992 window,
26993 cx,
26994 )
26995 });
26996 save.await.unwrap();
26997
26998 color_request_handle.next().await.unwrap();
26999 cx.run_until_parked();
27000 assert_eq!(
27001 2,
27002 requests_made.load(atomic::Ordering::Acquire),
27003 "Should query for colors once per save (deduplicated) and once per formatting after save"
27004 );
27005
27006 drop(editor);
27007 let close = workspace
27008 .update(cx, |workspace, window, cx| {
27009 workspace.active_pane().update(cx, |pane, cx| {
27010 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27011 })
27012 })
27013 .unwrap();
27014 close.await.unwrap();
27015 let close = workspace
27016 .update(cx, |workspace, window, cx| {
27017 workspace.active_pane().update(cx, |pane, cx| {
27018 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27019 })
27020 })
27021 .unwrap();
27022 close.await.unwrap();
27023 assert_eq!(
27024 2,
27025 requests_made.load(atomic::Ordering::Acquire),
27026 "After saving and closing all editors, no extra requests should be made"
27027 );
27028 workspace
27029 .update(cx, |workspace, _, cx| {
27030 assert!(
27031 workspace.active_item(cx).is_none(),
27032 "Should close all editors"
27033 )
27034 })
27035 .unwrap();
27036
27037 workspace
27038 .update(cx, |workspace, window, cx| {
27039 workspace.active_pane().update(cx, |pane, cx| {
27040 pane.navigate_backward(&workspace::GoBack, window, cx);
27041 })
27042 })
27043 .unwrap();
27044 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27045 cx.run_until_parked();
27046 let editor = workspace
27047 .update(cx, |workspace, _, cx| {
27048 workspace
27049 .active_item(cx)
27050 .expect("Should have reopened the editor again after navigating back")
27051 .downcast::<Editor>()
27052 .expect("Should be an editor")
27053 })
27054 .unwrap();
27055
27056 assert_eq!(
27057 2,
27058 requests_made.load(atomic::Ordering::Acquire),
27059 "Cache should be reused on buffer close and reopen"
27060 );
27061 editor.update(cx, |editor, cx| {
27062 assert_eq!(
27063 vec![expected_color],
27064 extract_color_inlays(editor, cx),
27065 "Should have an initial inlay"
27066 );
27067 });
27068
27069 drop(color_request_handle);
27070 let closure_requests_made = Arc::clone(&requests_made);
27071 let mut empty_color_request_handle = fake_language_server
27072 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27073 let requests_made = Arc::clone(&closure_requests_made);
27074 async move {
27075 assert_eq!(
27076 params.text_document.uri,
27077 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27078 );
27079 requests_made.fetch_add(1, atomic::Ordering::Release);
27080 Ok(Vec::new())
27081 }
27082 });
27083 let save = editor.update_in(cx, |editor, window, cx| {
27084 editor.move_to_end(&MoveToEnd, window, cx);
27085 editor.handle_input("dirty_again", window, cx);
27086 editor.save(
27087 SaveOptions {
27088 format: false,
27089 autosave: true,
27090 },
27091 project.clone(),
27092 window,
27093 cx,
27094 )
27095 });
27096 save.await.unwrap();
27097
27098 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27099 empty_color_request_handle.next().await.unwrap();
27100 cx.run_until_parked();
27101 assert_eq!(
27102 3,
27103 requests_made.load(atomic::Ordering::Acquire),
27104 "Should query for colors once per save only, as formatting was not requested"
27105 );
27106 editor.update(cx, |editor, cx| {
27107 assert_eq!(
27108 Vec::<Rgba>::new(),
27109 extract_color_inlays(editor, cx),
27110 "Should clear all colors when the server returns an empty response"
27111 );
27112 });
27113}
27114
27115#[gpui::test]
27116async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27117 init_test(cx, |_| {});
27118 let (editor, cx) = cx.add_window_view(Editor::single_line);
27119 editor.update_in(cx, |editor, window, cx| {
27120 editor.set_text("oops\n\nwow\n", window, cx)
27121 });
27122 cx.run_until_parked();
27123 editor.update(cx, |editor, cx| {
27124 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27125 });
27126 editor.update(cx, |editor, cx| {
27127 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27128 });
27129 cx.run_until_parked();
27130 editor.update(cx, |editor, cx| {
27131 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27132 });
27133}
27134
27135#[gpui::test]
27136async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27137 init_test(cx, |_| {});
27138
27139 cx.update(|cx| {
27140 register_project_item::<Editor>(cx);
27141 });
27142
27143 let fs = FakeFs::new(cx.executor());
27144 fs.insert_tree("/root1", json!({})).await;
27145 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27146 .await;
27147
27148 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27149 let (workspace, cx) =
27150 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27151
27152 let worktree_id = project.update(cx, |project, cx| {
27153 project.worktrees(cx).next().unwrap().read(cx).id()
27154 });
27155
27156 let handle = workspace
27157 .update_in(cx, |workspace, window, cx| {
27158 let project_path = (worktree_id, rel_path("one.pdf"));
27159 workspace.open_path(project_path, None, true, window, cx)
27160 })
27161 .await
27162 .unwrap();
27163
27164 assert_eq!(
27165 handle.to_any_view().entity_type(),
27166 TypeId::of::<InvalidItemView>()
27167 );
27168}
27169
27170#[gpui::test]
27171async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27172 init_test(cx, |_| {});
27173
27174 let language = Arc::new(Language::new(
27175 LanguageConfig::default(),
27176 Some(tree_sitter_rust::LANGUAGE.into()),
27177 ));
27178
27179 // Test hierarchical sibling navigation
27180 let text = r#"
27181 fn outer() {
27182 if condition {
27183 let a = 1;
27184 }
27185 let b = 2;
27186 }
27187
27188 fn another() {
27189 let c = 3;
27190 }
27191 "#;
27192
27193 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27194 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27195 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27196
27197 // Wait for parsing to complete
27198 editor
27199 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27200 .await;
27201
27202 editor.update_in(cx, |editor, window, cx| {
27203 // Start by selecting "let a = 1;" inside the if block
27204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27205 s.select_display_ranges([
27206 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27207 ]);
27208 });
27209
27210 let initial_selection = editor
27211 .selections
27212 .display_ranges(&editor.display_snapshot(cx));
27213 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27214
27215 // Test select next sibling - should move up levels to find the next sibling
27216 // Since "let a = 1;" has no siblings in the if block, it should move up
27217 // to find "let b = 2;" which is a sibling of the if block
27218 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27219 let next_selection = editor
27220 .selections
27221 .display_ranges(&editor.display_snapshot(cx));
27222
27223 // Should have a selection and it should be different from the initial
27224 assert_eq!(
27225 next_selection.len(),
27226 1,
27227 "Should have one selection after next"
27228 );
27229 assert_ne!(
27230 next_selection[0], initial_selection[0],
27231 "Next sibling selection should be different"
27232 );
27233
27234 // Test hierarchical navigation by going to the end of the current function
27235 // and trying to navigate to the next function
27236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27237 s.select_display_ranges([
27238 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27239 ]);
27240 });
27241
27242 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27243 let function_next_selection = editor
27244 .selections
27245 .display_ranges(&editor.display_snapshot(cx));
27246
27247 // Should move to the next function
27248 assert_eq!(
27249 function_next_selection.len(),
27250 1,
27251 "Should have one selection after function next"
27252 );
27253
27254 // Test select previous sibling navigation
27255 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27256 let prev_selection = editor
27257 .selections
27258 .display_ranges(&editor.display_snapshot(cx));
27259
27260 // Should have a selection and it should be different
27261 assert_eq!(
27262 prev_selection.len(),
27263 1,
27264 "Should have one selection after prev"
27265 );
27266 assert_ne!(
27267 prev_selection[0], function_next_selection[0],
27268 "Previous sibling selection should be different from next"
27269 );
27270 });
27271}
27272
27273#[gpui::test]
27274async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27275 init_test(cx, |_| {});
27276
27277 let mut cx = EditorTestContext::new(cx).await;
27278 cx.set_state(
27279 "let ˇvariable = 42;
27280let another = variable + 1;
27281let result = variable * 2;",
27282 );
27283
27284 // Set up document highlights manually (simulating LSP response)
27285 cx.update_editor(|editor, _window, cx| {
27286 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27287
27288 // Create highlights for "variable" occurrences
27289 let highlight_ranges = [
27290 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27291 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27292 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27293 ];
27294
27295 let anchor_ranges: Vec<_> = highlight_ranges
27296 .iter()
27297 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27298 .collect();
27299
27300 editor.highlight_background::<DocumentHighlightRead>(
27301 &anchor_ranges,
27302 |theme| theme.colors().editor_document_highlight_read_background,
27303 cx,
27304 );
27305 });
27306
27307 // Go to next highlight - should move to second "variable"
27308 cx.update_editor(|editor, window, cx| {
27309 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27310 });
27311 cx.assert_editor_state(
27312 "let variable = 42;
27313let another = ˇvariable + 1;
27314let result = variable * 2;",
27315 );
27316
27317 // Go to next highlight - should move to third "variable"
27318 cx.update_editor(|editor, window, cx| {
27319 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27320 });
27321 cx.assert_editor_state(
27322 "let variable = 42;
27323let another = variable + 1;
27324let result = ˇvariable * 2;",
27325 );
27326
27327 // Go to next highlight - should stay at third "variable" (no wrap-around)
27328 cx.update_editor(|editor, window, cx| {
27329 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27330 });
27331 cx.assert_editor_state(
27332 "let variable = 42;
27333let another = variable + 1;
27334let result = ˇvariable * 2;",
27335 );
27336
27337 // Now test going backwards from third position
27338 cx.update_editor(|editor, window, cx| {
27339 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27340 });
27341 cx.assert_editor_state(
27342 "let variable = 42;
27343let another = ˇvariable + 1;
27344let result = variable * 2;",
27345 );
27346
27347 // Go to previous highlight - should move to first "variable"
27348 cx.update_editor(|editor, window, cx| {
27349 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27350 });
27351 cx.assert_editor_state(
27352 "let ˇvariable = 42;
27353let another = variable + 1;
27354let result = variable * 2;",
27355 );
27356
27357 // Go to previous highlight - should stay on first "variable"
27358 cx.update_editor(|editor, window, cx| {
27359 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27360 });
27361 cx.assert_editor_state(
27362 "let ˇvariable = 42;
27363let another = variable + 1;
27364let result = variable * 2;",
27365 );
27366}
27367
27368#[gpui::test]
27369async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27370 cx: &mut gpui::TestAppContext,
27371) {
27372 init_test(cx, |_| {});
27373
27374 let url = "https://zed.dev";
27375
27376 let markdown_language = Arc::new(Language::new(
27377 LanguageConfig {
27378 name: "Markdown".into(),
27379 ..LanguageConfig::default()
27380 },
27381 None,
27382 ));
27383
27384 let mut cx = EditorTestContext::new(cx).await;
27385 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27386 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27387
27388 cx.update_editor(|editor, window, cx| {
27389 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27390 editor.paste(&Paste, window, cx);
27391 });
27392
27393 cx.assert_editor_state(&format!(
27394 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27395 ));
27396}
27397
27398#[gpui::test]
27399async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
27400 cx: &mut gpui::TestAppContext,
27401) {
27402 init_test(cx, |_| {});
27403
27404 let url = "https://zed.dev";
27405
27406 let markdown_language = Arc::new(Language::new(
27407 LanguageConfig {
27408 name: "Markdown".into(),
27409 ..LanguageConfig::default()
27410 },
27411 None,
27412 ));
27413
27414 let mut cx = EditorTestContext::new(cx).await;
27415 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27416 cx.set_state(&format!(
27417 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
27418 ));
27419
27420 cx.update_editor(|editor, window, cx| {
27421 editor.copy(&Copy, window, cx);
27422 });
27423
27424 cx.set_state(&format!(
27425 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
27426 ));
27427
27428 cx.update_editor(|editor, window, cx| {
27429 editor.paste(&Paste, window, cx);
27430 });
27431
27432 cx.assert_editor_state(&format!(
27433 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
27434 ));
27435}
27436
27437#[gpui::test]
27438async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
27439 cx: &mut gpui::TestAppContext,
27440) {
27441 init_test(cx, |_| {});
27442
27443 let url = "https://zed.dev";
27444
27445 let markdown_language = Arc::new(Language::new(
27446 LanguageConfig {
27447 name: "Markdown".into(),
27448 ..LanguageConfig::default()
27449 },
27450 None,
27451 ));
27452
27453 let mut cx = EditorTestContext::new(cx).await;
27454 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27455 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
27456
27457 cx.update_editor(|editor, window, cx| {
27458 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27459 editor.paste(&Paste, window, cx);
27460 });
27461
27462 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
27463}
27464
27465#[gpui::test]
27466async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
27467 cx: &mut gpui::TestAppContext,
27468) {
27469 init_test(cx, |_| {});
27470
27471 let text = "Awesome";
27472
27473 let markdown_language = Arc::new(Language::new(
27474 LanguageConfig {
27475 name: "Markdown".into(),
27476 ..LanguageConfig::default()
27477 },
27478 None,
27479 ));
27480
27481 let mut cx = EditorTestContext::new(cx).await;
27482 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27483 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
27484
27485 cx.update_editor(|editor, window, cx| {
27486 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
27487 editor.paste(&Paste, window, cx);
27488 });
27489
27490 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
27491}
27492
27493#[gpui::test]
27494async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
27495 cx: &mut gpui::TestAppContext,
27496) {
27497 init_test(cx, |_| {});
27498
27499 let url = "https://zed.dev";
27500
27501 let markdown_language = Arc::new(Language::new(
27502 LanguageConfig {
27503 name: "Rust".into(),
27504 ..LanguageConfig::default()
27505 },
27506 None,
27507 ));
27508
27509 let mut cx = EditorTestContext::new(cx).await;
27510 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27511 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
27512
27513 cx.update_editor(|editor, window, cx| {
27514 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27515 editor.paste(&Paste, window, cx);
27516 });
27517
27518 cx.assert_editor_state(&format!(
27519 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
27520 ));
27521}
27522
27523#[gpui::test]
27524async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
27525 cx: &mut TestAppContext,
27526) {
27527 init_test(cx, |_| {});
27528
27529 let url = "https://zed.dev";
27530
27531 let markdown_language = Arc::new(Language::new(
27532 LanguageConfig {
27533 name: "Markdown".into(),
27534 ..LanguageConfig::default()
27535 },
27536 None,
27537 ));
27538
27539 let (editor, cx) = cx.add_window_view(|window, cx| {
27540 let multi_buffer = MultiBuffer::build_multi(
27541 [
27542 ("this will embed -> link", vec![Point::row_range(0..1)]),
27543 ("this will replace -> link", vec![Point::row_range(0..1)]),
27544 ],
27545 cx,
27546 );
27547 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
27548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27549 s.select_ranges(vec![
27550 Point::new(0, 19)..Point::new(0, 23),
27551 Point::new(1, 21)..Point::new(1, 25),
27552 ])
27553 });
27554 let first_buffer_id = multi_buffer
27555 .read(cx)
27556 .excerpt_buffer_ids()
27557 .into_iter()
27558 .next()
27559 .unwrap();
27560 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
27561 first_buffer.update(cx, |buffer, cx| {
27562 buffer.set_language(Some(markdown_language.clone()), cx);
27563 });
27564
27565 editor
27566 });
27567 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27568
27569 cx.update_editor(|editor, window, cx| {
27570 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27571 editor.paste(&Paste, window, cx);
27572 });
27573
27574 cx.assert_editor_state(&format!(
27575 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
27576 ));
27577}
27578
27579#[gpui::test]
27580async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
27581 init_test(cx, |_| {});
27582
27583 let fs = FakeFs::new(cx.executor());
27584 fs.insert_tree(
27585 path!("/project"),
27586 json!({
27587 "first.rs": "# First Document\nSome content here.",
27588 "second.rs": "Plain text content for second file.",
27589 }),
27590 )
27591 .await;
27592
27593 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
27594 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27595 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27596
27597 let language = rust_lang();
27598 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27599 language_registry.add(language.clone());
27600 let mut fake_servers = language_registry.register_fake_lsp(
27601 "Rust",
27602 FakeLspAdapter {
27603 ..FakeLspAdapter::default()
27604 },
27605 );
27606
27607 let buffer1 = project
27608 .update(cx, |project, cx| {
27609 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
27610 })
27611 .await
27612 .unwrap();
27613 let buffer2 = project
27614 .update(cx, |project, cx| {
27615 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
27616 })
27617 .await
27618 .unwrap();
27619
27620 let multi_buffer = cx.new(|cx| {
27621 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
27622 multi_buffer.set_excerpts_for_path(
27623 PathKey::for_buffer(&buffer1, cx),
27624 buffer1.clone(),
27625 [Point::zero()..buffer1.read(cx).max_point()],
27626 3,
27627 cx,
27628 );
27629 multi_buffer.set_excerpts_for_path(
27630 PathKey::for_buffer(&buffer2, cx),
27631 buffer2.clone(),
27632 [Point::zero()..buffer1.read(cx).max_point()],
27633 3,
27634 cx,
27635 );
27636 multi_buffer
27637 });
27638
27639 let (editor, cx) = cx.add_window_view(|window, cx| {
27640 Editor::new(
27641 EditorMode::full(),
27642 multi_buffer,
27643 Some(project.clone()),
27644 window,
27645 cx,
27646 )
27647 });
27648
27649 let fake_language_server = fake_servers.next().await.unwrap();
27650
27651 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
27652
27653 let save = editor.update_in(cx, |editor, window, cx| {
27654 assert!(editor.is_dirty(cx));
27655
27656 editor.save(
27657 SaveOptions {
27658 format: true,
27659 autosave: true,
27660 },
27661 project,
27662 window,
27663 cx,
27664 )
27665 });
27666 let (start_edit_tx, start_edit_rx) = oneshot::channel();
27667 let (done_edit_tx, done_edit_rx) = oneshot::channel();
27668 let mut done_edit_rx = Some(done_edit_rx);
27669 let mut start_edit_tx = Some(start_edit_tx);
27670
27671 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
27672 start_edit_tx.take().unwrap().send(()).unwrap();
27673 let done_edit_rx = done_edit_rx.take().unwrap();
27674 async move {
27675 done_edit_rx.await.unwrap();
27676 Ok(None)
27677 }
27678 });
27679
27680 start_edit_rx.await.unwrap();
27681 buffer2
27682 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
27683 .unwrap();
27684
27685 done_edit_tx.send(()).unwrap();
27686
27687 save.await.unwrap();
27688 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
27689}
27690
27691#[track_caller]
27692fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
27693 editor
27694 .all_inlays(cx)
27695 .into_iter()
27696 .filter_map(|inlay| inlay.get_color())
27697 .map(Rgba::from)
27698 .collect()
27699}
27700
27701#[gpui::test]
27702fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
27703 init_test(cx, |_| {});
27704
27705 let editor = cx.add_window(|window, cx| {
27706 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
27707 build_editor(buffer, window, cx)
27708 });
27709
27710 editor
27711 .update(cx, |editor, window, cx| {
27712 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27713 s.select_display_ranges([
27714 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
27715 ])
27716 });
27717
27718 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
27719
27720 assert_eq!(
27721 editor.display_text(cx),
27722 "line1\nline2\nline2",
27723 "Duplicating last line upward should create duplicate above, not on same line"
27724 );
27725
27726 assert_eq!(
27727 editor
27728 .selections
27729 .display_ranges(&editor.display_snapshot(cx)),
27730 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27731 "Selection should move to the duplicated line"
27732 );
27733 })
27734 .unwrap();
27735}
27736
27737#[gpui::test]
27738async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27739 init_test(cx, |_| {});
27740
27741 let mut cx = EditorTestContext::new(cx).await;
27742
27743 cx.set_state("line1\nline2ˇ");
27744
27745 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27746
27747 let clipboard_text = cx
27748 .read_from_clipboard()
27749 .and_then(|item| item.text().as_deref().map(str::to_string));
27750
27751 assert_eq!(
27752 clipboard_text,
27753 Some("line2\n".to_string()),
27754 "Copying a line without trailing newline should include a newline"
27755 );
27756
27757 cx.set_state("line1\nˇ");
27758
27759 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27760
27761 cx.assert_editor_state("line1\nline2\nˇ");
27762}
27763
27764#[gpui::test]
27765async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27766 init_test(cx, |_| {});
27767
27768 let mut cx = EditorTestContext::new(cx).await;
27769
27770 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27771
27772 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27773
27774 let clipboard_text = cx
27775 .read_from_clipboard()
27776 .and_then(|item| item.text().as_deref().map(str::to_string));
27777
27778 assert_eq!(
27779 clipboard_text,
27780 Some("line1\nline2\nline3\n".to_string()),
27781 "Copying multiple lines should include a single newline between lines"
27782 );
27783
27784 cx.set_state("lineA\nˇ");
27785
27786 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27787
27788 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27789}
27790
27791#[gpui::test]
27792async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27793 init_test(cx, |_| {});
27794
27795 let mut cx = EditorTestContext::new(cx).await;
27796
27797 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27798
27799 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
27800
27801 let clipboard_text = cx
27802 .read_from_clipboard()
27803 .and_then(|item| item.text().as_deref().map(str::to_string));
27804
27805 assert_eq!(
27806 clipboard_text,
27807 Some("line1\nline2\nline3\n".to_string()),
27808 "Copying multiple lines should include a single newline between lines"
27809 );
27810
27811 cx.set_state("lineA\nˇ");
27812
27813 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27814
27815 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27816}
27817
27818#[gpui::test]
27819async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27820 init_test(cx, |_| {});
27821
27822 let mut cx = EditorTestContext::new(cx).await;
27823
27824 cx.set_state("line1\nline2ˇ");
27825 cx.update_editor(|e, window, cx| {
27826 e.set_mode(EditorMode::SingleLine);
27827 assert!(e.key_context(window, cx).contains("end_of_input"));
27828 });
27829 cx.set_state("ˇline1\nline2");
27830 cx.update_editor(|e, window, cx| {
27831 assert!(!e.key_context(window, cx).contains("end_of_input"));
27832 });
27833 cx.set_state("line1ˇ\nline2");
27834 cx.update_editor(|e, window, cx| {
27835 assert!(!e.key_context(window, cx).contains("end_of_input"));
27836 });
27837}
27838
27839#[gpui::test]
27840async fn test_sticky_scroll(cx: &mut TestAppContext) {
27841 init_test(cx, |_| {});
27842 let mut cx = EditorTestContext::new(cx).await;
27843
27844 let buffer = indoc! {"
27845 ˇfn foo() {
27846 let abc = 123;
27847 }
27848 struct Bar;
27849 impl Bar {
27850 fn new() -> Self {
27851 Self
27852 }
27853 }
27854 fn baz() {
27855 }
27856 "};
27857 cx.set_state(&buffer);
27858
27859 cx.update_editor(|e, _, cx| {
27860 e.buffer()
27861 .read(cx)
27862 .as_singleton()
27863 .unwrap()
27864 .update(cx, |buffer, cx| {
27865 buffer.set_language(Some(rust_lang()), cx);
27866 })
27867 });
27868
27869 let mut sticky_headers = |offset: ScrollOffset| {
27870 cx.update_editor(|e, window, cx| {
27871 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27872 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27873 .into_iter()
27874 .map(
27875 |StickyHeader {
27876 start_point,
27877 offset,
27878 ..
27879 }| { (start_point, offset) },
27880 )
27881 .collect::<Vec<_>>()
27882 })
27883 };
27884
27885 let fn_foo = Point { row: 0, column: 0 };
27886 let impl_bar = Point { row: 4, column: 0 };
27887 let fn_new = Point { row: 5, column: 4 };
27888
27889 assert_eq!(sticky_headers(0.0), vec![]);
27890 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27891 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27892 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27893 assert_eq!(sticky_headers(2.0), vec![]);
27894 assert_eq!(sticky_headers(2.5), vec![]);
27895 assert_eq!(sticky_headers(3.0), vec![]);
27896 assert_eq!(sticky_headers(3.5), vec![]);
27897 assert_eq!(sticky_headers(4.0), vec![]);
27898 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27899 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27900 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27901 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27902 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27903 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27904 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27905 assert_eq!(sticky_headers(8.0), vec![]);
27906 assert_eq!(sticky_headers(8.5), vec![]);
27907 assert_eq!(sticky_headers(9.0), vec![]);
27908 assert_eq!(sticky_headers(9.5), vec![]);
27909 assert_eq!(sticky_headers(10.0), vec![]);
27910}
27911
27912#[gpui::test]
27913async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27914 init_test(cx, |_| {});
27915 cx.update(|cx| {
27916 SettingsStore::update_global(cx, |store, cx| {
27917 store.update_user_settings(cx, |settings| {
27918 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27919 enabled: Some(true),
27920 })
27921 });
27922 });
27923 });
27924 let mut cx = EditorTestContext::new(cx).await;
27925
27926 let line_height = cx.editor(|editor, window, _cx| {
27927 editor
27928 .style()
27929 .unwrap()
27930 .text
27931 .line_height_in_pixels(window.rem_size())
27932 });
27933
27934 let buffer = indoc! {"
27935 ˇfn foo() {
27936 let abc = 123;
27937 }
27938 struct Bar;
27939 impl Bar {
27940 fn new() -> Self {
27941 Self
27942 }
27943 }
27944 fn baz() {
27945 }
27946 "};
27947 cx.set_state(&buffer);
27948
27949 cx.update_editor(|e, _, cx| {
27950 e.buffer()
27951 .read(cx)
27952 .as_singleton()
27953 .unwrap()
27954 .update(cx, |buffer, cx| {
27955 buffer.set_language(Some(rust_lang()), cx);
27956 })
27957 });
27958
27959 let fn_foo = || empty_range(0, 0);
27960 let impl_bar = || empty_range(4, 0);
27961 let fn_new = || empty_range(5, 4);
27962
27963 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27964 cx.update_editor(|e, window, cx| {
27965 e.scroll(
27966 gpui::Point {
27967 x: 0.,
27968 y: scroll_offset,
27969 },
27970 None,
27971 window,
27972 cx,
27973 );
27974 });
27975 cx.simulate_click(
27976 gpui::Point {
27977 x: px(0.),
27978 y: click_offset as f32 * line_height,
27979 },
27980 Modifiers::none(),
27981 );
27982 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27983 };
27984
27985 assert_eq!(
27986 scroll_and_click(
27987 4.5, // impl Bar is halfway off the screen
27988 0.0 // click top of screen
27989 ),
27990 // scrolled to impl Bar
27991 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27992 );
27993
27994 assert_eq!(
27995 scroll_and_click(
27996 4.5, // impl Bar is halfway off the screen
27997 0.25 // click middle of impl Bar
27998 ),
27999 // scrolled to impl Bar
28000 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28001 );
28002
28003 assert_eq!(
28004 scroll_and_click(
28005 4.5, // impl Bar is halfway off the screen
28006 1.5 // click below impl Bar (e.g. fn new())
28007 ),
28008 // scrolled to fn new() - this is below the impl Bar header which has persisted
28009 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28010 );
28011
28012 assert_eq!(
28013 scroll_and_click(
28014 5.5, // fn new is halfway underneath impl Bar
28015 0.75 // click on the overlap of impl Bar and fn new()
28016 ),
28017 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28018 );
28019
28020 assert_eq!(
28021 scroll_and_click(
28022 5.5, // fn new is halfway underneath impl Bar
28023 1.25 // click on the visible part of fn new()
28024 ),
28025 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28026 );
28027
28028 assert_eq!(
28029 scroll_and_click(
28030 1.5, // fn foo is halfway off the screen
28031 0.0 // click top of screen
28032 ),
28033 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28034 );
28035
28036 assert_eq!(
28037 scroll_and_click(
28038 1.5, // fn foo is halfway off the screen
28039 0.75 // click visible part of let abc...
28040 )
28041 .0,
28042 // no change in scroll
28043 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28044 (gpui::Point { x: 0., y: 1.5 })
28045 );
28046}
28047
28048#[gpui::test]
28049async fn test_next_prev_reference(cx: &mut TestAppContext) {
28050 const CYCLE_POSITIONS: &[&'static str] = &[
28051 indoc! {"
28052 fn foo() {
28053 let ˇabc = 123;
28054 let x = abc + 1;
28055 let y = abc + 2;
28056 let z = abc + 2;
28057 }
28058 "},
28059 indoc! {"
28060 fn foo() {
28061 let abc = 123;
28062 let x = ˇabc + 1;
28063 let y = abc + 2;
28064 let z = abc + 2;
28065 }
28066 "},
28067 indoc! {"
28068 fn foo() {
28069 let abc = 123;
28070 let x = abc + 1;
28071 let y = ˇabc + 2;
28072 let z = abc + 2;
28073 }
28074 "},
28075 indoc! {"
28076 fn foo() {
28077 let abc = 123;
28078 let x = abc + 1;
28079 let y = abc + 2;
28080 let z = ˇabc + 2;
28081 }
28082 "},
28083 ];
28084
28085 init_test(cx, |_| {});
28086
28087 let mut cx = EditorLspTestContext::new_rust(
28088 lsp::ServerCapabilities {
28089 references_provider: Some(lsp::OneOf::Left(true)),
28090 ..Default::default()
28091 },
28092 cx,
28093 )
28094 .await;
28095
28096 // importantly, the cursor is in the middle
28097 cx.set_state(indoc! {"
28098 fn foo() {
28099 let aˇbc = 123;
28100 let x = abc + 1;
28101 let y = abc + 2;
28102 let z = abc + 2;
28103 }
28104 "});
28105
28106 let reference_ranges = [
28107 lsp::Position::new(1, 8),
28108 lsp::Position::new(2, 12),
28109 lsp::Position::new(3, 12),
28110 lsp::Position::new(4, 12),
28111 ]
28112 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28113
28114 cx.lsp
28115 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
28116 Ok(Some(
28117 reference_ranges
28118 .map(|range| lsp::Location {
28119 uri: params.text_document_position.text_document.uri.clone(),
28120 range,
28121 })
28122 .to_vec(),
28123 ))
28124 });
28125
28126 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
28127 cx.update_editor(|editor, window, cx| {
28128 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
28129 })
28130 .unwrap()
28131 .await
28132 .unwrap()
28133 };
28134
28135 _move(Direction::Next, 1, &mut cx).await;
28136 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28137
28138 _move(Direction::Next, 1, &mut cx).await;
28139 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28140
28141 _move(Direction::Next, 1, &mut cx).await;
28142 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28143
28144 // loops back to the start
28145 _move(Direction::Next, 1, &mut cx).await;
28146 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28147
28148 // loops back to the end
28149 _move(Direction::Prev, 1, &mut cx).await;
28150 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28151
28152 _move(Direction::Prev, 1, &mut cx).await;
28153 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28154
28155 _move(Direction::Prev, 1, &mut cx).await;
28156 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28157
28158 _move(Direction::Prev, 1, &mut cx).await;
28159 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28160
28161 _move(Direction::Next, 3, &mut cx).await;
28162 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28163
28164 _move(Direction::Prev, 2, &mut cx).await;
28165 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28166}
28167
28168#[gpui::test]
28169async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
28170 init_test(cx, |_| {});
28171
28172 let (editor, cx) = cx.add_window_view(|window, cx| {
28173 let multi_buffer = MultiBuffer::build_multi(
28174 [
28175 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28176 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28177 ],
28178 cx,
28179 );
28180 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28181 });
28182
28183 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28184 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28185
28186 cx.assert_excerpts_with_selections(indoc! {"
28187 [EXCERPT]
28188 ˇ1
28189 2
28190 3
28191 [EXCERPT]
28192 1
28193 2
28194 3
28195 "});
28196
28197 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28198 cx.update_editor(|editor, window, cx| {
28199 editor.change_selections(None.into(), window, cx, |s| {
28200 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28201 });
28202 });
28203 cx.assert_excerpts_with_selections(indoc! {"
28204 [EXCERPT]
28205 1
28206 2ˇ
28207 3
28208 [EXCERPT]
28209 1
28210 2
28211 3
28212 "});
28213
28214 cx.update_editor(|editor, window, cx| {
28215 editor
28216 .select_all_matches(&SelectAllMatches, window, cx)
28217 .unwrap();
28218 });
28219 cx.assert_excerpts_with_selections(indoc! {"
28220 [EXCERPT]
28221 1
28222 2ˇ
28223 3
28224 [EXCERPT]
28225 1
28226 2ˇ
28227 3
28228 "});
28229
28230 cx.update_editor(|editor, window, cx| {
28231 editor.handle_input("X", window, cx);
28232 });
28233 cx.assert_excerpts_with_selections(indoc! {"
28234 [EXCERPT]
28235 1
28236 Xˇ
28237 3
28238 [EXCERPT]
28239 1
28240 Xˇ
28241 3
28242 "});
28243
28244 // Scenario 2: Select "2", then fold second buffer before insertion
28245 cx.update_multibuffer(|mb, cx| {
28246 for buffer_id in buffer_ids.iter() {
28247 let buffer = mb.buffer(*buffer_id).unwrap();
28248 buffer.update(cx, |buffer, cx| {
28249 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28250 });
28251 }
28252 });
28253
28254 // Select "2" and select all matches
28255 cx.update_editor(|editor, window, cx| {
28256 editor.change_selections(None.into(), window, cx, |s| {
28257 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28258 });
28259 editor
28260 .select_all_matches(&SelectAllMatches, window, cx)
28261 .unwrap();
28262 });
28263
28264 // Fold second buffer - should remove selections from folded buffer
28265 cx.update_editor(|editor, _, cx| {
28266 editor.fold_buffer(buffer_ids[1], cx);
28267 });
28268 cx.assert_excerpts_with_selections(indoc! {"
28269 [EXCERPT]
28270 1
28271 2ˇ
28272 3
28273 [EXCERPT]
28274 [FOLDED]
28275 "});
28276
28277 // Insert text - should only affect first buffer
28278 cx.update_editor(|editor, window, cx| {
28279 editor.handle_input("Y", window, cx);
28280 });
28281 cx.update_editor(|editor, _, cx| {
28282 editor.unfold_buffer(buffer_ids[1], cx);
28283 });
28284 cx.assert_excerpts_with_selections(indoc! {"
28285 [EXCERPT]
28286 1
28287 Yˇ
28288 3
28289 [EXCERPT]
28290 1
28291 2
28292 3
28293 "});
28294
28295 // Scenario 3: Select "2", then fold first buffer before insertion
28296 cx.update_multibuffer(|mb, cx| {
28297 for buffer_id in buffer_ids.iter() {
28298 let buffer = mb.buffer(*buffer_id).unwrap();
28299 buffer.update(cx, |buffer, cx| {
28300 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28301 });
28302 }
28303 });
28304
28305 // Select "2" and select all matches
28306 cx.update_editor(|editor, window, cx| {
28307 editor.change_selections(None.into(), window, cx, |s| {
28308 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28309 });
28310 editor
28311 .select_all_matches(&SelectAllMatches, window, cx)
28312 .unwrap();
28313 });
28314
28315 // Fold first buffer - should remove selections from folded buffer
28316 cx.update_editor(|editor, _, cx| {
28317 editor.fold_buffer(buffer_ids[0], cx);
28318 });
28319 cx.assert_excerpts_with_selections(indoc! {"
28320 [EXCERPT]
28321 [FOLDED]
28322 [EXCERPT]
28323 1
28324 2ˇ
28325 3
28326 "});
28327
28328 // Insert text - should only affect second buffer
28329 cx.update_editor(|editor, window, cx| {
28330 editor.handle_input("Z", window, cx);
28331 });
28332 cx.update_editor(|editor, _, cx| {
28333 editor.unfold_buffer(buffer_ids[0], cx);
28334 });
28335 cx.assert_excerpts_with_selections(indoc! {"
28336 [EXCERPT]
28337 1
28338 2
28339 3
28340 [EXCERPT]
28341 1
28342 Zˇ
28343 3
28344 "});
28345
28346 // Edge case scenario: fold all buffers, then try to insert
28347 cx.update_editor(|editor, _, cx| {
28348 editor.fold_buffer(buffer_ids[0], cx);
28349 editor.fold_buffer(buffer_ids[1], cx);
28350 });
28351 cx.assert_excerpts_with_selections(indoc! {"
28352 [EXCERPT]
28353 ˇ[FOLDED]
28354 [EXCERPT]
28355 [FOLDED]
28356 "});
28357
28358 // Insert should work via default selection
28359 cx.update_editor(|editor, window, cx| {
28360 editor.handle_input("W", window, cx);
28361 });
28362 cx.update_editor(|editor, _, cx| {
28363 editor.unfold_buffer(buffer_ids[0], cx);
28364 editor.unfold_buffer(buffer_ids[1], cx);
28365 });
28366 cx.assert_excerpts_with_selections(indoc! {"
28367 [EXCERPT]
28368 Wˇ1
28369 2
28370 3
28371 [EXCERPT]
28372 1
28373 Z
28374 3
28375 "});
28376}
28377
28378#[gpui::test]
28379async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
28380 init_test(cx, |_| {});
28381 let mut leader_cx = EditorTestContext::new(cx).await;
28382
28383 let diff_base = indoc!(
28384 r#"
28385 one
28386 two
28387 three
28388 four
28389 five
28390 six
28391 "#
28392 );
28393
28394 let initial_state = indoc!(
28395 r#"
28396 ˇone
28397 two
28398 THREE
28399 four
28400 five
28401 six
28402 "#
28403 );
28404
28405 leader_cx.set_state(initial_state);
28406
28407 leader_cx.set_head_text(&diff_base);
28408 leader_cx.run_until_parked();
28409
28410 let follower = leader_cx.update_multibuffer(|leader, cx| {
28411 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28412 leader.set_all_diff_hunks_expanded(cx);
28413 leader.get_or_create_follower(cx)
28414 });
28415 follower.update(cx, |follower, cx| {
28416 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28417 follower.set_all_diff_hunks_expanded(cx);
28418 });
28419
28420 let follower_editor =
28421 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
28422 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
28423
28424 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
28425 cx.run_until_parked();
28426
28427 leader_cx.assert_editor_state(initial_state);
28428 follower_cx.assert_editor_state(indoc! {
28429 r#"
28430 ˇone
28431 two
28432 three
28433 four
28434 five
28435 six
28436 "#
28437 });
28438
28439 follower_cx.editor(|editor, _window, cx| {
28440 assert!(editor.read_only(cx));
28441 });
28442
28443 leader_cx.update_editor(|editor, _window, cx| {
28444 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
28445 });
28446 cx.run_until_parked();
28447
28448 leader_cx.assert_editor_state(indoc! {
28449 r#"
28450 ˇone
28451 two
28452 THREE
28453 four
28454 FIVE
28455 six
28456 "#
28457 });
28458
28459 follower_cx.assert_editor_state(indoc! {
28460 r#"
28461 ˇone
28462 two
28463 three
28464 four
28465 five
28466 six
28467 "#
28468 });
28469
28470 leader_cx.update_editor(|editor, _window, cx| {
28471 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
28472 });
28473 cx.run_until_parked();
28474
28475 leader_cx.assert_editor_state(indoc! {
28476 r#"
28477 ˇone
28478 two
28479 THREE
28480 four
28481 FIVE
28482 six
28483 SEVEN"#
28484 });
28485
28486 follower_cx.assert_editor_state(indoc! {
28487 r#"
28488 ˇone
28489 two
28490 three
28491 four
28492 five
28493 six
28494 "#
28495 });
28496
28497 leader_cx.update_editor(|editor, window, cx| {
28498 editor.move_down(&MoveDown, window, cx);
28499 editor.refresh_selected_text_highlights(true, window, cx);
28500 });
28501 leader_cx.run_until_parked();
28502}
28503
28504#[gpui::test]
28505async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
28506 init_test(cx, |_| {});
28507 let base_text = "base\n";
28508 let buffer_text = "buffer\n";
28509
28510 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
28511 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
28512
28513 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
28514 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
28515 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
28516 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
28517
28518 let leader = cx.new(|cx| {
28519 let mut leader = MultiBuffer::new(Capability::ReadWrite);
28520 leader.set_all_diff_hunks_expanded(cx);
28521 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28522 leader
28523 });
28524 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
28525 follower.update(cx, |follower, _| {
28526 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28527 });
28528
28529 leader.update(cx, |leader, cx| {
28530 leader.insert_excerpts_after(
28531 ExcerptId::min(),
28532 extra_buffer_2.clone(),
28533 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28534 cx,
28535 );
28536 leader.add_diff(extra_diff_2.clone(), cx);
28537
28538 leader.insert_excerpts_after(
28539 ExcerptId::min(),
28540 extra_buffer_1.clone(),
28541 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28542 cx,
28543 );
28544 leader.add_diff(extra_diff_1.clone(), cx);
28545
28546 leader.insert_excerpts_after(
28547 ExcerptId::min(),
28548 buffer1.clone(),
28549 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28550 cx,
28551 );
28552 leader.add_diff(diff1.clone(), cx);
28553 });
28554
28555 cx.run_until_parked();
28556 let mut cx = cx.add_empty_window();
28557
28558 let leader_editor = cx
28559 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
28560 let follower_editor = cx.new_window_entity(|window, cx| {
28561 Editor::for_multibuffer(follower.clone(), None, window, cx)
28562 });
28563
28564 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
28565 leader_cx.assert_editor_state(indoc! {"
28566 ˇbuffer
28567
28568 dummy text 1
28569
28570 dummy text 2
28571 "});
28572 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
28573 follower_cx.assert_editor_state(indoc! {"
28574 ˇbase
28575
28576
28577 "});
28578}
28579
28580#[gpui::test]
28581async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
28582 init_test(cx, |_| {});
28583
28584 let (editor, cx) = cx.add_window_view(|window, cx| {
28585 let multi_buffer = MultiBuffer::build_multi(
28586 [
28587 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28588 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
28589 ],
28590 cx,
28591 );
28592 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28593 });
28594
28595 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28596
28597 cx.assert_excerpts_with_selections(indoc! {"
28598 [EXCERPT]
28599 ˇ1
28600 2
28601 3
28602 [EXCERPT]
28603 1
28604 2
28605 3
28606 4
28607 5
28608 6
28609 7
28610 8
28611 9
28612 "});
28613
28614 cx.update_editor(|editor, window, cx| {
28615 editor.change_selections(None.into(), window, cx, |s| {
28616 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
28617 });
28618 });
28619
28620 cx.assert_excerpts_with_selections(indoc! {"
28621 [EXCERPT]
28622 1
28623 2
28624 3
28625 [EXCERPT]
28626 1
28627 2
28628 3
28629 4
28630 5
28631 6
28632 ˇ7
28633 8
28634 9
28635 "});
28636
28637 cx.update_editor(|editor, _window, cx| {
28638 editor.set_vertical_scroll_margin(0, cx);
28639 });
28640
28641 cx.update_editor(|editor, window, cx| {
28642 assert_eq!(editor.vertical_scroll_margin(), 0);
28643 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28644 assert_eq!(
28645 editor.snapshot(window, cx).scroll_position(),
28646 gpui::Point::new(0., 12.0)
28647 );
28648 });
28649
28650 cx.update_editor(|editor, _window, cx| {
28651 editor.set_vertical_scroll_margin(3, cx);
28652 });
28653
28654 cx.update_editor(|editor, window, cx| {
28655 assert_eq!(editor.vertical_scroll_margin(), 3);
28656 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28657 assert_eq!(
28658 editor.snapshot(window, cx).scroll_position(),
28659 gpui::Point::new(0., 9.0)
28660 );
28661 });
28662}