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::{IndentGuide, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey};
39use parking_lot::Mutex;
40use pretty_assertions::{assert_eq, assert_ne};
41use project::{
42 FakeFs,
43 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
44 project_settings::LspSettings,
45};
46use serde_json::{self, json};
47use settings::{
48 AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
49 IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
50};
51use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
52use std::{
53 iter,
54 sync::atomic::{self, AtomicUsize},
55};
56use test::build_editor_with_project;
57use text::ToPoint as _;
58use unindent::Unindent;
59use util::{
60 assert_set_eq, path,
61 rel_path::rel_path,
62 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
63 uri,
64};
65use workspace::{
66 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
67 OpenOptions, ViewId,
68 invalid_item_view::InvalidItemView,
69 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
70 register_project_item,
71};
72
73fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
74 editor
75 .selections
76 .display_ranges(&editor.display_snapshot(cx))
77}
78
79#[gpui::test]
80fn test_edit_events(cx: &mut TestAppContext) {
81 init_test(cx, |_| {});
82
83 let buffer = cx.new(|cx| {
84 let mut buffer = language::Buffer::local("123456", cx);
85 buffer.set_group_interval(Duration::from_secs(1));
86 buffer
87 });
88
89 let events = Rc::new(RefCell::new(Vec::new()));
90 let editor1 = cx.add_window({
91 let events = events.clone();
92 |window, cx| {
93 let entity = cx.entity();
94 cx.subscribe_in(
95 &entity,
96 window,
97 move |_, _, event: &EditorEvent, _, _| match event {
98 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
99 EditorEvent::BufferEdited => {
100 events.borrow_mut().push(("editor1", "buffer edited"))
101 }
102 _ => {}
103 },
104 )
105 .detach();
106 Editor::for_buffer(buffer.clone(), None, window, cx)
107 }
108 });
109
110 let editor2 = cx.add_window({
111 let events = events.clone();
112 |window, cx| {
113 cx.subscribe_in(
114 &cx.entity(),
115 window,
116 move |_, _, event: &EditorEvent, _, _| match event {
117 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
118 EditorEvent::BufferEdited => {
119 events.borrow_mut().push(("editor2", "buffer edited"))
120 }
121 _ => {}
122 },
123 )
124 .detach();
125 Editor::for_buffer(buffer.clone(), None, window, cx)
126 }
127 });
128
129 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
130
131 // Mutating editor 1 will emit an `Edited` event only for that editor.
132 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
133 assert_eq!(
134 mem::take(&mut *events.borrow_mut()),
135 [
136 ("editor1", "edited"),
137 ("editor1", "buffer edited"),
138 ("editor2", "buffer edited"),
139 ]
140 );
141
142 // Mutating editor 2 will emit an `Edited` event only for that editor.
143 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
144 assert_eq!(
145 mem::take(&mut *events.borrow_mut()),
146 [
147 ("editor2", "edited"),
148 ("editor1", "buffer edited"),
149 ("editor2", "buffer edited"),
150 ]
151 );
152
153 // Undoing on editor 1 will emit an `Edited` event only for that editor.
154 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
155 assert_eq!(
156 mem::take(&mut *events.borrow_mut()),
157 [
158 ("editor1", "edited"),
159 ("editor1", "buffer edited"),
160 ("editor2", "buffer edited"),
161 ]
162 );
163
164 // Redoing on editor 1 will emit an `Edited` event only for that editor.
165 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
166 assert_eq!(
167 mem::take(&mut *events.borrow_mut()),
168 [
169 ("editor1", "edited"),
170 ("editor1", "buffer edited"),
171 ("editor2", "buffer edited"),
172 ]
173 );
174
175 // Undoing on editor 2 will emit an `Edited` event only for that editor.
176 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
177 assert_eq!(
178 mem::take(&mut *events.borrow_mut()),
179 [
180 ("editor2", "edited"),
181 ("editor1", "buffer edited"),
182 ("editor2", "buffer edited"),
183 ]
184 );
185
186 // Redoing on editor 2 will emit an `Edited` event only for that editor.
187 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
188 assert_eq!(
189 mem::take(&mut *events.borrow_mut()),
190 [
191 ("editor2", "edited"),
192 ("editor1", "buffer edited"),
193 ("editor2", "buffer edited"),
194 ]
195 );
196
197 // No event is emitted when the mutation is a no-op.
198 _ = editor2.update(cx, |editor, window, cx| {
199 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
200 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
201 });
202
203 editor.backspace(&Backspace, window, cx);
204 });
205 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
206}
207
208#[gpui::test]
209fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
210 init_test(cx, |_| {});
211
212 let mut now = Instant::now();
213 let group_interval = Duration::from_millis(1);
214 let buffer = cx.new(|cx| {
215 let mut buf = language::Buffer::local("123456", cx);
216 buf.set_group_interval(group_interval);
217 buf
218 });
219 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
220 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
221
222 _ = editor.update(cx, |editor, window, cx| {
223 editor.start_transaction_at(now, window, cx);
224 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
225 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
226 });
227
228 editor.insert("cd", window, cx);
229 editor.end_transaction_at(now, cx);
230 assert_eq!(editor.text(cx), "12cd56");
231 assert_eq!(
232 editor.selections.ranges(&editor.display_snapshot(cx)),
233 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
234 );
235
236 editor.start_transaction_at(now, window, cx);
237 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
238 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
239 });
240 editor.insert("e", window, cx);
241 editor.end_transaction_at(now, cx);
242 assert_eq!(editor.text(cx), "12cde6");
243 assert_eq!(
244 editor.selections.ranges(&editor.display_snapshot(cx)),
245 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
246 );
247
248 now += group_interval + Duration::from_millis(1);
249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
250 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
251 });
252
253 // Simulate an edit in another editor
254 buffer.update(cx, |buffer, cx| {
255 buffer.start_transaction_at(now, cx);
256 buffer.edit(
257 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
258 None,
259 cx,
260 );
261 buffer.edit(
262 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
263 None,
264 cx,
265 );
266 buffer.end_transaction_at(now, cx);
267 });
268
269 assert_eq!(editor.text(cx), "ab2cde6");
270 assert_eq!(
271 editor.selections.ranges(&editor.display_snapshot(cx)),
272 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
273 );
274
275 // Last transaction happened past the group interval in a different editor.
276 // Undo it individually and don't restore selections.
277 editor.undo(&Undo, window, cx);
278 assert_eq!(editor.text(cx), "12cde6");
279 assert_eq!(
280 editor.selections.ranges(&editor.display_snapshot(cx)),
281 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
282 );
283
284 // First two transactions happened within the group interval in this editor.
285 // Undo them together and restore selections.
286 editor.undo(&Undo, window, cx);
287 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
288 assert_eq!(editor.text(cx), "123456");
289 assert_eq!(
290 editor.selections.ranges(&editor.display_snapshot(cx)),
291 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
292 );
293
294 // Redo the first two transactions together.
295 editor.redo(&Redo, window, cx);
296 assert_eq!(editor.text(cx), "12cde6");
297 assert_eq!(
298 editor.selections.ranges(&editor.display_snapshot(cx)),
299 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
300 );
301
302 // Redo the last transaction on its own.
303 editor.redo(&Redo, window, cx);
304 assert_eq!(editor.text(cx), "ab2cde6");
305 assert_eq!(
306 editor.selections.ranges(&editor.display_snapshot(cx)),
307 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
308 );
309
310 // Test empty transactions.
311 editor.start_transaction_at(now, window, cx);
312 editor.end_transaction_at(now, cx);
313 editor.undo(&Undo, window, cx);
314 assert_eq!(editor.text(cx), "12cde6");
315 });
316}
317
318#[gpui::test]
319fn test_ime_composition(cx: &mut TestAppContext) {
320 init_test(cx, |_| {});
321
322 let buffer = cx.new(|cx| {
323 let mut buffer = language::Buffer::local("abcde", cx);
324 // Ensure automatic grouping doesn't occur.
325 buffer.set_group_interval(Duration::ZERO);
326 buffer
327 });
328
329 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
330 cx.add_window(|window, cx| {
331 let mut editor = build_editor(buffer.clone(), window, cx);
332
333 // Start a new IME composition.
334 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
335 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
336 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
337 assert_eq!(editor.text(cx), "äbcde");
338 assert_eq!(
339 editor.marked_text_ranges(cx),
340 Some(vec![
341 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
342 ])
343 );
344
345 // Finalize IME composition.
346 editor.replace_text_in_range(None, "ā", window, cx);
347 assert_eq!(editor.text(cx), "ābcde");
348 assert_eq!(editor.marked_text_ranges(cx), None);
349
350 // IME composition edits are grouped and are undone/redone at once.
351 editor.undo(&Default::default(), window, cx);
352 assert_eq!(editor.text(cx), "abcde");
353 assert_eq!(editor.marked_text_ranges(cx), None);
354 editor.redo(&Default::default(), window, cx);
355 assert_eq!(editor.text(cx), "ābcde");
356 assert_eq!(editor.marked_text_ranges(cx), None);
357
358 // Start a new IME composition.
359 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
360 assert_eq!(
361 editor.marked_text_ranges(cx),
362 Some(vec![
363 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
364 ])
365 );
366
367 // Undoing during an IME composition cancels it.
368 editor.undo(&Default::default(), window, cx);
369 assert_eq!(editor.text(cx), "ābcde");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
373 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
374 assert_eq!(editor.text(cx), "ābcdè");
375 assert_eq!(
376 editor.marked_text_ranges(cx),
377 Some(vec![
378 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
379 ])
380 );
381
382 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
383 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
384 assert_eq!(editor.text(cx), "ābcdę");
385 assert_eq!(editor.marked_text_ranges(cx), None);
386
387 // Start a new IME composition with multiple cursors.
388 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
389 s.select_ranges([
390 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
391 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
392 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
393 ])
394 });
395 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
396 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
397 assert_eq!(
398 editor.marked_text_ranges(cx),
399 Some(vec![
400 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
401 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
402 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
403 ])
404 );
405
406 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
407 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
408 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
409 assert_eq!(
410 editor.marked_text_ranges(cx),
411 Some(vec![
412 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
413 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
414 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
415 ])
416 );
417
418 // Finalize IME composition with multiple cursors.
419 editor.replace_text_in_range(Some(9..10), "2", window, cx);
420 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
421 assert_eq!(editor.marked_text_ranges(cx), None);
422
423 editor
424 });
425}
426
427#[gpui::test]
428fn test_selection_with_mouse(cx: &mut TestAppContext) {
429 init_test(cx, |_| {});
430
431 let editor = cx.add_window(|window, cx| {
432 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
433 build_editor(buffer, window, cx)
434 });
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
438 });
439 assert_eq!(
440 editor
441 .update(cx, |editor, _, cx| display_ranges(editor, cx))
442 .unwrap(),
443 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
444 );
445
446 _ = editor.update(cx, |editor, window, cx| {
447 editor.update_selection(
448 DisplayPoint::new(DisplayRow(3), 3),
449 0,
450 gpui::Point::<f32>::default(),
451 window,
452 cx,
453 );
454 });
455
456 assert_eq!(
457 editor
458 .update(cx, |editor, _, cx| display_ranges(editor, cx))
459 .unwrap(),
460 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
461 );
462
463 _ = editor.update(cx, |editor, window, cx| {
464 editor.update_selection(
465 DisplayPoint::new(DisplayRow(1), 1),
466 0,
467 gpui::Point::<f32>::default(),
468 window,
469 cx,
470 );
471 });
472
473 assert_eq!(
474 editor
475 .update(cx, |editor, _, cx| display_ranges(editor, cx))
476 .unwrap(),
477 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
478 );
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 editor.update_selection(
483 DisplayPoint::new(DisplayRow(3), 3),
484 0,
485 gpui::Point::<f32>::default(),
486 window,
487 cx,
488 );
489 });
490
491 assert_eq!(
492 editor
493 .update(cx, |editor, _, cx| display_ranges(editor, cx))
494 .unwrap(),
495 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
496 );
497
498 _ = editor.update(cx, |editor, window, cx| {
499 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
500 editor.update_selection(
501 DisplayPoint::new(DisplayRow(0), 0),
502 0,
503 gpui::Point::<f32>::default(),
504 window,
505 cx,
506 );
507 });
508
509 assert_eq!(
510 editor
511 .update(cx, |editor, _, cx| display_ranges(editor, cx))
512 .unwrap(),
513 [
514 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
515 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
516 ]
517 );
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| display_ranges(editor, cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
528 );
529}
530
531#[gpui::test]
532fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.end_selection(window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
550 });
551
552 _ = editor.update(cx, |editor, window, cx| {
553 editor.end_selection(window, cx);
554 });
555
556 assert_eq!(
557 editor
558 .update(cx, |editor, _, cx| display_ranges(editor, cx))
559 .unwrap(),
560 [
561 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
562 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
563 ]
564 );
565
566 _ = editor.update(cx, |editor, window, cx| {
567 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
568 });
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.end_selection(window, cx);
572 });
573
574 assert_eq!(
575 editor
576 .update(cx, |editor, _, cx| display_ranges(editor, cx))
577 .unwrap(),
578 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
579 );
580}
581
582#[gpui::test]
583fn test_canceling_pending_selection(cx: &mut TestAppContext) {
584 init_test(cx, |_| {});
585
586 let editor = cx.add_window(|window, cx| {
587 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
588 build_editor(buffer, window, cx)
589 });
590
591 _ = editor.update(cx, |editor, window, cx| {
592 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
593 assert_eq!(
594 display_ranges(editor, cx),
595 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
596 );
597 });
598
599 _ = editor.update(cx, |editor, window, cx| {
600 editor.update_selection(
601 DisplayPoint::new(DisplayRow(3), 3),
602 0,
603 gpui::Point::<f32>::default(),
604 window,
605 cx,
606 );
607 assert_eq!(
608 display_ranges(editor, cx),
609 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
610 );
611 });
612
613 _ = editor.update(cx, |editor, window, cx| {
614 editor.cancel(&Cancel, window, cx);
615 editor.update_selection(
616 DisplayPoint::new(DisplayRow(1), 1),
617 0,
618 gpui::Point::<f32>::default(),
619 window,
620 cx,
621 );
622 assert_eq!(
623 display_ranges(editor, cx),
624 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
625 );
626 });
627}
628
629#[gpui::test]
630fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
631 init_test(cx, |_| {});
632
633 let editor = cx.add_window(|window, cx| {
634 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
635 build_editor(buffer, window, cx)
636 });
637
638 _ = editor.update(cx, |editor, window, cx| {
639 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
640 assert_eq!(
641 display_ranges(editor, cx),
642 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
643 );
644
645 editor.move_down(&Default::default(), window, cx);
646 assert_eq!(
647 display_ranges(editor, cx),
648 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
649 );
650
651 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
652 assert_eq!(
653 display_ranges(editor, cx),
654 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
655 );
656
657 editor.move_up(&Default::default(), window, cx);
658 assert_eq!(
659 display_ranges(editor, cx),
660 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
661 );
662 });
663}
664
665#[gpui::test]
666fn test_extending_selection(cx: &mut TestAppContext) {
667 init_test(cx, |_| {});
668
669 let editor = cx.add_window(|window, cx| {
670 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
671 build_editor(buffer, window, cx)
672 });
673
674 _ = editor.update(cx, |editor, window, cx| {
675 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
676 editor.end_selection(window, cx);
677 assert_eq!(
678 display_ranges(editor, cx),
679 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
680 );
681
682 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
683 editor.end_selection(window, cx);
684 assert_eq!(
685 display_ranges(editor, cx),
686 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
687 );
688
689 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
690 editor.end_selection(window, cx);
691 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
692 assert_eq!(
693 display_ranges(editor, cx),
694 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
695 );
696
697 editor.update_selection(
698 DisplayPoint::new(DisplayRow(0), 1),
699 0,
700 gpui::Point::<f32>::default(),
701 window,
702 cx,
703 );
704 editor.end_selection(window, cx);
705 assert_eq!(
706 display_ranges(editor, cx),
707 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
708 );
709
710 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
711 editor.end_selection(window, cx);
712 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
713 editor.end_selection(window, cx);
714 assert_eq!(
715 display_ranges(editor, cx),
716 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
717 );
718
719 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
720 assert_eq!(
721 display_ranges(editor, cx),
722 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
723 );
724
725 editor.update_selection(
726 DisplayPoint::new(DisplayRow(0), 6),
727 0,
728 gpui::Point::<f32>::default(),
729 window,
730 cx,
731 );
732 assert_eq!(
733 display_ranges(editor, cx),
734 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
735 );
736
737 editor.update_selection(
738 DisplayPoint::new(DisplayRow(0), 1),
739 0,
740 gpui::Point::<f32>::default(),
741 window,
742 cx,
743 );
744 editor.end_selection(window, cx);
745 assert_eq!(
746 display_ranges(editor, cx),
747 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
748 );
749 });
750}
751
752#[gpui::test]
753fn test_clone(cx: &mut TestAppContext) {
754 init_test(cx, |_| {});
755
756 let (text, selection_ranges) = marked_text_ranges(
757 indoc! {"
758 one
759 two
760 threeˇ
761 four
762 fiveˇ
763 "},
764 true,
765 );
766
767 let editor = cx.add_window(|window, cx| {
768 let buffer = MultiBuffer::build_simple(&text, cx);
769 build_editor(buffer, window, cx)
770 });
771
772 _ = editor.update(cx, |editor, window, cx| {
773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
774 s.select_ranges(
775 selection_ranges
776 .iter()
777 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
778 )
779 });
780 editor.fold_creases(
781 vec![
782 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
783 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
784 ],
785 true,
786 window,
787 cx,
788 );
789 });
790
791 let cloned_editor = editor
792 .update(cx, |editor, _, cx| {
793 cx.open_window(Default::default(), |window, cx| {
794 cx.new(|cx| editor.clone(window, cx))
795 })
796 })
797 .unwrap()
798 .unwrap();
799
800 let snapshot = editor
801 .update(cx, |e, window, cx| e.snapshot(window, cx))
802 .unwrap();
803 let cloned_snapshot = cloned_editor
804 .update(cx, |e, window, cx| e.snapshot(window, cx))
805 .unwrap();
806
807 assert_eq!(
808 cloned_editor
809 .update(cx, |e, _, cx| e.display_text(cx))
810 .unwrap(),
811 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
812 );
813 assert_eq!(
814 cloned_snapshot
815 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
816 .collect::<Vec<_>>(),
817 snapshot
818 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
819 .collect::<Vec<_>>(),
820 );
821 assert_set_eq!(
822 cloned_editor
823 .update(cx, |editor, _, cx| editor
824 .selections
825 .ranges::<Point>(&editor.display_snapshot(cx)))
826 .unwrap(),
827 editor
828 .update(cx, |editor, _, cx| editor
829 .selections
830 .ranges(&editor.display_snapshot(cx)))
831 .unwrap()
832 );
833 assert_set_eq!(
834 cloned_editor
835 .update(cx, |e, _window, cx| e
836 .selections
837 .display_ranges(&e.display_snapshot(cx)))
838 .unwrap(),
839 editor
840 .update(cx, |e, _, cx| e
841 .selections
842 .display_ranges(&e.display_snapshot(cx)))
843 .unwrap()
844 );
845}
846
847#[gpui::test]
848async fn test_navigation_history(cx: &mut TestAppContext) {
849 init_test(cx, |_| {});
850
851 use workspace::item::Item;
852
853 let fs = FakeFs::new(cx.executor());
854 let project = Project::test(fs, [], cx).await;
855 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
856 let pane = workspace
857 .update(cx, |workspace, _, _| workspace.active_pane().clone())
858 .unwrap();
859
860 _ = workspace.update(cx, |_v, window, cx| {
861 cx.new(|cx| {
862 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
863 let mut editor = build_editor(buffer, window, cx);
864 let handle = cx.entity();
865 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
866
867 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
868 editor.nav_history.as_mut().unwrap().pop_backward(cx)
869 }
870
871 // Move the cursor a small distance.
872 // Nothing is added to the navigation history.
873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
874 s.select_display_ranges([
875 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
876 ])
877 });
878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
879 s.select_display_ranges([
880 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
881 ])
882 });
883 assert!(pop_history(&mut editor, cx).is_none());
884
885 // Move the cursor a large distance.
886 // The history can jump back to the previous position.
887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
888 s.select_display_ranges([
889 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
890 ])
891 });
892 let nav_entry = pop_history(&mut editor, cx).unwrap();
893 editor.navigate(nav_entry.data.unwrap(), window, cx);
894 assert_eq!(nav_entry.item.id(), cx.entity_id());
895 assert_eq!(
896 editor
897 .selections
898 .display_ranges(&editor.display_snapshot(cx)),
899 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
900 );
901 assert!(pop_history(&mut editor, cx).is_none());
902
903 // Move the cursor a small distance via the mouse.
904 // Nothing is added to the navigation history.
905 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
906 editor.end_selection(window, cx);
907 assert_eq!(
908 editor
909 .selections
910 .display_ranges(&editor.display_snapshot(cx)),
911 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
912 );
913 assert!(pop_history(&mut editor, cx).is_none());
914
915 // Move the cursor a large distance via the mouse.
916 // The history can jump back to the previous position.
917 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
918 editor.end_selection(window, cx);
919 assert_eq!(
920 editor
921 .selections
922 .display_ranges(&editor.display_snapshot(cx)),
923 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
924 );
925 let nav_entry = pop_history(&mut editor, cx).unwrap();
926 editor.navigate(nav_entry.data.unwrap(), window, cx);
927 assert_eq!(nav_entry.item.id(), cx.entity_id());
928 assert_eq!(
929 editor
930 .selections
931 .display_ranges(&editor.display_snapshot(cx)),
932 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
933 );
934 assert!(pop_history(&mut editor, cx).is_none());
935
936 // Set scroll position to check later
937 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
938 let original_scroll_position = editor.scroll_manager.anchor();
939
940 // Jump to the end of the document and adjust scroll
941 editor.move_to_end(&MoveToEnd, window, cx);
942 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
943 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
944
945 let nav_entry = pop_history(&mut editor, cx).unwrap();
946 editor.navigate(nav_entry.data.unwrap(), window, cx);
947 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
948
949 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
950 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
951 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
952 let invalid_point = Point::new(9999, 0);
953 editor.navigate(
954 Box::new(NavigationData {
955 cursor_anchor: invalid_anchor,
956 cursor_position: invalid_point,
957 scroll_anchor: ScrollAnchor {
958 anchor: invalid_anchor,
959 offset: Default::default(),
960 },
961 scroll_top_row: invalid_point.row,
962 }),
963 window,
964 cx,
965 );
966 assert_eq!(
967 editor
968 .selections
969 .display_ranges(&editor.display_snapshot(cx)),
970 &[editor.max_point(cx)..editor.max_point(cx)]
971 );
972 assert_eq!(
973 editor.scroll_position(cx),
974 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
975 );
976
977 editor
978 })
979 });
980}
981
982#[gpui::test]
983fn test_cancel(cx: &mut TestAppContext) {
984 init_test(cx, |_| {});
985
986 let editor = cx.add_window(|window, cx| {
987 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
988 build_editor(buffer, window, cx)
989 });
990
991 _ = editor.update(cx, |editor, window, cx| {
992 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
993 editor.update_selection(
994 DisplayPoint::new(DisplayRow(1), 1),
995 0,
996 gpui::Point::<f32>::default(),
997 window,
998 cx,
999 );
1000 editor.end_selection(window, cx);
1001
1002 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1003 editor.update_selection(
1004 DisplayPoint::new(DisplayRow(0), 3),
1005 0,
1006 gpui::Point::<f32>::default(),
1007 window,
1008 cx,
1009 );
1010 editor.end_selection(window, cx);
1011 assert_eq!(
1012 display_ranges(editor, cx),
1013 [
1014 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1015 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1016 ]
1017 );
1018 });
1019
1020 _ = editor.update(cx, |editor, window, cx| {
1021 editor.cancel(&Cancel, window, cx);
1022 assert_eq!(
1023 display_ranges(editor, cx),
1024 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1025 );
1026 });
1027
1028 _ = editor.update(cx, |editor, window, cx| {
1029 editor.cancel(&Cancel, window, cx);
1030 assert_eq!(
1031 display_ranges(editor, cx),
1032 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1033 );
1034 });
1035}
1036
1037#[gpui::test]
1038fn test_fold_action(cx: &mut TestAppContext) {
1039 init_test(cx, |_| {});
1040
1041 let editor = cx.add_window(|window, cx| {
1042 let buffer = MultiBuffer::build_simple(
1043 &"
1044 impl Foo {
1045 // Hello!
1046
1047 fn a() {
1048 1
1049 }
1050
1051 fn b() {
1052 2
1053 }
1054
1055 fn c() {
1056 3
1057 }
1058 }
1059 "
1060 .unindent(),
1061 cx,
1062 );
1063 build_editor(buffer, window, cx)
1064 });
1065
1066 _ = editor.update(cx, |editor, window, cx| {
1067 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1068 s.select_display_ranges([
1069 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1070 ]);
1071 });
1072 editor.fold(&Fold, window, cx);
1073 assert_eq!(
1074 editor.display_text(cx),
1075 "
1076 impl Foo {
1077 // Hello!
1078
1079 fn a() {
1080 1
1081 }
1082
1083 fn b() {⋯
1084 }
1085
1086 fn c() {⋯
1087 }
1088 }
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 impl Foo {⋯
1098 }
1099 "
1100 .unindent(),
1101 );
1102
1103 editor.unfold_lines(&UnfoldLines, window, cx);
1104 assert_eq!(
1105 editor.display_text(cx),
1106 "
1107 impl Foo {
1108 // Hello!
1109
1110 fn a() {
1111 1
1112 }
1113
1114 fn b() {⋯
1115 }
1116
1117 fn c() {⋯
1118 }
1119 }
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148 def c():
1149 print(3)
1150 "
1151 .unindent(),
1152 cx,
1153 );
1154 build_editor(buffer, window, cx)
1155 });
1156
1157 _ = editor.update(cx, |editor, window, cx| {
1158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1159 s.select_display_ranges([
1160 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1161 ]);
1162 });
1163 editor.fold(&Fold, window, cx);
1164 assert_eq!(
1165 editor.display_text(cx),
1166 "
1167 class Foo:
1168 # Hello!
1169
1170 def a():
1171 print(1)
1172
1173 def b():⋯
1174
1175 def c():⋯
1176 "
1177 .unindent(),
1178 );
1179
1180 editor.fold(&Fold, window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:⋯
1185 "
1186 .unindent(),
1187 );
1188
1189 editor.unfold_lines(&UnfoldLines, window, cx);
1190 assert_eq!(
1191 editor.display_text(cx),
1192 "
1193 class Foo:
1194 # Hello!
1195
1196 def a():
1197 print(1)
1198
1199 def b():⋯
1200
1201 def c():⋯
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.unfold_lines(&UnfoldLines, window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 editor.buffer.read(cx).read(cx).text()
1210 );
1211 });
1212}
1213
1214#[gpui::test]
1215fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1216 init_test(cx, |_| {});
1217
1218 let editor = cx.add_window(|window, cx| {
1219 let buffer = MultiBuffer::build_simple(
1220 &"
1221 class Foo:
1222 # Hello!
1223
1224 def a():
1225 print(1)
1226
1227 def b():
1228 print(2)
1229
1230
1231 def c():
1232 print(3)
1233
1234
1235 "
1236 .unindent(),
1237 cx,
1238 );
1239 build_editor(buffer, window, cx)
1240 });
1241
1242 _ = editor.update(cx, |editor, window, cx| {
1243 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1244 s.select_display_ranges([
1245 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1246 ]);
1247 });
1248 editor.fold(&Fold, window, cx);
1249 assert_eq!(
1250 editor.display_text(cx),
1251 "
1252 class Foo:
1253 # Hello!
1254
1255 def a():
1256 print(1)
1257
1258 def b():⋯
1259
1260
1261 def c():⋯
1262
1263
1264 "
1265 .unindent(),
1266 );
1267
1268 editor.fold(&Fold, window, cx);
1269 assert_eq!(
1270 editor.display_text(cx),
1271 "
1272 class Foo:⋯
1273
1274
1275 "
1276 .unindent(),
1277 );
1278
1279 editor.unfold_lines(&UnfoldLines, window, cx);
1280 assert_eq!(
1281 editor.display_text(cx),
1282 "
1283 class Foo:
1284 # Hello!
1285
1286 def a():
1287 print(1)
1288
1289 def b():⋯
1290
1291
1292 def c():⋯
1293
1294
1295 "
1296 .unindent(),
1297 );
1298
1299 editor.unfold_lines(&UnfoldLines, window, cx);
1300 assert_eq!(
1301 editor.display_text(cx),
1302 editor.buffer.read(cx).read(cx).text()
1303 );
1304 });
1305}
1306
1307#[gpui::test]
1308fn test_fold_at_level(cx: &mut TestAppContext) {
1309 init_test(cx, |_| {});
1310
1311 let editor = cx.add_window(|window, cx| {
1312 let buffer = MultiBuffer::build_simple(
1313 &"
1314 class Foo:
1315 # Hello!
1316
1317 def a():
1318 print(1)
1319
1320 def b():
1321 print(2)
1322
1323
1324 class Bar:
1325 # World!
1326
1327 def a():
1328 print(1)
1329
1330 def b():
1331 print(2)
1332
1333
1334 "
1335 .unindent(),
1336 cx,
1337 );
1338 build_editor(buffer, window, cx)
1339 });
1340
1341 _ = editor.update(cx, |editor, window, cx| {
1342 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1343 assert_eq!(
1344 editor.display_text(cx),
1345 "
1346 class Foo:
1347 # Hello!
1348
1349 def a():⋯
1350
1351 def b():⋯
1352
1353
1354 class Bar:
1355 # World!
1356
1357 def a():⋯
1358
1359 def b():⋯
1360
1361
1362 "
1363 .unindent(),
1364 );
1365
1366 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1367 assert_eq!(
1368 editor.display_text(cx),
1369 "
1370 class Foo:⋯
1371
1372
1373 class Bar:⋯
1374
1375
1376 "
1377 .unindent(),
1378 );
1379
1380 editor.unfold_all(&UnfoldAll, window, cx);
1381 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1382 assert_eq!(
1383 editor.display_text(cx),
1384 "
1385 class Foo:
1386 # Hello!
1387
1388 def a():
1389 print(1)
1390
1391 def b():
1392 print(2)
1393
1394
1395 class Bar:
1396 # World!
1397
1398 def a():
1399 print(1)
1400
1401 def b():
1402 print(2)
1403
1404
1405 "
1406 .unindent(),
1407 );
1408
1409 assert_eq!(
1410 editor.display_text(cx),
1411 editor.buffer.read(cx).read(cx).text()
1412 );
1413 let (_, positions) = marked_text_ranges(
1414 &"
1415 class Foo:
1416 # Hello!
1417
1418 def a():
1419 print(1)
1420
1421 def b():
1422 p«riˇ»nt(2)
1423
1424
1425 class Bar:
1426 # World!
1427
1428 def a():
1429 «ˇprint(1)
1430
1431 def b():
1432 print(2)»
1433
1434
1435 "
1436 .unindent(),
1437 true,
1438 );
1439
1440 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1441 s.select_ranges(
1442 positions
1443 .iter()
1444 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1445 )
1446 });
1447
1448 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1449 assert_eq!(
1450 editor.display_text(cx),
1451 "
1452 class Foo:
1453 # Hello!
1454
1455 def a():⋯
1456
1457 def b():
1458 print(2)
1459
1460
1461 class Bar:
1462 # World!
1463
1464 def a():
1465 print(1)
1466
1467 def b():
1468 print(2)
1469
1470
1471 "
1472 .unindent(),
1473 );
1474 });
1475}
1476
1477#[gpui::test]
1478fn test_move_cursor(cx: &mut TestAppContext) {
1479 init_test(cx, |_| {});
1480
1481 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1482 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1483
1484 buffer.update(cx, |buffer, cx| {
1485 buffer.edit(
1486 vec![
1487 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1488 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1489 ],
1490 None,
1491 cx,
1492 );
1493 });
1494 _ = editor.update(cx, |editor, window, cx| {
1495 assert_eq!(
1496 display_ranges(editor, cx),
1497 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1498 );
1499
1500 editor.move_down(&MoveDown, window, cx);
1501 assert_eq!(
1502 display_ranges(editor, cx),
1503 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1504 );
1505
1506 editor.move_right(&MoveRight, window, cx);
1507 assert_eq!(
1508 display_ranges(editor, cx),
1509 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1510 );
1511
1512 editor.move_left(&MoveLeft, window, cx);
1513 assert_eq!(
1514 display_ranges(editor, cx),
1515 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1516 );
1517
1518 editor.move_up(&MoveUp, window, cx);
1519 assert_eq!(
1520 display_ranges(editor, cx),
1521 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1522 );
1523
1524 editor.move_to_end(&MoveToEnd, window, cx);
1525 assert_eq!(
1526 display_ranges(editor, cx),
1527 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1528 );
1529
1530 editor.move_to_beginning(&MoveToBeginning, window, cx);
1531 assert_eq!(
1532 display_ranges(editor, cx),
1533 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1534 );
1535
1536 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1537 s.select_display_ranges([
1538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1539 ]);
1540 });
1541 editor.select_to_beginning(&SelectToBeginning, window, cx);
1542 assert_eq!(
1543 display_ranges(editor, cx),
1544 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1545 );
1546
1547 editor.select_to_end(&SelectToEnd, window, cx);
1548 assert_eq!(
1549 display_ranges(editor, cx),
1550 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1551 );
1552 });
1553}
1554
1555#[gpui::test]
1556fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1557 init_test(cx, |_| {});
1558
1559 let editor = cx.add_window(|window, cx| {
1560 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1561 build_editor(buffer, window, cx)
1562 });
1563
1564 assert_eq!('🟥'.len_utf8(), 4);
1565 assert_eq!('α'.len_utf8(), 2);
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.fold_creases(
1569 vec![
1570 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1571 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1572 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1573 ],
1574 true,
1575 window,
1576 cx,
1577 );
1578 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1579
1580 editor.move_right(&MoveRight, window, cx);
1581 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
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
1587 editor.move_down(&MoveDown, window, cx);
1588 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1589 editor.move_left(&MoveLeft, window, cx);
1590 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".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, "a".len())]);
1595
1596 editor.move_down(&MoveDown, window, cx);
1597 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1598 editor.move_right(&MoveRight, 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
1605 editor.move_up(&MoveUp, window, cx);
1606 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1607 editor.move_down(&MoveDown, window, cx);
1608 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1609 editor.move_up(&MoveUp, window, cx);
1610 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1611
1612 editor.move_up(&MoveUp, window, cx);
1613 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1614 editor.move_left(&MoveLeft, 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 });
1619}
1620
1621#[gpui::test]
1622fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1623 init_test(cx, |_| {});
1624
1625 let editor = cx.add_window(|window, cx| {
1626 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1627 build_editor(buffer, window, cx)
1628 });
1629 _ = editor.update(cx, |editor, window, cx| {
1630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1631 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1632 });
1633
1634 // moving above start of document should move selection to start of document,
1635 // but the next move down should still be at the original goal_x
1636 editor.move_up(&MoveUp, window, cx);
1637 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1638
1639 editor.move_down(&MoveDown, window, cx);
1640 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1641
1642 editor.move_down(&MoveDown, window, cx);
1643 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1644
1645 editor.move_down(&MoveDown, window, cx);
1646 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1647
1648 editor.move_down(&MoveDown, window, cx);
1649 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1650
1651 // moving past end of document should not change goal_x
1652 editor.move_down(&MoveDown, window, cx);
1653 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1654
1655 editor.move_down(&MoveDown, window, cx);
1656 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1657
1658 editor.move_up(&MoveUp, window, cx);
1659 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1660
1661 editor.move_up(&MoveUp, window, cx);
1662 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1663
1664 editor.move_up(&MoveUp, window, cx);
1665 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1666 });
1667}
1668
1669#[gpui::test]
1670fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1671 init_test(cx, |_| {});
1672 let move_to_beg = MoveToBeginningOfLine {
1673 stop_at_soft_wraps: true,
1674 stop_at_indent: true,
1675 };
1676
1677 let delete_to_beg = DeleteToBeginningOfLine {
1678 stop_at_indent: false,
1679 };
1680
1681 let move_to_end = MoveToEndOfLine {
1682 stop_at_soft_wraps: true,
1683 };
1684
1685 let editor = cx.add_window(|window, cx| {
1686 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1687 build_editor(buffer, window, cx)
1688 });
1689 _ = editor.update(cx, |editor, window, cx| {
1690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1691 s.select_display_ranges([
1692 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1693 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1694 ]);
1695 });
1696 });
1697
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1700 assert_eq!(
1701 display_ranges(editor, cx),
1702 &[
1703 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1704 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1705 ]
1706 );
1707 });
1708
1709 _ = editor.update(cx, |editor, window, cx| {
1710 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1711 assert_eq!(
1712 display_ranges(editor, cx),
1713 &[
1714 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1715 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1716 ]
1717 );
1718 });
1719
1720 _ = editor.update(cx, |editor, window, cx| {
1721 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1722 assert_eq!(
1723 display_ranges(editor, cx),
1724 &[
1725 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1726 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1727 ]
1728 );
1729 });
1730
1731 _ = editor.update(cx, |editor, window, cx| {
1732 editor.move_to_end_of_line(&move_to_end, window, cx);
1733 assert_eq!(
1734 display_ranges(editor, cx),
1735 &[
1736 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1737 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1738 ]
1739 );
1740 });
1741
1742 // Moving to the end of line again is a no-op.
1743 _ = editor.update(cx, |editor, window, cx| {
1744 editor.move_to_end_of_line(&move_to_end, window, cx);
1745 assert_eq!(
1746 display_ranges(editor, cx),
1747 &[
1748 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1749 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1750 ]
1751 );
1752 });
1753
1754 _ = editor.update(cx, |editor, window, cx| {
1755 editor.move_left(&MoveLeft, window, cx);
1756 editor.select_to_beginning_of_line(
1757 &SelectToBeginningOfLine {
1758 stop_at_soft_wraps: true,
1759 stop_at_indent: true,
1760 },
1761 window,
1762 cx,
1763 );
1764 assert_eq!(
1765 display_ranges(editor, cx),
1766 &[
1767 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1768 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1769 ]
1770 );
1771 });
1772
1773 _ = editor.update(cx, |editor, window, cx| {
1774 editor.select_to_beginning_of_line(
1775 &SelectToBeginningOfLine {
1776 stop_at_soft_wraps: true,
1777 stop_at_indent: true,
1778 },
1779 window,
1780 cx,
1781 );
1782 assert_eq!(
1783 display_ranges(editor, cx),
1784 &[
1785 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1786 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1787 ]
1788 );
1789 });
1790
1791 _ = editor.update(cx, |editor, window, cx| {
1792 editor.select_to_beginning_of_line(
1793 &SelectToBeginningOfLine {
1794 stop_at_soft_wraps: true,
1795 stop_at_indent: true,
1796 },
1797 window,
1798 cx,
1799 );
1800 assert_eq!(
1801 display_ranges(editor, cx),
1802 &[
1803 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1804 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1805 ]
1806 );
1807 });
1808
1809 _ = editor.update(cx, |editor, window, cx| {
1810 editor.select_to_end_of_line(
1811 &SelectToEndOfLine {
1812 stop_at_soft_wraps: true,
1813 },
1814 window,
1815 cx,
1816 );
1817 assert_eq!(
1818 display_ranges(editor, cx),
1819 &[
1820 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1822 ]
1823 );
1824 });
1825
1826 _ = editor.update(cx, |editor, window, cx| {
1827 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1828 assert_eq!(editor.display_text(cx), "ab\n de");
1829 assert_eq!(
1830 display_ranges(editor, cx),
1831 &[
1832 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1833 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1834 ]
1835 );
1836 });
1837
1838 _ = editor.update(cx, |editor, window, cx| {
1839 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1840 assert_eq!(editor.display_text(cx), "\n");
1841 assert_eq!(
1842 display_ranges(editor, cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848 });
1849}
1850
1851#[gpui::test]
1852fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1853 init_test(cx, |_| {});
1854 let move_to_beg = MoveToBeginningOfLine {
1855 stop_at_soft_wraps: false,
1856 stop_at_indent: false,
1857 };
1858
1859 let move_to_end = MoveToEndOfLine {
1860 stop_at_soft_wraps: false,
1861 };
1862
1863 let editor = cx.add_window(|window, cx| {
1864 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1865 build_editor(buffer, window, cx)
1866 });
1867
1868 _ = editor.update(cx, |editor, window, cx| {
1869 editor.set_wrap_width(Some(140.0.into()), cx);
1870
1871 // We expect the following lines after wrapping
1872 // ```
1873 // thequickbrownfox
1874 // jumpedoverthelazydo
1875 // gs
1876 // ```
1877 // The final `gs` was soft-wrapped onto a new line.
1878 assert_eq!(
1879 "thequickbrownfox\njumpedoverthelaz\nydogs",
1880 editor.display_text(cx),
1881 );
1882
1883 // First, let's assert behavior on the first line, that was not soft-wrapped.
1884 // Start the cursor at the `k` on the first line
1885 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1886 s.select_display_ranges([
1887 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1888 ]);
1889 });
1890
1891 // Moving to the beginning of the line should put us at the beginning of the line.
1892 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1893 assert_eq!(
1894 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1895 display_ranges(editor, cx)
1896 );
1897
1898 // Moving to the end of the line should put us at the end of the line.
1899 editor.move_to_end_of_line(&move_to_end, window, cx);
1900 assert_eq!(
1901 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1902 display_ranges(editor, cx)
1903 );
1904
1905 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1906 // Start the cursor at the last line (`y` that was wrapped to a new line)
1907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1908 s.select_display_ranges([
1909 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1910 ]);
1911 });
1912
1913 // Moving to the beginning of the line should put us at the start of the second line of
1914 // display text, i.e., the `j`.
1915 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1916 assert_eq!(
1917 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1918 display_ranges(editor, cx)
1919 );
1920
1921 // Moving to the beginning of the line again should be a no-op.
1922 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1923 assert_eq!(
1924 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1925 display_ranges(editor, cx)
1926 );
1927
1928 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1929 // next display line.
1930 editor.move_to_end_of_line(&move_to_end, window, cx);
1931 assert_eq!(
1932 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1933 display_ranges(editor, cx)
1934 );
1935
1936 // Moving to the end of the line again should be a no-op.
1937 editor.move_to_end_of_line(&move_to_end, window, cx);
1938 assert_eq!(
1939 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1940 display_ranges(editor, cx)
1941 );
1942 });
1943}
1944
1945#[gpui::test]
1946fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1947 init_test(cx, |_| {});
1948
1949 let move_to_beg = MoveToBeginningOfLine {
1950 stop_at_soft_wraps: true,
1951 stop_at_indent: true,
1952 };
1953
1954 let select_to_beg = SelectToBeginningOfLine {
1955 stop_at_soft_wraps: true,
1956 stop_at_indent: true,
1957 };
1958
1959 let delete_to_beg = DeleteToBeginningOfLine {
1960 stop_at_indent: true,
1961 };
1962
1963 let move_to_end = MoveToEndOfLine {
1964 stop_at_soft_wraps: false,
1965 };
1966
1967 let editor = cx.add_window(|window, cx| {
1968 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1969 build_editor(buffer, window, cx)
1970 });
1971
1972 _ = editor.update(cx, |editor, window, cx| {
1973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1976 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1977 ]);
1978 });
1979
1980 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1981 // and the second cursor at the first non-whitespace character in the line.
1982 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1983 assert_eq!(
1984 display_ranges(editor, cx),
1985 &[
1986 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1987 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1988 ]
1989 );
1990
1991 // Moving to the beginning of the line again should be a no-op for the first cursor,
1992 // and should move the second cursor to the beginning of the line.
1993 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1994 assert_eq!(
1995 display_ranges(editor, cx),
1996 &[
1997 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1998 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1999 ]
2000 );
2001
2002 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2003 // and should move the second cursor back to the first non-whitespace character in the line.
2004 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2005 assert_eq!(
2006 display_ranges(editor, cx),
2007 &[
2008 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2009 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2010 ]
2011 );
2012
2013 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2014 // and to the first non-whitespace character in the line for the second cursor.
2015 editor.move_to_end_of_line(&move_to_end, window, cx);
2016 editor.move_left(&MoveLeft, window, cx);
2017 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2018 assert_eq!(
2019 display_ranges(editor, cx),
2020 &[
2021 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2022 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2023 ]
2024 );
2025
2026 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2027 // and should select to the beginning of the line for the second cursor.
2028 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2029 assert_eq!(
2030 display_ranges(editor, cx),
2031 &[
2032 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2033 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2034 ]
2035 );
2036
2037 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2038 // and should delete to the first non-whitespace character in the line for the second cursor.
2039 editor.move_to_end_of_line(&move_to_end, window, cx);
2040 editor.move_left(&MoveLeft, window, cx);
2041 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2042 assert_eq!(editor.text(cx), "c\n f");
2043 });
2044}
2045
2046#[gpui::test]
2047fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2048 init_test(cx, |_| {});
2049
2050 let move_to_beg = MoveToBeginningOfLine {
2051 stop_at_soft_wraps: true,
2052 stop_at_indent: true,
2053 };
2054
2055 let editor = cx.add_window(|window, cx| {
2056 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2057 build_editor(buffer, window, cx)
2058 });
2059
2060 _ = editor.update(cx, |editor, window, cx| {
2061 // test cursor between line_start and indent_start
2062 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2063 s.select_display_ranges([
2064 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2065 ]);
2066 });
2067
2068 // cursor should move to line_start
2069 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2070 assert_eq!(
2071 display_ranges(editor, cx),
2072 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2073 );
2074
2075 // cursor should move to indent_start
2076 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2077 assert_eq!(
2078 display_ranges(editor, cx),
2079 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2080 );
2081
2082 // cursor should move to back to line_start
2083 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2084 assert_eq!(
2085 display_ranges(editor, cx),
2086 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2087 );
2088 });
2089}
2090
2091#[gpui::test]
2092fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2093 init_test(cx, |_| {});
2094
2095 let editor = cx.add_window(|window, cx| {
2096 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2097 build_editor(buffer, window, cx)
2098 });
2099 _ = editor.update(cx, |editor, window, cx| {
2100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2101 s.select_display_ranges([
2102 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2103 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2104 ])
2105 });
2106 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2107 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2108
2109 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2110 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2111
2112 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2113 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2114
2115 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2116 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2117
2118 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2119 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2120
2121 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2122 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2123
2124 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2125 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2126
2127 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2128 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2129
2130 editor.move_right(&MoveRight, window, cx);
2131 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2132 assert_selection_ranges(
2133 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2134 editor,
2135 cx,
2136 );
2137
2138 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2139 assert_selection_ranges(
2140 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2141 editor,
2142 cx,
2143 );
2144
2145 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2146 assert_selection_ranges(
2147 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2148 editor,
2149 cx,
2150 );
2151 });
2152}
2153
2154#[gpui::test]
2155fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2156 init_test(cx, |_| {});
2157
2158 let editor = cx.add_window(|window, cx| {
2159 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2160 build_editor(buffer, window, cx)
2161 });
2162
2163 _ = editor.update(cx, |editor, window, cx| {
2164 editor.set_wrap_width(Some(140.0.into()), cx);
2165 assert_eq!(
2166 editor.display_text(cx),
2167 "use one::{\n two::three::\n four::five\n};"
2168 );
2169
2170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2171 s.select_display_ranges([
2172 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2173 ]);
2174 });
2175
2176 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2177 assert_eq!(
2178 display_ranges(editor, cx),
2179 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2180 );
2181
2182 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2183 assert_eq!(
2184 display_ranges(editor, cx),
2185 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2186 );
2187
2188 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2189 assert_eq!(
2190 display_ranges(editor, cx),
2191 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2192 );
2193
2194 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2195 assert_eq!(
2196 display_ranges(editor, cx),
2197 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2198 );
2199
2200 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2201 assert_eq!(
2202 display_ranges(editor, cx),
2203 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2204 );
2205
2206 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2207 assert_eq!(
2208 display_ranges(editor, cx),
2209 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2210 );
2211 });
2212}
2213
2214#[gpui::test]
2215async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2216 init_test(cx, |_| {});
2217 let mut cx = EditorTestContext::new(cx).await;
2218
2219 let line_height = cx.editor(|editor, window, _| {
2220 editor
2221 .style()
2222 .unwrap()
2223 .text
2224 .line_height_in_pixels(window.rem_size())
2225 });
2226 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2227
2228 cx.set_state(
2229 &r#"ˇone
2230 two
2231
2232 three
2233 fourˇ
2234 five
2235
2236 six"#
2237 .unindent(),
2238 );
2239
2240 cx.update_editor(|editor, window, cx| {
2241 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2242 });
2243 cx.assert_editor_state(
2244 &r#"one
2245 two
2246 ˇ
2247 three
2248 four
2249 five
2250 ˇ
2251 six"#
2252 .unindent(),
2253 );
2254
2255 cx.update_editor(|editor, window, cx| {
2256 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2257 });
2258 cx.assert_editor_state(
2259 &r#"one
2260 two
2261
2262 three
2263 four
2264 five
2265 ˇ
2266 sixˇ"#
2267 .unindent(),
2268 );
2269
2270 cx.update_editor(|editor, window, cx| {
2271 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2272 });
2273 cx.assert_editor_state(
2274 &r#"one
2275 two
2276
2277 three
2278 four
2279 five
2280
2281 sixˇ"#
2282 .unindent(),
2283 );
2284
2285 cx.update_editor(|editor, window, cx| {
2286 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2287 });
2288 cx.assert_editor_state(
2289 &r#"one
2290 two
2291
2292 three
2293 four
2294 five
2295 ˇ
2296 six"#
2297 .unindent(),
2298 );
2299
2300 cx.update_editor(|editor, window, cx| {
2301 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2302 });
2303 cx.assert_editor_state(
2304 &r#"one
2305 two
2306 ˇ
2307 three
2308 four
2309 five
2310
2311 six"#
2312 .unindent(),
2313 );
2314
2315 cx.update_editor(|editor, window, cx| {
2316 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2317 });
2318 cx.assert_editor_state(
2319 &r#"ˇone
2320 two
2321
2322 three
2323 four
2324 five
2325
2326 six"#
2327 .unindent(),
2328 );
2329}
2330
2331#[gpui::test]
2332async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2333 init_test(cx, |_| {});
2334 let mut cx = EditorTestContext::new(cx).await;
2335 let line_height = cx.editor(|editor, window, _| {
2336 editor
2337 .style()
2338 .unwrap()
2339 .text
2340 .line_height_in_pixels(window.rem_size())
2341 });
2342 let window = cx.window;
2343 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2344
2345 cx.set_state(
2346 r#"ˇone
2347 two
2348 three
2349 four
2350 five
2351 six
2352 seven
2353 eight
2354 nine
2355 ten
2356 "#,
2357 );
2358
2359 cx.update_editor(|editor, window, cx| {
2360 assert_eq!(
2361 editor.snapshot(window, cx).scroll_position(),
2362 gpui::Point::new(0., 0.)
2363 );
2364 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2365 assert_eq!(
2366 editor.snapshot(window, cx).scroll_position(),
2367 gpui::Point::new(0., 3.)
2368 );
2369 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2370 assert_eq!(
2371 editor.snapshot(window, cx).scroll_position(),
2372 gpui::Point::new(0., 6.)
2373 );
2374 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2375 assert_eq!(
2376 editor.snapshot(window, cx).scroll_position(),
2377 gpui::Point::new(0., 3.)
2378 );
2379
2380 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2381 assert_eq!(
2382 editor.snapshot(window, cx).scroll_position(),
2383 gpui::Point::new(0., 1.)
2384 );
2385 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2386 assert_eq!(
2387 editor.snapshot(window, cx).scroll_position(),
2388 gpui::Point::new(0., 3.)
2389 );
2390 });
2391}
2392
2393#[gpui::test]
2394async fn test_autoscroll(cx: &mut TestAppContext) {
2395 init_test(cx, |_| {});
2396 let mut cx = EditorTestContext::new(cx).await;
2397
2398 let line_height = cx.update_editor(|editor, window, cx| {
2399 editor.set_vertical_scroll_margin(2, cx);
2400 editor
2401 .style()
2402 .unwrap()
2403 .text
2404 .line_height_in_pixels(window.rem_size())
2405 });
2406 let window = cx.window;
2407 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2408
2409 cx.set_state(
2410 r#"ˇone
2411 two
2412 three
2413 four
2414 five
2415 six
2416 seven
2417 eight
2418 nine
2419 ten
2420 "#,
2421 );
2422 cx.update_editor(|editor, window, cx| {
2423 assert_eq!(
2424 editor.snapshot(window, cx).scroll_position(),
2425 gpui::Point::new(0., 0.0)
2426 );
2427 });
2428
2429 // Add a cursor below the visible area. Since both cursors cannot fit
2430 // on screen, the editor autoscrolls to reveal the newest cursor, and
2431 // allows the vertical scroll margin below that cursor.
2432 cx.update_editor(|editor, window, cx| {
2433 editor.change_selections(Default::default(), window, cx, |selections| {
2434 selections.select_ranges([
2435 Point::new(0, 0)..Point::new(0, 0),
2436 Point::new(6, 0)..Point::new(6, 0),
2437 ]);
2438 })
2439 });
2440 cx.update_editor(|editor, window, cx| {
2441 assert_eq!(
2442 editor.snapshot(window, cx).scroll_position(),
2443 gpui::Point::new(0., 3.0)
2444 );
2445 });
2446
2447 // Move down. The editor cursor scrolls down to track the newest cursor.
2448 cx.update_editor(|editor, window, cx| {
2449 editor.move_down(&Default::default(), window, cx);
2450 });
2451 cx.update_editor(|editor, window, cx| {
2452 assert_eq!(
2453 editor.snapshot(window, cx).scroll_position(),
2454 gpui::Point::new(0., 4.0)
2455 );
2456 });
2457
2458 // Add a cursor above the visible area. Since both cursors fit on screen,
2459 // the editor scrolls to show both.
2460 cx.update_editor(|editor, window, cx| {
2461 editor.change_selections(Default::default(), window, cx, |selections| {
2462 selections.select_ranges([
2463 Point::new(1, 0)..Point::new(1, 0),
2464 Point::new(6, 0)..Point::new(6, 0),
2465 ]);
2466 })
2467 });
2468 cx.update_editor(|editor, window, cx| {
2469 assert_eq!(
2470 editor.snapshot(window, cx).scroll_position(),
2471 gpui::Point::new(0., 1.0)
2472 );
2473 });
2474}
2475
2476#[gpui::test]
2477async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2478 init_test(cx, |_| {});
2479 let mut cx = EditorTestContext::new(cx).await;
2480
2481 let line_height = cx.editor(|editor, window, _cx| {
2482 editor
2483 .style()
2484 .unwrap()
2485 .text
2486 .line_height_in_pixels(window.rem_size())
2487 });
2488 let window = cx.window;
2489 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2490 cx.set_state(
2491 &r#"
2492 ˇone
2493 two
2494 threeˇ
2495 four
2496 five
2497 six
2498 seven
2499 eight
2500 nine
2501 ten
2502 "#
2503 .unindent(),
2504 );
2505
2506 cx.update_editor(|editor, window, cx| {
2507 editor.move_page_down(&MovePageDown::default(), window, cx)
2508 });
2509 cx.assert_editor_state(
2510 &r#"
2511 one
2512 two
2513 three
2514 ˇfour
2515 five
2516 sixˇ
2517 seven
2518 eight
2519 nine
2520 ten
2521 "#
2522 .unindent(),
2523 );
2524
2525 cx.update_editor(|editor, window, cx| {
2526 editor.move_page_down(&MovePageDown::default(), window, cx)
2527 });
2528 cx.assert_editor_state(
2529 &r#"
2530 one
2531 two
2532 three
2533 four
2534 five
2535 six
2536 ˇseven
2537 eight
2538 nineˇ
2539 ten
2540 "#
2541 .unindent(),
2542 );
2543
2544 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2545 cx.assert_editor_state(
2546 &r#"
2547 one
2548 two
2549 three
2550 ˇfour
2551 five
2552 sixˇ
2553 seven
2554 eight
2555 nine
2556 ten
2557 "#
2558 .unindent(),
2559 );
2560
2561 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2562 cx.assert_editor_state(
2563 &r#"
2564 ˇone
2565 two
2566 threeˇ
2567 four
2568 five
2569 six
2570 seven
2571 eight
2572 nine
2573 ten
2574 "#
2575 .unindent(),
2576 );
2577
2578 // Test select collapsing
2579 cx.update_editor(|editor, window, cx| {
2580 editor.move_page_down(&MovePageDown::default(), window, cx);
2581 editor.move_page_down(&MovePageDown::default(), window, cx);
2582 editor.move_page_down(&MovePageDown::default(), window, cx);
2583 });
2584 cx.assert_editor_state(
2585 &r#"
2586 one
2587 two
2588 three
2589 four
2590 five
2591 six
2592 seven
2593 eight
2594 nine
2595 ˇten
2596 ˇ"#
2597 .unindent(),
2598 );
2599}
2600
2601#[gpui::test]
2602async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2603 init_test(cx, |_| {});
2604 let mut cx = EditorTestContext::new(cx).await;
2605 cx.set_state("one «two threeˇ» four");
2606 cx.update_editor(|editor, window, cx| {
2607 editor.delete_to_beginning_of_line(
2608 &DeleteToBeginningOfLine {
2609 stop_at_indent: false,
2610 },
2611 window,
2612 cx,
2613 );
2614 assert_eq!(editor.text(cx), " four");
2615 });
2616}
2617
2618#[gpui::test]
2619async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2620 init_test(cx, |_| {});
2621
2622 let mut cx = EditorTestContext::new(cx).await;
2623
2624 // For an empty selection, the preceding word fragment is deleted.
2625 // For non-empty selections, only selected characters are deleted.
2626 cx.set_state("onˇe two t«hreˇ»e four");
2627 cx.update_editor(|editor, window, cx| {
2628 editor.delete_to_previous_word_start(
2629 &DeleteToPreviousWordStart {
2630 ignore_newlines: false,
2631 ignore_brackets: false,
2632 },
2633 window,
2634 cx,
2635 );
2636 });
2637 cx.assert_editor_state("ˇe two tˇe four");
2638
2639 cx.set_state("e tˇwo te «fˇ»our");
2640 cx.update_editor(|editor, window, cx| {
2641 editor.delete_to_next_word_end(
2642 &DeleteToNextWordEnd {
2643 ignore_newlines: false,
2644 ignore_brackets: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 });
2650 cx.assert_editor_state("e tˇ te ˇour");
2651}
2652
2653#[gpui::test]
2654async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2655 init_test(cx, |_| {});
2656
2657 let mut cx = EditorTestContext::new(cx).await;
2658
2659 cx.set_state("here is some text ˇwith a space");
2660 cx.update_editor(|editor, window, cx| {
2661 editor.delete_to_previous_word_start(
2662 &DeleteToPreviousWordStart {
2663 ignore_newlines: false,
2664 ignore_brackets: true,
2665 },
2666 window,
2667 cx,
2668 );
2669 });
2670 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2671 cx.assert_editor_state("here is some textˇwith a space");
2672
2673 cx.set_state("here is some text ˇwith a space");
2674 cx.update_editor(|editor, window, cx| {
2675 editor.delete_to_previous_word_start(
2676 &DeleteToPreviousWordStart {
2677 ignore_newlines: false,
2678 ignore_brackets: false,
2679 },
2680 window,
2681 cx,
2682 );
2683 });
2684 cx.assert_editor_state("here is some textˇwith a space");
2685
2686 cx.set_state("here is some textˇ with a space");
2687 cx.update_editor(|editor, window, cx| {
2688 editor.delete_to_next_word_end(
2689 &DeleteToNextWordEnd {
2690 ignore_newlines: false,
2691 ignore_brackets: true,
2692 },
2693 window,
2694 cx,
2695 );
2696 });
2697 // Same happens in the other direction.
2698 cx.assert_editor_state("here is some textˇwith a space");
2699
2700 cx.set_state("here is some textˇ with a space");
2701 cx.update_editor(|editor, window, cx| {
2702 editor.delete_to_next_word_end(
2703 &DeleteToNextWordEnd {
2704 ignore_newlines: false,
2705 ignore_brackets: false,
2706 },
2707 window,
2708 cx,
2709 );
2710 });
2711 cx.assert_editor_state("here is some textˇwith a space");
2712
2713 cx.set_state("here is some textˇ with a space");
2714 cx.update_editor(|editor, window, cx| {
2715 editor.delete_to_next_word_end(
2716 &DeleteToNextWordEnd {
2717 ignore_newlines: true,
2718 ignore_brackets: false,
2719 },
2720 window,
2721 cx,
2722 );
2723 });
2724 cx.assert_editor_state("here is some textˇwith a space");
2725 cx.update_editor(|editor, window, cx| {
2726 editor.delete_to_previous_word_start(
2727 &DeleteToPreviousWordStart {
2728 ignore_newlines: true,
2729 ignore_brackets: false,
2730 },
2731 window,
2732 cx,
2733 );
2734 });
2735 cx.assert_editor_state("here is some ˇwith a space");
2736 cx.update_editor(|editor, window, cx| {
2737 editor.delete_to_previous_word_start(
2738 &DeleteToPreviousWordStart {
2739 ignore_newlines: true,
2740 ignore_brackets: false,
2741 },
2742 window,
2743 cx,
2744 );
2745 });
2746 // Single whitespaces are removed with the word behind them.
2747 cx.assert_editor_state("here is ˇwith a space");
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_previous_word_start(
2750 &DeleteToPreviousWordStart {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 cx.assert_editor_state("here ˇwith a space");
2759 cx.update_editor(|editor, window, cx| {
2760 editor.delete_to_previous_word_start(
2761 &DeleteToPreviousWordStart {
2762 ignore_newlines: true,
2763 ignore_brackets: false,
2764 },
2765 window,
2766 cx,
2767 );
2768 });
2769 cx.assert_editor_state("ˇwith a space");
2770 cx.update_editor(|editor, window, cx| {
2771 editor.delete_to_previous_word_start(
2772 &DeleteToPreviousWordStart {
2773 ignore_newlines: true,
2774 ignore_brackets: false,
2775 },
2776 window,
2777 cx,
2778 );
2779 });
2780 cx.assert_editor_state("ˇwith a space");
2781 cx.update_editor(|editor, window, cx| {
2782 editor.delete_to_next_word_end(
2783 &DeleteToNextWordEnd {
2784 ignore_newlines: true,
2785 ignore_brackets: false,
2786 },
2787 window,
2788 cx,
2789 );
2790 });
2791 // Same happens in the other direction.
2792 cx.assert_editor_state("ˇ a space");
2793 cx.update_editor(|editor, window, cx| {
2794 editor.delete_to_next_word_end(
2795 &DeleteToNextWordEnd {
2796 ignore_newlines: true,
2797 ignore_brackets: false,
2798 },
2799 window,
2800 cx,
2801 );
2802 });
2803 cx.assert_editor_state("ˇ space");
2804 cx.update_editor(|editor, window, cx| {
2805 editor.delete_to_next_word_end(
2806 &DeleteToNextWordEnd {
2807 ignore_newlines: true,
2808 ignore_brackets: false,
2809 },
2810 window,
2811 cx,
2812 );
2813 });
2814 cx.assert_editor_state("ˇ");
2815 cx.update_editor(|editor, window, cx| {
2816 editor.delete_to_next_word_end(
2817 &DeleteToNextWordEnd {
2818 ignore_newlines: true,
2819 ignore_brackets: false,
2820 },
2821 window,
2822 cx,
2823 );
2824 });
2825 cx.assert_editor_state("ˇ");
2826 cx.update_editor(|editor, window, cx| {
2827 editor.delete_to_previous_word_start(
2828 &DeleteToPreviousWordStart {
2829 ignore_newlines: true,
2830 ignore_brackets: false,
2831 },
2832 window,
2833 cx,
2834 );
2835 });
2836 cx.assert_editor_state("ˇ");
2837}
2838
2839#[gpui::test]
2840async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2841 init_test(cx, |_| {});
2842
2843 let language = Arc::new(
2844 Language::new(
2845 LanguageConfig {
2846 brackets: BracketPairConfig {
2847 pairs: vec![
2848 BracketPair {
2849 start: "\"".to_string(),
2850 end: "\"".to_string(),
2851 close: true,
2852 surround: true,
2853 newline: false,
2854 },
2855 BracketPair {
2856 start: "(".to_string(),
2857 end: ")".to_string(),
2858 close: true,
2859 surround: true,
2860 newline: true,
2861 },
2862 ],
2863 ..BracketPairConfig::default()
2864 },
2865 ..LanguageConfig::default()
2866 },
2867 Some(tree_sitter_rust::LANGUAGE.into()),
2868 )
2869 .with_brackets_query(
2870 r#"
2871 ("(" @open ")" @close)
2872 ("\"" @open "\"" @close)
2873 "#,
2874 )
2875 .unwrap(),
2876 );
2877
2878 let mut cx = EditorTestContext::new(cx).await;
2879 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2880
2881 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2882 cx.update_editor(|editor, window, cx| {
2883 editor.delete_to_previous_word_start(
2884 &DeleteToPreviousWordStart {
2885 ignore_newlines: true,
2886 ignore_brackets: false,
2887 },
2888 window,
2889 cx,
2890 );
2891 });
2892 // Deletion stops before brackets if asked to not ignore them.
2893 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2894 cx.update_editor(|editor, window, cx| {
2895 editor.delete_to_previous_word_start(
2896 &DeleteToPreviousWordStart {
2897 ignore_newlines: true,
2898 ignore_brackets: false,
2899 },
2900 window,
2901 cx,
2902 );
2903 });
2904 // Deletion has to remove a single bracket and then stop again.
2905 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2906
2907 cx.update_editor(|editor, window, cx| {
2908 editor.delete_to_previous_word_start(
2909 &DeleteToPreviousWordStart {
2910 ignore_newlines: true,
2911 ignore_brackets: false,
2912 },
2913 window,
2914 cx,
2915 );
2916 });
2917 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2918
2919 cx.update_editor(|editor, window, cx| {
2920 editor.delete_to_previous_word_start(
2921 &DeleteToPreviousWordStart {
2922 ignore_newlines: true,
2923 ignore_brackets: false,
2924 },
2925 window,
2926 cx,
2927 );
2928 });
2929 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2930
2931 cx.update_editor(|editor, window, cx| {
2932 editor.delete_to_previous_word_start(
2933 &DeleteToPreviousWordStart {
2934 ignore_newlines: true,
2935 ignore_brackets: false,
2936 },
2937 window,
2938 cx,
2939 );
2940 });
2941 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2942
2943 cx.update_editor(|editor, window, cx| {
2944 editor.delete_to_next_word_end(
2945 &DeleteToNextWordEnd {
2946 ignore_newlines: true,
2947 ignore_brackets: false,
2948 },
2949 window,
2950 cx,
2951 );
2952 });
2953 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2954 cx.assert_editor_state(r#"ˇ");"#);
2955
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_next_word_end(
2958 &DeleteToNextWordEnd {
2959 ignore_newlines: true,
2960 ignore_brackets: false,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 cx.assert_editor_state(r#"ˇ"#);
2967
2968 cx.update_editor(|editor, window, cx| {
2969 editor.delete_to_next_word_end(
2970 &DeleteToNextWordEnd {
2971 ignore_newlines: true,
2972 ignore_brackets: false,
2973 },
2974 window,
2975 cx,
2976 );
2977 });
2978 cx.assert_editor_state(r#"ˇ"#);
2979
2980 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2981 cx.update_editor(|editor, window, cx| {
2982 editor.delete_to_previous_word_start(
2983 &DeleteToPreviousWordStart {
2984 ignore_newlines: true,
2985 ignore_brackets: true,
2986 },
2987 window,
2988 cx,
2989 );
2990 });
2991 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2992}
2993
2994#[gpui::test]
2995fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2996 init_test(cx, |_| {});
2997
2998 let editor = cx.add_window(|window, cx| {
2999 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3000 build_editor(buffer, window, cx)
3001 });
3002 let del_to_prev_word_start = DeleteToPreviousWordStart {
3003 ignore_newlines: false,
3004 ignore_brackets: false,
3005 };
3006 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3007 ignore_newlines: true,
3008 ignore_brackets: false,
3009 };
3010
3011 _ = editor.update(cx, |editor, window, cx| {
3012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3013 s.select_display_ranges([
3014 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3015 ])
3016 });
3017 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3018 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
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");
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\n");
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");
3025 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3026 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
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(), "");
3029 });
3030}
3031
3032#[gpui::test]
3033fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3034 init_test(cx, |_| {});
3035
3036 let editor = cx.add_window(|window, cx| {
3037 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3038 build_editor(buffer, window, cx)
3039 });
3040 let del_to_next_word_end = DeleteToNextWordEnd {
3041 ignore_newlines: false,
3042 ignore_brackets: false,
3043 };
3044 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3045 ignore_newlines: true,
3046 ignore_brackets: false,
3047 };
3048
3049 _ = editor.update(cx, |editor, window, cx| {
3050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3051 s.select_display_ranges([
3052 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3053 ])
3054 });
3055 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3056 assert_eq!(
3057 editor.buffer.read(cx).read(cx).text(),
3058 "one\n two\nthree\n four"
3059 );
3060 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3061 assert_eq!(
3062 editor.buffer.read(cx).read(cx).text(),
3063 "\n two\nthree\n four"
3064 );
3065 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3066 assert_eq!(
3067 editor.buffer.read(cx).read(cx).text(),
3068 "two\nthree\n four"
3069 );
3070 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3071 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3072 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3073 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\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(), "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(), "");
3078 });
3079}
3080
3081#[gpui::test]
3082fn test_newline(cx: &mut TestAppContext) {
3083 init_test(cx, |_| {});
3084
3085 let editor = cx.add_window(|window, cx| {
3086 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3087 build_editor(buffer, window, cx)
3088 });
3089
3090 _ = editor.update(cx, |editor, window, cx| {
3091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3092 s.select_display_ranges([
3093 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3094 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3095 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3096 ])
3097 });
3098
3099 editor.newline(&Newline, window, cx);
3100 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_newline_yaml(cx: &mut TestAppContext) {
3106 init_test(cx, |_| {});
3107
3108 let mut cx = EditorTestContext::new(cx).await;
3109 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3110 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3111
3112 // Object (between 2 fields)
3113 cx.set_state(indoc! {"
3114 test:ˇ
3115 hello: bye"});
3116 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 test:
3119 ˇ
3120 hello: bye"});
3121
3122 // Object (first and single line)
3123 cx.set_state(indoc! {"
3124 test:ˇ"});
3125 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3126 cx.assert_editor_state(indoc! {"
3127 test:
3128 ˇ"});
3129
3130 // Array with objects (after first element)
3131 cx.set_state(indoc! {"
3132 test:
3133 - foo: barˇ"});
3134 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3135 cx.assert_editor_state(indoc! {"
3136 test:
3137 - foo: bar
3138 ˇ"});
3139
3140 // Array with objects and comment
3141 cx.set_state(indoc! {"
3142 test:
3143 - foo: bar
3144 - bar: # testˇ"});
3145 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3146 cx.assert_editor_state(indoc! {"
3147 test:
3148 - foo: bar
3149 - bar: # test
3150 ˇ"});
3151
3152 // Array with objects (after second element)
3153 cx.set_state(indoc! {"
3154 test:
3155 - foo: bar
3156 - bar: fooˇ"});
3157 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 test:
3160 - foo: bar
3161 - bar: foo
3162 ˇ"});
3163
3164 // Array with strings (after first element)
3165 cx.set_state(indoc! {"
3166 test:
3167 - fooˇ"});
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 test:
3171 - foo
3172 ˇ"});
3173}
3174
3175#[gpui::test]
3176fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3177 init_test(cx, |_| {});
3178
3179 let editor = cx.add_window(|window, cx| {
3180 let buffer = MultiBuffer::build_simple(
3181 "
3182 a
3183 b(
3184 X
3185 )
3186 c(
3187 X
3188 )
3189 "
3190 .unindent()
3191 .as_str(),
3192 cx,
3193 );
3194 let mut editor = build_editor(buffer, window, cx);
3195 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3196 s.select_ranges([
3197 Point::new(2, 4)..Point::new(2, 5),
3198 Point::new(5, 4)..Point::new(5, 5),
3199 ])
3200 });
3201 editor
3202 });
3203
3204 _ = editor.update(cx, |editor, window, cx| {
3205 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3206 editor.buffer.update(cx, |buffer, cx| {
3207 buffer.edit(
3208 [
3209 (Point::new(1, 2)..Point::new(3, 0), ""),
3210 (Point::new(4, 2)..Point::new(6, 0), ""),
3211 ],
3212 None,
3213 cx,
3214 );
3215 assert_eq!(
3216 buffer.read(cx).text(),
3217 "
3218 a
3219 b()
3220 c()
3221 "
3222 .unindent()
3223 );
3224 });
3225 assert_eq!(
3226 editor.selections.ranges(&editor.display_snapshot(cx)),
3227 &[
3228 Point::new(1, 2)..Point::new(1, 2),
3229 Point::new(2, 2)..Point::new(2, 2),
3230 ],
3231 );
3232
3233 editor.newline(&Newline, window, cx);
3234 assert_eq!(
3235 editor.text(cx),
3236 "
3237 a
3238 b(
3239 )
3240 c(
3241 )
3242 "
3243 .unindent()
3244 );
3245
3246 // The selections are moved after the inserted newlines
3247 assert_eq!(
3248 editor.selections.ranges(&editor.display_snapshot(cx)),
3249 &[
3250 Point::new(2, 0)..Point::new(2, 0),
3251 Point::new(4, 0)..Point::new(4, 0),
3252 ],
3253 );
3254 });
3255}
3256
3257#[gpui::test]
3258async fn test_newline_above(cx: &mut TestAppContext) {
3259 init_test(cx, |settings| {
3260 settings.defaults.tab_size = NonZeroU32::new(4)
3261 });
3262
3263 let language = Arc::new(
3264 Language::new(
3265 LanguageConfig::default(),
3266 Some(tree_sitter_rust::LANGUAGE.into()),
3267 )
3268 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3269 .unwrap(),
3270 );
3271
3272 let mut cx = EditorTestContext::new(cx).await;
3273 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3274 cx.set_state(indoc! {"
3275 const a: ˇA = (
3276 (ˇ
3277 «const_functionˇ»(ˇ),
3278 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3279 )ˇ
3280 ˇ);ˇ
3281 "});
3282
3283 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3284 cx.assert_editor_state(indoc! {"
3285 ˇ
3286 const a: A = (
3287 ˇ
3288 (
3289 ˇ
3290 ˇ
3291 const_function(),
3292 ˇ
3293 ˇ
3294 ˇ
3295 ˇ
3296 something_else,
3297 ˇ
3298 )
3299 ˇ
3300 ˇ
3301 );
3302 "});
3303}
3304
3305#[gpui::test]
3306async fn test_newline_below(cx: &mut TestAppContext) {
3307 init_test(cx, |settings| {
3308 settings.defaults.tab_size = NonZeroU32::new(4)
3309 });
3310
3311 let language = Arc::new(
3312 Language::new(
3313 LanguageConfig::default(),
3314 Some(tree_sitter_rust::LANGUAGE.into()),
3315 )
3316 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3317 .unwrap(),
3318 );
3319
3320 let mut cx = EditorTestContext::new(cx).await;
3321 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3322 cx.set_state(indoc! {"
3323 const a: ˇA = (
3324 (ˇ
3325 «const_functionˇ»(ˇ),
3326 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3327 )ˇ
3328 ˇ);ˇ
3329 "});
3330
3331 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3332 cx.assert_editor_state(indoc! {"
3333 const a: A = (
3334 ˇ
3335 (
3336 ˇ
3337 const_function(),
3338 ˇ
3339 ˇ
3340 something_else,
3341 ˇ
3342 ˇ
3343 ˇ
3344 ˇ
3345 )
3346 ˇ
3347 );
3348 ˇ
3349 ˇ
3350 "});
3351}
3352
3353#[gpui::test]
3354async fn test_newline_comments(cx: &mut TestAppContext) {
3355 init_test(cx, |settings| {
3356 settings.defaults.tab_size = NonZeroU32::new(4)
3357 });
3358
3359 let language = Arc::new(Language::new(
3360 LanguageConfig {
3361 line_comments: vec!["// ".into()],
3362 ..LanguageConfig::default()
3363 },
3364 None,
3365 ));
3366 {
3367 let mut cx = EditorTestContext::new(cx).await;
3368 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3369 cx.set_state(indoc! {"
3370 // Fooˇ
3371 "});
3372
3373 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3374 cx.assert_editor_state(indoc! {"
3375 // Foo
3376 // ˇ
3377 "});
3378 // Ensure that we add comment prefix when existing line contains space
3379 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3380 cx.assert_editor_state(
3381 indoc! {"
3382 // Foo
3383 //s
3384 // ˇ
3385 "}
3386 .replace("s", " ") // s is used as space placeholder to prevent format on save
3387 .as_str(),
3388 );
3389 // Ensure that we add comment prefix when existing line does not contain space
3390 cx.set_state(indoc! {"
3391 // Foo
3392 //ˇ
3393 "});
3394 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3395 cx.assert_editor_state(indoc! {"
3396 // Foo
3397 //
3398 // ˇ
3399 "});
3400 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3401 cx.set_state(indoc! {"
3402 ˇ// Foo
3403 "});
3404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406
3407 ˇ// Foo
3408 "});
3409 }
3410 // Ensure that comment continuations can be disabled.
3411 update_test_language_settings(cx, |settings| {
3412 settings.defaults.extend_comment_on_newline = Some(false);
3413 });
3414 let mut cx = EditorTestContext::new(cx).await;
3415 cx.set_state(indoc! {"
3416 // Fooˇ
3417 "});
3418 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 // Foo
3421 ˇ
3422 "});
3423}
3424
3425#[gpui::test]
3426async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3427 init_test(cx, |settings| {
3428 settings.defaults.tab_size = NonZeroU32::new(4)
3429 });
3430
3431 let language = Arc::new(Language::new(
3432 LanguageConfig {
3433 line_comments: vec!["// ".into(), "/// ".into()],
3434 ..LanguageConfig::default()
3435 },
3436 None,
3437 ));
3438 {
3439 let mut cx = EditorTestContext::new(cx).await;
3440 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3441 cx.set_state(indoc! {"
3442 //ˇ
3443 "});
3444 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3445 cx.assert_editor_state(indoc! {"
3446 //
3447 // ˇ
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 ///ˇ
3452 "});
3453 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3454 cx.assert_editor_state(indoc! {"
3455 ///
3456 /// ˇ
3457 "});
3458 }
3459}
3460
3461#[gpui::test]
3462async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3463 init_test(cx, |settings| {
3464 settings.defaults.tab_size = NonZeroU32::new(4)
3465 });
3466
3467 let language = Arc::new(
3468 Language::new(
3469 LanguageConfig {
3470 documentation_comment: Some(language::BlockCommentConfig {
3471 start: "/**".into(),
3472 end: "*/".into(),
3473 prefix: "* ".into(),
3474 tab_size: 1,
3475 }),
3476
3477 ..LanguageConfig::default()
3478 },
3479 Some(tree_sitter_rust::LANGUAGE.into()),
3480 )
3481 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3482 .unwrap(),
3483 );
3484
3485 {
3486 let mut cx = EditorTestContext::new(cx).await;
3487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3488 cx.set_state(indoc! {"
3489 /**ˇ
3490 "});
3491
3492 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3493 cx.assert_editor_state(indoc! {"
3494 /**
3495 * ˇ
3496 "});
3497 // Ensure that if cursor is before the comment start,
3498 // we do not actually insert a comment prefix.
3499 cx.set_state(indoc! {"
3500 ˇ/**
3501 "});
3502 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504
3505 ˇ/**
3506 "});
3507 // Ensure that if cursor is between it doesn't add comment prefix.
3508 cx.set_state(indoc! {"
3509 /*ˇ*
3510 "});
3511 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3512 cx.assert_editor_state(indoc! {"
3513 /*
3514 ˇ*
3515 "});
3516 // Ensure that if suffix exists on same line after cursor it adds new line.
3517 cx.set_state(indoc! {"
3518 /**ˇ*/
3519 "});
3520 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 /**
3523 * ˇ
3524 */
3525 "});
3526 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3527 cx.set_state(indoc! {"
3528 /**ˇ */
3529 "});
3530 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3531 cx.assert_editor_state(indoc! {"
3532 /**
3533 * ˇ
3534 */
3535 "});
3536 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3537 cx.set_state(indoc! {"
3538 /** ˇ*/
3539 "});
3540 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3541 cx.assert_editor_state(
3542 indoc! {"
3543 /**s
3544 * ˇ
3545 */
3546 "}
3547 .replace("s", " ") // s is used as space placeholder to prevent format on save
3548 .as_str(),
3549 );
3550 // Ensure that delimiter space is preserved when newline on already
3551 // spaced delimiter.
3552 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3553 cx.assert_editor_state(
3554 indoc! {"
3555 /**s
3556 *s
3557 * ˇ
3558 */
3559 "}
3560 .replace("s", " ") // s is used as space placeholder to prevent format on save
3561 .as_str(),
3562 );
3563 // Ensure that delimiter space is preserved when space is not
3564 // on existing delimiter.
3565 cx.set_state(indoc! {"
3566 /**
3567 *ˇ
3568 */
3569 "});
3570 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3571 cx.assert_editor_state(indoc! {"
3572 /**
3573 *
3574 * ˇ
3575 */
3576 "});
3577 // Ensure that if suffix exists on same line after cursor it
3578 // doesn't add extra new line if prefix is not on same line.
3579 cx.set_state(indoc! {"
3580 /**
3581 ˇ*/
3582 "});
3583 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3584 cx.assert_editor_state(indoc! {"
3585 /**
3586
3587 ˇ*/
3588 "});
3589 // Ensure that it detects suffix after existing prefix.
3590 cx.set_state(indoc! {"
3591 /**ˇ/
3592 "});
3593 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 /**
3596 ˇ/
3597 "});
3598 // Ensure that if suffix exists on same line before
3599 // cursor it does not add comment prefix.
3600 cx.set_state(indoc! {"
3601 /** */ˇ
3602 "});
3603 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3604 cx.assert_editor_state(indoc! {"
3605 /** */
3606 ˇ
3607 "});
3608 // Ensure that if suffix exists on same line before
3609 // cursor it does not add comment prefix.
3610 cx.set_state(indoc! {"
3611 /**
3612 *
3613 */ˇ
3614 "});
3615 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3616 cx.assert_editor_state(indoc! {"
3617 /**
3618 *
3619 */
3620 ˇ
3621 "});
3622
3623 // Ensure that inline comment followed by code
3624 // doesn't add comment prefix on newline
3625 cx.set_state(indoc! {"
3626 /** */ textˇ
3627 "});
3628 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3629 cx.assert_editor_state(indoc! {"
3630 /** */ text
3631 ˇ
3632 "});
3633
3634 // Ensure that text after comment end tag
3635 // doesn't add comment prefix on newline
3636 cx.set_state(indoc! {"
3637 /**
3638 *
3639 */ˇtext
3640 "});
3641 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3642 cx.assert_editor_state(indoc! {"
3643 /**
3644 *
3645 */
3646 ˇtext
3647 "});
3648
3649 // Ensure if not comment block it doesn't
3650 // add comment prefix on newline
3651 cx.set_state(indoc! {"
3652 * textˇ
3653 "});
3654 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3655 cx.assert_editor_state(indoc! {"
3656 * text
3657 ˇ
3658 "});
3659 }
3660 // Ensure that comment continuations can be disabled.
3661 update_test_language_settings(cx, |settings| {
3662 settings.defaults.extend_comment_on_newline = Some(false);
3663 });
3664 let mut cx = EditorTestContext::new(cx).await;
3665 cx.set_state(indoc! {"
3666 /**ˇ
3667 "});
3668 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3669 cx.assert_editor_state(indoc! {"
3670 /**
3671 ˇ
3672 "});
3673}
3674
3675#[gpui::test]
3676async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.defaults.tab_size = NonZeroU32::new(4)
3679 });
3680
3681 let lua_language = Arc::new(Language::new(
3682 LanguageConfig {
3683 line_comments: vec!["--".into()],
3684 block_comment: Some(language::BlockCommentConfig {
3685 start: "--[[".into(),
3686 prefix: "".into(),
3687 end: "]]".into(),
3688 tab_size: 0,
3689 }),
3690 ..LanguageConfig::default()
3691 },
3692 None,
3693 ));
3694
3695 let mut cx = EditorTestContext::new(cx).await;
3696 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3697
3698 // Line with line comment should extend
3699 cx.set_state(indoc! {"
3700 --ˇ
3701 "});
3702 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 --
3705 --ˇ
3706 "});
3707
3708 // Line with block comment that matches line comment should not extend
3709 cx.set_state(indoc! {"
3710 --[[ˇ
3711 "});
3712 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3713 cx.assert_editor_state(indoc! {"
3714 --[[
3715 ˇ
3716 "});
3717}
3718
3719#[gpui::test]
3720fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3721 init_test(cx, |_| {});
3722
3723 let editor = cx.add_window(|window, cx| {
3724 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3725 let mut editor = build_editor(buffer, window, cx);
3726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3727 s.select_ranges([
3728 MultiBufferOffset(3)..MultiBufferOffset(4),
3729 MultiBufferOffset(11)..MultiBufferOffset(12),
3730 MultiBufferOffset(19)..MultiBufferOffset(20),
3731 ])
3732 });
3733 editor
3734 });
3735
3736 _ = editor.update(cx, |editor, window, cx| {
3737 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3738 editor.buffer.update(cx, |buffer, cx| {
3739 buffer.edit(
3740 [
3741 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3742 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3743 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3744 ],
3745 None,
3746 cx,
3747 );
3748 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3749 });
3750 assert_eq!(
3751 editor.selections.ranges(&editor.display_snapshot(cx)),
3752 &[
3753 MultiBufferOffset(2)..MultiBufferOffset(2),
3754 MultiBufferOffset(7)..MultiBufferOffset(7),
3755 MultiBufferOffset(12)..MultiBufferOffset(12)
3756 ],
3757 );
3758
3759 editor.insert("Z", window, cx);
3760 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3761
3762 // The selections are moved after the inserted characters
3763 assert_eq!(
3764 editor.selections.ranges(&editor.display_snapshot(cx)),
3765 &[
3766 MultiBufferOffset(3)..MultiBufferOffset(3),
3767 MultiBufferOffset(9)..MultiBufferOffset(9),
3768 MultiBufferOffset(15)..MultiBufferOffset(15)
3769 ],
3770 );
3771 });
3772}
3773
3774#[gpui::test]
3775async fn test_tab(cx: &mut TestAppContext) {
3776 init_test(cx, |settings| {
3777 settings.defaults.tab_size = NonZeroU32::new(3)
3778 });
3779
3780 let mut cx = EditorTestContext::new(cx).await;
3781 cx.set_state(indoc! {"
3782 ˇabˇc
3783 ˇ🏀ˇ🏀ˇefg
3784 dˇ
3785 "});
3786 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3787 cx.assert_editor_state(indoc! {"
3788 ˇab ˇc
3789 ˇ🏀 ˇ🏀 ˇefg
3790 d ˇ
3791 "});
3792
3793 cx.set_state(indoc! {"
3794 a
3795 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3796 "});
3797 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3798 cx.assert_editor_state(indoc! {"
3799 a
3800 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3801 "});
3802}
3803
3804#[gpui::test]
3805async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3806 init_test(cx, |_| {});
3807
3808 let mut cx = EditorTestContext::new(cx).await;
3809 let language = Arc::new(
3810 Language::new(
3811 LanguageConfig::default(),
3812 Some(tree_sitter_rust::LANGUAGE.into()),
3813 )
3814 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3815 .unwrap(),
3816 );
3817 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3818
3819 // test when all cursors are not at suggested indent
3820 // then simply move to their suggested indent location
3821 cx.set_state(indoc! {"
3822 const a: B = (
3823 c(
3824 ˇ
3825 ˇ )
3826 );
3827 "});
3828 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3829 cx.assert_editor_state(indoc! {"
3830 const a: B = (
3831 c(
3832 ˇ
3833 ˇ)
3834 );
3835 "});
3836
3837 // test cursor already at suggested indent not moving when
3838 // other cursors are yet to reach their suggested indents
3839 cx.set_state(indoc! {"
3840 ˇ
3841 const a: B = (
3842 c(
3843 d(
3844 ˇ
3845 )
3846 ˇ
3847 ˇ )
3848 );
3849 "});
3850 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3851 cx.assert_editor_state(indoc! {"
3852 ˇ
3853 const a: B = (
3854 c(
3855 d(
3856 ˇ
3857 )
3858 ˇ
3859 ˇ)
3860 );
3861 "});
3862 // test when all cursors are at suggested indent then tab is inserted
3863 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3864 cx.assert_editor_state(indoc! {"
3865 ˇ
3866 const a: B = (
3867 c(
3868 d(
3869 ˇ
3870 )
3871 ˇ
3872 ˇ)
3873 );
3874 "});
3875
3876 // test when current indent is less than suggested indent,
3877 // we adjust line to match suggested indent and move cursor to it
3878 //
3879 // when no other cursor is at word boundary, all of them should move
3880 cx.set_state(indoc! {"
3881 const a: B = (
3882 c(
3883 d(
3884 ˇ
3885 ˇ )
3886 ˇ )
3887 );
3888 "});
3889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3890 cx.assert_editor_state(indoc! {"
3891 const a: B = (
3892 c(
3893 d(
3894 ˇ
3895 ˇ)
3896 ˇ)
3897 );
3898 "});
3899
3900 // test when current indent is less than suggested indent,
3901 // we adjust line to match suggested indent and move cursor to it
3902 //
3903 // when some other cursor is at word boundary, it should not move
3904 cx.set_state(indoc! {"
3905 const a: B = (
3906 c(
3907 d(
3908 ˇ
3909 ˇ )
3910 ˇ)
3911 );
3912 "});
3913 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3914 cx.assert_editor_state(indoc! {"
3915 const a: B = (
3916 c(
3917 d(
3918 ˇ
3919 ˇ)
3920 ˇ)
3921 );
3922 "});
3923
3924 // test when current indent is more than suggested indent,
3925 // we just move cursor to current indent instead of suggested indent
3926 //
3927 // when no other cursor is at word boundary, all of them should move
3928 cx.set_state(indoc! {"
3929 const a: B = (
3930 c(
3931 d(
3932 ˇ
3933 ˇ )
3934 ˇ )
3935 );
3936 "});
3937 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3938 cx.assert_editor_state(indoc! {"
3939 const a: B = (
3940 c(
3941 d(
3942 ˇ
3943 ˇ)
3944 ˇ)
3945 );
3946 "});
3947 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3948 cx.assert_editor_state(indoc! {"
3949 const a: B = (
3950 c(
3951 d(
3952 ˇ
3953 ˇ)
3954 ˇ)
3955 );
3956 "});
3957
3958 // test when current indent is more than suggested indent,
3959 // we just move cursor to current indent instead of suggested indent
3960 //
3961 // when some other cursor is at word boundary, it doesn't move
3962 cx.set_state(indoc! {"
3963 const a: B = (
3964 c(
3965 d(
3966 ˇ
3967 ˇ )
3968 ˇ)
3969 );
3970 "});
3971 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3972 cx.assert_editor_state(indoc! {"
3973 const a: B = (
3974 c(
3975 d(
3976 ˇ
3977 ˇ)
3978 ˇ)
3979 );
3980 "});
3981
3982 // handle auto-indent when there are multiple cursors on the same line
3983 cx.set_state(indoc! {"
3984 const a: B = (
3985 c(
3986 ˇ ˇ
3987 ˇ )
3988 );
3989 "});
3990 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3991 cx.assert_editor_state(indoc! {"
3992 const a: B = (
3993 c(
3994 ˇ
3995 ˇ)
3996 );
3997 "});
3998}
3999
4000#[gpui::test]
4001async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4002 init_test(cx, |settings| {
4003 settings.defaults.tab_size = NonZeroU32::new(3)
4004 });
4005
4006 let mut cx = EditorTestContext::new(cx).await;
4007 cx.set_state(indoc! {"
4008 ˇ
4009 \t ˇ
4010 \t ˇ
4011 \t ˇ
4012 \t \t\t \t \t\t \t\t \t \t ˇ
4013 "});
4014
4015 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4016 cx.assert_editor_state(indoc! {"
4017 ˇ
4018 \t ˇ
4019 \t ˇ
4020 \t ˇ
4021 \t \t\t \t \t\t \t\t \t \t ˇ
4022 "});
4023}
4024
4025#[gpui::test]
4026async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4027 init_test(cx, |settings| {
4028 settings.defaults.tab_size = NonZeroU32::new(4)
4029 });
4030
4031 let language = Arc::new(
4032 Language::new(
4033 LanguageConfig::default(),
4034 Some(tree_sitter_rust::LANGUAGE.into()),
4035 )
4036 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4037 .unwrap(),
4038 );
4039
4040 let mut cx = EditorTestContext::new(cx).await;
4041 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4042 cx.set_state(indoc! {"
4043 fn a() {
4044 if b {
4045 \t ˇc
4046 }
4047 }
4048 "});
4049
4050 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4051 cx.assert_editor_state(indoc! {"
4052 fn a() {
4053 if b {
4054 ˇc
4055 }
4056 }
4057 "});
4058}
4059
4060#[gpui::test]
4061async fn test_indent_outdent(cx: &mut TestAppContext) {
4062 init_test(cx, |settings| {
4063 settings.defaults.tab_size = NonZeroU32::new(4);
4064 });
4065
4066 let mut cx = EditorTestContext::new(cx).await;
4067
4068 cx.set_state(indoc! {"
4069 «oneˇ» «twoˇ»
4070 three
4071 four
4072 "});
4073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4074 cx.assert_editor_state(indoc! {"
4075 «oneˇ» «twoˇ»
4076 three
4077 four
4078 "});
4079
4080 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4081 cx.assert_editor_state(indoc! {"
4082 «oneˇ» «twoˇ»
4083 three
4084 four
4085 "});
4086
4087 // select across line ending
4088 cx.set_state(indoc! {"
4089 one two
4090 t«hree
4091 ˇ» four
4092 "});
4093 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4094 cx.assert_editor_state(indoc! {"
4095 one two
4096 t«hree
4097 ˇ» four
4098 "});
4099
4100 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4101 cx.assert_editor_state(indoc! {"
4102 one two
4103 t«hree
4104 ˇ» four
4105 "});
4106
4107 // Ensure that indenting/outdenting works when the cursor is at column 0.
4108 cx.set_state(indoc! {"
4109 one two
4110 ˇthree
4111 four
4112 "});
4113 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4114 cx.assert_editor_state(indoc! {"
4115 one two
4116 ˇthree
4117 four
4118 "});
4119
4120 cx.set_state(indoc! {"
4121 one two
4122 ˇ three
4123 four
4124 "});
4125 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4126 cx.assert_editor_state(indoc! {"
4127 one two
4128 ˇthree
4129 four
4130 "});
4131}
4132
4133#[gpui::test]
4134async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4135 // This is a regression test for issue #33761
4136 init_test(cx, |_| {});
4137
4138 let mut cx = EditorTestContext::new(cx).await;
4139 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4140 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4141
4142 cx.set_state(
4143 r#"ˇ# ingress:
4144ˇ# api:
4145ˇ# enabled: false
4146ˇ# pathType: Prefix
4147ˇ# console:
4148ˇ# enabled: false
4149ˇ# pathType: Prefix
4150"#,
4151 );
4152
4153 // Press tab to indent all lines
4154 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4155
4156 cx.assert_editor_state(
4157 r#" ˇ# ingress:
4158 ˇ# api:
4159 ˇ# enabled: false
4160 ˇ# pathType: Prefix
4161 ˇ# console:
4162 ˇ# enabled: false
4163 ˇ# pathType: Prefix
4164"#,
4165 );
4166}
4167
4168#[gpui::test]
4169async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4170 // This is a test to make sure our fix for issue #33761 didn't break anything
4171 init_test(cx, |_| {});
4172
4173 let mut cx = EditorTestContext::new(cx).await;
4174 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4175 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4176
4177 cx.set_state(
4178 r#"ˇingress:
4179ˇ api:
4180ˇ enabled: false
4181ˇ pathType: Prefix
4182"#,
4183 );
4184
4185 // Press tab to indent all lines
4186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4187
4188 cx.assert_editor_state(
4189 r#"ˇingress:
4190 ˇapi:
4191 ˇenabled: false
4192 ˇpathType: Prefix
4193"#,
4194 );
4195}
4196
4197#[gpui::test]
4198async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4199 init_test(cx, |settings| {
4200 settings.defaults.hard_tabs = Some(true);
4201 });
4202
4203 let mut cx = EditorTestContext::new(cx).await;
4204
4205 // select two ranges on one line
4206 cx.set_state(indoc! {"
4207 «oneˇ» «twoˇ»
4208 three
4209 four
4210 "});
4211 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4212 cx.assert_editor_state(indoc! {"
4213 \t«oneˇ» «twoˇ»
4214 three
4215 four
4216 "});
4217 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 \t\t«oneˇ» «twoˇ»
4220 three
4221 four
4222 "});
4223 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4224 cx.assert_editor_state(indoc! {"
4225 \t«oneˇ» «twoˇ»
4226 three
4227 four
4228 "});
4229 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4230 cx.assert_editor_state(indoc! {"
4231 «oneˇ» «twoˇ»
4232 three
4233 four
4234 "});
4235
4236 // select across a line ending
4237 cx.set_state(indoc! {"
4238 one two
4239 t«hree
4240 ˇ»four
4241 "});
4242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4243 cx.assert_editor_state(indoc! {"
4244 one two
4245 \tt«hree
4246 ˇ»four
4247 "});
4248 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4249 cx.assert_editor_state(indoc! {"
4250 one two
4251 \t\tt«hree
4252 ˇ»four
4253 "});
4254 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4255 cx.assert_editor_state(indoc! {"
4256 one two
4257 \tt«hree
4258 ˇ»four
4259 "});
4260 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4261 cx.assert_editor_state(indoc! {"
4262 one two
4263 t«hree
4264 ˇ»four
4265 "});
4266
4267 // Ensure that indenting/outdenting works when the cursor is at column 0.
4268 cx.set_state(indoc! {"
4269 one two
4270 ˇthree
4271 four
4272 "});
4273 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4274 cx.assert_editor_state(indoc! {"
4275 one two
4276 ˇthree
4277 four
4278 "});
4279 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4280 cx.assert_editor_state(indoc! {"
4281 one two
4282 \tˇthree
4283 four
4284 "});
4285 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4286 cx.assert_editor_state(indoc! {"
4287 one two
4288 ˇthree
4289 four
4290 "});
4291}
4292
4293#[gpui::test]
4294fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4295 init_test(cx, |settings| {
4296 settings.languages.0.extend([
4297 (
4298 "TOML".into(),
4299 LanguageSettingsContent {
4300 tab_size: NonZeroU32::new(2),
4301 ..Default::default()
4302 },
4303 ),
4304 (
4305 "Rust".into(),
4306 LanguageSettingsContent {
4307 tab_size: NonZeroU32::new(4),
4308 ..Default::default()
4309 },
4310 ),
4311 ]);
4312 });
4313
4314 let toml_language = Arc::new(Language::new(
4315 LanguageConfig {
4316 name: "TOML".into(),
4317 ..Default::default()
4318 },
4319 None,
4320 ));
4321 let rust_language = Arc::new(Language::new(
4322 LanguageConfig {
4323 name: "Rust".into(),
4324 ..Default::default()
4325 },
4326 None,
4327 ));
4328
4329 let toml_buffer =
4330 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4331 let rust_buffer =
4332 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4333 let multibuffer = cx.new(|cx| {
4334 let mut multibuffer = MultiBuffer::new(ReadWrite);
4335 multibuffer.push_excerpts(
4336 toml_buffer.clone(),
4337 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4338 cx,
4339 );
4340 multibuffer.push_excerpts(
4341 rust_buffer.clone(),
4342 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4343 cx,
4344 );
4345 multibuffer
4346 });
4347
4348 cx.add_window(|window, cx| {
4349 let mut editor = build_editor(multibuffer, window, cx);
4350
4351 assert_eq!(
4352 editor.text(cx),
4353 indoc! {"
4354 a = 1
4355 b = 2
4356
4357 const c: usize = 3;
4358 "}
4359 );
4360
4361 select_ranges(
4362 &mut editor,
4363 indoc! {"
4364 «aˇ» = 1
4365 b = 2
4366
4367 «const c:ˇ» usize = 3;
4368 "},
4369 window,
4370 cx,
4371 );
4372
4373 editor.tab(&Tab, window, cx);
4374 assert_text_with_selections(
4375 &mut editor,
4376 indoc! {"
4377 «aˇ» = 1
4378 b = 2
4379
4380 «const c:ˇ» usize = 3;
4381 "},
4382 cx,
4383 );
4384 editor.backtab(&Backtab, window, cx);
4385 assert_text_with_selections(
4386 &mut editor,
4387 indoc! {"
4388 «aˇ» = 1
4389 b = 2
4390
4391 «const c:ˇ» usize = 3;
4392 "},
4393 cx,
4394 );
4395
4396 editor
4397 });
4398}
4399
4400#[gpui::test]
4401async fn test_backspace(cx: &mut TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let mut cx = EditorTestContext::new(cx).await;
4405
4406 // Basic backspace
4407 cx.set_state(indoc! {"
4408 onˇe two three
4409 fou«rˇ» five six
4410 seven «ˇeight nine
4411 »ten
4412 "});
4413 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4414 cx.assert_editor_state(indoc! {"
4415 oˇe two three
4416 fouˇ five six
4417 seven ˇten
4418 "});
4419
4420 // Test backspace inside and around indents
4421 cx.set_state(indoc! {"
4422 zero
4423 ˇone
4424 ˇtwo
4425 ˇ ˇ ˇ three
4426 ˇ ˇ four
4427 "});
4428 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 zero
4431 ˇone
4432 ˇtwo
4433 ˇ threeˇ four
4434 "});
4435}
4436
4437#[gpui::test]
4438async fn test_delete(cx: &mut TestAppContext) {
4439 init_test(cx, |_| {});
4440
4441 let mut cx = EditorTestContext::new(cx).await;
4442 cx.set_state(indoc! {"
4443 onˇe two three
4444 fou«rˇ» five six
4445 seven «ˇeight nine
4446 »ten
4447 "});
4448 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4449 cx.assert_editor_state(indoc! {"
4450 onˇ two three
4451 fouˇ five six
4452 seven ˇten
4453 "});
4454}
4455
4456#[gpui::test]
4457fn test_delete_line(cx: &mut TestAppContext) {
4458 init_test(cx, |_| {});
4459
4460 let editor = cx.add_window(|window, cx| {
4461 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4462 build_editor(buffer, window, cx)
4463 });
4464 _ = editor.update(cx, |editor, window, cx| {
4465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4466 s.select_display_ranges([
4467 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4468 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4469 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4470 ])
4471 });
4472 editor.delete_line(&DeleteLine, window, cx);
4473 assert_eq!(editor.display_text(cx), "ghi");
4474 assert_eq!(
4475 display_ranges(editor, cx),
4476 vec![
4477 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4478 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4479 ]
4480 );
4481 });
4482
4483 let editor = cx.add_window(|window, cx| {
4484 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4485 build_editor(buffer, window, cx)
4486 });
4487 _ = editor.update(cx, |editor, window, cx| {
4488 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4489 s.select_display_ranges([
4490 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4491 ])
4492 });
4493 editor.delete_line(&DeleteLine, window, cx);
4494 assert_eq!(editor.display_text(cx), "ghi\n");
4495 assert_eq!(
4496 display_ranges(editor, cx),
4497 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4498 );
4499 });
4500
4501 let editor = cx.add_window(|window, cx| {
4502 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4503 build_editor(buffer, window, cx)
4504 });
4505 _ = editor.update(cx, |editor, window, cx| {
4506 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4507 s.select_display_ranges([
4508 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4509 ])
4510 });
4511 editor.delete_line(&DeleteLine, window, cx);
4512 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4513 assert_eq!(
4514 display_ranges(editor, cx),
4515 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4516 );
4517 });
4518}
4519
4520#[gpui::test]
4521fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4522 init_test(cx, |_| {});
4523
4524 cx.add_window(|window, cx| {
4525 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4526 let mut editor = build_editor(buffer.clone(), window, cx);
4527 let buffer = buffer.read(cx).as_singleton().unwrap();
4528
4529 assert_eq!(
4530 editor
4531 .selections
4532 .ranges::<Point>(&editor.display_snapshot(cx)),
4533 &[Point::new(0, 0)..Point::new(0, 0)]
4534 );
4535
4536 // When on single line, replace newline at end by space
4537 editor.join_lines(&JoinLines, window, cx);
4538 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4539 assert_eq!(
4540 editor
4541 .selections
4542 .ranges::<Point>(&editor.display_snapshot(cx)),
4543 &[Point::new(0, 3)..Point::new(0, 3)]
4544 );
4545
4546 // When multiple lines are selected, remove newlines that are spanned by the selection
4547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4548 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4549 });
4550 editor.join_lines(&JoinLines, window, cx);
4551 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4552 assert_eq!(
4553 editor
4554 .selections
4555 .ranges::<Point>(&editor.display_snapshot(cx)),
4556 &[Point::new(0, 11)..Point::new(0, 11)]
4557 );
4558
4559 // Undo should be transactional
4560 editor.undo(&Undo, window, cx);
4561 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4562 assert_eq!(
4563 editor
4564 .selections
4565 .ranges::<Point>(&editor.display_snapshot(cx)),
4566 &[Point::new(0, 5)..Point::new(2, 2)]
4567 );
4568
4569 // When joining an empty line don't insert a space
4570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4571 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4572 });
4573 editor.join_lines(&JoinLines, window, cx);
4574 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4575 assert_eq!(
4576 editor
4577 .selections
4578 .ranges::<Point>(&editor.display_snapshot(cx)),
4579 [Point::new(2, 3)..Point::new(2, 3)]
4580 );
4581
4582 // We can remove trailing newlines
4583 editor.join_lines(&JoinLines, window, cx);
4584 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4585 assert_eq!(
4586 editor
4587 .selections
4588 .ranges::<Point>(&editor.display_snapshot(cx)),
4589 [Point::new(2, 3)..Point::new(2, 3)]
4590 );
4591
4592 // We don't blow up on the last line
4593 editor.join_lines(&JoinLines, window, cx);
4594 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4595 assert_eq!(
4596 editor
4597 .selections
4598 .ranges::<Point>(&editor.display_snapshot(cx)),
4599 [Point::new(2, 3)..Point::new(2, 3)]
4600 );
4601
4602 // reset to test indentation
4603 editor.buffer.update(cx, |buffer, cx| {
4604 buffer.edit(
4605 [
4606 (Point::new(1, 0)..Point::new(1, 2), " "),
4607 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4608 ],
4609 None,
4610 cx,
4611 )
4612 });
4613
4614 // We remove any leading spaces
4615 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4617 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4618 });
4619 editor.join_lines(&JoinLines, window, cx);
4620 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4621
4622 // We don't insert a space for a line containing only spaces
4623 editor.join_lines(&JoinLines, window, cx);
4624 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4625
4626 // We ignore any leading tabs
4627 editor.join_lines(&JoinLines, window, cx);
4628 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4629
4630 editor
4631 });
4632}
4633
4634#[gpui::test]
4635fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4636 init_test(cx, |_| {});
4637
4638 cx.add_window(|window, cx| {
4639 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4640 let mut editor = build_editor(buffer.clone(), window, cx);
4641 let buffer = buffer.read(cx).as_singleton().unwrap();
4642
4643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4644 s.select_ranges([
4645 Point::new(0, 2)..Point::new(1, 1),
4646 Point::new(1, 2)..Point::new(1, 2),
4647 Point::new(3, 1)..Point::new(3, 2),
4648 ])
4649 });
4650
4651 editor.join_lines(&JoinLines, window, cx);
4652 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4653
4654 assert_eq!(
4655 editor
4656 .selections
4657 .ranges::<Point>(&editor.display_snapshot(cx)),
4658 [
4659 Point::new(0, 7)..Point::new(0, 7),
4660 Point::new(1, 3)..Point::new(1, 3)
4661 ]
4662 );
4663 editor
4664 });
4665}
4666
4667#[gpui::test]
4668async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4669 init_test(cx, |_| {});
4670
4671 let mut cx = EditorTestContext::new(cx).await;
4672
4673 let diff_base = r#"
4674 Line 0
4675 Line 1
4676 Line 2
4677 Line 3
4678 "#
4679 .unindent();
4680
4681 cx.set_state(
4682 &r#"
4683 ˇLine 0
4684 Line 1
4685 Line 2
4686 Line 3
4687 "#
4688 .unindent(),
4689 );
4690
4691 cx.set_head_text(&diff_base);
4692 executor.run_until_parked();
4693
4694 // Join lines
4695 cx.update_editor(|editor, window, cx| {
4696 editor.join_lines(&JoinLines, window, cx);
4697 });
4698 executor.run_until_parked();
4699
4700 cx.assert_editor_state(
4701 &r#"
4702 Line 0ˇ Line 1
4703 Line 2
4704 Line 3
4705 "#
4706 .unindent(),
4707 );
4708 // Join again
4709 cx.update_editor(|editor, window, cx| {
4710 editor.join_lines(&JoinLines, window, cx);
4711 });
4712 executor.run_until_parked();
4713
4714 cx.assert_editor_state(
4715 &r#"
4716 Line 0 Line 1ˇ Line 2
4717 Line 3
4718 "#
4719 .unindent(),
4720 );
4721}
4722
4723#[gpui::test]
4724async fn test_custom_newlines_cause_no_false_positive_diffs(
4725 executor: BackgroundExecutor,
4726 cx: &mut TestAppContext,
4727) {
4728 init_test(cx, |_| {});
4729 let mut cx = EditorTestContext::new(cx).await;
4730 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4731 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4732 executor.run_until_parked();
4733
4734 cx.update_editor(|editor, window, cx| {
4735 let snapshot = editor.snapshot(window, cx);
4736 assert_eq!(
4737 snapshot
4738 .buffer_snapshot()
4739 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4740 .collect::<Vec<_>>(),
4741 Vec::new(),
4742 "Should not have any diffs for files with custom newlines"
4743 );
4744 });
4745}
4746
4747#[gpui::test]
4748async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4749 init_test(cx, |_| {});
4750
4751 let mut cx = EditorTestContext::new(cx).await;
4752
4753 // Test sort_lines_case_insensitive()
4754 cx.set_state(indoc! {"
4755 «z
4756 y
4757 x
4758 Z
4759 Y
4760 Xˇ»
4761 "});
4762 cx.update_editor(|e, window, cx| {
4763 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4764 });
4765 cx.assert_editor_state(indoc! {"
4766 «x
4767 X
4768 y
4769 Y
4770 z
4771 Zˇ»
4772 "});
4773
4774 // Test sort_lines_by_length()
4775 //
4776 // Demonstrates:
4777 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4778 // - sort is stable
4779 cx.set_state(indoc! {"
4780 «123
4781 æ
4782 12
4783 ∞
4784 1
4785 æˇ»
4786 "});
4787 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4788 cx.assert_editor_state(indoc! {"
4789 «æ
4790 ∞
4791 1
4792 æ
4793 12
4794 123ˇ»
4795 "});
4796
4797 // Test reverse_lines()
4798 cx.set_state(indoc! {"
4799 «5
4800 4
4801 3
4802 2
4803 1ˇ»
4804 "});
4805 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4806 cx.assert_editor_state(indoc! {"
4807 «1
4808 2
4809 3
4810 4
4811 5ˇ»
4812 "});
4813
4814 // Skip testing shuffle_line()
4815
4816 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4817 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4818
4819 // Don't manipulate when cursor is on single line, but expand the selection
4820 cx.set_state(indoc! {"
4821 ddˇdd
4822 ccc
4823 bb
4824 a
4825 "});
4826 cx.update_editor(|e, window, cx| {
4827 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4828 });
4829 cx.assert_editor_state(indoc! {"
4830 «ddddˇ»
4831 ccc
4832 bb
4833 a
4834 "});
4835
4836 // Basic manipulate case
4837 // Start selection moves to column 0
4838 // End of selection shrinks to fit shorter line
4839 cx.set_state(indoc! {"
4840 dd«d
4841 ccc
4842 bb
4843 aaaaaˇ»
4844 "});
4845 cx.update_editor(|e, window, cx| {
4846 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4847 });
4848 cx.assert_editor_state(indoc! {"
4849 «aaaaa
4850 bb
4851 ccc
4852 dddˇ»
4853 "});
4854
4855 // Manipulate case with newlines
4856 cx.set_state(indoc! {"
4857 dd«d
4858 ccc
4859
4860 bb
4861 aaaaa
4862
4863 ˇ»
4864 "});
4865 cx.update_editor(|e, window, cx| {
4866 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4867 });
4868 cx.assert_editor_state(indoc! {"
4869 «
4870
4871 aaaaa
4872 bb
4873 ccc
4874 dddˇ»
4875
4876 "});
4877
4878 // Adding new line
4879 cx.set_state(indoc! {"
4880 aa«a
4881 bbˇ»b
4882 "});
4883 cx.update_editor(|e, window, cx| {
4884 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4885 });
4886 cx.assert_editor_state(indoc! {"
4887 «aaa
4888 bbb
4889 added_lineˇ»
4890 "});
4891
4892 // Removing line
4893 cx.set_state(indoc! {"
4894 aa«a
4895 bbbˇ»
4896 "});
4897 cx.update_editor(|e, window, cx| {
4898 e.manipulate_immutable_lines(window, cx, |lines| {
4899 lines.pop();
4900 })
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «aaaˇ»
4904 "});
4905
4906 // Removing all lines
4907 cx.set_state(indoc! {"
4908 aa«a
4909 bbbˇ»
4910 "});
4911 cx.update_editor(|e, window, cx| {
4912 e.manipulate_immutable_lines(window, cx, |lines| {
4913 lines.drain(..);
4914 })
4915 });
4916 cx.assert_editor_state(indoc! {"
4917 ˇ
4918 "});
4919}
4920
4921#[gpui::test]
4922async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4923 init_test(cx, |_| {});
4924
4925 let mut cx = EditorTestContext::new(cx).await;
4926
4927 // Consider continuous selection as single selection
4928 cx.set_state(indoc! {"
4929 Aaa«aa
4930 cˇ»c«c
4931 bb
4932 aaaˇ»aa
4933 "});
4934 cx.update_editor(|e, window, cx| {
4935 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4936 });
4937 cx.assert_editor_state(indoc! {"
4938 «Aaaaa
4939 ccc
4940 bb
4941 aaaaaˇ»
4942 "});
4943
4944 cx.set_state(indoc! {"
4945 Aaa«aa
4946 cˇ»c«c
4947 bb
4948 aaaˇ»aa
4949 "});
4950 cx.update_editor(|e, window, cx| {
4951 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4952 });
4953 cx.assert_editor_state(indoc! {"
4954 «Aaaaa
4955 ccc
4956 bbˇ»
4957 "});
4958
4959 // Consider non continuous selection as distinct dedup operations
4960 cx.set_state(indoc! {"
4961 «aaaaa
4962 bb
4963 aaaaa
4964 aaaaaˇ»
4965
4966 aaa«aaˇ»
4967 "});
4968 cx.update_editor(|e, window, cx| {
4969 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4970 });
4971 cx.assert_editor_state(indoc! {"
4972 «aaaaa
4973 bbˇ»
4974
4975 «aaaaaˇ»
4976 "});
4977}
4978
4979#[gpui::test]
4980async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4981 init_test(cx, |_| {});
4982
4983 let mut cx = EditorTestContext::new(cx).await;
4984
4985 cx.set_state(indoc! {"
4986 «Aaa
4987 aAa
4988 Aaaˇ»
4989 "});
4990 cx.update_editor(|e, window, cx| {
4991 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4992 });
4993 cx.assert_editor_state(indoc! {"
4994 «Aaa
4995 aAaˇ»
4996 "});
4997
4998 cx.set_state(indoc! {"
4999 «Aaa
5000 aAa
5001 aaAˇ»
5002 "});
5003 cx.update_editor(|e, window, cx| {
5004 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5005 });
5006 cx.assert_editor_state(indoc! {"
5007 «Aaaˇ»
5008 "});
5009}
5010
5011#[gpui::test]
5012async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5013 init_test(cx, |_| {});
5014
5015 let mut cx = EditorTestContext::new(cx).await;
5016
5017 let js_language = Arc::new(Language::new(
5018 LanguageConfig {
5019 name: "JavaScript".into(),
5020 wrap_characters: Some(language::WrapCharactersConfig {
5021 start_prefix: "<".into(),
5022 start_suffix: ">".into(),
5023 end_prefix: "</".into(),
5024 end_suffix: ">".into(),
5025 }),
5026 ..LanguageConfig::default()
5027 },
5028 None,
5029 ));
5030
5031 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5032
5033 cx.set_state(indoc! {"
5034 «testˇ»
5035 "});
5036 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5037 cx.assert_editor_state(indoc! {"
5038 <«ˇ»>test</«ˇ»>
5039 "});
5040
5041 cx.set_state(indoc! {"
5042 «test
5043 testˇ»
5044 "});
5045 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5046 cx.assert_editor_state(indoc! {"
5047 <«ˇ»>test
5048 test</«ˇ»>
5049 "});
5050
5051 cx.set_state(indoc! {"
5052 teˇst
5053 "});
5054 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5055 cx.assert_editor_state(indoc! {"
5056 te<«ˇ»></«ˇ»>st
5057 "});
5058}
5059
5060#[gpui::test]
5061async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5062 init_test(cx, |_| {});
5063
5064 let mut cx = EditorTestContext::new(cx).await;
5065
5066 let js_language = Arc::new(Language::new(
5067 LanguageConfig {
5068 name: "JavaScript".into(),
5069 wrap_characters: Some(language::WrapCharactersConfig {
5070 start_prefix: "<".into(),
5071 start_suffix: ">".into(),
5072 end_prefix: "</".into(),
5073 end_suffix: ">".into(),
5074 }),
5075 ..LanguageConfig::default()
5076 },
5077 None,
5078 ));
5079
5080 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5081
5082 cx.set_state(indoc! {"
5083 «testˇ»
5084 «testˇ» «testˇ»
5085 «testˇ»
5086 "});
5087 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5088 cx.assert_editor_state(indoc! {"
5089 <«ˇ»>test</«ˇ»>
5090 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5091 <«ˇ»>test</«ˇ»>
5092 "});
5093
5094 cx.set_state(indoc! {"
5095 «test
5096 testˇ»
5097 «test
5098 testˇ»
5099 "});
5100 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5101 cx.assert_editor_state(indoc! {"
5102 <«ˇ»>test
5103 test</«ˇ»>
5104 <«ˇ»>test
5105 test</«ˇ»>
5106 "});
5107}
5108
5109#[gpui::test]
5110async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5111 init_test(cx, |_| {});
5112
5113 let mut cx = EditorTestContext::new(cx).await;
5114
5115 let plaintext_language = Arc::new(Language::new(
5116 LanguageConfig {
5117 name: "Plain Text".into(),
5118 ..LanguageConfig::default()
5119 },
5120 None,
5121 ));
5122
5123 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5124
5125 cx.set_state(indoc! {"
5126 «testˇ»
5127 "});
5128 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5129 cx.assert_editor_state(indoc! {"
5130 «testˇ»
5131 "});
5132}
5133
5134#[gpui::test]
5135async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5136 init_test(cx, |_| {});
5137
5138 let mut cx = EditorTestContext::new(cx).await;
5139
5140 // Manipulate with multiple selections on a single line
5141 cx.set_state(indoc! {"
5142 dd«dd
5143 cˇ»c«c
5144 bb
5145 aaaˇ»aa
5146 "});
5147 cx.update_editor(|e, window, cx| {
5148 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5149 });
5150 cx.assert_editor_state(indoc! {"
5151 «aaaaa
5152 bb
5153 ccc
5154 ddddˇ»
5155 "});
5156
5157 // Manipulate with multiple disjoin selections
5158 cx.set_state(indoc! {"
5159 5«
5160 4
5161 3
5162 2
5163 1ˇ»
5164
5165 dd«dd
5166 ccc
5167 bb
5168 aaaˇ»aa
5169 "});
5170 cx.update_editor(|e, window, cx| {
5171 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5172 });
5173 cx.assert_editor_state(indoc! {"
5174 «1
5175 2
5176 3
5177 4
5178 5ˇ»
5179
5180 «aaaaa
5181 bb
5182 ccc
5183 ddddˇ»
5184 "});
5185
5186 // Adding lines on each selection
5187 cx.set_state(indoc! {"
5188 2«
5189 1ˇ»
5190
5191 bb«bb
5192 aaaˇ»aa
5193 "});
5194 cx.update_editor(|e, window, cx| {
5195 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5196 });
5197 cx.assert_editor_state(indoc! {"
5198 «2
5199 1
5200 added lineˇ»
5201
5202 «bbbb
5203 aaaaa
5204 added lineˇ»
5205 "});
5206
5207 // Removing lines on each selection
5208 cx.set_state(indoc! {"
5209 2«
5210 1ˇ»
5211
5212 bb«bb
5213 aaaˇ»aa
5214 "});
5215 cx.update_editor(|e, window, cx| {
5216 e.manipulate_immutable_lines(window, cx, |lines| {
5217 lines.pop();
5218 })
5219 });
5220 cx.assert_editor_state(indoc! {"
5221 «2ˇ»
5222
5223 «bbbbˇ»
5224 "});
5225}
5226
5227#[gpui::test]
5228async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5229 init_test(cx, |settings| {
5230 settings.defaults.tab_size = NonZeroU32::new(3)
5231 });
5232
5233 let mut cx = EditorTestContext::new(cx).await;
5234
5235 // MULTI SELECTION
5236 // Ln.1 "«" tests empty lines
5237 // Ln.9 tests just leading whitespace
5238 cx.set_state(indoc! {"
5239 «
5240 abc // No indentationˇ»
5241 «\tabc // 1 tabˇ»
5242 \t\tabc « ˇ» // 2 tabs
5243 \t ab«c // Tab followed by space
5244 \tabc // Space followed by tab (3 spaces should be the result)
5245 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5246 abˇ»ˇc ˇ ˇ // Already space indented«
5247 \t
5248 \tabc\tdef // Only the leading tab is manipulatedˇ»
5249 "});
5250 cx.update_editor(|e, window, cx| {
5251 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5252 });
5253 cx.assert_editor_state(
5254 indoc! {"
5255 «
5256 abc // No indentation
5257 abc // 1 tab
5258 abc // 2 tabs
5259 abc // Tab followed by space
5260 abc // Space followed by tab (3 spaces should be the result)
5261 abc // Mixed indentation (tab conversion depends on the column)
5262 abc // Already space indented
5263 ·
5264 abc\tdef // Only the leading tab is manipulatedˇ»
5265 "}
5266 .replace("·", "")
5267 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5268 );
5269
5270 // Test on just a few lines, the others should remain unchanged
5271 // Only lines (3, 5, 10, 11) should change
5272 cx.set_state(
5273 indoc! {"
5274 ·
5275 abc // No indentation
5276 \tabcˇ // 1 tab
5277 \t\tabc // 2 tabs
5278 \t abcˇ // Tab followed by space
5279 \tabc // Space followed by tab (3 spaces should be the result)
5280 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5281 abc // Already space indented
5282 «\t
5283 \tabc\tdef // Only the leading tab is manipulatedˇ»
5284 "}
5285 .replace("·", "")
5286 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5287 );
5288 cx.update_editor(|e, window, cx| {
5289 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5290 });
5291 cx.assert_editor_state(
5292 indoc! {"
5293 ·
5294 abc // No indentation
5295 « abc // 1 tabˇ»
5296 \t\tabc // 2 tabs
5297 « abc // Tab followed by spaceˇ»
5298 \tabc // Space followed by tab (3 spaces should be the result)
5299 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5300 abc // Already space indented
5301 « ·
5302 abc\tdef // Only the leading tab is manipulatedˇ»
5303 "}
5304 .replace("·", "")
5305 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5306 );
5307
5308 // SINGLE SELECTION
5309 // Ln.1 "«" tests empty lines
5310 // Ln.9 tests just leading whitespace
5311 cx.set_state(indoc! {"
5312 «
5313 abc // No indentation
5314 \tabc // 1 tab
5315 \t\tabc // 2 tabs
5316 \t abc // Tab followed by space
5317 \tabc // Space followed by tab (3 spaces should be the result)
5318 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5319 abc // Already space indented
5320 \t
5321 \tabc\tdef // Only the leading tab is manipulatedˇ»
5322 "});
5323 cx.update_editor(|e, window, cx| {
5324 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5325 });
5326 cx.assert_editor_state(
5327 indoc! {"
5328 «
5329 abc // No indentation
5330 abc // 1 tab
5331 abc // 2 tabs
5332 abc // Tab followed by space
5333 abc // Space followed by tab (3 spaces should be the result)
5334 abc // Mixed indentation (tab conversion depends on the column)
5335 abc // Already space indented
5336 ·
5337 abc\tdef // Only the leading tab is manipulatedˇ»
5338 "}
5339 .replace("·", "")
5340 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5341 );
5342}
5343
5344#[gpui::test]
5345async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5346 init_test(cx, |settings| {
5347 settings.defaults.tab_size = NonZeroU32::new(3)
5348 });
5349
5350 let mut cx = EditorTestContext::new(cx).await;
5351
5352 // MULTI SELECTION
5353 // Ln.1 "«" tests empty lines
5354 // Ln.11 tests just leading whitespace
5355 cx.set_state(indoc! {"
5356 «
5357 abˇ»ˇc // No indentation
5358 abc ˇ ˇ // 1 space (< 3 so dont convert)
5359 abc « // 2 spaces (< 3 so dont convert)
5360 abc // 3 spaces (convert)
5361 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5362 «\tˇ»\t«\tˇ»abc // Already tab indented
5363 «\t abc // Tab followed by space
5364 \tabc // Space followed by tab (should be consumed due to tab)
5365 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5366 \tˇ» «\t
5367 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5368 "});
5369 cx.update_editor(|e, window, cx| {
5370 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5371 });
5372 cx.assert_editor_state(indoc! {"
5373 «
5374 abc // No indentation
5375 abc // 1 space (< 3 so dont convert)
5376 abc // 2 spaces (< 3 so dont convert)
5377 \tabc // 3 spaces (convert)
5378 \t abc // 5 spaces (1 tab + 2 spaces)
5379 \t\t\tabc // Already tab indented
5380 \t abc // Tab followed by space
5381 \tabc // Space followed by tab (should be consumed due to tab)
5382 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5383 \t\t\t
5384 \tabc \t // Only the leading spaces should be convertedˇ»
5385 "});
5386
5387 // Test on just a few lines, the other should remain unchanged
5388 // Only lines (4, 8, 11, 12) should change
5389 cx.set_state(
5390 indoc! {"
5391 ·
5392 abc // No indentation
5393 abc // 1 space (< 3 so dont convert)
5394 abc // 2 spaces (< 3 so dont convert)
5395 « abc // 3 spaces (convert)ˇ»
5396 abc // 5 spaces (1 tab + 2 spaces)
5397 \t\t\tabc // Already tab indented
5398 \t abc // Tab followed by space
5399 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5400 \t\t \tabc // Mixed indentation
5401 \t \t \t \tabc // Mixed indentation
5402 \t \tˇ
5403 « abc \t // Only the leading spaces should be convertedˇ»
5404 "}
5405 .replace("·", "")
5406 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5407 );
5408 cx.update_editor(|e, window, cx| {
5409 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5410 });
5411 cx.assert_editor_state(
5412 indoc! {"
5413 ·
5414 abc // No indentation
5415 abc // 1 space (< 3 so dont convert)
5416 abc // 2 spaces (< 3 so dont convert)
5417 «\tabc // 3 spaces (convert)ˇ»
5418 abc // 5 spaces (1 tab + 2 spaces)
5419 \t\t\tabc // Already tab indented
5420 \t abc // Tab followed by space
5421 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5422 \t\t \tabc // Mixed indentation
5423 \t \t \t \tabc // Mixed indentation
5424 «\t\t\t
5425 \tabc \t // Only the leading spaces should be convertedˇ»
5426 "}
5427 .replace("·", "")
5428 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5429 );
5430
5431 // SINGLE SELECTION
5432 // Ln.1 "«" tests empty lines
5433 // Ln.11 tests just leading whitespace
5434 cx.set_state(indoc! {"
5435 «
5436 abc // No indentation
5437 abc // 1 space (< 3 so dont convert)
5438 abc // 2 spaces (< 3 so dont convert)
5439 abc // 3 spaces (convert)
5440 abc // 5 spaces (1 tab + 2 spaces)
5441 \t\t\tabc // Already tab indented
5442 \t abc // Tab followed by space
5443 \tabc // Space followed by tab (should be consumed due to tab)
5444 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5445 \t \t
5446 abc \t // Only the leading spaces should be convertedˇ»
5447 "});
5448 cx.update_editor(|e, window, cx| {
5449 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5450 });
5451 cx.assert_editor_state(indoc! {"
5452 «
5453 abc // No indentation
5454 abc // 1 space (< 3 so dont convert)
5455 abc // 2 spaces (< 3 so dont convert)
5456 \tabc // 3 spaces (convert)
5457 \t abc // 5 spaces (1 tab + 2 spaces)
5458 \t\t\tabc // Already tab indented
5459 \t abc // Tab followed by space
5460 \tabc // Space followed by tab (should be consumed due to tab)
5461 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5462 \t\t\t
5463 \tabc \t // Only the leading spaces should be convertedˇ»
5464 "});
5465}
5466
5467#[gpui::test]
5468async fn test_toggle_case(cx: &mut TestAppContext) {
5469 init_test(cx, |_| {});
5470
5471 let mut cx = EditorTestContext::new(cx).await;
5472
5473 // If all lower case -> upper case
5474 cx.set_state(indoc! {"
5475 «hello worldˇ»
5476 "});
5477 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5478 cx.assert_editor_state(indoc! {"
5479 «HELLO WORLDˇ»
5480 "});
5481
5482 // If all upper case -> lower case
5483 cx.set_state(indoc! {"
5484 «HELLO WORLDˇ»
5485 "});
5486 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5487 cx.assert_editor_state(indoc! {"
5488 «hello worldˇ»
5489 "});
5490
5491 // If any upper case characters are identified -> lower case
5492 // This matches JetBrains IDEs
5493 cx.set_state(indoc! {"
5494 «hEllo worldˇ»
5495 "});
5496 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5497 cx.assert_editor_state(indoc! {"
5498 «hello worldˇ»
5499 "});
5500}
5501
5502#[gpui::test]
5503async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5504 init_test(cx, |_| {});
5505
5506 let mut cx = EditorTestContext::new(cx).await;
5507
5508 cx.set_state(indoc! {"
5509 «implement-windows-supportˇ»
5510 "});
5511 cx.update_editor(|e, window, cx| {
5512 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5513 });
5514 cx.assert_editor_state(indoc! {"
5515 «Implement windows supportˇ»
5516 "});
5517}
5518
5519#[gpui::test]
5520async fn test_manipulate_text(cx: &mut TestAppContext) {
5521 init_test(cx, |_| {});
5522
5523 let mut cx = EditorTestContext::new(cx).await;
5524
5525 // Test convert_to_upper_case()
5526 cx.set_state(indoc! {"
5527 «hello worldˇ»
5528 "});
5529 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5530 cx.assert_editor_state(indoc! {"
5531 «HELLO WORLDˇ»
5532 "});
5533
5534 // Test convert_to_lower_case()
5535 cx.set_state(indoc! {"
5536 «HELLO WORLDˇ»
5537 "});
5538 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5539 cx.assert_editor_state(indoc! {"
5540 «hello worldˇ»
5541 "});
5542
5543 // Test multiple line, single selection case
5544 cx.set_state(indoc! {"
5545 «The quick brown
5546 fox jumps over
5547 the lazy dogˇ»
5548 "});
5549 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5550 cx.assert_editor_state(indoc! {"
5551 «The Quick Brown
5552 Fox Jumps Over
5553 The Lazy Dogˇ»
5554 "});
5555
5556 // Test multiple line, single selection case
5557 cx.set_state(indoc! {"
5558 «The quick brown
5559 fox jumps over
5560 the lazy dogˇ»
5561 "});
5562 cx.update_editor(|e, window, cx| {
5563 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5564 });
5565 cx.assert_editor_state(indoc! {"
5566 «TheQuickBrown
5567 FoxJumpsOver
5568 TheLazyDogˇ»
5569 "});
5570
5571 // From here on out, test more complex cases of manipulate_text()
5572
5573 // Test no selection case - should affect words cursors are in
5574 // Cursor at beginning, middle, and end of word
5575 cx.set_state(indoc! {"
5576 ˇhello big beauˇtiful worldˇ
5577 "});
5578 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5579 cx.assert_editor_state(indoc! {"
5580 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5581 "});
5582
5583 // Test multiple selections on a single line and across multiple lines
5584 cx.set_state(indoc! {"
5585 «Theˇ» quick «brown
5586 foxˇ» jumps «overˇ»
5587 the «lazyˇ» dog
5588 "});
5589 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5590 cx.assert_editor_state(indoc! {"
5591 «THEˇ» quick «BROWN
5592 FOXˇ» jumps «OVERˇ»
5593 the «LAZYˇ» dog
5594 "});
5595
5596 // Test case where text length grows
5597 cx.set_state(indoc! {"
5598 «tschüߡ»
5599 "});
5600 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5601 cx.assert_editor_state(indoc! {"
5602 «TSCHÜSSˇ»
5603 "});
5604
5605 // Test to make sure we don't crash when text shrinks
5606 cx.set_state(indoc! {"
5607 aaa_bbbˇ
5608 "});
5609 cx.update_editor(|e, window, cx| {
5610 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5611 });
5612 cx.assert_editor_state(indoc! {"
5613 «aaaBbbˇ»
5614 "});
5615
5616 // Test to make sure we all aware of the fact that each word can grow and shrink
5617 // Final selections should be aware of this fact
5618 cx.set_state(indoc! {"
5619 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5620 "});
5621 cx.update_editor(|e, window, cx| {
5622 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5623 });
5624 cx.assert_editor_state(indoc! {"
5625 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5626 "});
5627
5628 cx.set_state(indoc! {"
5629 «hElLo, WoRld!ˇ»
5630 "});
5631 cx.update_editor(|e, window, cx| {
5632 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5633 });
5634 cx.assert_editor_state(indoc! {"
5635 «HeLlO, wOrLD!ˇ»
5636 "});
5637
5638 // Test selections with `line_mode() = true`.
5639 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5640 cx.set_state(indoc! {"
5641 «The quick brown
5642 fox jumps over
5643 tˇ»he lazy dog
5644 "});
5645 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5646 cx.assert_editor_state(indoc! {"
5647 «THE QUICK BROWN
5648 FOX JUMPS OVER
5649 THE LAZY DOGˇ»
5650 "});
5651}
5652
5653#[gpui::test]
5654fn test_duplicate_line(cx: &mut TestAppContext) {
5655 init_test(cx, |_| {});
5656
5657 let editor = cx.add_window(|window, cx| {
5658 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5659 build_editor(buffer, window, cx)
5660 });
5661 _ = editor.update(cx, |editor, window, cx| {
5662 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5663 s.select_display_ranges([
5664 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5665 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5666 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5667 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5668 ])
5669 });
5670 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5671 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5672 assert_eq!(
5673 display_ranges(editor, cx),
5674 vec![
5675 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5676 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5677 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5678 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5679 ]
5680 );
5681 });
5682
5683 let editor = cx.add_window(|window, cx| {
5684 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5685 build_editor(buffer, window, cx)
5686 });
5687 _ = editor.update(cx, |editor, window, cx| {
5688 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5689 s.select_display_ranges([
5690 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5691 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5692 ])
5693 });
5694 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5695 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5696 assert_eq!(
5697 display_ranges(editor, cx),
5698 vec![
5699 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5700 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5701 ]
5702 );
5703 });
5704
5705 // With `duplicate_line_up` the selections move to the duplicated lines,
5706 // which are inserted above the original lines
5707 let editor = cx.add_window(|window, cx| {
5708 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5709 build_editor(buffer, window, cx)
5710 });
5711 _ = editor.update(cx, |editor, window, cx| {
5712 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5713 s.select_display_ranges([
5714 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5715 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5716 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5717 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5718 ])
5719 });
5720 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5721 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5722 assert_eq!(
5723 display_ranges(editor, cx),
5724 vec![
5725 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5726 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5727 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5728 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5729 ]
5730 );
5731 });
5732
5733 let editor = cx.add_window(|window, cx| {
5734 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5735 build_editor(buffer, window, cx)
5736 });
5737 _ = editor.update(cx, |editor, window, cx| {
5738 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5739 s.select_display_ranges([
5740 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5741 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5742 ])
5743 });
5744 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5745 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5746 assert_eq!(
5747 display_ranges(editor, cx),
5748 vec![
5749 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5750 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5751 ]
5752 );
5753 });
5754
5755 let editor = cx.add_window(|window, cx| {
5756 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5757 build_editor(buffer, window, cx)
5758 });
5759 _ = editor.update(cx, |editor, window, cx| {
5760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5761 s.select_display_ranges([
5762 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5763 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5764 ])
5765 });
5766 editor.duplicate_selection(&DuplicateSelection, window, cx);
5767 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5768 assert_eq!(
5769 display_ranges(editor, cx),
5770 vec![
5771 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5772 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5773 ]
5774 );
5775 });
5776}
5777
5778#[gpui::test]
5779fn test_move_line_up_down(cx: &mut TestAppContext) {
5780 init_test(cx, |_| {});
5781
5782 let editor = cx.add_window(|window, cx| {
5783 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5784 build_editor(buffer, window, cx)
5785 });
5786 _ = editor.update(cx, |editor, window, cx| {
5787 editor.fold_creases(
5788 vec![
5789 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5790 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5791 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5792 ],
5793 true,
5794 window,
5795 cx,
5796 );
5797 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5798 s.select_display_ranges([
5799 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5800 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5801 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5802 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5803 ])
5804 });
5805 assert_eq!(
5806 editor.display_text(cx),
5807 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5808 );
5809
5810 editor.move_line_up(&MoveLineUp, window, cx);
5811 assert_eq!(
5812 editor.display_text(cx),
5813 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5814 );
5815 assert_eq!(
5816 display_ranges(editor, cx),
5817 vec![
5818 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5819 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5820 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5821 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5822 ]
5823 );
5824 });
5825
5826 _ = editor.update(cx, |editor, window, cx| {
5827 editor.move_line_down(&MoveLineDown, window, cx);
5828 assert_eq!(
5829 editor.display_text(cx),
5830 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5831 );
5832 assert_eq!(
5833 display_ranges(editor, cx),
5834 vec![
5835 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5836 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5837 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5838 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5839 ]
5840 );
5841 });
5842
5843 _ = editor.update(cx, |editor, window, cx| {
5844 editor.move_line_down(&MoveLineDown, window, cx);
5845 assert_eq!(
5846 editor.display_text(cx),
5847 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5848 );
5849 assert_eq!(
5850 display_ranges(editor, cx),
5851 vec![
5852 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5853 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5854 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5855 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5856 ]
5857 );
5858 });
5859
5860 _ = editor.update(cx, |editor, window, cx| {
5861 editor.move_line_up(&MoveLineUp, window, cx);
5862 assert_eq!(
5863 editor.display_text(cx),
5864 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5865 );
5866 assert_eq!(
5867 display_ranges(editor, cx),
5868 vec![
5869 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5870 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5871 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5872 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5873 ]
5874 );
5875 });
5876}
5877
5878#[gpui::test]
5879fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5880 init_test(cx, |_| {});
5881 let editor = cx.add_window(|window, cx| {
5882 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5883 build_editor(buffer, window, cx)
5884 });
5885 _ = editor.update(cx, |editor, window, cx| {
5886 editor.fold_creases(
5887 vec![Crease::simple(
5888 Point::new(6, 4)..Point::new(7, 4),
5889 FoldPlaceholder::test(),
5890 )],
5891 true,
5892 window,
5893 cx,
5894 );
5895 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5896 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5897 });
5898 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5899 editor.move_line_up(&MoveLineUp, window, cx);
5900 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5901 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5902 });
5903}
5904
5905#[gpui::test]
5906fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5907 init_test(cx, |_| {});
5908
5909 let editor = cx.add_window(|window, cx| {
5910 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5911 build_editor(buffer, window, cx)
5912 });
5913 _ = editor.update(cx, |editor, window, cx| {
5914 let snapshot = editor.buffer.read(cx).snapshot(cx);
5915 editor.insert_blocks(
5916 [BlockProperties {
5917 style: BlockStyle::Fixed,
5918 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5919 height: Some(1),
5920 render: Arc::new(|_| div().into_any()),
5921 priority: 0,
5922 }],
5923 Some(Autoscroll::fit()),
5924 cx,
5925 );
5926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5927 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5928 });
5929 editor.move_line_down(&MoveLineDown, window, cx);
5930 });
5931}
5932
5933#[gpui::test]
5934async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5935 init_test(cx, |_| {});
5936
5937 let mut cx = EditorTestContext::new(cx).await;
5938 cx.set_state(
5939 &"
5940 ˇzero
5941 one
5942 two
5943 three
5944 four
5945 five
5946 "
5947 .unindent(),
5948 );
5949
5950 // Create a four-line block that replaces three lines of text.
5951 cx.update_editor(|editor, window, cx| {
5952 let snapshot = editor.snapshot(window, cx);
5953 let snapshot = &snapshot.buffer_snapshot();
5954 let placement = BlockPlacement::Replace(
5955 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5956 );
5957 editor.insert_blocks(
5958 [BlockProperties {
5959 placement,
5960 height: Some(4),
5961 style: BlockStyle::Sticky,
5962 render: Arc::new(|_| gpui::div().into_any_element()),
5963 priority: 0,
5964 }],
5965 None,
5966 cx,
5967 );
5968 });
5969
5970 // Move down so that the cursor touches the block.
5971 cx.update_editor(|editor, window, cx| {
5972 editor.move_down(&Default::default(), window, cx);
5973 });
5974 cx.assert_editor_state(
5975 &"
5976 zero
5977 «one
5978 two
5979 threeˇ»
5980 four
5981 five
5982 "
5983 .unindent(),
5984 );
5985
5986 // Move down past the block.
5987 cx.update_editor(|editor, window, cx| {
5988 editor.move_down(&Default::default(), window, cx);
5989 });
5990 cx.assert_editor_state(
5991 &"
5992 zero
5993 one
5994 two
5995 three
5996 ˇfour
5997 five
5998 "
5999 .unindent(),
6000 );
6001}
6002
6003#[gpui::test]
6004fn test_transpose(cx: &mut TestAppContext) {
6005 init_test(cx, |_| {});
6006
6007 _ = cx.add_window(|window, cx| {
6008 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6009 editor.set_style(EditorStyle::default(), window, cx);
6010 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6011 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6012 });
6013 editor.transpose(&Default::default(), window, cx);
6014 assert_eq!(editor.text(cx), "bac");
6015 assert_eq!(
6016 editor.selections.ranges(&editor.display_snapshot(cx)),
6017 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6018 );
6019
6020 editor.transpose(&Default::default(), window, cx);
6021 assert_eq!(editor.text(cx), "bca");
6022 assert_eq!(
6023 editor.selections.ranges(&editor.display_snapshot(cx)),
6024 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6025 );
6026
6027 editor.transpose(&Default::default(), window, cx);
6028 assert_eq!(editor.text(cx), "bac");
6029 assert_eq!(
6030 editor.selections.ranges(&editor.display_snapshot(cx)),
6031 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6032 );
6033
6034 editor
6035 });
6036
6037 _ = cx.add_window(|window, cx| {
6038 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6039 editor.set_style(EditorStyle::default(), window, cx);
6040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6041 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6042 });
6043 editor.transpose(&Default::default(), window, cx);
6044 assert_eq!(editor.text(cx), "acb\nde");
6045 assert_eq!(
6046 editor.selections.ranges(&editor.display_snapshot(cx)),
6047 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6048 );
6049
6050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6051 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6052 });
6053 editor.transpose(&Default::default(), window, cx);
6054 assert_eq!(editor.text(cx), "acbd\ne");
6055 assert_eq!(
6056 editor.selections.ranges(&editor.display_snapshot(cx)),
6057 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6058 );
6059
6060 editor.transpose(&Default::default(), window, cx);
6061 assert_eq!(editor.text(cx), "acbde\n");
6062 assert_eq!(
6063 editor.selections.ranges(&editor.display_snapshot(cx)),
6064 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6065 );
6066
6067 editor.transpose(&Default::default(), window, cx);
6068 assert_eq!(editor.text(cx), "acbd\ne");
6069 assert_eq!(
6070 editor.selections.ranges(&editor.display_snapshot(cx)),
6071 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6072 );
6073
6074 editor
6075 });
6076
6077 _ = cx.add_window(|window, cx| {
6078 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6079 editor.set_style(EditorStyle::default(), window, cx);
6080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6081 s.select_ranges([
6082 MultiBufferOffset(1)..MultiBufferOffset(1),
6083 MultiBufferOffset(2)..MultiBufferOffset(2),
6084 MultiBufferOffset(4)..MultiBufferOffset(4),
6085 ])
6086 });
6087 editor.transpose(&Default::default(), window, cx);
6088 assert_eq!(editor.text(cx), "bacd\ne");
6089 assert_eq!(
6090 editor.selections.ranges(&editor.display_snapshot(cx)),
6091 [
6092 MultiBufferOffset(2)..MultiBufferOffset(2),
6093 MultiBufferOffset(3)..MultiBufferOffset(3),
6094 MultiBufferOffset(5)..MultiBufferOffset(5)
6095 ]
6096 );
6097
6098 editor.transpose(&Default::default(), window, cx);
6099 assert_eq!(editor.text(cx), "bcade\n");
6100 assert_eq!(
6101 editor.selections.ranges(&editor.display_snapshot(cx)),
6102 [
6103 MultiBufferOffset(3)..MultiBufferOffset(3),
6104 MultiBufferOffset(4)..MultiBufferOffset(4),
6105 MultiBufferOffset(6)..MultiBufferOffset(6)
6106 ]
6107 );
6108
6109 editor.transpose(&Default::default(), window, cx);
6110 assert_eq!(editor.text(cx), "bcda\ne");
6111 assert_eq!(
6112 editor.selections.ranges(&editor.display_snapshot(cx)),
6113 [
6114 MultiBufferOffset(4)..MultiBufferOffset(4),
6115 MultiBufferOffset(6)..MultiBufferOffset(6)
6116 ]
6117 );
6118
6119 editor.transpose(&Default::default(), window, cx);
6120 assert_eq!(editor.text(cx), "bcade\n");
6121 assert_eq!(
6122 editor.selections.ranges(&editor.display_snapshot(cx)),
6123 [
6124 MultiBufferOffset(4)..MultiBufferOffset(4),
6125 MultiBufferOffset(6)..MultiBufferOffset(6)
6126 ]
6127 );
6128
6129 editor.transpose(&Default::default(), window, cx);
6130 assert_eq!(editor.text(cx), "bcaed\n");
6131 assert_eq!(
6132 editor.selections.ranges(&editor.display_snapshot(cx)),
6133 [
6134 MultiBufferOffset(5)..MultiBufferOffset(5),
6135 MultiBufferOffset(6)..MultiBufferOffset(6)
6136 ]
6137 );
6138
6139 editor
6140 });
6141
6142 _ = cx.add_window(|window, cx| {
6143 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6144 editor.set_style(EditorStyle::default(), window, cx);
6145 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6146 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6147 });
6148 editor.transpose(&Default::default(), window, cx);
6149 assert_eq!(editor.text(cx), "🏀🍐✋");
6150 assert_eq!(
6151 editor.selections.ranges(&editor.display_snapshot(cx)),
6152 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6153 );
6154
6155 editor.transpose(&Default::default(), window, cx);
6156 assert_eq!(editor.text(cx), "🏀✋🍐");
6157 assert_eq!(
6158 editor.selections.ranges(&editor.display_snapshot(cx)),
6159 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6160 );
6161
6162 editor.transpose(&Default::default(), window, cx);
6163 assert_eq!(editor.text(cx), "🏀🍐✋");
6164 assert_eq!(
6165 editor.selections.ranges(&editor.display_snapshot(cx)),
6166 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6167 );
6168
6169 editor
6170 });
6171}
6172
6173#[gpui::test]
6174async fn test_rewrap(cx: &mut TestAppContext) {
6175 init_test(cx, |settings| {
6176 settings.languages.0.extend([
6177 (
6178 "Markdown".into(),
6179 LanguageSettingsContent {
6180 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6181 preferred_line_length: Some(40),
6182 ..Default::default()
6183 },
6184 ),
6185 (
6186 "Plain Text".into(),
6187 LanguageSettingsContent {
6188 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6189 preferred_line_length: Some(40),
6190 ..Default::default()
6191 },
6192 ),
6193 (
6194 "C++".into(),
6195 LanguageSettingsContent {
6196 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6197 preferred_line_length: Some(40),
6198 ..Default::default()
6199 },
6200 ),
6201 (
6202 "Python".into(),
6203 LanguageSettingsContent {
6204 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6205 preferred_line_length: Some(40),
6206 ..Default::default()
6207 },
6208 ),
6209 (
6210 "Rust".into(),
6211 LanguageSettingsContent {
6212 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6213 preferred_line_length: Some(40),
6214 ..Default::default()
6215 },
6216 ),
6217 ])
6218 });
6219
6220 let mut cx = EditorTestContext::new(cx).await;
6221
6222 let cpp_language = Arc::new(Language::new(
6223 LanguageConfig {
6224 name: "C++".into(),
6225 line_comments: vec!["// ".into()],
6226 ..LanguageConfig::default()
6227 },
6228 None,
6229 ));
6230 let python_language = Arc::new(Language::new(
6231 LanguageConfig {
6232 name: "Python".into(),
6233 line_comments: vec!["# ".into()],
6234 ..LanguageConfig::default()
6235 },
6236 None,
6237 ));
6238 let markdown_language = Arc::new(Language::new(
6239 LanguageConfig {
6240 name: "Markdown".into(),
6241 rewrap_prefixes: vec![
6242 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6243 regex::Regex::new("[-*+]\\s+").unwrap(),
6244 ],
6245 ..LanguageConfig::default()
6246 },
6247 None,
6248 ));
6249 let rust_language = Arc::new(
6250 Language::new(
6251 LanguageConfig {
6252 name: "Rust".into(),
6253 line_comments: vec!["// ".into(), "/// ".into()],
6254 ..LanguageConfig::default()
6255 },
6256 Some(tree_sitter_rust::LANGUAGE.into()),
6257 )
6258 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6259 .unwrap(),
6260 );
6261
6262 let plaintext_language = Arc::new(Language::new(
6263 LanguageConfig {
6264 name: "Plain Text".into(),
6265 ..LanguageConfig::default()
6266 },
6267 None,
6268 ));
6269
6270 // Test basic rewrapping of a long line with a cursor
6271 assert_rewrap(
6272 indoc! {"
6273 // ˇThis is a long comment that needs to be wrapped.
6274 "},
6275 indoc! {"
6276 // ˇThis is a long comment that needs to
6277 // be wrapped.
6278 "},
6279 cpp_language.clone(),
6280 &mut cx,
6281 );
6282
6283 // Test rewrapping a full selection
6284 assert_rewrap(
6285 indoc! {"
6286 «// This selected long comment needs to be wrapped.ˇ»"
6287 },
6288 indoc! {"
6289 «// This selected long comment needs to
6290 // be wrapped.ˇ»"
6291 },
6292 cpp_language.clone(),
6293 &mut cx,
6294 );
6295
6296 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6297 assert_rewrap(
6298 indoc! {"
6299 // ˇThis is the first line.
6300 // Thisˇ is the second line.
6301 // This is the thirdˇ line, all part of one paragraph.
6302 "},
6303 indoc! {"
6304 // ˇThis is the first line. Thisˇ is the
6305 // second line. This is the thirdˇ line,
6306 // all part of one paragraph.
6307 "},
6308 cpp_language.clone(),
6309 &mut cx,
6310 );
6311
6312 // Test multiple cursors in different paragraphs trigger separate rewraps
6313 assert_rewrap(
6314 indoc! {"
6315 // ˇThis is the first paragraph, first line.
6316 // ˇThis is the first paragraph, second line.
6317
6318 // ˇThis is the second paragraph, first line.
6319 // ˇThis is the second paragraph, second line.
6320 "},
6321 indoc! {"
6322 // ˇThis is the first paragraph, first
6323 // line. ˇThis is the first paragraph,
6324 // second line.
6325
6326 // ˇThis is the second paragraph, first
6327 // line. ˇThis is the second paragraph,
6328 // second line.
6329 "},
6330 cpp_language.clone(),
6331 &mut cx,
6332 );
6333
6334 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6335 assert_rewrap(
6336 indoc! {"
6337 «// A regular long long comment to be wrapped.
6338 /// A documentation long comment to be wrapped.ˇ»
6339 "},
6340 indoc! {"
6341 «// A regular long long comment to be
6342 // wrapped.
6343 /// A documentation long comment to be
6344 /// wrapped.ˇ»
6345 "},
6346 rust_language.clone(),
6347 &mut cx,
6348 );
6349
6350 // Test that change in indentation level trigger seperate rewraps
6351 assert_rewrap(
6352 indoc! {"
6353 fn foo() {
6354 «// This is a long comment at the base indent.
6355 // This is a long comment at the next indent.ˇ»
6356 }
6357 "},
6358 indoc! {"
6359 fn foo() {
6360 «// This is a long comment at the
6361 // base indent.
6362 // This is a long comment at the
6363 // next indent.ˇ»
6364 }
6365 "},
6366 rust_language.clone(),
6367 &mut cx,
6368 );
6369
6370 // Test that different comment prefix characters (e.g., '#') are handled correctly
6371 assert_rewrap(
6372 indoc! {"
6373 # ˇThis is a long comment using a pound sign.
6374 "},
6375 indoc! {"
6376 # ˇThis is a long comment using a pound
6377 # sign.
6378 "},
6379 python_language,
6380 &mut cx,
6381 );
6382
6383 // Test rewrapping only affects comments, not code even when selected
6384 assert_rewrap(
6385 indoc! {"
6386 «/// This doc comment is long and should be wrapped.
6387 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6388 "},
6389 indoc! {"
6390 «/// This doc comment is long and should
6391 /// be wrapped.
6392 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6393 "},
6394 rust_language.clone(),
6395 &mut cx,
6396 );
6397
6398 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6399 assert_rewrap(
6400 indoc! {"
6401 # Header
6402
6403 A long long long line of markdown text to wrap.ˇ
6404 "},
6405 indoc! {"
6406 # Header
6407
6408 A long long long line of markdown text
6409 to wrap.ˇ
6410 "},
6411 markdown_language.clone(),
6412 &mut cx,
6413 );
6414
6415 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6416 assert_rewrap(
6417 indoc! {"
6418 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6419 2. This is a numbered list item that is very long and needs to be wrapped properly.
6420 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6421 "},
6422 indoc! {"
6423 «1. This is a numbered list item that is
6424 very long and needs to be wrapped
6425 properly.
6426 2. This is a numbered list item that is
6427 very long and needs to be wrapped
6428 properly.
6429 - This is an unordered list item that is
6430 also very long and should not merge
6431 with the numbered item.ˇ»
6432 "},
6433 markdown_language.clone(),
6434 &mut cx,
6435 );
6436
6437 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6438 assert_rewrap(
6439 indoc! {"
6440 «1. This is a numbered list item that is
6441 very long and needs to be wrapped
6442 properly.
6443 2. This is a numbered list item that is
6444 very long and needs to be wrapped
6445 properly.
6446 - This is an unordered list item that is
6447 also very long and should not merge with
6448 the numbered item.ˇ»
6449 "},
6450 indoc! {"
6451 «1. This is a numbered list item that is
6452 very long and needs to be wrapped
6453 properly.
6454 2. This is a numbered list item that is
6455 very long and needs to be wrapped
6456 properly.
6457 - This is an unordered list item that is
6458 also very long and should not merge
6459 with the numbered item.ˇ»
6460 "},
6461 markdown_language.clone(),
6462 &mut cx,
6463 );
6464
6465 // Test that rewrapping maintain indents even when they already exists.
6466 assert_rewrap(
6467 indoc! {"
6468 «1. This is a numbered list
6469 item that is very long and needs to be wrapped properly.
6470 2. This is a numbered list
6471 item that is very long and needs to be wrapped properly.
6472 - This is an unordered list item that is also very long and
6473 should not merge with the numbered item.ˇ»
6474 "},
6475 indoc! {"
6476 «1. This is a numbered list item that is
6477 very long and needs to be wrapped
6478 properly.
6479 2. This is a numbered list item that is
6480 very long and needs to be wrapped
6481 properly.
6482 - This is an unordered list item that is
6483 also very long and should not merge
6484 with the numbered item.ˇ»
6485 "},
6486 markdown_language,
6487 &mut cx,
6488 );
6489
6490 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6491 assert_rewrap(
6492 indoc! {"
6493 ˇThis is a very long line of plain text that will be wrapped.
6494 "},
6495 indoc! {"
6496 ˇThis is a very long line of plain text
6497 that will be wrapped.
6498 "},
6499 plaintext_language.clone(),
6500 &mut cx,
6501 );
6502
6503 // Test that non-commented code acts as a paragraph boundary within a selection
6504 assert_rewrap(
6505 indoc! {"
6506 «// This is the first long comment block to be wrapped.
6507 fn my_func(a: u32);
6508 // This is the second long comment block to be wrapped.ˇ»
6509 "},
6510 indoc! {"
6511 «// This is the first long comment block
6512 // to be wrapped.
6513 fn my_func(a: u32);
6514 // This is the second long comment block
6515 // to be wrapped.ˇ»
6516 "},
6517 rust_language,
6518 &mut cx,
6519 );
6520
6521 // Test rewrapping multiple selections, including ones with blank lines or tabs
6522 assert_rewrap(
6523 indoc! {"
6524 «ˇThis is a very long line that will be wrapped.
6525
6526 This is another paragraph in the same selection.»
6527
6528 «\tThis is a very long indented line that will be wrapped.ˇ»
6529 "},
6530 indoc! {"
6531 «ˇThis is a very long line that will be
6532 wrapped.
6533
6534 This is another paragraph in the same
6535 selection.»
6536
6537 «\tThis is a very long indented line
6538 \tthat will be wrapped.ˇ»
6539 "},
6540 plaintext_language,
6541 &mut cx,
6542 );
6543
6544 // Test that an empty comment line acts as a paragraph boundary
6545 assert_rewrap(
6546 indoc! {"
6547 // ˇThis is a long comment that will be wrapped.
6548 //
6549 // And this is another long comment that will also be wrapped.ˇ
6550 "},
6551 indoc! {"
6552 // ˇThis is a long comment that will be
6553 // wrapped.
6554 //
6555 // And this is another long comment that
6556 // will also be wrapped.ˇ
6557 "},
6558 cpp_language,
6559 &mut cx,
6560 );
6561
6562 #[track_caller]
6563 fn assert_rewrap(
6564 unwrapped_text: &str,
6565 wrapped_text: &str,
6566 language: Arc<Language>,
6567 cx: &mut EditorTestContext,
6568 ) {
6569 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6570 cx.set_state(unwrapped_text);
6571 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6572 cx.assert_editor_state(wrapped_text);
6573 }
6574}
6575
6576#[gpui::test]
6577async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6578 init_test(cx, |settings| {
6579 settings.languages.0.extend([(
6580 "Rust".into(),
6581 LanguageSettingsContent {
6582 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6583 preferred_line_length: Some(40),
6584 ..Default::default()
6585 },
6586 )])
6587 });
6588
6589 let mut cx = EditorTestContext::new(cx).await;
6590
6591 let rust_lang = Arc::new(
6592 Language::new(
6593 LanguageConfig {
6594 name: "Rust".into(),
6595 line_comments: vec!["// ".into()],
6596 block_comment: Some(BlockCommentConfig {
6597 start: "/*".into(),
6598 end: "*/".into(),
6599 prefix: "* ".into(),
6600 tab_size: 1,
6601 }),
6602 documentation_comment: Some(BlockCommentConfig {
6603 start: "/**".into(),
6604 end: "*/".into(),
6605 prefix: "* ".into(),
6606 tab_size: 1,
6607 }),
6608
6609 ..LanguageConfig::default()
6610 },
6611 Some(tree_sitter_rust::LANGUAGE.into()),
6612 )
6613 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6614 .unwrap(),
6615 );
6616
6617 // regular block comment
6618 assert_rewrap(
6619 indoc! {"
6620 /*
6621 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6622 */
6623 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6624 "},
6625 indoc! {"
6626 /*
6627 *ˇ Lorem ipsum dolor sit amet,
6628 * consectetur adipiscing elit.
6629 */
6630 /*
6631 *ˇ Lorem ipsum dolor sit amet,
6632 * consectetur adipiscing elit.
6633 */
6634 "},
6635 rust_lang.clone(),
6636 &mut cx,
6637 );
6638
6639 // indent is respected
6640 assert_rewrap(
6641 indoc! {"
6642 {}
6643 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6644 "},
6645 indoc! {"
6646 {}
6647 /*
6648 *ˇ Lorem ipsum dolor sit amet,
6649 * consectetur adipiscing elit.
6650 */
6651 "},
6652 rust_lang.clone(),
6653 &mut cx,
6654 );
6655
6656 // short block comments with inline delimiters
6657 assert_rewrap(
6658 indoc! {"
6659 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6660 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6661 */
6662 /*
6663 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6664 "},
6665 indoc! {"
6666 /*
6667 *ˇ Lorem ipsum dolor sit amet,
6668 * consectetur adipiscing elit.
6669 */
6670 /*
6671 *ˇ Lorem ipsum dolor sit amet,
6672 * consectetur adipiscing elit.
6673 */
6674 /*
6675 *ˇ Lorem ipsum dolor sit amet,
6676 * consectetur adipiscing elit.
6677 */
6678 "},
6679 rust_lang.clone(),
6680 &mut cx,
6681 );
6682
6683 // multiline block comment with inline start/end delimiters
6684 assert_rewrap(
6685 indoc! {"
6686 /*ˇ Lorem ipsum dolor sit amet,
6687 * consectetur adipiscing elit. */
6688 "},
6689 indoc! {"
6690 /*
6691 *ˇ Lorem ipsum dolor sit amet,
6692 * consectetur adipiscing elit.
6693 */
6694 "},
6695 rust_lang.clone(),
6696 &mut cx,
6697 );
6698
6699 // block comment rewrap still respects paragraph bounds
6700 assert_rewrap(
6701 indoc! {"
6702 /*
6703 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6704 *
6705 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6706 */
6707 "},
6708 indoc! {"
6709 /*
6710 *ˇ Lorem ipsum dolor sit amet,
6711 * consectetur adipiscing elit.
6712 *
6713 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6714 */
6715 "},
6716 rust_lang.clone(),
6717 &mut cx,
6718 );
6719
6720 // documentation comments
6721 assert_rewrap(
6722 indoc! {"
6723 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6724 /**
6725 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6726 */
6727 "},
6728 indoc! {"
6729 /**
6730 *ˇ Lorem ipsum dolor sit amet,
6731 * consectetur adipiscing elit.
6732 */
6733 /**
6734 *ˇ Lorem ipsum dolor sit amet,
6735 * consectetur adipiscing elit.
6736 */
6737 "},
6738 rust_lang.clone(),
6739 &mut cx,
6740 );
6741
6742 // different, adjacent comments
6743 assert_rewrap(
6744 indoc! {"
6745 /**
6746 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6747 */
6748 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6749 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6750 "},
6751 indoc! {"
6752 /**
6753 *ˇ Lorem ipsum dolor sit amet,
6754 * consectetur adipiscing elit.
6755 */
6756 /*
6757 *ˇ Lorem ipsum dolor sit amet,
6758 * consectetur adipiscing elit.
6759 */
6760 //ˇ Lorem ipsum dolor sit amet,
6761 // consectetur adipiscing elit.
6762 "},
6763 rust_lang.clone(),
6764 &mut cx,
6765 );
6766
6767 // selection w/ single short block comment
6768 assert_rewrap(
6769 indoc! {"
6770 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6771 "},
6772 indoc! {"
6773 «/*
6774 * Lorem ipsum dolor sit amet,
6775 * consectetur adipiscing elit.
6776 */ˇ»
6777 "},
6778 rust_lang.clone(),
6779 &mut cx,
6780 );
6781
6782 // rewrapping a single comment w/ abutting comments
6783 assert_rewrap(
6784 indoc! {"
6785 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6786 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6787 "},
6788 indoc! {"
6789 /*
6790 * ˇLorem ipsum dolor sit amet,
6791 * consectetur adipiscing elit.
6792 */
6793 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6794 "},
6795 rust_lang.clone(),
6796 &mut cx,
6797 );
6798
6799 // selection w/ non-abutting short block comments
6800 assert_rewrap(
6801 indoc! {"
6802 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6803
6804 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6805 "},
6806 indoc! {"
6807 «/*
6808 * Lorem ipsum dolor sit amet,
6809 * consectetur adipiscing elit.
6810 */
6811
6812 /*
6813 * Lorem ipsum dolor sit amet,
6814 * consectetur adipiscing elit.
6815 */ˇ»
6816 "},
6817 rust_lang.clone(),
6818 &mut cx,
6819 );
6820
6821 // selection of multiline block comments
6822 assert_rewrap(
6823 indoc! {"
6824 «/* Lorem ipsum dolor sit amet,
6825 * consectetur adipiscing elit. */ˇ»
6826 "},
6827 indoc! {"
6828 «/*
6829 * Lorem ipsum dolor sit amet,
6830 * consectetur adipiscing elit.
6831 */ˇ»
6832 "},
6833 rust_lang.clone(),
6834 &mut cx,
6835 );
6836
6837 // partial selection of multiline block comments
6838 assert_rewrap(
6839 indoc! {"
6840 «/* Lorem ipsum dolor sit amet,ˇ»
6841 * consectetur adipiscing elit. */
6842 /* Lorem ipsum dolor sit amet,
6843 «* consectetur adipiscing elit. */ˇ»
6844 "},
6845 indoc! {"
6846 «/*
6847 * Lorem ipsum dolor sit amet,ˇ»
6848 * consectetur adipiscing elit. */
6849 /* Lorem ipsum dolor sit amet,
6850 «* consectetur adipiscing elit.
6851 */ˇ»
6852 "},
6853 rust_lang.clone(),
6854 &mut cx,
6855 );
6856
6857 // selection w/ abutting short block comments
6858 // TODO: should not be combined; should rewrap as 2 comments
6859 assert_rewrap(
6860 indoc! {"
6861 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6862 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6863 "},
6864 // desired behavior:
6865 // indoc! {"
6866 // «/*
6867 // * Lorem ipsum dolor sit amet,
6868 // * consectetur adipiscing elit.
6869 // */
6870 // /*
6871 // * Lorem ipsum dolor sit amet,
6872 // * consectetur adipiscing elit.
6873 // */ˇ»
6874 // "},
6875 // actual behaviour:
6876 indoc! {"
6877 «/*
6878 * Lorem ipsum dolor sit amet,
6879 * consectetur adipiscing elit. Lorem
6880 * ipsum dolor sit amet, consectetur
6881 * adipiscing elit.
6882 */ˇ»
6883 "},
6884 rust_lang.clone(),
6885 &mut cx,
6886 );
6887
6888 // TODO: same as above, but with delimiters on separate line
6889 // assert_rewrap(
6890 // indoc! {"
6891 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6892 // */
6893 // /*
6894 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6895 // "},
6896 // // desired:
6897 // // indoc! {"
6898 // // «/*
6899 // // * Lorem ipsum dolor sit amet,
6900 // // * consectetur adipiscing elit.
6901 // // */
6902 // // /*
6903 // // * Lorem ipsum dolor sit amet,
6904 // // * consectetur adipiscing elit.
6905 // // */ˇ»
6906 // // "},
6907 // // actual: (but with trailing w/s on the empty lines)
6908 // indoc! {"
6909 // «/*
6910 // * Lorem ipsum dolor sit amet,
6911 // * consectetur adipiscing elit.
6912 // *
6913 // */
6914 // /*
6915 // *
6916 // * Lorem ipsum dolor sit amet,
6917 // * consectetur adipiscing elit.
6918 // */ˇ»
6919 // "},
6920 // rust_lang.clone(),
6921 // &mut cx,
6922 // );
6923
6924 // TODO these are unhandled edge cases; not correct, just documenting known issues
6925 assert_rewrap(
6926 indoc! {"
6927 /*
6928 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6929 */
6930 /*
6931 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6932 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6933 "},
6934 // desired:
6935 // indoc! {"
6936 // /*
6937 // *ˇ Lorem ipsum dolor sit amet,
6938 // * consectetur adipiscing elit.
6939 // */
6940 // /*
6941 // *ˇ Lorem ipsum dolor sit amet,
6942 // * consectetur adipiscing elit.
6943 // */
6944 // /*
6945 // *ˇ Lorem ipsum dolor sit amet
6946 // */ /* consectetur adipiscing elit. */
6947 // "},
6948 // actual:
6949 indoc! {"
6950 /*
6951 //ˇ Lorem ipsum dolor sit amet,
6952 // consectetur adipiscing elit.
6953 */
6954 /*
6955 * //ˇ Lorem ipsum dolor sit amet,
6956 * consectetur adipiscing elit.
6957 */
6958 /*
6959 *ˇ Lorem ipsum dolor sit amet */ /*
6960 * consectetur adipiscing elit.
6961 */
6962 "},
6963 rust_lang,
6964 &mut cx,
6965 );
6966
6967 #[track_caller]
6968 fn assert_rewrap(
6969 unwrapped_text: &str,
6970 wrapped_text: &str,
6971 language: Arc<Language>,
6972 cx: &mut EditorTestContext,
6973 ) {
6974 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6975 cx.set_state(unwrapped_text);
6976 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6977 cx.assert_editor_state(wrapped_text);
6978 }
6979}
6980
6981#[gpui::test]
6982async fn test_hard_wrap(cx: &mut TestAppContext) {
6983 init_test(cx, |_| {});
6984 let mut cx = EditorTestContext::new(cx).await;
6985
6986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6987 cx.update_editor(|editor, _, cx| {
6988 editor.set_hard_wrap(Some(14), cx);
6989 });
6990
6991 cx.set_state(indoc!(
6992 "
6993 one two three ˇ
6994 "
6995 ));
6996 cx.simulate_input("four");
6997 cx.run_until_parked();
6998
6999 cx.assert_editor_state(indoc!(
7000 "
7001 one two three
7002 fourˇ
7003 "
7004 ));
7005
7006 cx.update_editor(|editor, window, cx| {
7007 editor.newline(&Default::default(), window, cx);
7008 });
7009 cx.run_until_parked();
7010 cx.assert_editor_state(indoc!(
7011 "
7012 one two three
7013 four
7014 ˇ
7015 "
7016 ));
7017
7018 cx.simulate_input("five");
7019 cx.run_until_parked();
7020 cx.assert_editor_state(indoc!(
7021 "
7022 one two three
7023 four
7024 fiveˇ
7025 "
7026 ));
7027
7028 cx.update_editor(|editor, window, cx| {
7029 editor.newline(&Default::default(), window, cx);
7030 });
7031 cx.run_until_parked();
7032 cx.simulate_input("# ");
7033 cx.run_until_parked();
7034 cx.assert_editor_state(indoc!(
7035 "
7036 one two three
7037 four
7038 five
7039 # ˇ
7040 "
7041 ));
7042
7043 cx.update_editor(|editor, window, cx| {
7044 editor.newline(&Default::default(), window, cx);
7045 });
7046 cx.run_until_parked();
7047 cx.assert_editor_state(indoc!(
7048 "
7049 one two three
7050 four
7051 five
7052 #\x20
7053 #ˇ
7054 "
7055 ));
7056
7057 cx.simulate_input(" 6");
7058 cx.run_until_parked();
7059 cx.assert_editor_state(indoc!(
7060 "
7061 one two three
7062 four
7063 five
7064 #
7065 # 6ˇ
7066 "
7067 ));
7068}
7069
7070#[gpui::test]
7071async fn test_cut_line_ends(cx: &mut TestAppContext) {
7072 init_test(cx, |_| {});
7073
7074 let mut cx = EditorTestContext::new(cx).await;
7075
7076 cx.set_state(indoc! {"The quick brownˇ"});
7077 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7078 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7079
7080 cx.set_state(indoc! {"The emacs foxˇ"});
7081 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7082 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7083
7084 cx.set_state(indoc! {"
7085 The quick« brownˇ»
7086 fox jumps overˇ
7087 the lazy dog"});
7088 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7089 cx.assert_editor_state(indoc! {"
7090 The quickˇ
7091 ˇthe lazy dog"});
7092
7093 cx.set_state(indoc! {"
7094 The quick« brownˇ»
7095 fox jumps overˇ
7096 the lazy dog"});
7097 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7098 cx.assert_editor_state(indoc! {"
7099 The quickˇ
7100 fox jumps overˇthe lazy dog"});
7101
7102 cx.set_state(indoc! {"
7103 The quick« brownˇ»
7104 fox jumps overˇ
7105 the lazy dog"});
7106 cx.update_editor(|e, window, cx| {
7107 e.cut_to_end_of_line(
7108 &CutToEndOfLine {
7109 stop_at_newlines: true,
7110 },
7111 window,
7112 cx,
7113 )
7114 });
7115 cx.assert_editor_state(indoc! {"
7116 The quickˇ
7117 fox jumps overˇ
7118 the lazy dog"});
7119
7120 cx.set_state(indoc! {"
7121 The quick« brownˇ»
7122 fox jumps overˇ
7123 the lazy dog"});
7124 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7125 cx.assert_editor_state(indoc! {"
7126 The quickˇ
7127 fox jumps overˇthe lazy dog"});
7128}
7129
7130#[gpui::test]
7131async fn test_clipboard(cx: &mut TestAppContext) {
7132 init_test(cx, |_| {});
7133
7134 let mut cx = EditorTestContext::new(cx).await;
7135
7136 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7137 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7138 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7139
7140 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7141 cx.set_state("two ˇfour ˇsix ˇ");
7142 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7143 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7144
7145 // Paste again but with only two cursors. Since the number of cursors doesn't
7146 // match the number of slices in the clipboard, the entire clipboard text
7147 // is pasted at each cursor.
7148 cx.set_state("ˇtwo one✅ four three six five ˇ");
7149 cx.update_editor(|e, window, cx| {
7150 e.handle_input("( ", window, cx);
7151 e.paste(&Paste, window, cx);
7152 e.handle_input(") ", window, cx);
7153 });
7154 cx.assert_editor_state(
7155 &([
7156 "( one✅ ",
7157 "three ",
7158 "five ) ˇtwo one✅ four three six five ( one✅ ",
7159 "three ",
7160 "five ) ˇ",
7161 ]
7162 .join("\n")),
7163 );
7164
7165 // Cut with three selections, one of which is full-line.
7166 cx.set_state(indoc! {"
7167 1«2ˇ»3
7168 4ˇ567
7169 «8ˇ»9"});
7170 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7171 cx.assert_editor_state(indoc! {"
7172 1ˇ3
7173 ˇ9"});
7174
7175 // Paste with three selections, noticing how the copied selection that was full-line
7176 // gets inserted before the second cursor.
7177 cx.set_state(indoc! {"
7178 1ˇ3
7179 9ˇ
7180 «oˇ»ne"});
7181 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7182 cx.assert_editor_state(indoc! {"
7183 12ˇ3
7184 4567
7185 9ˇ
7186 8ˇne"});
7187
7188 // Copy with a single cursor only, which writes the whole line into the clipboard.
7189 cx.set_state(indoc! {"
7190 The quick brown
7191 fox juˇmps over
7192 the lazy dog"});
7193 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7194 assert_eq!(
7195 cx.read_from_clipboard()
7196 .and_then(|item| item.text().as_deref().map(str::to_string)),
7197 Some("fox jumps over\n".to_string())
7198 );
7199
7200 // Paste with three selections, noticing how the copied full-line selection is inserted
7201 // before the empty selections but replaces the selection that is non-empty.
7202 cx.set_state(indoc! {"
7203 Tˇhe quick brown
7204 «foˇ»x jumps over
7205 tˇhe lazy dog"});
7206 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7207 cx.assert_editor_state(indoc! {"
7208 fox jumps over
7209 Tˇhe quick brown
7210 fox jumps over
7211 ˇx jumps over
7212 fox jumps over
7213 tˇhe lazy dog"});
7214}
7215
7216#[gpui::test]
7217async fn test_copy_trim(cx: &mut TestAppContext) {
7218 init_test(cx, |_| {});
7219
7220 let mut cx = EditorTestContext::new(cx).await;
7221 cx.set_state(
7222 r#" «for selection in selections.iter() {
7223 let mut start = selection.start;
7224 let mut end = selection.end;
7225 let is_entire_line = selection.is_empty();
7226 if is_entire_line {
7227 start = Point::new(start.row, 0);ˇ»
7228 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7229 }
7230 "#,
7231 );
7232 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7233 assert_eq!(
7234 cx.read_from_clipboard()
7235 .and_then(|item| item.text().as_deref().map(str::to_string)),
7236 Some(
7237 "for selection in selections.iter() {
7238 let mut start = selection.start;
7239 let mut end = selection.end;
7240 let is_entire_line = selection.is_empty();
7241 if is_entire_line {
7242 start = Point::new(start.row, 0);"
7243 .to_string()
7244 ),
7245 "Regular copying preserves all indentation selected",
7246 );
7247 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7248 assert_eq!(
7249 cx.read_from_clipboard()
7250 .and_then(|item| item.text().as_deref().map(str::to_string)),
7251 Some(
7252 "for selection in selections.iter() {
7253let mut start = selection.start;
7254let mut end = selection.end;
7255let is_entire_line = selection.is_empty();
7256if is_entire_line {
7257 start = Point::new(start.row, 0);"
7258 .to_string()
7259 ),
7260 "Copying with stripping should strip all leading whitespaces"
7261 );
7262
7263 cx.set_state(
7264 r#" « for selection in selections.iter() {
7265 let mut start = selection.start;
7266 let mut end = selection.end;
7267 let is_entire_line = selection.is_empty();
7268 if is_entire_line {
7269 start = Point::new(start.row, 0);ˇ»
7270 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7271 }
7272 "#,
7273 );
7274 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7275 assert_eq!(
7276 cx.read_from_clipboard()
7277 .and_then(|item| item.text().as_deref().map(str::to_string)),
7278 Some(
7279 " for selection in selections.iter() {
7280 let mut start = selection.start;
7281 let mut end = selection.end;
7282 let is_entire_line = selection.is_empty();
7283 if is_entire_line {
7284 start = Point::new(start.row, 0);"
7285 .to_string()
7286 ),
7287 "Regular copying preserves all indentation selected",
7288 );
7289 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7290 assert_eq!(
7291 cx.read_from_clipboard()
7292 .and_then(|item| item.text().as_deref().map(str::to_string)),
7293 Some(
7294 "for selection in selections.iter() {
7295let mut start = selection.start;
7296let mut end = selection.end;
7297let is_entire_line = selection.is_empty();
7298if is_entire_line {
7299 start = Point::new(start.row, 0);"
7300 .to_string()
7301 ),
7302 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7303 );
7304
7305 cx.set_state(
7306 r#" «ˇ for selection in selections.iter() {
7307 let mut start = selection.start;
7308 let mut end = selection.end;
7309 let is_entire_line = selection.is_empty();
7310 if is_entire_line {
7311 start = Point::new(start.row, 0);»
7312 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7313 }
7314 "#,
7315 );
7316 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7317 assert_eq!(
7318 cx.read_from_clipboard()
7319 .and_then(|item| item.text().as_deref().map(str::to_string)),
7320 Some(
7321 " for selection in selections.iter() {
7322 let mut start = selection.start;
7323 let mut end = selection.end;
7324 let is_entire_line = selection.is_empty();
7325 if is_entire_line {
7326 start = Point::new(start.row, 0);"
7327 .to_string()
7328 ),
7329 "Regular copying for reverse selection works the same",
7330 );
7331 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7332 assert_eq!(
7333 cx.read_from_clipboard()
7334 .and_then(|item| item.text().as_deref().map(str::to_string)),
7335 Some(
7336 "for selection in selections.iter() {
7337let mut start = selection.start;
7338let mut end = selection.end;
7339let is_entire_line = selection.is_empty();
7340if is_entire_line {
7341 start = Point::new(start.row, 0);"
7342 .to_string()
7343 ),
7344 "Copying with stripping for reverse selection works the same"
7345 );
7346
7347 cx.set_state(
7348 r#" for selection «in selections.iter() {
7349 let mut start = selection.start;
7350 let mut end = selection.end;
7351 let is_entire_line = selection.is_empty();
7352 if is_entire_line {
7353 start = Point::new(start.row, 0);ˇ»
7354 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7355 }
7356 "#,
7357 );
7358 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7359 assert_eq!(
7360 cx.read_from_clipboard()
7361 .and_then(|item| item.text().as_deref().map(str::to_string)),
7362 Some(
7363 "in selections.iter() {
7364 let mut start = selection.start;
7365 let mut end = selection.end;
7366 let is_entire_line = selection.is_empty();
7367 if is_entire_line {
7368 start = Point::new(start.row, 0);"
7369 .to_string()
7370 ),
7371 "When selecting past the indent, the copying works as usual",
7372 );
7373 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7374 assert_eq!(
7375 cx.read_from_clipboard()
7376 .and_then(|item| item.text().as_deref().map(str::to_string)),
7377 Some(
7378 "in selections.iter() {
7379 let mut start = selection.start;
7380 let mut end = selection.end;
7381 let is_entire_line = selection.is_empty();
7382 if is_entire_line {
7383 start = Point::new(start.row, 0);"
7384 .to_string()
7385 ),
7386 "When selecting past the indent, nothing is trimmed"
7387 );
7388
7389 cx.set_state(
7390 r#" «for selection in selections.iter() {
7391 let mut start = selection.start;
7392
7393 let mut end = selection.end;
7394 let is_entire_line = selection.is_empty();
7395 if is_entire_line {
7396 start = Point::new(start.row, 0);
7397ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7398 }
7399 "#,
7400 );
7401 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7402 assert_eq!(
7403 cx.read_from_clipboard()
7404 .and_then(|item| item.text().as_deref().map(str::to_string)),
7405 Some(
7406 "for selection in selections.iter() {
7407let mut start = selection.start;
7408
7409let mut end = selection.end;
7410let is_entire_line = selection.is_empty();
7411if is_entire_line {
7412 start = Point::new(start.row, 0);
7413"
7414 .to_string()
7415 ),
7416 "Copying with stripping should ignore empty lines"
7417 );
7418}
7419
7420#[gpui::test]
7421async fn test_paste_multiline(cx: &mut TestAppContext) {
7422 init_test(cx, |_| {});
7423
7424 let mut cx = EditorTestContext::new(cx).await;
7425 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7426
7427 // Cut an indented block, without the leading whitespace.
7428 cx.set_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 «d(
7432 e,
7433 f
7434 )ˇ»
7435 );
7436 "});
7437 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7438 cx.assert_editor_state(indoc! {"
7439 const a: B = (
7440 c(),
7441 ˇ
7442 );
7443 "});
7444
7445 // Paste it at the same position.
7446 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7447 cx.assert_editor_state(indoc! {"
7448 const a: B = (
7449 c(),
7450 d(
7451 e,
7452 f
7453 )ˇ
7454 );
7455 "});
7456
7457 // Paste it at a line with a lower indent level.
7458 cx.set_state(indoc! {"
7459 ˇ
7460 const a: B = (
7461 c(),
7462 );
7463 "});
7464 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7465 cx.assert_editor_state(indoc! {"
7466 d(
7467 e,
7468 f
7469 )ˇ
7470 const a: B = (
7471 c(),
7472 );
7473 "});
7474
7475 // Cut an indented block, with the leading whitespace.
7476 cx.set_state(indoc! {"
7477 const a: B = (
7478 c(),
7479 « d(
7480 e,
7481 f
7482 )
7483 ˇ»);
7484 "});
7485 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7486 cx.assert_editor_state(indoc! {"
7487 const a: B = (
7488 c(),
7489 ˇ);
7490 "});
7491
7492 // Paste it at the same position.
7493 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7494 cx.assert_editor_state(indoc! {"
7495 const a: B = (
7496 c(),
7497 d(
7498 e,
7499 f
7500 )
7501 ˇ);
7502 "});
7503
7504 // Paste it at a line with a higher indent level.
7505 cx.set_state(indoc! {"
7506 const a: B = (
7507 c(),
7508 d(
7509 e,
7510 fˇ
7511 )
7512 );
7513 "});
7514 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7515 cx.assert_editor_state(indoc! {"
7516 const a: B = (
7517 c(),
7518 d(
7519 e,
7520 f d(
7521 e,
7522 f
7523 )
7524 ˇ
7525 )
7526 );
7527 "});
7528
7529 // Copy an indented block, starting mid-line
7530 cx.set_state(indoc! {"
7531 const a: B = (
7532 c(),
7533 somethin«g(
7534 e,
7535 f
7536 )ˇ»
7537 );
7538 "});
7539 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7540
7541 // Paste it on a line with a lower indent level
7542 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7543 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7544 cx.assert_editor_state(indoc! {"
7545 const a: B = (
7546 c(),
7547 something(
7548 e,
7549 f
7550 )
7551 );
7552 g(
7553 e,
7554 f
7555 )ˇ"});
7556}
7557
7558#[gpui::test]
7559async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7560 init_test(cx, |_| {});
7561
7562 cx.write_to_clipboard(ClipboardItem::new_string(
7563 " d(\n e\n );\n".into(),
7564 ));
7565
7566 let mut cx = EditorTestContext::new(cx).await;
7567 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7568
7569 cx.set_state(indoc! {"
7570 fn a() {
7571 b();
7572 if c() {
7573 ˇ
7574 }
7575 }
7576 "});
7577
7578 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7579 cx.assert_editor_state(indoc! {"
7580 fn a() {
7581 b();
7582 if c() {
7583 d(
7584 e
7585 );
7586 ˇ
7587 }
7588 }
7589 "});
7590
7591 cx.set_state(indoc! {"
7592 fn a() {
7593 b();
7594 ˇ
7595 }
7596 "});
7597
7598 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7599 cx.assert_editor_state(indoc! {"
7600 fn a() {
7601 b();
7602 d(
7603 e
7604 );
7605 ˇ
7606 }
7607 "});
7608}
7609
7610#[gpui::test]
7611fn test_select_all(cx: &mut TestAppContext) {
7612 init_test(cx, |_| {});
7613
7614 let editor = cx.add_window(|window, cx| {
7615 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7616 build_editor(buffer, window, cx)
7617 });
7618 _ = editor.update(cx, |editor, window, cx| {
7619 editor.select_all(&SelectAll, window, cx);
7620 assert_eq!(
7621 display_ranges(editor, cx),
7622 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7623 );
7624 });
7625}
7626
7627#[gpui::test]
7628fn test_select_line(cx: &mut TestAppContext) {
7629 init_test(cx, |_| {});
7630
7631 let editor = cx.add_window(|window, cx| {
7632 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7633 build_editor(buffer, window, cx)
7634 });
7635 _ = editor.update(cx, |editor, window, cx| {
7636 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7637 s.select_display_ranges([
7638 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7639 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7640 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7641 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7642 ])
7643 });
7644 editor.select_line(&SelectLine, window, cx);
7645 assert_eq!(
7646 display_ranges(editor, cx),
7647 vec![
7648 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7649 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7650 ]
7651 );
7652 });
7653
7654 _ = editor.update(cx, |editor, window, cx| {
7655 editor.select_line(&SelectLine, window, cx);
7656 assert_eq!(
7657 display_ranges(editor, cx),
7658 vec![
7659 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7660 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7661 ]
7662 );
7663 });
7664
7665 _ = editor.update(cx, |editor, window, cx| {
7666 editor.select_line(&SelectLine, window, cx);
7667 assert_eq!(
7668 display_ranges(editor, cx),
7669 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7670 );
7671 });
7672}
7673
7674#[gpui::test]
7675async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7676 init_test(cx, |_| {});
7677 let mut cx = EditorTestContext::new(cx).await;
7678
7679 #[track_caller]
7680 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7681 cx.set_state(initial_state);
7682 cx.update_editor(|e, window, cx| {
7683 e.split_selection_into_lines(&Default::default(), window, cx)
7684 });
7685 cx.assert_editor_state(expected_state);
7686 }
7687
7688 // Selection starts and ends at the middle of lines, left-to-right
7689 test(
7690 &mut cx,
7691 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7692 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7693 );
7694 // Same thing, right-to-left
7695 test(
7696 &mut cx,
7697 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7698 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7699 );
7700
7701 // Whole buffer, left-to-right, last line *doesn't* end with newline
7702 test(
7703 &mut cx,
7704 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7705 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7706 );
7707 // Same thing, right-to-left
7708 test(
7709 &mut cx,
7710 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7711 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7712 );
7713
7714 // Whole buffer, left-to-right, last line ends with newline
7715 test(
7716 &mut cx,
7717 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7718 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7719 );
7720 // Same thing, right-to-left
7721 test(
7722 &mut cx,
7723 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7724 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7725 );
7726
7727 // Starts at the end of a line, ends at the start of another
7728 test(
7729 &mut cx,
7730 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7731 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7732 );
7733}
7734
7735#[gpui::test]
7736async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7737 init_test(cx, |_| {});
7738
7739 let editor = cx.add_window(|window, cx| {
7740 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7741 build_editor(buffer, window, cx)
7742 });
7743
7744 // setup
7745 _ = editor.update(cx, |editor, window, cx| {
7746 editor.fold_creases(
7747 vec![
7748 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7749 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7750 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7751 ],
7752 true,
7753 window,
7754 cx,
7755 );
7756 assert_eq!(
7757 editor.display_text(cx),
7758 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7759 );
7760 });
7761
7762 _ = editor.update(cx, |editor, window, cx| {
7763 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7764 s.select_display_ranges([
7765 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7766 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7767 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7768 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7769 ])
7770 });
7771 editor.split_selection_into_lines(&Default::default(), window, cx);
7772 assert_eq!(
7773 editor.display_text(cx),
7774 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7775 );
7776 });
7777 EditorTestContext::for_editor(editor, cx)
7778 .await
7779 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7780
7781 _ = editor.update(cx, |editor, window, cx| {
7782 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7783 s.select_display_ranges([
7784 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7785 ])
7786 });
7787 editor.split_selection_into_lines(&Default::default(), window, cx);
7788 assert_eq!(
7789 editor.display_text(cx),
7790 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7791 );
7792 assert_eq!(
7793 display_ranges(editor, cx),
7794 [
7795 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7796 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7797 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7798 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7799 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7800 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7801 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7802 ]
7803 );
7804 });
7805 EditorTestContext::for_editor(editor, cx)
7806 .await
7807 .assert_editor_state(
7808 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7809 );
7810}
7811
7812#[gpui::test]
7813async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7814 init_test(cx, |_| {});
7815
7816 let mut cx = EditorTestContext::new(cx).await;
7817
7818 cx.set_state(indoc!(
7819 r#"abc
7820 defˇghi
7821
7822 jk
7823 nlmo
7824 "#
7825 ));
7826
7827 cx.update_editor(|editor, window, cx| {
7828 editor.add_selection_above(&Default::default(), window, cx);
7829 });
7830
7831 cx.assert_editor_state(indoc!(
7832 r#"abcˇ
7833 defˇghi
7834
7835 jk
7836 nlmo
7837 "#
7838 ));
7839
7840 cx.update_editor(|editor, window, cx| {
7841 editor.add_selection_above(&Default::default(), window, cx);
7842 });
7843
7844 cx.assert_editor_state(indoc!(
7845 r#"abcˇ
7846 defˇghi
7847
7848 jk
7849 nlmo
7850 "#
7851 ));
7852
7853 cx.update_editor(|editor, window, cx| {
7854 editor.add_selection_below(&Default::default(), window, cx);
7855 });
7856
7857 cx.assert_editor_state(indoc!(
7858 r#"abc
7859 defˇghi
7860
7861 jk
7862 nlmo
7863 "#
7864 ));
7865
7866 cx.update_editor(|editor, window, cx| {
7867 editor.undo_selection(&Default::default(), window, cx);
7868 });
7869
7870 cx.assert_editor_state(indoc!(
7871 r#"abcˇ
7872 defˇghi
7873
7874 jk
7875 nlmo
7876 "#
7877 ));
7878
7879 cx.update_editor(|editor, window, cx| {
7880 editor.redo_selection(&Default::default(), window, cx);
7881 });
7882
7883 cx.assert_editor_state(indoc!(
7884 r#"abc
7885 defˇghi
7886
7887 jk
7888 nlmo
7889 "#
7890 ));
7891
7892 cx.update_editor(|editor, window, cx| {
7893 editor.add_selection_below(&Default::default(), window, cx);
7894 });
7895
7896 cx.assert_editor_state(indoc!(
7897 r#"abc
7898 defˇghi
7899 ˇ
7900 jk
7901 nlmo
7902 "#
7903 ));
7904
7905 cx.update_editor(|editor, window, cx| {
7906 editor.add_selection_below(&Default::default(), window, cx);
7907 });
7908
7909 cx.assert_editor_state(indoc!(
7910 r#"abc
7911 defˇghi
7912 ˇ
7913 jkˇ
7914 nlmo
7915 "#
7916 ));
7917
7918 cx.update_editor(|editor, window, cx| {
7919 editor.add_selection_below(&Default::default(), window, cx);
7920 });
7921
7922 cx.assert_editor_state(indoc!(
7923 r#"abc
7924 defˇghi
7925 ˇ
7926 jkˇ
7927 nlmˇo
7928 "#
7929 ));
7930
7931 cx.update_editor(|editor, window, cx| {
7932 editor.add_selection_below(&Default::default(), window, cx);
7933 });
7934
7935 cx.assert_editor_state(indoc!(
7936 r#"abc
7937 defˇghi
7938 ˇ
7939 jkˇ
7940 nlmˇo
7941 ˇ"#
7942 ));
7943
7944 // change selections
7945 cx.set_state(indoc!(
7946 r#"abc
7947 def«ˇg»hi
7948
7949 jk
7950 nlmo
7951 "#
7952 ));
7953
7954 cx.update_editor(|editor, window, cx| {
7955 editor.add_selection_below(&Default::default(), window, cx);
7956 });
7957
7958 cx.assert_editor_state(indoc!(
7959 r#"abc
7960 def«ˇg»hi
7961
7962 jk
7963 nlm«ˇo»
7964 "#
7965 ));
7966
7967 cx.update_editor(|editor, window, cx| {
7968 editor.add_selection_below(&Default::default(), window, cx);
7969 });
7970
7971 cx.assert_editor_state(indoc!(
7972 r#"abc
7973 def«ˇg»hi
7974
7975 jk
7976 nlm«ˇo»
7977 "#
7978 ));
7979
7980 cx.update_editor(|editor, window, cx| {
7981 editor.add_selection_above(&Default::default(), window, cx);
7982 });
7983
7984 cx.assert_editor_state(indoc!(
7985 r#"abc
7986 def«ˇg»hi
7987
7988 jk
7989 nlmo
7990 "#
7991 ));
7992
7993 cx.update_editor(|editor, window, cx| {
7994 editor.add_selection_above(&Default::default(), window, cx);
7995 });
7996
7997 cx.assert_editor_state(indoc!(
7998 r#"abc
7999 def«ˇg»hi
8000
8001 jk
8002 nlmo
8003 "#
8004 ));
8005
8006 // Change selections again
8007 cx.set_state(indoc!(
8008 r#"a«bc
8009 defgˇ»hi
8010
8011 jk
8012 nlmo
8013 "#
8014 ));
8015
8016 cx.update_editor(|editor, window, cx| {
8017 editor.add_selection_below(&Default::default(), window, cx);
8018 });
8019
8020 cx.assert_editor_state(indoc!(
8021 r#"a«bcˇ»
8022 d«efgˇ»hi
8023
8024 j«kˇ»
8025 nlmo
8026 "#
8027 ));
8028
8029 cx.update_editor(|editor, window, cx| {
8030 editor.add_selection_below(&Default::default(), window, cx);
8031 });
8032 cx.assert_editor_state(indoc!(
8033 r#"a«bcˇ»
8034 d«efgˇ»hi
8035
8036 j«kˇ»
8037 n«lmoˇ»
8038 "#
8039 ));
8040 cx.update_editor(|editor, window, cx| {
8041 editor.add_selection_above(&Default::default(), window, cx);
8042 });
8043
8044 cx.assert_editor_state(indoc!(
8045 r#"a«bcˇ»
8046 d«efgˇ»hi
8047
8048 j«kˇ»
8049 nlmo
8050 "#
8051 ));
8052
8053 // Change selections again
8054 cx.set_state(indoc!(
8055 r#"abc
8056 d«ˇefghi
8057
8058 jk
8059 nlm»o
8060 "#
8061 ));
8062
8063 cx.update_editor(|editor, window, cx| {
8064 editor.add_selection_above(&Default::default(), window, cx);
8065 });
8066
8067 cx.assert_editor_state(indoc!(
8068 r#"a«ˇbc»
8069 d«ˇef»ghi
8070
8071 j«ˇk»
8072 n«ˇlm»o
8073 "#
8074 ));
8075
8076 cx.update_editor(|editor, window, cx| {
8077 editor.add_selection_below(&Default::default(), window, cx);
8078 });
8079
8080 cx.assert_editor_state(indoc!(
8081 r#"abc
8082 d«ˇef»ghi
8083
8084 j«ˇk»
8085 n«ˇlm»o
8086 "#
8087 ));
8088}
8089
8090#[gpui::test]
8091async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8092 init_test(cx, |_| {});
8093 let mut cx = EditorTestContext::new(cx).await;
8094
8095 cx.set_state(indoc!(
8096 r#"line onˇe
8097 liˇne two
8098 line three
8099 line four"#
8100 ));
8101
8102 cx.update_editor(|editor, window, cx| {
8103 editor.add_selection_below(&Default::default(), window, cx);
8104 });
8105
8106 // test multiple cursors expand in the same direction
8107 cx.assert_editor_state(indoc!(
8108 r#"line onˇe
8109 liˇne twˇo
8110 liˇne three
8111 line four"#
8112 ));
8113
8114 cx.update_editor(|editor, window, cx| {
8115 editor.add_selection_below(&Default::default(), window, cx);
8116 });
8117
8118 cx.update_editor(|editor, window, cx| {
8119 editor.add_selection_below(&Default::default(), window, cx);
8120 });
8121
8122 // test multiple cursors expand below overflow
8123 cx.assert_editor_state(indoc!(
8124 r#"line onˇe
8125 liˇne twˇo
8126 liˇne thˇree
8127 liˇne foˇur"#
8128 ));
8129
8130 cx.update_editor(|editor, window, cx| {
8131 editor.add_selection_above(&Default::default(), window, cx);
8132 });
8133
8134 // test multiple cursors retrieves back correctly
8135 cx.assert_editor_state(indoc!(
8136 r#"line onˇe
8137 liˇne twˇo
8138 liˇne thˇree
8139 line four"#
8140 ));
8141
8142 cx.update_editor(|editor, window, cx| {
8143 editor.add_selection_above(&Default::default(), window, cx);
8144 });
8145
8146 cx.update_editor(|editor, window, cx| {
8147 editor.add_selection_above(&Default::default(), window, cx);
8148 });
8149
8150 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8151 cx.assert_editor_state(indoc!(
8152 r#"liˇne onˇe
8153 liˇne two
8154 line three
8155 line four"#
8156 ));
8157
8158 cx.update_editor(|editor, window, cx| {
8159 editor.undo_selection(&Default::default(), window, cx);
8160 });
8161
8162 // test undo
8163 cx.assert_editor_state(indoc!(
8164 r#"line onˇe
8165 liˇne twˇo
8166 line three
8167 line four"#
8168 ));
8169
8170 cx.update_editor(|editor, window, cx| {
8171 editor.redo_selection(&Default::default(), window, cx);
8172 });
8173
8174 // test redo
8175 cx.assert_editor_state(indoc!(
8176 r#"liˇne onˇe
8177 liˇne two
8178 line three
8179 line four"#
8180 ));
8181
8182 cx.set_state(indoc!(
8183 r#"abcd
8184 ef«ghˇ»
8185 ijkl
8186 «mˇ»nop"#
8187 ));
8188
8189 cx.update_editor(|editor, window, cx| {
8190 editor.add_selection_above(&Default::default(), window, cx);
8191 });
8192
8193 // test multiple selections expand in the same direction
8194 cx.assert_editor_state(indoc!(
8195 r#"ab«cdˇ»
8196 ef«ghˇ»
8197 «iˇ»jkl
8198 «mˇ»nop"#
8199 ));
8200
8201 cx.update_editor(|editor, window, cx| {
8202 editor.add_selection_above(&Default::default(), window, cx);
8203 });
8204
8205 // test multiple selection upward overflow
8206 cx.assert_editor_state(indoc!(
8207 r#"ab«cdˇ»
8208 «eˇ»f«ghˇ»
8209 «iˇ»jkl
8210 «mˇ»nop"#
8211 ));
8212
8213 cx.update_editor(|editor, window, cx| {
8214 editor.add_selection_below(&Default::default(), window, cx);
8215 });
8216
8217 // test multiple selection retrieves back correctly
8218 cx.assert_editor_state(indoc!(
8219 r#"abcd
8220 ef«ghˇ»
8221 «iˇ»jkl
8222 «mˇ»nop"#
8223 ));
8224
8225 cx.update_editor(|editor, window, cx| {
8226 editor.add_selection_below(&Default::default(), window, cx);
8227 });
8228
8229 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8230 cx.assert_editor_state(indoc!(
8231 r#"abcd
8232 ef«ghˇ»
8233 ij«klˇ»
8234 «mˇ»nop"#
8235 ));
8236
8237 cx.update_editor(|editor, window, cx| {
8238 editor.undo_selection(&Default::default(), window, cx);
8239 });
8240
8241 // test undo
8242 cx.assert_editor_state(indoc!(
8243 r#"abcd
8244 ef«ghˇ»
8245 «iˇ»jkl
8246 «mˇ»nop"#
8247 ));
8248
8249 cx.update_editor(|editor, window, cx| {
8250 editor.redo_selection(&Default::default(), window, cx);
8251 });
8252
8253 // test redo
8254 cx.assert_editor_state(indoc!(
8255 r#"abcd
8256 ef«ghˇ»
8257 ij«klˇ»
8258 «mˇ»nop"#
8259 ));
8260}
8261
8262#[gpui::test]
8263async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8264 init_test(cx, |_| {});
8265 let mut cx = EditorTestContext::new(cx).await;
8266
8267 cx.set_state(indoc!(
8268 r#"line onˇe
8269 liˇne two
8270 line three
8271 line four"#
8272 ));
8273
8274 cx.update_editor(|editor, window, cx| {
8275 editor.add_selection_below(&Default::default(), window, cx);
8276 editor.add_selection_below(&Default::default(), window, cx);
8277 editor.add_selection_below(&Default::default(), window, cx);
8278 });
8279
8280 // initial state with two multi cursor groups
8281 cx.assert_editor_state(indoc!(
8282 r#"line onˇe
8283 liˇne twˇo
8284 liˇne thˇree
8285 liˇne foˇur"#
8286 ));
8287
8288 // add single cursor in middle - simulate opt click
8289 cx.update_editor(|editor, window, cx| {
8290 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8291 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8292 editor.end_selection(window, cx);
8293 });
8294
8295 cx.assert_editor_state(indoc!(
8296 r#"line onˇe
8297 liˇne twˇo
8298 liˇneˇ thˇree
8299 liˇne foˇur"#
8300 ));
8301
8302 cx.update_editor(|editor, window, cx| {
8303 editor.add_selection_above(&Default::default(), window, cx);
8304 });
8305
8306 // test new added selection expands above and existing selection shrinks
8307 cx.assert_editor_state(indoc!(
8308 r#"line onˇe
8309 liˇneˇ twˇo
8310 liˇneˇ thˇree
8311 line four"#
8312 ));
8313
8314 cx.update_editor(|editor, window, cx| {
8315 editor.add_selection_above(&Default::default(), window, cx);
8316 });
8317
8318 // test new added selection expands above and existing selection shrinks
8319 cx.assert_editor_state(indoc!(
8320 r#"lineˇ onˇe
8321 liˇneˇ twˇo
8322 lineˇ three
8323 line four"#
8324 ));
8325
8326 // intial state with two selection groups
8327 cx.set_state(indoc!(
8328 r#"abcd
8329 ef«ghˇ»
8330 ijkl
8331 «mˇ»nop"#
8332 ));
8333
8334 cx.update_editor(|editor, window, cx| {
8335 editor.add_selection_above(&Default::default(), window, cx);
8336 editor.add_selection_above(&Default::default(), window, cx);
8337 });
8338
8339 cx.assert_editor_state(indoc!(
8340 r#"ab«cdˇ»
8341 «eˇ»f«ghˇ»
8342 «iˇ»jkl
8343 «mˇ»nop"#
8344 ));
8345
8346 // add single selection in middle - simulate opt drag
8347 cx.update_editor(|editor, window, cx| {
8348 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8349 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8350 editor.update_selection(
8351 DisplayPoint::new(DisplayRow(2), 4),
8352 0,
8353 gpui::Point::<f32>::default(),
8354 window,
8355 cx,
8356 );
8357 editor.end_selection(window, cx);
8358 });
8359
8360 cx.assert_editor_state(indoc!(
8361 r#"ab«cdˇ»
8362 «eˇ»f«ghˇ»
8363 «iˇ»jk«lˇ»
8364 «mˇ»nop"#
8365 ));
8366
8367 cx.update_editor(|editor, window, cx| {
8368 editor.add_selection_below(&Default::default(), window, cx);
8369 });
8370
8371 // test new added selection expands below, others shrinks from above
8372 cx.assert_editor_state(indoc!(
8373 r#"abcd
8374 ef«ghˇ»
8375 «iˇ»jk«lˇ»
8376 «mˇ»no«pˇ»"#
8377 ));
8378}
8379
8380#[gpui::test]
8381async fn test_select_next(cx: &mut TestAppContext) {
8382 init_test(cx, |_| {});
8383 let mut cx = EditorTestContext::new(cx).await;
8384
8385 // Enable case sensitive search.
8386 update_test_editor_settings(&mut cx, |settings| {
8387 let mut search_settings = SearchSettingsContent::default();
8388 search_settings.case_sensitive = Some(true);
8389 settings.search = Some(search_settings);
8390 });
8391
8392 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8393
8394 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8395 .unwrap();
8396 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8397
8398 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8399 .unwrap();
8400 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8401
8402 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8403 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8404
8405 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8406 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8407
8408 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8409 .unwrap();
8410 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8411
8412 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8413 .unwrap();
8414 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8415
8416 // Test selection direction should be preserved
8417 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8418
8419 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8420 .unwrap();
8421 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8422
8423 // Test case sensitivity
8424 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8425 cx.update_editor(|e, window, cx| {
8426 e.select_next(&SelectNext::default(), window, cx).unwrap();
8427 });
8428 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8429
8430 // Disable case sensitive search.
8431 update_test_editor_settings(&mut cx, |settings| {
8432 let mut search_settings = SearchSettingsContent::default();
8433 search_settings.case_sensitive = Some(false);
8434 settings.search = Some(search_settings);
8435 });
8436
8437 cx.set_state("«ˇfoo»\nFOO\nFoo");
8438 cx.update_editor(|e, window, cx| {
8439 e.select_next(&SelectNext::default(), window, cx).unwrap();
8440 e.select_next(&SelectNext::default(), window, cx).unwrap();
8441 });
8442 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8443}
8444
8445#[gpui::test]
8446async fn test_select_all_matches(cx: &mut TestAppContext) {
8447 init_test(cx, |_| {});
8448 let mut cx = EditorTestContext::new(cx).await;
8449
8450 // Enable case sensitive search.
8451 update_test_editor_settings(&mut cx, |settings| {
8452 let mut search_settings = SearchSettingsContent::default();
8453 search_settings.case_sensitive = Some(true);
8454 settings.search = Some(search_settings);
8455 });
8456
8457 // Test caret-only selections
8458 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8459 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8460 .unwrap();
8461 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8462
8463 // Test left-to-right selections
8464 cx.set_state("abc\n«abcˇ»\nabc");
8465 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8466 .unwrap();
8467 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8468
8469 // Test right-to-left selections
8470 cx.set_state("abc\n«ˇabc»\nabc");
8471 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8472 .unwrap();
8473 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8474
8475 // Test selecting whitespace with caret selection
8476 cx.set_state("abc\nˇ abc\nabc");
8477 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8478 .unwrap();
8479 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8480
8481 // Test selecting whitespace with left-to-right selection
8482 cx.set_state("abc\n«ˇ »abc\nabc");
8483 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8484 .unwrap();
8485 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8486
8487 // Test no matches with right-to-left selection
8488 cx.set_state("abc\n« ˇ»abc\nabc");
8489 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8490 .unwrap();
8491 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8492
8493 // Test with a single word and clip_at_line_ends=true (#29823)
8494 cx.set_state("aˇbc");
8495 cx.update_editor(|e, window, cx| {
8496 e.set_clip_at_line_ends(true, cx);
8497 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8498 e.set_clip_at_line_ends(false, cx);
8499 });
8500 cx.assert_editor_state("«abcˇ»");
8501
8502 // Test case sensitivity
8503 cx.set_state("fˇoo\nFOO\nFoo");
8504 cx.update_editor(|e, window, cx| {
8505 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8506 });
8507 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8508
8509 // Disable case sensitive search.
8510 update_test_editor_settings(&mut cx, |settings| {
8511 let mut search_settings = SearchSettingsContent::default();
8512 search_settings.case_sensitive = Some(false);
8513 settings.search = Some(search_settings);
8514 });
8515
8516 cx.set_state("fˇoo\nFOO\nFoo");
8517 cx.update_editor(|e, window, cx| {
8518 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8519 });
8520 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8521}
8522
8523#[gpui::test]
8524async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8525 init_test(cx, |_| {});
8526
8527 let mut cx = EditorTestContext::new(cx).await;
8528
8529 let large_body_1 = "\nd".repeat(200);
8530 let large_body_2 = "\ne".repeat(200);
8531
8532 cx.set_state(&format!(
8533 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8534 ));
8535 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8536 let scroll_position = editor.scroll_position(cx);
8537 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8538 scroll_position
8539 });
8540
8541 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8542 .unwrap();
8543 cx.assert_editor_state(&format!(
8544 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8545 ));
8546 let scroll_position_after_selection =
8547 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8548 assert_eq!(
8549 initial_scroll_position, scroll_position_after_selection,
8550 "Scroll position should not change after selecting all matches"
8551 );
8552}
8553
8554#[gpui::test]
8555async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8556 init_test(cx, |_| {});
8557
8558 let mut cx = EditorLspTestContext::new_rust(
8559 lsp::ServerCapabilities {
8560 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8561 ..Default::default()
8562 },
8563 cx,
8564 )
8565 .await;
8566
8567 cx.set_state(indoc! {"
8568 line 1
8569 line 2
8570 linˇe 3
8571 line 4
8572 line 5
8573 "});
8574
8575 // Make an edit
8576 cx.update_editor(|editor, window, cx| {
8577 editor.handle_input("X", window, cx);
8578 });
8579
8580 // Move cursor to a different position
8581 cx.update_editor(|editor, window, cx| {
8582 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8583 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8584 });
8585 });
8586
8587 cx.assert_editor_state(indoc! {"
8588 line 1
8589 line 2
8590 linXe 3
8591 line 4
8592 liˇne 5
8593 "});
8594
8595 cx.lsp
8596 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8597 Ok(Some(vec![lsp::TextEdit::new(
8598 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8599 "PREFIX ".to_string(),
8600 )]))
8601 });
8602
8603 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8604 .unwrap()
8605 .await
8606 .unwrap();
8607
8608 cx.assert_editor_state(indoc! {"
8609 PREFIX line 1
8610 line 2
8611 linXe 3
8612 line 4
8613 liˇne 5
8614 "});
8615
8616 // Undo formatting
8617 cx.update_editor(|editor, window, cx| {
8618 editor.undo(&Default::default(), window, cx);
8619 });
8620
8621 // Verify cursor moved back to position after edit
8622 cx.assert_editor_state(indoc! {"
8623 line 1
8624 line 2
8625 linXˇe 3
8626 line 4
8627 line 5
8628 "});
8629}
8630
8631#[gpui::test]
8632async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8633 init_test(cx, |_| {});
8634
8635 let mut cx = EditorTestContext::new(cx).await;
8636
8637 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8638 cx.update_editor(|editor, window, cx| {
8639 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8640 });
8641
8642 cx.set_state(indoc! {"
8643 line 1
8644 line 2
8645 linˇe 3
8646 line 4
8647 line 5
8648 line 6
8649 line 7
8650 line 8
8651 line 9
8652 line 10
8653 "});
8654
8655 let snapshot = cx.buffer_snapshot();
8656 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8657
8658 cx.update(|_, cx| {
8659 provider.update(cx, |provider, _| {
8660 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8661 id: None,
8662 edits: vec![(edit_position..edit_position, "X".into())],
8663 edit_preview: None,
8664 }))
8665 })
8666 });
8667
8668 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8669 cx.update_editor(|editor, window, cx| {
8670 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8671 });
8672
8673 cx.assert_editor_state(indoc! {"
8674 line 1
8675 line 2
8676 lineXˇ 3
8677 line 4
8678 line 5
8679 line 6
8680 line 7
8681 line 8
8682 line 9
8683 line 10
8684 "});
8685
8686 cx.update_editor(|editor, window, cx| {
8687 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8688 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8689 });
8690 });
8691
8692 cx.assert_editor_state(indoc! {"
8693 line 1
8694 line 2
8695 lineX 3
8696 line 4
8697 line 5
8698 line 6
8699 line 7
8700 line 8
8701 line 9
8702 liˇne 10
8703 "});
8704
8705 cx.update_editor(|editor, window, cx| {
8706 editor.undo(&Default::default(), window, cx);
8707 });
8708
8709 cx.assert_editor_state(indoc! {"
8710 line 1
8711 line 2
8712 lineˇ 3
8713 line 4
8714 line 5
8715 line 6
8716 line 7
8717 line 8
8718 line 9
8719 line 10
8720 "});
8721}
8722
8723#[gpui::test]
8724async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728 cx.set_state(
8729 r#"let foo = 2;
8730lˇet foo = 2;
8731let fooˇ = 2;
8732let foo = 2;
8733let foo = ˇ2;"#,
8734 );
8735
8736 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8737 .unwrap();
8738 cx.assert_editor_state(
8739 r#"let foo = 2;
8740«letˇ» foo = 2;
8741let «fooˇ» = 2;
8742let foo = 2;
8743let foo = «2ˇ»;"#,
8744 );
8745
8746 // noop for multiple selections with different contents
8747 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8748 .unwrap();
8749 cx.assert_editor_state(
8750 r#"let foo = 2;
8751«letˇ» foo = 2;
8752let «fooˇ» = 2;
8753let foo = 2;
8754let foo = «2ˇ»;"#,
8755 );
8756
8757 // Test last selection direction should be preserved
8758 cx.set_state(
8759 r#"let foo = 2;
8760let foo = 2;
8761let «fooˇ» = 2;
8762let «ˇfoo» = 2;
8763let foo = 2;"#,
8764 );
8765
8766 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8767 .unwrap();
8768 cx.assert_editor_state(
8769 r#"let foo = 2;
8770let foo = 2;
8771let «fooˇ» = 2;
8772let «ˇfoo» = 2;
8773let «ˇfoo» = 2;"#,
8774 );
8775}
8776
8777#[gpui::test]
8778async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8779 init_test(cx, |_| {});
8780
8781 let mut cx =
8782 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8783
8784 cx.assert_editor_state(indoc! {"
8785 ˇbbb
8786 ccc
8787
8788 bbb
8789 ccc
8790 "});
8791 cx.dispatch_action(SelectPrevious::default());
8792 cx.assert_editor_state(indoc! {"
8793 «bbbˇ»
8794 ccc
8795
8796 bbb
8797 ccc
8798 "});
8799 cx.dispatch_action(SelectPrevious::default());
8800 cx.assert_editor_state(indoc! {"
8801 «bbbˇ»
8802 ccc
8803
8804 «bbbˇ»
8805 ccc
8806 "});
8807}
8808
8809#[gpui::test]
8810async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8811 init_test(cx, |_| {});
8812
8813 let mut cx = EditorTestContext::new(cx).await;
8814 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8815
8816 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8817 .unwrap();
8818 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8819
8820 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8821 .unwrap();
8822 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8823
8824 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8825 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8826
8827 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8828 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8829
8830 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8831 .unwrap();
8832 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8833
8834 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8835 .unwrap();
8836 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8837}
8838
8839#[gpui::test]
8840async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8841 init_test(cx, |_| {});
8842
8843 let mut cx = EditorTestContext::new(cx).await;
8844 cx.set_state("aˇ");
8845
8846 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8847 .unwrap();
8848 cx.assert_editor_state("«aˇ»");
8849 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8850 .unwrap();
8851 cx.assert_editor_state("«aˇ»");
8852}
8853
8854#[gpui::test]
8855async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8856 init_test(cx, |_| {});
8857
8858 let mut cx = EditorTestContext::new(cx).await;
8859 cx.set_state(
8860 r#"let foo = 2;
8861lˇet foo = 2;
8862let fooˇ = 2;
8863let foo = 2;
8864let foo = ˇ2;"#,
8865 );
8866
8867 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8868 .unwrap();
8869 cx.assert_editor_state(
8870 r#"let foo = 2;
8871«letˇ» foo = 2;
8872let «fooˇ» = 2;
8873let foo = 2;
8874let foo = «2ˇ»;"#,
8875 );
8876
8877 // noop for multiple selections with different contents
8878 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8879 .unwrap();
8880 cx.assert_editor_state(
8881 r#"let foo = 2;
8882«letˇ» foo = 2;
8883let «fooˇ» = 2;
8884let foo = 2;
8885let foo = «2ˇ»;"#,
8886 );
8887}
8888
8889#[gpui::test]
8890async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8891 init_test(cx, |_| {});
8892 let mut cx = EditorTestContext::new(cx).await;
8893
8894 // Enable case sensitive search.
8895 update_test_editor_settings(&mut cx, |settings| {
8896 let mut search_settings = SearchSettingsContent::default();
8897 search_settings.case_sensitive = Some(true);
8898 settings.search = Some(search_settings);
8899 });
8900
8901 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8902
8903 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8904 .unwrap();
8905 // selection direction is preserved
8906 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8907
8908 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8909 .unwrap();
8910 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8911
8912 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8913 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8914
8915 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8916 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8917
8918 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8919 .unwrap();
8920 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8921
8922 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8923 .unwrap();
8924 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8925
8926 // Test case sensitivity
8927 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
8928 cx.update_editor(|e, window, cx| {
8929 e.select_previous(&SelectPrevious::default(), window, cx)
8930 .unwrap();
8931 e.select_previous(&SelectPrevious::default(), window, cx)
8932 .unwrap();
8933 });
8934 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8935
8936 // Disable case sensitive search.
8937 update_test_editor_settings(&mut cx, |settings| {
8938 let mut search_settings = SearchSettingsContent::default();
8939 search_settings.case_sensitive = Some(false);
8940 settings.search = Some(search_settings);
8941 });
8942
8943 cx.set_state("foo\nFOO\n«ˇFoo»");
8944 cx.update_editor(|e, window, cx| {
8945 e.select_previous(&SelectPrevious::default(), window, cx)
8946 .unwrap();
8947 e.select_previous(&SelectPrevious::default(), window, cx)
8948 .unwrap();
8949 });
8950 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8951}
8952
8953#[gpui::test]
8954async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8955 init_test(cx, |_| {});
8956
8957 let language = Arc::new(Language::new(
8958 LanguageConfig::default(),
8959 Some(tree_sitter_rust::LANGUAGE.into()),
8960 ));
8961
8962 let text = r#"
8963 use mod1::mod2::{mod3, mod4};
8964
8965 fn fn_1(param1: bool, param2: &str) {
8966 let var1 = "text";
8967 }
8968 "#
8969 .unindent();
8970
8971 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8972 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8973 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8974
8975 editor
8976 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8977 .await;
8978
8979 editor.update_in(cx, |editor, window, cx| {
8980 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8981 s.select_display_ranges([
8982 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8983 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8984 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8985 ]);
8986 });
8987 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8988 });
8989 editor.update(cx, |editor, cx| {
8990 assert_text_with_selections(
8991 editor,
8992 indoc! {r#"
8993 use mod1::mod2::{mod3, «mod4ˇ»};
8994
8995 fn fn_1«ˇ(param1: bool, param2: &str)» {
8996 let var1 = "«ˇtext»";
8997 }
8998 "#},
8999 cx,
9000 );
9001 });
9002
9003 editor.update_in(cx, |editor, window, cx| {
9004 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9005 });
9006 editor.update(cx, |editor, cx| {
9007 assert_text_with_selections(
9008 editor,
9009 indoc! {r#"
9010 use mod1::mod2::«{mod3, mod4}ˇ»;
9011
9012 «ˇfn fn_1(param1: bool, param2: &str) {
9013 let var1 = "text";
9014 }»
9015 "#},
9016 cx,
9017 );
9018 });
9019
9020 editor.update_in(cx, |editor, window, cx| {
9021 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9022 });
9023 assert_eq!(
9024 editor.update(cx, |editor, cx| editor
9025 .selections
9026 .display_ranges(&editor.display_snapshot(cx))),
9027 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9028 );
9029
9030 // Trying to expand the selected syntax node one more time has no effect.
9031 editor.update_in(cx, |editor, window, cx| {
9032 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9033 });
9034 assert_eq!(
9035 editor.update(cx, |editor, cx| editor
9036 .selections
9037 .display_ranges(&editor.display_snapshot(cx))),
9038 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9039 );
9040
9041 editor.update_in(cx, |editor, window, cx| {
9042 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9043 });
9044 editor.update(cx, |editor, cx| {
9045 assert_text_with_selections(
9046 editor,
9047 indoc! {r#"
9048 use mod1::mod2::«{mod3, mod4}ˇ»;
9049
9050 «ˇfn fn_1(param1: bool, param2: &str) {
9051 let var1 = "text";
9052 }»
9053 "#},
9054 cx,
9055 );
9056 });
9057
9058 editor.update_in(cx, |editor, window, cx| {
9059 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9060 });
9061 editor.update(cx, |editor, cx| {
9062 assert_text_with_selections(
9063 editor,
9064 indoc! {r#"
9065 use mod1::mod2::{mod3, «mod4ˇ»};
9066
9067 fn fn_1«ˇ(param1: bool, param2: &str)» {
9068 let var1 = "«ˇtext»";
9069 }
9070 "#},
9071 cx,
9072 );
9073 });
9074
9075 editor.update_in(cx, |editor, window, cx| {
9076 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9077 });
9078 editor.update(cx, |editor, cx| {
9079 assert_text_with_selections(
9080 editor,
9081 indoc! {r#"
9082 use mod1::mod2::{mod3, moˇd4};
9083
9084 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9085 let var1 = "teˇxt";
9086 }
9087 "#},
9088 cx,
9089 );
9090 });
9091
9092 // Trying to shrink the selected syntax node one more time has no effect.
9093 editor.update_in(cx, |editor, window, cx| {
9094 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9095 });
9096 editor.update_in(cx, |editor, _, cx| {
9097 assert_text_with_selections(
9098 editor,
9099 indoc! {r#"
9100 use mod1::mod2::{mod3, moˇd4};
9101
9102 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9103 let var1 = "teˇxt";
9104 }
9105 "#},
9106 cx,
9107 );
9108 });
9109
9110 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9111 // a fold.
9112 editor.update_in(cx, |editor, window, cx| {
9113 editor.fold_creases(
9114 vec![
9115 Crease::simple(
9116 Point::new(0, 21)..Point::new(0, 24),
9117 FoldPlaceholder::test(),
9118 ),
9119 Crease::simple(
9120 Point::new(3, 20)..Point::new(3, 22),
9121 FoldPlaceholder::test(),
9122 ),
9123 ],
9124 true,
9125 window,
9126 cx,
9127 );
9128 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9129 });
9130 editor.update(cx, |editor, cx| {
9131 assert_text_with_selections(
9132 editor,
9133 indoc! {r#"
9134 use mod1::mod2::«{mod3, mod4}ˇ»;
9135
9136 fn fn_1«ˇ(param1: bool, param2: &str)» {
9137 let var1 = "«ˇtext»";
9138 }
9139 "#},
9140 cx,
9141 );
9142 });
9143}
9144
9145#[gpui::test]
9146async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9147 init_test(cx, |_| {});
9148
9149 let language = Arc::new(Language::new(
9150 LanguageConfig::default(),
9151 Some(tree_sitter_rust::LANGUAGE.into()),
9152 ));
9153
9154 let text = "let a = 2;";
9155
9156 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9157 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9158 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9159
9160 editor
9161 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9162 .await;
9163
9164 // Test case 1: Cursor at end of word
9165 editor.update_in(cx, |editor, window, cx| {
9166 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9167 s.select_display_ranges([
9168 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9169 ]);
9170 });
9171 });
9172 editor.update(cx, |editor, cx| {
9173 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9174 });
9175 editor.update_in(cx, |editor, window, cx| {
9176 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9177 });
9178 editor.update(cx, |editor, cx| {
9179 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9180 });
9181 editor.update_in(cx, |editor, window, cx| {
9182 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9183 });
9184 editor.update(cx, |editor, cx| {
9185 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9186 });
9187
9188 // Test case 2: Cursor at end of statement
9189 editor.update_in(cx, |editor, window, cx| {
9190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9191 s.select_display_ranges([
9192 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9193 ]);
9194 });
9195 });
9196 editor.update(cx, |editor, cx| {
9197 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9198 });
9199 editor.update_in(cx, |editor, window, cx| {
9200 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9201 });
9202 editor.update(cx, |editor, cx| {
9203 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9204 });
9205}
9206
9207#[gpui::test]
9208async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9209 init_test(cx, |_| {});
9210
9211 let language = Arc::new(Language::new(
9212 LanguageConfig {
9213 name: "JavaScript".into(),
9214 ..Default::default()
9215 },
9216 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9217 ));
9218
9219 let text = r#"
9220 let a = {
9221 key: "value",
9222 };
9223 "#
9224 .unindent();
9225
9226 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9227 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9228 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9229
9230 editor
9231 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9232 .await;
9233
9234 // Test case 1: Cursor after '{'
9235 editor.update_in(cx, |editor, window, cx| {
9236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9237 s.select_display_ranges([
9238 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9239 ]);
9240 });
9241 });
9242 editor.update(cx, |editor, cx| {
9243 assert_text_with_selections(
9244 editor,
9245 indoc! {r#"
9246 let a = {ˇ
9247 key: "value",
9248 };
9249 "#},
9250 cx,
9251 );
9252 });
9253 editor.update_in(cx, |editor, window, cx| {
9254 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9255 });
9256 editor.update(cx, |editor, cx| {
9257 assert_text_with_selections(
9258 editor,
9259 indoc! {r#"
9260 let a = «ˇ{
9261 key: "value",
9262 }»;
9263 "#},
9264 cx,
9265 );
9266 });
9267
9268 // Test case 2: Cursor after ':'
9269 editor.update_in(cx, |editor, window, cx| {
9270 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9271 s.select_display_ranges([
9272 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9273 ]);
9274 });
9275 });
9276 editor.update(cx, |editor, cx| {
9277 assert_text_with_selections(
9278 editor,
9279 indoc! {r#"
9280 let a = {
9281 key:ˇ "value",
9282 };
9283 "#},
9284 cx,
9285 );
9286 });
9287 editor.update_in(cx, |editor, window, cx| {
9288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9289 });
9290 editor.update(cx, |editor, cx| {
9291 assert_text_with_selections(
9292 editor,
9293 indoc! {r#"
9294 let a = {
9295 «ˇkey: "value"»,
9296 };
9297 "#},
9298 cx,
9299 );
9300 });
9301 editor.update_in(cx, |editor, window, cx| {
9302 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9303 });
9304 editor.update(cx, |editor, cx| {
9305 assert_text_with_selections(
9306 editor,
9307 indoc! {r#"
9308 let a = «ˇ{
9309 key: "value",
9310 }»;
9311 "#},
9312 cx,
9313 );
9314 });
9315
9316 // Test case 3: Cursor after ','
9317 editor.update_in(cx, |editor, window, cx| {
9318 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9319 s.select_display_ranges([
9320 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9321 ]);
9322 });
9323 });
9324 editor.update(cx, |editor, cx| {
9325 assert_text_with_selections(
9326 editor,
9327 indoc! {r#"
9328 let a = {
9329 key: "value",ˇ
9330 };
9331 "#},
9332 cx,
9333 );
9334 });
9335 editor.update_in(cx, |editor, window, cx| {
9336 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9337 });
9338 editor.update(cx, |editor, cx| {
9339 assert_text_with_selections(
9340 editor,
9341 indoc! {r#"
9342 let a = «ˇ{
9343 key: "value",
9344 }»;
9345 "#},
9346 cx,
9347 );
9348 });
9349
9350 // Test case 4: Cursor after ';'
9351 editor.update_in(cx, |editor, window, cx| {
9352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9353 s.select_display_ranges([
9354 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9355 ]);
9356 });
9357 });
9358 editor.update(cx, |editor, cx| {
9359 assert_text_with_selections(
9360 editor,
9361 indoc! {r#"
9362 let a = {
9363 key: "value",
9364 };ˇ
9365 "#},
9366 cx,
9367 );
9368 });
9369 editor.update_in(cx, |editor, window, cx| {
9370 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9371 });
9372 editor.update(cx, |editor, cx| {
9373 assert_text_with_selections(
9374 editor,
9375 indoc! {r#"
9376 «ˇlet a = {
9377 key: "value",
9378 };
9379 »"#},
9380 cx,
9381 );
9382 });
9383}
9384
9385#[gpui::test]
9386async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9387 init_test(cx, |_| {});
9388
9389 let language = Arc::new(Language::new(
9390 LanguageConfig::default(),
9391 Some(tree_sitter_rust::LANGUAGE.into()),
9392 ));
9393
9394 let text = r#"
9395 use mod1::mod2::{mod3, mod4};
9396
9397 fn fn_1(param1: bool, param2: &str) {
9398 let var1 = "hello world";
9399 }
9400 "#
9401 .unindent();
9402
9403 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9404 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9405 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9406
9407 editor
9408 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9409 .await;
9410
9411 // Test 1: Cursor on a letter of a string word
9412 editor.update_in(cx, |editor, window, cx| {
9413 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9414 s.select_display_ranges([
9415 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9416 ]);
9417 });
9418 });
9419 editor.update_in(cx, |editor, window, cx| {
9420 assert_text_with_selections(
9421 editor,
9422 indoc! {r#"
9423 use mod1::mod2::{mod3, mod4};
9424
9425 fn fn_1(param1: bool, param2: &str) {
9426 let var1 = "hˇello world";
9427 }
9428 "#},
9429 cx,
9430 );
9431 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9432 assert_text_with_selections(
9433 editor,
9434 indoc! {r#"
9435 use mod1::mod2::{mod3, mod4};
9436
9437 fn fn_1(param1: bool, param2: &str) {
9438 let var1 = "«ˇhello» world";
9439 }
9440 "#},
9441 cx,
9442 );
9443 });
9444
9445 // Test 2: Partial selection within a word
9446 editor.update_in(cx, |editor, window, cx| {
9447 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9448 s.select_display_ranges([
9449 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9450 ]);
9451 });
9452 });
9453 editor.update_in(cx, |editor, window, cx| {
9454 assert_text_with_selections(
9455 editor,
9456 indoc! {r#"
9457 use mod1::mod2::{mod3, mod4};
9458
9459 fn fn_1(param1: bool, param2: &str) {
9460 let var1 = "h«elˇ»lo world";
9461 }
9462 "#},
9463 cx,
9464 );
9465 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9466 assert_text_with_selections(
9467 editor,
9468 indoc! {r#"
9469 use mod1::mod2::{mod3, mod4};
9470
9471 fn fn_1(param1: bool, param2: &str) {
9472 let var1 = "«ˇhello» world";
9473 }
9474 "#},
9475 cx,
9476 );
9477 });
9478
9479 // Test 3: Complete word already selected
9480 editor.update_in(cx, |editor, window, cx| {
9481 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9482 s.select_display_ranges([
9483 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9484 ]);
9485 });
9486 });
9487 editor.update_in(cx, |editor, window, cx| {
9488 assert_text_with_selections(
9489 editor,
9490 indoc! {r#"
9491 use mod1::mod2::{mod3, mod4};
9492
9493 fn fn_1(param1: bool, param2: &str) {
9494 let var1 = "«helloˇ» world";
9495 }
9496 "#},
9497 cx,
9498 );
9499 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9500 assert_text_with_selections(
9501 editor,
9502 indoc! {r#"
9503 use mod1::mod2::{mod3, mod4};
9504
9505 fn fn_1(param1: bool, param2: &str) {
9506 let var1 = "«hello worldˇ»";
9507 }
9508 "#},
9509 cx,
9510 );
9511 });
9512
9513 // Test 4: Selection spanning across words
9514 editor.update_in(cx, |editor, window, cx| {
9515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9516 s.select_display_ranges([
9517 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9518 ]);
9519 });
9520 });
9521 editor.update_in(cx, |editor, window, cx| {
9522 assert_text_with_selections(
9523 editor,
9524 indoc! {r#"
9525 use mod1::mod2::{mod3, mod4};
9526
9527 fn fn_1(param1: bool, param2: &str) {
9528 let var1 = "hel«lo woˇ»rld";
9529 }
9530 "#},
9531 cx,
9532 );
9533 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9534 assert_text_with_selections(
9535 editor,
9536 indoc! {r#"
9537 use mod1::mod2::{mod3, mod4};
9538
9539 fn fn_1(param1: bool, param2: &str) {
9540 let var1 = "«ˇhello world»";
9541 }
9542 "#},
9543 cx,
9544 );
9545 });
9546
9547 // Test 5: Expansion beyond string
9548 editor.update_in(cx, |editor, window, cx| {
9549 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9550 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9551 assert_text_with_selections(
9552 editor,
9553 indoc! {r#"
9554 use mod1::mod2::{mod3, mod4};
9555
9556 fn fn_1(param1: bool, param2: &str) {
9557 «ˇlet var1 = "hello world";»
9558 }
9559 "#},
9560 cx,
9561 );
9562 });
9563}
9564
9565#[gpui::test]
9566async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9567 init_test(cx, |_| {});
9568
9569 let mut cx = EditorTestContext::new(cx).await;
9570
9571 let language = Arc::new(Language::new(
9572 LanguageConfig::default(),
9573 Some(tree_sitter_rust::LANGUAGE.into()),
9574 ));
9575
9576 cx.update_buffer(|buffer, cx| {
9577 buffer.set_language(Some(language), cx);
9578 });
9579
9580 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9581 cx.update_editor(|editor, window, cx| {
9582 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9583 });
9584
9585 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9586
9587 cx.set_state(indoc! { r#"fn a() {
9588 // what
9589 // a
9590 // ˇlong
9591 // method
9592 // I
9593 // sure
9594 // hope
9595 // it
9596 // works
9597 }"# });
9598
9599 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9600 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9601 cx.update(|_, cx| {
9602 multi_buffer.update(cx, |multi_buffer, cx| {
9603 multi_buffer.set_excerpts_for_path(
9604 PathKey::for_buffer(&buffer, cx),
9605 buffer,
9606 [Point::new(1, 0)..Point::new(1, 0)],
9607 3,
9608 cx,
9609 );
9610 });
9611 });
9612
9613 let editor2 = cx.new_window_entity(|window, cx| {
9614 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9615 });
9616
9617 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9618 cx.update_editor(|editor, window, cx| {
9619 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9620 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9621 })
9622 });
9623
9624 cx.assert_editor_state(indoc! { "
9625 fn a() {
9626 // what
9627 // a
9628 ˇ // long
9629 // method"});
9630
9631 cx.update_editor(|editor, window, cx| {
9632 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9633 });
9634
9635 // Although we could potentially make the action work when the syntax node
9636 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9637 // did. Maybe we could also expand the excerpt to contain the range?
9638 cx.assert_editor_state(indoc! { "
9639 fn a() {
9640 // what
9641 // a
9642 ˇ // long
9643 // method"});
9644}
9645
9646#[gpui::test]
9647async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9648 init_test(cx, |_| {});
9649
9650 let base_text = r#"
9651 impl A {
9652 // this is an uncommitted comment
9653
9654 fn b() {
9655 c();
9656 }
9657
9658 // this is another uncommitted comment
9659
9660 fn d() {
9661 // e
9662 // f
9663 }
9664 }
9665
9666 fn g() {
9667 // h
9668 }
9669 "#
9670 .unindent();
9671
9672 let text = r#"
9673 ˇimpl A {
9674
9675 fn b() {
9676 c();
9677 }
9678
9679 fn d() {
9680 // e
9681 // f
9682 }
9683 }
9684
9685 fn g() {
9686 // h
9687 }
9688 "#
9689 .unindent();
9690
9691 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9692 cx.set_state(&text);
9693 cx.set_head_text(&base_text);
9694 cx.update_editor(|editor, window, cx| {
9695 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9696 });
9697
9698 cx.assert_state_with_diff(
9699 "
9700 ˇimpl A {
9701 - // this is an uncommitted comment
9702
9703 fn b() {
9704 c();
9705 }
9706
9707 - // this is another uncommitted comment
9708 -
9709 fn d() {
9710 // e
9711 // f
9712 }
9713 }
9714
9715 fn g() {
9716 // h
9717 }
9718 "
9719 .unindent(),
9720 );
9721
9722 let expected_display_text = "
9723 impl A {
9724 // this is an uncommitted comment
9725
9726 fn b() {
9727 ⋯
9728 }
9729
9730 // this is another uncommitted comment
9731
9732 fn d() {
9733 ⋯
9734 }
9735 }
9736
9737 fn g() {
9738 ⋯
9739 }
9740 "
9741 .unindent();
9742
9743 cx.update_editor(|editor, window, cx| {
9744 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9745 assert_eq!(editor.display_text(cx), expected_display_text);
9746 });
9747}
9748
9749#[gpui::test]
9750async fn test_autoindent(cx: &mut TestAppContext) {
9751 init_test(cx, |_| {});
9752
9753 let language = Arc::new(
9754 Language::new(
9755 LanguageConfig {
9756 brackets: BracketPairConfig {
9757 pairs: vec![
9758 BracketPair {
9759 start: "{".to_string(),
9760 end: "}".to_string(),
9761 close: false,
9762 surround: false,
9763 newline: true,
9764 },
9765 BracketPair {
9766 start: "(".to_string(),
9767 end: ")".to_string(),
9768 close: false,
9769 surround: false,
9770 newline: true,
9771 },
9772 ],
9773 ..Default::default()
9774 },
9775 ..Default::default()
9776 },
9777 Some(tree_sitter_rust::LANGUAGE.into()),
9778 )
9779 .with_indents_query(
9780 r#"
9781 (_ "(" ")" @end) @indent
9782 (_ "{" "}" @end) @indent
9783 "#,
9784 )
9785 .unwrap(),
9786 );
9787
9788 let text = "fn a() {}";
9789
9790 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9791 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9792 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9793 editor
9794 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9795 .await;
9796
9797 editor.update_in(cx, |editor, window, cx| {
9798 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9799 s.select_ranges([
9800 MultiBufferOffset(5)..MultiBufferOffset(5),
9801 MultiBufferOffset(8)..MultiBufferOffset(8),
9802 MultiBufferOffset(9)..MultiBufferOffset(9),
9803 ])
9804 });
9805 editor.newline(&Newline, window, cx);
9806 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9807 assert_eq!(
9808 editor.selections.ranges(&editor.display_snapshot(cx)),
9809 &[
9810 Point::new(1, 4)..Point::new(1, 4),
9811 Point::new(3, 4)..Point::new(3, 4),
9812 Point::new(5, 0)..Point::new(5, 0)
9813 ]
9814 );
9815 });
9816}
9817
9818#[gpui::test]
9819async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9820 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9821
9822 let language = Arc::new(
9823 Language::new(
9824 LanguageConfig {
9825 brackets: BracketPairConfig {
9826 pairs: vec![
9827 BracketPair {
9828 start: "{".to_string(),
9829 end: "}".to_string(),
9830 close: false,
9831 surround: false,
9832 newline: true,
9833 },
9834 BracketPair {
9835 start: "(".to_string(),
9836 end: ")".to_string(),
9837 close: false,
9838 surround: false,
9839 newline: true,
9840 },
9841 ],
9842 ..Default::default()
9843 },
9844 ..Default::default()
9845 },
9846 Some(tree_sitter_rust::LANGUAGE.into()),
9847 )
9848 .with_indents_query(
9849 r#"
9850 (_ "(" ")" @end) @indent
9851 (_ "{" "}" @end) @indent
9852 "#,
9853 )
9854 .unwrap(),
9855 );
9856
9857 let text = "fn a() {}";
9858
9859 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9860 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9861 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9862 editor
9863 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9864 .await;
9865
9866 editor.update_in(cx, |editor, window, cx| {
9867 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9868 s.select_ranges([
9869 MultiBufferOffset(5)..MultiBufferOffset(5),
9870 MultiBufferOffset(8)..MultiBufferOffset(8),
9871 MultiBufferOffset(9)..MultiBufferOffset(9),
9872 ])
9873 });
9874 editor.newline(&Newline, window, cx);
9875 assert_eq!(
9876 editor.text(cx),
9877 indoc!(
9878 "
9879 fn a(
9880
9881 ) {
9882
9883 }
9884 "
9885 )
9886 );
9887 assert_eq!(
9888 editor.selections.ranges(&editor.display_snapshot(cx)),
9889 &[
9890 Point::new(1, 0)..Point::new(1, 0),
9891 Point::new(3, 0)..Point::new(3, 0),
9892 Point::new(5, 0)..Point::new(5, 0)
9893 ]
9894 );
9895 });
9896}
9897
9898#[gpui::test]
9899async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9900 init_test(cx, |settings| {
9901 settings.defaults.auto_indent = Some(true);
9902 settings.languages.0.insert(
9903 "python".into(),
9904 LanguageSettingsContent {
9905 auto_indent: Some(false),
9906 ..Default::default()
9907 },
9908 );
9909 });
9910
9911 let mut cx = EditorTestContext::new(cx).await;
9912
9913 let injected_language = Arc::new(
9914 Language::new(
9915 LanguageConfig {
9916 brackets: BracketPairConfig {
9917 pairs: vec![
9918 BracketPair {
9919 start: "{".to_string(),
9920 end: "}".to_string(),
9921 close: false,
9922 surround: false,
9923 newline: true,
9924 },
9925 BracketPair {
9926 start: "(".to_string(),
9927 end: ")".to_string(),
9928 close: true,
9929 surround: false,
9930 newline: true,
9931 },
9932 ],
9933 ..Default::default()
9934 },
9935 name: "python".into(),
9936 ..Default::default()
9937 },
9938 Some(tree_sitter_python::LANGUAGE.into()),
9939 )
9940 .with_indents_query(
9941 r#"
9942 (_ "(" ")" @end) @indent
9943 (_ "{" "}" @end) @indent
9944 "#,
9945 )
9946 .unwrap(),
9947 );
9948
9949 let language = Arc::new(
9950 Language::new(
9951 LanguageConfig {
9952 brackets: BracketPairConfig {
9953 pairs: vec![
9954 BracketPair {
9955 start: "{".to_string(),
9956 end: "}".to_string(),
9957 close: false,
9958 surround: false,
9959 newline: true,
9960 },
9961 BracketPair {
9962 start: "(".to_string(),
9963 end: ")".to_string(),
9964 close: true,
9965 surround: false,
9966 newline: true,
9967 },
9968 ],
9969 ..Default::default()
9970 },
9971 name: LanguageName::new("rust"),
9972 ..Default::default()
9973 },
9974 Some(tree_sitter_rust::LANGUAGE.into()),
9975 )
9976 .with_indents_query(
9977 r#"
9978 (_ "(" ")" @end) @indent
9979 (_ "{" "}" @end) @indent
9980 "#,
9981 )
9982 .unwrap()
9983 .with_injection_query(
9984 r#"
9985 (macro_invocation
9986 macro: (identifier) @_macro_name
9987 (token_tree) @injection.content
9988 (#set! injection.language "python"))
9989 "#,
9990 )
9991 .unwrap(),
9992 );
9993
9994 cx.language_registry().add(injected_language);
9995 cx.language_registry().add(language.clone());
9996
9997 cx.update_buffer(|buffer, cx| {
9998 buffer.set_language(Some(language), cx);
9999 });
10000
10001 cx.set_state(r#"struct A {ˇ}"#);
10002
10003 cx.update_editor(|editor, window, cx| {
10004 editor.newline(&Default::default(), window, cx);
10005 });
10006
10007 cx.assert_editor_state(indoc!(
10008 "struct A {
10009 ˇ
10010 }"
10011 ));
10012
10013 cx.set_state(r#"select_biased!(ˇ)"#);
10014
10015 cx.update_editor(|editor, window, cx| {
10016 editor.newline(&Default::default(), window, cx);
10017 editor.handle_input("def ", window, cx);
10018 editor.handle_input("(", window, cx);
10019 editor.newline(&Default::default(), window, cx);
10020 editor.handle_input("a", window, cx);
10021 });
10022
10023 cx.assert_editor_state(indoc!(
10024 "select_biased!(
10025 def (
10026 aˇ
10027 )
10028 )"
10029 ));
10030}
10031
10032#[gpui::test]
10033async fn test_autoindent_selections(cx: &mut TestAppContext) {
10034 init_test(cx, |_| {});
10035
10036 {
10037 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10038 cx.set_state(indoc! {"
10039 impl A {
10040
10041 fn b() {}
10042
10043 «fn c() {
10044
10045 }ˇ»
10046 }
10047 "});
10048
10049 cx.update_editor(|editor, window, cx| {
10050 editor.autoindent(&Default::default(), window, cx);
10051 });
10052
10053 cx.assert_editor_state(indoc! {"
10054 impl A {
10055
10056 fn b() {}
10057
10058 «fn c() {
10059
10060 }ˇ»
10061 }
10062 "});
10063 }
10064
10065 {
10066 let mut cx = EditorTestContext::new_multibuffer(
10067 cx,
10068 [indoc! { "
10069 impl A {
10070 «
10071 // a
10072 fn b(){}
10073 »
10074 «
10075 }
10076 fn c(){}
10077 »
10078 "}],
10079 );
10080
10081 let buffer = cx.update_editor(|editor, _, cx| {
10082 let buffer = editor.buffer().update(cx, |buffer, _| {
10083 buffer.all_buffers().iter().next().unwrap().clone()
10084 });
10085 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10086 buffer
10087 });
10088
10089 cx.run_until_parked();
10090 cx.update_editor(|editor, window, cx| {
10091 editor.select_all(&Default::default(), window, cx);
10092 editor.autoindent(&Default::default(), window, cx)
10093 });
10094 cx.run_until_parked();
10095
10096 cx.update(|_, cx| {
10097 assert_eq!(
10098 buffer.read(cx).text(),
10099 indoc! { "
10100 impl A {
10101
10102 // a
10103 fn b(){}
10104
10105
10106 }
10107 fn c(){}
10108
10109 " }
10110 )
10111 });
10112 }
10113}
10114
10115#[gpui::test]
10116async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10117 init_test(cx, |_| {});
10118
10119 let mut cx = EditorTestContext::new(cx).await;
10120
10121 let language = Arc::new(Language::new(
10122 LanguageConfig {
10123 brackets: BracketPairConfig {
10124 pairs: vec![
10125 BracketPair {
10126 start: "{".to_string(),
10127 end: "}".to_string(),
10128 close: true,
10129 surround: true,
10130 newline: true,
10131 },
10132 BracketPair {
10133 start: "(".to_string(),
10134 end: ")".to_string(),
10135 close: true,
10136 surround: true,
10137 newline: true,
10138 },
10139 BracketPair {
10140 start: "/*".to_string(),
10141 end: " */".to_string(),
10142 close: true,
10143 surround: true,
10144 newline: true,
10145 },
10146 BracketPair {
10147 start: "[".to_string(),
10148 end: "]".to_string(),
10149 close: false,
10150 surround: false,
10151 newline: true,
10152 },
10153 BracketPair {
10154 start: "\"".to_string(),
10155 end: "\"".to_string(),
10156 close: true,
10157 surround: true,
10158 newline: false,
10159 },
10160 BracketPair {
10161 start: "<".to_string(),
10162 end: ">".to_string(),
10163 close: false,
10164 surround: true,
10165 newline: true,
10166 },
10167 ],
10168 ..Default::default()
10169 },
10170 autoclose_before: "})]".to_string(),
10171 ..Default::default()
10172 },
10173 Some(tree_sitter_rust::LANGUAGE.into()),
10174 ));
10175
10176 cx.language_registry().add(language.clone());
10177 cx.update_buffer(|buffer, cx| {
10178 buffer.set_language(Some(language), cx);
10179 });
10180
10181 cx.set_state(
10182 &r#"
10183 🏀ˇ
10184 εˇ
10185 ❤️ˇ
10186 "#
10187 .unindent(),
10188 );
10189
10190 // autoclose multiple nested brackets at multiple cursors
10191 cx.update_editor(|editor, window, cx| {
10192 editor.handle_input("{", window, cx);
10193 editor.handle_input("{", window, cx);
10194 editor.handle_input("{", window, cx);
10195 });
10196 cx.assert_editor_state(
10197 &"
10198 🏀{{{ˇ}}}
10199 ε{{{ˇ}}}
10200 ❤️{{{ˇ}}}
10201 "
10202 .unindent(),
10203 );
10204
10205 // insert a different closing bracket
10206 cx.update_editor(|editor, window, cx| {
10207 editor.handle_input(")", window, cx);
10208 });
10209 cx.assert_editor_state(
10210 &"
10211 🏀{{{)ˇ}}}
10212 ε{{{)ˇ}}}
10213 ❤️{{{)ˇ}}}
10214 "
10215 .unindent(),
10216 );
10217
10218 // skip over the auto-closed brackets when typing a closing bracket
10219 cx.update_editor(|editor, window, cx| {
10220 editor.move_right(&MoveRight, window, cx);
10221 editor.handle_input("}", window, cx);
10222 editor.handle_input("}", window, cx);
10223 editor.handle_input("}", window, cx);
10224 });
10225 cx.assert_editor_state(
10226 &"
10227 🏀{{{)}}}}ˇ
10228 ε{{{)}}}}ˇ
10229 ❤️{{{)}}}}ˇ
10230 "
10231 .unindent(),
10232 );
10233
10234 // autoclose multi-character pairs
10235 cx.set_state(
10236 &"
10237 ˇ
10238 ˇ
10239 "
10240 .unindent(),
10241 );
10242 cx.update_editor(|editor, window, cx| {
10243 editor.handle_input("/", window, cx);
10244 editor.handle_input("*", window, cx);
10245 });
10246 cx.assert_editor_state(
10247 &"
10248 /*ˇ */
10249 /*ˇ */
10250 "
10251 .unindent(),
10252 );
10253
10254 // one cursor autocloses a multi-character pair, one cursor
10255 // does not autoclose.
10256 cx.set_state(
10257 &"
10258 /ˇ
10259 ˇ
10260 "
10261 .unindent(),
10262 );
10263 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10264 cx.assert_editor_state(
10265 &"
10266 /*ˇ */
10267 *ˇ
10268 "
10269 .unindent(),
10270 );
10271
10272 // Don't autoclose if the next character isn't whitespace and isn't
10273 // listed in the language's "autoclose_before" section.
10274 cx.set_state("ˇa b");
10275 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10276 cx.assert_editor_state("{ˇa b");
10277
10278 // Don't autoclose if `close` is false for the bracket pair
10279 cx.set_state("ˇ");
10280 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10281 cx.assert_editor_state("[ˇ");
10282
10283 // Surround with brackets if text is selected
10284 cx.set_state("«aˇ» b");
10285 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10286 cx.assert_editor_state("{«aˇ»} b");
10287
10288 // Autoclose when not immediately after a word character
10289 cx.set_state("a ˇ");
10290 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10291 cx.assert_editor_state("a \"ˇ\"");
10292
10293 // Autoclose pair where the start and end characters are the same
10294 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10295 cx.assert_editor_state("a \"\"ˇ");
10296
10297 // Don't autoclose when immediately after a word character
10298 cx.set_state("aˇ");
10299 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10300 cx.assert_editor_state("a\"ˇ");
10301
10302 // Do autoclose when after a non-word character
10303 cx.set_state("{ˇ");
10304 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10305 cx.assert_editor_state("{\"ˇ\"");
10306
10307 // Non identical pairs autoclose regardless of preceding character
10308 cx.set_state("aˇ");
10309 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10310 cx.assert_editor_state("a{ˇ}");
10311
10312 // Don't autoclose pair if autoclose is disabled
10313 cx.set_state("ˇ");
10314 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10315 cx.assert_editor_state("<ˇ");
10316
10317 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10318 cx.set_state("«aˇ» b");
10319 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10320 cx.assert_editor_state("<«aˇ»> b");
10321}
10322
10323#[gpui::test]
10324async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10325 init_test(cx, |settings| {
10326 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10327 });
10328
10329 let mut cx = EditorTestContext::new(cx).await;
10330
10331 let language = Arc::new(Language::new(
10332 LanguageConfig {
10333 brackets: BracketPairConfig {
10334 pairs: vec![
10335 BracketPair {
10336 start: "{".to_string(),
10337 end: "}".to_string(),
10338 close: true,
10339 surround: true,
10340 newline: true,
10341 },
10342 BracketPair {
10343 start: "(".to_string(),
10344 end: ")".to_string(),
10345 close: true,
10346 surround: true,
10347 newline: true,
10348 },
10349 BracketPair {
10350 start: "[".to_string(),
10351 end: "]".to_string(),
10352 close: false,
10353 surround: false,
10354 newline: true,
10355 },
10356 ],
10357 ..Default::default()
10358 },
10359 autoclose_before: "})]".to_string(),
10360 ..Default::default()
10361 },
10362 Some(tree_sitter_rust::LANGUAGE.into()),
10363 ));
10364
10365 cx.language_registry().add(language.clone());
10366 cx.update_buffer(|buffer, cx| {
10367 buffer.set_language(Some(language), cx);
10368 });
10369
10370 cx.set_state(
10371 &"
10372 ˇ
10373 ˇ
10374 ˇ
10375 "
10376 .unindent(),
10377 );
10378
10379 // ensure only matching closing brackets are skipped over
10380 cx.update_editor(|editor, window, cx| {
10381 editor.handle_input("}", window, cx);
10382 editor.move_left(&MoveLeft, window, cx);
10383 editor.handle_input(")", window, cx);
10384 editor.move_left(&MoveLeft, window, cx);
10385 });
10386 cx.assert_editor_state(
10387 &"
10388 ˇ)}
10389 ˇ)}
10390 ˇ)}
10391 "
10392 .unindent(),
10393 );
10394
10395 // skip-over closing brackets at multiple cursors
10396 cx.update_editor(|editor, window, cx| {
10397 editor.handle_input(")", window, cx);
10398 editor.handle_input("}", window, cx);
10399 });
10400 cx.assert_editor_state(
10401 &"
10402 )}ˇ
10403 )}ˇ
10404 )}ˇ
10405 "
10406 .unindent(),
10407 );
10408
10409 // ignore non-close brackets
10410 cx.update_editor(|editor, window, cx| {
10411 editor.handle_input("]", window, cx);
10412 editor.move_left(&MoveLeft, window, cx);
10413 editor.handle_input("]", window, cx);
10414 });
10415 cx.assert_editor_state(
10416 &"
10417 )}]ˇ]
10418 )}]ˇ]
10419 )}]ˇ]
10420 "
10421 .unindent(),
10422 );
10423}
10424
10425#[gpui::test]
10426async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10427 init_test(cx, |_| {});
10428
10429 let mut cx = EditorTestContext::new(cx).await;
10430
10431 let html_language = Arc::new(
10432 Language::new(
10433 LanguageConfig {
10434 name: "HTML".into(),
10435 brackets: BracketPairConfig {
10436 pairs: vec![
10437 BracketPair {
10438 start: "<".into(),
10439 end: ">".into(),
10440 close: true,
10441 ..Default::default()
10442 },
10443 BracketPair {
10444 start: "{".into(),
10445 end: "}".into(),
10446 close: true,
10447 ..Default::default()
10448 },
10449 BracketPair {
10450 start: "(".into(),
10451 end: ")".into(),
10452 close: true,
10453 ..Default::default()
10454 },
10455 ],
10456 ..Default::default()
10457 },
10458 autoclose_before: "})]>".into(),
10459 ..Default::default()
10460 },
10461 Some(tree_sitter_html::LANGUAGE.into()),
10462 )
10463 .with_injection_query(
10464 r#"
10465 (script_element
10466 (raw_text) @injection.content
10467 (#set! injection.language "javascript"))
10468 "#,
10469 )
10470 .unwrap(),
10471 );
10472
10473 let javascript_language = Arc::new(Language::new(
10474 LanguageConfig {
10475 name: "JavaScript".into(),
10476 brackets: BracketPairConfig {
10477 pairs: vec![
10478 BracketPair {
10479 start: "/*".into(),
10480 end: " */".into(),
10481 close: true,
10482 ..Default::default()
10483 },
10484 BracketPair {
10485 start: "{".into(),
10486 end: "}".into(),
10487 close: true,
10488 ..Default::default()
10489 },
10490 BracketPair {
10491 start: "(".into(),
10492 end: ")".into(),
10493 close: true,
10494 ..Default::default()
10495 },
10496 ],
10497 ..Default::default()
10498 },
10499 autoclose_before: "})]>".into(),
10500 ..Default::default()
10501 },
10502 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10503 ));
10504
10505 cx.language_registry().add(html_language.clone());
10506 cx.language_registry().add(javascript_language);
10507 cx.executor().run_until_parked();
10508
10509 cx.update_buffer(|buffer, cx| {
10510 buffer.set_language(Some(html_language), cx);
10511 });
10512
10513 cx.set_state(
10514 &r#"
10515 <body>ˇ
10516 <script>
10517 var x = 1;ˇ
10518 </script>
10519 </body>ˇ
10520 "#
10521 .unindent(),
10522 );
10523
10524 // Precondition: different languages are active at different locations.
10525 cx.update_editor(|editor, window, cx| {
10526 let snapshot = editor.snapshot(window, cx);
10527 let cursors = editor
10528 .selections
10529 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10530 let languages = cursors
10531 .iter()
10532 .map(|c| snapshot.language_at(c.start).unwrap().name())
10533 .collect::<Vec<_>>();
10534 assert_eq!(
10535 languages,
10536 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10537 );
10538 });
10539
10540 // Angle brackets autoclose in HTML, but not JavaScript.
10541 cx.update_editor(|editor, window, cx| {
10542 editor.handle_input("<", window, cx);
10543 editor.handle_input("a", window, cx);
10544 });
10545 cx.assert_editor_state(
10546 &r#"
10547 <body><aˇ>
10548 <script>
10549 var x = 1;<aˇ
10550 </script>
10551 </body><aˇ>
10552 "#
10553 .unindent(),
10554 );
10555
10556 // Curly braces and parens autoclose in both HTML and JavaScript.
10557 cx.update_editor(|editor, window, cx| {
10558 editor.handle_input(" b=", window, cx);
10559 editor.handle_input("{", window, cx);
10560 editor.handle_input("c", window, cx);
10561 editor.handle_input("(", window, cx);
10562 });
10563 cx.assert_editor_state(
10564 &r#"
10565 <body><a b={c(ˇ)}>
10566 <script>
10567 var x = 1;<a b={c(ˇ)}
10568 </script>
10569 </body><a b={c(ˇ)}>
10570 "#
10571 .unindent(),
10572 );
10573
10574 // Brackets that were already autoclosed are skipped.
10575 cx.update_editor(|editor, window, cx| {
10576 editor.handle_input(")", window, cx);
10577 editor.handle_input("d", window, cx);
10578 editor.handle_input("}", window, cx);
10579 });
10580 cx.assert_editor_state(
10581 &r#"
10582 <body><a b={c()d}ˇ>
10583 <script>
10584 var x = 1;<a b={c()d}ˇ
10585 </script>
10586 </body><a b={c()d}ˇ>
10587 "#
10588 .unindent(),
10589 );
10590 cx.update_editor(|editor, window, cx| {
10591 editor.handle_input(">", window, cx);
10592 });
10593 cx.assert_editor_state(
10594 &r#"
10595 <body><a b={c()d}>ˇ
10596 <script>
10597 var x = 1;<a b={c()d}>ˇ
10598 </script>
10599 </body><a b={c()d}>ˇ
10600 "#
10601 .unindent(),
10602 );
10603
10604 // Reset
10605 cx.set_state(
10606 &r#"
10607 <body>ˇ
10608 <script>
10609 var x = 1;ˇ
10610 </script>
10611 </body>ˇ
10612 "#
10613 .unindent(),
10614 );
10615
10616 cx.update_editor(|editor, window, cx| {
10617 editor.handle_input("<", window, cx);
10618 });
10619 cx.assert_editor_state(
10620 &r#"
10621 <body><ˇ>
10622 <script>
10623 var x = 1;<ˇ
10624 </script>
10625 </body><ˇ>
10626 "#
10627 .unindent(),
10628 );
10629
10630 // When backspacing, the closing angle brackets are removed.
10631 cx.update_editor(|editor, window, cx| {
10632 editor.backspace(&Backspace, window, cx);
10633 });
10634 cx.assert_editor_state(
10635 &r#"
10636 <body>ˇ
10637 <script>
10638 var x = 1;ˇ
10639 </script>
10640 </body>ˇ
10641 "#
10642 .unindent(),
10643 );
10644
10645 // Block comments autoclose in JavaScript, but not HTML.
10646 cx.update_editor(|editor, window, cx| {
10647 editor.handle_input("/", window, cx);
10648 editor.handle_input("*", window, cx);
10649 });
10650 cx.assert_editor_state(
10651 &r#"
10652 <body>/*ˇ
10653 <script>
10654 var x = 1;/*ˇ */
10655 </script>
10656 </body>/*ˇ
10657 "#
10658 .unindent(),
10659 );
10660}
10661
10662#[gpui::test]
10663async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10664 init_test(cx, |_| {});
10665
10666 let mut cx = EditorTestContext::new(cx).await;
10667
10668 let rust_language = Arc::new(
10669 Language::new(
10670 LanguageConfig {
10671 name: "Rust".into(),
10672 brackets: serde_json::from_value(json!([
10673 { "start": "{", "end": "}", "close": true, "newline": true },
10674 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10675 ]))
10676 .unwrap(),
10677 autoclose_before: "})]>".into(),
10678 ..Default::default()
10679 },
10680 Some(tree_sitter_rust::LANGUAGE.into()),
10681 )
10682 .with_override_query("(string_literal) @string")
10683 .unwrap(),
10684 );
10685
10686 cx.language_registry().add(rust_language.clone());
10687 cx.update_buffer(|buffer, cx| {
10688 buffer.set_language(Some(rust_language), cx);
10689 });
10690
10691 cx.set_state(
10692 &r#"
10693 let x = ˇ
10694 "#
10695 .unindent(),
10696 );
10697
10698 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10699 cx.update_editor(|editor, window, cx| {
10700 editor.handle_input("\"", window, cx);
10701 });
10702 cx.assert_editor_state(
10703 &r#"
10704 let x = "ˇ"
10705 "#
10706 .unindent(),
10707 );
10708
10709 // Inserting another quotation mark. The cursor moves across the existing
10710 // automatically-inserted quotation mark.
10711 cx.update_editor(|editor, window, cx| {
10712 editor.handle_input("\"", window, cx);
10713 });
10714 cx.assert_editor_state(
10715 &r#"
10716 let x = ""ˇ
10717 "#
10718 .unindent(),
10719 );
10720
10721 // Reset
10722 cx.set_state(
10723 &r#"
10724 let x = ˇ
10725 "#
10726 .unindent(),
10727 );
10728
10729 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10730 cx.update_editor(|editor, window, cx| {
10731 editor.handle_input("\"", window, cx);
10732 editor.handle_input(" ", window, cx);
10733 editor.move_left(&Default::default(), window, cx);
10734 editor.handle_input("\\", window, cx);
10735 editor.handle_input("\"", window, cx);
10736 });
10737 cx.assert_editor_state(
10738 &r#"
10739 let x = "\"ˇ "
10740 "#
10741 .unindent(),
10742 );
10743
10744 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10745 // mark. Nothing is inserted.
10746 cx.update_editor(|editor, window, cx| {
10747 editor.move_right(&Default::default(), window, cx);
10748 editor.handle_input("\"", window, cx);
10749 });
10750 cx.assert_editor_state(
10751 &r#"
10752 let x = "\" "ˇ
10753 "#
10754 .unindent(),
10755 );
10756}
10757
10758#[gpui::test]
10759async fn test_surround_with_pair(cx: &mut TestAppContext) {
10760 init_test(cx, |_| {});
10761
10762 let language = Arc::new(Language::new(
10763 LanguageConfig {
10764 brackets: BracketPairConfig {
10765 pairs: vec![
10766 BracketPair {
10767 start: "{".to_string(),
10768 end: "}".to_string(),
10769 close: true,
10770 surround: true,
10771 newline: true,
10772 },
10773 BracketPair {
10774 start: "/* ".to_string(),
10775 end: "*/".to_string(),
10776 close: true,
10777 surround: true,
10778 ..Default::default()
10779 },
10780 ],
10781 ..Default::default()
10782 },
10783 ..Default::default()
10784 },
10785 Some(tree_sitter_rust::LANGUAGE.into()),
10786 ));
10787
10788 let text = r#"
10789 a
10790 b
10791 c
10792 "#
10793 .unindent();
10794
10795 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10796 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10797 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10798 editor
10799 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10800 .await;
10801
10802 editor.update_in(cx, |editor, window, cx| {
10803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10804 s.select_display_ranges([
10805 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10806 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10807 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10808 ])
10809 });
10810
10811 editor.handle_input("{", window, cx);
10812 editor.handle_input("{", window, cx);
10813 editor.handle_input("{", window, cx);
10814 assert_eq!(
10815 editor.text(cx),
10816 "
10817 {{{a}}}
10818 {{{b}}}
10819 {{{c}}}
10820 "
10821 .unindent()
10822 );
10823 assert_eq!(
10824 display_ranges(editor, cx),
10825 [
10826 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10827 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10828 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10829 ]
10830 );
10831
10832 editor.undo(&Undo, window, cx);
10833 editor.undo(&Undo, window, cx);
10834 editor.undo(&Undo, window, cx);
10835 assert_eq!(
10836 editor.text(cx),
10837 "
10838 a
10839 b
10840 c
10841 "
10842 .unindent()
10843 );
10844 assert_eq!(
10845 display_ranges(editor, cx),
10846 [
10847 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10848 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10849 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10850 ]
10851 );
10852
10853 // Ensure inserting the first character of a multi-byte bracket pair
10854 // doesn't surround the selections with the bracket.
10855 editor.handle_input("/", window, cx);
10856 assert_eq!(
10857 editor.text(cx),
10858 "
10859 /
10860 /
10861 /
10862 "
10863 .unindent()
10864 );
10865 assert_eq!(
10866 display_ranges(editor, cx),
10867 [
10868 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10869 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10870 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10871 ]
10872 );
10873
10874 editor.undo(&Undo, window, cx);
10875 assert_eq!(
10876 editor.text(cx),
10877 "
10878 a
10879 b
10880 c
10881 "
10882 .unindent()
10883 );
10884 assert_eq!(
10885 display_ranges(editor, cx),
10886 [
10887 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10888 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10889 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10890 ]
10891 );
10892
10893 // Ensure inserting the last character of a multi-byte bracket pair
10894 // doesn't surround the selections with the bracket.
10895 editor.handle_input("*", window, cx);
10896 assert_eq!(
10897 editor.text(cx),
10898 "
10899 *
10900 *
10901 *
10902 "
10903 .unindent()
10904 );
10905 assert_eq!(
10906 display_ranges(editor, cx),
10907 [
10908 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10909 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10910 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10911 ]
10912 );
10913 });
10914}
10915
10916#[gpui::test]
10917async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10918 init_test(cx, |_| {});
10919
10920 let language = Arc::new(Language::new(
10921 LanguageConfig {
10922 brackets: BracketPairConfig {
10923 pairs: vec![BracketPair {
10924 start: "{".to_string(),
10925 end: "}".to_string(),
10926 close: true,
10927 surround: true,
10928 newline: true,
10929 }],
10930 ..Default::default()
10931 },
10932 autoclose_before: "}".to_string(),
10933 ..Default::default()
10934 },
10935 Some(tree_sitter_rust::LANGUAGE.into()),
10936 ));
10937
10938 let text = r#"
10939 a
10940 b
10941 c
10942 "#
10943 .unindent();
10944
10945 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10946 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10947 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10948 editor
10949 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10950 .await;
10951
10952 editor.update_in(cx, |editor, window, cx| {
10953 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10954 s.select_ranges([
10955 Point::new(0, 1)..Point::new(0, 1),
10956 Point::new(1, 1)..Point::new(1, 1),
10957 Point::new(2, 1)..Point::new(2, 1),
10958 ])
10959 });
10960
10961 editor.handle_input("{", window, cx);
10962 editor.handle_input("{", window, cx);
10963 editor.handle_input("_", window, cx);
10964 assert_eq!(
10965 editor.text(cx),
10966 "
10967 a{{_}}
10968 b{{_}}
10969 c{{_}}
10970 "
10971 .unindent()
10972 );
10973 assert_eq!(
10974 editor
10975 .selections
10976 .ranges::<Point>(&editor.display_snapshot(cx)),
10977 [
10978 Point::new(0, 4)..Point::new(0, 4),
10979 Point::new(1, 4)..Point::new(1, 4),
10980 Point::new(2, 4)..Point::new(2, 4)
10981 ]
10982 );
10983
10984 editor.backspace(&Default::default(), window, cx);
10985 editor.backspace(&Default::default(), window, cx);
10986 assert_eq!(
10987 editor.text(cx),
10988 "
10989 a{}
10990 b{}
10991 c{}
10992 "
10993 .unindent()
10994 );
10995 assert_eq!(
10996 editor
10997 .selections
10998 .ranges::<Point>(&editor.display_snapshot(cx)),
10999 [
11000 Point::new(0, 2)..Point::new(0, 2),
11001 Point::new(1, 2)..Point::new(1, 2),
11002 Point::new(2, 2)..Point::new(2, 2)
11003 ]
11004 );
11005
11006 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11007 assert_eq!(
11008 editor.text(cx),
11009 "
11010 a
11011 b
11012 c
11013 "
11014 .unindent()
11015 );
11016 assert_eq!(
11017 editor
11018 .selections
11019 .ranges::<Point>(&editor.display_snapshot(cx)),
11020 [
11021 Point::new(0, 1)..Point::new(0, 1),
11022 Point::new(1, 1)..Point::new(1, 1),
11023 Point::new(2, 1)..Point::new(2, 1)
11024 ]
11025 );
11026 });
11027}
11028
11029#[gpui::test]
11030async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11031 init_test(cx, |settings| {
11032 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11033 });
11034
11035 let mut cx = EditorTestContext::new(cx).await;
11036
11037 let language = Arc::new(Language::new(
11038 LanguageConfig {
11039 brackets: BracketPairConfig {
11040 pairs: vec![
11041 BracketPair {
11042 start: "{".to_string(),
11043 end: "}".to_string(),
11044 close: true,
11045 surround: true,
11046 newline: true,
11047 },
11048 BracketPair {
11049 start: "(".to_string(),
11050 end: ")".to_string(),
11051 close: true,
11052 surround: true,
11053 newline: true,
11054 },
11055 BracketPair {
11056 start: "[".to_string(),
11057 end: "]".to_string(),
11058 close: false,
11059 surround: true,
11060 newline: true,
11061 },
11062 ],
11063 ..Default::default()
11064 },
11065 autoclose_before: "})]".to_string(),
11066 ..Default::default()
11067 },
11068 Some(tree_sitter_rust::LANGUAGE.into()),
11069 ));
11070
11071 cx.language_registry().add(language.clone());
11072 cx.update_buffer(|buffer, cx| {
11073 buffer.set_language(Some(language), cx);
11074 });
11075
11076 cx.set_state(
11077 &"
11078 {(ˇ)}
11079 [[ˇ]]
11080 {(ˇ)}
11081 "
11082 .unindent(),
11083 );
11084
11085 cx.update_editor(|editor, window, cx| {
11086 editor.backspace(&Default::default(), window, cx);
11087 editor.backspace(&Default::default(), window, cx);
11088 });
11089
11090 cx.assert_editor_state(
11091 &"
11092 ˇ
11093 ˇ]]
11094 ˇ
11095 "
11096 .unindent(),
11097 );
11098
11099 cx.update_editor(|editor, window, cx| {
11100 editor.handle_input("{", window, cx);
11101 editor.handle_input("{", window, cx);
11102 editor.move_right(&MoveRight, window, cx);
11103 editor.move_right(&MoveRight, window, cx);
11104 editor.move_left(&MoveLeft, window, cx);
11105 editor.move_left(&MoveLeft, window, cx);
11106 editor.backspace(&Default::default(), window, cx);
11107 });
11108
11109 cx.assert_editor_state(
11110 &"
11111 {ˇ}
11112 {ˇ}]]
11113 {ˇ}
11114 "
11115 .unindent(),
11116 );
11117
11118 cx.update_editor(|editor, window, cx| {
11119 editor.backspace(&Default::default(), window, cx);
11120 });
11121
11122 cx.assert_editor_state(
11123 &"
11124 ˇ
11125 ˇ]]
11126 ˇ
11127 "
11128 .unindent(),
11129 );
11130}
11131
11132#[gpui::test]
11133async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11134 init_test(cx, |_| {});
11135
11136 let language = Arc::new(Language::new(
11137 LanguageConfig::default(),
11138 Some(tree_sitter_rust::LANGUAGE.into()),
11139 ));
11140
11141 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11142 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11143 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11144 editor
11145 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11146 .await;
11147
11148 editor.update_in(cx, |editor, window, cx| {
11149 editor.set_auto_replace_emoji_shortcode(true);
11150
11151 editor.handle_input("Hello ", window, cx);
11152 editor.handle_input(":wave", window, cx);
11153 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11154
11155 editor.handle_input(":", window, cx);
11156 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11157
11158 editor.handle_input(" :smile", window, cx);
11159 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11160
11161 editor.handle_input(":", window, cx);
11162 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11163
11164 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11165 editor.handle_input(":wave", window, cx);
11166 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11167
11168 editor.handle_input(":", window, cx);
11169 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11170
11171 editor.handle_input(":1", window, cx);
11172 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11173
11174 editor.handle_input(":", window, cx);
11175 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11176
11177 // Ensure shortcode does not get replaced when it is part of a word
11178 editor.handle_input(" Test:wave", window, cx);
11179 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11180
11181 editor.handle_input(":", window, cx);
11182 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11183
11184 editor.set_auto_replace_emoji_shortcode(false);
11185
11186 // Ensure shortcode does not get replaced when auto replace is off
11187 editor.handle_input(" :wave", window, cx);
11188 assert_eq!(
11189 editor.text(cx),
11190 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11191 );
11192
11193 editor.handle_input(":", window, cx);
11194 assert_eq!(
11195 editor.text(cx),
11196 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11197 );
11198 });
11199}
11200
11201#[gpui::test]
11202async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11203 init_test(cx, |_| {});
11204
11205 let (text, insertion_ranges) = marked_text_ranges(
11206 indoc! {"
11207 ˇ
11208 "},
11209 false,
11210 );
11211
11212 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11213 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11214
11215 _ = editor.update_in(cx, |editor, window, cx| {
11216 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11217
11218 editor
11219 .insert_snippet(
11220 &insertion_ranges
11221 .iter()
11222 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11223 .collect::<Vec<_>>(),
11224 snippet,
11225 window,
11226 cx,
11227 )
11228 .unwrap();
11229
11230 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11231 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11232 assert_eq!(editor.text(cx), expected_text);
11233 assert_eq!(
11234 editor.selections.ranges(&editor.display_snapshot(cx)),
11235 selection_ranges
11236 .iter()
11237 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11238 .collect::<Vec<_>>()
11239 );
11240 }
11241
11242 assert(
11243 editor,
11244 cx,
11245 indoc! {"
11246 type «» =•
11247 "},
11248 );
11249
11250 assert!(editor.context_menu_visible(), "There should be a matches");
11251 });
11252}
11253
11254#[gpui::test]
11255async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11256 init_test(cx, |_| {});
11257
11258 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11259 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11260 assert_eq!(editor.text(cx), expected_text);
11261 assert_eq!(
11262 editor.selections.ranges(&editor.display_snapshot(cx)),
11263 selection_ranges
11264 .iter()
11265 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11266 .collect::<Vec<_>>()
11267 );
11268 }
11269
11270 let (text, insertion_ranges) = marked_text_ranges(
11271 indoc! {"
11272 ˇ
11273 "},
11274 false,
11275 );
11276
11277 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11278 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11279
11280 _ = editor.update_in(cx, |editor, window, cx| {
11281 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11282
11283 editor
11284 .insert_snippet(
11285 &insertion_ranges
11286 .iter()
11287 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11288 .collect::<Vec<_>>(),
11289 snippet,
11290 window,
11291 cx,
11292 )
11293 .unwrap();
11294
11295 assert_state(
11296 editor,
11297 cx,
11298 indoc! {"
11299 type «» = ;•
11300 "},
11301 );
11302
11303 assert!(
11304 editor.context_menu_visible(),
11305 "Context menu should be visible for placeholder choices"
11306 );
11307
11308 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11309
11310 assert_state(
11311 editor,
11312 cx,
11313 indoc! {"
11314 type = «»;•
11315 "},
11316 );
11317
11318 assert!(
11319 !editor.context_menu_visible(),
11320 "Context menu should be hidden after moving to next tabstop"
11321 );
11322
11323 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11324
11325 assert_state(
11326 editor,
11327 cx,
11328 indoc! {"
11329 type = ; ˇ
11330 "},
11331 );
11332
11333 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11334
11335 assert_state(
11336 editor,
11337 cx,
11338 indoc! {"
11339 type = ; ˇ
11340 "},
11341 );
11342 });
11343
11344 _ = editor.update_in(cx, |editor, window, cx| {
11345 editor.select_all(&SelectAll, window, cx);
11346 editor.backspace(&Backspace, window, cx);
11347
11348 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11349 let insertion_ranges = editor
11350 .selections
11351 .all(&editor.display_snapshot(cx))
11352 .iter()
11353 .map(|s| s.range())
11354 .collect::<Vec<_>>();
11355
11356 editor
11357 .insert_snippet(&insertion_ranges, snippet, window, cx)
11358 .unwrap();
11359
11360 assert_state(editor, cx, "fn «» = value;•");
11361
11362 assert!(
11363 editor.context_menu_visible(),
11364 "Context menu should be visible for placeholder choices"
11365 );
11366
11367 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11368
11369 assert_state(editor, cx, "fn = «valueˇ»;•");
11370
11371 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11372
11373 assert_state(editor, cx, "fn «» = value;•");
11374
11375 assert!(
11376 editor.context_menu_visible(),
11377 "Context menu should be visible again after returning to first tabstop"
11378 );
11379
11380 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11381
11382 assert_state(editor, cx, "fn «» = value;•");
11383 });
11384}
11385
11386#[gpui::test]
11387async fn test_snippets(cx: &mut TestAppContext) {
11388 init_test(cx, |_| {});
11389
11390 let mut cx = EditorTestContext::new(cx).await;
11391
11392 cx.set_state(indoc! {"
11393 a.ˇ b
11394 a.ˇ b
11395 a.ˇ b
11396 "});
11397
11398 cx.update_editor(|editor, window, cx| {
11399 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11400 let insertion_ranges = editor
11401 .selections
11402 .all(&editor.display_snapshot(cx))
11403 .iter()
11404 .map(|s| s.range())
11405 .collect::<Vec<_>>();
11406 editor
11407 .insert_snippet(&insertion_ranges, snippet, window, cx)
11408 .unwrap();
11409 });
11410
11411 cx.assert_editor_state(indoc! {"
11412 a.f(«oneˇ», two, «threeˇ») b
11413 a.f(«oneˇ», two, «threeˇ») b
11414 a.f(«oneˇ», two, «threeˇ») b
11415 "});
11416
11417 // Can't move earlier than the first tab stop
11418 cx.update_editor(|editor, window, cx| {
11419 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11420 });
11421 cx.assert_editor_state(indoc! {"
11422 a.f(«oneˇ», two, «threeˇ») b
11423 a.f(«oneˇ», two, «threeˇ») b
11424 a.f(«oneˇ», two, «threeˇ») b
11425 "});
11426
11427 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11428 cx.assert_editor_state(indoc! {"
11429 a.f(one, «twoˇ», three) b
11430 a.f(one, «twoˇ», three) b
11431 a.f(one, «twoˇ», three) b
11432 "});
11433
11434 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11435 cx.assert_editor_state(indoc! {"
11436 a.f(«oneˇ», two, «threeˇ») b
11437 a.f(«oneˇ», two, «threeˇ») b
11438 a.f(«oneˇ», two, «threeˇ») b
11439 "});
11440
11441 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11442 cx.assert_editor_state(indoc! {"
11443 a.f(one, «twoˇ», three) b
11444 a.f(one, «twoˇ», three) b
11445 a.f(one, «twoˇ», three) b
11446 "});
11447 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11448 cx.assert_editor_state(indoc! {"
11449 a.f(one, two, three)ˇ b
11450 a.f(one, two, three)ˇ b
11451 a.f(one, two, three)ˇ b
11452 "});
11453
11454 // As soon as the last tab stop is reached, snippet state is gone
11455 cx.update_editor(|editor, window, cx| {
11456 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11457 });
11458 cx.assert_editor_state(indoc! {"
11459 a.f(one, two, three)ˇ b
11460 a.f(one, two, three)ˇ b
11461 a.f(one, two, three)ˇ b
11462 "});
11463}
11464
11465#[gpui::test]
11466async fn test_snippet_indentation(cx: &mut TestAppContext) {
11467 init_test(cx, |_| {});
11468
11469 let mut cx = EditorTestContext::new(cx).await;
11470
11471 cx.update_editor(|editor, window, cx| {
11472 let snippet = Snippet::parse(indoc! {"
11473 /*
11474 * Multiline comment with leading indentation
11475 *
11476 * $1
11477 */
11478 $0"})
11479 .unwrap();
11480 let insertion_ranges = editor
11481 .selections
11482 .all(&editor.display_snapshot(cx))
11483 .iter()
11484 .map(|s| s.range())
11485 .collect::<Vec<_>>();
11486 editor
11487 .insert_snippet(&insertion_ranges, snippet, window, cx)
11488 .unwrap();
11489 });
11490
11491 cx.assert_editor_state(indoc! {"
11492 /*
11493 * Multiline comment with leading indentation
11494 *
11495 * ˇ
11496 */
11497 "});
11498
11499 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11500 cx.assert_editor_state(indoc! {"
11501 /*
11502 * Multiline comment with leading indentation
11503 *
11504 *•
11505 */
11506 ˇ"});
11507}
11508
11509#[gpui::test]
11510async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11511 init_test(cx, |_| {});
11512
11513 let mut cx = EditorTestContext::new(cx).await;
11514 cx.update_editor(|editor, _, cx| {
11515 editor.project().unwrap().update(cx, |project, cx| {
11516 project.snippets().update(cx, |snippets, _cx| {
11517 let snippet = project::snippet_provider::Snippet {
11518 prefix: vec!["multi word".to_string()],
11519 body: "this is many words".to_string(),
11520 description: Some("description".to_string()),
11521 name: "multi-word snippet test".to_string(),
11522 };
11523 snippets.add_snippet_for_test(
11524 None,
11525 PathBuf::from("test_snippets.json"),
11526 vec![Arc::new(snippet)],
11527 );
11528 });
11529 })
11530 });
11531
11532 for (input_to_simulate, should_match_snippet) in [
11533 ("m", true),
11534 ("m ", true),
11535 ("m w", true),
11536 ("aa m w", true),
11537 ("aa m g", false),
11538 ] {
11539 cx.set_state("ˇ");
11540 cx.simulate_input(input_to_simulate); // fails correctly
11541
11542 cx.update_editor(|editor, _, _| {
11543 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11544 else {
11545 assert!(!should_match_snippet); // no completions! don't even show the menu
11546 return;
11547 };
11548 assert!(context_menu.visible());
11549 let completions = context_menu.completions.borrow();
11550
11551 assert_eq!(!completions.is_empty(), should_match_snippet);
11552 });
11553 }
11554}
11555
11556#[gpui::test]
11557async fn test_document_format_during_save(cx: &mut TestAppContext) {
11558 init_test(cx, |_| {});
11559
11560 let fs = FakeFs::new(cx.executor());
11561 fs.insert_file(path!("/file.rs"), Default::default()).await;
11562
11563 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11564
11565 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11566 language_registry.add(rust_lang());
11567 let mut fake_servers = language_registry.register_fake_lsp(
11568 "Rust",
11569 FakeLspAdapter {
11570 capabilities: lsp::ServerCapabilities {
11571 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11572 ..Default::default()
11573 },
11574 ..Default::default()
11575 },
11576 );
11577
11578 let buffer = project
11579 .update(cx, |project, cx| {
11580 project.open_local_buffer(path!("/file.rs"), cx)
11581 })
11582 .await
11583 .unwrap();
11584
11585 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11586 let (editor, cx) = cx.add_window_view(|window, cx| {
11587 build_editor_with_project(project.clone(), buffer, window, cx)
11588 });
11589 editor.update_in(cx, |editor, window, cx| {
11590 editor.set_text("one\ntwo\nthree\n", window, cx)
11591 });
11592 assert!(cx.read(|cx| editor.is_dirty(cx)));
11593
11594 cx.executor().start_waiting();
11595 let fake_server = fake_servers.next().await.unwrap();
11596
11597 {
11598 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11599 move |params, _| async move {
11600 assert_eq!(
11601 params.text_document.uri,
11602 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11603 );
11604 assert_eq!(params.options.tab_size, 4);
11605 Ok(Some(vec![lsp::TextEdit::new(
11606 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11607 ", ".to_string(),
11608 )]))
11609 },
11610 );
11611 let save = editor
11612 .update_in(cx, |editor, window, cx| {
11613 editor.save(
11614 SaveOptions {
11615 format: true,
11616 autosave: false,
11617 },
11618 project.clone(),
11619 window,
11620 cx,
11621 )
11622 })
11623 .unwrap();
11624 cx.executor().start_waiting();
11625 save.await;
11626
11627 assert_eq!(
11628 editor.update(cx, |editor, cx| editor.text(cx)),
11629 "one, two\nthree\n"
11630 );
11631 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11632 }
11633
11634 {
11635 editor.update_in(cx, |editor, window, cx| {
11636 editor.set_text("one\ntwo\nthree\n", window, cx)
11637 });
11638 assert!(cx.read(|cx| editor.is_dirty(cx)));
11639
11640 // Ensure we can still save even if formatting hangs.
11641 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11642 move |params, _| async move {
11643 assert_eq!(
11644 params.text_document.uri,
11645 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11646 );
11647 futures::future::pending::<()>().await;
11648 unreachable!()
11649 },
11650 );
11651 let save = editor
11652 .update_in(cx, |editor, window, cx| {
11653 editor.save(
11654 SaveOptions {
11655 format: true,
11656 autosave: false,
11657 },
11658 project.clone(),
11659 window,
11660 cx,
11661 )
11662 })
11663 .unwrap();
11664 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11665 cx.executor().start_waiting();
11666 save.await;
11667 assert_eq!(
11668 editor.update(cx, |editor, cx| editor.text(cx)),
11669 "one\ntwo\nthree\n"
11670 );
11671 }
11672
11673 // Set rust language override and assert overridden tabsize is sent to language server
11674 update_test_language_settings(cx, |settings| {
11675 settings.languages.0.insert(
11676 "Rust".into(),
11677 LanguageSettingsContent {
11678 tab_size: NonZeroU32::new(8),
11679 ..Default::default()
11680 },
11681 );
11682 });
11683
11684 {
11685 editor.update_in(cx, |editor, window, cx| {
11686 editor.set_text("somehting_new\n", window, cx)
11687 });
11688 assert!(cx.read(|cx| editor.is_dirty(cx)));
11689 let _formatting_request_signal = fake_server
11690 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11691 assert_eq!(
11692 params.text_document.uri,
11693 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11694 );
11695 assert_eq!(params.options.tab_size, 8);
11696 Ok(Some(vec![]))
11697 });
11698 let save = editor
11699 .update_in(cx, |editor, window, cx| {
11700 editor.save(
11701 SaveOptions {
11702 format: true,
11703 autosave: false,
11704 },
11705 project.clone(),
11706 window,
11707 cx,
11708 )
11709 })
11710 .unwrap();
11711 cx.executor().start_waiting();
11712 save.await;
11713 }
11714}
11715
11716#[gpui::test]
11717async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11718 init_test(cx, |settings| {
11719 settings.defaults.ensure_final_newline_on_save = Some(false);
11720 });
11721
11722 let fs = FakeFs::new(cx.executor());
11723 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11724
11725 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11726
11727 let buffer = project
11728 .update(cx, |project, cx| {
11729 project.open_local_buffer(path!("/file.txt"), cx)
11730 })
11731 .await
11732 .unwrap();
11733
11734 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11735 let (editor, cx) = cx.add_window_view(|window, cx| {
11736 build_editor_with_project(project.clone(), buffer, window, cx)
11737 });
11738 editor.update_in(cx, |editor, window, cx| {
11739 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11740 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11741 });
11742 });
11743 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11744
11745 editor.update_in(cx, |editor, window, cx| {
11746 editor.handle_input("\n", window, cx)
11747 });
11748 cx.run_until_parked();
11749 save(&editor, &project, cx).await;
11750 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11751
11752 editor.update_in(cx, |editor, window, cx| {
11753 editor.undo(&Default::default(), window, cx);
11754 });
11755 save(&editor, &project, cx).await;
11756 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11757
11758 editor.update_in(cx, |editor, window, cx| {
11759 editor.redo(&Default::default(), window, cx);
11760 });
11761 cx.run_until_parked();
11762 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11763
11764 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11765 let save = editor
11766 .update_in(cx, |editor, window, cx| {
11767 editor.save(
11768 SaveOptions {
11769 format: true,
11770 autosave: false,
11771 },
11772 project.clone(),
11773 window,
11774 cx,
11775 )
11776 })
11777 .unwrap();
11778 cx.executor().start_waiting();
11779 save.await;
11780 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11781 }
11782}
11783
11784#[gpui::test]
11785async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11786 init_test(cx, |_| {});
11787
11788 let cols = 4;
11789 let rows = 10;
11790 let sample_text_1 = sample_text(rows, cols, 'a');
11791 assert_eq!(
11792 sample_text_1,
11793 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11794 );
11795 let sample_text_2 = sample_text(rows, cols, 'l');
11796 assert_eq!(
11797 sample_text_2,
11798 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11799 );
11800 let sample_text_3 = sample_text(rows, cols, 'v');
11801 assert_eq!(
11802 sample_text_3,
11803 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11804 );
11805
11806 let fs = FakeFs::new(cx.executor());
11807 fs.insert_tree(
11808 path!("/a"),
11809 json!({
11810 "main.rs": sample_text_1,
11811 "other.rs": sample_text_2,
11812 "lib.rs": sample_text_3,
11813 }),
11814 )
11815 .await;
11816
11817 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11818 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11819 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11820
11821 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11822 language_registry.add(rust_lang());
11823 let mut fake_servers = language_registry.register_fake_lsp(
11824 "Rust",
11825 FakeLspAdapter {
11826 capabilities: lsp::ServerCapabilities {
11827 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11828 ..Default::default()
11829 },
11830 ..Default::default()
11831 },
11832 );
11833
11834 let worktree = project.update(cx, |project, cx| {
11835 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11836 assert_eq!(worktrees.len(), 1);
11837 worktrees.pop().unwrap()
11838 });
11839 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11840
11841 let buffer_1 = project
11842 .update(cx, |project, cx| {
11843 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11844 })
11845 .await
11846 .unwrap();
11847 let buffer_2 = project
11848 .update(cx, |project, cx| {
11849 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11850 })
11851 .await
11852 .unwrap();
11853 let buffer_3 = project
11854 .update(cx, |project, cx| {
11855 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11856 })
11857 .await
11858 .unwrap();
11859
11860 let multi_buffer = cx.new(|cx| {
11861 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11862 multi_buffer.push_excerpts(
11863 buffer_1.clone(),
11864 [
11865 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11866 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11867 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11868 ],
11869 cx,
11870 );
11871 multi_buffer.push_excerpts(
11872 buffer_2.clone(),
11873 [
11874 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11875 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11876 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11877 ],
11878 cx,
11879 );
11880 multi_buffer.push_excerpts(
11881 buffer_3.clone(),
11882 [
11883 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11884 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11885 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11886 ],
11887 cx,
11888 );
11889 multi_buffer
11890 });
11891 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11892 Editor::new(
11893 EditorMode::full(),
11894 multi_buffer,
11895 Some(project.clone()),
11896 window,
11897 cx,
11898 )
11899 });
11900
11901 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11902 editor.change_selections(
11903 SelectionEffects::scroll(Autoscroll::Next),
11904 window,
11905 cx,
11906 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
11907 );
11908 editor.insert("|one|two|three|", window, cx);
11909 });
11910 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11911 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11912 editor.change_selections(
11913 SelectionEffects::scroll(Autoscroll::Next),
11914 window,
11915 cx,
11916 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
11917 );
11918 editor.insert("|four|five|six|", window, cx);
11919 });
11920 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11921
11922 // First two buffers should be edited, but not the third one.
11923 assert_eq!(
11924 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11925 "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}",
11926 );
11927 buffer_1.update(cx, |buffer, _| {
11928 assert!(buffer.is_dirty());
11929 assert_eq!(
11930 buffer.text(),
11931 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11932 )
11933 });
11934 buffer_2.update(cx, |buffer, _| {
11935 assert!(buffer.is_dirty());
11936 assert_eq!(
11937 buffer.text(),
11938 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11939 )
11940 });
11941 buffer_3.update(cx, |buffer, _| {
11942 assert!(!buffer.is_dirty());
11943 assert_eq!(buffer.text(), sample_text_3,)
11944 });
11945 cx.executor().run_until_parked();
11946
11947 cx.executor().start_waiting();
11948 let save = multi_buffer_editor
11949 .update_in(cx, |editor, window, cx| {
11950 editor.save(
11951 SaveOptions {
11952 format: true,
11953 autosave: false,
11954 },
11955 project.clone(),
11956 window,
11957 cx,
11958 )
11959 })
11960 .unwrap();
11961
11962 let fake_server = fake_servers.next().await.unwrap();
11963 fake_server
11964 .server
11965 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11966 Ok(Some(vec![lsp::TextEdit::new(
11967 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11968 format!("[{} formatted]", params.text_document.uri),
11969 )]))
11970 })
11971 .detach();
11972 save.await;
11973
11974 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11975 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11976 assert_eq!(
11977 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11978 uri!(
11979 "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}"
11980 ),
11981 );
11982 buffer_1.update(cx, |buffer, _| {
11983 assert!(!buffer.is_dirty());
11984 assert_eq!(
11985 buffer.text(),
11986 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11987 )
11988 });
11989 buffer_2.update(cx, |buffer, _| {
11990 assert!(!buffer.is_dirty());
11991 assert_eq!(
11992 buffer.text(),
11993 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11994 )
11995 });
11996 buffer_3.update(cx, |buffer, _| {
11997 assert!(!buffer.is_dirty());
11998 assert_eq!(buffer.text(), sample_text_3,)
11999 });
12000}
12001
12002#[gpui::test]
12003async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12004 init_test(cx, |_| {});
12005
12006 let fs = FakeFs::new(cx.executor());
12007 fs.insert_tree(
12008 path!("/dir"),
12009 json!({
12010 "file1.rs": "fn main() { println!(\"hello\"); }",
12011 "file2.rs": "fn test() { println!(\"test\"); }",
12012 "file3.rs": "fn other() { println!(\"other\"); }\n",
12013 }),
12014 )
12015 .await;
12016
12017 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12018 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12019 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12020
12021 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12022 language_registry.add(rust_lang());
12023
12024 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12025 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12026
12027 // Open three buffers
12028 let buffer_1 = project
12029 .update(cx, |project, cx| {
12030 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12031 })
12032 .await
12033 .unwrap();
12034 let buffer_2 = project
12035 .update(cx, |project, cx| {
12036 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12037 })
12038 .await
12039 .unwrap();
12040 let buffer_3 = project
12041 .update(cx, |project, cx| {
12042 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12043 })
12044 .await
12045 .unwrap();
12046
12047 // Create a multi-buffer with all three buffers
12048 let multi_buffer = cx.new(|cx| {
12049 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12050 multi_buffer.push_excerpts(
12051 buffer_1.clone(),
12052 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12053 cx,
12054 );
12055 multi_buffer.push_excerpts(
12056 buffer_2.clone(),
12057 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12058 cx,
12059 );
12060 multi_buffer.push_excerpts(
12061 buffer_3.clone(),
12062 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12063 cx,
12064 );
12065 multi_buffer
12066 });
12067
12068 let editor = cx.new_window_entity(|window, cx| {
12069 Editor::new(
12070 EditorMode::full(),
12071 multi_buffer,
12072 Some(project.clone()),
12073 window,
12074 cx,
12075 )
12076 });
12077
12078 // Edit only the first buffer
12079 editor.update_in(cx, |editor, window, cx| {
12080 editor.change_selections(
12081 SelectionEffects::scroll(Autoscroll::Next),
12082 window,
12083 cx,
12084 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12085 );
12086 editor.insert("// edited", window, cx);
12087 });
12088
12089 // Verify that only buffer 1 is dirty
12090 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12091 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12092 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12093
12094 // Get write counts after file creation (files were created with initial content)
12095 // We expect each file to have been written once during creation
12096 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12097 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12098 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12099
12100 // Perform autosave
12101 let save_task = editor.update_in(cx, |editor, window, cx| {
12102 editor.save(
12103 SaveOptions {
12104 format: true,
12105 autosave: true,
12106 },
12107 project.clone(),
12108 window,
12109 cx,
12110 )
12111 });
12112 save_task.await.unwrap();
12113
12114 // Only the dirty buffer should have been saved
12115 assert_eq!(
12116 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12117 1,
12118 "Buffer 1 was dirty, so it should have been written once during autosave"
12119 );
12120 assert_eq!(
12121 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12122 0,
12123 "Buffer 2 was clean, so it should not have been written during autosave"
12124 );
12125 assert_eq!(
12126 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12127 0,
12128 "Buffer 3 was clean, so it should not have been written during autosave"
12129 );
12130
12131 // Verify buffer states after autosave
12132 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12133 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12134 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12135
12136 // Now perform a manual save (format = true)
12137 let save_task = editor.update_in(cx, |editor, window, cx| {
12138 editor.save(
12139 SaveOptions {
12140 format: true,
12141 autosave: false,
12142 },
12143 project.clone(),
12144 window,
12145 cx,
12146 )
12147 });
12148 save_task.await.unwrap();
12149
12150 // During manual save, clean buffers don't get written to disk
12151 // They just get did_save called for language server notifications
12152 assert_eq!(
12153 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12154 1,
12155 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12156 );
12157 assert_eq!(
12158 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12159 0,
12160 "Buffer 2 should not have been written at all"
12161 );
12162 assert_eq!(
12163 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12164 0,
12165 "Buffer 3 should not have been written at all"
12166 );
12167}
12168
12169async fn setup_range_format_test(
12170 cx: &mut TestAppContext,
12171) -> (
12172 Entity<Project>,
12173 Entity<Editor>,
12174 &mut gpui::VisualTestContext,
12175 lsp::FakeLanguageServer,
12176) {
12177 init_test(cx, |_| {});
12178
12179 let fs = FakeFs::new(cx.executor());
12180 fs.insert_file(path!("/file.rs"), Default::default()).await;
12181
12182 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12183
12184 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12185 language_registry.add(rust_lang());
12186 let mut fake_servers = language_registry.register_fake_lsp(
12187 "Rust",
12188 FakeLspAdapter {
12189 capabilities: lsp::ServerCapabilities {
12190 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12191 ..lsp::ServerCapabilities::default()
12192 },
12193 ..FakeLspAdapter::default()
12194 },
12195 );
12196
12197 let buffer = project
12198 .update(cx, |project, cx| {
12199 project.open_local_buffer(path!("/file.rs"), cx)
12200 })
12201 .await
12202 .unwrap();
12203
12204 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12205 let (editor, cx) = cx.add_window_view(|window, cx| {
12206 build_editor_with_project(project.clone(), buffer, window, cx)
12207 });
12208
12209 cx.executor().start_waiting();
12210 let fake_server = fake_servers.next().await.unwrap();
12211
12212 (project, editor, cx, fake_server)
12213}
12214
12215#[gpui::test]
12216async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12217 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12218
12219 editor.update_in(cx, |editor, window, cx| {
12220 editor.set_text("one\ntwo\nthree\n", window, cx)
12221 });
12222 assert!(cx.read(|cx| editor.is_dirty(cx)));
12223
12224 let save = editor
12225 .update_in(cx, |editor, window, cx| {
12226 editor.save(
12227 SaveOptions {
12228 format: true,
12229 autosave: false,
12230 },
12231 project.clone(),
12232 window,
12233 cx,
12234 )
12235 })
12236 .unwrap();
12237 fake_server
12238 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12239 assert_eq!(
12240 params.text_document.uri,
12241 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12242 );
12243 assert_eq!(params.options.tab_size, 4);
12244 Ok(Some(vec![lsp::TextEdit::new(
12245 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12246 ", ".to_string(),
12247 )]))
12248 })
12249 .next()
12250 .await;
12251 cx.executor().start_waiting();
12252 save.await;
12253 assert_eq!(
12254 editor.update(cx, |editor, cx| editor.text(cx)),
12255 "one, two\nthree\n"
12256 );
12257 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12258}
12259
12260#[gpui::test]
12261async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12262 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12263
12264 editor.update_in(cx, |editor, window, cx| {
12265 editor.set_text("one\ntwo\nthree\n", window, cx)
12266 });
12267 assert!(cx.read(|cx| editor.is_dirty(cx)));
12268
12269 // Test that save still works when formatting hangs
12270 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12271 move |params, _| async move {
12272 assert_eq!(
12273 params.text_document.uri,
12274 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12275 );
12276 futures::future::pending::<()>().await;
12277 unreachable!()
12278 },
12279 );
12280 let save = editor
12281 .update_in(cx, |editor, window, cx| {
12282 editor.save(
12283 SaveOptions {
12284 format: true,
12285 autosave: false,
12286 },
12287 project.clone(),
12288 window,
12289 cx,
12290 )
12291 })
12292 .unwrap();
12293 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12294 cx.executor().start_waiting();
12295 save.await;
12296 assert_eq!(
12297 editor.update(cx, |editor, cx| editor.text(cx)),
12298 "one\ntwo\nthree\n"
12299 );
12300 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12301}
12302
12303#[gpui::test]
12304async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12305 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12306
12307 // Buffer starts clean, no formatting should be requested
12308 let save = editor
12309 .update_in(cx, |editor, window, cx| {
12310 editor.save(
12311 SaveOptions {
12312 format: false,
12313 autosave: false,
12314 },
12315 project.clone(),
12316 window,
12317 cx,
12318 )
12319 })
12320 .unwrap();
12321 let _pending_format_request = fake_server
12322 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12323 panic!("Should not be invoked");
12324 })
12325 .next();
12326 cx.executor().start_waiting();
12327 save.await;
12328 cx.run_until_parked();
12329}
12330
12331#[gpui::test]
12332async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12333 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12334
12335 // Set Rust language override and assert overridden tabsize is sent to language server
12336 update_test_language_settings(cx, |settings| {
12337 settings.languages.0.insert(
12338 "Rust".into(),
12339 LanguageSettingsContent {
12340 tab_size: NonZeroU32::new(8),
12341 ..Default::default()
12342 },
12343 );
12344 });
12345
12346 editor.update_in(cx, |editor, window, cx| {
12347 editor.set_text("something_new\n", window, cx)
12348 });
12349 assert!(cx.read(|cx| editor.is_dirty(cx)));
12350 let save = editor
12351 .update_in(cx, |editor, window, cx| {
12352 editor.save(
12353 SaveOptions {
12354 format: true,
12355 autosave: false,
12356 },
12357 project.clone(),
12358 window,
12359 cx,
12360 )
12361 })
12362 .unwrap();
12363 fake_server
12364 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12365 assert_eq!(
12366 params.text_document.uri,
12367 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12368 );
12369 assert_eq!(params.options.tab_size, 8);
12370 Ok(Some(Vec::new()))
12371 })
12372 .next()
12373 .await;
12374 save.await;
12375}
12376
12377#[gpui::test]
12378async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12379 init_test(cx, |settings| {
12380 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12381 settings::LanguageServerFormatterSpecifier::Current,
12382 )))
12383 });
12384
12385 let fs = FakeFs::new(cx.executor());
12386 fs.insert_file(path!("/file.rs"), Default::default()).await;
12387
12388 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12389
12390 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12391 language_registry.add(Arc::new(Language::new(
12392 LanguageConfig {
12393 name: "Rust".into(),
12394 matcher: LanguageMatcher {
12395 path_suffixes: vec!["rs".to_string()],
12396 ..Default::default()
12397 },
12398 ..LanguageConfig::default()
12399 },
12400 Some(tree_sitter_rust::LANGUAGE.into()),
12401 )));
12402 update_test_language_settings(cx, |settings| {
12403 // Enable Prettier formatting for the same buffer, and ensure
12404 // LSP is called instead of Prettier.
12405 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12406 });
12407 let mut fake_servers = language_registry.register_fake_lsp(
12408 "Rust",
12409 FakeLspAdapter {
12410 capabilities: lsp::ServerCapabilities {
12411 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12412 ..Default::default()
12413 },
12414 ..Default::default()
12415 },
12416 );
12417
12418 let buffer = project
12419 .update(cx, |project, cx| {
12420 project.open_local_buffer(path!("/file.rs"), cx)
12421 })
12422 .await
12423 .unwrap();
12424
12425 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12426 let (editor, cx) = cx.add_window_view(|window, cx| {
12427 build_editor_with_project(project.clone(), buffer, window, cx)
12428 });
12429 editor.update_in(cx, |editor, window, cx| {
12430 editor.set_text("one\ntwo\nthree\n", window, cx)
12431 });
12432
12433 cx.executor().start_waiting();
12434 let fake_server = fake_servers.next().await.unwrap();
12435
12436 let format = editor
12437 .update_in(cx, |editor, window, cx| {
12438 editor.perform_format(
12439 project.clone(),
12440 FormatTrigger::Manual,
12441 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12442 window,
12443 cx,
12444 )
12445 })
12446 .unwrap();
12447 fake_server
12448 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12449 assert_eq!(
12450 params.text_document.uri,
12451 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12452 );
12453 assert_eq!(params.options.tab_size, 4);
12454 Ok(Some(vec![lsp::TextEdit::new(
12455 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12456 ", ".to_string(),
12457 )]))
12458 })
12459 .next()
12460 .await;
12461 cx.executor().start_waiting();
12462 format.await;
12463 assert_eq!(
12464 editor.update(cx, |editor, cx| editor.text(cx)),
12465 "one, two\nthree\n"
12466 );
12467
12468 editor.update_in(cx, |editor, window, cx| {
12469 editor.set_text("one\ntwo\nthree\n", window, cx)
12470 });
12471 // Ensure we don't lock if formatting hangs.
12472 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12473 move |params, _| async move {
12474 assert_eq!(
12475 params.text_document.uri,
12476 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12477 );
12478 futures::future::pending::<()>().await;
12479 unreachable!()
12480 },
12481 );
12482 let format = editor
12483 .update_in(cx, |editor, window, cx| {
12484 editor.perform_format(
12485 project,
12486 FormatTrigger::Manual,
12487 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12488 window,
12489 cx,
12490 )
12491 })
12492 .unwrap();
12493 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12494 cx.executor().start_waiting();
12495 format.await;
12496 assert_eq!(
12497 editor.update(cx, |editor, cx| editor.text(cx)),
12498 "one\ntwo\nthree\n"
12499 );
12500}
12501
12502#[gpui::test]
12503async fn test_multiple_formatters(cx: &mut TestAppContext) {
12504 init_test(cx, |settings| {
12505 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12506 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12507 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12508 Formatter::CodeAction("code-action-1".into()),
12509 Formatter::CodeAction("code-action-2".into()),
12510 ]))
12511 });
12512
12513 let fs = FakeFs::new(cx.executor());
12514 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12515 .await;
12516
12517 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12518 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12519 language_registry.add(rust_lang());
12520
12521 let mut fake_servers = language_registry.register_fake_lsp(
12522 "Rust",
12523 FakeLspAdapter {
12524 capabilities: lsp::ServerCapabilities {
12525 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12526 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12527 commands: vec!["the-command-for-code-action-1".into()],
12528 ..Default::default()
12529 }),
12530 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12531 ..Default::default()
12532 },
12533 ..Default::default()
12534 },
12535 );
12536
12537 let buffer = project
12538 .update(cx, |project, cx| {
12539 project.open_local_buffer(path!("/file.rs"), cx)
12540 })
12541 .await
12542 .unwrap();
12543
12544 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12545 let (editor, cx) = cx.add_window_view(|window, cx| {
12546 build_editor_with_project(project.clone(), buffer, window, cx)
12547 });
12548
12549 cx.executor().start_waiting();
12550
12551 let fake_server = fake_servers.next().await.unwrap();
12552 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12553 move |_params, _| async move {
12554 Ok(Some(vec![lsp::TextEdit::new(
12555 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12556 "applied-formatting\n".to_string(),
12557 )]))
12558 },
12559 );
12560 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12561 move |params, _| async move {
12562 let requested_code_actions = params.context.only.expect("Expected code action request");
12563 assert_eq!(requested_code_actions.len(), 1);
12564
12565 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12566 let code_action = match requested_code_actions[0].as_str() {
12567 "code-action-1" => lsp::CodeAction {
12568 kind: Some("code-action-1".into()),
12569 edit: Some(lsp::WorkspaceEdit::new(
12570 [(
12571 uri,
12572 vec![lsp::TextEdit::new(
12573 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12574 "applied-code-action-1-edit\n".to_string(),
12575 )],
12576 )]
12577 .into_iter()
12578 .collect(),
12579 )),
12580 command: Some(lsp::Command {
12581 command: "the-command-for-code-action-1".into(),
12582 ..Default::default()
12583 }),
12584 ..Default::default()
12585 },
12586 "code-action-2" => lsp::CodeAction {
12587 kind: Some("code-action-2".into()),
12588 edit: Some(lsp::WorkspaceEdit::new(
12589 [(
12590 uri,
12591 vec![lsp::TextEdit::new(
12592 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12593 "applied-code-action-2-edit\n".to_string(),
12594 )],
12595 )]
12596 .into_iter()
12597 .collect(),
12598 )),
12599 ..Default::default()
12600 },
12601 req => panic!("Unexpected code action request: {:?}", req),
12602 };
12603 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12604 code_action,
12605 )]))
12606 },
12607 );
12608
12609 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12610 move |params, _| async move { Ok(params) }
12611 });
12612
12613 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12614 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12615 let fake = fake_server.clone();
12616 let lock = command_lock.clone();
12617 move |params, _| {
12618 assert_eq!(params.command, "the-command-for-code-action-1");
12619 let fake = fake.clone();
12620 let lock = lock.clone();
12621 async move {
12622 lock.lock().await;
12623 fake.server
12624 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12625 label: None,
12626 edit: lsp::WorkspaceEdit {
12627 changes: Some(
12628 [(
12629 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12630 vec![lsp::TextEdit {
12631 range: lsp::Range::new(
12632 lsp::Position::new(0, 0),
12633 lsp::Position::new(0, 0),
12634 ),
12635 new_text: "applied-code-action-1-command\n".into(),
12636 }],
12637 )]
12638 .into_iter()
12639 .collect(),
12640 ),
12641 ..Default::default()
12642 },
12643 })
12644 .await
12645 .into_response()
12646 .unwrap();
12647 Ok(Some(json!(null)))
12648 }
12649 }
12650 });
12651
12652 cx.executor().start_waiting();
12653 editor
12654 .update_in(cx, |editor, window, cx| {
12655 editor.perform_format(
12656 project.clone(),
12657 FormatTrigger::Manual,
12658 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12659 window,
12660 cx,
12661 )
12662 })
12663 .unwrap()
12664 .await;
12665 editor.update(cx, |editor, cx| {
12666 assert_eq!(
12667 editor.text(cx),
12668 r#"
12669 applied-code-action-2-edit
12670 applied-code-action-1-command
12671 applied-code-action-1-edit
12672 applied-formatting
12673 one
12674 two
12675 three
12676 "#
12677 .unindent()
12678 );
12679 });
12680
12681 editor.update_in(cx, |editor, window, cx| {
12682 editor.undo(&Default::default(), window, cx);
12683 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12684 });
12685
12686 // Perform a manual edit while waiting for an LSP command
12687 // that's being run as part of a formatting code action.
12688 let lock_guard = command_lock.lock().await;
12689 let format = editor
12690 .update_in(cx, |editor, window, cx| {
12691 editor.perform_format(
12692 project.clone(),
12693 FormatTrigger::Manual,
12694 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12695 window,
12696 cx,
12697 )
12698 })
12699 .unwrap();
12700 cx.run_until_parked();
12701 editor.update(cx, |editor, cx| {
12702 assert_eq!(
12703 editor.text(cx),
12704 r#"
12705 applied-code-action-1-edit
12706 applied-formatting
12707 one
12708 two
12709 three
12710 "#
12711 .unindent()
12712 );
12713
12714 editor.buffer.update(cx, |buffer, cx| {
12715 let ix = buffer.len(cx);
12716 buffer.edit([(ix..ix, "edited\n")], None, cx);
12717 });
12718 });
12719
12720 // Allow the LSP command to proceed. Because the buffer was edited,
12721 // the second code action will not be run.
12722 drop(lock_guard);
12723 format.await;
12724 editor.update_in(cx, |editor, window, cx| {
12725 assert_eq!(
12726 editor.text(cx),
12727 r#"
12728 applied-code-action-1-command
12729 applied-code-action-1-edit
12730 applied-formatting
12731 one
12732 two
12733 three
12734 edited
12735 "#
12736 .unindent()
12737 );
12738
12739 // The manual edit is undone first, because it is the last thing the user did
12740 // (even though the command completed afterwards).
12741 editor.undo(&Default::default(), window, cx);
12742 assert_eq!(
12743 editor.text(cx),
12744 r#"
12745 applied-code-action-1-command
12746 applied-code-action-1-edit
12747 applied-formatting
12748 one
12749 two
12750 three
12751 "#
12752 .unindent()
12753 );
12754
12755 // All the formatting (including the command, which completed after the manual edit)
12756 // is undone together.
12757 editor.undo(&Default::default(), window, cx);
12758 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12759 });
12760}
12761
12762#[gpui::test]
12763async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12764 init_test(cx, |settings| {
12765 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12766 settings::LanguageServerFormatterSpecifier::Current,
12767 )]))
12768 });
12769
12770 let fs = FakeFs::new(cx.executor());
12771 fs.insert_file(path!("/file.ts"), Default::default()).await;
12772
12773 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12774
12775 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12776 language_registry.add(Arc::new(Language::new(
12777 LanguageConfig {
12778 name: "TypeScript".into(),
12779 matcher: LanguageMatcher {
12780 path_suffixes: vec!["ts".to_string()],
12781 ..Default::default()
12782 },
12783 ..LanguageConfig::default()
12784 },
12785 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12786 )));
12787 update_test_language_settings(cx, |settings| {
12788 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12789 });
12790 let mut fake_servers = language_registry.register_fake_lsp(
12791 "TypeScript",
12792 FakeLspAdapter {
12793 capabilities: lsp::ServerCapabilities {
12794 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12795 ..Default::default()
12796 },
12797 ..Default::default()
12798 },
12799 );
12800
12801 let buffer = project
12802 .update(cx, |project, cx| {
12803 project.open_local_buffer(path!("/file.ts"), cx)
12804 })
12805 .await
12806 .unwrap();
12807
12808 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12809 let (editor, cx) = cx.add_window_view(|window, cx| {
12810 build_editor_with_project(project.clone(), buffer, window, cx)
12811 });
12812 editor.update_in(cx, |editor, window, cx| {
12813 editor.set_text(
12814 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12815 window,
12816 cx,
12817 )
12818 });
12819
12820 cx.executor().start_waiting();
12821 let fake_server = fake_servers.next().await.unwrap();
12822
12823 let format = editor
12824 .update_in(cx, |editor, window, cx| {
12825 editor.perform_code_action_kind(
12826 project.clone(),
12827 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12828 window,
12829 cx,
12830 )
12831 })
12832 .unwrap();
12833 fake_server
12834 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12835 assert_eq!(
12836 params.text_document.uri,
12837 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12838 );
12839 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12840 lsp::CodeAction {
12841 title: "Organize Imports".to_string(),
12842 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12843 edit: Some(lsp::WorkspaceEdit {
12844 changes: Some(
12845 [(
12846 params.text_document.uri.clone(),
12847 vec![lsp::TextEdit::new(
12848 lsp::Range::new(
12849 lsp::Position::new(1, 0),
12850 lsp::Position::new(2, 0),
12851 ),
12852 "".to_string(),
12853 )],
12854 )]
12855 .into_iter()
12856 .collect(),
12857 ),
12858 ..Default::default()
12859 }),
12860 ..Default::default()
12861 },
12862 )]))
12863 })
12864 .next()
12865 .await;
12866 cx.executor().start_waiting();
12867 format.await;
12868 assert_eq!(
12869 editor.update(cx, |editor, cx| editor.text(cx)),
12870 "import { a } from 'module';\n\nconst x = a;\n"
12871 );
12872
12873 editor.update_in(cx, |editor, window, cx| {
12874 editor.set_text(
12875 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12876 window,
12877 cx,
12878 )
12879 });
12880 // Ensure we don't lock if code action hangs.
12881 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12882 move |params, _| async move {
12883 assert_eq!(
12884 params.text_document.uri,
12885 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12886 );
12887 futures::future::pending::<()>().await;
12888 unreachable!()
12889 },
12890 );
12891 let format = editor
12892 .update_in(cx, |editor, window, cx| {
12893 editor.perform_code_action_kind(
12894 project,
12895 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12896 window,
12897 cx,
12898 )
12899 })
12900 .unwrap();
12901 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12902 cx.executor().start_waiting();
12903 format.await;
12904 assert_eq!(
12905 editor.update(cx, |editor, cx| editor.text(cx)),
12906 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12907 );
12908}
12909
12910#[gpui::test]
12911async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12912 init_test(cx, |_| {});
12913
12914 let mut cx = EditorLspTestContext::new_rust(
12915 lsp::ServerCapabilities {
12916 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12917 ..Default::default()
12918 },
12919 cx,
12920 )
12921 .await;
12922
12923 cx.set_state(indoc! {"
12924 one.twoˇ
12925 "});
12926
12927 // The format request takes a long time. When it completes, it inserts
12928 // a newline and an indent before the `.`
12929 cx.lsp
12930 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12931 let executor = cx.background_executor().clone();
12932 async move {
12933 executor.timer(Duration::from_millis(100)).await;
12934 Ok(Some(vec![lsp::TextEdit {
12935 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12936 new_text: "\n ".into(),
12937 }]))
12938 }
12939 });
12940
12941 // Submit a format request.
12942 let format_1 = cx
12943 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12944 .unwrap();
12945 cx.executor().run_until_parked();
12946
12947 // Submit a second format request.
12948 let format_2 = cx
12949 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12950 .unwrap();
12951 cx.executor().run_until_parked();
12952
12953 // Wait for both format requests to complete
12954 cx.executor().advance_clock(Duration::from_millis(200));
12955 cx.executor().start_waiting();
12956 format_1.await.unwrap();
12957 cx.executor().start_waiting();
12958 format_2.await.unwrap();
12959
12960 // The formatting edits only happens once.
12961 cx.assert_editor_state(indoc! {"
12962 one
12963 .twoˇ
12964 "});
12965}
12966
12967#[gpui::test]
12968async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12969 init_test(cx, |settings| {
12970 settings.defaults.formatter = Some(FormatterList::default())
12971 });
12972
12973 let mut cx = EditorLspTestContext::new_rust(
12974 lsp::ServerCapabilities {
12975 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12976 ..Default::default()
12977 },
12978 cx,
12979 )
12980 .await;
12981
12982 // Record which buffer changes have been sent to the language server
12983 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12984 cx.lsp
12985 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12986 let buffer_changes = buffer_changes.clone();
12987 move |params, _| {
12988 buffer_changes.lock().extend(
12989 params
12990 .content_changes
12991 .into_iter()
12992 .map(|e| (e.range.unwrap(), e.text)),
12993 );
12994 }
12995 });
12996 // Handle formatting requests to the language server.
12997 cx.lsp
12998 .set_request_handler::<lsp::request::Formatting, _, _>({
12999 let buffer_changes = buffer_changes.clone();
13000 move |_, _| {
13001 let buffer_changes = buffer_changes.clone();
13002 // Insert blank lines between each line of the buffer.
13003 async move {
13004 // When formatting is requested, trailing whitespace has already been stripped,
13005 // and the trailing newline has already been added.
13006 assert_eq!(
13007 &buffer_changes.lock()[1..],
13008 &[
13009 (
13010 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13011 "".into()
13012 ),
13013 (
13014 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13015 "".into()
13016 ),
13017 (
13018 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13019 "\n".into()
13020 ),
13021 ]
13022 );
13023
13024 Ok(Some(vec![
13025 lsp::TextEdit {
13026 range: lsp::Range::new(
13027 lsp::Position::new(1, 0),
13028 lsp::Position::new(1, 0),
13029 ),
13030 new_text: "\n".into(),
13031 },
13032 lsp::TextEdit {
13033 range: lsp::Range::new(
13034 lsp::Position::new(2, 0),
13035 lsp::Position::new(2, 0),
13036 ),
13037 new_text: "\n".into(),
13038 },
13039 ]))
13040 }
13041 }
13042 });
13043
13044 // Set up a buffer white some trailing whitespace and no trailing newline.
13045 cx.set_state(
13046 &[
13047 "one ", //
13048 "twoˇ", //
13049 "three ", //
13050 "four", //
13051 ]
13052 .join("\n"),
13053 );
13054 cx.run_until_parked();
13055
13056 // Submit a format request.
13057 let format = cx
13058 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13059 .unwrap();
13060
13061 cx.run_until_parked();
13062 // After formatting the buffer, the trailing whitespace is stripped,
13063 // a newline is appended, and the edits provided by the language server
13064 // have been applied.
13065 format.await.unwrap();
13066
13067 cx.assert_editor_state(
13068 &[
13069 "one", //
13070 "", //
13071 "twoˇ", //
13072 "", //
13073 "three", //
13074 "four", //
13075 "", //
13076 ]
13077 .join("\n"),
13078 );
13079
13080 // Undoing the formatting undoes the trailing whitespace removal, the
13081 // trailing newline, and the LSP edits.
13082 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13083 cx.assert_editor_state(
13084 &[
13085 "one ", //
13086 "twoˇ", //
13087 "three ", //
13088 "four", //
13089 ]
13090 .join("\n"),
13091 );
13092}
13093
13094#[gpui::test]
13095async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13096 cx: &mut TestAppContext,
13097) {
13098 init_test(cx, |_| {});
13099
13100 cx.update(|cx| {
13101 cx.update_global::<SettingsStore, _>(|settings, cx| {
13102 settings.update_user_settings(cx, |settings| {
13103 settings.editor.auto_signature_help = Some(true);
13104 });
13105 });
13106 });
13107
13108 let mut cx = EditorLspTestContext::new_rust(
13109 lsp::ServerCapabilities {
13110 signature_help_provider: Some(lsp::SignatureHelpOptions {
13111 ..Default::default()
13112 }),
13113 ..Default::default()
13114 },
13115 cx,
13116 )
13117 .await;
13118
13119 let language = Language::new(
13120 LanguageConfig {
13121 name: "Rust".into(),
13122 brackets: BracketPairConfig {
13123 pairs: vec![
13124 BracketPair {
13125 start: "{".to_string(),
13126 end: "}".to_string(),
13127 close: true,
13128 surround: true,
13129 newline: true,
13130 },
13131 BracketPair {
13132 start: "(".to_string(),
13133 end: ")".to_string(),
13134 close: true,
13135 surround: true,
13136 newline: true,
13137 },
13138 BracketPair {
13139 start: "/*".to_string(),
13140 end: " */".to_string(),
13141 close: true,
13142 surround: true,
13143 newline: true,
13144 },
13145 BracketPair {
13146 start: "[".to_string(),
13147 end: "]".to_string(),
13148 close: false,
13149 surround: false,
13150 newline: true,
13151 },
13152 BracketPair {
13153 start: "\"".to_string(),
13154 end: "\"".to_string(),
13155 close: true,
13156 surround: true,
13157 newline: false,
13158 },
13159 BracketPair {
13160 start: "<".to_string(),
13161 end: ">".to_string(),
13162 close: false,
13163 surround: true,
13164 newline: true,
13165 },
13166 ],
13167 ..Default::default()
13168 },
13169 autoclose_before: "})]".to_string(),
13170 ..Default::default()
13171 },
13172 Some(tree_sitter_rust::LANGUAGE.into()),
13173 );
13174 let language = Arc::new(language);
13175
13176 cx.language_registry().add(language.clone());
13177 cx.update_buffer(|buffer, cx| {
13178 buffer.set_language(Some(language), cx);
13179 });
13180
13181 cx.set_state(
13182 &r#"
13183 fn main() {
13184 sampleˇ
13185 }
13186 "#
13187 .unindent(),
13188 );
13189
13190 cx.update_editor(|editor, window, cx| {
13191 editor.handle_input("(", window, cx);
13192 });
13193 cx.assert_editor_state(
13194 &"
13195 fn main() {
13196 sample(ˇ)
13197 }
13198 "
13199 .unindent(),
13200 );
13201
13202 let mocked_response = lsp::SignatureHelp {
13203 signatures: vec![lsp::SignatureInformation {
13204 label: "fn sample(param1: u8, param2: u8)".to_string(),
13205 documentation: None,
13206 parameters: Some(vec![
13207 lsp::ParameterInformation {
13208 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13209 documentation: None,
13210 },
13211 lsp::ParameterInformation {
13212 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13213 documentation: None,
13214 },
13215 ]),
13216 active_parameter: None,
13217 }],
13218 active_signature: Some(0),
13219 active_parameter: Some(0),
13220 };
13221 handle_signature_help_request(&mut cx, mocked_response).await;
13222
13223 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13224 .await;
13225
13226 cx.editor(|editor, _, _| {
13227 let signature_help_state = editor.signature_help_state.popover().cloned();
13228 let signature = signature_help_state.unwrap();
13229 assert_eq!(
13230 signature.signatures[signature.current_signature].label,
13231 "fn sample(param1: u8, param2: u8)"
13232 );
13233 });
13234}
13235
13236#[gpui::test]
13237async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13238 init_test(cx, |_| {});
13239
13240 cx.update(|cx| {
13241 cx.update_global::<SettingsStore, _>(|settings, cx| {
13242 settings.update_user_settings(cx, |settings| {
13243 settings.editor.auto_signature_help = Some(false);
13244 settings.editor.show_signature_help_after_edits = Some(false);
13245 });
13246 });
13247 });
13248
13249 let mut cx = EditorLspTestContext::new_rust(
13250 lsp::ServerCapabilities {
13251 signature_help_provider: Some(lsp::SignatureHelpOptions {
13252 ..Default::default()
13253 }),
13254 ..Default::default()
13255 },
13256 cx,
13257 )
13258 .await;
13259
13260 let language = Language::new(
13261 LanguageConfig {
13262 name: "Rust".into(),
13263 brackets: BracketPairConfig {
13264 pairs: vec![
13265 BracketPair {
13266 start: "{".to_string(),
13267 end: "}".to_string(),
13268 close: true,
13269 surround: true,
13270 newline: true,
13271 },
13272 BracketPair {
13273 start: "(".to_string(),
13274 end: ")".to_string(),
13275 close: true,
13276 surround: true,
13277 newline: true,
13278 },
13279 BracketPair {
13280 start: "/*".to_string(),
13281 end: " */".to_string(),
13282 close: true,
13283 surround: true,
13284 newline: true,
13285 },
13286 BracketPair {
13287 start: "[".to_string(),
13288 end: "]".to_string(),
13289 close: false,
13290 surround: false,
13291 newline: true,
13292 },
13293 BracketPair {
13294 start: "\"".to_string(),
13295 end: "\"".to_string(),
13296 close: true,
13297 surround: true,
13298 newline: false,
13299 },
13300 BracketPair {
13301 start: "<".to_string(),
13302 end: ">".to_string(),
13303 close: false,
13304 surround: true,
13305 newline: true,
13306 },
13307 ],
13308 ..Default::default()
13309 },
13310 autoclose_before: "})]".to_string(),
13311 ..Default::default()
13312 },
13313 Some(tree_sitter_rust::LANGUAGE.into()),
13314 );
13315 let language = Arc::new(language);
13316
13317 cx.language_registry().add(language.clone());
13318 cx.update_buffer(|buffer, cx| {
13319 buffer.set_language(Some(language), cx);
13320 });
13321
13322 // Ensure that signature_help is not called when no signature help is enabled.
13323 cx.set_state(
13324 &r#"
13325 fn main() {
13326 sampleˇ
13327 }
13328 "#
13329 .unindent(),
13330 );
13331 cx.update_editor(|editor, window, cx| {
13332 editor.handle_input("(", window, cx);
13333 });
13334 cx.assert_editor_state(
13335 &"
13336 fn main() {
13337 sample(ˇ)
13338 }
13339 "
13340 .unindent(),
13341 );
13342 cx.editor(|editor, _, _| {
13343 assert!(editor.signature_help_state.task().is_none());
13344 });
13345
13346 let mocked_response = lsp::SignatureHelp {
13347 signatures: vec![lsp::SignatureInformation {
13348 label: "fn sample(param1: u8, param2: u8)".to_string(),
13349 documentation: None,
13350 parameters: Some(vec![
13351 lsp::ParameterInformation {
13352 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13353 documentation: None,
13354 },
13355 lsp::ParameterInformation {
13356 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13357 documentation: None,
13358 },
13359 ]),
13360 active_parameter: None,
13361 }],
13362 active_signature: Some(0),
13363 active_parameter: Some(0),
13364 };
13365
13366 // Ensure that signature_help is called when enabled afte edits
13367 cx.update(|_, cx| {
13368 cx.update_global::<SettingsStore, _>(|settings, cx| {
13369 settings.update_user_settings(cx, |settings| {
13370 settings.editor.auto_signature_help = Some(false);
13371 settings.editor.show_signature_help_after_edits = Some(true);
13372 });
13373 });
13374 });
13375 cx.set_state(
13376 &r#"
13377 fn main() {
13378 sampleˇ
13379 }
13380 "#
13381 .unindent(),
13382 );
13383 cx.update_editor(|editor, window, cx| {
13384 editor.handle_input("(", window, cx);
13385 });
13386 cx.assert_editor_state(
13387 &"
13388 fn main() {
13389 sample(ˇ)
13390 }
13391 "
13392 .unindent(),
13393 );
13394 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13395 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13396 .await;
13397 cx.update_editor(|editor, _, _| {
13398 let signature_help_state = editor.signature_help_state.popover().cloned();
13399 assert!(signature_help_state.is_some());
13400 let signature = signature_help_state.unwrap();
13401 assert_eq!(
13402 signature.signatures[signature.current_signature].label,
13403 "fn sample(param1: u8, param2: u8)"
13404 );
13405 editor.signature_help_state = SignatureHelpState::default();
13406 });
13407
13408 // Ensure that signature_help is called when auto signature help override is enabled
13409 cx.update(|_, cx| {
13410 cx.update_global::<SettingsStore, _>(|settings, cx| {
13411 settings.update_user_settings(cx, |settings| {
13412 settings.editor.auto_signature_help = Some(true);
13413 settings.editor.show_signature_help_after_edits = Some(false);
13414 });
13415 });
13416 });
13417 cx.set_state(
13418 &r#"
13419 fn main() {
13420 sampleˇ
13421 }
13422 "#
13423 .unindent(),
13424 );
13425 cx.update_editor(|editor, window, cx| {
13426 editor.handle_input("(", window, cx);
13427 });
13428 cx.assert_editor_state(
13429 &"
13430 fn main() {
13431 sample(ˇ)
13432 }
13433 "
13434 .unindent(),
13435 );
13436 handle_signature_help_request(&mut cx, mocked_response).await;
13437 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13438 .await;
13439 cx.editor(|editor, _, _| {
13440 let signature_help_state = editor.signature_help_state.popover().cloned();
13441 assert!(signature_help_state.is_some());
13442 let signature = signature_help_state.unwrap();
13443 assert_eq!(
13444 signature.signatures[signature.current_signature].label,
13445 "fn sample(param1: u8, param2: u8)"
13446 );
13447 });
13448}
13449
13450#[gpui::test]
13451async fn test_signature_help(cx: &mut TestAppContext) {
13452 init_test(cx, |_| {});
13453 cx.update(|cx| {
13454 cx.update_global::<SettingsStore, _>(|settings, cx| {
13455 settings.update_user_settings(cx, |settings| {
13456 settings.editor.auto_signature_help = Some(true);
13457 });
13458 });
13459 });
13460
13461 let mut cx = EditorLspTestContext::new_rust(
13462 lsp::ServerCapabilities {
13463 signature_help_provider: Some(lsp::SignatureHelpOptions {
13464 ..Default::default()
13465 }),
13466 ..Default::default()
13467 },
13468 cx,
13469 )
13470 .await;
13471
13472 // A test that directly calls `show_signature_help`
13473 cx.update_editor(|editor, window, cx| {
13474 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13475 });
13476
13477 let mocked_response = lsp::SignatureHelp {
13478 signatures: vec![lsp::SignatureInformation {
13479 label: "fn sample(param1: u8, param2: u8)".to_string(),
13480 documentation: None,
13481 parameters: Some(vec![
13482 lsp::ParameterInformation {
13483 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13484 documentation: None,
13485 },
13486 lsp::ParameterInformation {
13487 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13488 documentation: None,
13489 },
13490 ]),
13491 active_parameter: None,
13492 }],
13493 active_signature: Some(0),
13494 active_parameter: Some(0),
13495 };
13496 handle_signature_help_request(&mut cx, mocked_response).await;
13497
13498 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13499 .await;
13500
13501 cx.editor(|editor, _, _| {
13502 let signature_help_state = editor.signature_help_state.popover().cloned();
13503 assert!(signature_help_state.is_some());
13504 let signature = signature_help_state.unwrap();
13505 assert_eq!(
13506 signature.signatures[signature.current_signature].label,
13507 "fn sample(param1: u8, param2: u8)"
13508 );
13509 });
13510
13511 // When exiting outside from inside the brackets, `signature_help` is closed.
13512 cx.set_state(indoc! {"
13513 fn main() {
13514 sample(ˇ);
13515 }
13516
13517 fn sample(param1: u8, param2: u8) {}
13518 "});
13519
13520 cx.update_editor(|editor, window, cx| {
13521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13522 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13523 });
13524 });
13525
13526 let mocked_response = lsp::SignatureHelp {
13527 signatures: Vec::new(),
13528 active_signature: None,
13529 active_parameter: None,
13530 };
13531 handle_signature_help_request(&mut cx, mocked_response).await;
13532
13533 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13534 .await;
13535
13536 cx.editor(|editor, _, _| {
13537 assert!(!editor.signature_help_state.is_shown());
13538 });
13539
13540 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13541 cx.set_state(indoc! {"
13542 fn main() {
13543 sample(ˇ);
13544 }
13545
13546 fn sample(param1: u8, param2: u8) {}
13547 "});
13548
13549 let mocked_response = lsp::SignatureHelp {
13550 signatures: vec![lsp::SignatureInformation {
13551 label: "fn sample(param1: u8, param2: u8)".to_string(),
13552 documentation: None,
13553 parameters: Some(vec![
13554 lsp::ParameterInformation {
13555 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13556 documentation: None,
13557 },
13558 lsp::ParameterInformation {
13559 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13560 documentation: None,
13561 },
13562 ]),
13563 active_parameter: None,
13564 }],
13565 active_signature: Some(0),
13566 active_parameter: Some(0),
13567 };
13568 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13569 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13570 .await;
13571 cx.editor(|editor, _, _| {
13572 assert!(editor.signature_help_state.is_shown());
13573 });
13574
13575 // Restore the popover with more parameter input
13576 cx.set_state(indoc! {"
13577 fn main() {
13578 sample(param1, param2ˇ);
13579 }
13580
13581 fn sample(param1: u8, param2: u8) {}
13582 "});
13583
13584 let mocked_response = lsp::SignatureHelp {
13585 signatures: vec![lsp::SignatureInformation {
13586 label: "fn sample(param1: u8, param2: u8)".to_string(),
13587 documentation: None,
13588 parameters: Some(vec![
13589 lsp::ParameterInformation {
13590 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13591 documentation: None,
13592 },
13593 lsp::ParameterInformation {
13594 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13595 documentation: None,
13596 },
13597 ]),
13598 active_parameter: None,
13599 }],
13600 active_signature: Some(0),
13601 active_parameter: Some(1),
13602 };
13603 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13604 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13605 .await;
13606
13607 // When selecting a range, the popover is gone.
13608 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13609 cx.update_editor(|editor, window, cx| {
13610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13611 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13612 })
13613 });
13614 cx.assert_editor_state(indoc! {"
13615 fn main() {
13616 sample(param1, «ˇparam2»);
13617 }
13618
13619 fn sample(param1: u8, param2: u8) {}
13620 "});
13621 cx.editor(|editor, _, _| {
13622 assert!(!editor.signature_help_state.is_shown());
13623 });
13624
13625 // When unselecting again, the popover is back if within the brackets.
13626 cx.update_editor(|editor, window, cx| {
13627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13628 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13629 })
13630 });
13631 cx.assert_editor_state(indoc! {"
13632 fn main() {
13633 sample(param1, ˇparam2);
13634 }
13635
13636 fn sample(param1: u8, param2: u8) {}
13637 "});
13638 handle_signature_help_request(&mut cx, mocked_response).await;
13639 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13640 .await;
13641 cx.editor(|editor, _, _| {
13642 assert!(editor.signature_help_state.is_shown());
13643 });
13644
13645 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13646 cx.update_editor(|editor, window, cx| {
13647 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13648 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13649 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13650 })
13651 });
13652 cx.assert_editor_state(indoc! {"
13653 fn main() {
13654 sample(param1, ˇparam2);
13655 }
13656
13657 fn sample(param1: u8, param2: u8) {}
13658 "});
13659
13660 let mocked_response = lsp::SignatureHelp {
13661 signatures: vec![lsp::SignatureInformation {
13662 label: "fn sample(param1: u8, param2: u8)".to_string(),
13663 documentation: None,
13664 parameters: Some(vec![
13665 lsp::ParameterInformation {
13666 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13667 documentation: None,
13668 },
13669 lsp::ParameterInformation {
13670 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13671 documentation: None,
13672 },
13673 ]),
13674 active_parameter: None,
13675 }],
13676 active_signature: Some(0),
13677 active_parameter: Some(1),
13678 };
13679 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13680 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13681 .await;
13682 cx.update_editor(|editor, _, cx| {
13683 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13684 });
13685 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13686 .await;
13687 cx.update_editor(|editor, window, cx| {
13688 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13689 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13690 })
13691 });
13692 cx.assert_editor_state(indoc! {"
13693 fn main() {
13694 sample(param1, «ˇparam2»);
13695 }
13696
13697 fn sample(param1: u8, param2: u8) {}
13698 "});
13699 cx.update_editor(|editor, window, cx| {
13700 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13701 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13702 })
13703 });
13704 cx.assert_editor_state(indoc! {"
13705 fn main() {
13706 sample(param1, ˇparam2);
13707 }
13708
13709 fn sample(param1: u8, param2: u8) {}
13710 "});
13711 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13712 .await;
13713}
13714
13715#[gpui::test]
13716async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13717 init_test(cx, |_| {});
13718
13719 let mut cx = EditorLspTestContext::new_rust(
13720 lsp::ServerCapabilities {
13721 signature_help_provider: Some(lsp::SignatureHelpOptions {
13722 ..Default::default()
13723 }),
13724 ..Default::default()
13725 },
13726 cx,
13727 )
13728 .await;
13729
13730 cx.set_state(indoc! {"
13731 fn main() {
13732 overloadedˇ
13733 }
13734 "});
13735
13736 cx.update_editor(|editor, window, cx| {
13737 editor.handle_input("(", window, cx);
13738 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13739 });
13740
13741 // Mock response with 3 signatures
13742 let mocked_response = lsp::SignatureHelp {
13743 signatures: vec![
13744 lsp::SignatureInformation {
13745 label: "fn overloaded(x: i32)".to_string(),
13746 documentation: None,
13747 parameters: Some(vec![lsp::ParameterInformation {
13748 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13749 documentation: None,
13750 }]),
13751 active_parameter: None,
13752 },
13753 lsp::SignatureInformation {
13754 label: "fn overloaded(x: i32, y: i32)".to_string(),
13755 documentation: None,
13756 parameters: Some(vec![
13757 lsp::ParameterInformation {
13758 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13759 documentation: None,
13760 },
13761 lsp::ParameterInformation {
13762 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13763 documentation: None,
13764 },
13765 ]),
13766 active_parameter: None,
13767 },
13768 lsp::SignatureInformation {
13769 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13770 documentation: None,
13771 parameters: Some(vec![
13772 lsp::ParameterInformation {
13773 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13774 documentation: None,
13775 },
13776 lsp::ParameterInformation {
13777 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13778 documentation: None,
13779 },
13780 lsp::ParameterInformation {
13781 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13782 documentation: None,
13783 },
13784 ]),
13785 active_parameter: None,
13786 },
13787 ],
13788 active_signature: Some(1),
13789 active_parameter: Some(0),
13790 };
13791 handle_signature_help_request(&mut cx, mocked_response).await;
13792
13793 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13794 .await;
13795
13796 // Verify we have multiple signatures and the right one is selected
13797 cx.editor(|editor, _, _| {
13798 let popover = editor.signature_help_state.popover().cloned().unwrap();
13799 assert_eq!(popover.signatures.len(), 3);
13800 // active_signature was 1, so that should be the current
13801 assert_eq!(popover.current_signature, 1);
13802 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13803 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13804 assert_eq!(
13805 popover.signatures[2].label,
13806 "fn overloaded(x: i32, y: i32, z: i32)"
13807 );
13808 });
13809
13810 // Test navigation functionality
13811 cx.update_editor(|editor, window, cx| {
13812 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13813 });
13814
13815 cx.editor(|editor, _, _| {
13816 let popover = editor.signature_help_state.popover().cloned().unwrap();
13817 assert_eq!(popover.current_signature, 2);
13818 });
13819
13820 // Test wrap around
13821 cx.update_editor(|editor, window, cx| {
13822 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13823 });
13824
13825 cx.editor(|editor, _, _| {
13826 let popover = editor.signature_help_state.popover().cloned().unwrap();
13827 assert_eq!(popover.current_signature, 0);
13828 });
13829
13830 // Test previous navigation
13831 cx.update_editor(|editor, window, cx| {
13832 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13833 });
13834
13835 cx.editor(|editor, _, _| {
13836 let popover = editor.signature_help_state.popover().cloned().unwrap();
13837 assert_eq!(popover.current_signature, 2);
13838 });
13839}
13840
13841#[gpui::test]
13842async fn test_completion_mode(cx: &mut TestAppContext) {
13843 init_test(cx, |_| {});
13844 let mut cx = EditorLspTestContext::new_rust(
13845 lsp::ServerCapabilities {
13846 completion_provider: Some(lsp::CompletionOptions {
13847 resolve_provider: Some(true),
13848 ..Default::default()
13849 }),
13850 ..Default::default()
13851 },
13852 cx,
13853 )
13854 .await;
13855
13856 struct Run {
13857 run_description: &'static str,
13858 initial_state: String,
13859 buffer_marked_text: String,
13860 completion_label: &'static str,
13861 completion_text: &'static str,
13862 expected_with_insert_mode: String,
13863 expected_with_replace_mode: String,
13864 expected_with_replace_subsequence_mode: String,
13865 expected_with_replace_suffix_mode: String,
13866 }
13867
13868 let runs = [
13869 Run {
13870 run_description: "Start of word matches completion text",
13871 initial_state: "before ediˇ after".into(),
13872 buffer_marked_text: "before <edi|> after".into(),
13873 completion_label: "editor",
13874 completion_text: "editor",
13875 expected_with_insert_mode: "before editorˇ after".into(),
13876 expected_with_replace_mode: "before editorˇ after".into(),
13877 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13878 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13879 },
13880 Run {
13881 run_description: "Accept same text at the middle of the word",
13882 initial_state: "before ediˇtor after".into(),
13883 buffer_marked_text: "before <edi|tor> after".into(),
13884 completion_label: "editor",
13885 completion_text: "editor",
13886 expected_with_insert_mode: "before editorˇtor after".into(),
13887 expected_with_replace_mode: "before editorˇ after".into(),
13888 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13889 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13890 },
13891 Run {
13892 run_description: "End of word matches completion text -- cursor at end",
13893 initial_state: "before torˇ after".into(),
13894 buffer_marked_text: "before <tor|> after".into(),
13895 completion_label: "editor",
13896 completion_text: "editor",
13897 expected_with_insert_mode: "before editorˇ after".into(),
13898 expected_with_replace_mode: "before editorˇ after".into(),
13899 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13900 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13901 },
13902 Run {
13903 run_description: "End of word matches completion text -- cursor at start",
13904 initial_state: "before ˇtor after".into(),
13905 buffer_marked_text: "before <|tor> after".into(),
13906 completion_label: "editor",
13907 completion_text: "editor",
13908 expected_with_insert_mode: "before editorˇtor after".into(),
13909 expected_with_replace_mode: "before editorˇ after".into(),
13910 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13911 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13912 },
13913 Run {
13914 run_description: "Prepend text containing whitespace",
13915 initial_state: "pˇfield: bool".into(),
13916 buffer_marked_text: "<p|field>: bool".into(),
13917 completion_label: "pub ",
13918 completion_text: "pub ",
13919 expected_with_insert_mode: "pub ˇfield: bool".into(),
13920 expected_with_replace_mode: "pub ˇ: bool".into(),
13921 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13922 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13923 },
13924 Run {
13925 run_description: "Add element to start of list",
13926 initial_state: "[element_ˇelement_2]".into(),
13927 buffer_marked_text: "[<element_|element_2>]".into(),
13928 completion_label: "element_1",
13929 completion_text: "element_1",
13930 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13931 expected_with_replace_mode: "[element_1ˇ]".into(),
13932 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13933 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13934 },
13935 Run {
13936 run_description: "Add element to start of list -- first and second elements are equal",
13937 initial_state: "[elˇelement]".into(),
13938 buffer_marked_text: "[<el|element>]".into(),
13939 completion_label: "element",
13940 completion_text: "element",
13941 expected_with_insert_mode: "[elementˇelement]".into(),
13942 expected_with_replace_mode: "[elementˇ]".into(),
13943 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13944 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13945 },
13946 Run {
13947 run_description: "Ends with matching suffix",
13948 initial_state: "SubˇError".into(),
13949 buffer_marked_text: "<Sub|Error>".into(),
13950 completion_label: "SubscriptionError",
13951 completion_text: "SubscriptionError",
13952 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13953 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13954 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13955 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13956 },
13957 Run {
13958 run_description: "Suffix is a subsequence -- contiguous",
13959 initial_state: "SubˇErr".into(),
13960 buffer_marked_text: "<Sub|Err>".into(),
13961 completion_label: "SubscriptionError",
13962 completion_text: "SubscriptionError",
13963 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13964 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13965 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13966 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13967 },
13968 Run {
13969 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13970 initial_state: "Suˇscrirr".into(),
13971 buffer_marked_text: "<Su|scrirr>".into(),
13972 completion_label: "SubscriptionError",
13973 completion_text: "SubscriptionError",
13974 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13975 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13976 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13977 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13978 },
13979 Run {
13980 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13981 initial_state: "foo(indˇix)".into(),
13982 buffer_marked_text: "foo(<ind|ix>)".into(),
13983 completion_label: "node_index",
13984 completion_text: "node_index",
13985 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13986 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13987 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13988 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13989 },
13990 Run {
13991 run_description: "Replace range ends before cursor - should extend to cursor",
13992 initial_state: "before editˇo after".into(),
13993 buffer_marked_text: "before <{ed}>it|o after".into(),
13994 completion_label: "editor",
13995 completion_text: "editor",
13996 expected_with_insert_mode: "before editorˇo after".into(),
13997 expected_with_replace_mode: "before editorˇo after".into(),
13998 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13999 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14000 },
14001 Run {
14002 run_description: "Uses label for suffix matching",
14003 initial_state: "before ediˇtor after".into(),
14004 buffer_marked_text: "before <edi|tor> after".into(),
14005 completion_label: "editor",
14006 completion_text: "editor()",
14007 expected_with_insert_mode: "before editor()ˇtor after".into(),
14008 expected_with_replace_mode: "before editor()ˇ after".into(),
14009 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14010 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14011 },
14012 Run {
14013 run_description: "Case insensitive subsequence and suffix matching",
14014 initial_state: "before EDiˇtoR after".into(),
14015 buffer_marked_text: "before <EDi|toR> after".into(),
14016 completion_label: "editor",
14017 completion_text: "editor",
14018 expected_with_insert_mode: "before editorˇtoR after".into(),
14019 expected_with_replace_mode: "before editorˇ after".into(),
14020 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14021 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14022 },
14023 ];
14024
14025 for run in runs {
14026 let run_variations = [
14027 (LspInsertMode::Insert, run.expected_with_insert_mode),
14028 (LspInsertMode::Replace, run.expected_with_replace_mode),
14029 (
14030 LspInsertMode::ReplaceSubsequence,
14031 run.expected_with_replace_subsequence_mode,
14032 ),
14033 (
14034 LspInsertMode::ReplaceSuffix,
14035 run.expected_with_replace_suffix_mode,
14036 ),
14037 ];
14038
14039 for (lsp_insert_mode, expected_text) in run_variations {
14040 eprintln!(
14041 "run = {:?}, mode = {lsp_insert_mode:.?}",
14042 run.run_description,
14043 );
14044
14045 update_test_language_settings(&mut cx, |settings| {
14046 settings.defaults.completions = Some(CompletionSettingsContent {
14047 lsp_insert_mode: Some(lsp_insert_mode),
14048 words: Some(WordsCompletionMode::Disabled),
14049 words_min_length: Some(0),
14050 ..Default::default()
14051 });
14052 });
14053
14054 cx.set_state(&run.initial_state);
14055 cx.update_editor(|editor, window, cx| {
14056 editor.show_completions(&ShowCompletions, window, cx);
14057 });
14058
14059 let counter = Arc::new(AtomicUsize::new(0));
14060 handle_completion_request_with_insert_and_replace(
14061 &mut cx,
14062 &run.buffer_marked_text,
14063 vec![(run.completion_label, run.completion_text)],
14064 counter.clone(),
14065 )
14066 .await;
14067 cx.condition(|editor, _| editor.context_menu_visible())
14068 .await;
14069 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14070
14071 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14072 editor
14073 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14074 .unwrap()
14075 });
14076 cx.assert_editor_state(&expected_text);
14077 handle_resolve_completion_request(&mut cx, None).await;
14078 apply_additional_edits.await.unwrap();
14079 }
14080 }
14081}
14082
14083#[gpui::test]
14084async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14085 init_test(cx, |_| {});
14086 let mut cx = EditorLspTestContext::new_rust(
14087 lsp::ServerCapabilities {
14088 completion_provider: Some(lsp::CompletionOptions {
14089 resolve_provider: Some(true),
14090 ..Default::default()
14091 }),
14092 ..Default::default()
14093 },
14094 cx,
14095 )
14096 .await;
14097
14098 let initial_state = "SubˇError";
14099 let buffer_marked_text = "<Sub|Error>";
14100 let completion_text = "SubscriptionError";
14101 let expected_with_insert_mode = "SubscriptionErrorˇError";
14102 let expected_with_replace_mode = "SubscriptionErrorˇ";
14103
14104 update_test_language_settings(&mut cx, |settings| {
14105 settings.defaults.completions = Some(CompletionSettingsContent {
14106 words: Some(WordsCompletionMode::Disabled),
14107 words_min_length: Some(0),
14108 // set the opposite here to ensure that the action is overriding the default behavior
14109 lsp_insert_mode: Some(LspInsertMode::Insert),
14110 ..Default::default()
14111 });
14112 });
14113
14114 cx.set_state(initial_state);
14115 cx.update_editor(|editor, window, cx| {
14116 editor.show_completions(&ShowCompletions, window, cx);
14117 });
14118
14119 let counter = Arc::new(AtomicUsize::new(0));
14120 handle_completion_request_with_insert_and_replace(
14121 &mut cx,
14122 buffer_marked_text,
14123 vec![(completion_text, completion_text)],
14124 counter.clone(),
14125 )
14126 .await;
14127 cx.condition(|editor, _| editor.context_menu_visible())
14128 .await;
14129 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14130
14131 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14132 editor
14133 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14134 .unwrap()
14135 });
14136 cx.assert_editor_state(expected_with_replace_mode);
14137 handle_resolve_completion_request(&mut cx, None).await;
14138 apply_additional_edits.await.unwrap();
14139
14140 update_test_language_settings(&mut cx, |settings| {
14141 settings.defaults.completions = Some(CompletionSettingsContent {
14142 words: Some(WordsCompletionMode::Disabled),
14143 words_min_length: Some(0),
14144 // set the opposite here to ensure that the action is overriding the default behavior
14145 lsp_insert_mode: Some(LspInsertMode::Replace),
14146 ..Default::default()
14147 });
14148 });
14149
14150 cx.set_state(initial_state);
14151 cx.update_editor(|editor, window, cx| {
14152 editor.show_completions(&ShowCompletions, window, cx);
14153 });
14154 handle_completion_request_with_insert_and_replace(
14155 &mut cx,
14156 buffer_marked_text,
14157 vec![(completion_text, completion_text)],
14158 counter.clone(),
14159 )
14160 .await;
14161 cx.condition(|editor, _| editor.context_menu_visible())
14162 .await;
14163 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14164
14165 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14166 editor
14167 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14168 .unwrap()
14169 });
14170 cx.assert_editor_state(expected_with_insert_mode);
14171 handle_resolve_completion_request(&mut cx, None).await;
14172 apply_additional_edits.await.unwrap();
14173}
14174
14175#[gpui::test]
14176async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14177 init_test(cx, |_| {});
14178 let mut cx = EditorLspTestContext::new_rust(
14179 lsp::ServerCapabilities {
14180 completion_provider: Some(lsp::CompletionOptions {
14181 resolve_provider: Some(true),
14182 ..Default::default()
14183 }),
14184 ..Default::default()
14185 },
14186 cx,
14187 )
14188 .await;
14189
14190 // scenario: surrounding text matches completion text
14191 let completion_text = "to_offset";
14192 let initial_state = indoc! {"
14193 1. buf.to_offˇsuffix
14194 2. buf.to_offˇsuf
14195 3. buf.to_offˇfix
14196 4. buf.to_offˇ
14197 5. into_offˇensive
14198 6. ˇsuffix
14199 7. let ˇ //
14200 8. aaˇzz
14201 9. buf.to_off«zzzzzˇ»suffix
14202 10. buf.«ˇzzzzz»suffix
14203 11. to_off«ˇzzzzz»
14204
14205 buf.to_offˇsuffix // newest cursor
14206 "};
14207 let completion_marked_buffer = indoc! {"
14208 1. buf.to_offsuffix
14209 2. buf.to_offsuf
14210 3. buf.to_offfix
14211 4. buf.to_off
14212 5. into_offensive
14213 6. suffix
14214 7. let //
14215 8. aazz
14216 9. buf.to_offzzzzzsuffix
14217 10. buf.zzzzzsuffix
14218 11. to_offzzzzz
14219
14220 buf.<to_off|suffix> // newest cursor
14221 "};
14222 let expected = indoc! {"
14223 1. buf.to_offsetˇ
14224 2. buf.to_offsetˇsuf
14225 3. buf.to_offsetˇfix
14226 4. buf.to_offsetˇ
14227 5. into_offsetˇensive
14228 6. to_offsetˇsuffix
14229 7. let to_offsetˇ //
14230 8. aato_offsetˇzz
14231 9. buf.to_offsetˇ
14232 10. buf.to_offsetˇsuffix
14233 11. to_offsetˇ
14234
14235 buf.to_offsetˇ // newest cursor
14236 "};
14237 cx.set_state(initial_state);
14238 cx.update_editor(|editor, window, cx| {
14239 editor.show_completions(&ShowCompletions, window, cx);
14240 });
14241 handle_completion_request_with_insert_and_replace(
14242 &mut cx,
14243 completion_marked_buffer,
14244 vec![(completion_text, completion_text)],
14245 Arc::new(AtomicUsize::new(0)),
14246 )
14247 .await;
14248 cx.condition(|editor, _| editor.context_menu_visible())
14249 .await;
14250 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14251 editor
14252 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14253 .unwrap()
14254 });
14255 cx.assert_editor_state(expected);
14256 handle_resolve_completion_request(&mut cx, None).await;
14257 apply_additional_edits.await.unwrap();
14258
14259 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14260 let completion_text = "foo_and_bar";
14261 let initial_state = indoc! {"
14262 1. ooanbˇ
14263 2. zooanbˇ
14264 3. ooanbˇz
14265 4. zooanbˇz
14266 5. ooanˇ
14267 6. oanbˇ
14268
14269 ooanbˇ
14270 "};
14271 let completion_marked_buffer = indoc! {"
14272 1. ooanb
14273 2. zooanb
14274 3. ooanbz
14275 4. zooanbz
14276 5. ooan
14277 6. oanb
14278
14279 <ooanb|>
14280 "};
14281 let expected = indoc! {"
14282 1. foo_and_barˇ
14283 2. zfoo_and_barˇ
14284 3. foo_and_barˇz
14285 4. zfoo_and_barˇz
14286 5. ooanfoo_and_barˇ
14287 6. oanbfoo_and_barˇ
14288
14289 foo_and_barˇ
14290 "};
14291 cx.set_state(initial_state);
14292 cx.update_editor(|editor, window, cx| {
14293 editor.show_completions(&ShowCompletions, window, cx);
14294 });
14295 handle_completion_request_with_insert_and_replace(
14296 &mut cx,
14297 completion_marked_buffer,
14298 vec![(completion_text, completion_text)],
14299 Arc::new(AtomicUsize::new(0)),
14300 )
14301 .await;
14302 cx.condition(|editor, _| editor.context_menu_visible())
14303 .await;
14304 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14305 editor
14306 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14307 .unwrap()
14308 });
14309 cx.assert_editor_state(expected);
14310 handle_resolve_completion_request(&mut cx, None).await;
14311 apply_additional_edits.await.unwrap();
14312
14313 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14314 // (expects the same as if it was inserted at the end)
14315 let completion_text = "foo_and_bar";
14316 let initial_state = indoc! {"
14317 1. ooˇanb
14318 2. zooˇanb
14319 3. ooˇanbz
14320 4. zooˇanbz
14321
14322 ooˇanb
14323 "};
14324 let completion_marked_buffer = indoc! {"
14325 1. ooanb
14326 2. zooanb
14327 3. ooanbz
14328 4. zooanbz
14329
14330 <oo|anb>
14331 "};
14332 let expected = indoc! {"
14333 1. foo_and_barˇ
14334 2. zfoo_and_barˇ
14335 3. foo_and_barˇz
14336 4. zfoo_and_barˇz
14337
14338 foo_and_barˇ
14339 "};
14340 cx.set_state(initial_state);
14341 cx.update_editor(|editor, window, cx| {
14342 editor.show_completions(&ShowCompletions, window, cx);
14343 });
14344 handle_completion_request_with_insert_and_replace(
14345 &mut cx,
14346 completion_marked_buffer,
14347 vec![(completion_text, completion_text)],
14348 Arc::new(AtomicUsize::new(0)),
14349 )
14350 .await;
14351 cx.condition(|editor, _| editor.context_menu_visible())
14352 .await;
14353 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14354 editor
14355 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14356 .unwrap()
14357 });
14358 cx.assert_editor_state(expected);
14359 handle_resolve_completion_request(&mut cx, None).await;
14360 apply_additional_edits.await.unwrap();
14361}
14362
14363// This used to crash
14364#[gpui::test]
14365async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14366 init_test(cx, |_| {});
14367
14368 let buffer_text = indoc! {"
14369 fn main() {
14370 10.satu;
14371
14372 //
14373 // separate cursors so they open in different excerpts (manually reproducible)
14374 //
14375
14376 10.satu20;
14377 }
14378 "};
14379 let multibuffer_text_with_selections = indoc! {"
14380 fn main() {
14381 10.satuˇ;
14382
14383 //
14384
14385 //
14386
14387 10.satuˇ20;
14388 }
14389 "};
14390 let expected_multibuffer = indoc! {"
14391 fn main() {
14392 10.saturating_sub()ˇ;
14393
14394 //
14395
14396 //
14397
14398 10.saturating_sub()ˇ;
14399 }
14400 "};
14401
14402 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14403 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14404
14405 let fs = FakeFs::new(cx.executor());
14406 fs.insert_tree(
14407 path!("/a"),
14408 json!({
14409 "main.rs": buffer_text,
14410 }),
14411 )
14412 .await;
14413
14414 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14415 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14416 language_registry.add(rust_lang());
14417 let mut fake_servers = language_registry.register_fake_lsp(
14418 "Rust",
14419 FakeLspAdapter {
14420 capabilities: lsp::ServerCapabilities {
14421 completion_provider: Some(lsp::CompletionOptions {
14422 resolve_provider: None,
14423 ..lsp::CompletionOptions::default()
14424 }),
14425 ..lsp::ServerCapabilities::default()
14426 },
14427 ..FakeLspAdapter::default()
14428 },
14429 );
14430 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14431 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14432 let buffer = project
14433 .update(cx, |project, cx| {
14434 project.open_local_buffer(path!("/a/main.rs"), cx)
14435 })
14436 .await
14437 .unwrap();
14438
14439 let multi_buffer = cx.new(|cx| {
14440 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14441 multi_buffer.push_excerpts(
14442 buffer.clone(),
14443 [ExcerptRange::new(0..first_excerpt_end)],
14444 cx,
14445 );
14446 multi_buffer.push_excerpts(
14447 buffer.clone(),
14448 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14449 cx,
14450 );
14451 multi_buffer
14452 });
14453
14454 let editor = workspace
14455 .update(cx, |_, window, cx| {
14456 cx.new(|cx| {
14457 Editor::new(
14458 EditorMode::Full {
14459 scale_ui_elements_with_buffer_font_size: false,
14460 show_active_line_background: false,
14461 sizing_behavior: SizingBehavior::Default,
14462 },
14463 multi_buffer.clone(),
14464 Some(project.clone()),
14465 window,
14466 cx,
14467 )
14468 })
14469 })
14470 .unwrap();
14471
14472 let pane = workspace
14473 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14474 .unwrap();
14475 pane.update_in(cx, |pane, window, cx| {
14476 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14477 });
14478
14479 let fake_server = fake_servers.next().await.unwrap();
14480
14481 editor.update_in(cx, |editor, window, cx| {
14482 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14483 s.select_ranges([
14484 Point::new(1, 11)..Point::new(1, 11),
14485 Point::new(7, 11)..Point::new(7, 11),
14486 ])
14487 });
14488
14489 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14490 });
14491
14492 editor.update_in(cx, |editor, window, cx| {
14493 editor.show_completions(&ShowCompletions, window, cx);
14494 });
14495
14496 fake_server
14497 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14498 let completion_item = lsp::CompletionItem {
14499 label: "saturating_sub()".into(),
14500 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14501 lsp::InsertReplaceEdit {
14502 new_text: "saturating_sub()".to_owned(),
14503 insert: lsp::Range::new(
14504 lsp::Position::new(7, 7),
14505 lsp::Position::new(7, 11),
14506 ),
14507 replace: lsp::Range::new(
14508 lsp::Position::new(7, 7),
14509 lsp::Position::new(7, 13),
14510 ),
14511 },
14512 )),
14513 ..lsp::CompletionItem::default()
14514 };
14515
14516 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14517 })
14518 .next()
14519 .await
14520 .unwrap();
14521
14522 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14523 .await;
14524
14525 editor
14526 .update_in(cx, |editor, window, cx| {
14527 editor
14528 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14529 .unwrap()
14530 })
14531 .await
14532 .unwrap();
14533
14534 editor.update(cx, |editor, cx| {
14535 assert_text_with_selections(editor, expected_multibuffer, cx);
14536 })
14537}
14538
14539#[gpui::test]
14540async fn test_completion(cx: &mut TestAppContext) {
14541 init_test(cx, |_| {});
14542
14543 let mut cx = EditorLspTestContext::new_rust(
14544 lsp::ServerCapabilities {
14545 completion_provider: Some(lsp::CompletionOptions {
14546 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14547 resolve_provider: Some(true),
14548 ..Default::default()
14549 }),
14550 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14551 ..Default::default()
14552 },
14553 cx,
14554 )
14555 .await;
14556 let counter = Arc::new(AtomicUsize::new(0));
14557
14558 cx.set_state(indoc! {"
14559 oneˇ
14560 two
14561 three
14562 "});
14563 cx.simulate_keystroke(".");
14564 handle_completion_request(
14565 indoc! {"
14566 one.|<>
14567 two
14568 three
14569 "},
14570 vec!["first_completion", "second_completion"],
14571 true,
14572 counter.clone(),
14573 &mut cx,
14574 )
14575 .await;
14576 cx.condition(|editor, _| editor.context_menu_visible())
14577 .await;
14578 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14579
14580 let _handler = handle_signature_help_request(
14581 &mut cx,
14582 lsp::SignatureHelp {
14583 signatures: vec![lsp::SignatureInformation {
14584 label: "test signature".to_string(),
14585 documentation: None,
14586 parameters: Some(vec![lsp::ParameterInformation {
14587 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14588 documentation: None,
14589 }]),
14590 active_parameter: None,
14591 }],
14592 active_signature: None,
14593 active_parameter: None,
14594 },
14595 );
14596 cx.update_editor(|editor, window, cx| {
14597 assert!(
14598 !editor.signature_help_state.is_shown(),
14599 "No signature help was called for"
14600 );
14601 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14602 });
14603 cx.run_until_parked();
14604 cx.update_editor(|editor, _, _| {
14605 assert!(
14606 !editor.signature_help_state.is_shown(),
14607 "No signature help should be shown when completions menu is open"
14608 );
14609 });
14610
14611 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14612 editor.context_menu_next(&Default::default(), window, cx);
14613 editor
14614 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14615 .unwrap()
14616 });
14617 cx.assert_editor_state(indoc! {"
14618 one.second_completionˇ
14619 two
14620 three
14621 "});
14622
14623 handle_resolve_completion_request(
14624 &mut cx,
14625 Some(vec![
14626 (
14627 //This overlaps with the primary completion edit which is
14628 //misbehavior from the LSP spec, test that we filter it out
14629 indoc! {"
14630 one.second_ˇcompletion
14631 two
14632 threeˇ
14633 "},
14634 "overlapping additional edit",
14635 ),
14636 (
14637 indoc! {"
14638 one.second_completion
14639 two
14640 threeˇ
14641 "},
14642 "\nadditional edit",
14643 ),
14644 ]),
14645 )
14646 .await;
14647 apply_additional_edits.await.unwrap();
14648 cx.assert_editor_state(indoc! {"
14649 one.second_completionˇ
14650 two
14651 three
14652 additional edit
14653 "});
14654
14655 cx.set_state(indoc! {"
14656 one.second_completion
14657 twoˇ
14658 threeˇ
14659 additional edit
14660 "});
14661 cx.simulate_keystroke(" ");
14662 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14663 cx.simulate_keystroke("s");
14664 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14665
14666 cx.assert_editor_state(indoc! {"
14667 one.second_completion
14668 two sˇ
14669 three sˇ
14670 additional edit
14671 "});
14672 handle_completion_request(
14673 indoc! {"
14674 one.second_completion
14675 two s
14676 three <s|>
14677 additional edit
14678 "},
14679 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14680 true,
14681 counter.clone(),
14682 &mut cx,
14683 )
14684 .await;
14685 cx.condition(|editor, _| editor.context_menu_visible())
14686 .await;
14687 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14688
14689 cx.simulate_keystroke("i");
14690
14691 handle_completion_request(
14692 indoc! {"
14693 one.second_completion
14694 two si
14695 three <si|>
14696 additional edit
14697 "},
14698 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14699 true,
14700 counter.clone(),
14701 &mut cx,
14702 )
14703 .await;
14704 cx.condition(|editor, _| editor.context_menu_visible())
14705 .await;
14706 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14707
14708 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14709 editor
14710 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14711 .unwrap()
14712 });
14713 cx.assert_editor_state(indoc! {"
14714 one.second_completion
14715 two sixth_completionˇ
14716 three sixth_completionˇ
14717 additional edit
14718 "});
14719
14720 apply_additional_edits.await.unwrap();
14721
14722 update_test_language_settings(&mut cx, |settings| {
14723 settings.defaults.show_completions_on_input = Some(false);
14724 });
14725 cx.set_state("editorˇ");
14726 cx.simulate_keystroke(".");
14727 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14728 cx.simulate_keystrokes("c l o");
14729 cx.assert_editor_state("editor.cloˇ");
14730 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14731 cx.update_editor(|editor, window, cx| {
14732 editor.show_completions(&ShowCompletions, window, cx);
14733 });
14734 handle_completion_request(
14735 "editor.<clo|>",
14736 vec!["close", "clobber"],
14737 true,
14738 counter.clone(),
14739 &mut cx,
14740 )
14741 .await;
14742 cx.condition(|editor, _| editor.context_menu_visible())
14743 .await;
14744 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14745
14746 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14747 editor
14748 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14749 .unwrap()
14750 });
14751 cx.assert_editor_state("editor.clobberˇ");
14752 handle_resolve_completion_request(&mut cx, None).await;
14753 apply_additional_edits.await.unwrap();
14754}
14755
14756#[gpui::test]
14757async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
14758 init_test(cx, |_| {});
14759
14760 let fs = FakeFs::new(cx.executor());
14761 fs.insert_tree(
14762 path!("/a"),
14763 json!({
14764 "main.rs": "",
14765 }),
14766 )
14767 .await;
14768
14769 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14770 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14771 language_registry.add(rust_lang());
14772 let command_calls = Arc::new(AtomicUsize::new(0));
14773 let registered_command = "_the/command";
14774
14775 let closure_command_calls = command_calls.clone();
14776 let mut fake_servers = language_registry.register_fake_lsp(
14777 "Rust",
14778 FakeLspAdapter {
14779 capabilities: lsp::ServerCapabilities {
14780 completion_provider: Some(lsp::CompletionOptions {
14781 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14782 ..lsp::CompletionOptions::default()
14783 }),
14784 execute_command_provider: Some(lsp::ExecuteCommandOptions {
14785 commands: vec![registered_command.to_owned()],
14786 ..lsp::ExecuteCommandOptions::default()
14787 }),
14788 ..lsp::ServerCapabilities::default()
14789 },
14790 initializer: Some(Box::new(move |fake_server| {
14791 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14792 move |params, _| async move {
14793 Ok(Some(lsp::CompletionResponse::Array(vec![
14794 lsp::CompletionItem {
14795 label: "registered_command".to_owned(),
14796 text_edit: gen_text_edit(¶ms, ""),
14797 command: Some(lsp::Command {
14798 title: registered_command.to_owned(),
14799 command: "_the/command".to_owned(),
14800 arguments: Some(vec![serde_json::Value::Bool(true)]),
14801 }),
14802 ..lsp::CompletionItem::default()
14803 },
14804 lsp::CompletionItem {
14805 label: "unregistered_command".to_owned(),
14806 text_edit: gen_text_edit(¶ms, ""),
14807 command: Some(lsp::Command {
14808 title: "????????????".to_owned(),
14809 command: "????????????".to_owned(),
14810 arguments: Some(vec![serde_json::Value::Null]),
14811 }),
14812 ..lsp::CompletionItem::default()
14813 },
14814 ])))
14815 },
14816 );
14817 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
14818 let command_calls = closure_command_calls.clone();
14819 move |params, _| {
14820 assert_eq!(params.command, registered_command);
14821 let command_calls = command_calls.clone();
14822 async move {
14823 command_calls.fetch_add(1, atomic::Ordering::Release);
14824 Ok(Some(json!(null)))
14825 }
14826 }
14827 });
14828 })),
14829 ..FakeLspAdapter::default()
14830 },
14831 );
14832 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14833 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14834 let editor = workspace
14835 .update(cx, |workspace, window, cx| {
14836 workspace.open_abs_path(
14837 PathBuf::from(path!("/a/main.rs")),
14838 OpenOptions::default(),
14839 window,
14840 cx,
14841 )
14842 })
14843 .unwrap()
14844 .await
14845 .unwrap()
14846 .downcast::<Editor>()
14847 .unwrap();
14848 let _fake_server = fake_servers.next().await.unwrap();
14849
14850 editor.update_in(cx, |editor, window, cx| {
14851 cx.focus_self(window);
14852 editor.move_to_end(&MoveToEnd, window, cx);
14853 editor.handle_input(".", window, cx);
14854 });
14855 cx.run_until_parked();
14856 editor.update(cx, |editor, _| {
14857 assert!(editor.context_menu_visible());
14858 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14859 {
14860 let completion_labels = menu
14861 .completions
14862 .borrow()
14863 .iter()
14864 .map(|c| c.label.text.clone())
14865 .collect::<Vec<_>>();
14866 assert_eq!(
14867 completion_labels,
14868 &["registered_command", "unregistered_command",],
14869 );
14870 } else {
14871 panic!("expected completion menu to be open");
14872 }
14873 });
14874
14875 editor
14876 .update_in(cx, |editor, window, cx| {
14877 editor
14878 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14879 .unwrap()
14880 })
14881 .await
14882 .unwrap();
14883 cx.run_until_parked();
14884 assert_eq!(
14885 command_calls.load(atomic::Ordering::Acquire),
14886 1,
14887 "For completion with a registered command, Zed should send a command execution request",
14888 );
14889
14890 editor.update_in(cx, |editor, window, cx| {
14891 cx.focus_self(window);
14892 editor.handle_input(".", window, cx);
14893 });
14894 cx.run_until_parked();
14895 editor.update(cx, |editor, _| {
14896 assert!(editor.context_menu_visible());
14897 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14898 {
14899 let completion_labels = menu
14900 .completions
14901 .borrow()
14902 .iter()
14903 .map(|c| c.label.text.clone())
14904 .collect::<Vec<_>>();
14905 assert_eq!(
14906 completion_labels,
14907 &["registered_command", "unregistered_command",],
14908 );
14909 } else {
14910 panic!("expected completion menu to be open");
14911 }
14912 });
14913 editor
14914 .update_in(cx, |editor, window, cx| {
14915 editor.context_menu_next(&Default::default(), window, cx);
14916 editor
14917 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14918 .unwrap()
14919 })
14920 .await
14921 .unwrap();
14922 cx.run_until_parked();
14923 assert_eq!(
14924 command_calls.load(atomic::Ordering::Acquire),
14925 1,
14926 "For completion with an unregistered command, Zed should not send a command execution request",
14927 );
14928}
14929
14930#[gpui::test]
14931async fn test_completion_reuse(cx: &mut TestAppContext) {
14932 init_test(cx, |_| {});
14933
14934 let mut cx = EditorLspTestContext::new_rust(
14935 lsp::ServerCapabilities {
14936 completion_provider: Some(lsp::CompletionOptions {
14937 trigger_characters: Some(vec![".".to_string()]),
14938 ..Default::default()
14939 }),
14940 ..Default::default()
14941 },
14942 cx,
14943 )
14944 .await;
14945
14946 let counter = Arc::new(AtomicUsize::new(0));
14947 cx.set_state("objˇ");
14948 cx.simulate_keystroke(".");
14949
14950 // Initial completion request returns complete results
14951 let is_incomplete = false;
14952 handle_completion_request(
14953 "obj.|<>",
14954 vec!["a", "ab", "abc"],
14955 is_incomplete,
14956 counter.clone(),
14957 &mut cx,
14958 )
14959 .await;
14960 cx.run_until_parked();
14961 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14962 cx.assert_editor_state("obj.ˇ");
14963 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14964
14965 // Type "a" - filters existing completions
14966 cx.simulate_keystroke("a");
14967 cx.run_until_parked();
14968 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14969 cx.assert_editor_state("obj.aˇ");
14970 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14971
14972 // Type "b" - filters existing completions
14973 cx.simulate_keystroke("b");
14974 cx.run_until_parked();
14975 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14976 cx.assert_editor_state("obj.abˇ");
14977 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14978
14979 // Type "c" - filters existing completions
14980 cx.simulate_keystroke("c");
14981 cx.run_until_parked();
14982 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14983 cx.assert_editor_state("obj.abcˇ");
14984 check_displayed_completions(vec!["abc"], &mut cx);
14985
14986 // Backspace to delete "c" - filters existing completions
14987 cx.update_editor(|editor, window, cx| {
14988 editor.backspace(&Backspace, window, cx);
14989 });
14990 cx.run_until_parked();
14991 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14992 cx.assert_editor_state("obj.abˇ");
14993 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14994
14995 // Moving cursor to the left dismisses menu.
14996 cx.update_editor(|editor, window, cx| {
14997 editor.move_left(&MoveLeft, window, cx);
14998 });
14999 cx.run_until_parked();
15000 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15001 cx.assert_editor_state("obj.aˇb");
15002 cx.update_editor(|editor, _, _| {
15003 assert_eq!(editor.context_menu_visible(), false);
15004 });
15005
15006 // Type "b" - new request
15007 cx.simulate_keystroke("b");
15008 let is_incomplete = false;
15009 handle_completion_request(
15010 "obj.<ab|>a",
15011 vec!["ab", "abc"],
15012 is_incomplete,
15013 counter.clone(),
15014 &mut cx,
15015 )
15016 .await;
15017 cx.run_until_parked();
15018 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15019 cx.assert_editor_state("obj.abˇb");
15020 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15021
15022 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15023 cx.update_editor(|editor, window, cx| {
15024 editor.backspace(&Backspace, window, cx);
15025 });
15026 let is_incomplete = false;
15027 handle_completion_request(
15028 "obj.<a|>b",
15029 vec!["a", "ab", "abc"],
15030 is_incomplete,
15031 counter.clone(),
15032 &mut cx,
15033 )
15034 .await;
15035 cx.run_until_parked();
15036 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15037 cx.assert_editor_state("obj.aˇb");
15038 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15039
15040 // Backspace to delete "a" - dismisses menu.
15041 cx.update_editor(|editor, window, cx| {
15042 editor.backspace(&Backspace, window, cx);
15043 });
15044 cx.run_until_parked();
15045 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15046 cx.assert_editor_state("obj.ˇb");
15047 cx.update_editor(|editor, _, _| {
15048 assert_eq!(editor.context_menu_visible(), false);
15049 });
15050}
15051
15052#[gpui::test]
15053async fn test_word_completion(cx: &mut TestAppContext) {
15054 let lsp_fetch_timeout_ms = 10;
15055 init_test(cx, |language_settings| {
15056 language_settings.defaults.completions = Some(CompletionSettingsContent {
15057 words_min_length: Some(0),
15058 lsp_fetch_timeout_ms: Some(10),
15059 lsp_insert_mode: Some(LspInsertMode::Insert),
15060 ..Default::default()
15061 });
15062 });
15063
15064 let mut cx = EditorLspTestContext::new_rust(
15065 lsp::ServerCapabilities {
15066 completion_provider: Some(lsp::CompletionOptions {
15067 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15068 ..lsp::CompletionOptions::default()
15069 }),
15070 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15071 ..lsp::ServerCapabilities::default()
15072 },
15073 cx,
15074 )
15075 .await;
15076
15077 let throttle_completions = Arc::new(AtomicBool::new(false));
15078
15079 let lsp_throttle_completions = throttle_completions.clone();
15080 let _completion_requests_handler =
15081 cx.lsp
15082 .server
15083 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15084 let lsp_throttle_completions = lsp_throttle_completions.clone();
15085 let cx = cx.clone();
15086 async move {
15087 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15088 cx.background_executor()
15089 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15090 .await;
15091 }
15092 Ok(Some(lsp::CompletionResponse::Array(vec![
15093 lsp::CompletionItem {
15094 label: "first".into(),
15095 ..lsp::CompletionItem::default()
15096 },
15097 lsp::CompletionItem {
15098 label: "last".into(),
15099 ..lsp::CompletionItem::default()
15100 },
15101 ])))
15102 }
15103 });
15104
15105 cx.set_state(indoc! {"
15106 oneˇ
15107 two
15108 three
15109 "});
15110 cx.simulate_keystroke(".");
15111 cx.executor().run_until_parked();
15112 cx.condition(|editor, _| editor.context_menu_visible())
15113 .await;
15114 cx.update_editor(|editor, window, cx| {
15115 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15116 {
15117 assert_eq!(
15118 completion_menu_entries(menu),
15119 &["first", "last"],
15120 "When LSP server is fast to reply, no fallback word completions are used"
15121 );
15122 } else {
15123 panic!("expected completion menu to be open");
15124 }
15125 editor.cancel(&Cancel, window, cx);
15126 });
15127 cx.executor().run_until_parked();
15128 cx.condition(|editor, _| !editor.context_menu_visible())
15129 .await;
15130
15131 throttle_completions.store(true, atomic::Ordering::Release);
15132 cx.simulate_keystroke(".");
15133 cx.executor()
15134 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15135 cx.executor().run_until_parked();
15136 cx.condition(|editor, _| editor.context_menu_visible())
15137 .await;
15138 cx.update_editor(|editor, _, _| {
15139 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15140 {
15141 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15142 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15143 } else {
15144 panic!("expected completion menu to be open");
15145 }
15146 });
15147}
15148
15149#[gpui::test]
15150async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15151 init_test(cx, |language_settings| {
15152 language_settings.defaults.completions = Some(CompletionSettingsContent {
15153 words: Some(WordsCompletionMode::Enabled),
15154 words_min_length: Some(0),
15155 lsp_insert_mode: Some(LspInsertMode::Insert),
15156 ..Default::default()
15157 });
15158 });
15159
15160 let mut cx = EditorLspTestContext::new_rust(
15161 lsp::ServerCapabilities {
15162 completion_provider: Some(lsp::CompletionOptions {
15163 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15164 ..lsp::CompletionOptions::default()
15165 }),
15166 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15167 ..lsp::ServerCapabilities::default()
15168 },
15169 cx,
15170 )
15171 .await;
15172
15173 let _completion_requests_handler =
15174 cx.lsp
15175 .server
15176 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15177 Ok(Some(lsp::CompletionResponse::Array(vec![
15178 lsp::CompletionItem {
15179 label: "first".into(),
15180 ..lsp::CompletionItem::default()
15181 },
15182 lsp::CompletionItem {
15183 label: "last".into(),
15184 ..lsp::CompletionItem::default()
15185 },
15186 ])))
15187 });
15188
15189 cx.set_state(indoc! {"ˇ
15190 first
15191 last
15192 second
15193 "});
15194 cx.simulate_keystroke(".");
15195 cx.executor().run_until_parked();
15196 cx.condition(|editor, _| editor.context_menu_visible())
15197 .await;
15198 cx.update_editor(|editor, _, _| {
15199 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15200 {
15201 assert_eq!(
15202 completion_menu_entries(menu),
15203 &["first", "last", "second"],
15204 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15205 );
15206 } else {
15207 panic!("expected completion menu to be open");
15208 }
15209 });
15210}
15211
15212#[gpui::test]
15213async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15214 init_test(cx, |language_settings| {
15215 language_settings.defaults.completions = Some(CompletionSettingsContent {
15216 words: Some(WordsCompletionMode::Disabled),
15217 words_min_length: Some(0),
15218 lsp_insert_mode: Some(LspInsertMode::Insert),
15219 ..Default::default()
15220 });
15221 });
15222
15223 let mut cx = EditorLspTestContext::new_rust(
15224 lsp::ServerCapabilities {
15225 completion_provider: Some(lsp::CompletionOptions {
15226 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15227 ..lsp::CompletionOptions::default()
15228 }),
15229 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15230 ..lsp::ServerCapabilities::default()
15231 },
15232 cx,
15233 )
15234 .await;
15235
15236 let _completion_requests_handler =
15237 cx.lsp
15238 .server
15239 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15240 panic!("LSP completions should not be queried when dealing with word completions")
15241 });
15242
15243 cx.set_state(indoc! {"ˇ
15244 first
15245 last
15246 second
15247 "});
15248 cx.update_editor(|editor, window, cx| {
15249 editor.show_word_completions(&ShowWordCompletions, window, cx);
15250 });
15251 cx.executor().run_until_parked();
15252 cx.condition(|editor, _| editor.context_menu_visible())
15253 .await;
15254 cx.update_editor(|editor, _, _| {
15255 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15256 {
15257 assert_eq!(
15258 completion_menu_entries(menu),
15259 &["first", "last", "second"],
15260 "`ShowWordCompletions` action should show word completions"
15261 );
15262 } else {
15263 panic!("expected completion menu to be open");
15264 }
15265 });
15266
15267 cx.simulate_keystroke("l");
15268 cx.executor().run_until_parked();
15269 cx.condition(|editor, _| editor.context_menu_visible())
15270 .await;
15271 cx.update_editor(|editor, _, _| {
15272 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15273 {
15274 assert_eq!(
15275 completion_menu_entries(menu),
15276 &["last"],
15277 "After showing word completions, further editing should filter them and not query the LSP"
15278 );
15279 } else {
15280 panic!("expected completion menu to be open");
15281 }
15282 });
15283}
15284
15285#[gpui::test]
15286async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15287 init_test(cx, |language_settings| {
15288 language_settings.defaults.completions = Some(CompletionSettingsContent {
15289 words_min_length: Some(0),
15290 lsp: Some(false),
15291 lsp_insert_mode: Some(LspInsertMode::Insert),
15292 ..Default::default()
15293 });
15294 });
15295
15296 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15297
15298 cx.set_state(indoc! {"ˇ
15299 0_usize
15300 let
15301 33
15302 4.5f32
15303 "});
15304 cx.update_editor(|editor, window, cx| {
15305 editor.show_completions(&ShowCompletions, window, cx);
15306 });
15307 cx.executor().run_until_parked();
15308 cx.condition(|editor, _| editor.context_menu_visible())
15309 .await;
15310 cx.update_editor(|editor, window, cx| {
15311 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15312 {
15313 assert_eq!(
15314 completion_menu_entries(menu),
15315 &["let"],
15316 "With no digits in the completion query, no digits should be in the word completions"
15317 );
15318 } else {
15319 panic!("expected completion menu to be open");
15320 }
15321 editor.cancel(&Cancel, window, cx);
15322 });
15323
15324 cx.set_state(indoc! {"3ˇ
15325 0_usize
15326 let
15327 3
15328 33.35f32
15329 "});
15330 cx.update_editor(|editor, window, cx| {
15331 editor.show_completions(&ShowCompletions, window, cx);
15332 });
15333 cx.executor().run_until_parked();
15334 cx.condition(|editor, _| editor.context_menu_visible())
15335 .await;
15336 cx.update_editor(|editor, _, _| {
15337 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15338 {
15339 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15340 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15341 } else {
15342 panic!("expected completion menu to be open");
15343 }
15344 });
15345}
15346
15347#[gpui::test]
15348async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15349 init_test(cx, |language_settings| {
15350 language_settings.defaults.completions = Some(CompletionSettingsContent {
15351 words: Some(WordsCompletionMode::Enabled),
15352 words_min_length: Some(3),
15353 lsp_insert_mode: Some(LspInsertMode::Insert),
15354 ..Default::default()
15355 });
15356 });
15357
15358 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15359 cx.set_state(indoc! {"ˇ
15360 wow
15361 wowen
15362 wowser
15363 "});
15364 cx.simulate_keystroke("w");
15365 cx.executor().run_until_parked();
15366 cx.update_editor(|editor, _, _| {
15367 if editor.context_menu.borrow_mut().is_some() {
15368 panic!(
15369 "expected completion menu to be hidden, as words completion threshold is not met"
15370 );
15371 }
15372 });
15373
15374 cx.update_editor(|editor, window, cx| {
15375 editor.show_word_completions(&ShowWordCompletions, window, cx);
15376 });
15377 cx.executor().run_until_parked();
15378 cx.update_editor(|editor, window, cx| {
15379 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15380 {
15381 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");
15382 } else {
15383 panic!("expected completion menu to be open after the word completions are called with an action");
15384 }
15385
15386 editor.cancel(&Cancel, window, cx);
15387 });
15388 cx.update_editor(|editor, _, _| {
15389 if editor.context_menu.borrow_mut().is_some() {
15390 panic!("expected completion menu to be hidden after canceling");
15391 }
15392 });
15393
15394 cx.simulate_keystroke("o");
15395 cx.executor().run_until_parked();
15396 cx.update_editor(|editor, _, _| {
15397 if editor.context_menu.borrow_mut().is_some() {
15398 panic!(
15399 "expected completion menu to be hidden, as words completion threshold is not met still"
15400 );
15401 }
15402 });
15403
15404 cx.simulate_keystroke("w");
15405 cx.executor().run_until_parked();
15406 cx.update_editor(|editor, _, _| {
15407 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15408 {
15409 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15410 } else {
15411 panic!("expected completion menu to be open after the word completions threshold is met");
15412 }
15413 });
15414}
15415
15416#[gpui::test]
15417async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15418 init_test(cx, |language_settings| {
15419 language_settings.defaults.completions = Some(CompletionSettingsContent {
15420 words: Some(WordsCompletionMode::Enabled),
15421 words_min_length: Some(0),
15422 lsp_insert_mode: Some(LspInsertMode::Insert),
15423 ..Default::default()
15424 });
15425 });
15426
15427 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15428 cx.update_editor(|editor, _, _| {
15429 editor.disable_word_completions();
15430 });
15431 cx.set_state(indoc! {"ˇ
15432 wow
15433 wowen
15434 wowser
15435 "});
15436 cx.simulate_keystroke("w");
15437 cx.executor().run_until_parked();
15438 cx.update_editor(|editor, _, _| {
15439 if editor.context_menu.borrow_mut().is_some() {
15440 panic!(
15441 "expected completion menu to be hidden, as words completion are disabled for this editor"
15442 );
15443 }
15444 });
15445
15446 cx.update_editor(|editor, window, cx| {
15447 editor.show_word_completions(&ShowWordCompletions, window, cx);
15448 });
15449 cx.executor().run_until_parked();
15450 cx.update_editor(|editor, _, _| {
15451 if editor.context_menu.borrow_mut().is_some() {
15452 panic!(
15453 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15454 );
15455 }
15456 });
15457}
15458
15459#[gpui::test]
15460async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15461 init_test(cx, |language_settings| {
15462 language_settings.defaults.completions = Some(CompletionSettingsContent {
15463 words: Some(WordsCompletionMode::Disabled),
15464 words_min_length: Some(0),
15465 lsp_insert_mode: Some(LspInsertMode::Insert),
15466 ..Default::default()
15467 });
15468 });
15469
15470 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15471 cx.update_editor(|editor, _, _| {
15472 editor.set_completion_provider(None);
15473 });
15474 cx.set_state(indoc! {"ˇ
15475 wow
15476 wowen
15477 wowser
15478 "});
15479 cx.simulate_keystroke("w");
15480 cx.executor().run_until_parked();
15481 cx.update_editor(|editor, _, _| {
15482 if editor.context_menu.borrow_mut().is_some() {
15483 panic!("expected completion menu to be hidden, as disabled in settings");
15484 }
15485 });
15486}
15487
15488fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15489 let position = || lsp::Position {
15490 line: params.text_document_position.position.line,
15491 character: params.text_document_position.position.character,
15492 };
15493 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15494 range: lsp::Range {
15495 start: position(),
15496 end: position(),
15497 },
15498 new_text: text.to_string(),
15499 }))
15500}
15501
15502#[gpui::test]
15503async fn test_multiline_completion(cx: &mut TestAppContext) {
15504 init_test(cx, |_| {});
15505
15506 let fs = FakeFs::new(cx.executor());
15507 fs.insert_tree(
15508 path!("/a"),
15509 json!({
15510 "main.ts": "a",
15511 }),
15512 )
15513 .await;
15514
15515 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15516 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15517 let typescript_language = Arc::new(Language::new(
15518 LanguageConfig {
15519 name: "TypeScript".into(),
15520 matcher: LanguageMatcher {
15521 path_suffixes: vec!["ts".to_string()],
15522 ..LanguageMatcher::default()
15523 },
15524 line_comments: vec!["// ".into()],
15525 ..LanguageConfig::default()
15526 },
15527 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15528 ));
15529 language_registry.add(typescript_language.clone());
15530 let mut fake_servers = language_registry.register_fake_lsp(
15531 "TypeScript",
15532 FakeLspAdapter {
15533 capabilities: lsp::ServerCapabilities {
15534 completion_provider: Some(lsp::CompletionOptions {
15535 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15536 ..lsp::CompletionOptions::default()
15537 }),
15538 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15539 ..lsp::ServerCapabilities::default()
15540 },
15541 // Emulate vtsls label generation
15542 label_for_completion: Some(Box::new(|item, _| {
15543 let text = if let Some(description) = item
15544 .label_details
15545 .as_ref()
15546 .and_then(|label_details| label_details.description.as_ref())
15547 {
15548 format!("{} {}", item.label, description)
15549 } else if let Some(detail) = &item.detail {
15550 format!("{} {}", item.label, detail)
15551 } else {
15552 item.label.clone()
15553 };
15554 Some(language::CodeLabel::plain(text, None))
15555 })),
15556 ..FakeLspAdapter::default()
15557 },
15558 );
15559 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15560 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15561 let worktree_id = workspace
15562 .update(cx, |workspace, _window, cx| {
15563 workspace.project().update(cx, |project, cx| {
15564 project.worktrees(cx).next().unwrap().read(cx).id()
15565 })
15566 })
15567 .unwrap();
15568 let _buffer = project
15569 .update(cx, |project, cx| {
15570 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15571 })
15572 .await
15573 .unwrap();
15574 let editor = workspace
15575 .update(cx, |workspace, window, cx| {
15576 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15577 })
15578 .unwrap()
15579 .await
15580 .unwrap()
15581 .downcast::<Editor>()
15582 .unwrap();
15583 let fake_server = fake_servers.next().await.unwrap();
15584
15585 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15586 let multiline_label_2 = "a\nb\nc\n";
15587 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15588 let multiline_description = "d\ne\nf\n";
15589 let multiline_detail_2 = "g\nh\ni\n";
15590
15591 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15592 move |params, _| async move {
15593 Ok(Some(lsp::CompletionResponse::Array(vec![
15594 lsp::CompletionItem {
15595 label: multiline_label.to_string(),
15596 text_edit: gen_text_edit(¶ms, "new_text_1"),
15597 ..lsp::CompletionItem::default()
15598 },
15599 lsp::CompletionItem {
15600 label: "single line label 1".to_string(),
15601 detail: Some(multiline_detail.to_string()),
15602 text_edit: gen_text_edit(¶ms, "new_text_2"),
15603 ..lsp::CompletionItem::default()
15604 },
15605 lsp::CompletionItem {
15606 label: "single line label 2".to_string(),
15607 label_details: Some(lsp::CompletionItemLabelDetails {
15608 description: Some(multiline_description.to_string()),
15609 detail: None,
15610 }),
15611 text_edit: gen_text_edit(¶ms, "new_text_2"),
15612 ..lsp::CompletionItem::default()
15613 },
15614 lsp::CompletionItem {
15615 label: multiline_label_2.to_string(),
15616 detail: Some(multiline_detail_2.to_string()),
15617 text_edit: gen_text_edit(¶ms, "new_text_3"),
15618 ..lsp::CompletionItem::default()
15619 },
15620 lsp::CompletionItem {
15621 label: "Label with many spaces and \t but without newlines".to_string(),
15622 detail: Some(
15623 "Details with many spaces and \t but without newlines".to_string(),
15624 ),
15625 text_edit: gen_text_edit(¶ms, "new_text_4"),
15626 ..lsp::CompletionItem::default()
15627 },
15628 ])))
15629 },
15630 );
15631
15632 editor.update_in(cx, |editor, window, cx| {
15633 cx.focus_self(window);
15634 editor.move_to_end(&MoveToEnd, window, cx);
15635 editor.handle_input(".", window, cx);
15636 });
15637 cx.run_until_parked();
15638 completion_handle.next().await.unwrap();
15639
15640 editor.update(cx, |editor, _| {
15641 assert!(editor.context_menu_visible());
15642 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15643 {
15644 let completion_labels = menu
15645 .completions
15646 .borrow()
15647 .iter()
15648 .map(|c| c.label.text.clone())
15649 .collect::<Vec<_>>();
15650 assert_eq!(
15651 completion_labels,
15652 &[
15653 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15654 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15655 "single line label 2 d e f ",
15656 "a b c g h i ",
15657 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15658 ],
15659 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15660 );
15661
15662 for completion in menu
15663 .completions
15664 .borrow()
15665 .iter() {
15666 assert_eq!(
15667 completion.label.filter_range,
15668 0..completion.label.text.len(),
15669 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15670 );
15671 }
15672 } else {
15673 panic!("expected completion menu to be open");
15674 }
15675 });
15676}
15677
15678#[gpui::test]
15679async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15680 init_test(cx, |_| {});
15681 let mut cx = EditorLspTestContext::new_rust(
15682 lsp::ServerCapabilities {
15683 completion_provider: Some(lsp::CompletionOptions {
15684 trigger_characters: Some(vec![".".to_string()]),
15685 ..Default::default()
15686 }),
15687 ..Default::default()
15688 },
15689 cx,
15690 )
15691 .await;
15692 cx.lsp
15693 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15694 Ok(Some(lsp::CompletionResponse::Array(vec![
15695 lsp::CompletionItem {
15696 label: "first".into(),
15697 ..Default::default()
15698 },
15699 lsp::CompletionItem {
15700 label: "last".into(),
15701 ..Default::default()
15702 },
15703 ])))
15704 });
15705 cx.set_state("variableˇ");
15706 cx.simulate_keystroke(".");
15707 cx.executor().run_until_parked();
15708
15709 cx.update_editor(|editor, _, _| {
15710 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15711 {
15712 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15713 } else {
15714 panic!("expected completion menu to be open");
15715 }
15716 });
15717
15718 cx.update_editor(|editor, window, cx| {
15719 editor.move_page_down(&MovePageDown::default(), window, cx);
15720 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15721 {
15722 assert!(
15723 menu.selected_item == 1,
15724 "expected PageDown to select the last item from the context menu"
15725 );
15726 } else {
15727 panic!("expected completion menu to stay open after PageDown");
15728 }
15729 });
15730
15731 cx.update_editor(|editor, window, cx| {
15732 editor.move_page_up(&MovePageUp::default(), window, cx);
15733 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15734 {
15735 assert!(
15736 menu.selected_item == 0,
15737 "expected PageUp to select the first item from the context menu"
15738 );
15739 } else {
15740 panic!("expected completion menu to stay open after PageUp");
15741 }
15742 });
15743}
15744
15745#[gpui::test]
15746async fn test_as_is_completions(cx: &mut TestAppContext) {
15747 init_test(cx, |_| {});
15748 let mut cx = EditorLspTestContext::new_rust(
15749 lsp::ServerCapabilities {
15750 completion_provider: Some(lsp::CompletionOptions {
15751 ..Default::default()
15752 }),
15753 ..Default::default()
15754 },
15755 cx,
15756 )
15757 .await;
15758 cx.lsp
15759 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15760 Ok(Some(lsp::CompletionResponse::Array(vec![
15761 lsp::CompletionItem {
15762 label: "unsafe".into(),
15763 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15764 range: lsp::Range {
15765 start: lsp::Position {
15766 line: 1,
15767 character: 2,
15768 },
15769 end: lsp::Position {
15770 line: 1,
15771 character: 3,
15772 },
15773 },
15774 new_text: "unsafe".to_string(),
15775 })),
15776 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15777 ..Default::default()
15778 },
15779 ])))
15780 });
15781 cx.set_state("fn a() {}\n nˇ");
15782 cx.executor().run_until_parked();
15783 cx.update_editor(|editor, window, cx| {
15784 editor.trigger_completion_on_input("n", true, window, cx)
15785 });
15786 cx.executor().run_until_parked();
15787
15788 cx.update_editor(|editor, window, cx| {
15789 editor.confirm_completion(&Default::default(), window, cx)
15790 });
15791 cx.executor().run_until_parked();
15792 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15793}
15794
15795#[gpui::test]
15796async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15797 init_test(cx, |_| {});
15798 let language =
15799 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15800 let mut cx = EditorLspTestContext::new(
15801 language,
15802 lsp::ServerCapabilities {
15803 completion_provider: Some(lsp::CompletionOptions {
15804 ..lsp::CompletionOptions::default()
15805 }),
15806 ..lsp::ServerCapabilities::default()
15807 },
15808 cx,
15809 )
15810 .await;
15811
15812 cx.set_state(
15813 "#ifndef BAR_H
15814#define BAR_H
15815
15816#include <stdbool.h>
15817
15818int fn_branch(bool do_branch1, bool do_branch2);
15819
15820#endif // BAR_H
15821ˇ",
15822 );
15823 cx.executor().run_until_parked();
15824 cx.update_editor(|editor, window, cx| {
15825 editor.handle_input("#", window, cx);
15826 });
15827 cx.executor().run_until_parked();
15828 cx.update_editor(|editor, window, cx| {
15829 editor.handle_input("i", window, cx);
15830 });
15831 cx.executor().run_until_parked();
15832 cx.update_editor(|editor, window, cx| {
15833 editor.handle_input("n", window, cx);
15834 });
15835 cx.executor().run_until_parked();
15836 cx.assert_editor_state(
15837 "#ifndef BAR_H
15838#define BAR_H
15839
15840#include <stdbool.h>
15841
15842int fn_branch(bool do_branch1, bool do_branch2);
15843
15844#endif // BAR_H
15845#inˇ",
15846 );
15847
15848 cx.lsp
15849 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15850 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15851 is_incomplete: false,
15852 item_defaults: None,
15853 items: vec![lsp::CompletionItem {
15854 kind: Some(lsp::CompletionItemKind::SNIPPET),
15855 label_details: Some(lsp::CompletionItemLabelDetails {
15856 detail: Some("header".to_string()),
15857 description: None,
15858 }),
15859 label: " include".to_string(),
15860 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15861 range: lsp::Range {
15862 start: lsp::Position {
15863 line: 8,
15864 character: 1,
15865 },
15866 end: lsp::Position {
15867 line: 8,
15868 character: 1,
15869 },
15870 },
15871 new_text: "include \"$0\"".to_string(),
15872 })),
15873 sort_text: Some("40b67681include".to_string()),
15874 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15875 filter_text: Some("include".to_string()),
15876 insert_text: Some("include \"$0\"".to_string()),
15877 ..lsp::CompletionItem::default()
15878 }],
15879 })))
15880 });
15881 cx.update_editor(|editor, window, cx| {
15882 editor.show_completions(&ShowCompletions, window, cx);
15883 });
15884 cx.executor().run_until_parked();
15885 cx.update_editor(|editor, window, cx| {
15886 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15887 });
15888 cx.executor().run_until_parked();
15889 cx.assert_editor_state(
15890 "#ifndef BAR_H
15891#define BAR_H
15892
15893#include <stdbool.h>
15894
15895int fn_branch(bool do_branch1, bool do_branch2);
15896
15897#endif // BAR_H
15898#include \"ˇ\"",
15899 );
15900
15901 cx.lsp
15902 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15903 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15904 is_incomplete: true,
15905 item_defaults: None,
15906 items: vec![lsp::CompletionItem {
15907 kind: Some(lsp::CompletionItemKind::FILE),
15908 label: "AGL/".to_string(),
15909 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15910 range: lsp::Range {
15911 start: lsp::Position {
15912 line: 8,
15913 character: 10,
15914 },
15915 end: lsp::Position {
15916 line: 8,
15917 character: 11,
15918 },
15919 },
15920 new_text: "AGL/".to_string(),
15921 })),
15922 sort_text: Some("40b67681AGL/".to_string()),
15923 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15924 filter_text: Some("AGL/".to_string()),
15925 insert_text: Some("AGL/".to_string()),
15926 ..lsp::CompletionItem::default()
15927 }],
15928 })))
15929 });
15930 cx.update_editor(|editor, window, cx| {
15931 editor.show_completions(&ShowCompletions, window, cx);
15932 });
15933 cx.executor().run_until_parked();
15934 cx.update_editor(|editor, window, cx| {
15935 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15936 });
15937 cx.executor().run_until_parked();
15938 cx.assert_editor_state(
15939 r##"#ifndef BAR_H
15940#define BAR_H
15941
15942#include <stdbool.h>
15943
15944int fn_branch(bool do_branch1, bool do_branch2);
15945
15946#endif // BAR_H
15947#include "AGL/ˇ"##,
15948 );
15949
15950 cx.update_editor(|editor, window, cx| {
15951 editor.handle_input("\"", window, cx);
15952 });
15953 cx.executor().run_until_parked();
15954 cx.assert_editor_state(
15955 r##"#ifndef BAR_H
15956#define BAR_H
15957
15958#include <stdbool.h>
15959
15960int fn_branch(bool do_branch1, bool do_branch2);
15961
15962#endif // BAR_H
15963#include "AGL/"ˇ"##,
15964 );
15965}
15966
15967#[gpui::test]
15968async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15969 init_test(cx, |_| {});
15970
15971 let mut cx = EditorLspTestContext::new_rust(
15972 lsp::ServerCapabilities {
15973 completion_provider: Some(lsp::CompletionOptions {
15974 trigger_characters: Some(vec![".".to_string()]),
15975 resolve_provider: Some(true),
15976 ..Default::default()
15977 }),
15978 ..Default::default()
15979 },
15980 cx,
15981 )
15982 .await;
15983
15984 cx.set_state("fn main() { let a = 2ˇ; }");
15985 cx.simulate_keystroke(".");
15986 let completion_item = lsp::CompletionItem {
15987 label: "Some".into(),
15988 kind: Some(lsp::CompletionItemKind::SNIPPET),
15989 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15990 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15991 kind: lsp::MarkupKind::Markdown,
15992 value: "```rust\nSome(2)\n```".to_string(),
15993 })),
15994 deprecated: Some(false),
15995 sort_text: Some("Some".to_string()),
15996 filter_text: Some("Some".to_string()),
15997 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15998 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15999 range: lsp::Range {
16000 start: lsp::Position {
16001 line: 0,
16002 character: 22,
16003 },
16004 end: lsp::Position {
16005 line: 0,
16006 character: 22,
16007 },
16008 },
16009 new_text: "Some(2)".to_string(),
16010 })),
16011 additional_text_edits: Some(vec![lsp::TextEdit {
16012 range: lsp::Range {
16013 start: lsp::Position {
16014 line: 0,
16015 character: 20,
16016 },
16017 end: lsp::Position {
16018 line: 0,
16019 character: 22,
16020 },
16021 },
16022 new_text: "".to_string(),
16023 }]),
16024 ..Default::default()
16025 };
16026
16027 let closure_completion_item = completion_item.clone();
16028 let counter = Arc::new(AtomicUsize::new(0));
16029 let counter_clone = counter.clone();
16030 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16031 let task_completion_item = closure_completion_item.clone();
16032 counter_clone.fetch_add(1, atomic::Ordering::Release);
16033 async move {
16034 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16035 is_incomplete: true,
16036 item_defaults: None,
16037 items: vec![task_completion_item],
16038 })))
16039 }
16040 });
16041
16042 cx.condition(|editor, _| editor.context_menu_visible())
16043 .await;
16044 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16045 assert!(request.next().await.is_some());
16046 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16047
16048 cx.simulate_keystrokes("S o m");
16049 cx.condition(|editor, _| editor.context_menu_visible())
16050 .await;
16051 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16052 assert!(request.next().await.is_some());
16053 assert!(request.next().await.is_some());
16054 assert!(request.next().await.is_some());
16055 request.close();
16056 assert!(request.next().await.is_none());
16057 assert_eq!(
16058 counter.load(atomic::Ordering::Acquire),
16059 4,
16060 "With the completions menu open, only one LSP request should happen per input"
16061 );
16062}
16063
16064#[gpui::test]
16065async fn test_toggle_comment(cx: &mut TestAppContext) {
16066 init_test(cx, |_| {});
16067 let mut cx = EditorTestContext::new(cx).await;
16068 let language = Arc::new(Language::new(
16069 LanguageConfig {
16070 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16071 ..Default::default()
16072 },
16073 Some(tree_sitter_rust::LANGUAGE.into()),
16074 ));
16075 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16076
16077 // If multiple selections intersect a line, the line is only toggled once.
16078 cx.set_state(indoc! {"
16079 fn a() {
16080 «//b();
16081 ˇ»// «c();
16082 //ˇ» d();
16083 }
16084 "});
16085
16086 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16087
16088 cx.assert_editor_state(indoc! {"
16089 fn a() {
16090 «b();
16091 c();
16092 ˇ» d();
16093 }
16094 "});
16095
16096 // The comment prefix is inserted at the same column for every line in a
16097 // selection.
16098 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16099
16100 cx.assert_editor_state(indoc! {"
16101 fn a() {
16102 // «b();
16103 // c();
16104 ˇ»// d();
16105 }
16106 "});
16107
16108 // If a selection ends at the beginning of a line, that line is not toggled.
16109 cx.set_selections_state(indoc! {"
16110 fn a() {
16111 // b();
16112 «// c();
16113 ˇ» // d();
16114 }
16115 "});
16116
16117 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16118
16119 cx.assert_editor_state(indoc! {"
16120 fn a() {
16121 // b();
16122 «c();
16123 ˇ» // d();
16124 }
16125 "});
16126
16127 // If a selection span a single line and is empty, the line is toggled.
16128 cx.set_state(indoc! {"
16129 fn a() {
16130 a();
16131 b();
16132 ˇ
16133 }
16134 "});
16135
16136 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16137
16138 cx.assert_editor_state(indoc! {"
16139 fn a() {
16140 a();
16141 b();
16142 //•ˇ
16143 }
16144 "});
16145
16146 // If a selection span multiple lines, empty lines are not toggled.
16147 cx.set_state(indoc! {"
16148 fn a() {
16149 «a();
16150
16151 c();ˇ»
16152 }
16153 "});
16154
16155 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16156
16157 cx.assert_editor_state(indoc! {"
16158 fn a() {
16159 // «a();
16160
16161 // c();ˇ»
16162 }
16163 "});
16164
16165 // If a selection includes multiple comment prefixes, all lines are uncommented.
16166 cx.set_state(indoc! {"
16167 fn a() {
16168 «// a();
16169 /// b();
16170 //! c();ˇ»
16171 }
16172 "});
16173
16174 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16175
16176 cx.assert_editor_state(indoc! {"
16177 fn a() {
16178 «a();
16179 b();
16180 c();ˇ»
16181 }
16182 "});
16183}
16184
16185#[gpui::test]
16186async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16187 init_test(cx, |_| {});
16188 let mut cx = EditorTestContext::new(cx).await;
16189 let language = Arc::new(Language::new(
16190 LanguageConfig {
16191 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16192 ..Default::default()
16193 },
16194 Some(tree_sitter_rust::LANGUAGE.into()),
16195 ));
16196 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16197
16198 let toggle_comments = &ToggleComments {
16199 advance_downwards: false,
16200 ignore_indent: true,
16201 };
16202
16203 // If multiple selections intersect a line, the line is only toggled once.
16204 cx.set_state(indoc! {"
16205 fn a() {
16206 // «b();
16207 // c();
16208 // ˇ» d();
16209 }
16210 "});
16211
16212 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16213
16214 cx.assert_editor_state(indoc! {"
16215 fn a() {
16216 «b();
16217 c();
16218 ˇ» d();
16219 }
16220 "});
16221
16222 // The comment prefix is inserted at the beginning of each line
16223 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16224
16225 cx.assert_editor_state(indoc! {"
16226 fn a() {
16227 // «b();
16228 // c();
16229 // ˇ» d();
16230 }
16231 "});
16232
16233 // If a selection ends at the beginning of a line, that line is not toggled.
16234 cx.set_selections_state(indoc! {"
16235 fn a() {
16236 // b();
16237 // «c();
16238 ˇ»// d();
16239 }
16240 "});
16241
16242 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16243
16244 cx.assert_editor_state(indoc! {"
16245 fn a() {
16246 // b();
16247 «c();
16248 ˇ»// d();
16249 }
16250 "});
16251
16252 // If a selection span a single line and is empty, the line is toggled.
16253 cx.set_state(indoc! {"
16254 fn a() {
16255 a();
16256 b();
16257 ˇ
16258 }
16259 "});
16260
16261 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16262
16263 cx.assert_editor_state(indoc! {"
16264 fn a() {
16265 a();
16266 b();
16267 //ˇ
16268 }
16269 "});
16270
16271 // If a selection span multiple lines, empty lines are not toggled.
16272 cx.set_state(indoc! {"
16273 fn a() {
16274 «a();
16275
16276 c();ˇ»
16277 }
16278 "});
16279
16280 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16281
16282 cx.assert_editor_state(indoc! {"
16283 fn a() {
16284 // «a();
16285
16286 // c();ˇ»
16287 }
16288 "});
16289
16290 // If a selection includes multiple comment prefixes, all lines are uncommented.
16291 cx.set_state(indoc! {"
16292 fn a() {
16293 // «a();
16294 /// b();
16295 //! c();ˇ»
16296 }
16297 "});
16298
16299 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16300
16301 cx.assert_editor_state(indoc! {"
16302 fn a() {
16303 «a();
16304 b();
16305 c();ˇ»
16306 }
16307 "});
16308}
16309
16310#[gpui::test]
16311async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16312 init_test(cx, |_| {});
16313
16314 let language = Arc::new(Language::new(
16315 LanguageConfig {
16316 line_comments: vec!["// ".into()],
16317 ..Default::default()
16318 },
16319 Some(tree_sitter_rust::LANGUAGE.into()),
16320 ));
16321
16322 let mut cx = EditorTestContext::new(cx).await;
16323
16324 cx.language_registry().add(language.clone());
16325 cx.update_buffer(|buffer, cx| {
16326 buffer.set_language(Some(language), cx);
16327 });
16328
16329 let toggle_comments = &ToggleComments {
16330 advance_downwards: true,
16331 ignore_indent: false,
16332 };
16333
16334 // Single cursor on one line -> advance
16335 // Cursor moves horizontally 3 characters as well on non-blank line
16336 cx.set_state(indoc!(
16337 "fn a() {
16338 ˇdog();
16339 cat();
16340 }"
16341 ));
16342 cx.update_editor(|editor, window, cx| {
16343 editor.toggle_comments(toggle_comments, window, cx);
16344 });
16345 cx.assert_editor_state(indoc!(
16346 "fn a() {
16347 // dog();
16348 catˇ();
16349 }"
16350 ));
16351
16352 // Single selection on one line -> don't advance
16353 cx.set_state(indoc!(
16354 "fn a() {
16355 «dog()ˇ»;
16356 cat();
16357 }"
16358 ));
16359 cx.update_editor(|editor, window, cx| {
16360 editor.toggle_comments(toggle_comments, window, cx);
16361 });
16362 cx.assert_editor_state(indoc!(
16363 "fn a() {
16364 // «dog()ˇ»;
16365 cat();
16366 }"
16367 ));
16368
16369 // Multiple cursors on one line -> advance
16370 cx.set_state(indoc!(
16371 "fn a() {
16372 ˇdˇog();
16373 cat();
16374 }"
16375 ));
16376 cx.update_editor(|editor, window, cx| {
16377 editor.toggle_comments(toggle_comments, window, cx);
16378 });
16379 cx.assert_editor_state(indoc!(
16380 "fn a() {
16381 // dog();
16382 catˇ(ˇ);
16383 }"
16384 ));
16385
16386 // Multiple cursors on one line, with selection -> don't advance
16387 cx.set_state(indoc!(
16388 "fn a() {
16389 ˇdˇog«()ˇ»;
16390 cat();
16391 }"
16392 ));
16393 cx.update_editor(|editor, window, cx| {
16394 editor.toggle_comments(toggle_comments, window, cx);
16395 });
16396 cx.assert_editor_state(indoc!(
16397 "fn a() {
16398 // ˇdˇog«()ˇ»;
16399 cat();
16400 }"
16401 ));
16402
16403 // Single cursor on one line -> advance
16404 // Cursor moves to column 0 on blank line
16405 cx.set_state(indoc!(
16406 "fn a() {
16407 ˇdog();
16408
16409 cat();
16410 }"
16411 ));
16412 cx.update_editor(|editor, window, cx| {
16413 editor.toggle_comments(toggle_comments, window, cx);
16414 });
16415 cx.assert_editor_state(indoc!(
16416 "fn a() {
16417 // dog();
16418 ˇ
16419 cat();
16420 }"
16421 ));
16422
16423 // Single cursor on one line -> advance
16424 // Cursor starts and ends at column 0
16425 cx.set_state(indoc!(
16426 "fn a() {
16427 ˇ dog();
16428 cat();
16429 }"
16430 ));
16431 cx.update_editor(|editor, window, cx| {
16432 editor.toggle_comments(toggle_comments, window, cx);
16433 });
16434 cx.assert_editor_state(indoc!(
16435 "fn a() {
16436 // dog();
16437 ˇ cat();
16438 }"
16439 ));
16440}
16441
16442#[gpui::test]
16443async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16444 init_test(cx, |_| {});
16445
16446 let mut cx = EditorTestContext::new(cx).await;
16447
16448 let html_language = Arc::new(
16449 Language::new(
16450 LanguageConfig {
16451 name: "HTML".into(),
16452 block_comment: Some(BlockCommentConfig {
16453 start: "<!-- ".into(),
16454 prefix: "".into(),
16455 end: " -->".into(),
16456 tab_size: 0,
16457 }),
16458 ..Default::default()
16459 },
16460 Some(tree_sitter_html::LANGUAGE.into()),
16461 )
16462 .with_injection_query(
16463 r#"
16464 (script_element
16465 (raw_text) @injection.content
16466 (#set! injection.language "javascript"))
16467 "#,
16468 )
16469 .unwrap(),
16470 );
16471
16472 let javascript_language = Arc::new(Language::new(
16473 LanguageConfig {
16474 name: "JavaScript".into(),
16475 line_comments: vec!["// ".into()],
16476 ..Default::default()
16477 },
16478 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16479 ));
16480
16481 cx.language_registry().add(html_language.clone());
16482 cx.language_registry().add(javascript_language);
16483 cx.update_buffer(|buffer, cx| {
16484 buffer.set_language(Some(html_language), cx);
16485 });
16486
16487 // Toggle comments for empty selections
16488 cx.set_state(
16489 &r#"
16490 <p>A</p>ˇ
16491 <p>B</p>ˇ
16492 <p>C</p>ˇ
16493 "#
16494 .unindent(),
16495 );
16496 cx.update_editor(|editor, window, cx| {
16497 editor.toggle_comments(&ToggleComments::default(), window, cx)
16498 });
16499 cx.assert_editor_state(
16500 &r#"
16501 <!-- <p>A</p>ˇ -->
16502 <!-- <p>B</p>ˇ -->
16503 <!-- <p>C</p>ˇ -->
16504 "#
16505 .unindent(),
16506 );
16507 cx.update_editor(|editor, window, cx| {
16508 editor.toggle_comments(&ToggleComments::default(), window, cx)
16509 });
16510 cx.assert_editor_state(
16511 &r#"
16512 <p>A</p>ˇ
16513 <p>B</p>ˇ
16514 <p>C</p>ˇ
16515 "#
16516 .unindent(),
16517 );
16518
16519 // Toggle comments for mixture of empty and non-empty selections, where
16520 // multiple selections occupy a given line.
16521 cx.set_state(
16522 &r#"
16523 <p>A«</p>
16524 <p>ˇ»B</p>ˇ
16525 <p>C«</p>
16526 <p>ˇ»D</p>ˇ
16527 "#
16528 .unindent(),
16529 );
16530
16531 cx.update_editor(|editor, window, cx| {
16532 editor.toggle_comments(&ToggleComments::default(), window, cx)
16533 });
16534 cx.assert_editor_state(
16535 &r#"
16536 <!-- <p>A«</p>
16537 <p>ˇ»B</p>ˇ -->
16538 <!-- <p>C«</p>
16539 <p>ˇ»D</p>ˇ -->
16540 "#
16541 .unindent(),
16542 );
16543 cx.update_editor(|editor, window, cx| {
16544 editor.toggle_comments(&ToggleComments::default(), window, cx)
16545 });
16546 cx.assert_editor_state(
16547 &r#"
16548 <p>A«</p>
16549 <p>ˇ»B</p>ˇ
16550 <p>C«</p>
16551 <p>ˇ»D</p>ˇ
16552 "#
16553 .unindent(),
16554 );
16555
16556 // Toggle comments when different languages are active for different
16557 // selections.
16558 cx.set_state(
16559 &r#"
16560 ˇ<script>
16561 ˇvar x = new Y();
16562 ˇ</script>
16563 "#
16564 .unindent(),
16565 );
16566 cx.executor().run_until_parked();
16567 cx.update_editor(|editor, window, cx| {
16568 editor.toggle_comments(&ToggleComments::default(), window, cx)
16569 });
16570 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16571 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16572 cx.assert_editor_state(
16573 &r#"
16574 <!-- ˇ<script> -->
16575 // ˇvar x = new Y();
16576 <!-- ˇ</script> -->
16577 "#
16578 .unindent(),
16579 );
16580}
16581
16582#[gpui::test]
16583fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16584 init_test(cx, |_| {});
16585
16586 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16587 let multibuffer = cx.new(|cx| {
16588 let mut multibuffer = MultiBuffer::new(ReadWrite);
16589 multibuffer.push_excerpts(
16590 buffer.clone(),
16591 [
16592 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16593 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16594 ],
16595 cx,
16596 );
16597 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16598 multibuffer
16599 });
16600
16601 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16602 editor.update_in(cx, |editor, window, cx| {
16603 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16604 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16605 s.select_ranges([
16606 Point::new(0, 0)..Point::new(0, 0),
16607 Point::new(1, 0)..Point::new(1, 0),
16608 ])
16609 });
16610
16611 editor.handle_input("X", window, cx);
16612 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16613 assert_eq!(
16614 editor.selections.ranges(&editor.display_snapshot(cx)),
16615 [
16616 Point::new(0, 1)..Point::new(0, 1),
16617 Point::new(1, 1)..Point::new(1, 1),
16618 ]
16619 );
16620
16621 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16622 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16623 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16624 });
16625 editor.backspace(&Default::default(), window, cx);
16626 assert_eq!(editor.text(cx), "Xa\nbbb");
16627 assert_eq!(
16628 editor.selections.ranges(&editor.display_snapshot(cx)),
16629 [Point::new(1, 0)..Point::new(1, 0)]
16630 );
16631
16632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16633 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16634 });
16635 editor.backspace(&Default::default(), window, cx);
16636 assert_eq!(editor.text(cx), "X\nbb");
16637 assert_eq!(
16638 editor.selections.ranges(&editor.display_snapshot(cx)),
16639 [Point::new(0, 1)..Point::new(0, 1)]
16640 );
16641 });
16642}
16643
16644#[gpui::test]
16645fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16646 init_test(cx, |_| {});
16647
16648 let markers = vec![('[', ']').into(), ('(', ')').into()];
16649 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16650 indoc! {"
16651 [aaaa
16652 (bbbb]
16653 cccc)",
16654 },
16655 markers.clone(),
16656 );
16657 let excerpt_ranges = markers.into_iter().map(|marker| {
16658 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16659 ExcerptRange::new(context)
16660 });
16661 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16662 let multibuffer = cx.new(|cx| {
16663 let mut multibuffer = MultiBuffer::new(ReadWrite);
16664 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16665 multibuffer
16666 });
16667
16668 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16669 editor.update_in(cx, |editor, window, cx| {
16670 let (expected_text, selection_ranges) = marked_text_ranges(
16671 indoc! {"
16672 aaaa
16673 bˇbbb
16674 bˇbbˇb
16675 cccc"
16676 },
16677 true,
16678 );
16679 assert_eq!(editor.text(cx), expected_text);
16680 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16681 s.select_ranges(
16682 selection_ranges
16683 .iter()
16684 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16685 )
16686 });
16687
16688 editor.handle_input("X", window, cx);
16689
16690 let (expected_text, expected_selections) = marked_text_ranges(
16691 indoc! {"
16692 aaaa
16693 bXˇbbXb
16694 bXˇbbXˇb
16695 cccc"
16696 },
16697 false,
16698 );
16699 assert_eq!(editor.text(cx), expected_text);
16700 assert_eq!(
16701 editor.selections.ranges(&editor.display_snapshot(cx)),
16702 expected_selections
16703 .iter()
16704 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16705 .collect::<Vec<_>>()
16706 );
16707
16708 editor.newline(&Newline, window, cx);
16709 let (expected_text, expected_selections) = marked_text_ranges(
16710 indoc! {"
16711 aaaa
16712 bX
16713 ˇbbX
16714 b
16715 bX
16716 ˇbbX
16717 ˇb
16718 cccc"
16719 },
16720 false,
16721 );
16722 assert_eq!(editor.text(cx), expected_text);
16723 assert_eq!(
16724 editor.selections.ranges(&editor.display_snapshot(cx)),
16725 expected_selections
16726 .iter()
16727 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16728 .collect::<Vec<_>>()
16729 );
16730 });
16731}
16732
16733#[gpui::test]
16734fn test_refresh_selections(cx: &mut TestAppContext) {
16735 init_test(cx, |_| {});
16736
16737 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16738 let mut excerpt1_id = None;
16739 let multibuffer = cx.new(|cx| {
16740 let mut multibuffer = MultiBuffer::new(ReadWrite);
16741 excerpt1_id = multibuffer
16742 .push_excerpts(
16743 buffer.clone(),
16744 [
16745 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16746 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16747 ],
16748 cx,
16749 )
16750 .into_iter()
16751 .next();
16752 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16753 multibuffer
16754 });
16755
16756 let editor = cx.add_window(|window, cx| {
16757 let mut editor = build_editor(multibuffer.clone(), window, cx);
16758 let snapshot = editor.snapshot(window, cx);
16759 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16760 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16761 });
16762 editor.begin_selection(
16763 Point::new(2, 1).to_display_point(&snapshot),
16764 true,
16765 1,
16766 window,
16767 cx,
16768 );
16769 assert_eq!(
16770 editor.selections.ranges(&editor.display_snapshot(cx)),
16771 [
16772 Point::new(1, 3)..Point::new(1, 3),
16773 Point::new(2, 1)..Point::new(2, 1),
16774 ]
16775 );
16776 editor
16777 });
16778
16779 // Refreshing selections is a no-op when excerpts haven't changed.
16780 _ = editor.update(cx, |editor, window, cx| {
16781 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16782 assert_eq!(
16783 editor.selections.ranges(&editor.display_snapshot(cx)),
16784 [
16785 Point::new(1, 3)..Point::new(1, 3),
16786 Point::new(2, 1)..Point::new(2, 1),
16787 ]
16788 );
16789 });
16790
16791 multibuffer.update(cx, |multibuffer, cx| {
16792 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16793 });
16794 _ = editor.update(cx, |editor, window, cx| {
16795 // Removing an excerpt causes the first selection to become degenerate.
16796 assert_eq!(
16797 editor.selections.ranges(&editor.display_snapshot(cx)),
16798 [
16799 Point::new(0, 0)..Point::new(0, 0),
16800 Point::new(0, 1)..Point::new(0, 1)
16801 ]
16802 );
16803
16804 // Refreshing selections will relocate the first selection to the original buffer
16805 // location.
16806 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16807 assert_eq!(
16808 editor.selections.ranges(&editor.display_snapshot(cx)),
16809 [
16810 Point::new(0, 1)..Point::new(0, 1),
16811 Point::new(0, 3)..Point::new(0, 3)
16812 ]
16813 );
16814 assert!(editor.selections.pending_anchor().is_some());
16815 });
16816}
16817
16818#[gpui::test]
16819fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16820 init_test(cx, |_| {});
16821
16822 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16823 let mut excerpt1_id = None;
16824 let multibuffer = cx.new(|cx| {
16825 let mut multibuffer = MultiBuffer::new(ReadWrite);
16826 excerpt1_id = multibuffer
16827 .push_excerpts(
16828 buffer.clone(),
16829 [
16830 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16831 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16832 ],
16833 cx,
16834 )
16835 .into_iter()
16836 .next();
16837 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16838 multibuffer
16839 });
16840
16841 let editor = cx.add_window(|window, cx| {
16842 let mut editor = build_editor(multibuffer.clone(), window, cx);
16843 let snapshot = editor.snapshot(window, cx);
16844 editor.begin_selection(
16845 Point::new(1, 3).to_display_point(&snapshot),
16846 false,
16847 1,
16848 window,
16849 cx,
16850 );
16851 assert_eq!(
16852 editor.selections.ranges(&editor.display_snapshot(cx)),
16853 [Point::new(1, 3)..Point::new(1, 3)]
16854 );
16855 editor
16856 });
16857
16858 multibuffer.update(cx, |multibuffer, cx| {
16859 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16860 });
16861 _ = editor.update(cx, |editor, window, cx| {
16862 assert_eq!(
16863 editor.selections.ranges(&editor.display_snapshot(cx)),
16864 [Point::new(0, 0)..Point::new(0, 0)]
16865 );
16866
16867 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16868 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16869 assert_eq!(
16870 editor.selections.ranges(&editor.display_snapshot(cx)),
16871 [Point::new(0, 3)..Point::new(0, 3)]
16872 );
16873 assert!(editor.selections.pending_anchor().is_some());
16874 });
16875}
16876
16877#[gpui::test]
16878async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16879 init_test(cx, |_| {});
16880
16881 let language = Arc::new(
16882 Language::new(
16883 LanguageConfig {
16884 brackets: BracketPairConfig {
16885 pairs: vec![
16886 BracketPair {
16887 start: "{".to_string(),
16888 end: "}".to_string(),
16889 close: true,
16890 surround: true,
16891 newline: true,
16892 },
16893 BracketPair {
16894 start: "/* ".to_string(),
16895 end: " */".to_string(),
16896 close: true,
16897 surround: true,
16898 newline: true,
16899 },
16900 ],
16901 ..Default::default()
16902 },
16903 ..Default::default()
16904 },
16905 Some(tree_sitter_rust::LANGUAGE.into()),
16906 )
16907 .with_indents_query("")
16908 .unwrap(),
16909 );
16910
16911 let text = concat!(
16912 "{ }\n", //
16913 " x\n", //
16914 " /* */\n", //
16915 "x\n", //
16916 "{{} }\n", //
16917 );
16918
16919 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16920 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16921 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16922 editor
16923 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16924 .await;
16925
16926 editor.update_in(cx, |editor, window, cx| {
16927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16928 s.select_display_ranges([
16929 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16930 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16931 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16932 ])
16933 });
16934 editor.newline(&Newline, window, cx);
16935
16936 assert_eq!(
16937 editor.buffer().read(cx).read(cx).text(),
16938 concat!(
16939 "{ \n", // Suppress rustfmt
16940 "\n", //
16941 "}\n", //
16942 " x\n", //
16943 " /* \n", //
16944 " \n", //
16945 " */\n", //
16946 "x\n", //
16947 "{{} \n", //
16948 "}\n", //
16949 )
16950 );
16951 });
16952}
16953
16954#[gpui::test]
16955fn test_highlighted_ranges(cx: &mut TestAppContext) {
16956 init_test(cx, |_| {});
16957
16958 let editor = cx.add_window(|window, cx| {
16959 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16960 build_editor(buffer, window, cx)
16961 });
16962
16963 _ = editor.update(cx, |editor, window, cx| {
16964 struct Type1;
16965 struct Type2;
16966
16967 let buffer = editor.buffer.read(cx).snapshot(cx);
16968
16969 let anchor_range =
16970 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16971
16972 editor.highlight_background::<Type1>(
16973 &[
16974 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16975 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16976 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16977 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16978 ],
16979 |_| Hsla::red(),
16980 cx,
16981 );
16982 editor.highlight_background::<Type2>(
16983 &[
16984 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16985 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16986 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16987 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16988 ],
16989 |_| Hsla::green(),
16990 cx,
16991 );
16992
16993 let snapshot = editor.snapshot(window, cx);
16994 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16995 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16996 &snapshot,
16997 cx.theme(),
16998 );
16999 assert_eq!(
17000 highlighted_ranges,
17001 &[
17002 (
17003 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17004 Hsla::green(),
17005 ),
17006 (
17007 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17008 Hsla::red(),
17009 ),
17010 (
17011 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17012 Hsla::green(),
17013 ),
17014 (
17015 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17016 Hsla::red(),
17017 ),
17018 ]
17019 );
17020 assert_eq!(
17021 editor.sorted_background_highlights_in_range(
17022 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17023 &snapshot,
17024 cx.theme(),
17025 ),
17026 &[(
17027 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17028 Hsla::red(),
17029 )]
17030 );
17031 });
17032}
17033
17034#[gpui::test]
17035async fn test_following(cx: &mut TestAppContext) {
17036 init_test(cx, |_| {});
17037
17038 let fs = FakeFs::new(cx.executor());
17039 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17040
17041 let buffer = project.update(cx, |project, cx| {
17042 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17043 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17044 });
17045 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17046 let follower = cx.update(|cx| {
17047 cx.open_window(
17048 WindowOptions {
17049 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17050 gpui::Point::new(px(0.), px(0.)),
17051 gpui::Point::new(px(10.), px(80.)),
17052 ))),
17053 ..Default::default()
17054 },
17055 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17056 )
17057 .unwrap()
17058 });
17059
17060 let is_still_following = Rc::new(RefCell::new(true));
17061 let follower_edit_event_count = Rc::new(RefCell::new(0));
17062 let pending_update = Rc::new(RefCell::new(None));
17063 let leader_entity = leader.root(cx).unwrap();
17064 let follower_entity = follower.root(cx).unwrap();
17065 _ = follower.update(cx, {
17066 let update = pending_update.clone();
17067 let is_still_following = is_still_following.clone();
17068 let follower_edit_event_count = follower_edit_event_count.clone();
17069 |_, window, cx| {
17070 cx.subscribe_in(
17071 &leader_entity,
17072 window,
17073 move |_, leader, event, window, cx| {
17074 leader.read(cx).add_event_to_update_proto(
17075 event,
17076 &mut update.borrow_mut(),
17077 window,
17078 cx,
17079 );
17080 },
17081 )
17082 .detach();
17083
17084 cx.subscribe_in(
17085 &follower_entity,
17086 window,
17087 move |_, _, event: &EditorEvent, _window, _cx| {
17088 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17089 *is_still_following.borrow_mut() = false;
17090 }
17091
17092 if let EditorEvent::BufferEdited = event {
17093 *follower_edit_event_count.borrow_mut() += 1;
17094 }
17095 },
17096 )
17097 .detach();
17098 }
17099 });
17100
17101 // Update the selections only
17102 _ = leader.update(cx, |leader, window, cx| {
17103 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17104 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17105 });
17106 });
17107 follower
17108 .update(cx, |follower, window, cx| {
17109 follower.apply_update_proto(
17110 &project,
17111 pending_update.borrow_mut().take().unwrap(),
17112 window,
17113 cx,
17114 )
17115 })
17116 .unwrap()
17117 .await
17118 .unwrap();
17119 _ = follower.update(cx, |follower, _, cx| {
17120 assert_eq!(
17121 follower.selections.ranges(&follower.display_snapshot(cx)),
17122 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17123 );
17124 });
17125 assert!(*is_still_following.borrow());
17126 assert_eq!(*follower_edit_event_count.borrow(), 0);
17127
17128 // Update the scroll position only
17129 _ = leader.update(cx, |leader, window, cx| {
17130 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17131 });
17132 follower
17133 .update(cx, |follower, window, cx| {
17134 follower.apply_update_proto(
17135 &project,
17136 pending_update.borrow_mut().take().unwrap(),
17137 window,
17138 cx,
17139 )
17140 })
17141 .unwrap()
17142 .await
17143 .unwrap();
17144 assert_eq!(
17145 follower
17146 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17147 .unwrap(),
17148 gpui::Point::new(1.5, 3.5)
17149 );
17150 assert!(*is_still_following.borrow());
17151 assert_eq!(*follower_edit_event_count.borrow(), 0);
17152
17153 // Update the selections and scroll position. The follower's scroll position is updated
17154 // via autoscroll, not via the leader's exact scroll position.
17155 _ = leader.update(cx, |leader, window, cx| {
17156 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17157 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17158 });
17159 leader.request_autoscroll(Autoscroll::newest(), cx);
17160 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17161 });
17162 follower
17163 .update(cx, |follower, window, cx| {
17164 follower.apply_update_proto(
17165 &project,
17166 pending_update.borrow_mut().take().unwrap(),
17167 window,
17168 cx,
17169 )
17170 })
17171 .unwrap()
17172 .await
17173 .unwrap();
17174 _ = follower.update(cx, |follower, _, cx| {
17175 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17176 assert_eq!(
17177 follower.selections.ranges(&follower.display_snapshot(cx)),
17178 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17179 );
17180 });
17181 assert!(*is_still_following.borrow());
17182
17183 // Creating a pending selection that precedes another selection
17184 _ = leader.update(cx, |leader, window, cx| {
17185 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17186 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17187 });
17188 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17189 });
17190 follower
17191 .update(cx, |follower, window, cx| {
17192 follower.apply_update_proto(
17193 &project,
17194 pending_update.borrow_mut().take().unwrap(),
17195 window,
17196 cx,
17197 )
17198 })
17199 .unwrap()
17200 .await
17201 .unwrap();
17202 _ = follower.update(cx, |follower, _, cx| {
17203 assert_eq!(
17204 follower.selections.ranges(&follower.display_snapshot(cx)),
17205 vec![
17206 MultiBufferOffset(0)..MultiBufferOffset(0),
17207 MultiBufferOffset(1)..MultiBufferOffset(1)
17208 ]
17209 );
17210 });
17211 assert!(*is_still_following.borrow());
17212
17213 // Extend the pending selection so that it surrounds another selection
17214 _ = leader.update(cx, |leader, window, cx| {
17215 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17216 });
17217 follower
17218 .update(cx, |follower, window, cx| {
17219 follower.apply_update_proto(
17220 &project,
17221 pending_update.borrow_mut().take().unwrap(),
17222 window,
17223 cx,
17224 )
17225 })
17226 .unwrap()
17227 .await
17228 .unwrap();
17229 _ = follower.update(cx, |follower, _, cx| {
17230 assert_eq!(
17231 follower.selections.ranges(&follower.display_snapshot(cx)),
17232 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17233 );
17234 });
17235
17236 // Scrolling locally breaks the follow
17237 _ = follower.update(cx, |follower, window, cx| {
17238 let top_anchor = follower
17239 .buffer()
17240 .read(cx)
17241 .read(cx)
17242 .anchor_after(MultiBufferOffset(0));
17243 follower.set_scroll_anchor(
17244 ScrollAnchor {
17245 anchor: top_anchor,
17246 offset: gpui::Point::new(0.0, 0.5),
17247 },
17248 window,
17249 cx,
17250 );
17251 });
17252 assert!(!(*is_still_following.borrow()));
17253}
17254
17255#[gpui::test]
17256async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17257 init_test(cx, |_| {});
17258
17259 let fs = FakeFs::new(cx.executor());
17260 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17261 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17262 let pane = workspace
17263 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17264 .unwrap();
17265
17266 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17267
17268 let leader = pane.update_in(cx, |_, window, cx| {
17269 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17270 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17271 });
17272
17273 // Start following the editor when it has no excerpts.
17274 let mut state_message =
17275 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17276 let workspace_entity = workspace.root(cx).unwrap();
17277 let follower_1 = cx
17278 .update_window(*workspace.deref(), |_, window, cx| {
17279 Editor::from_state_proto(
17280 workspace_entity,
17281 ViewId {
17282 creator: CollaboratorId::PeerId(PeerId::default()),
17283 id: 0,
17284 },
17285 &mut state_message,
17286 window,
17287 cx,
17288 )
17289 })
17290 .unwrap()
17291 .unwrap()
17292 .await
17293 .unwrap();
17294
17295 let update_message = Rc::new(RefCell::new(None));
17296 follower_1.update_in(cx, {
17297 let update = update_message.clone();
17298 |_, window, cx| {
17299 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17300 leader.read(cx).add_event_to_update_proto(
17301 event,
17302 &mut update.borrow_mut(),
17303 window,
17304 cx,
17305 );
17306 })
17307 .detach();
17308 }
17309 });
17310
17311 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17312 (
17313 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17314 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17315 )
17316 });
17317
17318 // Insert some excerpts.
17319 leader.update(cx, |leader, cx| {
17320 leader.buffer.update(cx, |multibuffer, cx| {
17321 multibuffer.set_excerpts_for_path(
17322 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17323 buffer_1.clone(),
17324 vec![
17325 Point::row_range(0..3),
17326 Point::row_range(1..6),
17327 Point::row_range(12..15),
17328 ],
17329 0,
17330 cx,
17331 );
17332 multibuffer.set_excerpts_for_path(
17333 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17334 buffer_2.clone(),
17335 vec![Point::row_range(0..6), Point::row_range(8..12)],
17336 0,
17337 cx,
17338 );
17339 });
17340 });
17341
17342 // Apply the update of adding the excerpts.
17343 follower_1
17344 .update_in(cx, |follower, window, cx| {
17345 follower.apply_update_proto(
17346 &project,
17347 update_message.borrow().clone().unwrap(),
17348 window,
17349 cx,
17350 )
17351 })
17352 .await
17353 .unwrap();
17354 assert_eq!(
17355 follower_1.update(cx, |editor, cx| editor.text(cx)),
17356 leader.update(cx, |editor, cx| editor.text(cx))
17357 );
17358 update_message.borrow_mut().take();
17359
17360 // Start following separately after it already has excerpts.
17361 let mut state_message =
17362 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17363 let workspace_entity = workspace.root(cx).unwrap();
17364 let follower_2 = cx
17365 .update_window(*workspace.deref(), |_, window, cx| {
17366 Editor::from_state_proto(
17367 workspace_entity,
17368 ViewId {
17369 creator: CollaboratorId::PeerId(PeerId::default()),
17370 id: 0,
17371 },
17372 &mut state_message,
17373 window,
17374 cx,
17375 )
17376 })
17377 .unwrap()
17378 .unwrap()
17379 .await
17380 .unwrap();
17381 assert_eq!(
17382 follower_2.update(cx, |editor, cx| editor.text(cx)),
17383 leader.update(cx, |editor, cx| editor.text(cx))
17384 );
17385
17386 // Remove some excerpts.
17387 leader.update(cx, |leader, cx| {
17388 leader.buffer.update(cx, |multibuffer, cx| {
17389 let excerpt_ids = multibuffer.excerpt_ids();
17390 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17391 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17392 });
17393 });
17394
17395 // Apply the update of removing the excerpts.
17396 follower_1
17397 .update_in(cx, |follower, window, cx| {
17398 follower.apply_update_proto(
17399 &project,
17400 update_message.borrow().clone().unwrap(),
17401 window,
17402 cx,
17403 )
17404 })
17405 .await
17406 .unwrap();
17407 follower_2
17408 .update_in(cx, |follower, window, cx| {
17409 follower.apply_update_proto(
17410 &project,
17411 update_message.borrow().clone().unwrap(),
17412 window,
17413 cx,
17414 )
17415 })
17416 .await
17417 .unwrap();
17418 update_message.borrow_mut().take();
17419 assert_eq!(
17420 follower_1.update(cx, |editor, cx| editor.text(cx)),
17421 leader.update(cx, |editor, cx| editor.text(cx))
17422 );
17423}
17424
17425#[gpui::test]
17426async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17427 init_test(cx, |_| {});
17428
17429 let mut cx = EditorTestContext::new(cx).await;
17430 let lsp_store =
17431 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17432
17433 cx.set_state(indoc! {"
17434 ˇfn func(abc def: i32) -> u32 {
17435 }
17436 "});
17437
17438 cx.update(|_, cx| {
17439 lsp_store.update(cx, |lsp_store, cx| {
17440 lsp_store
17441 .update_diagnostics(
17442 LanguageServerId(0),
17443 lsp::PublishDiagnosticsParams {
17444 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17445 version: None,
17446 diagnostics: vec![
17447 lsp::Diagnostic {
17448 range: lsp::Range::new(
17449 lsp::Position::new(0, 11),
17450 lsp::Position::new(0, 12),
17451 ),
17452 severity: Some(lsp::DiagnosticSeverity::ERROR),
17453 ..Default::default()
17454 },
17455 lsp::Diagnostic {
17456 range: lsp::Range::new(
17457 lsp::Position::new(0, 12),
17458 lsp::Position::new(0, 15),
17459 ),
17460 severity: Some(lsp::DiagnosticSeverity::ERROR),
17461 ..Default::default()
17462 },
17463 lsp::Diagnostic {
17464 range: lsp::Range::new(
17465 lsp::Position::new(0, 25),
17466 lsp::Position::new(0, 28),
17467 ),
17468 severity: Some(lsp::DiagnosticSeverity::ERROR),
17469 ..Default::default()
17470 },
17471 ],
17472 },
17473 None,
17474 DiagnosticSourceKind::Pushed,
17475 &[],
17476 cx,
17477 )
17478 .unwrap()
17479 });
17480 });
17481
17482 executor.run_until_parked();
17483
17484 cx.update_editor(|editor, window, cx| {
17485 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17486 });
17487
17488 cx.assert_editor_state(indoc! {"
17489 fn func(abc def: i32) -> ˇu32 {
17490 }
17491 "});
17492
17493 cx.update_editor(|editor, window, cx| {
17494 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17495 });
17496
17497 cx.assert_editor_state(indoc! {"
17498 fn func(abc ˇdef: i32) -> u32 {
17499 }
17500 "});
17501
17502 cx.update_editor(|editor, window, cx| {
17503 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17504 });
17505
17506 cx.assert_editor_state(indoc! {"
17507 fn func(abcˇ def: i32) -> u32 {
17508 }
17509 "});
17510
17511 cx.update_editor(|editor, window, cx| {
17512 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17513 });
17514
17515 cx.assert_editor_state(indoc! {"
17516 fn func(abc def: i32) -> ˇu32 {
17517 }
17518 "});
17519}
17520
17521#[gpui::test]
17522async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17523 init_test(cx, |_| {});
17524
17525 let mut cx = EditorTestContext::new(cx).await;
17526
17527 let diff_base = r#"
17528 use some::mod;
17529
17530 const A: u32 = 42;
17531
17532 fn main() {
17533 println!("hello");
17534
17535 println!("world");
17536 }
17537 "#
17538 .unindent();
17539
17540 // Edits are modified, removed, modified, added
17541 cx.set_state(
17542 &r#"
17543 use some::modified;
17544
17545 ˇ
17546 fn main() {
17547 println!("hello there");
17548
17549 println!("around the");
17550 println!("world");
17551 }
17552 "#
17553 .unindent(),
17554 );
17555
17556 cx.set_head_text(&diff_base);
17557 executor.run_until_parked();
17558
17559 cx.update_editor(|editor, window, cx| {
17560 //Wrap around the bottom of the buffer
17561 for _ in 0..3 {
17562 editor.go_to_next_hunk(&GoToHunk, window, cx);
17563 }
17564 });
17565
17566 cx.assert_editor_state(
17567 &r#"
17568 ˇuse some::modified;
17569
17570
17571 fn main() {
17572 println!("hello there");
17573
17574 println!("around the");
17575 println!("world");
17576 }
17577 "#
17578 .unindent(),
17579 );
17580
17581 cx.update_editor(|editor, window, cx| {
17582 //Wrap around the top of the buffer
17583 for _ in 0..2 {
17584 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17585 }
17586 });
17587
17588 cx.assert_editor_state(
17589 &r#"
17590 use some::modified;
17591
17592
17593 fn main() {
17594 ˇ println!("hello there");
17595
17596 println!("around the");
17597 println!("world");
17598 }
17599 "#
17600 .unindent(),
17601 );
17602
17603 cx.update_editor(|editor, window, cx| {
17604 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17605 });
17606
17607 cx.assert_editor_state(
17608 &r#"
17609 use some::modified;
17610
17611 ˇ
17612 fn main() {
17613 println!("hello there");
17614
17615 println!("around the");
17616 println!("world");
17617 }
17618 "#
17619 .unindent(),
17620 );
17621
17622 cx.update_editor(|editor, window, cx| {
17623 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17624 });
17625
17626 cx.assert_editor_state(
17627 &r#"
17628 ˇuse some::modified;
17629
17630
17631 fn main() {
17632 println!("hello there");
17633
17634 println!("around the");
17635 println!("world");
17636 }
17637 "#
17638 .unindent(),
17639 );
17640
17641 cx.update_editor(|editor, window, cx| {
17642 for _ in 0..2 {
17643 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17644 }
17645 });
17646
17647 cx.assert_editor_state(
17648 &r#"
17649 use some::modified;
17650
17651
17652 fn main() {
17653 ˇ println!("hello there");
17654
17655 println!("around the");
17656 println!("world");
17657 }
17658 "#
17659 .unindent(),
17660 );
17661
17662 cx.update_editor(|editor, window, cx| {
17663 editor.fold(&Fold, window, cx);
17664 });
17665
17666 cx.update_editor(|editor, window, cx| {
17667 editor.go_to_next_hunk(&GoToHunk, window, cx);
17668 });
17669
17670 cx.assert_editor_state(
17671 &r#"
17672 ˇuse some::modified;
17673
17674
17675 fn main() {
17676 println!("hello there");
17677
17678 println!("around the");
17679 println!("world");
17680 }
17681 "#
17682 .unindent(),
17683 );
17684}
17685
17686#[test]
17687fn test_split_words() {
17688 fn split(text: &str) -> Vec<&str> {
17689 split_words(text).collect()
17690 }
17691
17692 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17693 assert_eq!(split("hello_world"), &["hello_", "world"]);
17694 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17695 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17696 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17697 assert_eq!(split("helloworld"), &["helloworld"]);
17698
17699 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17700}
17701
17702#[test]
17703fn test_split_words_for_snippet_prefix() {
17704 fn split(text: &str) -> Vec<&str> {
17705 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17706 }
17707
17708 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17709 assert_eq!(split("hello_world"), &["hello_world"]);
17710 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17711 assert_eq!(split("Hello_World"), &["Hello_World"]);
17712 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17713 assert_eq!(split("helloworld"), &["helloworld"]);
17714 assert_eq!(
17715 split("this@is!@#$^many . symbols"),
17716 &[
17717 "symbols",
17718 " symbols",
17719 ". symbols",
17720 " . symbols",
17721 " . symbols",
17722 " . symbols",
17723 "many . symbols",
17724 "^many . symbols",
17725 "$^many . symbols",
17726 "#$^many . symbols",
17727 "@#$^many . symbols",
17728 "!@#$^many . symbols",
17729 "is!@#$^many . symbols",
17730 "@is!@#$^many . symbols",
17731 "this@is!@#$^many . symbols",
17732 ],
17733 );
17734 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17735}
17736
17737#[gpui::test]
17738async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17739 init_test(cx, |_| {});
17740
17741 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17742
17743 #[track_caller]
17744 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17745 let _state_context = cx.set_state(before);
17746 cx.run_until_parked();
17747 cx.update_editor(|editor, window, cx| {
17748 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17749 });
17750 cx.run_until_parked();
17751 cx.assert_editor_state(after);
17752 }
17753
17754 // Outside bracket jumps to outside of matching bracket
17755 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17756 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17757
17758 // Inside bracket jumps to inside of matching bracket
17759 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17760 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17761
17762 // When outside a bracket and inside, favor jumping to the inside bracket
17763 assert(
17764 "console.log('foo', [1, 2, 3]ˇ);",
17765 "console.log('foo', ˇ[1, 2, 3]);",
17766 &mut cx,
17767 );
17768 assert(
17769 "console.log(ˇ'foo', [1, 2, 3]);",
17770 "console.log('foo'ˇ, [1, 2, 3]);",
17771 &mut cx,
17772 );
17773
17774 // Bias forward if two options are equally likely
17775 assert(
17776 "let result = curried_fun()ˇ();",
17777 "let result = curried_fun()()ˇ;",
17778 &mut cx,
17779 );
17780
17781 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17782 assert(
17783 indoc! {"
17784 function test() {
17785 console.log('test')ˇ
17786 }"},
17787 indoc! {"
17788 function test() {
17789 console.logˇ('test')
17790 }"},
17791 &mut cx,
17792 );
17793}
17794
17795#[gpui::test]
17796async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
17797 init_test(cx, |_| {});
17798 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
17799 language_registry.add(markdown_lang());
17800 language_registry.add(rust_lang());
17801 let buffer = cx.new(|cx| {
17802 let mut buffer = language::Buffer::local(
17803 indoc! {"
17804 ```rs
17805 impl Worktree {
17806 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17807 }
17808 }
17809 ```
17810 "},
17811 cx,
17812 );
17813 buffer.set_language_registry(language_registry.clone());
17814 buffer.set_language(Some(markdown_lang()), cx);
17815 buffer
17816 });
17817 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17818 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17819 cx.executor().run_until_parked();
17820 _ = editor.update(cx, |editor, window, cx| {
17821 // Case 1: Test outer enclosing brackets
17822 select_ranges(
17823 editor,
17824 &indoc! {"
17825 ```rs
17826 impl Worktree {
17827 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17828 }
17829 }ˇ
17830 ```
17831 "},
17832 window,
17833 cx,
17834 );
17835 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17836 assert_text_with_selections(
17837 editor,
17838 &indoc! {"
17839 ```rs
17840 impl Worktree ˇ{
17841 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17842 }
17843 }
17844 ```
17845 "},
17846 cx,
17847 );
17848 // Case 2: Test inner enclosing brackets
17849 select_ranges(
17850 editor,
17851 &indoc! {"
17852 ```rs
17853 impl Worktree {
17854 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
17855 }ˇ
17856 }
17857 ```
17858 "},
17859 window,
17860 cx,
17861 );
17862 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
17863 assert_text_with_selections(
17864 editor,
17865 &indoc! {"
17866 ```rs
17867 impl Worktree {
17868 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
17869 }
17870 }
17871 ```
17872 "},
17873 cx,
17874 );
17875 });
17876}
17877
17878#[gpui::test]
17879async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17880 init_test(cx, |_| {});
17881
17882 let fs = FakeFs::new(cx.executor());
17883 fs.insert_tree(
17884 path!("/a"),
17885 json!({
17886 "main.rs": "fn main() { let a = 5; }",
17887 "other.rs": "// Test file",
17888 }),
17889 )
17890 .await;
17891 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17892
17893 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17894 language_registry.add(Arc::new(Language::new(
17895 LanguageConfig {
17896 name: "Rust".into(),
17897 matcher: LanguageMatcher {
17898 path_suffixes: vec!["rs".to_string()],
17899 ..Default::default()
17900 },
17901 brackets: BracketPairConfig {
17902 pairs: vec![BracketPair {
17903 start: "{".to_string(),
17904 end: "}".to_string(),
17905 close: true,
17906 surround: true,
17907 newline: true,
17908 }],
17909 disabled_scopes_by_bracket_ix: Vec::new(),
17910 },
17911 ..Default::default()
17912 },
17913 Some(tree_sitter_rust::LANGUAGE.into()),
17914 )));
17915 let mut fake_servers = language_registry.register_fake_lsp(
17916 "Rust",
17917 FakeLspAdapter {
17918 capabilities: lsp::ServerCapabilities {
17919 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17920 first_trigger_character: "{".to_string(),
17921 more_trigger_character: None,
17922 }),
17923 ..Default::default()
17924 },
17925 ..Default::default()
17926 },
17927 );
17928
17929 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17930
17931 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17932
17933 let worktree_id = workspace
17934 .update(cx, |workspace, _, cx| {
17935 workspace.project().update(cx, |project, cx| {
17936 project.worktrees(cx).next().unwrap().read(cx).id()
17937 })
17938 })
17939 .unwrap();
17940
17941 let buffer = project
17942 .update(cx, |project, cx| {
17943 project.open_local_buffer(path!("/a/main.rs"), cx)
17944 })
17945 .await
17946 .unwrap();
17947 let editor_handle = workspace
17948 .update(cx, |workspace, window, cx| {
17949 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17950 })
17951 .unwrap()
17952 .await
17953 .unwrap()
17954 .downcast::<Editor>()
17955 .unwrap();
17956
17957 cx.executor().start_waiting();
17958 let fake_server = fake_servers.next().await.unwrap();
17959
17960 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17961 |params, _| async move {
17962 assert_eq!(
17963 params.text_document_position.text_document.uri,
17964 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17965 );
17966 assert_eq!(
17967 params.text_document_position.position,
17968 lsp::Position::new(0, 21),
17969 );
17970
17971 Ok(Some(vec![lsp::TextEdit {
17972 new_text: "]".to_string(),
17973 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17974 }]))
17975 },
17976 );
17977
17978 editor_handle.update_in(cx, |editor, window, cx| {
17979 window.focus(&editor.focus_handle(cx));
17980 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17981 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17982 });
17983 editor.handle_input("{", window, cx);
17984 });
17985
17986 cx.executor().run_until_parked();
17987
17988 buffer.update(cx, |buffer, _| {
17989 assert_eq!(
17990 buffer.text(),
17991 "fn main() { let a = {5}; }",
17992 "No extra braces from on type formatting should appear in the buffer"
17993 )
17994 });
17995}
17996
17997#[gpui::test(iterations = 20, seeds(31))]
17998async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17999 init_test(cx, |_| {});
18000
18001 let mut cx = EditorLspTestContext::new_rust(
18002 lsp::ServerCapabilities {
18003 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18004 first_trigger_character: ".".to_string(),
18005 more_trigger_character: None,
18006 }),
18007 ..Default::default()
18008 },
18009 cx,
18010 )
18011 .await;
18012
18013 cx.update_buffer(|buffer, _| {
18014 // This causes autoindent to be async.
18015 buffer.set_sync_parse_timeout(Duration::ZERO)
18016 });
18017
18018 cx.set_state("fn c() {\n d()ˇ\n}\n");
18019 cx.simulate_keystroke("\n");
18020 cx.run_until_parked();
18021
18022 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18023 let mut request =
18024 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18025 let buffer_cloned = buffer_cloned.clone();
18026 async move {
18027 buffer_cloned.update(&mut cx, |buffer, _| {
18028 assert_eq!(
18029 buffer.text(),
18030 "fn c() {\n d()\n .\n}\n",
18031 "OnTypeFormatting should triggered after autoindent applied"
18032 )
18033 })?;
18034
18035 Ok(Some(vec![]))
18036 }
18037 });
18038
18039 cx.simulate_keystroke(".");
18040 cx.run_until_parked();
18041
18042 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18043 assert!(request.next().await.is_some());
18044 request.close();
18045 assert!(request.next().await.is_none());
18046}
18047
18048#[gpui::test]
18049async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18050 init_test(cx, |_| {});
18051
18052 let fs = FakeFs::new(cx.executor());
18053 fs.insert_tree(
18054 path!("/a"),
18055 json!({
18056 "main.rs": "fn main() { let a = 5; }",
18057 "other.rs": "// Test file",
18058 }),
18059 )
18060 .await;
18061
18062 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18063
18064 let server_restarts = Arc::new(AtomicUsize::new(0));
18065 let closure_restarts = Arc::clone(&server_restarts);
18066 let language_server_name = "test language server";
18067 let language_name: LanguageName = "Rust".into();
18068
18069 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18070 language_registry.add(Arc::new(Language::new(
18071 LanguageConfig {
18072 name: language_name.clone(),
18073 matcher: LanguageMatcher {
18074 path_suffixes: vec!["rs".to_string()],
18075 ..Default::default()
18076 },
18077 ..Default::default()
18078 },
18079 Some(tree_sitter_rust::LANGUAGE.into()),
18080 )));
18081 let mut fake_servers = language_registry.register_fake_lsp(
18082 "Rust",
18083 FakeLspAdapter {
18084 name: language_server_name,
18085 initialization_options: Some(json!({
18086 "testOptionValue": true
18087 })),
18088 initializer: Some(Box::new(move |fake_server| {
18089 let task_restarts = Arc::clone(&closure_restarts);
18090 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18091 task_restarts.fetch_add(1, atomic::Ordering::Release);
18092 futures::future::ready(Ok(()))
18093 });
18094 })),
18095 ..Default::default()
18096 },
18097 );
18098
18099 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18100 let _buffer = project
18101 .update(cx, |project, cx| {
18102 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18103 })
18104 .await
18105 .unwrap();
18106 let _fake_server = fake_servers.next().await.unwrap();
18107 update_test_language_settings(cx, |language_settings| {
18108 language_settings.languages.0.insert(
18109 language_name.clone().0,
18110 LanguageSettingsContent {
18111 tab_size: NonZeroU32::new(8),
18112 ..Default::default()
18113 },
18114 );
18115 });
18116 cx.executor().run_until_parked();
18117 assert_eq!(
18118 server_restarts.load(atomic::Ordering::Acquire),
18119 0,
18120 "Should not restart LSP server on an unrelated change"
18121 );
18122
18123 update_test_project_settings(cx, |project_settings| {
18124 project_settings.lsp.insert(
18125 "Some other server name".into(),
18126 LspSettings {
18127 binary: None,
18128 settings: None,
18129 initialization_options: Some(json!({
18130 "some other init value": false
18131 })),
18132 enable_lsp_tasks: false,
18133 fetch: None,
18134 },
18135 );
18136 });
18137 cx.executor().run_until_parked();
18138 assert_eq!(
18139 server_restarts.load(atomic::Ordering::Acquire),
18140 0,
18141 "Should not restart LSP server on an unrelated LSP settings change"
18142 );
18143
18144 update_test_project_settings(cx, |project_settings| {
18145 project_settings.lsp.insert(
18146 language_server_name.into(),
18147 LspSettings {
18148 binary: None,
18149 settings: None,
18150 initialization_options: Some(json!({
18151 "anotherInitValue": false
18152 })),
18153 enable_lsp_tasks: false,
18154 fetch: None,
18155 },
18156 );
18157 });
18158 cx.executor().run_until_parked();
18159 assert_eq!(
18160 server_restarts.load(atomic::Ordering::Acquire),
18161 1,
18162 "Should restart LSP server on a related LSP settings change"
18163 );
18164
18165 update_test_project_settings(cx, |project_settings| {
18166 project_settings.lsp.insert(
18167 language_server_name.into(),
18168 LspSettings {
18169 binary: None,
18170 settings: None,
18171 initialization_options: Some(json!({
18172 "anotherInitValue": false
18173 })),
18174 enable_lsp_tasks: false,
18175 fetch: None,
18176 },
18177 );
18178 });
18179 cx.executor().run_until_parked();
18180 assert_eq!(
18181 server_restarts.load(atomic::Ordering::Acquire),
18182 1,
18183 "Should not restart LSP server on a related LSP settings change that is the same"
18184 );
18185
18186 update_test_project_settings(cx, |project_settings| {
18187 project_settings.lsp.insert(
18188 language_server_name.into(),
18189 LspSettings {
18190 binary: None,
18191 settings: None,
18192 initialization_options: None,
18193 enable_lsp_tasks: false,
18194 fetch: None,
18195 },
18196 );
18197 });
18198 cx.executor().run_until_parked();
18199 assert_eq!(
18200 server_restarts.load(atomic::Ordering::Acquire),
18201 2,
18202 "Should restart LSP server on another related LSP settings change"
18203 );
18204}
18205
18206#[gpui::test]
18207async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18208 init_test(cx, |_| {});
18209
18210 let mut cx = EditorLspTestContext::new_rust(
18211 lsp::ServerCapabilities {
18212 completion_provider: Some(lsp::CompletionOptions {
18213 trigger_characters: Some(vec![".".to_string()]),
18214 resolve_provider: Some(true),
18215 ..Default::default()
18216 }),
18217 ..Default::default()
18218 },
18219 cx,
18220 )
18221 .await;
18222
18223 cx.set_state("fn main() { let a = 2ˇ; }");
18224 cx.simulate_keystroke(".");
18225 let completion_item = lsp::CompletionItem {
18226 label: "some".into(),
18227 kind: Some(lsp::CompletionItemKind::SNIPPET),
18228 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18229 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18230 kind: lsp::MarkupKind::Markdown,
18231 value: "```rust\nSome(2)\n```".to_string(),
18232 })),
18233 deprecated: Some(false),
18234 sort_text: Some("fffffff2".to_string()),
18235 filter_text: Some("some".to_string()),
18236 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18237 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18238 range: lsp::Range {
18239 start: lsp::Position {
18240 line: 0,
18241 character: 22,
18242 },
18243 end: lsp::Position {
18244 line: 0,
18245 character: 22,
18246 },
18247 },
18248 new_text: "Some(2)".to_string(),
18249 })),
18250 additional_text_edits: Some(vec![lsp::TextEdit {
18251 range: lsp::Range {
18252 start: lsp::Position {
18253 line: 0,
18254 character: 20,
18255 },
18256 end: lsp::Position {
18257 line: 0,
18258 character: 22,
18259 },
18260 },
18261 new_text: "".to_string(),
18262 }]),
18263 ..Default::default()
18264 };
18265
18266 let closure_completion_item = completion_item.clone();
18267 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18268 let task_completion_item = closure_completion_item.clone();
18269 async move {
18270 Ok(Some(lsp::CompletionResponse::Array(vec![
18271 task_completion_item,
18272 ])))
18273 }
18274 });
18275
18276 request.next().await;
18277
18278 cx.condition(|editor, _| editor.context_menu_visible())
18279 .await;
18280 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18281 editor
18282 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18283 .unwrap()
18284 });
18285 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18286
18287 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18288 let task_completion_item = completion_item.clone();
18289 async move { Ok(task_completion_item) }
18290 })
18291 .next()
18292 .await
18293 .unwrap();
18294 apply_additional_edits.await.unwrap();
18295 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18296}
18297
18298#[gpui::test]
18299async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18300 init_test(cx, |_| {});
18301
18302 let mut cx = EditorLspTestContext::new_rust(
18303 lsp::ServerCapabilities {
18304 completion_provider: Some(lsp::CompletionOptions {
18305 trigger_characters: Some(vec![".".to_string()]),
18306 resolve_provider: Some(true),
18307 ..Default::default()
18308 }),
18309 ..Default::default()
18310 },
18311 cx,
18312 )
18313 .await;
18314
18315 cx.set_state("fn main() { let a = 2ˇ; }");
18316 cx.simulate_keystroke(".");
18317
18318 let item1 = lsp::CompletionItem {
18319 label: "method id()".to_string(),
18320 filter_text: Some("id".to_string()),
18321 detail: None,
18322 documentation: None,
18323 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18324 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18325 new_text: ".id".to_string(),
18326 })),
18327 ..lsp::CompletionItem::default()
18328 };
18329
18330 let item2 = lsp::CompletionItem {
18331 label: "other".to_string(),
18332 filter_text: Some("other".to_string()),
18333 detail: None,
18334 documentation: None,
18335 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18336 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18337 new_text: ".other".to_string(),
18338 })),
18339 ..lsp::CompletionItem::default()
18340 };
18341
18342 let item1 = item1.clone();
18343 cx.set_request_handler::<lsp::request::Completion, _, _>({
18344 let item1 = item1.clone();
18345 move |_, _, _| {
18346 let item1 = item1.clone();
18347 let item2 = item2.clone();
18348 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18349 }
18350 })
18351 .next()
18352 .await;
18353
18354 cx.condition(|editor, _| editor.context_menu_visible())
18355 .await;
18356 cx.update_editor(|editor, _, _| {
18357 let context_menu = editor.context_menu.borrow_mut();
18358 let context_menu = context_menu
18359 .as_ref()
18360 .expect("Should have the context menu deployed");
18361 match context_menu {
18362 CodeContextMenu::Completions(completions_menu) => {
18363 let completions = completions_menu.completions.borrow_mut();
18364 assert_eq!(
18365 completions
18366 .iter()
18367 .map(|completion| &completion.label.text)
18368 .collect::<Vec<_>>(),
18369 vec!["method id()", "other"]
18370 )
18371 }
18372 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18373 }
18374 });
18375
18376 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18377 let item1 = item1.clone();
18378 move |_, item_to_resolve, _| {
18379 let item1 = item1.clone();
18380 async move {
18381 if item1 == item_to_resolve {
18382 Ok(lsp::CompletionItem {
18383 label: "method id()".to_string(),
18384 filter_text: Some("id".to_string()),
18385 detail: Some("Now resolved!".to_string()),
18386 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18387 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18388 range: lsp::Range::new(
18389 lsp::Position::new(0, 22),
18390 lsp::Position::new(0, 22),
18391 ),
18392 new_text: ".id".to_string(),
18393 })),
18394 ..lsp::CompletionItem::default()
18395 })
18396 } else {
18397 Ok(item_to_resolve)
18398 }
18399 }
18400 }
18401 })
18402 .next()
18403 .await
18404 .unwrap();
18405 cx.run_until_parked();
18406
18407 cx.update_editor(|editor, window, cx| {
18408 editor.context_menu_next(&Default::default(), window, cx);
18409 });
18410
18411 cx.update_editor(|editor, _, _| {
18412 let context_menu = editor.context_menu.borrow_mut();
18413 let context_menu = context_menu
18414 .as_ref()
18415 .expect("Should have the context menu deployed");
18416 match context_menu {
18417 CodeContextMenu::Completions(completions_menu) => {
18418 let completions = completions_menu.completions.borrow_mut();
18419 assert_eq!(
18420 completions
18421 .iter()
18422 .map(|completion| &completion.label.text)
18423 .collect::<Vec<_>>(),
18424 vec!["method id() Now resolved!", "other"],
18425 "Should update first completion label, but not second as the filter text did not match."
18426 );
18427 }
18428 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18429 }
18430 });
18431}
18432
18433#[gpui::test]
18434async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18435 init_test(cx, |_| {});
18436 let mut cx = EditorLspTestContext::new_rust(
18437 lsp::ServerCapabilities {
18438 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18439 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18440 completion_provider: Some(lsp::CompletionOptions {
18441 resolve_provider: Some(true),
18442 ..Default::default()
18443 }),
18444 ..Default::default()
18445 },
18446 cx,
18447 )
18448 .await;
18449 cx.set_state(indoc! {"
18450 struct TestStruct {
18451 field: i32
18452 }
18453
18454 fn mainˇ() {
18455 let unused_var = 42;
18456 let test_struct = TestStruct { field: 42 };
18457 }
18458 "});
18459 let symbol_range = cx.lsp_range(indoc! {"
18460 struct TestStruct {
18461 field: i32
18462 }
18463
18464 «fn main»() {
18465 let unused_var = 42;
18466 let test_struct = TestStruct { field: 42 };
18467 }
18468 "});
18469 let mut hover_requests =
18470 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18471 Ok(Some(lsp::Hover {
18472 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18473 kind: lsp::MarkupKind::Markdown,
18474 value: "Function documentation".to_string(),
18475 }),
18476 range: Some(symbol_range),
18477 }))
18478 });
18479
18480 // Case 1: Test that code action menu hide hover popover
18481 cx.dispatch_action(Hover);
18482 hover_requests.next().await;
18483 cx.condition(|editor, _| editor.hover_state.visible()).await;
18484 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18485 move |_, _, _| async move {
18486 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18487 lsp::CodeAction {
18488 title: "Remove unused variable".to_string(),
18489 kind: Some(CodeActionKind::QUICKFIX),
18490 edit: Some(lsp::WorkspaceEdit {
18491 changes: Some(
18492 [(
18493 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18494 vec![lsp::TextEdit {
18495 range: lsp::Range::new(
18496 lsp::Position::new(5, 4),
18497 lsp::Position::new(5, 27),
18498 ),
18499 new_text: "".to_string(),
18500 }],
18501 )]
18502 .into_iter()
18503 .collect(),
18504 ),
18505 ..Default::default()
18506 }),
18507 ..Default::default()
18508 },
18509 )]))
18510 },
18511 );
18512 cx.update_editor(|editor, window, cx| {
18513 editor.toggle_code_actions(
18514 &ToggleCodeActions {
18515 deployed_from: None,
18516 quick_launch: false,
18517 },
18518 window,
18519 cx,
18520 );
18521 });
18522 code_action_requests.next().await;
18523 cx.run_until_parked();
18524 cx.condition(|editor, _| editor.context_menu_visible())
18525 .await;
18526 cx.update_editor(|editor, _, _| {
18527 assert!(
18528 !editor.hover_state.visible(),
18529 "Hover popover should be hidden when code action menu is shown"
18530 );
18531 // Hide code actions
18532 editor.context_menu.take();
18533 });
18534
18535 // Case 2: Test that code completions hide hover popover
18536 cx.dispatch_action(Hover);
18537 hover_requests.next().await;
18538 cx.condition(|editor, _| editor.hover_state.visible()).await;
18539 let counter = Arc::new(AtomicUsize::new(0));
18540 let mut completion_requests =
18541 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18542 let counter = counter.clone();
18543 async move {
18544 counter.fetch_add(1, atomic::Ordering::Release);
18545 Ok(Some(lsp::CompletionResponse::Array(vec![
18546 lsp::CompletionItem {
18547 label: "main".into(),
18548 kind: Some(lsp::CompletionItemKind::FUNCTION),
18549 detail: Some("() -> ()".to_string()),
18550 ..Default::default()
18551 },
18552 lsp::CompletionItem {
18553 label: "TestStruct".into(),
18554 kind: Some(lsp::CompletionItemKind::STRUCT),
18555 detail: Some("struct TestStruct".to_string()),
18556 ..Default::default()
18557 },
18558 ])))
18559 }
18560 });
18561 cx.update_editor(|editor, window, cx| {
18562 editor.show_completions(&ShowCompletions, window, cx);
18563 });
18564 completion_requests.next().await;
18565 cx.condition(|editor, _| editor.context_menu_visible())
18566 .await;
18567 cx.update_editor(|editor, _, _| {
18568 assert!(
18569 !editor.hover_state.visible(),
18570 "Hover popover should be hidden when completion menu is shown"
18571 );
18572 });
18573}
18574
18575#[gpui::test]
18576async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18577 init_test(cx, |_| {});
18578
18579 let mut cx = EditorLspTestContext::new_rust(
18580 lsp::ServerCapabilities {
18581 completion_provider: Some(lsp::CompletionOptions {
18582 trigger_characters: Some(vec![".".to_string()]),
18583 resolve_provider: Some(true),
18584 ..Default::default()
18585 }),
18586 ..Default::default()
18587 },
18588 cx,
18589 )
18590 .await;
18591
18592 cx.set_state("fn main() { let a = 2ˇ; }");
18593 cx.simulate_keystroke(".");
18594
18595 let unresolved_item_1 = lsp::CompletionItem {
18596 label: "id".to_string(),
18597 filter_text: Some("id".to_string()),
18598 detail: None,
18599 documentation: None,
18600 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18601 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18602 new_text: ".id".to_string(),
18603 })),
18604 ..lsp::CompletionItem::default()
18605 };
18606 let resolved_item_1 = lsp::CompletionItem {
18607 additional_text_edits: Some(vec![lsp::TextEdit {
18608 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18609 new_text: "!!".to_string(),
18610 }]),
18611 ..unresolved_item_1.clone()
18612 };
18613 let unresolved_item_2 = lsp::CompletionItem {
18614 label: "other".to_string(),
18615 filter_text: Some("other".to_string()),
18616 detail: None,
18617 documentation: None,
18618 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18619 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18620 new_text: ".other".to_string(),
18621 })),
18622 ..lsp::CompletionItem::default()
18623 };
18624 let resolved_item_2 = lsp::CompletionItem {
18625 additional_text_edits: Some(vec![lsp::TextEdit {
18626 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18627 new_text: "??".to_string(),
18628 }]),
18629 ..unresolved_item_2.clone()
18630 };
18631
18632 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18633 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18634 cx.lsp
18635 .server
18636 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18637 let unresolved_item_1 = unresolved_item_1.clone();
18638 let resolved_item_1 = resolved_item_1.clone();
18639 let unresolved_item_2 = unresolved_item_2.clone();
18640 let resolved_item_2 = resolved_item_2.clone();
18641 let resolve_requests_1 = resolve_requests_1.clone();
18642 let resolve_requests_2 = resolve_requests_2.clone();
18643 move |unresolved_request, _| {
18644 let unresolved_item_1 = unresolved_item_1.clone();
18645 let resolved_item_1 = resolved_item_1.clone();
18646 let unresolved_item_2 = unresolved_item_2.clone();
18647 let resolved_item_2 = resolved_item_2.clone();
18648 let resolve_requests_1 = resolve_requests_1.clone();
18649 let resolve_requests_2 = resolve_requests_2.clone();
18650 async move {
18651 if unresolved_request == unresolved_item_1 {
18652 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18653 Ok(resolved_item_1.clone())
18654 } else if unresolved_request == unresolved_item_2 {
18655 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18656 Ok(resolved_item_2.clone())
18657 } else {
18658 panic!("Unexpected completion item {unresolved_request:?}")
18659 }
18660 }
18661 }
18662 })
18663 .detach();
18664
18665 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18666 let unresolved_item_1 = unresolved_item_1.clone();
18667 let unresolved_item_2 = unresolved_item_2.clone();
18668 async move {
18669 Ok(Some(lsp::CompletionResponse::Array(vec![
18670 unresolved_item_1,
18671 unresolved_item_2,
18672 ])))
18673 }
18674 })
18675 .next()
18676 .await;
18677
18678 cx.condition(|editor, _| editor.context_menu_visible())
18679 .await;
18680 cx.update_editor(|editor, _, _| {
18681 let context_menu = editor.context_menu.borrow_mut();
18682 let context_menu = context_menu
18683 .as_ref()
18684 .expect("Should have the context menu deployed");
18685 match context_menu {
18686 CodeContextMenu::Completions(completions_menu) => {
18687 let completions = completions_menu.completions.borrow_mut();
18688 assert_eq!(
18689 completions
18690 .iter()
18691 .map(|completion| &completion.label.text)
18692 .collect::<Vec<_>>(),
18693 vec!["id", "other"]
18694 )
18695 }
18696 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18697 }
18698 });
18699 cx.run_until_parked();
18700
18701 cx.update_editor(|editor, window, cx| {
18702 editor.context_menu_next(&ContextMenuNext, window, cx);
18703 });
18704 cx.run_until_parked();
18705 cx.update_editor(|editor, window, cx| {
18706 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18707 });
18708 cx.run_until_parked();
18709 cx.update_editor(|editor, window, cx| {
18710 editor.context_menu_next(&ContextMenuNext, window, cx);
18711 });
18712 cx.run_until_parked();
18713 cx.update_editor(|editor, window, cx| {
18714 editor
18715 .compose_completion(&ComposeCompletion::default(), window, cx)
18716 .expect("No task returned")
18717 })
18718 .await
18719 .expect("Completion failed");
18720 cx.run_until_parked();
18721
18722 cx.update_editor(|editor, _, cx| {
18723 assert_eq!(
18724 resolve_requests_1.load(atomic::Ordering::Acquire),
18725 1,
18726 "Should always resolve once despite multiple selections"
18727 );
18728 assert_eq!(
18729 resolve_requests_2.load(atomic::Ordering::Acquire),
18730 1,
18731 "Should always resolve once after multiple selections and applying the completion"
18732 );
18733 assert_eq!(
18734 editor.text(cx),
18735 "fn main() { let a = ??.other; }",
18736 "Should use resolved data when applying the completion"
18737 );
18738 });
18739}
18740
18741#[gpui::test]
18742async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18743 init_test(cx, |_| {});
18744
18745 let item_0 = lsp::CompletionItem {
18746 label: "abs".into(),
18747 insert_text: Some("abs".into()),
18748 data: Some(json!({ "very": "special"})),
18749 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18750 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18751 lsp::InsertReplaceEdit {
18752 new_text: "abs".to_string(),
18753 insert: lsp::Range::default(),
18754 replace: lsp::Range::default(),
18755 },
18756 )),
18757 ..lsp::CompletionItem::default()
18758 };
18759 let items = iter::once(item_0.clone())
18760 .chain((11..51).map(|i| lsp::CompletionItem {
18761 label: format!("item_{}", i),
18762 insert_text: Some(format!("item_{}", i)),
18763 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18764 ..lsp::CompletionItem::default()
18765 }))
18766 .collect::<Vec<_>>();
18767
18768 let default_commit_characters = vec!["?".to_string()];
18769 let default_data = json!({ "default": "data"});
18770 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18771 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18772 let default_edit_range = lsp::Range {
18773 start: lsp::Position {
18774 line: 0,
18775 character: 5,
18776 },
18777 end: lsp::Position {
18778 line: 0,
18779 character: 5,
18780 },
18781 };
18782
18783 let mut cx = EditorLspTestContext::new_rust(
18784 lsp::ServerCapabilities {
18785 completion_provider: Some(lsp::CompletionOptions {
18786 trigger_characters: Some(vec![".".to_string()]),
18787 resolve_provider: Some(true),
18788 ..Default::default()
18789 }),
18790 ..Default::default()
18791 },
18792 cx,
18793 )
18794 .await;
18795
18796 cx.set_state("fn main() { let a = 2ˇ; }");
18797 cx.simulate_keystroke(".");
18798
18799 let completion_data = default_data.clone();
18800 let completion_characters = default_commit_characters.clone();
18801 let completion_items = items.clone();
18802 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18803 let default_data = completion_data.clone();
18804 let default_commit_characters = completion_characters.clone();
18805 let items = completion_items.clone();
18806 async move {
18807 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18808 items,
18809 item_defaults: Some(lsp::CompletionListItemDefaults {
18810 data: Some(default_data.clone()),
18811 commit_characters: Some(default_commit_characters.clone()),
18812 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18813 default_edit_range,
18814 )),
18815 insert_text_format: Some(default_insert_text_format),
18816 insert_text_mode: Some(default_insert_text_mode),
18817 }),
18818 ..lsp::CompletionList::default()
18819 })))
18820 }
18821 })
18822 .next()
18823 .await;
18824
18825 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18826 cx.lsp
18827 .server
18828 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18829 let closure_resolved_items = resolved_items.clone();
18830 move |item_to_resolve, _| {
18831 let closure_resolved_items = closure_resolved_items.clone();
18832 async move {
18833 closure_resolved_items.lock().push(item_to_resolve.clone());
18834 Ok(item_to_resolve)
18835 }
18836 }
18837 })
18838 .detach();
18839
18840 cx.condition(|editor, _| editor.context_menu_visible())
18841 .await;
18842 cx.run_until_parked();
18843 cx.update_editor(|editor, _, _| {
18844 let menu = editor.context_menu.borrow_mut();
18845 match menu.as_ref().expect("should have the completions menu") {
18846 CodeContextMenu::Completions(completions_menu) => {
18847 assert_eq!(
18848 completions_menu
18849 .entries
18850 .borrow()
18851 .iter()
18852 .map(|mat| mat.string.clone())
18853 .collect::<Vec<String>>(),
18854 items
18855 .iter()
18856 .map(|completion| completion.label.clone())
18857 .collect::<Vec<String>>()
18858 );
18859 }
18860 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18861 }
18862 });
18863 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18864 // with 4 from the end.
18865 assert_eq!(
18866 *resolved_items.lock(),
18867 [&items[0..16], &items[items.len() - 4..items.len()]]
18868 .concat()
18869 .iter()
18870 .cloned()
18871 .map(|mut item| {
18872 if item.data.is_none() {
18873 item.data = Some(default_data.clone());
18874 }
18875 item
18876 })
18877 .collect::<Vec<lsp::CompletionItem>>(),
18878 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18879 );
18880 resolved_items.lock().clear();
18881
18882 cx.update_editor(|editor, window, cx| {
18883 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18884 });
18885 cx.run_until_parked();
18886 // Completions that have already been resolved are skipped.
18887 assert_eq!(
18888 *resolved_items.lock(),
18889 items[items.len() - 17..items.len() - 4]
18890 .iter()
18891 .cloned()
18892 .map(|mut item| {
18893 if item.data.is_none() {
18894 item.data = Some(default_data.clone());
18895 }
18896 item
18897 })
18898 .collect::<Vec<lsp::CompletionItem>>()
18899 );
18900 resolved_items.lock().clear();
18901}
18902
18903#[gpui::test]
18904async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18905 init_test(cx, |_| {});
18906
18907 let mut cx = EditorLspTestContext::new(
18908 Language::new(
18909 LanguageConfig {
18910 matcher: LanguageMatcher {
18911 path_suffixes: vec!["jsx".into()],
18912 ..Default::default()
18913 },
18914 overrides: [(
18915 "element".into(),
18916 LanguageConfigOverride {
18917 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18918 ..Default::default()
18919 },
18920 )]
18921 .into_iter()
18922 .collect(),
18923 ..Default::default()
18924 },
18925 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18926 )
18927 .with_override_query("(jsx_self_closing_element) @element")
18928 .unwrap(),
18929 lsp::ServerCapabilities {
18930 completion_provider: Some(lsp::CompletionOptions {
18931 trigger_characters: Some(vec![":".to_string()]),
18932 ..Default::default()
18933 }),
18934 ..Default::default()
18935 },
18936 cx,
18937 )
18938 .await;
18939
18940 cx.lsp
18941 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18942 Ok(Some(lsp::CompletionResponse::Array(vec![
18943 lsp::CompletionItem {
18944 label: "bg-blue".into(),
18945 ..Default::default()
18946 },
18947 lsp::CompletionItem {
18948 label: "bg-red".into(),
18949 ..Default::default()
18950 },
18951 lsp::CompletionItem {
18952 label: "bg-yellow".into(),
18953 ..Default::default()
18954 },
18955 ])))
18956 });
18957
18958 cx.set_state(r#"<p class="bgˇ" />"#);
18959
18960 // Trigger completion when typing a dash, because the dash is an extra
18961 // word character in the 'element' scope, which contains the cursor.
18962 cx.simulate_keystroke("-");
18963 cx.executor().run_until_parked();
18964 cx.update_editor(|editor, _, _| {
18965 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18966 {
18967 assert_eq!(
18968 completion_menu_entries(menu),
18969 &["bg-blue", "bg-red", "bg-yellow"]
18970 );
18971 } else {
18972 panic!("expected completion menu to be open");
18973 }
18974 });
18975
18976 cx.simulate_keystroke("l");
18977 cx.executor().run_until_parked();
18978 cx.update_editor(|editor, _, _| {
18979 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18980 {
18981 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18982 } else {
18983 panic!("expected completion menu to be open");
18984 }
18985 });
18986
18987 // When filtering completions, consider the character after the '-' to
18988 // be the start of a subword.
18989 cx.set_state(r#"<p class="yelˇ" />"#);
18990 cx.simulate_keystroke("l");
18991 cx.executor().run_until_parked();
18992 cx.update_editor(|editor, _, _| {
18993 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18994 {
18995 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18996 } else {
18997 panic!("expected completion menu to be open");
18998 }
18999 });
19000}
19001
19002fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19003 let entries = menu.entries.borrow();
19004 entries.iter().map(|mat| mat.string.clone()).collect()
19005}
19006
19007#[gpui::test]
19008async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19009 init_test(cx, |settings| {
19010 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19011 });
19012
19013 let fs = FakeFs::new(cx.executor());
19014 fs.insert_file(path!("/file.ts"), Default::default()).await;
19015
19016 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19017 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19018
19019 language_registry.add(Arc::new(Language::new(
19020 LanguageConfig {
19021 name: "TypeScript".into(),
19022 matcher: LanguageMatcher {
19023 path_suffixes: vec!["ts".to_string()],
19024 ..Default::default()
19025 },
19026 ..Default::default()
19027 },
19028 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19029 )));
19030 update_test_language_settings(cx, |settings| {
19031 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19032 });
19033
19034 let test_plugin = "test_plugin";
19035 let _ = language_registry.register_fake_lsp(
19036 "TypeScript",
19037 FakeLspAdapter {
19038 prettier_plugins: vec![test_plugin],
19039 ..Default::default()
19040 },
19041 );
19042
19043 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19044 let buffer = project
19045 .update(cx, |project, cx| {
19046 project.open_local_buffer(path!("/file.ts"), cx)
19047 })
19048 .await
19049 .unwrap();
19050
19051 let buffer_text = "one\ntwo\nthree\n";
19052 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19053 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19054 editor.update_in(cx, |editor, window, cx| {
19055 editor.set_text(buffer_text, window, cx)
19056 });
19057
19058 editor
19059 .update_in(cx, |editor, window, cx| {
19060 editor.perform_format(
19061 project.clone(),
19062 FormatTrigger::Manual,
19063 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19064 window,
19065 cx,
19066 )
19067 })
19068 .unwrap()
19069 .await;
19070 assert_eq!(
19071 editor.update(cx, |editor, cx| editor.text(cx)),
19072 buffer_text.to_string() + prettier_format_suffix,
19073 "Test prettier formatting was not applied to the original buffer text",
19074 );
19075
19076 update_test_language_settings(cx, |settings| {
19077 settings.defaults.formatter = Some(FormatterList::default())
19078 });
19079 let format = editor.update_in(cx, |editor, window, cx| {
19080 editor.perform_format(
19081 project.clone(),
19082 FormatTrigger::Manual,
19083 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19084 window,
19085 cx,
19086 )
19087 });
19088 format.await.unwrap();
19089 assert_eq!(
19090 editor.update(cx, |editor, cx| editor.text(cx)),
19091 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19092 "Autoformatting (via test prettier) was not applied to the original buffer text",
19093 );
19094}
19095
19096#[gpui::test]
19097async fn test_addition_reverts(cx: &mut TestAppContext) {
19098 init_test(cx, |_| {});
19099 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19100 let base_text = indoc! {r#"
19101 struct Row;
19102 struct Row1;
19103 struct Row2;
19104
19105 struct Row4;
19106 struct Row5;
19107 struct Row6;
19108
19109 struct Row8;
19110 struct Row9;
19111 struct Row10;"#};
19112
19113 // When addition hunks are not adjacent to carets, no hunk revert is performed
19114 assert_hunk_revert(
19115 indoc! {r#"struct Row;
19116 struct Row1;
19117 struct Row1.1;
19118 struct Row1.2;
19119 struct Row2;ˇ
19120
19121 struct Row4;
19122 struct Row5;
19123 struct Row6;
19124
19125 struct Row8;
19126 ˇstruct Row9;
19127 struct Row9.1;
19128 struct Row9.2;
19129 struct Row9.3;
19130 struct Row10;"#},
19131 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19132 indoc! {r#"struct Row;
19133 struct Row1;
19134 struct Row1.1;
19135 struct Row1.2;
19136 struct Row2;ˇ
19137
19138 struct Row4;
19139 struct Row5;
19140 struct Row6;
19141
19142 struct Row8;
19143 ˇstruct Row9;
19144 struct Row9.1;
19145 struct Row9.2;
19146 struct Row9.3;
19147 struct Row10;"#},
19148 base_text,
19149 &mut cx,
19150 );
19151 // Same for selections
19152 assert_hunk_revert(
19153 indoc! {r#"struct Row;
19154 struct Row1;
19155 struct Row2;
19156 struct Row2.1;
19157 struct Row2.2;
19158 «ˇ
19159 struct Row4;
19160 struct» Row5;
19161 «struct Row6;
19162 ˇ»
19163 struct Row9.1;
19164 struct Row9.2;
19165 struct Row9.3;
19166 struct Row8;
19167 struct Row9;
19168 struct Row10;"#},
19169 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19170 indoc! {r#"struct Row;
19171 struct Row1;
19172 struct Row2;
19173 struct Row2.1;
19174 struct Row2.2;
19175 «ˇ
19176 struct Row4;
19177 struct» Row5;
19178 «struct Row6;
19179 ˇ»
19180 struct Row9.1;
19181 struct Row9.2;
19182 struct Row9.3;
19183 struct Row8;
19184 struct Row9;
19185 struct Row10;"#},
19186 base_text,
19187 &mut cx,
19188 );
19189
19190 // When carets and selections intersect the addition hunks, those are reverted.
19191 // Adjacent carets got merged.
19192 assert_hunk_revert(
19193 indoc! {r#"struct Row;
19194 ˇ// something on the top
19195 struct Row1;
19196 struct Row2;
19197 struct Roˇw3.1;
19198 struct Row2.2;
19199 struct Row2.3;ˇ
19200
19201 struct Row4;
19202 struct ˇRow5.1;
19203 struct Row5.2;
19204 struct «Rowˇ»5.3;
19205 struct Row5;
19206 struct Row6;
19207 ˇ
19208 struct Row9.1;
19209 struct «Rowˇ»9.2;
19210 struct «ˇRow»9.3;
19211 struct Row8;
19212 struct Row9;
19213 «ˇ// something on bottom»
19214 struct Row10;"#},
19215 vec![
19216 DiffHunkStatusKind::Added,
19217 DiffHunkStatusKind::Added,
19218 DiffHunkStatusKind::Added,
19219 DiffHunkStatusKind::Added,
19220 DiffHunkStatusKind::Added,
19221 ],
19222 indoc! {r#"struct Row;
19223 ˇstruct Row1;
19224 struct Row2;
19225 ˇ
19226 struct Row4;
19227 ˇstruct Row5;
19228 struct Row6;
19229 ˇ
19230 ˇstruct Row8;
19231 struct Row9;
19232 ˇstruct Row10;"#},
19233 base_text,
19234 &mut cx,
19235 );
19236}
19237
19238#[gpui::test]
19239async fn test_modification_reverts(cx: &mut TestAppContext) {
19240 init_test(cx, |_| {});
19241 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19242 let base_text = indoc! {r#"
19243 struct Row;
19244 struct Row1;
19245 struct Row2;
19246
19247 struct Row4;
19248 struct Row5;
19249 struct Row6;
19250
19251 struct Row8;
19252 struct Row9;
19253 struct Row10;"#};
19254
19255 // Modification hunks behave the same as the addition ones.
19256 assert_hunk_revert(
19257 indoc! {r#"struct Row;
19258 struct Row1;
19259 struct Row33;
19260 ˇ
19261 struct Row4;
19262 struct Row5;
19263 struct Row6;
19264 ˇ
19265 struct Row99;
19266 struct Row9;
19267 struct Row10;"#},
19268 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19269 indoc! {r#"struct Row;
19270 struct Row1;
19271 struct Row33;
19272 ˇ
19273 struct Row4;
19274 struct Row5;
19275 struct Row6;
19276 ˇ
19277 struct Row99;
19278 struct Row9;
19279 struct Row10;"#},
19280 base_text,
19281 &mut cx,
19282 );
19283 assert_hunk_revert(
19284 indoc! {r#"struct Row;
19285 struct Row1;
19286 struct Row33;
19287 «ˇ
19288 struct Row4;
19289 struct» Row5;
19290 «struct Row6;
19291 ˇ»
19292 struct Row99;
19293 struct Row9;
19294 struct Row10;"#},
19295 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19296 indoc! {r#"struct Row;
19297 struct Row1;
19298 struct Row33;
19299 «ˇ
19300 struct Row4;
19301 struct» Row5;
19302 «struct Row6;
19303 ˇ»
19304 struct Row99;
19305 struct Row9;
19306 struct Row10;"#},
19307 base_text,
19308 &mut cx,
19309 );
19310
19311 assert_hunk_revert(
19312 indoc! {r#"ˇstruct Row1.1;
19313 struct Row1;
19314 «ˇstr»uct Row22;
19315
19316 struct ˇRow44;
19317 struct Row5;
19318 struct «Rˇ»ow66;ˇ
19319
19320 «struˇ»ct Row88;
19321 struct Row9;
19322 struct Row1011;ˇ"#},
19323 vec![
19324 DiffHunkStatusKind::Modified,
19325 DiffHunkStatusKind::Modified,
19326 DiffHunkStatusKind::Modified,
19327 DiffHunkStatusKind::Modified,
19328 DiffHunkStatusKind::Modified,
19329 DiffHunkStatusKind::Modified,
19330 ],
19331 indoc! {r#"struct Row;
19332 ˇstruct Row1;
19333 struct Row2;
19334 ˇ
19335 struct Row4;
19336 ˇstruct Row5;
19337 struct Row6;
19338 ˇ
19339 struct Row8;
19340 ˇstruct Row9;
19341 struct Row10;ˇ"#},
19342 base_text,
19343 &mut cx,
19344 );
19345}
19346
19347#[gpui::test]
19348async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19349 init_test(cx, |_| {});
19350 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19351 let base_text = indoc! {r#"
19352 one
19353
19354 two
19355 three
19356 "#};
19357
19358 cx.set_head_text(base_text);
19359 cx.set_state("\nˇ\n");
19360 cx.executor().run_until_parked();
19361 cx.update_editor(|editor, _window, cx| {
19362 editor.expand_selected_diff_hunks(cx);
19363 });
19364 cx.executor().run_until_parked();
19365 cx.update_editor(|editor, window, cx| {
19366 editor.backspace(&Default::default(), window, cx);
19367 });
19368 cx.run_until_parked();
19369 cx.assert_state_with_diff(
19370 indoc! {r#"
19371
19372 - two
19373 - threeˇ
19374 +
19375 "#}
19376 .to_string(),
19377 );
19378}
19379
19380#[gpui::test]
19381async fn test_deletion_reverts(cx: &mut TestAppContext) {
19382 init_test(cx, |_| {});
19383 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19384 let base_text = indoc! {r#"struct Row;
19385struct Row1;
19386struct Row2;
19387
19388struct Row4;
19389struct Row5;
19390struct Row6;
19391
19392struct Row8;
19393struct Row9;
19394struct Row10;"#};
19395
19396 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19397 assert_hunk_revert(
19398 indoc! {r#"struct Row;
19399 struct Row2;
19400
19401 ˇstruct Row4;
19402 struct Row5;
19403 struct Row6;
19404 ˇ
19405 struct Row8;
19406 struct Row10;"#},
19407 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19408 indoc! {r#"struct Row;
19409 struct Row2;
19410
19411 ˇstruct Row4;
19412 struct Row5;
19413 struct Row6;
19414 ˇ
19415 struct Row8;
19416 struct Row10;"#},
19417 base_text,
19418 &mut cx,
19419 );
19420 assert_hunk_revert(
19421 indoc! {r#"struct Row;
19422 struct Row2;
19423
19424 «ˇstruct Row4;
19425 struct» Row5;
19426 «struct Row6;
19427 ˇ»
19428 struct Row8;
19429 struct Row10;"#},
19430 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19431 indoc! {r#"struct Row;
19432 struct Row2;
19433
19434 «ˇstruct Row4;
19435 struct» Row5;
19436 «struct Row6;
19437 ˇ»
19438 struct Row8;
19439 struct Row10;"#},
19440 base_text,
19441 &mut cx,
19442 );
19443
19444 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19445 assert_hunk_revert(
19446 indoc! {r#"struct Row;
19447 ˇstruct Row2;
19448
19449 struct Row4;
19450 struct Row5;
19451 struct Row6;
19452
19453 struct Row8;ˇ
19454 struct Row10;"#},
19455 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19456 indoc! {r#"struct Row;
19457 struct Row1;
19458 ˇstruct Row2;
19459
19460 struct Row4;
19461 struct Row5;
19462 struct Row6;
19463
19464 struct Row8;ˇ
19465 struct Row9;
19466 struct Row10;"#},
19467 base_text,
19468 &mut cx,
19469 );
19470 assert_hunk_revert(
19471 indoc! {r#"struct Row;
19472 struct Row2«ˇ;
19473 struct Row4;
19474 struct» Row5;
19475 «struct Row6;
19476
19477 struct Row8;ˇ»
19478 struct Row10;"#},
19479 vec![
19480 DiffHunkStatusKind::Deleted,
19481 DiffHunkStatusKind::Deleted,
19482 DiffHunkStatusKind::Deleted,
19483 ],
19484 indoc! {r#"struct Row;
19485 struct Row1;
19486 struct Row2«ˇ;
19487
19488 struct Row4;
19489 struct» Row5;
19490 «struct Row6;
19491
19492 struct Row8;ˇ»
19493 struct Row9;
19494 struct Row10;"#},
19495 base_text,
19496 &mut cx,
19497 );
19498}
19499
19500#[gpui::test]
19501async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19502 init_test(cx, |_| {});
19503
19504 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19505 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19506 let base_text_3 =
19507 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19508
19509 let text_1 = edit_first_char_of_every_line(base_text_1);
19510 let text_2 = edit_first_char_of_every_line(base_text_2);
19511 let text_3 = edit_first_char_of_every_line(base_text_3);
19512
19513 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19514 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19515 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19516
19517 let multibuffer = cx.new(|cx| {
19518 let mut multibuffer = MultiBuffer::new(ReadWrite);
19519 multibuffer.push_excerpts(
19520 buffer_1.clone(),
19521 [
19522 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19523 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19524 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19525 ],
19526 cx,
19527 );
19528 multibuffer.push_excerpts(
19529 buffer_2.clone(),
19530 [
19531 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19532 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19533 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19534 ],
19535 cx,
19536 );
19537 multibuffer.push_excerpts(
19538 buffer_3.clone(),
19539 [
19540 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19541 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19542 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19543 ],
19544 cx,
19545 );
19546 multibuffer
19547 });
19548
19549 let fs = FakeFs::new(cx.executor());
19550 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19551 let (editor, cx) = cx
19552 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19553 editor.update_in(cx, |editor, _window, cx| {
19554 for (buffer, diff_base) in [
19555 (buffer_1.clone(), base_text_1),
19556 (buffer_2.clone(), base_text_2),
19557 (buffer_3.clone(), base_text_3),
19558 ] {
19559 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19560 editor
19561 .buffer
19562 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19563 }
19564 });
19565 cx.executor().run_until_parked();
19566
19567 editor.update_in(cx, |editor, window, cx| {
19568 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}");
19569 editor.select_all(&SelectAll, window, cx);
19570 editor.git_restore(&Default::default(), window, cx);
19571 });
19572 cx.executor().run_until_parked();
19573
19574 // When all ranges are selected, all buffer hunks are reverted.
19575 editor.update(cx, |editor, cx| {
19576 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");
19577 });
19578 buffer_1.update(cx, |buffer, _| {
19579 assert_eq!(buffer.text(), base_text_1);
19580 });
19581 buffer_2.update(cx, |buffer, _| {
19582 assert_eq!(buffer.text(), base_text_2);
19583 });
19584 buffer_3.update(cx, |buffer, _| {
19585 assert_eq!(buffer.text(), base_text_3);
19586 });
19587
19588 editor.update_in(cx, |editor, window, cx| {
19589 editor.undo(&Default::default(), window, cx);
19590 });
19591
19592 editor.update_in(cx, |editor, window, cx| {
19593 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19594 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19595 });
19596 editor.git_restore(&Default::default(), window, cx);
19597 });
19598
19599 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19600 // but not affect buffer_2 and its related excerpts.
19601 editor.update(cx, |editor, cx| {
19602 assert_eq!(
19603 editor.text(cx),
19604 "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}"
19605 );
19606 });
19607 buffer_1.update(cx, |buffer, _| {
19608 assert_eq!(buffer.text(), base_text_1);
19609 });
19610 buffer_2.update(cx, |buffer, _| {
19611 assert_eq!(
19612 buffer.text(),
19613 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19614 );
19615 });
19616 buffer_3.update(cx, |buffer, _| {
19617 assert_eq!(
19618 buffer.text(),
19619 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19620 );
19621 });
19622
19623 fn edit_first_char_of_every_line(text: &str) -> String {
19624 text.split('\n')
19625 .map(|line| format!("X{}", &line[1..]))
19626 .collect::<Vec<_>>()
19627 .join("\n")
19628 }
19629}
19630
19631#[gpui::test]
19632async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19633 init_test(cx, |_| {});
19634
19635 let cols = 4;
19636 let rows = 10;
19637 let sample_text_1 = sample_text(rows, cols, 'a');
19638 assert_eq!(
19639 sample_text_1,
19640 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19641 );
19642 let sample_text_2 = sample_text(rows, cols, 'l');
19643 assert_eq!(
19644 sample_text_2,
19645 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19646 );
19647 let sample_text_3 = sample_text(rows, cols, 'v');
19648 assert_eq!(
19649 sample_text_3,
19650 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19651 );
19652
19653 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19654 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19655 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19656
19657 let multi_buffer = cx.new(|cx| {
19658 let mut multibuffer = MultiBuffer::new(ReadWrite);
19659 multibuffer.push_excerpts(
19660 buffer_1.clone(),
19661 [
19662 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19663 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19664 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19665 ],
19666 cx,
19667 );
19668 multibuffer.push_excerpts(
19669 buffer_2.clone(),
19670 [
19671 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19672 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19673 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19674 ],
19675 cx,
19676 );
19677 multibuffer.push_excerpts(
19678 buffer_3.clone(),
19679 [
19680 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19681 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19682 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19683 ],
19684 cx,
19685 );
19686 multibuffer
19687 });
19688
19689 let fs = FakeFs::new(cx.executor());
19690 fs.insert_tree(
19691 "/a",
19692 json!({
19693 "main.rs": sample_text_1,
19694 "other.rs": sample_text_2,
19695 "lib.rs": sample_text_3,
19696 }),
19697 )
19698 .await;
19699 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19700 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19701 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19702 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19703 Editor::new(
19704 EditorMode::full(),
19705 multi_buffer,
19706 Some(project.clone()),
19707 window,
19708 cx,
19709 )
19710 });
19711 let multibuffer_item_id = workspace
19712 .update(cx, |workspace, window, cx| {
19713 assert!(
19714 workspace.active_item(cx).is_none(),
19715 "active item should be None before the first item is added"
19716 );
19717 workspace.add_item_to_active_pane(
19718 Box::new(multi_buffer_editor.clone()),
19719 None,
19720 true,
19721 window,
19722 cx,
19723 );
19724 let active_item = workspace
19725 .active_item(cx)
19726 .expect("should have an active item after adding the multi buffer");
19727 assert_eq!(
19728 active_item.buffer_kind(cx),
19729 ItemBufferKind::Multibuffer,
19730 "A multi buffer was expected to active after adding"
19731 );
19732 active_item.item_id()
19733 })
19734 .unwrap();
19735 cx.executor().run_until_parked();
19736
19737 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19738 editor.change_selections(
19739 SelectionEffects::scroll(Autoscroll::Next),
19740 window,
19741 cx,
19742 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
19743 );
19744 editor.open_excerpts(&OpenExcerpts, window, cx);
19745 });
19746 cx.executor().run_until_parked();
19747 let first_item_id = workspace
19748 .update(cx, |workspace, window, cx| {
19749 let active_item = workspace
19750 .active_item(cx)
19751 .expect("should have an active item after navigating into the 1st buffer");
19752 let first_item_id = active_item.item_id();
19753 assert_ne!(
19754 first_item_id, multibuffer_item_id,
19755 "Should navigate into the 1st buffer and activate it"
19756 );
19757 assert_eq!(
19758 active_item.buffer_kind(cx),
19759 ItemBufferKind::Singleton,
19760 "New active item should be a singleton buffer"
19761 );
19762 assert_eq!(
19763 active_item
19764 .act_as::<Editor>(cx)
19765 .expect("should have navigated into an editor for the 1st buffer")
19766 .read(cx)
19767 .text(cx),
19768 sample_text_1
19769 );
19770
19771 workspace
19772 .go_back(workspace.active_pane().downgrade(), window, cx)
19773 .detach_and_log_err(cx);
19774
19775 first_item_id
19776 })
19777 .unwrap();
19778 cx.executor().run_until_parked();
19779 workspace
19780 .update(cx, |workspace, _, cx| {
19781 let active_item = workspace
19782 .active_item(cx)
19783 .expect("should have an active item after navigating back");
19784 assert_eq!(
19785 active_item.item_id(),
19786 multibuffer_item_id,
19787 "Should navigate back to the multi buffer"
19788 );
19789 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19790 })
19791 .unwrap();
19792
19793 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19794 editor.change_selections(
19795 SelectionEffects::scroll(Autoscroll::Next),
19796 window,
19797 cx,
19798 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
19799 );
19800 editor.open_excerpts(&OpenExcerpts, window, cx);
19801 });
19802 cx.executor().run_until_parked();
19803 let second_item_id = workspace
19804 .update(cx, |workspace, window, cx| {
19805 let active_item = workspace
19806 .active_item(cx)
19807 .expect("should have an active item after navigating into the 2nd buffer");
19808 let second_item_id = active_item.item_id();
19809 assert_ne!(
19810 second_item_id, multibuffer_item_id,
19811 "Should navigate away from the multibuffer"
19812 );
19813 assert_ne!(
19814 second_item_id, first_item_id,
19815 "Should navigate into the 2nd buffer and activate it"
19816 );
19817 assert_eq!(
19818 active_item.buffer_kind(cx),
19819 ItemBufferKind::Singleton,
19820 "New active item should be a singleton buffer"
19821 );
19822 assert_eq!(
19823 active_item
19824 .act_as::<Editor>(cx)
19825 .expect("should have navigated into an editor")
19826 .read(cx)
19827 .text(cx),
19828 sample_text_2
19829 );
19830
19831 workspace
19832 .go_back(workspace.active_pane().downgrade(), window, cx)
19833 .detach_and_log_err(cx);
19834
19835 second_item_id
19836 })
19837 .unwrap();
19838 cx.executor().run_until_parked();
19839 workspace
19840 .update(cx, |workspace, _, cx| {
19841 let active_item = workspace
19842 .active_item(cx)
19843 .expect("should have an active item after navigating back from the 2nd buffer");
19844 assert_eq!(
19845 active_item.item_id(),
19846 multibuffer_item_id,
19847 "Should navigate back from the 2nd buffer to the multi buffer"
19848 );
19849 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19850 })
19851 .unwrap();
19852
19853 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19854 editor.change_selections(
19855 SelectionEffects::scroll(Autoscroll::Next),
19856 window,
19857 cx,
19858 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
19859 );
19860 editor.open_excerpts(&OpenExcerpts, window, cx);
19861 });
19862 cx.executor().run_until_parked();
19863 workspace
19864 .update(cx, |workspace, window, cx| {
19865 let active_item = workspace
19866 .active_item(cx)
19867 .expect("should have an active item after navigating into the 3rd buffer");
19868 let third_item_id = active_item.item_id();
19869 assert_ne!(
19870 third_item_id, multibuffer_item_id,
19871 "Should navigate into the 3rd buffer and activate it"
19872 );
19873 assert_ne!(third_item_id, first_item_id);
19874 assert_ne!(third_item_id, second_item_id);
19875 assert_eq!(
19876 active_item.buffer_kind(cx),
19877 ItemBufferKind::Singleton,
19878 "New active item should be a singleton buffer"
19879 );
19880 assert_eq!(
19881 active_item
19882 .act_as::<Editor>(cx)
19883 .expect("should have navigated into an editor")
19884 .read(cx)
19885 .text(cx),
19886 sample_text_3
19887 );
19888
19889 workspace
19890 .go_back(workspace.active_pane().downgrade(), window, cx)
19891 .detach_and_log_err(cx);
19892 })
19893 .unwrap();
19894 cx.executor().run_until_parked();
19895 workspace
19896 .update(cx, |workspace, _, cx| {
19897 let active_item = workspace
19898 .active_item(cx)
19899 .expect("should have an active item after navigating back from the 3rd buffer");
19900 assert_eq!(
19901 active_item.item_id(),
19902 multibuffer_item_id,
19903 "Should navigate back from the 3rd buffer to the multi buffer"
19904 );
19905 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19906 })
19907 .unwrap();
19908}
19909
19910#[gpui::test]
19911async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19912 init_test(cx, |_| {});
19913
19914 let mut cx = EditorTestContext::new(cx).await;
19915
19916 let diff_base = r#"
19917 use some::mod;
19918
19919 const A: u32 = 42;
19920
19921 fn main() {
19922 println!("hello");
19923
19924 println!("world");
19925 }
19926 "#
19927 .unindent();
19928
19929 cx.set_state(
19930 &r#"
19931 use some::modified;
19932
19933 ˇ
19934 fn main() {
19935 println!("hello there");
19936
19937 println!("around the");
19938 println!("world");
19939 }
19940 "#
19941 .unindent(),
19942 );
19943
19944 cx.set_head_text(&diff_base);
19945 executor.run_until_parked();
19946
19947 cx.update_editor(|editor, window, cx| {
19948 editor.go_to_next_hunk(&GoToHunk, window, cx);
19949 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19950 });
19951 executor.run_until_parked();
19952 cx.assert_state_with_diff(
19953 r#"
19954 use some::modified;
19955
19956
19957 fn main() {
19958 - println!("hello");
19959 + ˇ println!("hello there");
19960
19961 println!("around the");
19962 println!("world");
19963 }
19964 "#
19965 .unindent(),
19966 );
19967
19968 cx.update_editor(|editor, window, cx| {
19969 for _ in 0..2 {
19970 editor.go_to_next_hunk(&GoToHunk, window, cx);
19971 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19972 }
19973 });
19974 executor.run_until_parked();
19975 cx.assert_state_with_diff(
19976 r#"
19977 - use some::mod;
19978 + ˇuse some::modified;
19979
19980
19981 fn main() {
19982 - println!("hello");
19983 + println!("hello there");
19984
19985 + println!("around the");
19986 println!("world");
19987 }
19988 "#
19989 .unindent(),
19990 );
19991
19992 cx.update_editor(|editor, window, cx| {
19993 editor.go_to_next_hunk(&GoToHunk, window, cx);
19994 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19995 });
19996 executor.run_until_parked();
19997 cx.assert_state_with_diff(
19998 r#"
19999 - use some::mod;
20000 + use some::modified;
20001
20002 - const A: u32 = 42;
20003 ˇ
20004 fn main() {
20005 - println!("hello");
20006 + println!("hello there");
20007
20008 + println!("around the");
20009 println!("world");
20010 }
20011 "#
20012 .unindent(),
20013 );
20014
20015 cx.update_editor(|editor, window, cx| {
20016 editor.cancel(&Cancel, window, cx);
20017 });
20018
20019 cx.assert_state_with_diff(
20020 r#"
20021 use some::modified;
20022
20023 ˇ
20024 fn main() {
20025 println!("hello there");
20026
20027 println!("around the");
20028 println!("world");
20029 }
20030 "#
20031 .unindent(),
20032 );
20033}
20034
20035#[gpui::test]
20036async fn test_diff_base_change_with_expanded_diff_hunks(
20037 executor: BackgroundExecutor,
20038 cx: &mut TestAppContext,
20039) {
20040 init_test(cx, |_| {});
20041
20042 let mut cx = EditorTestContext::new(cx).await;
20043
20044 let diff_base = r#"
20045 use some::mod1;
20046 use some::mod2;
20047
20048 const A: u32 = 42;
20049 const B: u32 = 42;
20050 const C: u32 = 42;
20051
20052 fn main() {
20053 println!("hello");
20054
20055 println!("world");
20056 }
20057 "#
20058 .unindent();
20059
20060 cx.set_state(
20061 &r#"
20062 use some::mod2;
20063
20064 const A: u32 = 42;
20065 const C: u32 = 42;
20066
20067 fn main(ˇ) {
20068 //println!("hello");
20069
20070 println!("world");
20071 //
20072 //
20073 }
20074 "#
20075 .unindent(),
20076 );
20077
20078 cx.set_head_text(&diff_base);
20079 executor.run_until_parked();
20080
20081 cx.update_editor(|editor, window, cx| {
20082 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20083 });
20084 executor.run_until_parked();
20085 cx.assert_state_with_diff(
20086 r#"
20087 - use some::mod1;
20088 use some::mod2;
20089
20090 const A: u32 = 42;
20091 - const B: u32 = 42;
20092 const C: u32 = 42;
20093
20094 fn main(ˇ) {
20095 - println!("hello");
20096 + //println!("hello");
20097
20098 println!("world");
20099 + //
20100 + //
20101 }
20102 "#
20103 .unindent(),
20104 );
20105
20106 cx.set_head_text("new diff base!");
20107 executor.run_until_parked();
20108 cx.assert_state_with_diff(
20109 r#"
20110 - new diff base!
20111 + use some::mod2;
20112 +
20113 + const A: u32 = 42;
20114 + const C: u32 = 42;
20115 +
20116 + fn main(ˇ) {
20117 + //println!("hello");
20118 +
20119 + println!("world");
20120 + //
20121 + //
20122 + }
20123 "#
20124 .unindent(),
20125 );
20126}
20127
20128#[gpui::test]
20129async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20130 init_test(cx, |_| {});
20131
20132 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20133 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20134 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20135 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20136 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20137 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20138
20139 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20140 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20141 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20142
20143 let multi_buffer = cx.new(|cx| {
20144 let mut multibuffer = MultiBuffer::new(ReadWrite);
20145 multibuffer.push_excerpts(
20146 buffer_1.clone(),
20147 [
20148 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20149 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20150 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20151 ],
20152 cx,
20153 );
20154 multibuffer.push_excerpts(
20155 buffer_2.clone(),
20156 [
20157 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20158 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20159 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20160 ],
20161 cx,
20162 );
20163 multibuffer.push_excerpts(
20164 buffer_3.clone(),
20165 [
20166 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20167 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20168 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20169 ],
20170 cx,
20171 );
20172 multibuffer
20173 });
20174
20175 let editor =
20176 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20177 editor
20178 .update(cx, |editor, _window, cx| {
20179 for (buffer, diff_base) in [
20180 (buffer_1.clone(), file_1_old),
20181 (buffer_2.clone(), file_2_old),
20182 (buffer_3.clone(), file_3_old),
20183 ] {
20184 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20185 editor
20186 .buffer
20187 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20188 }
20189 })
20190 .unwrap();
20191
20192 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20193 cx.run_until_parked();
20194
20195 cx.assert_editor_state(
20196 &"
20197 ˇaaa
20198 ccc
20199 ddd
20200
20201 ggg
20202 hhh
20203
20204
20205 lll
20206 mmm
20207 NNN
20208
20209 qqq
20210 rrr
20211
20212 uuu
20213 111
20214 222
20215 333
20216
20217 666
20218 777
20219
20220 000
20221 !!!"
20222 .unindent(),
20223 );
20224
20225 cx.update_editor(|editor, window, cx| {
20226 editor.select_all(&SelectAll, window, cx);
20227 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20228 });
20229 cx.executor().run_until_parked();
20230
20231 cx.assert_state_with_diff(
20232 "
20233 «aaa
20234 - bbb
20235 ccc
20236 ddd
20237
20238 ggg
20239 hhh
20240
20241
20242 lll
20243 mmm
20244 - nnn
20245 + NNN
20246
20247 qqq
20248 rrr
20249
20250 uuu
20251 111
20252 222
20253 333
20254
20255 + 666
20256 777
20257
20258 000
20259 !!!ˇ»"
20260 .unindent(),
20261 );
20262}
20263
20264#[gpui::test]
20265async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20266 init_test(cx, |_| {});
20267
20268 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20269 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20270
20271 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20272 let multi_buffer = cx.new(|cx| {
20273 let mut multibuffer = MultiBuffer::new(ReadWrite);
20274 multibuffer.push_excerpts(
20275 buffer.clone(),
20276 [
20277 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20278 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20279 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20280 ],
20281 cx,
20282 );
20283 multibuffer
20284 });
20285
20286 let editor =
20287 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20288 editor
20289 .update(cx, |editor, _window, cx| {
20290 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20291 editor
20292 .buffer
20293 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20294 })
20295 .unwrap();
20296
20297 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20298 cx.run_until_parked();
20299
20300 cx.update_editor(|editor, window, cx| {
20301 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20302 });
20303 cx.executor().run_until_parked();
20304
20305 // When the start of a hunk coincides with the start of its excerpt,
20306 // the hunk is expanded. When the start of a hunk is earlier than
20307 // the start of its excerpt, the hunk is not expanded.
20308 cx.assert_state_with_diff(
20309 "
20310 ˇaaa
20311 - bbb
20312 + BBB
20313
20314 - ddd
20315 - eee
20316 + DDD
20317 + EEE
20318 fff
20319
20320 iii
20321 "
20322 .unindent(),
20323 );
20324}
20325
20326#[gpui::test]
20327async fn test_edits_around_expanded_insertion_hunks(
20328 executor: BackgroundExecutor,
20329 cx: &mut TestAppContext,
20330) {
20331 init_test(cx, |_| {});
20332
20333 let mut cx = EditorTestContext::new(cx).await;
20334
20335 let diff_base = r#"
20336 use some::mod1;
20337 use some::mod2;
20338
20339 const A: u32 = 42;
20340
20341 fn main() {
20342 println!("hello");
20343
20344 println!("world");
20345 }
20346 "#
20347 .unindent();
20348 executor.run_until_parked();
20349 cx.set_state(
20350 &r#"
20351 use some::mod1;
20352 use some::mod2;
20353
20354 const A: u32 = 42;
20355 const B: u32 = 42;
20356 const C: u32 = 42;
20357 ˇ
20358
20359 fn main() {
20360 println!("hello");
20361
20362 println!("world");
20363 }
20364 "#
20365 .unindent(),
20366 );
20367
20368 cx.set_head_text(&diff_base);
20369 executor.run_until_parked();
20370
20371 cx.update_editor(|editor, window, cx| {
20372 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20373 });
20374 executor.run_until_parked();
20375
20376 cx.assert_state_with_diff(
20377 r#"
20378 use some::mod1;
20379 use some::mod2;
20380
20381 const A: u32 = 42;
20382 + const B: u32 = 42;
20383 + const C: u32 = 42;
20384 + ˇ
20385
20386 fn main() {
20387 println!("hello");
20388
20389 println!("world");
20390 }
20391 "#
20392 .unindent(),
20393 );
20394
20395 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20396 executor.run_until_parked();
20397
20398 cx.assert_state_with_diff(
20399 r#"
20400 use some::mod1;
20401 use some::mod2;
20402
20403 const A: u32 = 42;
20404 + const B: u32 = 42;
20405 + const C: u32 = 42;
20406 + const D: u32 = 42;
20407 + ˇ
20408
20409 fn main() {
20410 println!("hello");
20411
20412 println!("world");
20413 }
20414 "#
20415 .unindent(),
20416 );
20417
20418 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20419 executor.run_until_parked();
20420
20421 cx.assert_state_with_diff(
20422 r#"
20423 use some::mod1;
20424 use some::mod2;
20425
20426 const A: u32 = 42;
20427 + const B: u32 = 42;
20428 + const C: u32 = 42;
20429 + const D: u32 = 42;
20430 + const E: u32 = 42;
20431 + ˇ
20432
20433 fn main() {
20434 println!("hello");
20435
20436 println!("world");
20437 }
20438 "#
20439 .unindent(),
20440 );
20441
20442 cx.update_editor(|editor, window, cx| {
20443 editor.delete_line(&DeleteLine, window, cx);
20444 });
20445 executor.run_until_parked();
20446
20447 cx.assert_state_with_diff(
20448 r#"
20449 use some::mod1;
20450 use some::mod2;
20451
20452 const A: u32 = 42;
20453 + const B: u32 = 42;
20454 + const C: u32 = 42;
20455 + const D: u32 = 42;
20456 + const E: u32 = 42;
20457 ˇ
20458 fn main() {
20459 println!("hello");
20460
20461 println!("world");
20462 }
20463 "#
20464 .unindent(),
20465 );
20466
20467 cx.update_editor(|editor, window, cx| {
20468 editor.move_up(&MoveUp, window, cx);
20469 editor.delete_line(&DeleteLine, 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 });
20475 executor.run_until_parked();
20476 cx.assert_state_with_diff(
20477 r#"
20478 use some::mod1;
20479 use some::mod2;
20480
20481 const A: u32 = 42;
20482 + const B: u32 = 42;
20483 ˇ
20484 fn main() {
20485 println!("hello");
20486
20487 println!("world");
20488 }
20489 "#
20490 .unindent(),
20491 );
20492
20493 cx.update_editor(|editor, window, cx| {
20494 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20495 editor.delete_line(&DeleteLine, window, cx);
20496 });
20497 executor.run_until_parked();
20498 cx.assert_state_with_diff(
20499 r#"
20500 ˇ
20501 fn main() {
20502 println!("hello");
20503
20504 println!("world");
20505 }
20506 "#
20507 .unindent(),
20508 );
20509}
20510
20511#[gpui::test]
20512async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20513 init_test(cx, |_| {});
20514
20515 let mut cx = EditorTestContext::new(cx).await;
20516 cx.set_head_text(indoc! { "
20517 one
20518 two
20519 three
20520 four
20521 five
20522 "
20523 });
20524 cx.set_state(indoc! { "
20525 one
20526 ˇthree
20527 five
20528 "});
20529 cx.run_until_parked();
20530 cx.update_editor(|editor, window, cx| {
20531 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20532 });
20533 cx.assert_state_with_diff(
20534 indoc! { "
20535 one
20536 - two
20537 ˇthree
20538 - four
20539 five
20540 "}
20541 .to_string(),
20542 );
20543 cx.update_editor(|editor, window, cx| {
20544 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20545 });
20546
20547 cx.assert_state_with_diff(
20548 indoc! { "
20549 one
20550 ˇthree
20551 five
20552 "}
20553 .to_string(),
20554 );
20555
20556 cx.set_state(indoc! { "
20557 one
20558 ˇTWO
20559 three
20560 four
20561 five
20562 "});
20563 cx.run_until_parked();
20564 cx.update_editor(|editor, window, cx| {
20565 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20566 });
20567
20568 cx.assert_state_with_diff(
20569 indoc! { "
20570 one
20571 - two
20572 + ˇTWO
20573 three
20574 four
20575 five
20576 "}
20577 .to_string(),
20578 );
20579 cx.update_editor(|editor, window, cx| {
20580 editor.move_up(&Default::default(), window, cx);
20581 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20582 });
20583 cx.assert_state_with_diff(
20584 indoc! { "
20585 one
20586 ˇTWO
20587 three
20588 four
20589 five
20590 "}
20591 .to_string(),
20592 );
20593}
20594
20595#[gpui::test]
20596async fn test_edits_around_expanded_deletion_hunks(
20597 executor: BackgroundExecutor,
20598 cx: &mut TestAppContext,
20599) {
20600 init_test(cx, |_| {});
20601
20602 let mut cx = EditorTestContext::new(cx).await;
20603
20604 let diff_base = r#"
20605 use some::mod1;
20606 use some::mod2;
20607
20608 const A: u32 = 42;
20609 const B: u32 = 42;
20610 const C: u32 = 42;
20611
20612
20613 fn main() {
20614 println!("hello");
20615
20616 println!("world");
20617 }
20618 "#
20619 .unindent();
20620 executor.run_until_parked();
20621 cx.set_state(
20622 &r#"
20623 use some::mod1;
20624 use some::mod2;
20625
20626 ˇconst B: u32 = 42;
20627 const C: u32 = 42;
20628
20629
20630 fn main() {
20631 println!("hello");
20632
20633 println!("world");
20634 }
20635 "#
20636 .unindent(),
20637 );
20638
20639 cx.set_head_text(&diff_base);
20640 executor.run_until_parked();
20641
20642 cx.update_editor(|editor, window, cx| {
20643 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20644 });
20645 executor.run_until_parked();
20646
20647 cx.assert_state_with_diff(
20648 r#"
20649 use some::mod1;
20650 use some::mod2;
20651
20652 - const A: u32 = 42;
20653 ˇconst B: u32 = 42;
20654 const C: u32 = 42;
20655
20656
20657 fn main() {
20658 println!("hello");
20659
20660 println!("world");
20661 }
20662 "#
20663 .unindent(),
20664 );
20665
20666 cx.update_editor(|editor, window, cx| {
20667 editor.delete_line(&DeleteLine, window, cx);
20668 });
20669 executor.run_until_parked();
20670 cx.assert_state_with_diff(
20671 r#"
20672 use some::mod1;
20673 use some::mod2;
20674
20675 - const A: u32 = 42;
20676 - const B: u32 = 42;
20677 ˇconst C: u32 = 42;
20678
20679
20680 fn main() {
20681 println!("hello");
20682
20683 println!("world");
20684 }
20685 "#
20686 .unindent(),
20687 );
20688
20689 cx.update_editor(|editor, window, cx| {
20690 editor.delete_line(&DeleteLine, window, cx);
20691 });
20692 executor.run_until_parked();
20693 cx.assert_state_with_diff(
20694 r#"
20695 use some::mod1;
20696 use some::mod2;
20697
20698 - const A: u32 = 42;
20699 - const B: u32 = 42;
20700 - const C: u32 = 42;
20701 ˇ
20702
20703 fn main() {
20704 println!("hello");
20705
20706 println!("world");
20707 }
20708 "#
20709 .unindent(),
20710 );
20711
20712 cx.update_editor(|editor, window, cx| {
20713 editor.handle_input("replacement", window, cx);
20714 });
20715 executor.run_until_parked();
20716 cx.assert_state_with_diff(
20717 r#"
20718 use some::mod1;
20719 use some::mod2;
20720
20721 - const A: u32 = 42;
20722 - const B: u32 = 42;
20723 - const C: u32 = 42;
20724 -
20725 + replacementˇ
20726
20727 fn main() {
20728 println!("hello");
20729
20730 println!("world");
20731 }
20732 "#
20733 .unindent(),
20734 );
20735}
20736
20737#[gpui::test]
20738async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20739 init_test(cx, |_| {});
20740
20741 let mut cx = EditorTestContext::new(cx).await;
20742
20743 let base_text = r#"
20744 one
20745 two
20746 three
20747 four
20748 five
20749 "#
20750 .unindent();
20751 executor.run_until_parked();
20752 cx.set_state(
20753 &r#"
20754 one
20755 two
20756 fˇour
20757 five
20758 "#
20759 .unindent(),
20760 );
20761
20762 cx.set_head_text(&base_text);
20763 executor.run_until_parked();
20764
20765 cx.update_editor(|editor, window, cx| {
20766 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20767 });
20768 executor.run_until_parked();
20769
20770 cx.assert_state_with_diff(
20771 r#"
20772 one
20773 two
20774 - three
20775 fˇour
20776 five
20777 "#
20778 .unindent(),
20779 );
20780
20781 cx.update_editor(|editor, window, cx| {
20782 editor.backspace(&Backspace, window, cx);
20783 editor.backspace(&Backspace, window, cx);
20784 });
20785 executor.run_until_parked();
20786 cx.assert_state_with_diff(
20787 r#"
20788 one
20789 two
20790 - threeˇ
20791 - four
20792 + our
20793 five
20794 "#
20795 .unindent(),
20796 );
20797}
20798
20799#[gpui::test]
20800async fn test_edit_after_expanded_modification_hunk(
20801 executor: BackgroundExecutor,
20802 cx: &mut TestAppContext,
20803) {
20804 init_test(cx, |_| {});
20805
20806 let mut cx = EditorTestContext::new(cx).await;
20807
20808 let diff_base = r#"
20809 use some::mod1;
20810 use some::mod2;
20811
20812 const A: u32 = 42;
20813 const B: u32 = 42;
20814 const C: u32 = 42;
20815 const D: u32 = 42;
20816
20817
20818 fn main() {
20819 println!("hello");
20820
20821 println!("world");
20822 }"#
20823 .unindent();
20824
20825 cx.set_state(
20826 &r#"
20827 use some::mod1;
20828 use some::mod2;
20829
20830 const A: u32 = 42;
20831 const B: u32 = 42;
20832 const C: u32 = 43ˇ
20833 const D: u32 = 42;
20834
20835
20836 fn main() {
20837 println!("hello");
20838
20839 println!("world");
20840 }"#
20841 .unindent(),
20842 );
20843
20844 cx.set_head_text(&diff_base);
20845 executor.run_until_parked();
20846 cx.update_editor(|editor, window, cx| {
20847 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20848 });
20849 executor.run_until_parked();
20850
20851 cx.assert_state_with_diff(
20852 r#"
20853 use some::mod1;
20854 use some::mod2;
20855
20856 const A: u32 = 42;
20857 const B: u32 = 42;
20858 - const C: u32 = 42;
20859 + const C: u32 = 43ˇ
20860 const D: u32 = 42;
20861
20862
20863 fn main() {
20864 println!("hello");
20865
20866 println!("world");
20867 }"#
20868 .unindent(),
20869 );
20870
20871 cx.update_editor(|editor, window, cx| {
20872 editor.handle_input("\nnew_line\n", window, cx);
20873 });
20874 executor.run_until_parked();
20875
20876 cx.assert_state_with_diff(
20877 r#"
20878 use some::mod1;
20879 use some::mod2;
20880
20881 const A: u32 = 42;
20882 const B: u32 = 42;
20883 - const C: u32 = 42;
20884 + const C: u32 = 43
20885 + new_line
20886 + ˇ
20887 const D: u32 = 42;
20888
20889
20890 fn main() {
20891 println!("hello");
20892
20893 println!("world");
20894 }"#
20895 .unindent(),
20896 );
20897}
20898
20899#[gpui::test]
20900async fn test_stage_and_unstage_added_file_hunk(
20901 executor: BackgroundExecutor,
20902 cx: &mut TestAppContext,
20903) {
20904 init_test(cx, |_| {});
20905
20906 let mut cx = EditorTestContext::new(cx).await;
20907 cx.update_editor(|editor, _, cx| {
20908 editor.set_expand_all_diff_hunks(cx);
20909 });
20910
20911 let working_copy = r#"
20912 ˇfn main() {
20913 println!("hello, world!");
20914 }
20915 "#
20916 .unindent();
20917
20918 cx.set_state(&working_copy);
20919 executor.run_until_parked();
20920
20921 cx.assert_state_with_diff(
20922 r#"
20923 + ˇfn main() {
20924 + println!("hello, world!");
20925 + }
20926 "#
20927 .unindent(),
20928 );
20929 cx.assert_index_text(None);
20930
20931 cx.update_editor(|editor, window, cx| {
20932 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20933 });
20934 executor.run_until_parked();
20935 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20936 cx.assert_state_with_diff(
20937 r#"
20938 + ˇfn main() {
20939 + println!("hello, world!");
20940 + }
20941 "#
20942 .unindent(),
20943 );
20944
20945 cx.update_editor(|editor, window, cx| {
20946 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20947 });
20948 executor.run_until_parked();
20949 cx.assert_index_text(None);
20950}
20951
20952async fn setup_indent_guides_editor(
20953 text: &str,
20954 cx: &mut TestAppContext,
20955) -> (BufferId, EditorTestContext) {
20956 init_test(cx, |_| {});
20957
20958 let mut cx = EditorTestContext::new(cx).await;
20959
20960 let buffer_id = cx.update_editor(|editor, window, cx| {
20961 editor.set_text(text, window, cx);
20962 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20963
20964 buffer_ids[0]
20965 });
20966
20967 (buffer_id, cx)
20968}
20969
20970fn assert_indent_guides(
20971 range: Range<u32>,
20972 expected: Vec<IndentGuide>,
20973 active_indices: Option<Vec<usize>>,
20974 cx: &mut EditorTestContext,
20975) {
20976 let indent_guides = cx.update_editor(|editor, window, cx| {
20977 let snapshot = editor.snapshot(window, cx).display_snapshot;
20978 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20979 editor,
20980 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20981 true,
20982 &snapshot,
20983 cx,
20984 );
20985
20986 indent_guides.sort_by(|a, b| {
20987 a.depth.cmp(&b.depth).then(
20988 a.start_row
20989 .cmp(&b.start_row)
20990 .then(a.end_row.cmp(&b.end_row)),
20991 )
20992 });
20993 indent_guides
20994 });
20995
20996 if let Some(expected) = active_indices {
20997 let active_indices = cx.update_editor(|editor, window, cx| {
20998 let snapshot = editor.snapshot(window, cx).display_snapshot;
20999 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21000 });
21001
21002 assert_eq!(
21003 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21004 expected,
21005 "Active indent guide indices do not match"
21006 );
21007 }
21008
21009 assert_eq!(indent_guides, expected, "Indent guides do not match");
21010}
21011
21012fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21013 IndentGuide {
21014 buffer_id,
21015 start_row: MultiBufferRow(start_row),
21016 end_row: MultiBufferRow(end_row),
21017 depth,
21018 tab_size: 4,
21019 settings: IndentGuideSettings {
21020 enabled: true,
21021 line_width: 1,
21022 active_line_width: 1,
21023 coloring: IndentGuideColoring::default(),
21024 background_coloring: IndentGuideBackgroundColoring::default(),
21025 },
21026 }
21027}
21028
21029#[gpui::test]
21030async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21031 let (buffer_id, mut cx) = setup_indent_guides_editor(
21032 &"
21033 fn main() {
21034 let a = 1;
21035 }"
21036 .unindent(),
21037 cx,
21038 )
21039 .await;
21040
21041 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21042}
21043
21044#[gpui::test]
21045async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21046 let (buffer_id, mut cx) = setup_indent_guides_editor(
21047 &"
21048 fn main() {
21049 let a = 1;
21050 let b = 2;
21051 }"
21052 .unindent(),
21053 cx,
21054 )
21055 .await;
21056
21057 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21058}
21059
21060#[gpui::test]
21061async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21062 let (buffer_id, mut cx) = setup_indent_guides_editor(
21063 &"
21064 fn main() {
21065 let a = 1;
21066 if a == 3 {
21067 let b = 2;
21068 } else {
21069 let c = 3;
21070 }
21071 }"
21072 .unindent(),
21073 cx,
21074 )
21075 .await;
21076
21077 assert_indent_guides(
21078 0..8,
21079 vec![
21080 indent_guide(buffer_id, 1, 6, 0),
21081 indent_guide(buffer_id, 3, 3, 1),
21082 indent_guide(buffer_id, 5, 5, 1),
21083 ],
21084 None,
21085 &mut cx,
21086 );
21087}
21088
21089#[gpui::test]
21090async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21091 let (buffer_id, mut cx) = setup_indent_guides_editor(
21092 &"
21093 fn main() {
21094 let a = 1;
21095 let b = 2;
21096 let c = 3;
21097 }"
21098 .unindent(),
21099 cx,
21100 )
21101 .await;
21102
21103 assert_indent_guides(
21104 0..5,
21105 vec![
21106 indent_guide(buffer_id, 1, 3, 0),
21107 indent_guide(buffer_id, 2, 2, 1),
21108 ],
21109 None,
21110 &mut cx,
21111 );
21112}
21113
21114#[gpui::test]
21115async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21116 let (buffer_id, mut cx) = setup_indent_guides_editor(
21117 &"
21118 fn main() {
21119 let a = 1;
21120
21121 let c = 3;
21122 }"
21123 .unindent(),
21124 cx,
21125 )
21126 .await;
21127
21128 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21129}
21130
21131#[gpui::test]
21132async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21133 let (buffer_id, mut cx) = setup_indent_guides_editor(
21134 &"
21135 fn main() {
21136 let a = 1;
21137
21138 let c = 3;
21139
21140 if a == 3 {
21141 let b = 2;
21142 } else {
21143 let c = 3;
21144 }
21145 }"
21146 .unindent(),
21147 cx,
21148 )
21149 .await;
21150
21151 assert_indent_guides(
21152 0..11,
21153 vec![
21154 indent_guide(buffer_id, 1, 9, 0),
21155 indent_guide(buffer_id, 6, 6, 1),
21156 indent_guide(buffer_id, 8, 8, 1),
21157 ],
21158 None,
21159 &mut cx,
21160 );
21161}
21162
21163#[gpui::test]
21164async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21165 let (buffer_id, mut cx) = setup_indent_guides_editor(
21166 &"
21167 fn main() {
21168 let a = 1;
21169
21170 let c = 3;
21171
21172 if a == 3 {
21173 let b = 2;
21174 } else {
21175 let c = 3;
21176 }
21177 }"
21178 .unindent(),
21179 cx,
21180 )
21181 .await;
21182
21183 assert_indent_guides(
21184 1..11,
21185 vec![
21186 indent_guide(buffer_id, 1, 9, 0),
21187 indent_guide(buffer_id, 6, 6, 1),
21188 indent_guide(buffer_id, 8, 8, 1),
21189 ],
21190 None,
21191 &mut cx,
21192 );
21193}
21194
21195#[gpui::test]
21196async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21197 let (buffer_id, mut cx) = setup_indent_guides_editor(
21198 &"
21199 fn main() {
21200 let a = 1;
21201
21202 let c = 3;
21203
21204 if a == 3 {
21205 let b = 2;
21206 } else {
21207 let c = 3;
21208 }
21209 }"
21210 .unindent(),
21211 cx,
21212 )
21213 .await;
21214
21215 assert_indent_guides(
21216 1..10,
21217 vec![
21218 indent_guide(buffer_id, 1, 9, 0),
21219 indent_guide(buffer_id, 6, 6, 1),
21220 indent_guide(buffer_id, 8, 8, 1),
21221 ],
21222 None,
21223 &mut cx,
21224 );
21225}
21226
21227#[gpui::test]
21228async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21229 let (buffer_id, mut cx) = setup_indent_guides_editor(
21230 &"
21231 fn main() {
21232 if a {
21233 b(
21234 c,
21235 d,
21236 )
21237 } else {
21238 e(
21239 f
21240 )
21241 }
21242 }"
21243 .unindent(),
21244 cx,
21245 )
21246 .await;
21247
21248 assert_indent_guides(
21249 0..11,
21250 vec![
21251 indent_guide(buffer_id, 1, 10, 0),
21252 indent_guide(buffer_id, 2, 5, 1),
21253 indent_guide(buffer_id, 7, 9, 1),
21254 indent_guide(buffer_id, 3, 4, 2),
21255 indent_guide(buffer_id, 8, 8, 2),
21256 ],
21257 None,
21258 &mut cx,
21259 );
21260
21261 cx.update_editor(|editor, window, cx| {
21262 editor.fold_at(MultiBufferRow(2), window, cx);
21263 assert_eq!(
21264 editor.display_text(cx),
21265 "
21266 fn main() {
21267 if a {
21268 b(⋯
21269 )
21270 } else {
21271 e(
21272 f
21273 )
21274 }
21275 }"
21276 .unindent()
21277 );
21278 });
21279
21280 assert_indent_guides(
21281 0..11,
21282 vec![
21283 indent_guide(buffer_id, 1, 10, 0),
21284 indent_guide(buffer_id, 2, 5, 1),
21285 indent_guide(buffer_id, 7, 9, 1),
21286 indent_guide(buffer_id, 8, 8, 2),
21287 ],
21288 None,
21289 &mut cx,
21290 );
21291}
21292
21293#[gpui::test]
21294async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21295 let (buffer_id, mut cx) = setup_indent_guides_editor(
21296 &"
21297 block1
21298 block2
21299 block3
21300 block4
21301 block2
21302 block1
21303 block1"
21304 .unindent(),
21305 cx,
21306 )
21307 .await;
21308
21309 assert_indent_guides(
21310 1..10,
21311 vec![
21312 indent_guide(buffer_id, 1, 4, 0),
21313 indent_guide(buffer_id, 2, 3, 1),
21314 indent_guide(buffer_id, 3, 3, 2),
21315 ],
21316 None,
21317 &mut cx,
21318 );
21319}
21320
21321#[gpui::test]
21322async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21323 let (buffer_id, mut cx) = setup_indent_guides_editor(
21324 &"
21325 block1
21326 block2
21327 block3
21328
21329 block1
21330 block1"
21331 .unindent(),
21332 cx,
21333 )
21334 .await;
21335
21336 assert_indent_guides(
21337 0..6,
21338 vec![
21339 indent_guide(buffer_id, 1, 2, 0),
21340 indent_guide(buffer_id, 2, 2, 1),
21341 ],
21342 None,
21343 &mut cx,
21344 );
21345}
21346
21347#[gpui::test]
21348async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21349 let (buffer_id, mut cx) = setup_indent_guides_editor(
21350 &"
21351 function component() {
21352 \treturn (
21353 \t\t\t
21354 \t\t<div>
21355 \t\t\t<abc></abc>
21356 \t\t</div>
21357 \t)
21358 }"
21359 .unindent(),
21360 cx,
21361 )
21362 .await;
21363
21364 assert_indent_guides(
21365 0..8,
21366 vec![
21367 indent_guide(buffer_id, 1, 6, 0),
21368 indent_guide(buffer_id, 2, 5, 1),
21369 indent_guide(buffer_id, 4, 4, 2),
21370 ],
21371 None,
21372 &mut cx,
21373 );
21374}
21375
21376#[gpui::test]
21377async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21378 let (buffer_id, mut cx) = setup_indent_guides_editor(
21379 &"
21380 function component() {
21381 \treturn (
21382 \t
21383 \t\t<div>
21384 \t\t\t<abc></abc>
21385 \t\t</div>
21386 \t)
21387 }"
21388 .unindent(),
21389 cx,
21390 )
21391 .await;
21392
21393 assert_indent_guides(
21394 0..8,
21395 vec![
21396 indent_guide(buffer_id, 1, 6, 0),
21397 indent_guide(buffer_id, 2, 5, 1),
21398 indent_guide(buffer_id, 4, 4, 2),
21399 ],
21400 None,
21401 &mut cx,
21402 );
21403}
21404
21405#[gpui::test]
21406async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21407 let (buffer_id, mut cx) = setup_indent_guides_editor(
21408 &"
21409 block1
21410
21411
21412
21413 block2
21414 "
21415 .unindent(),
21416 cx,
21417 )
21418 .await;
21419
21420 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21421}
21422
21423#[gpui::test]
21424async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21425 let (buffer_id, mut cx) = setup_indent_guides_editor(
21426 &"
21427 def a:
21428 \tb = 3
21429 \tif True:
21430 \t\tc = 4
21431 \t\td = 5
21432 \tprint(b)
21433 "
21434 .unindent(),
21435 cx,
21436 )
21437 .await;
21438
21439 assert_indent_guides(
21440 0..6,
21441 vec![
21442 indent_guide(buffer_id, 1, 5, 0),
21443 indent_guide(buffer_id, 3, 4, 1),
21444 ],
21445 None,
21446 &mut cx,
21447 );
21448}
21449
21450#[gpui::test]
21451async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21452 let (buffer_id, mut cx) = setup_indent_guides_editor(
21453 &"
21454 fn main() {
21455 let a = 1;
21456 }"
21457 .unindent(),
21458 cx,
21459 )
21460 .await;
21461
21462 cx.update_editor(|editor, window, cx| {
21463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21464 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21465 });
21466 });
21467
21468 assert_indent_guides(
21469 0..3,
21470 vec![indent_guide(buffer_id, 1, 1, 0)],
21471 Some(vec![0]),
21472 &mut cx,
21473 );
21474}
21475
21476#[gpui::test]
21477async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21478 let (buffer_id, mut cx) = setup_indent_guides_editor(
21479 &"
21480 fn main() {
21481 if 1 == 2 {
21482 let a = 1;
21483 }
21484 }"
21485 .unindent(),
21486 cx,
21487 )
21488 .await;
21489
21490 cx.update_editor(|editor, window, cx| {
21491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21492 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21493 });
21494 });
21495
21496 assert_indent_guides(
21497 0..4,
21498 vec![
21499 indent_guide(buffer_id, 1, 3, 0),
21500 indent_guide(buffer_id, 2, 2, 1),
21501 ],
21502 Some(vec![1]),
21503 &mut cx,
21504 );
21505
21506 cx.update_editor(|editor, window, cx| {
21507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21508 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21509 });
21510 });
21511
21512 assert_indent_guides(
21513 0..4,
21514 vec![
21515 indent_guide(buffer_id, 1, 3, 0),
21516 indent_guide(buffer_id, 2, 2, 1),
21517 ],
21518 Some(vec![1]),
21519 &mut cx,
21520 );
21521
21522 cx.update_editor(|editor, window, cx| {
21523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21524 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21525 });
21526 });
21527
21528 assert_indent_guides(
21529 0..4,
21530 vec![
21531 indent_guide(buffer_id, 1, 3, 0),
21532 indent_guide(buffer_id, 2, 2, 1),
21533 ],
21534 Some(vec![0]),
21535 &mut cx,
21536 );
21537}
21538
21539#[gpui::test]
21540async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21541 let (buffer_id, mut cx) = setup_indent_guides_editor(
21542 &"
21543 fn main() {
21544 let a = 1;
21545
21546 let b = 2;
21547 }"
21548 .unindent(),
21549 cx,
21550 )
21551 .await;
21552
21553 cx.update_editor(|editor, window, cx| {
21554 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21555 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21556 });
21557 });
21558
21559 assert_indent_guides(
21560 0..5,
21561 vec![indent_guide(buffer_id, 1, 3, 0)],
21562 Some(vec![0]),
21563 &mut cx,
21564 );
21565}
21566
21567#[gpui::test]
21568async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21569 let (buffer_id, mut cx) = setup_indent_guides_editor(
21570 &"
21571 def m:
21572 a = 1
21573 pass"
21574 .unindent(),
21575 cx,
21576 )
21577 .await;
21578
21579 cx.update_editor(|editor, window, cx| {
21580 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21581 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21582 });
21583 });
21584
21585 assert_indent_guides(
21586 0..3,
21587 vec![indent_guide(buffer_id, 1, 2, 0)],
21588 Some(vec![0]),
21589 &mut cx,
21590 );
21591}
21592
21593#[gpui::test]
21594async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21595 init_test(cx, |_| {});
21596 let mut cx = EditorTestContext::new(cx).await;
21597 let text = indoc! {
21598 "
21599 impl A {
21600 fn b() {
21601 0;
21602 3;
21603 5;
21604 6;
21605 7;
21606 }
21607 }
21608 "
21609 };
21610 let base_text = indoc! {
21611 "
21612 impl A {
21613 fn b() {
21614 0;
21615 1;
21616 2;
21617 3;
21618 4;
21619 }
21620 fn c() {
21621 5;
21622 6;
21623 7;
21624 }
21625 }
21626 "
21627 };
21628
21629 cx.update_editor(|editor, window, cx| {
21630 editor.set_text(text, window, cx);
21631
21632 editor.buffer().update(cx, |multibuffer, cx| {
21633 let buffer = multibuffer.as_singleton().unwrap();
21634 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21635
21636 multibuffer.set_all_diff_hunks_expanded(cx);
21637 multibuffer.add_diff(diff, cx);
21638
21639 buffer.read(cx).remote_id()
21640 })
21641 });
21642 cx.run_until_parked();
21643
21644 cx.assert_state_with_diff(
21645 indoc! { "
21646 impl A {
21647 fn b() {
21648 0;
21649 - 1;
21650 - 2;
21651 3;
21652 - 4;
21653 - }
21654 - fn c() {
21655 5;
21656 6;
21657 7;
21658 }
21659 }
21660 ˇ"
21661 }
21662 .to_string(),
21663 );
21664
21665 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21666 editor
21667 .snapshot(window, cx)
21668 .buffer_snapshot()
21669 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21670 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21671 .collect::<Vec<_>>()
21672 });
21673 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21674 assert_eq!(
21675 actual_guides,
21676 vec![
21677 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21678 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21679 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21680 ]
21681 );
21682}
21683
21684#[gpui::test]
21685async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21686 init_test(cx, |_| {});
21687 let mut cx = EditorTestContext::new(cx).await;
21688
21689 let diff_base = r#"
21690 a
21691 b
21692 c
21693 "#
21694 .unindent();
21695
21696 cx.set_state(
21697 &r#"
21698 ˇA
21699 b
21700 C
21701 "#
21702 .unindent(),
21703 );
21704 cx.set_head_text(&diff_base);
21705 cx.update_editor(|editor, window, cx| {
21706 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21707 });
21708 executor.run_until_parked();
21709
21710 let both_hunks_expanded = r#"
21711 - a
21712 + ˇA
21713 b
21714 - c
21715 + C
21716 "#
21717 .unindent();
21718
21719 cx.assert_state_with_diff(both_hunks_expanded.clone());
21720
21721 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21722 let snapshot = editor.snapshot(window, cx);
21723 let hunks = editor
21724 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21725 .collect::<Vec<_>>();
21726 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21727 hunks
21728 .into_iter()
21729 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21730 .collect::<Vec<_>>()
21731 });
21732 assert_eq!(hunk_ranges.len(), 2);
21733
21734 cx.update_editor(|editor, _, cx| {
21735 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21736 });
21737 executor.run_until_parked();
21738
21739 let second_hunk_expanded = r#"
21740 ˇA
21741 b
21742 - c
21743 + C
21744 "#
21745 .unindent();
21746
21747 cx.assert_state_with_diff(second_hunk_expanded);
21748
21749 cx.update_editor(|editor, _, cx| {
21750 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21751 });
21752 executor.run_until_parked();
21753
21754 cx.assert_state_with_diff(both_hunks_expanded.clone());
21755
21756 cx.update_editor(|editor, _, cx| {
21757 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21758 });
21759 executor.run_until_parked();
21760
21761 let first_hunk_expanded = r#"
21762 - a
21763 + ˇA
21764 b
21765 C
21766 "#
21767 .unindent();
21768
21769 cx.assert_state_with_diff(first_hunk_expanded);
21770
21771 cx.update_editor(|editor, _, cx| {
21772 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21773 });
21774 executor.run_until_parked();
21775
21776 cx.assert_state_with_diff(both_hunks_expanded);
21777
21778 cx.set_state(
21779 &r#"
21780 ˇA
21781 b
21782 "#
21783 .unindent(),
21784 );
21785 cx.run_until_parked();
21786
21787 // TODO this cursor position seems bad
21788 cx.assert_state_with_diff(
21789 r#"
21790 - ˇa
21791 + A
21792 b
21793 "#
21794 .unindent(),
21795 );
21796
21797 cx.update_editor(|editor, window, cx| {
21798 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21799 });
21800
21801 cx.assert_state_with_diff(
21802 r#"
21803 - ˇa
21804 + A
21805 b
21806 - c
21807 "#
21808 .unindent(),
21809 );
21810
21811 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21812 let snapshot = editor.snapshot(window, cx);
21813 let hunks = editor
21814 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21815 .collect::<Vec<_>>();
21816 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21817 hunks
21818 .into_iter()
21819 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21820 .collect::<Vec<_>>()
21821 });
21822 assert_eq!(hunk_ranges.len(), 2);
21823
21824 cx.update_editor(|editor, _, cx| {
21825 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21826 });
21827 executor.run_until_parked();
21828
21829 cx.assert_state_with_diff(
21830 r#"
21831 - ˇa
21832 + A
21833 b
21834 "#
21835 .unindent(),
21836 );
21837}
21838
21839#[gpui::test]
21840async fn test_toggle_deletion_hunk_at_start_of_file(
21841 executor: BackgroundExecutor,
21842 cx: &mut TestAppContext,
21843) {
21844 init_test(cx, |_| {});
21845 let mut cx = EditorTestContext::new(cx).await;
21846
21847 let diff_base = r#"
21848 a
21849 b
21850 c
21851 "#
21852 .unindent();
21853
21854 cx.set_state(
21855 &r#"
21856 ˇb
21857 c
21858 "#
21859 .unindent(),
21860 );
21861 cx.set_head_text(&diff_base);
21862 cx.update_editor(|editor, window, cx| {
21863 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21864 });
21865 executor.run_until_parked();
21866
21867 let hunk_expanded = r#"
21868 - a
21869 ˇb
21870 c
21871 "#
21872 .unindent();
21873
21874 cx.assert_state_with_diff(hunk_expanded.clone());
21875
21876 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21877 let snapshot = editor.snapshot(window, cx);
21878 let hunks = editor
21879 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21880 .collect::<Vec<_>>();
21881 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21882 hunks
21883 .into_iter()
21884 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
21885 .collect::<Vec<_>>()
21886 });
21887 assert_eq!(hunk_ranges.len(), 1);
21888
21889 cx.update_editor(|editor, _, cx| {
21890 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21891 });
21892 executor.run_until_parked();
21893
21894 let hunk_collapsed = r#"
21895 ˇb
21896 c
21897 "#
21898 .unindent();
21899
21900 cx.assert_state_with_diff(hunk_collapsed);
21901
21902 cx.update_editor(|editor, _, cx| {
21903 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21904 });
21905 executor.run_until_parked();
21906
21907 cx.assert_state_with_diff(hunk_expanded);
21908}
21909
21910#[gpui::test]
21911async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21912 init_test(cx, |_| {});
21913
21914 let fs = FakeFs::new(cx.executor());
21915 fs.insert_tree(
21916 path!("/test"),
21917 json!({
21918 ".git": {},
21919 "file-1": "ONE\n",
21920 "file-2": "TWO\n",
21921 "file-3": "THREE\n",
21922 }),
21923 )
21924 .await;
21925
21926 fs.set_head_for_repo(
21927 path!("/test/.git").as_ref(),
21928 &[
21929 ("file-1", "one\n".into()),
21930 ("file-2", "two\n".into()),
21931 ("file-3", "three\n".into()),
21932 ],
21933 "deadbeef",
21934 );
21935
21936 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21937 let mut buffers = vec![];
21938 for i in 1..=3 {
21939 let buffer = project
21940 .update(cx, |project, cx| {
21941 let path = format!(path!("/test/file-{}"), i);
21942 project.open_local_buffer(path, cx)
21943 })
21944 .await
21945 .unwrap();
21946 buffers.push(buffer);
21947 }
21948
21949 let multibuffer = cx.new(|cx| {
21950 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21951 multibuffer.set_all_diff_hunks_expanded(cx);
21952 for buffer in &buffers {
21953 let snapshot = buffer.read(cx).snapshot();
21954 multibuffer.set_excerpts_for_path(
21955 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21956 buffer.clone(),
21957 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21958 2,
21959 cx,
21960 );
21961 }
21962 multibuffer
21963 });
21964
21965 let editor = cx.add_window(|window, cx| {
21966 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21967 });
21968 cx.run_until_parked();
21969
21970 let snapshot = editor
21971 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21972 .unwrap();
21973 let hunks = snapshot
21974 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21975 .map(|hunk| match hunk {
21976 DisplayDiffHunk::Unfolded {
21977 display_row_range, ..
21978 } => display_row_range,
21979 DisplayDiffHunk::Folded { .. } => unreachable!(),
21980 })
21981 .collect::<Vec<_>>();
21982 assert_eq!(
21983 hunks,
21984 [
21985 DisplayRow(2)..DisplayRow(4),
21986 DisplayRow(7)..DisplayRow(9),
21987 DisplayRow(12)..DisplayRow(14),
21988 ]
21989 );
21990}
21991
21992#[gpui::test]
21993async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21994 init_test(cx, |_| {});
21995
21996 let mut cx = EditorTestContext::new(cx).await;
21997 cx.set_head_text(indoc! { "
21998 one
21999 two
22000 three
22001 four
22002 five
22003 "
22004 });
22005 cx.set_index_text(indoc! { "
22006 one
22007 two
22008 three
22009 four
22010 five
22011 "
22012 });
22013 cx.set_state(indoc! {"
22014 one
22015 TWO
22016 ˇTHREE
22017 FOUR
22018 five
22019 "});
22020 cx.run_until_parked();
22021 cx.update_editor(|editor, window, cx| {
22022 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22023 });
22024 cx.run_until_parked();
22025 cx.assert_index_text(Some(indoc! {"
22026 one
22027 TWO
22028 THREE
22029 FOUR
22030 five
22031 "}));
22032 cx.set_state(indoc! { "
22033 one
22034 TWO
22035 ˇTHREE-HUNDRED
22036 FOUR
22037 five
22038 "});
22039 cx.run_until_parked();
22040 cx.update_editor(|editor, window, cx| {
22041 let snapshot = editor.snapshot(window, cx);
22042 let hunks = editor
22043 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22044 .collect::<Vec<_>>();
22045 assert_eq!(hunks.len(), 1);
22046 assert_eq!(
22047 hunks[0].status(),
22048 DiffHunkStatus {
22049 kind: DiffHunkStatusKind::Modified,
22050 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22051 }
22052 );
22053
22054 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22055 });
22056 cx.run_until_parked();
22057 cx.assert_index_text(Some(indoc! {"
22058 one
22059 TWO
22060 THREE-HUNDRED
22061 FOUR
22062 five
22063 "}));
22064}
22065
22066#[gpui::test]
22067fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22068 init_test(cx, |_| {});
22069
22070 let editor = cx.add_window(|window, cx| {
22071 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22072 build_editor(buffer, window, cx)
22073 });
22074
22075 let render_args = Arc::new(Mutex::new(None));
22076 let snapshot = editor
22077 .update(cx, |editor, window, cx| {
22078 let snapshot = editor.buffer().read(cx).snapshot(cx);
22079 let range =
22080 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22081
22082 struct RenderArgs {
22083 row: MultiBufferRow,
22084 folded: bool,
22085 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22086 }
22087
22088 let crease = Crease::inline(
22089 range,
22090 FoldPlaceholder::test(),
22091 {
22092 let toggle_callback = render_args.clone();
22093 move |row, folded, callback, _window, _cx| {
22094 *toggle_callback.lock() = Some(RenderArgs {
22095 row,
22096 folded,
22097 callback,
22098 });
22099 div()
22100 }
22101 },
22102 |_row, _folded, _window, _cx| div(),
22103 );
22104
22105 editor.insert_creases(Some(crease), cx);
22106 let snapshot = editor.snapshot(window, cx);
22107 let _div =
22108 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22109 snapshot
22110 })
22111 .unwrap();
22112
22113 let render_args = render_args.lock().take().unwrap();
22114 assert_eq!(render_args.row, MultiBufferRow(1));
22115 assert!(!render_args.folded);
22116 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22117
22118 cx.update_window(*editor, |_, window, cx| {
22119 (render_args.callback)(true, window, cx)
22120 })
22121 .unwrap();
22122 let snapshot = editor
22123 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22124 .unwrap();
22125 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22126
22127 cx.update_window(*editor, |_, window, cx| {
22128 (render_args.callback)(false, window, cx)
22129 })
22130 .unwrap();
22131 let snapshot = editor
22132 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22133 .unwrap();
22134 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22135}
22136
22137#[gpui::test]
22138async fn test_input_text(cx: &mut TestAppContext) {
22139 init_test(cx, |_| {});
22140 let mut cx = EditorTestContext::new(cx).await;
22141
22142 cx.set_state(
22143 &r#"ˇone
22144 two
22145
22146 three
22147 fourˇ
22148 five
22149
22150 siˇx"#
22151 .unindent(),
22152 );
22153
22154 cx.dispatch_action(HandleInput(String::new()));
22155 cx.assert_editor_state(
22156 &r#"ˇone
22157 two
22158
22159 three
22160 fourˇ
22161 five
22162
22163 siˇx"#
22164 .unindent(),
22165 );
22166
22167 cx.dispatch_action(HandleInput("AAAA".to_string()));
22168 cx.assert_editor_state(
22169 &r#"AAAAˇone
22170 two
22171
22172 three
22173 fourAAAAˇ
22174 five
22175
22176 siAAAAˇx"#
22177 .unindent(),
22178 );
22179}
22180
22181#[gpui::test]
22182async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22183 init_test(cx, |_| {});
22184
22185 let mut cx = EditorTestContext::new(cx).await;
22186 cx.set_state(
22187 r#"let foo = 1;
22188let foo = 2;
22189let foo = 3;
22190let fooˇ = 4;
22191let foo = 5;
22192let foo = 6;
22193let foo = 7;
22194let foo = 8;
22195let foo = 9;
22196let foo = 10;
22197let foo = 11;
22198let foo = 12;
22199let foo = 13;
22200let foo = 14;
22201let foo = 15;"#,
22202 );
22203
22204 cx.update_editor(|e, window, cx| {
22205 assert_eq!(
22206 e.next_scroll_position,
22207 NextScrollCursorCenterTopBottom::Center,
22208 "Default next scroll direction is center",
22209 );
22210
22211 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22212 assert_eq!(
22213 e.next_scroll_position,
22214 NextScrollCursorCenterTopBottom::Top,
22215 "After center, next scroll direction should be top",
22216 );
22217
22218 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22219 assert_eq!(
22220 e.next_scroll_position,
22221 NextScrollCursorCenterTopBottom::Bottom,
22222 "After top, next scroll direction should be bottom",
22223 );
22224
22225 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22226 assert_eq!(
22227 e.next_scroll_position,
22228 NextScrollCursorCenterTopBottom::Center,
22229 "After bottom, scrolling should start over",
22230 );
22231
22232 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22233 assert_eq!(
22234 e.next_scroll_position,
22235 NextScrollCursorCenterTopBottom::Top,
22236 "Scrolling continues if retriggered fast enough"
22237 );
22238 });
22239
22240 cx.executor()
22241 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22242 cx.executor().run_until_parked();
22243 cx.update_editor(|e, _, _| {
22244 assert_eq!(
22245 e.next_scroll_position,
22246 NextScrollCursorCenterTopBottom::Center,
22247 "If scrolling is not triggered fast enough, it should reset"
22248 );
22249 });
22250}
22251
22252#[gpui::test]
22253async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22254 init_test(cx, |_| {});
22255 let mut cx = EditorLspTestContext::new_rust(
22256 lsp::ServerCapabilities {
22257 definition_provider: Some(lsp::OneOf::Left(true)),
22258 references_provider: Some(lsp::OneOf::Left(true)),
22259 ..lsp::ServerCapabilities::default()
22260 },
22261 cx,
22262 )
22263 .await;
22264
22265 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22266 let go_to_definition = cx
22267 .lsp
22268 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22269 move |params, _| async move {
22270 if empty_go_to_definition {
22271 Ok(None)
22272 } else {
22273 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22274 uri: params.text_document_position_params.text_document.uri,
22275 range: lsp::Range::new(
22276 lsp::Position::new(4, 3),
22277 lsp::Position::new(4, 6),
22278 ),
22279 })))
22280 }
22281 },
22282 );
22283 let references = cx
22284 .lsp
22285 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22286 Ok(Some(vec![lsp::Location {
22287 uri: params.text_document_position.text_document.uri,
22288 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22289 }]))
22290 });
22291 (go_to_definition, references)
22292 };
22293
22294 cx.set_state(
22295 &r#"fn one() {
22296 let mut a = ˇtwo();
22297 }
22298
22299 fn two() {}"#
22300 .unindent(),
22301 );
22302 set_up_lsp_handlers(false, &mut cx);
22303 let navigated = cx
22304 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22305 .await
22306 .expect("Failed to navigate to definition");
22307 assert_eq!(
22308 navigated,
22309 Navigated::Yes,
22310 "Should have navigated to definition from the GetDefinition response"
22311 );
22312 cx.assert_editor_state(
22313 &r#"fn one() {
22314 let mut a = two();
22315 }
22316
22317 fn «twoˇ»() {}"#
22318 .unindent(),
22319 );
22320
22321 let editors = cx.update_workspace(|workspace, _, cx| {
22322 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22323 });
22324 cx.update_editor(|_, _, test_editor_cx| {
22325 assert_eq!(
22326 editors.len(),
22327 1,
22328 "Initially, only one, test, editor should be open in the workspace"
22329 );
22330 assert_eq!(
22331 test_editor_cx.entity(),
22332 editors.last().expect("Asserted len is 1").clone()
22333 );
22334 });
22335
22336 set_up_lsp_handlers(true, &mut cx);
22337 let navigated = cx
22338 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22339 .await
22340 .expect("Failed to navigate to lookup references");
22341 assert_eq!(
22342 navigated,
22343 Navigated::Yes,
22344 "Should have navigated to references as a fallback after empty GoToDefinition response"
22345 );
22346 // We should not change the selections in the existing file,
22347 // if opening another milti buffer with the references
22348 cx.assert_editor_state(
22349 &r#"fn one() {
22350 let mut a = two();
22351 }
22352
22353 fn «twoˇ»() {}"#
22354 .unindent(),
22355 );
22356 let editors = cx.update_workspace(|workspace, _, cx| {
22357 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22358 });
22359 cx.update_editor(|_, _, test_editor_cx| {
22360 assert_eq!(
22361 editors.len(),
22362 2,
22363 "After falling back to references search, we open a new editor with the results"
22364 );
22365 let references_fallback_text = editors
22366 .into_iter()
22367 .find(|new_editor| *new_editor != test_editor_cx.entity())
22368 .expect("Should have one non-test editor now")
22369 .read(test_editor_cx)
22370 .text(test_editor_cx);
22371 assert_eq!(
22372 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22373 "Should use the range from the references response and not the GoToDefinition one"
22374 );
22375 });
22376}
22377
22378#[gpui::test]
22379async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22380 init_test(cx, |_| {});
22381 cx.update(|cx| {
22382 let mut editor_settings = EditorSettings::get_global(cx).clone();
22383 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22384 EditorSettings::override_global(editor_settings, cx);
22385 });
22386 let mut cx = EditorLspTestContext::new_rust(
22387 lsp::ServerCapabilities {
22388 definition_provider: Some(lsp::OneOf::Left(true)),
22389 references_provider: Some(lsp::OneOf::Left(true)),
22390 ..lsp::ServerCapabilities::default()
22391 },
22392 cx,
22393 )
22394 .await;
22395 let original_state = r#"fn one() {
22396 let mut a = ˇtwo();
22397 }
22398
22399 fn two() {}"#
22400 .unindent();
22401 cx.set_state(&original_state);
22402
22403 let mut go_to_definition = cx
22404 .lsp
22405 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22406 move |_, _| async move { Ok(None) },
22407 );
22408 let _references = cx
22409 .lsp
22410 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22411 panic!("Should not call for references with no go to definition fallback")
22412 });
22413
22414 let navigated = cx
22415 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22416 .await
22417 .expect("Failed to navigate to lookup references");
22418 go_to_definition
22419 .next()
22420 .await
22421 .expect("Should have called the go_to_definition handler");
22422
22423 assert_eq!(
22424 navigated,
22425 Navigated::No,
22426 "Should have navigated to references as a fallback after empty GoToDefinition response"
22427 );
22428 cx.assert_editor_state(&original_state);
22429 let editors = cx.update_workspace(|workspace, _, cx| {
22430 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22431 });
22432 cx.update_editor(|_, _, _| {
22433 assert_eq!(
22434 editors.len(),
22435 1,
22436 "After unsuccessful fallback, no other editor should have been opened"
22437 );
22438 });
22439}
22440
22441#[gpui::test]
22442async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22443 init_test(cx, |_| {});
22444 let mut cx = EditorLspTestContext::new_rust(
22445 lsp::ServerCapabilities {
22446 references_provider: Some(lsp::OneOf::Left(true)),
22447 ..lsp::ServerCapabilities::default()
22448 },
22449 cx,
22450 )
22451 .await;
22452
22453 cx.set_state(
22454 &r#"
22455 fn one() {
22456 let mut a = two();
22457 }
22458
22459 fn ˇtwo() {}"#
22460 .unindent(),
22461 );
22462 cx.lsp
22463 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22464 Ok(Some(vec![
22465 lsp::Location {
22466 uri: params.text_document_position.text_document.uri.clone(),
22467 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22468 },
22469 lsp::Location {
22470 uri: params.text_document_position.text_document.uri,
22471 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22472 },
22473 ]))
22474 });
22475 let navigated = cx
22476 .update_editor(|editor, window, cx| {
22477 editor.find_all_references(&FindAllReferences, window, cx)
22478 })
22479 .unwrap()
22480 .await
22481 .expect("Failed to navigate to references");
22482 assert_eq!(
22483 navigated,
22484 Navigated::Yes,
22485 "Should have navigated to references from the FindAllReferences response"
22486 );
22487 cx.assert_editor_state(
22488 &r#"fn one() {
22489 let mut a = two();
22490 }
22491
22492 fn ˇtwo() {}"#
22493 .unindent(),
22494 );
22495
22496 let editors = cx.update_workspace(|workspace, _, cx| {
22497 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22498 });
22499 cx.update_editor(|_, _, _| {
22500 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22501 });
22502
22503 cx.set_state(
22504 &r#"fn one() {
22505 let mut a = ˇtwo();
22506 }
22507
22508 fn two() {}"#
22509 .unindent(),
22510 );
22511 let navigated = cx
22512 .update_editor(|editor, window, cx| {
22513 editor.find_all_references(&FindAllReferences, window, cx)
22514 })
22515 .unwrap()
22516 .await
22517 .expect("Failed to navigate to references");
22518 assert_eq!(
22519 navigated,
22520 Navigated::Yes,
22521 "Should have navigated to references from the FindAllReferences response"
22522 );
22523 cx.assert_editor_state(
22524 &r#"fn one() {
22525 let mut a = ˇtwo();
22526 }
22527
22528 fn two() {}"#
22529 .unindent(),
22530 );
22531 let editors = cx.update_workspace(|workspace, _, cx| {
22532 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22533 });
22534 cx.update_editor(|_, _, _| {
22535 assert_eq!(
22536 editors.len(),
22537 2,
22538 "should have re-used the previous multibuffer"
22539 );
22540 });
22541
22542 cx.set_state(
22543 &r#"fn one() {
22544 let mut a = ˇtwo();
22545 }
22546 fn three() {}
22547 fn two() {}"#
22548 .unindent(),
22549 );
22550 cx.lsp
22551 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22552 Ok(Some(vec![
22553 lsp::Location {
22554 uri: params.text_document_position.text_document.uri.clone(),
22555 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22556 },
22557 lsp::Location {
22558 uri: params.text_document_position.text_document.uri,
22559 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22560 },
22561 ]))
22562 });
22563 let navigated = cx
22564 .update_editor(|editor, window, cx| {
22565 editor.find_all_references(&FindAllReferences, window, cx)
22566 })
22567 .unwrap()
22568 .await
22569 .expect("Failed to navigate to references");
22570 assert_eq!(
22571 navigated,
22572 Navigated::Yes,
22573 "Should have navigated to references from the FindAllReferences response"
22574 );
22575 cx.assert_editor_state(
22576 &r#"fn one() {
22577 let mut a = ˇtwo();
22578 }
22579 fn three() {}
22580 fn two() {}"#
22581 .unindent(),
22582 );
22583 let editors = cx.update_workspace(|workspace, _, cx| {
22584 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22585 });
22586 cx.update_editor(|_, _, _| {
22587 assert_eq!(
22588 editors.len(),
22589 3,
22590 "should have used a new multibuffer as offsets changed"
22591 );
22592 });
22593}
22594#[gpui::test]
22595async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22596 init_test(cx, |_| {});
22597
22598 let language = Arc::new(Language::new(
22599 LanguageConfig::default(),
22600 Some(tree_sitter_rust::LANGUAGE.into()),
22601 ));
22602
22603 let text = r#"
22604 #[cfg(test)]
22605 mod tests() {
22606 #[test]
22607 fn runnable_1() {
22608 let a = 1;
22609 }
22610
22611 #[test]
22612 fn runnable_2() {
22613 let a = 1;
22614 let b = 2;
22615 }
22616 }
22617 "#
22618 .unindent();
22619
22620 let fs = FakeFs::new(cx.executor());
22621 fs.insert_file("/file.rs", Default::default()).await;
22622
22623 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22624 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22625 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22626 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22627 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22628
22629 let editor = cx.new_window_entity(|window, cx| {
22630 Editor::new(
22631 EditorMode::full(),
22632 multi_buffer,
22633 Some(project.clone()),
22634 window,
22635 cx,
22636 )
22637 });
22638
22639 editor.update_in(cx, |editor, window, cx| {
22640 let snapshot = editor.buffer().read(cx).snapshot(cx);
22641 editor.tasks.insert(
22642 (buffer.read(cx).remote_id(), 3),
22643 RunnableTasks {
22644 templates: vec![],
22645 offset: snapshot.anchor_before(MultiBufferOffset(43)),
22646 column: 0,
22647 extra_variables: HashMap::default(),
22648 context_range: BufferOffset(43)..BufferOffset(85),
22649 },
22650 );
22651 editor.tasks.insert(
22652 (buffer.read(cx).remote_id(), 8),
22653 RunnableTasks {
22654 templates: vec![],
22655 offset: snapshot.anchor_before(MultiBufferOffset(86)),
22656 column: 0,
22657 extra_variables: HashMap::default(),
22658 context_range: BufferOffset(86)..BufferOffset(191),
22659 },
22660 );
22661
22662 // Test finding task when cursor is inside function body
22663 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22664 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22665 });
22666 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22667 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22668
22669 // Test finding task when cursor is on function name
22670 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22671 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22672 });
22673 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22674 assert_eq!(row, 8, "Should find task when cursor is on function name");
22675 });
22676}
22677
22678#[gpui::test]
22679async fn test_folding_buffers(cx: &mut TestAppContext) {
22680 init_test(cx, |_| {});
22681
22682 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22683 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22684 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22685
22686 let fs = FakeFs::new(cx.executor());
22687 fs.insert_tree(
22688 path!("/a"),
22689 json!({
22690 "first.rs": sample_text_1,
22691 "second.rs": sample_text_2,
22692 "third.rs": sample_text_3,
22693 }),
22694 )
22695 .await;
22696 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22697 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22698 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22699 let worktree = project.update(cx, |project, cx| {
22700 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22701 assert_eq!(worktrees.len(), 1);
22702 worktrees.pop().unwrap()
22703 });
22704 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22705
22706 let buffer_1 = project
22707 .update(cx, |project, cx| {
22708 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22709 })
22710 .await
22711 .unwrap();
22712 let buffer_2 = project
22713 .update(cx, |project, cx| {
22714 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22715 })
22716 .await
22717 .unwrap();
22718 let buffer_3 = project
22719 .update(cx, |project, cx| {
22720 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22721 })
22722 .await
22723 .unwrap();
22724
22725 let multi_buffer = cx.new(|cx| {
22726 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22727 multi_buffer.push_excerpts(
22728 buffer_1.clone(),
22729 [
22730 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22731 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22732 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22733 ],
22734 cx,
22735 );
22736 multi_buffer.push_excerpts(
22737 buffer_2.clone(),
22738 [
22739 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22740 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22741 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22742 ],
22743 cx,
22744 );
22745 multi_buffer.push_excerpts(
22746 buffer_3.clone(),
22747 [
22748 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22749 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22750 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22751 ],
22752 cx,
22753 );
22754 multi_buffer
22755 });
22756 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22757 Editor::new(
22758 EditorMode::full(),
22759 multi_buffer.clone(),
22760 Some(project.clone()),
22761 window,
22762 cx,
22763 )
22764 });
22765
22766 assert_eq!(
22767 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22768 "\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",
22769 );
22770
22771 multi_buffer_editor.update(cx, |editor, cx| {
22772 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22773 });
22774 assert_eq!(
22775 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22776 "\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",
22777 "After folding the first buffer, its text should not be displayed"
22778 );
22779
22780 multi_buffer_editor.update(cx, |editor, cx| {
22781 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22782 });
22783 assert_eq!(
22784 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22785 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22786 "After folding the second buffer, its text should not be displayed"
22787 );
22788
22789 multi_buffer_editor.update(cx, |editor, cx| {
22790 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22791 });
22792 assert_eq!(
22793 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22794 "\n\n\n\n\n",
22795 "After folding the third buffer, its text should not be displayed"
22796 );
22797
22798 // Emulate selection inside the fold logic, that should work
22799 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22800 editor
22801 .snapshot(window, cx)
22802 .next_line_boundary(Point::new(0, 4));
22803 });
22804
22805 multi_buffer_editor.update(cx, |editor, cx| {
22806 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22807 });
22808 assert_eq!(
22809 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22810 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22811 "After unfolding the second buffer, its text should be displayed"
22812 );
22813
22814 // Typing inside of buffer 1 causes that buffer to be unfolded.
22815 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22816 assert_eq!(
22817 multi_buffer
22818 .read(cx)
22819 .snapshot(cx)
22820 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22821 .collect::<String>(),
22822 "bbbb"
22823 );
22824 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22825 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22826 });
22827 editor.handle_input("B", window, cx);
22828 });
22829
22830 assert_eq!(
22831 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22832 "\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",
22833 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22834 );
22835
22836 multi_buffer_editor.update(cx, |editor, cx| {
22837 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22838 });
22839 assert_eq!(
22840 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22841 "\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",
22842 "After unfolding the all buffers, all original text should be displayed"
22843 );
22844}
22845
22846#[gpui::test]
22847async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22848 init_test(cx, |_| {});
22849
22850 let sample_text_1 = "1111\n2222\n3333".to_string();
22851 let sample_text_2 = "4444\n5555\n6666".to_string();
22852 let sample_text_3 = "7777\n8888\n9999".to_string();
22853
22854 let fs = FakeFs::new(cx.executor());
22855 fs.insert_tree(
22856 path!("/a"),
22857 json!({
22858 "first.rs": sample_text_1,
22859 "second.rs": sample_text_2,
22860 "third.rs": sample_text_3,
22861 }),
22862 )
22863 .await;
22864 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22865 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22866 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22867 let worktree = project.update(cx, |project, cx| {
22868 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22869 assert_eq!(worktrees.len(), 1);
22870 worktrees.pop().unwrap()
22871 });
22872 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22873
22874 let buffer_1 = project
22875 .update(cx, |project, cx| {
22876 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22877 })
22878 .await
22879 .unwrap();
22880 let buffer_2 = project
22881 .update(cx, |project, cx| {
22882 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22883 })
22884 .await
22885 .unwrap();
22886 let buffer_3 = project
22887 .update(cx, |project, cx| {
22888 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22889 })
22890 .await
22891 .unwrap();
22892
22893 let multi_buffer = cx.new(|cx| {
22894 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22895 multi_buffer.push_excerpts(
22896 buffer_1.clone(),
22897 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22898 cx,
22899 );
22900 multi_buffer.push_excerpts(
22901 buffer_2.clone(),
22902 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22903 cx,
22904 );
22905 multi_buffer.push_excerpts(
22906 buffer_3.clone(),
22907 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22908 cx,
22909 );
22910 multi_buffer
22911 });
22912
22913 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22914 Editor::new(
22915 EditorMode::full(),
22916 multi_buffer,
22917 Some(project.clone()),
22918 window,
22919 cx,
22920 )
22921 });
22922
22923 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22924 assert_eq!(
22925 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22926 full_text,
22927 );
22928
22929 multi_buffer_editor.update(cx, |editor, cx| {
22930 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22931 });
22932 assert_eq!(
22933 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22934 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22935 "After folding the first buffer, its text should not be displayed"
22936 );
22937
22938 multi_buffer_editor.update(cx, |editor, cx| {
22939 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22940 });
22941
22942 assert_eq!(
22943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22944 "\n\n\n\n\n\n7777\n8888\n9999",
22945 "After folding the second buffer, its text should not be displayed"
22946 );
22947
22948 multi_buffer_editor.update(cx, |editor, cx| {
22949 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22950 });
22951 assert_eq!(
22952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22953 "\n\n\n\n\n",
22954 "After folding the third buffer, its text should not be displayed"
22955 );
22956
22957 multi_buffer_editor.update(cx, |editor, cx| {
22958 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22959 });
22960 assert_eq!(
22961 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22962 "\n\n\n\n4444\n5555\n6666\n\n",
22963 "After unfolding the second buffer, its text should be displayed"
22964 );
22965
22966 multi_buffer_editor.update(cx, |editor, cx| {
22967 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22968 });
22969 assert_eq!(
22970 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22971 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22972 "After unfolding the first buffer, its text should be displayed"
22973 );
22974
22975 multi_buffer_editor.update(cx, |editor, cx| {
22976 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22977 });
22978 assert_eq!(
22979 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22980 full_text,
22981 "After unfolding all buffers, all original text should be displayed"
22982 );
22983}
22984
22985#[gpui::test]
22986async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22987 init_test(cx, |_| {});
22988
22989 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22990
22991 let fs = FakeFs::new(cx.executor());
22992 fs.insert_tree(
22993 path!("/a"),
22994 json!({
22995 "main.rs": sample_text,
22996 }),
22997 )
22998 .await;
22999 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23000 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23001 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23002 let worktree = project.update(cx, |project, cx| {
23003 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23004 assert_eq!(worktrees.len(), 1);
23005 worktrees.pop().unwrap()
23006 });
23007 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23008
23009 let buffer_1 = project
23010 .update(cx, |project, cx| {
23011 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23012 })
23013 .await
23014 .unwrap();
23015
23016 let multi_buffer = cx.new(|cx| {
23017 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23018 multi_buffer.push_excerpts(
23019 buffer_1.clone(),
23020 [ExcerptRange::new(
23021 Point::new(0, 0)
23022 ..Point::new(
23023 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23024 0,
23025 ),
23026 )],
23027 cx,
23028 );
23029 multi_buffer
23030 });
23031 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23032 Editor::new(
23033 EditorMode::full(),
23034 multi_buffer,
23035 Some(project.clone()),
23036 window,
23037 cx,
23038 )
23039 });
23040
23041 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23042 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23043 enum TestHighlight {}
23044 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23045 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23046 editor.highlight_text::<TestHighlight>(
23047 vec![highlight_range.clone()],
23048 HighlightStyle::color(Hsla::green()),
23049 cx,
23050 );
23051 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23052 s.select_ranges(Some(highlight_range))
23053 });
23054 });
23055
23056 let full_text = format!("\n\n{sample_text}");
23057 assert_eq!(
23058 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23059 full_text,
23060 );
23061}
23062
23063#[gpui::test]
23064async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23065 init_test(cx, |_| {});
23066 cx.update(|cx| {
23067 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23068 "keymaps/default-linux.json",
23069 cx,
23070 )
23071 .unwrap();
23072 cx.bind_keys(default_key_bindings);
23073 });
23074
23075 let (editor, cx) = cx.add_window_view(|window, cx| {
23076 let multi_buffer = MultiBuffer::build_multi(
23077 [
23078 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23079 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23080 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23081 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23082 ],
23083 cx,
23084 );
23085 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23086
23087 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23088 // fold all but the second buffer, so that we test navigating between two
23089 // adjacent folded buffers, as well as folded buffers at the start and
23090 // end the multibuffer
23091 editor.fold_buffer(buffer_ids[0], cx);
23092 editor.fold_buffer(buffer_ids[2], cx);
23093 editor.fold_buffer(buffer_ids[3], cx);
23094
23095 editor
23096 });
23097 cx.simulate_resize(size(px(1000.), px(1000.)));
23098
23099 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23100 cx.assert_excerpts_with_selections(indoc! {"
23101 [EXCERPT]
23102 ˇ[FOLDED]
23103 [EXCERPT]
23104 a1
23105 b1
23106 [EXCERPT]
23107 [FOLDED]
23108 [EXCERPT]
23109 [FOLDED]
23110 "
23111 });
23112 cx.simulate_keystroke("down");
23113 cx.assert_excerpts_with_selections(indoc! {"
23114 [EXCERPT]
23115 [FOLDED]
23116 [EXCERPT]
23117 ˇa1
23118 b1
23119 [EXCERPT]
23120 [FOLDED]
23121 [EXCERPT]
23122 [FOLDED]
23123 "
23124 });
23125 cx.simulate_keystroke("down");
23126 cx.assert_excerpts_with_selections(indoc! {"
23127 [EXCERPT]
23128 [FOLDED]
23129 [EXCERPT]
23130 a1
23131 ˇb1
23132 [EXCERPT]
23133 [FOLDED]
23134 [EXCERPT]
23135 [FOLDED]
23136 "
23137 });
23138 cx.simulate_keystroke("down");
23139 cx.assert_excerpts_with_selections(indoc! {"
23140 [EXCERPT]
23141 [FOLDED]
23142 [EXCERPT]
23143 a1
23144 b1
23145 ˇ[EXCERPT]
23146 [FOLDED]
23147 [EXCERPT]
23148 [FOLDED]
23149 "
23150 });
23151 cx.simulate_keystroke("down");
23152 cx.assert_excerpts_with_selections(indoc! {"
23153 [EXCERPT]
23154 [FOLDED]
23155 [EXCERPT]
23156 a1
23157 b1
23158 [EXCERPT]
23159 ˇ[FOLDED]
23160 [EXCERPT]
23161 [FOLDED]
23162 "
23163 });
23164 for _ in 0..5 {
23165 cx.simulate_keystroke("down");
23166 cx.assert_excerpts_with_selections(indoc! {"
23167 [EXCERPT]
23168 [FOLDED]
23169 [EXCERPT]
23170 a1
23171 b1
23172 [EXCERPT]
23173 [FOLDED]
23174 [EXCERPT]
23175 ˇ[FOLDED]
23176 "
23177 });
23178 }
23179
23180 cx.simulate_keystroke("up");
23181 cx.assert_excerpts_with_selections(indoc! {"
23182 [EXCERPT]
23183 [FOLDED]
23184 [EXCERPT]
23185 a1
23186 b1
23187 [EXCERPT]
23188 ˇ[FOLDED]
23189 [EXCERPT]
23190 [FOLDED]
23191 "
23192 });
23193 cx.simulate_keystroke("up");
23194 cx.assert_excerpts_with_selections(indoc! {"
23195 [EXCERPT]
23196 [FOLDED]
23197 [EXCERPT]
23198 a1
23199 b1
23200 ˇ[EXCERPT]
23201 [FOLDED]
23202 [EXCERPT]
23203 [FOLDED]
23204 "
23205 });
23206 cx.simulate_keystroke("up");
23207 cx.assert_excerpts_with_selections(indoc! {"
23208 [EXCERPT]
23209 [FOLDED]
23210 [EXCERPT]
23211 a1
23212 ˇb1
23213 [EXCERPT]
23214 [FOLDED]
23215 [EXCERPT]
23216 [FOLDED]
23217 "
23218 });
23219 cx.simulate_keystroke("up");
23220 cx.assert_excerpts_with_selections(indoc! {"
23221 [EXCERPT]
23222 [FOLDED]
23223 [EXCERPT]
23224 ˇa1
23225 b1
23226 [EXCERPT]
23227 [FOLDED]
23228 [EXCERPT]
23229 [FOLDED]
23230 "
23231 });
23232 for _ in 0..5 {
23233 cx.simulate_keystroke("up");
23234 cx.assert_excerpts_with_selections(indoc! {"
23235 [EXCERPT]
23236 ˇ[FOLDED]
23237 [EXCERPT]
23238 a1
23239 b1
23240 [EXCERPT]
23241 [FOLDED]
23242 [EXCERPT]
23243 [FOLDED]
23244 "
23245 });
23246 }
23247}
23248
23249#[gpui::test]
23250async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23251 init_test(cx, |_| {});
23252
23253 // Simple insertion
23254 assert_highlighted_edits(
23255 "Hello, world!",
23256 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23257 true,
23258 cx,
23259 |highlighted_edits, cx| {
23260 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23261 assert_eq!(highlighted_edits.highlights.len(), 1);
23262 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23263 assert_eq!(
23264 highlighted_edits.highlights[0].1.background_color,
23265 Some(cx.theme().status().created_background)
23266 );
23267 },
23268 )
23269 .await;
23270
23271 // Replacement
23272 assert_highlighted_edits(
23273 "This is a test.",
23274 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23275 false,
23276 cx,
23277 |highlighted_edits, cx| {
23278 assert_eq!(highlighted_edits.text, "That is a test.");
23279 assert_eq!(highlighted_edits.highlights.len(), 1);
23280 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23281 assert_eq!(
23282 highlighted_edits.highlights[0].1.background_color,
23283 Some(cx.theme().status().created_background)
23284 );
23285 },
23286 )
23287 .await;
23288
23289 // Multiple edits
23290 assert_highlighted_edits(
23291 "Hello, world!",
23292 vec![
23293 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23294 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23295 ],
23296 false,
23297 cx,
23298 |highlighted_edits, cx| {
23299 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23300 assert_eq!(highlighted_edits.highlights.len(), 2);
23301 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23302 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23303 assert_eq!(
23304 highlighted_edits.highlights[0].1.background_color,
23305 Some(cx.theme().status().created_background)
23306 );
23307 assert_eq!(
23308 highlighted_edits.highlights[1].1.background_color,
23309 Some(cx.theme().status().created_background)
23310 );
23311 },
23312 )
23313 .await;
23314
23315 // Multiple lines with edits
23316 assert_highlighted_edits(
23317 "First line\nSecond line\nThird line\nFourth line",
23318 vec![
23319 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23320 (
23321 Point::new(2, 0)..Point::new(2, 10),
23322 "New third line".to_string(),
23323 ),
23324 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23325 ],
23326 false,
23327 cx,
23328 |highlighted_edits, cx| {
23329 assert_eq!(
23330 highlighted_edits.text,
23331 "Second modified\nNew third line\nFourth updated line"
23332 );
23333 assert_eq!(highlighted_edits.highlights.len(), 3);
23334 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23335 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23336 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23337 for highlight in &highlighted_edits.highlights {
23338 assert_eq!(
23339 highlight.1.background_color,
23340 Some(cx.theme().status().created_background)
23341 );
23342 }
23343 },
23344 )
23345 .await;
23346}
23347
23348#[gpui::test]
23349async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23350 init_test(cx, |_| {});
23351
23352 // Deletion
23353 assert_highlighted_edits(
23354 "Hello, world!",
23355 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23356 true,
23357 cx,
23358 |highlighted_edits, cx| {
23359 assert_eq!(highlighted_edits.text, "Hello, world!");
23360 assert_eq!(highlighted_edits.highlights.len(), 1);
23361 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23362 assert_eq!(
23363 highlighted_edits.highlights[0].1.background_color,
23364 Some(cx.theme().status().deleted_background)
23365 );
23366 },
23367 )
23368 .await;
23369
23370 // Insertion
23371 assert_highlighted_edits(
23372 "Hello, world!",
23373 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23374 true,
23375 cx,
23376 |highlighted_edits, cx| {
23377 assert_eq!(highlighted_edits.highlights.len(), 1);
23378 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23379 assert_eq!(
23380 highlighted_edits.highlights[0].1.background_color,
23381 Some(cx.theme().status().created_background)
23382 );
23383 },
23384 )
23385 .await;
23386}
23387
23388async fn assert_highlighted_edits(
23389 text: &str,
23390 edits: Vec<(Range<Point>, String)>,
23391 include_deletions: bool,
23392 cx: &mut TestAppContext,
23393 assertion_fn: impl Fn(HighlightedText, &App),
23394) {
23395 let window = cx.add_window(|window, cx| {
23396 let buffer = MultiBuffer::build_simple(text, cx);
23397 Editor::new(EditorMode::full(), buffer, None, window, cx)
23398 });
23399 let cx = &mut VisualTestContext::from_window(*window, cx);
23400
23401 let (buffer, snapshot) = window
23402 .update(cx, |editor, _window, cx| {
23403 (
23404 editor.buffer().clone(),
23405 editor.buffer().read(cx).snapshot(cx),
23406 )
23407 })
23408 .unwrap();
23409
23410 let edits = edits
23411 .into_iter()
23412 .map(|(range, edit)| {
23413 (
23414 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23415 edit,
23416 )
23417 })
23418 .collect::<Vec<_>>();
23419
23420 let text_anchor_edits = edits
23421 .clone()
23422 .into_iter()
23423 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23424 .collect::<Vec<_>>();
23425
23426 let edit_preview = window
23427 .update(cx, |_, _window, cx| {
23428 buffer
23429 .read(cx)
23430 .as_singleton()
23431 .unwrap()
23432 .read(cx)
23433 .preview_edits(text_anchor_edits.into(), cx)
23434 })
23435 .unwrap()
23436 .await;
23437
23438 cx.update(|_window, cx| {
23439 let highlighted_edits = edit_prediction_edit_text(
23440 snapshot.as_singleton().unwrap().2,
23441 &edits,
23442 &edit_preview,
23443 include_deletions,
23444 cx,
23445 );
23446 assertion_fn(highlighted_edits, cx)
23447 });
23448}
23449
23450#[track_caller]
23451fn assert_breakpoint(
23452 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23453 path: &Arc<Path>,
23454 expected: Vec<(u32, Breakpoint)>,
23455) {
23456 if expected.is_empty() {
23457 assert!(!breakpoints.contains_key(path), "{}", path.display());
23458 } else {
23459 let mut breakpoint = breakpoints
23460 .get(path)
23461 .unwrap()
23462 .iter()
23463 .map(|breakpoint| {
23464 (
23465 breakpoint.row,
23466 Breakpoint {
23467 message: breakpoint.message.clone(),
23468 state: breakpoint.state,
23469 condition: breakpoint.condition.clone(),
23470 hit_condition: breakpoint.hit_condition.clone(),
23471 },
23472 )
23473 })
23474 .collect::<Vec<_>>();
23475
23476 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23477
23478 assert_eq!(expected, breakpoint);
23479 }
23480}
23481
23482fn add_log_breakpoint_at_cursor(
23483 editor: &mut Editor,
23484 log_message: &str,
23485 window: &mut Window,
23486 cx: &mut Context<Editor>,
23487) {
23488 let (anchor, bp) = editor
23489 .breakpoints_at_cursors(window, cx)
23490 .first()
23491 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23492 .unwrap_or_else(|| {
23493 let snapshot = editor.snapshot(window, cx);
23494 let cursor_position: Point =
23495 editor.selections.newest(&snapshot.display_snapshot).head();
23496
23497 let breakpoint_position = snapshot
23498 .buffer_snapshot()
23499 .anchor_before(Point::new(cursor_position.row, 0));
23500
23501 (breakpoint_position, Breakpoint::new_log(log_message))
23502 });
23503
23504 editor.edit_breakpoint_at_anchor(
23505 anchor,
23506 bp,
23507 BreakpointEditAction::EditLogMessage(log_message.into()),
23508 cx,
23509 );
23510}
23511
23512#[gpui::test]
23513async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23514 init_test(cx, |_| {});
23515
23516 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23517 let fs = FakeFs::new(cx.executor());
23518 fs.insert_tree(
23519 path!("/a"),
23520 json!({
23521 "main.rs": sample_text,
23522 }),
23523 )
23524 .await;
23525 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23526 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23527 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23528
23529 let fs = FakeFs::new(cx.executor());
23530 fs.insert_tree(
23531 path!("/a"),
23532 json!({
23533 "main.rs": sample_text,
23534 }),
23535 )
23536 .await;
23537 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23538 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23539 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23540 let worktree_id = workspace
23541 .update(cx, |workspace, _window, cx| {
23542 workspace.project().update(cx, |project, cx| {
23543 project.worktrees(cx).next().unwrap().read(cx).id()
23544 })
23545 })
23546 .unwrap();
23547
23548 let buffer = project
23549 .update(cx, |project, cx| {
23550 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23551 })
23552 .await
23553 .unwrap();
23554
23555 let (editor, cx) = cx.add_window_view(|window, cx| {
23556 Editor::new(
23557 EditorMode::full(),
23558 MultiBuffer::build_from_buffer(buffer, cx),
23559 Some(project.clone()),
23560 window,
23561 cx,
23562 )
23563 });
23564
23565 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23566 let abs_path = project.read_with(cx, |project, cx| {
23567 project
23568 .absolute_path(&project_path, cx)
23569 .map(Arc::from)
23570 .unwrap()
23571 });
23572
23573 // assert we can add breakpoint on the first line
23574 editor.update_in(cx, |editor, window, cx| {
23575 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23576 editor.move_to_end(&MoveToEnd, window, cx);
23577 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23578 });
23579
23580 let breakpoints = editor.update(cx, |editor, cx| {
23581 editor
23582 .breakpoint_store()
23583 .as_ref()
23584 .unwrap()
23585 .read(cx)
23586 .all_source_breakpoints(cx)
23587 });
23588
23589 assert_eq!(1, breakpoints.len());
23590 assert_breakpoint(
23591 &breakpoints,
23592 &abs_path,
23593 vec![
23594 (0, Breakpoint::new_standard()),
23595 (3, Breakpoint::new_standard()),
23596 ],
23597 );
23598
23599 editor.update_in(cx, |editor, window, cx| {
23600 editor.move_to_beginning(&MoveToBeginning, window, cx);
23601 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23602 });
23603
23604 let breakpoints = editor.update(cx, |editor, cx| {
23605 editor
23606 .breakpoint_store()
23607 .as_ref()
23608 .unwrap()
23609 .read(cx)
23610 .all_source_breakpoints(cx)
23611 });
23612
23613 assert_eq!(1, breakpoints.len());
23614 assert_breakpoint(
23615 &breakpoints,
23616 &abs_path,
23617 vec![(3, Breakpoint::new_standard())],
23618 );
23619
23620 editor.update_in(cx, |editor, window, cx| {
23621 editor.move_to_end(&MoveToEnd, window, cx);
23622 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23623 });
23624
23625 let breakpoints = editor.update(cx, |editor, cx| {
23626 editor
23627 .breakpoint_store()
23628 .as_ref()
23629 .unwrap()
23630 .read(cx)
23631 .all_source_breakpoints(cx)
23632 });
23633
23634 assert_eq!(0, breakpoints.len());
23635 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23636}
23637
23638#[gpui::test]
23639async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23640 init_test(cx, |_| {});
23641
23642 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23643
23644 let fs = FakeFs::new(cx.executor());
23645 fs.insert_tree(
23646 path!("/a"),
23647 json!({
23648 "main.rs": sample_text,
23649 }),
23650 )
23651 .await;
23652 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23653 let (workspace, cx) =
23654 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23655
23656 let worktree_id = workspace.update(cx, |workspace, cx| {
23657 workspace.project().update(cx, |project, cx| {
23658 project.worktrees(cx).next().unwrap().read(cx).id()
23659 })
23660 });
23661
23662 let buffer = project
23663 .update(cx, |project, cx| {
23664 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23665 })
23666 .await
23667 .unwrap();
23668
23669 let (editor, cx) = cx.add_window_view(|window, cx| {
23670 Editor::new(
23671 EditorMode::full(),
23672 MultiBuffer::build_from_buffer(buffer, cx),
23673 Some(project.clone()),
23674 window,
23675 cx,
23676 )
23677 });
23678
23679 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23680 let abs_path = project.read_with(cx, |project, cx| {
23681 project
23682 .absolute_path(&project_path, cx)
23683 .map(Arc::from)
23684 .unwrap()
23685 });
23686
23687 editor.update_in(cx, |editor, window, cx| {
23688 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23689 });
23690
23691 let breakpoints = editor.update(cx, |editor, cx| {
23692 editor
23693 .breakpoint_store()
23694 .as_ref()
23695 .unwrap()
23696 .read(cx)
23697 .all_source_breakpoints(cx)
23698 });
23699
23700 assert_breakpoint(
23701 &breakpoints,
23702 &abs_path,
23703 vec![(0, Breakpoint::new_log("hello world"))],
23704 );
23705
23706 // Removing a log message from a log breakpoint should remove it
23707 editor.update_in(cx, |editor, window, cx| {
23708 add_log_breakpoint_at_cursor(editor, "", window, cx);
23709 });
23710
23711 let breakpoints = editor.update(cx, |editor, cx| {
23712 editor
23713 .breakpoint_store()
23714 .as_ref()
23715 .unwrap()
23716 .read(cx)
23717 .all_source_breakpoints(cx)
23718 });
23719
23720 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23721
23722 editor.update_in(cx, |editor, window, cx| {
23723 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23724 editor.move_to_end(&MoveToEnd, window, cx);
23725 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23726 // Not adding a log message to a standard breakpoint shouldn't remove it
23727 add_log_breakpoint_at_cursor(editor, "", window, cx);
23728 });
23729
23730 let breakpoints = editor.update(cx, |editor, cx| {
23731 editor
23732 .breakpoint_store()
23733 .as_ref()
23734 .unwrap()
23735 .read(cx)
23736 .all_source_breakpoints(cx)
23737 });
23738
23739 assert_breakpoint(
23740 &breakpoints,
23741 &abs_path,
23742 vec![
23743 (0, Breakpoint::new_standard()),
23744 (3, Breakpoint::new_standard()),
23745 ],
23746 );
23747
23748 editor.update_in(cx, |editor, window, cx| {
23749 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23750 });
23751
23752 let breakpoints = editor.update(cx, |editor, cx| {
23753 editor
23754 .breakpoint_store()
23755 .as_ref()
23756 .unwrap()
23757 .read(cx)
23758 .all_source_breakpoints(cx)
23759 });
23760
23761 assert_breakpoint(
23762 &breakpoints,
23763 &abs_path,
23764 vec![
23765 (0, Breakpoint::new_standard()),
23766 (3, Breakpoint::new_log("hello world")),
23767 ],
23768 );
23769
23770 editor.update_in(cx, |editor, window, cx| {
23771 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23772 });
23773
23774 let breakpoints = editor.update(cx, |editor, cx| {
23775 editor
23776 .breakpoint_store()
23777 .as_ref()
23778 .unwrap()
23779 .read(cx)
23780 .all_source_breakpoints(cx)
23781 });
23782
23783 assert_breakpoint(
23784 &breakpoints,
23785 &abs_path,
23786 vec![
23787 (0, Breakpoint::new_standard()),
23788 (3, Breakpoint::new_log("hello Earth!!")),
23789 ],
23790 );
23791}
23792
23793/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23794/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23795/// or when breakpoints were placed out of order. This tests for a regression too
23796#[gpui::test]
23797async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23798 init_test(cx, |_| {});
23799
23800 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23801 let fs = FakeFs::new(cx.executor());
23802 fs.insert_tree(
23803 path!("/a"),
23804 json!({
23805 "main.rs": sample_text,
23806 }),
23807 )
23808 .await;
23809 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23810 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23811 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23812
23813 let fs = FakeFs::new(cx.executor());
23814 fs.insert_tree(
23815 path!("/a"),
23816 json!({
23817 "main.rs": sample_text,
23818 }),
23819 )
23820 .await;
23821 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23822 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23823 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23824 let worktree_id = workspace
23825 .update(cx, |workspace, _window, cx| {
23826 workspace.project().update(cx, |project, cx| {
23827 project.worktrees(cx).next().unwrap().read(cx).id()
23828 })
23829 })
23830 .unwrap();
23831
23832 let buffer = project
23833 .update(cx, |project, cx| {
23834 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23835 })
23836 .await
23837 .unwrap();
23838
23839 let (editor, cx) = cx.add_window_view(|window, cx| {
23840 Editor::new(
23841 EditorMode::full(),
23842 MultiBuffer::build_from_buffer(buffer, cx),
23843 Some(project.clone()),
23844 window,
23845 cx,
23846 )
23847 });
23848
23849 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23850 let abs_path = project.read_with(cx, |project, cx| {
23851 project
23852 .absolute_path(&project_path, cx)
23853 .map(Arc::from)
23854 .unwrap()
23855 });
23856
23857 // assert we can add breakpoint on the first line
23858 editor.update_in(cx, |editor, window, cx| {
23859 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23860 editor.move_to_end(&MoveToEnd, window, cx);
23861 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23862 editor.move_up(&MoveUp, window, cx);
23863 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23864 });
23865
23866 let breakpoints = editor.update(cx, |editor, cx| {
23867 editor
23868 .breakpoint_store()
23869 .as_ref()
23870 .unwrap()
23871 .read(cx)
23872 .all_source_breakpoints(cx)
23873 });
23874
23875 assert_eq!(1, breakpoints.len());
23876 assert_breakpoint(
23877 &breakpoints,
23878 &abs_path,
23879 vec![
23880 (0, Breakpoint::new_standard()),
23881 (2, Breakpoint::new_standard()),
23882 (3, Breakpoint::new_standard()),
23883 ],
23884 );
23885
23886 editor.update_in(cx, |editor, window, cx| {
23887 editor.move_to_beginning(&MoveToBeginning, window, cx);
23888 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23889 editor.move_to_end(&MoveToEnd, window, cx);
23890 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23891 // Disabling a breakpoint that doesn't exist should do nothing
23892 editor.move_up(&MoveUp, window, cx);
23893 editor.move_up(&MoveUp, window, cx);
23894 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23895 });
23896
23897 let breakpoints = editor.update(cx, |editor, cx| {
23898 editor
23899 .breakpoint_store()
23900 .as_ref()
23901 .unwrap()
23902 .read(cx)
23903 .all_source_breakpoints(cx)
23904 });
23905
23906 let disable_breakpoint = {
23907 let mut bp = Breakpoint::new_standard();
23908 bp.state = BreakpointState::Disabled;
23909 bp
23910 };
23911
23912 assert_eq!(1, breakpoints.len());
23913 assert_breakpoint(
23914 &breakpoints,
23915 &abs_path,
23916 vec![
23917 (0, disable_breakpoint.clone()),
23918 (2, Breakpoint::new_standard()),
23919 (3, disable_breakpoint.clone()),
23920 ],
23921 );
23922
23923 editor.update_in(cx, |editor, window, cx| {
23924 editor.move_to_beginning(&MoveToBeginning, window, cx);
23925 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23926 editor.move_to_end(&MoveToEnd, window, cx);
23927 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23928 editor.move_up(&MoveUp, window, cx);
23929 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23930 });
23931
23932 let breakpoints = editor.update(cx, |editor, cx| {
23933 editor
23934 .breakpoint_store()
23935 .as_ref()
23936 .unwrap()
23937 .read(cx)
23938 .all_source_breakpoints(cx)
23939 });
23940
23941 assert_eq!(1, breakpoints.len());
23942 assert_breakpoint(
23943 &breakpoints,
23944 &abs_path,
23945 vec![
23946 (0, Breakpoint::new_standard()),
23947 (2, disable_breakpoint),
23948 (3, Breakpoint::new_standard()),
23949 ],
23950 );
23951}
23952
23953#[gpui::test]
23954async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23955 init_test(cx, |_| {});
23956 let capabilities = lsp::ServerCapabilities {
23957 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23958 prepare_provider: Some(true),
23959 work_done_progress_options: Default::default(),
23960 })),
23961 ..Default::default()
23962 };
23963 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23964
23965 cx.set_state(indoc! {"
23966 struct Fˇoo {}
23967 "});
23968
23969 cx.update_editor(|editor, _, cx| {
23970 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23971 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23972 editor.highlight_background::<DocumentHighlightRead>(
23973 &[highlight_range],
23974 |theme| theme.colors().editor_document_highlight_read_background,
23975 cx,
23976 );
23977 });
23978
23979 let mut prepare_rename_handler = cx
23980 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23981 move |_, _, _| async move {
23982 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23983 start: lsp::Position {
23984 line: 0,
23985 character: 7,
23986 },
23987 end: lsp::Position {
23988 line: 0,
23989 character: 10,
23990 },
23991 })))
23992 },
23993 );
23994 let prepare_rename_task = cx
23995 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23996 .expect("Prepare rename was not started");
23997 prepare_rename_handler.next().await.unwrap();
23998 prepare_rename_task.await.expect("Prepare rename failed");
23999
24000 let mut rename_handler =
24001 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24002 let edit = lsp::TextEdit {
24003 range: lsp::Range {
24004 start: lsp::Position {
24005 line: 0,
24006 character: 7,
24007 },
24008 end: lsp::Position {
24009 line: 0,
24010 character: 10,
24011 },
24012 },
24013 new_text: "FooRenamed".to_string(),
24014 };
24015 Ok(Some(lsp::WorkspaceEdit::new(
24016 // Specify the same edit twice
24017 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24018 )))
24019 });
24020 let rename_task = cx
24021 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24022 .expect("Confirm rename was not started");
24023 rename_handler.next().await.unwrap();
24024 rename_task.await.expect("Confirm rename failed");
24025 cx.run_until_parked();
24026
24027 // Despite two edits, only one is actually applied as those are identical
24028 cx.assert_editor_state(indoc! {"
24029 struct FooRenamedˇ {}
24030 "});
24031}
24032
24033#[gpui::test]
24034async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24035 init_test(cx, |_| {});
24036 // These capabilities indicate that the server does not support prepare rename.
24037 let capabilities = lsp::ServerCapabilities {
24038 rename_provider: Some(lsp::OneOf::Left(true)),
24039 ..Default::default()
24040 };
24041 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24042
24043 cx.set_state(indoc! {"
24044 struct Fˇoo {}
24045 "});
24046
24047 cx.update_editor(|editor, _window, cx| {
24048 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24049 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24050 editor.highlight_background::<DocumentHighlightRead>(
24051 &[highlight_range],
24052 |theme| theme.colors().editor_document_highlight_read_background,
24053 cx,
24054 );
24055 });
24056
24057 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24058 .expect("Prepare rename was not started")
24059 .await
24060 .expect("Prepare rename failed");
24061
24062 let mut rename_handler =
24063 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24064 let edit = lsp::TextEdit {
24065 range: lsp::Range {
24066 start: lsp::Position {
24067 line: 0,
24068 character: 7,
24069 },
24070 end: lsp::Position {
24071 line: 0,
24072 character: 10,
24073 },
24074 },
24075 new_text: "FooRenamed".to_string(),
24076 };
24077 Ok(Some(lsp::WorkspaceEdit::new(
24078 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24079 )))
24080 });
24081 let rename_task = cx
24082 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24083 .expect("Confirm rename was not started");
24084 rename_handler.next().await.unwrap();
24085 rename_task.await.expect("Confirm rename failed");
24086 cx.run_until_parked();
24087
24088 // Correct range is renamed, as `surrounding_word` is used to find it.
24089 cx.assert_editor_state(indoc! {"
24090 struct FooRenamedˇ {}
24091 "});
24092}
24093
24094#[gpui::test]
24095async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24096 init_test(cx, |_| {});
24097 let mut cx = EditorTestContext::new(cx).await;
24098
24099 let language = Arc::new(
24100 Language::new(
24101 LanguageConfig::default(),
24102 Some(tree_sitter_html::LANGUAGE.into()),
24103 )
24104 .with_brackets_query(
24105 r#"
24106 ("<" @open "/>" @close)
24107 ("</" @open ">" @close)
24108 ("<" @open ">" @close)
24109 ("\"" @open "\"" @close)
24110 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24111 "#,
24112 )
24113 .unwrap(),
24114 );
24115 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24116
24117 cx.set_state(indoc! {"
24118 <span>ˇ</span>
24119 "});
24120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24121 cx.assert_editor_state(indoc! {"
24122 <span>
24123 ˇ
24124 </span>
24125 "});
24126
24127 cx.set_state(indoc! {"
24128 <span><span></span>ˇ</span>
24129 "});
24130 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24131 cx.assert_editor_state(indoc! {"
24132 <span><span></span>
24133 ˇ</span>
24134 "});
24135
24136 cx.set_state(indoc! {"
24137 <span>ˇ
24138 </span>
24139 "});
24140 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24141 cx.assert_editor_state(indoc! {"
24142 <span>
24143 ˇ
24144 </span>
24145 "});
24146}
24147
24148#[gpui::test(iterations = 10)]
24149async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24150 init_test(cx, |_| {});
24151
24152 let fs = FakeFs::new(cx.executor());
24153 fs.insert_tree(
24154 path!("/dir"),
24155 json!({
24156 "a.ts": "a",
24157 }),
24158 )
24159 .await;
24160
24161 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24162 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24163 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24164
24165 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24166 language_registry.add(Arc::new(Language::new(
24167 LanguageConfig {
24168 name: "TypeScript".into(),
24169 matcher: LanguageMatcher {
24170 path_suffixes: vec!["ts".to_string()],
24171 ..Default::default()
24172 },
24173 ..Default::default()
24174 },
24175 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24176 )));
24177 let mut fake_language_servers = language_registry.register_fake_lsp(
24178 "TypeScript",
24179 FakeLspAdapter {
24180 capabilities: lsp::ServerCapabilities {
24181 code_lens_provider: Some(lsp::CodeLensOptions {
24182 resolve_provider: Some(true),
24183 }),
24184 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24185 commands: vec!["_the/command".to_string()],
24186 ..lsp::ExecuteCommandOptions::default()
24187 }),
24188 ..lsp::ServerCapabilities::default()
24189 },
24190 ..FakeLspAdapter::default()
24191 },
24192 );
24193
24194 let editor = workspace
24195 .update(cx, |workspace, window, cx| {
24196 workspace.open_abs_path(
24197 PathBuf::from(path!("/dir/a.ts")),
24198 OpenOptions::default(),
24199 window,
24200 cx,
24201 )
24202 })
24203 .unwrap()
24204 .await
24205 .unwrap()
24206 .downcast::<Editor>()
24207 .unwrap();
24208 cx.executor().run_until_parked();
24209
24210 let fake_server = fake_language_servers.next().await.unwrap();
24211
24212 let buffer = editor.update(cx, |editor, cx| {
24213 editor
24214 .buffer()
24215 .read(cx)
24216 .as_singleton()
24217 .expect("have opened a single file by path")
24218 });
24219
24220 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24221 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24222 drop(buffer_snapshot);
24223 let actions = cx
24224 .update_window(*workspace, |_, window, cx| {
24225 project.code_actions(&buffer, anchor..anchor, window, cx)
24226 })
24227 .unwrap();
24228
24229 fake_server
24230 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24231 Ok(Some(vec![
24232 lsp::CodeLens {
24233 range: lsp::Range::default(),
24234 command: Some(lsp::Command {
24235 title: "Code lens command".to_owned(),
24236 command: "_the/command".to_owned(),
24237 arguments: None,
24238 }),
24239 data: None,
24240 },
24241 lsp::CodeLens {
24242 range: lsp::Range::default(),
24243 command: Some(lsp::Command {
24244 title: "Command not in capabilities".to_owned(),
24245 command: "not in capabilities".to_owned(),
24246 arguments: None,
24247 }),
24248 data: None,
24249 },
24250 lsp::CodeLens {
24251 range: lsp::Range {
24252 start: lsp::Position {
24253 line: 1,
24254 character: 1,
24255 },
24256 end: lsp::Position {
24257 line: 1,
24258 character: 1,
24259 },
24260 },
24261 command: Some(lsp::Command {
24262 title: "Command not in range".to_owned(),
24263 command: "_the/command".to_owned(),
24264 arguments: None,
24265 }),
24266 data: None,
24267 },
24268 ]))
24269 })
24270 .next()
24271 .await;
24272
24273 let actions = actions.await.unwrap();
24274 assert_eq!(
24275 actions.len(),
24276 1,
24277 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24278 );
24279 let action = actions[0].clone();
24280 let apply = project.update(cx, |project, cx| {
24281 project.apply_code_action(buffer.clone(), action, true, cx)
24282 });
24283
24284 // Resolving the code action does not populate its edits. In absence of
24285 // edits, we must execute the given command.
24286 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24287 |mut lens, _| async move {
24288 let lens_command = lens.command.as_mut().expect("should have a command");
24289 assert_eq!(lens_command.title, "Code lens command");
24290 lens_command.arguments = Some(vec![json!("the-argument")]);
24291 Ok(lens)
24292 },
24293 );
24294
24295 // While executing the command, the language server sends the editor
24296 // a `workspaceEdit` request.
24297 fake_server
24298 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24299 let fake = fake_server.clone();
24300 move |params, _| {
24301 assert_eq!(params.command, "_the/command");
24302 let fake = fake.clone();
24303 async move {
24304 fake.server
24305 .request::<lsp::request::ApplyWorkspaceEdit>(
24306 lsp::ApplyWorkspaceEditParams {
24307 label: None,
24308 edit: lsp::WorkspaceEdit {
24309 changes: Some(
24310 [(
24311 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24312 vec![lsp::TextEdit {
24313 range: lsp::Range::new(
24314 lsp::Position::new(0, 0),
24315 lsp::Position::new(0, 0),
24316 ),
24317 new_text: "X".into(),
24318 }],
24319 )]
24320 .into_iter()
24321 .collect(),
24322 ),
24323 ..lsp::WorkspaceEdit::default()
24324 },
24325 },
24326 )
24327 .await
24328 .into_response()
24329 .unwrap();
24330 Ok(Some(json!(null)))
24331 }
24332 }
24333 })
24334 .next()
24335 .await;
24336
24337 // Applying the code lens command returns a project transaction containing the edits
24338 // sent by the language server in its `workspaceEdit` request.
24339 let transaction = apply.await.unwrap();
24340 assert!(transaction.0.contains_key(&buffer));
24341 buffer.update(cx, |buffer, cx| {
24342 assert_eq!(buffer.text(), "Xa");
24343 buffer.undo(cx);
24344 assert_eq!(buffer.text(), "a");
24345 });
24346
24347 let actions_after_edits = cx
24348 .update_window(*workspace, |_, window, cx| {
24349 project.code_actions(&buffer, anchor..anchor, window, cx)
24350 })
24351 .unwrap()
24352 .await
24353 .unwrap();
24354 assert_eq!(
24355 actions, actions_after_edits,
24356 "For the same selection, same code lens actions should be returned"
24357 );
24358
24359 let _responses =
24360 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24361 panic!("No more code lens requests are expected");
24362 });
24363 editor.update_in(cx, |editor, window, cx| {
24364 editor.select_all(&SelectAll, window, cx);
24365 });
24366 cx.executor().run_until_parked();
24367 let new_actions = cx
24368 .update_window(*workspace, |_, window, cx| {
24369 project.code_actions(&buffer, anchor..anchor, window, cx)
24370 })
24371 .unwrap()
24372 .await
24373 .unwrap();
24374 assert_eq!(
24375 actions, new_actions,
24376 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24377 );
24378}
24379
24380#[gpui::test]
24381async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24382 init_test(cx, |_| {});
24383
24384 let fs = FakeFs::new(cx.executor());
24385 let main_text = r#"fn main() {
24386println!("1");
24387println!("2");
24388println!("3");
24389println!("4");
24390println!("5");
24391}"#;
24392 let lib_text = "mod foo {}";
24393 fs.insert_tree(
24394 path!("/a"),
24395 json!({
24396 "lib.rs": lib_text,
24397 "main.rs": main_text,
24398 }),
24399 )
24400 .await;
24401
24402 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24403 let (workspace, cx) =
24404 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24405 let worktree_id = workspace.update(cx, |workspace, cx| {
24406 workspace.project().update(cx, |project, cx| {
24407 project.worktrees(cx).next().unwrap().read(cx).id()
24408 })
24409 });
24410
24411 let expected_ranges = vec![
24412 Point::new(0, 0)..Point::new(0, 0),
24413 Point::new(1, 0)..Point::new(1, 1),
24414 Point::new(2, 0)..Point::new(2, 2),
24415 Point::new(3, 0)..Point::new(3, 3),
24416 ];
24417
24418 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24419 let editor_1 = workspace
24420 .update_in(cx, |workspace, window, cx| {
24421 workspace.open_path(
24422 (worktree_id, rel_path("main.rs")),
24423 Some(pane_1.downgrade()),
24424 true,
24425 window,
24426 cx,
24427 )
24428 })
24429 .unwrap()
24430 .await
24431 .downcast::<Editor>()
24432 .unwrap();
24433 pane_1.update(cx, |pane, cx| {
24434 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24435 open_editor.update(cx, |editor, cx| {
24436 assert_eq!(
24437 editor.display_text(cx),
24438 main_text,
24439 "Original main.rs text on initial open",
24440 );
24441 assert_eq!(
24442 editor
24443 .selections
24444 .all::<Point>(&editor.display_snapshot(cx))
24445 .into_iter()
24446 .map(|s| s.range())
24447 .collect::<Vec<_>>(),
24448 vec![Point::zero()..Point::zero()],
24449 "Default selections on initial open",
24450 );
24451 })
24452 });
24453 editor_1.update_in(cx, |editor, window, cx| {
24454 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24455 s.select_ranges(expected_ranges.clone());
24456 });
24457 });
24458
24459 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24460 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24461 });
24462 let editor_2 = workspace
24463 .update_in(cx, |workspace, window, cx| {
24464 workspace.open_path(
24465 (worktree_id, rel_path("main.rs")),
24466 Some(pane_2.downgrade()),
24467 true,
24468 window,
24469 cx,
24470 )
24471 })
24472 .unwrap()
24473 .await
24474 .downcast::<Editor>()
24475 .unwrap();
24476 pane_2.update(cx, |pane, cx| {
24477 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24478 open_editor.update(cx, |editor, cx| {
24479 assert_eq!(
24480 editor.display_text(cx),
24481 main_text,
24482 "Original main.rs text on initial open in another panel",
24483 );
24484 assert_eq!(
24485 editor
24486 .selections
24487 .all::<Point>(&editor.display_snapshot(cx))
24488 .into_iter()
24489 .map(|s| s.range())
24490 .collect::<Vec<_>>(),
24491 vec![Point::zero()..Point::zero()],
24492 "Default selections on initial open in another panel",
24493 );
24494 })
24495 });
24496
24497 editor_2.update_in(cx, |editor, window, cx| {
24498 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24499 });
24500
24501 let _other_editor_1 = workspace
24502 .update_in(cx, |workspace, window, cx| {
24503 workspace.open_path(
24504 (worktree_id, rel_path("lib.rs")),
24505 Some(pane_1.downgrade()),
24506 true,
24507 window,
24508 cx,
24509 )
24510 })
24511 .unwrap()
24512 .await
24513 .downcast::<Editor>()
24514 .unwrap();
24515 pane_1
24516 .update_in(cx, |pane, window, cx| {
24517 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24518 })
24519 .await
24520 .unwrap();
24521 drop(editor_1);
24522 pane_1.update(cx, |pane, cx| {
24523 pane.active_item()
24524 .unwrap()
24525 .downcast::<Editor>()
24526 .unwrap()
24527 .update(cx, |editor, cx| {
24528 assert_eq!(
24529 editor.display_text(cx),
24530 lib_text,
24531 "Other file should be open and active",
24532 );
24533 });
24534 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24535 });
24536
24537 let _other_editor_2 = workspace
24538 .update_in(cx, |workspace, window, cx| {
24539 workspace.open_path(
24540 (worktree_id, rel_path("lib.rs")),
24541 Some(pane_2.downgrade()),
24542 true,
24543 window,
24544 cx,
24545 )
24546 })
24547 .unwrap()
24548 .await
24549 .downcast::<Editor>()
24550 .unwrap();
24551 pane_2
24552 .update_in(cx, |pane, window, cx| {
24553 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24554 })
24555 .await
24556 .unwrap();
24557 drop(editor_2);
24558 pane_2.update(cx, |pane, cx| {
24559 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24560 open_editor.update(cx, |editor, cx| {
24561 assert_eq!(
24562 editor.display_text(cx),
24563 lib_text,
24564 "Other file should be open and active in another panel too",
24565 );
24566 });
24567 assert_eq!(
24568 pane.items().count(),
24569 1,
24570 "No other editors should be open in another pane",
24571 );
24572 });
24573
24574 let _editor_1_reopened = workspace
24575 .update_in(cx, |workspace, window, cx| {
24576 workspace.open_path(
24577 (worktree_id, rel_path("main.rs")),
24578 Some(pane_1.downgrade()),
24579 true,
24580 window,
24581 cx,
24582 )
24583 })
24584 .unwrap()
24585 .await
24586 .downcast::<Editor>()
24587 .unwrap();
24588 let _editor_2_reopened = workspace
24589 .update_in(cx, |workspace, window, cx| {
24590 workspace.open_path(
24591 (worktree_id, rel_path("main.rs")),
24592 Some(pane_2.downgrade()),
24593 true,
24594 window,
24595 cx,
24596 )
24597 })
24598 .unwrap()
24599 .await
24600 .downcast::<Editor>()
24601 .unwrap();
24602 pane_1.update(cx, |pane, cx| {
24603 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24604 open_editor.update(cx, |editor, cx| {
24605 assert_eq!(
24606 editor.display_text(cx),
24607 main_text,
24608 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24609 );
24610 assert_eq!(
24611 editor
24612 .selections
24613 .all::<Point>(&editor.display_snapshot(cx))
24614 .into_iter()
24615 .map(|s| s.range())
24616 .collect::<Vec<_>>(),
24617 expected_ranges,
24618 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24619 );
24620 })
24621 });
24622 pane_2.update(cx, |pane, cx| {
24623 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24624 open_editor.update(cx, |editor, cx| {
24625 assert_eq!(
24626 editor.display_text(cx),
24627 r#"fn main() {
24628⋯rintln!("1");
24629⋯intln!("2");
24630⋯ntln!("3");
24631println!("4");
24632println!("5");
24633}"#,
24634 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24635 );
24636 assert_eq!(
24637 editor
24638 .selections
24639 .all::<Point>(&editor.display_snapshot(cx))
24640 .into_iter()
24641 .map(|s| s.range())
24642 .collect::<Vec<_>>(),
24643 vec![Point::zero()..Point::zero()],
24644 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24645 );
24646 })
24647 });
24648}
24649
24650#[gpui::test]
24651async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24652 init_test(cx, |_| {});
24653
24654 let fs = FakeFs::new(cx.executor());
24655 let main_text = r#"fn main() {
24656println!("1");
24657println!("2");
24658println!("3");
24659println!("4");
24660println!("5");
24661}"#;
24662 let lib_text = "mod foo {}";
24663 fs.insert_tree(
24664 path!("/a"),
24665 json!({
24666 "lib.rs": lib_text,
24667 "main.rs": main_text,
24668 }),
24669 )
24670 .await;
24671
24672 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24673 let (workspace, cx) =
24674 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24675 let worktree_id = workspace.update(cx, |workspace, cx| {
24676 workspace.project().update(cx, |project, cx| {
24677 project.worktrees(cx).next().unwrap().read(cx).id()
24678 })
24679 });
24680
24681 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24682 let editor = workspace
24683 .update_in(cx, |workspace, window, cx| {
24684 workspace.open_path(
24685 (worktree_id, rel_path("main.rs")),
24686 Some(pane.downgrade()),
24687 true,
24688 window,
24689 cx,
24690 )
24691 })
24692 .unwrap()
24693 .await
24694 .downcast::<Editor>()
24695 .unwrap();
24696 pane.update(cx, |pane, cx| {
24697 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24698 open_editor.update(cx, |editor, cx| {
24699 assert_eq!(
24700 editor.display_text(cx),
24701 main_text,
24702 "Original main.rs text on initial open",
24703 );
24704 })
24705 });
24706 editor.update_in(cx, |editor, window, cx| {
24707 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24708 });
24709
24710 cx.update_global(|store: &mut SettingsStore, cx| {
24711 store.update_user_settings(cx, |s| {
24712 s.workspace.restore_on_file_reopen = Some(false);
24713 });
24714 });
24715 editor.update_in(cx, |editor, window, cx| {
24716 editor.fold_ranges(
24717 vec![
24718 Point::new(1, 0)..Point::new(1, 1),
24719 Point::new(2, 0)..Point::new(2, 2),
24720 Point::new(3, 0)..Point::new(3, 3),
24721 ],
24722 false,
24723 window,
24724 cx,
24725 );
24726 });
24727 pane.update_in(cx, |pane, window, cx| {
24728 pane.close_all_items(&CloseAllItems::default(), window, cx)
24729 })
24730 .await
24731 .unwrap();
24732 pane.update(cx, |pane, _| {
24733 assert!(pane.active_item().is_none());
24734 });
24735 cx.update_global(|store: &mut SettingsStore, cx| {
24736 store.update_user_settings(cx, |s| {
24737 s.workspace.restore_on_file_reopen = Some(true);
24738 });
24739 });
24740
24741 let _editor_reopened = workspace
24742 .update_in(cx, |workspace, window, cx| {
24743 workspace.open_path(
24744 (worktree_id, rel_path("main.rs")),
24745 Some(pane.downgrade()),
24746 true,
24747 window,
24748 cx,
24749 )
24750 })
24751 .unwrap()
24752 .await
24753 .downcast::<Editor>()
24754 .unwrap();
24755 pane.update(cx, |pane, cx| {
24756 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24757 open_editor.update(cx, |editor, cx| {
24758 assert_eq!(
24759 editor.display_text(cx),
24760 main_text,
24761 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24762 );
24763 })
24764 });
24765}
24766
24767#[gpui::test]
24768async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24769 struct EmptyModalView {
24770 focus_handle: gpui::FocusHandle,
24771 }
24772 impl EventEmitter<DismissEvent> for EmptyModalView {}
24773 impl Render for EmptyModalView {
24774 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24775 div()
24776 }
24777 }
24778 impl Focusable for EmptyModalView {
24779 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24780 self.focus_handle.clone()
24781 }
24782 }
24783 impl workspace::ModalView for EmptyModalView {}
24784 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24785 EmptyModalView {
24786 focus_handle: cx.focus_handle(),
24787 }
24788 }
24789
24790 init_test(cx, |_| {});
24791
24792 let fs = FakeFs::new(cx.executor());
24793 let project = Project::test(fs, [], cx).await;
24794 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24795 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24796 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24797 let editor = cx.new_window_entity(|window, cx| {
24798 Editor::new(
24799 EditorMode::full(),
24800 buffer,
24801 Some(project.clone()),
24802 window,
24803 cx,
24804 )
24805 });
24806 workspace
24807 .update(cx, |workspace, window, cx| {
24808 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24809 })
24810 .unwrap();
24811 editor.update_in(cx, |editor, window, cx| {
24812 editor.open_context_menu(&OpenContextMenu, window, cx);
24813 assert!(editor.mouse_context_menu.is_some());
24814 });
24815 workspace
24816 .update(cx, |workspace, window, cx| {
24817 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24818 })
24819 .unwrap();
24820 cx.read(|cx| {
24821 assert!(editor.read(cx).mouse_context_menu.is_none());
24822 });
24823}
24824
24825fn set_linked_edit_ranges(
24826 opening: (Point, Point),
24827 closing: (Point, Point),
24828 editor: &mut Editor,
24829 cx: &mut Context<Editor>,
24830) {
24831 let Some((buffer, _)) = editor
24832 .buffer
24833 .read(cx)
24834 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24835 else {
24836 panic!("Failed to get buffer for selection position");
24837 };
24838 let buffer = buffer.read(cx);
24839 let buffer_id = buffer.remote_id();
24840 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24841 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24842 let mut linked_ranges = HashMap::default();
24843 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24844 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24845}
24846
24847#[gpui::test]
24848async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24849 init_test(cx, |_| {});
24850
24851 let fs = FakeFs::new(cx.executor());
24852 fs.insert_file(path!("/file.html"), Default::default())
24853 .await;
24854
24855 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24856
24857 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24858 let html_language = Arc::new(Language::new(
24859 LanguageConfig {
24860 name: "HTML".into(),
24861 matcher: LanguageMatcher {
24862 path_suffixes: vec!["html".to_string()],
24863 ..LanguageMatcher::default()
24864 },
24865 brackets: BracketPairConfig {
24866 pairs: vec![BracketPair {
24867 start: "<".into(),
24868 end: ">".into(),
24869 close: true,
24870 ..Default::default()
24871 }],
24872 ..Default::default()
24873 },
24874 ..Default::default()
24875 },
24876 Some(tree_sitter_html::LANGUAGE.into()),
24877 ));
24878 language_registry.add(html_language);
24879 let mut fake_servers = language_registry.register_fake_lsp(
24880 "HTML",
24881 FakeLspAdapter {
24882 capabilities: lsp::ServerCapabilities {
24883 completion_provider: Some(lsp::CompletionOptions {
24884 resolve_provider: Some(true),
24885 ..Default::default()
24886 }),
24887 ..Default::default()
24888 },
24889 ..Default::default()
24890 },
24891 );
24892
24893 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24894 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24895
24896 let worktree_id = workspace
24897 .update(cx, |workspace, _window, cx| {
24898 workspace.project().update(cx, |project, cx| {
24899 project.worktrees(cx).next().unwrap().read(cx).id()
24900 })
24901 })
24902 .unwrap();
24903 project
24904 .update(cx, |project, cx| {
24905 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24906 })
24907 .await
24908 .unwrap();
24909 let editor = workspace
24910 .update(cx, |workspace, window, cx| {
24911 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24912 })
24913 .unwrap()
24914 .await
24915 .unwrap()
24916 .downcast::<Editor>()
24917 .unwrap();
24918
24919 let fake_server = fake_servers.next().await.unwrap();
24920 editor.update_in(cx, |editor, window, cx| {
24921 editor.set_text("<ad></ad>", window, cx);
24922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24923 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24924 });
24925 set_linked_edit_ranges(
24926 (Point::new(0, 1), Point::new(0, 3)),
24927 (Point::new(0, 6), Point::new(0, 8)),
24928 editor,
24929 cx,
24930 );
24931 });
24932 let mut completion_handle =
24933 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24934 Ok(Some(lsp::CompletionResponse::Array(vec![
24935 lsp::CompletionItem {
24936 label: "head".to_string(),
24937 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24938 lsp::InsertReplaceEdit {
24939 new_text: "head".to_string(),
24940 insert: lsp::Range::new(
24941 lsp::Position::new(0, 1),
24942 lsp::Position::new(0, 3),
24943 ),
24944 replace: lsp::Range::new(
24945 lsp::Position::new(0, 1),
24946 lsp::Position::new(0, 3),
24947 ),
24948 },
24949 )),
24950 ..Default::default()
24951 },
24952 ])))
24953 });
24954 editor.update_in(cx, |editor, window, cx| {
24955 editor.show_completions(&ShowCompletions, window, cx);
24956 });
24957 cx.run_until_parked();
24958 completion_handle.next().await.unwrap();
24959 editor.update(cx, |editor, _| {
24960 assert!(
24961 editor.context_menu_visible(),
24962 "Completion menu should be visible"
24963 );
24964 });
24965 editor.update_in(cx, |editor, window, cx| {
24966 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24967 });
24968 cx.executor().run_until_parked();
24969 editor.update(cx, |editor, cx| {
24970 assert_eq!(editor.text(cx), "<head></head>");
24971 });
24972}
24973
24974#[gpui::test]
24975async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24976 init_test(cx, |_| {});
24977
24978 let mut cx = EditorTestContext::new(cx).await;
24979 let language = Arc::new(Language::new(
24980 LanguageConfig {
24981 name: "TSX".into(),
24982 matcher: LanguageMatcher {
24983 path_suffixes: vec!["tsx".to_string()],
24984 ..LanguageMatcher::default()
24985 },
24986 brackets: BracketPairConfig {
24987 pairs: vec![BracketPair {
24988 start: "<".into(),
24989 end: ">".into(),
24990 close: true,
24991 ..Default::default()
24992 }],
24993 ..Default::default()
24994 },
24995 linked_edit_characters: HashSet::from_iter(['.']),
24996 ..Default::default()
24997 },
24998 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24999 ));
25000 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25001
25002 // Test typing > does not extend linked pair
25003 cx.set_state("<divˇ<div></div>");
25004 cx.update_editor(|editor, _, cx| {
25005 set_linked_edit_ranges(
25006 (Point::new(0, 1), Point::new(0, 4)),
25007 (Point::new(0, 11), Point::new(0, 14)),
25008 editor,
25009 cx,
25010 );
25011 });
25012 cx.update_editor(|editor, window, cx| {
25013 editor.handle_input(">", window, cx);
25014 });
25015 cx.assert_editor_state("<div>ˇ<div></div>");
25016
25017 // Test typing . do extend linked pair
25018 cx.set_state("<Animatedˇ></Animated>");
25019 cx.update_editor(|editor, _, cx| {
25020 set_linked_edit_ranges(
25021 (Point::new(0, 1), Point::new(0, 9)),
25022 (Point::new(0, 12), Point::new(0, 20)),
25023 editor,
25024 cx,
25025 );
25026 });
25027 cx.update_editor(|editor, window, cx| {
25028 editor.handle_input(".", window, cx);
25029 });
25030 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25031 cx.update_editor(|editor, _, cx| {
25032 set_linked_edit_ranges(
25033 (Point::new(0, 1), Point::new(0, 10)),
25034 (Point::new(0, 13), Point::new(0, 21)),
25035 editor,
25036 cx,
25037 );
25038 });
25039 cx.update_editor(|editor, window, cx| {
25040 editor.handle_input("V", window, cx);
25041 });
25042 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25043}
25044
25045#[gpui::test]
25046async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25047 init_test(cx, |_| {});
25048
25049 let fs = FakeFs::new(cx.executor());
25050 fs.insert_tree(
25051 path!("/root"),
25052 json!({
25053 "a": {
25054 "main.rs": "fn main() {}",
25055 },
25056 "foo": {
25057 "bar": {
25058 "external_file.rs": "pub mod external {}",
25059 }
25060 }
25061 }),
25062 )
25063 .await;
25064
25065 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25066 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25067 language_registry.add(rust_lang());
25068 let _fake_servers = language_registry.register_fake_lsp(
25069 "Rust",
25070 FakeLspAdapter {
25071 ..FakeLspAdapter::default()
25072 },
25073 );
25074 let (workspace, cx) =
25075 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25076 let worktree_id = workspace.update(cx, |workspace, cx| {
25077 workspace.project().update(cx, |project, cx| {
25078 project.worktrees(cx).next().unwrap().read(cx).id()
25079 })
25080 });
25081
25082 let assert_language_servers_count =
25083 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25084 project.update(cx, |project, cx| {
25085 let current = project
25086 .lsp_store()
25087 .read(cx)
25088 .as_local()
25089 .unwrap()
25090 .language_servers
25091 .len();
25092 assert_eq!(expected, current, "{context}");
25093 });
25094 };
25095
25096 assert_language_servers_count(
25097 0,
25098 "No servers should be running before any file is open",
25099 cx,
25100 );
25101 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25102 let main_editor = workspace
25103 .update_in(cx, |workspace, window, cx| {
25104 workspace.open_path(
25105 (worktree_id, rel_path("main.rs")),
25106 Some(pane.downgrade()),
25107 true,
25108 window,
25109 cx,
25110 )
25111 })
25112 .unwrap()
25113 .await
25114 .downcast::<Editor>()
25115 .unwrap();
25116 pane.update(cx, |pane, cx| {
25117 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25118 open_editor.update(cx, |editor, cx| {
25119 assert_eq!(
25120 editor.display_text(cx),
25121 "fn main() {}",
25122 "Original main.rs text on initial open",
25123 );
25124 });
25125 assert_eq!(open_editor, main_editor);
25126 });
25127 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25128
25129 let external_editor = workspace
25130 .update_in(cx, |workspace, window, cx| {
25131 workspace.open_abs_path(
25132 PathBuf::from("/root/foo/bar/external_file.rs"),
25133 OpenOptions::default(),
25134 window,
25135 cx,
25136 )
25137 })
25138 .await
25139 .expect("opening external file")
25140 .downcast::<Editor>()
25141 .expect("downcasted external file's open element to editor");
25142 pane.update(cx, |pane, cx| {
25143 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25144 open_editor.update(cx, |editor, cx| {
25145 assert_eq!(
25146 editor.display_text(cx),
25147 "pub mod external {}",
25148 "External file is open now",
25149 );
25150 });
25151 assert_eq!(open_editor, external_editor);
25152 });
25153 assert_language_servers_count(
25154 1,
25155 "Second, external, *.rs file should join the existing server",
25156 cx,
25157 );
25158
25159 pane.update_in(cx, |pane, window, cx| {
25160 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25161 })
25162 .await
25163 .unwrap();
25164 pane.update_in(cx, |pane, window, cx| {
25165 pane.navigate_backward(&Default::default(), window, cx);
25166 });
25167 cx.run_until_parked();
25168 pane.update(cx, |pane, cx| {
25169 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25170 open_editor.update(cx, |editor, cx| {
25171 assert_eq!(
25172 editor.display_text(cx),
25173 "pub mod external {}",
25174 "External file is open now",
25175 );
25176 });
25177 });
25178 assert_language_servers_count(
25179 1,
25180 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25181 cx,
25182 );
25183
25184 cx.update(|_, cx| {
25185 workspace::reload(cx);
25186 });
25187 assert_language_servers_count(
25188 1,
25189 "After reloading the worktree with local and external files opened, only one project should be started",
25190 cx,
25191 );
25192}
25193
25194#[gpui::test]
25195async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25196 init_test(cx, |_| {});
25197
25198 let mut cx = EditorTestContext::new(cx).await;
25199 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25200 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25201
25202 // test cursor move to start of each line on tab
25203 // for `if`, `elif`, `else`, `while`, `with` and `for`
25204 cx.set_state(indoc! {"
25205 def main():
25206 ˇ for item in items:
25207 ˇ while item.active:
25208 ˇ if item.value > 10:
25209 ˇ continue
25210 ˇ elif item.value < 0:
25211 ˇ break
25212 ˇ else:
25213 ˇ with item.context() as ctx:
25214 ˇ yield count
25215 ˇ else:
25216 ˇ log('while else')
25217 ˇ else:
25218 ˇ log('for else')
25219 "});
25220 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25221 cx.assert_editor_state(indoc! {"
25222 def main():
25223 ˇfor item in items:
25224 ˇwhile item.active:
25225 ˇif item.value > 10:
25226 ˇcontinue
25227 ˇelif item.value < 0:
25228 ˇbreak
25229 ˇelse:
25230 ˇwith item.context() as ctx:
25231 ˇyield count
25232 ˇelse:
25233 ˇlog('while else')
25234 ˇelse:
25235 ˇlog('for else')
25236 "});
25237 // test relative indent is preserved when tab
25238 // for `if`, `elif`, `else`, `while`, `with` and `for`
25239 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25240 cx.assert_editor_state(indoc! {"
25241 def main():
25242 ˇfor item in items:
25243 ˇwhile item.active:
25244 ˇif item.value > 10:
25245 ˇcontinue
25246 ˇelif item.value < 0:
25247 ˇbreak
25248 ˇelse:
25249 ˇwith item.context() as ctx:
25250 ˇyield count
25251 ˇelse:
25252 ˇlog('while else')
25253 ˇelse:
25254 ˇlog('for else')
25255 "});
25256
25257 // test cursor move to start of each line on tab
25258 // for `try`, `except`, `else`, `finally`, `match` and `def`
25259 cx.set_state(indoc! {"
25260 def main():
25261 ˇ try:
25262 ˇ fetch()
25263 ˇ except ValueError:
25264 ˇ handle_error()
25265 ˇ else:
25266 ˇ match value:
25267 ˇ case _:
25268 ˇ finally:
25269 ˇ def status():
25270 ˇ return 0
25271 "});
25272 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25273 cx.assert_editor_state(indoc! {"
25274 def main():
25275 ˇtry:
25276 ˇfetch()
25277 ˇexcept ValueError:
25278 ˇhandle_error()
25279 ˇelse:
25280 ˇmatch value:
25281 ˇcase _:
25282 ˇfinally:
25283 ˇdef status():
25284 ˇreturn 0
25285 "});
25286 // test relative indent is preserved when tab
25287 // for `try`, `except`, `else`, `finally`, `match` and `def`
25288 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25289 cx.assert_editor_state(indoc! {"
25290 def main():
25291 ˇtry:
25292 ˇfetch()
25293 ˇexcept ValueError:
25294 ˇhandle_error()
25295 ˇelse:
25296 ˇmatch value:
25297 ˇcase _:
25298 ˇfinally:
25299 ˇdef status():
25300 ˇreturn 0
25301 "});
25302}
25303
25304#[gpui::test]
25305async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25306 init_test(cx, |_| {});
25307
25308 let mut cx = EditorTestContext::new(cx).await;
25309 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25310 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25311
25312 // test `else` auto outdents when typed inside `if` block
25313 cx.set_state(indoc! {"
25314 def main():
25315 if i == 2:
25316 return
25317 ˇ
25318 "});
25319 cx.update_editor(|editor, window, cx| {
25320 editor.handle_input("else:", window, cx);
25321 });
25322 cx.assert_editor_state(indoc! {"
25323 def main():
25324 if i == 2:
25325 return
25326 else:ˇ
25327 "});
25328
25329 // test `except` auto outdents when typed inside `try` block
25330 cx.set_state(indoc! {"
25331 def main():
25332 try:
25333 i = 2
25334 ˇ
25335 "});
25336 cx.update_editor(|editor, window, cx| {
25337 editor.handle_input("except:", window, cx);
25338 });
25339 cx.assert_editor_state(indoc! {"
25340 def main():
25341 try:
25342 i = 2
25343 except:ˇ
25344 "});
25345
25346 // test `else` auto outdents when typed inside `except` block
25347 cx.set_state(indoc! {"
25348 def main():
25349 try:
25350 i = 2
25351 except:
25352 j = 2
25353 ˇ
25354 "});
25355 cx.update_editor(|editor, window, cx| {
25356 editor.handle_input("else:", window, cx);
25357 });
25358 cx.assert_editor_state(indoc! {"
25359 def main():
25360 try:
25361 i = 2
25362 except:
25363 j = 2
25364 else:ˇ
25365 "});
25366
25367 // test `finally` auto outdents when typed inside `else` block
25368 cx.set_state(indoc! {"
25369 def main():
25370 try:
25371 i = 2
25372 except:
25373 j = 2
25374 else:
25375 k = 2
25376 ˇ
25377 "});
25378 cx.update_editor(|editor, window, cx| {
25379 editor.handle_input("finally:", window, cx);
25380 });
25381 cx.assert_editor_state(indoc! {"
25382 def main():
25383 try:
25384 i = 2
25385 except:
25386 j = 2
25387 else:
25388 k = 2
25389 finally:ˇ
25390 "});
25391
25392 // test `else` does not outdents when typed inside `except` block right after for block
25393 cx.set_state(indoc! {"
25394 def main():
25395 try:
25396 i = 2
25397 except:
25398 for i in range(n):
25399 pass
25400 ˇ
25401 "});
25402 cx.update_editor(|editor, window, cx| {
25403 editor.handle_input("else:", window, cx);
25404 });
25405 cx.assert_editor_state(indoc! {"
25406 def main():
25407 try:
25408 i = 2
25409 except:
25410 for i in range(n):
25411 pass
25412 else:ˇ
25413 "});
25414
25415 // test `finally` auto outdents when typed inside `else` block right after for block
25416 cx.set_state(indoc! {"
25417 def main():
25418 try:
25419 i = 2
25420 except:
25421 j = 2
25422 else:
25423 for i in range(n):
25424 pass
25425 ˇ
25426 "});
25427 cx.update_editor(|editor, window, cx| {
25428 editor.handle_input("finally:", window, cx);
25429 });
25430 cx.assert_editor_state(indoc! {"
25431 def main():
25432 try:
25433 i = 2
25434 except:
25435 j = 2
25436 else:
25437 for i in range(n):
25438 pass
25439 finally:ˇ
25440 "});
25441
25442 // test `except` outdents to inner "try" block
25443 cx.set_state(indoc! {"
25444 def main():
25445 try:
25446 i = 2
25447 if i == 2:
25448 try:
25449 i = 3
25450 ˇ
25451 "});
25452 cx.update_editor(|editor, window, cx| {
25453 editor.handle_input("except:", window, cx);
25454 });
25455 cx.assert_editor_state(indoc! {"
25456 def main():
25457 try:
25458 i = 2
25459 if i == 2:
25460 try:
25461 i = 3
25462 except:ˇ
25463 "});
25464
25465 // test `except` outdents to outer "try" block
25466 cx.set_state(indoc! {"
25467 def main():
25468 try:
25469 i = 2
25470 if i == 2:
25471 try:
25472 i = 3
25473 ˇ
25474 "});
25475 cx.update_editor(|editor, window, cx| {
25476 editor.handle_input("except:", window, cx);
25477 });
25478 cx.assert_editor_state(indoc! {"
25479 def main():
25480 try:
25481 i = 2
25482 if i == 2:
25483 try:
25484 i = 3
25485 except:ˇ
25486 "});
25487
25488 // test `else` stays at correct indent when typed after `for` block
25489 cx.set_state(indoc! {"
25490 def main():
25491 for i in range(10):
25492 if i == 3:
25493 break
25494 ˇ
25495 "});
25496 cx.update_editor(|editor, window, cx| {
25497 editor.handle_input("else:", window, cx);
25498 });
25499 cx.assert_editor_state(indoc! {"
25500 def main():
25501 for i in range(10):
25502 if i == 3:
25503 break
25504 else:ˇ
25505 "});
25506
25507 // test does not outdent on typing after line with square brackets
25508 cx.set_state(indoc! {"
25509 def f() -> list[str]:
25510 ˇ
25511 "});
25512 cx.update_editor(|editor, window, cx| {
25513 editor.handle_input("a", window, cx);
25514 });
25515 cx.assert_editor_state(indoc! {"
25516 def f() -> list[str]:
25517 aˇ
25518 "});
25519
25520 // test does not outdent on typing : after case keyword
25521 cx.set_state(indoc! {"
25522 match 1:
25523 caseˇ
25524 "});
25525 cx.update_editor(|editor, window, cx| {
25526 editor.handle_input(":", window, cx);
25527 });
25528 cx.assert_editor_state(indoc! {"
25529 match 1:
25530 case:ˇ
25531 "});
25532}
25533
25534#[gpui::test]
25535async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25536 init_test(cx, |_| {});
25537 update_test_language_settings(cx, |settings| {
25538 settings.defaults.extend_comment_on_newline = Some(false);
25539 });
25540 let mut cx = EditorTestContext::new(cx).await;
25541 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25542 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25543
25544 // test correct indent after newline on comment
25545 cx.set_state(indoc! {"
25546 # COMMENT:ˇ
25547 "});
25548 cx.update_editor(|editor, window, cx| {
25549 editor.newline(&Newline, window, cx);
25550 });
25551 cx.assert_editor_state(indoc! {"
25552 # COMMENT:
25553 ˇ
25554 "});
25555
25556 // test correct indent after newline in brackets
25557 cx.set_state(indoc! {"
25558 {ˇ}
25559 "});
25560 cx.update_editor(|editor, window, cx| {
25561 editor.newline(&Newline, window, cx);
25562 });
25563 cx.run_until_parked();
25564 cx.assert_editor_state(indoc! {"
25565 {
25566 ˇ
25567 }
25568 "});
25569
25570 cx.set_state(indoc! {"
25571 (ˇ)
25572 "});
25573 cx.update_editor(|editor, window, cx| {
25574 editor.newline(&Newline, window, cx);
25575 });
25576 cx.run_until_parked();
25577 cx.assert_editor_state(indoc! {"
25578 (
25579 ˇ
25580 )
25581 "});
25582
25583 // do not indent after empty lists or dictionaries
25584 cx.set_state(indoc! {"
25585 a = []ˇ
25586 "});
25587 cx.update_editor(|editor, window, cx| {
25588 editor.newline(&Newline, window, cx);
25589 });
25590 cx.run_until_parked();
25591 cx.assert_editor_state(indoc! {"
25592 a = []
25593 ˇ
25594 "});
25595}
25596
25597#[gpui::test]
25598async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25599 init_test(cx, |_| {});
25600
25601 let mut cx = EditorTestContext::new(cx).await;
25602 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25603 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25604
25605 // test cursor move to start of each line on tab
25606 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25607 cx.set_state(indoc! {"
25608 function main() {
25609 ˇ for item in $items; do
25610 ˇ while [ -n \"$item\" ]; do
25611 ˇ if [ \"$value\" -gt 10 ]; then
25612 ˇ continue
25613 ˇ elif [ \"$value\" -lt 0 ]; then
25614 ˇ break
25615 ˇ else
25616 ˇ echo \"$item\"
25617 ˇ fi
25618 ˇ done
25619 ˇ done
25620 ˇ}
25621 "});
25622 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25623 cx.assert_editor_state(indoc! {"
25624 function main() {
25625 ˇfor item in $items; do
25626 ˇwhile [ -n \"$item\" ]; do
25627 ˇif [ \"$value\" -gt 10 ]; then
25628 ˇcontinue
25629 ˇelif [ \"$value\" -lt 0 ]; then
25630 ˇbreak
25631 ˇelse
25632 ˇecho \"$item\"
25633 ˇfi
25634 ˇdone
25635 ˇdone
25636 ˇ}
25637 "});
25638 // test relative indent is preserved when tab
25639 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25640 cx.assert_editor_state(indoc! {"
25641 function main() {
25642 ˇfor item in $items; do
25643 ˇwhile [ -n \"$item\" ]; do
25644 ˇif [ \"$value\" -gt 10 ]; then
25645 ˇcontinue
25646 ˇelif [ \"$value\" -lt 0 ]; then
25647 ˇbreak
25648 ˇelse
25649 ˇecho \"$item\"
25650 ˇfi
25651 ˇdone
25652 ˇdone
25653 ˇ}
25654 "});
25655
25656 // test cursor move to start of each line on tab
25657 // for `case` statement with patterns
25658 cx.set_state(indoc! {"
25659 function handle() {
25660 ˇ case \"$1\" in
25661 ˇ start)
25662 ˇ echo \"a\"
25663 ˇ ;;
25664 ˇ stop)
25665 ˇ echo \"b\"
25666 ˇ ;;
25667 ˇ *)
25668 ˇ echo \"c\"
25669 ˇ ;;
25670 ˇ esac
25671 ˇ}
25672 "});
25673 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25674 cx.assert_editor_state(indoc! {"
25675 function handle() {
25676 ˇcase \"$1\" in
25677 ˇstart)
25678 ˇecho \"a\"
25679 ˇ;;
25680 ˇstop)
25681 ˇecho \"b\"
25682 ˇ;;
25683 ˇ*)
25684 ˇecho \"c\"
25685 ˇ;;
25686 ˇesac
25687 ˇ}
25688 "});
25689}
25690
25691#[gpui::test]
25692async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25693 init_test(cx, |_| {});
25694
25695 let mut cx = EditorTestContext::new(cx).await;
25696 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25697 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25698
25699 // test indents on comment insert
25700 cx.set_state(indoc! {"
25701 function main() {
25702 ˇ for item in $items; do
25703 ˇ while [ -n \"$item\" ]; do
25704 ˇ if [ \"$value\" -gt 10 ]; then
25705 ˇ continue
25706 ˇ elif [ \"$value\" -lt 0 ]; then
25707 ˇ break
25708 ˇ else
25709 ˇ echo \"$item\"
25710 ˇ fi
25711 ˇ done
25712 ˇ done
25713 ˇ}
25714 "});
25715 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25716 cx.assert_editor_state(indoc! {"
25717 function main() {
25718 #ˇ for item in $items; do
25719 #ˇ while [ -n \"$item\" ]; do
25720 #ˇ if [ \"$value\" -gt 10 ]; then
25721 #ˇ continue
25722 #ˇ elif [ \"$value\" -lt 0 ]; then
25723 #ˇ break
25724 #ˇ else
25725 #ˇ echo \"$item\"
25726 #ˇ fi
25727 #ˇ done
25728 #ˇ done
25729 #ˇ}
25730 "});
25731}
25732
25733#[gpui::test]
25734async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25735 init_test(cx, |_| {});
25736
25737 let mut cx = EditorTestContext::new(cx).await;
25738 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25739 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25740
25741 // test `else` auto outdents when typed inside `if` block
25742 cx.set_state(indoc! {"
25743 if [ \"$1\" = \"test\" ]; then
25744 echo \"foo bar\"
25745 ˇ
25746 "});
25747 cx.update_editor(|editor, window, cx| {
25748 editor.handle_input("else", window, cx);
25749 });
25750 cx.assert_editor_state(indoc! {"
25751 if [ \"$1\" = \"test\" ]; then
25752 echo \"foo bar\"
25753 elseˇ
25754 "});
25755
25756 // test `elif` auto outdents when typed inside `if` block
25757 cx.set_state(indoc! {"
25758 if [ \"$1\" = \"test\" ]; then
25759 echo \"foo bar\"
25760 ˇ
25761 "});
25762 cx.update_editor(|editor, window, cx| {
25763 editor.handle_input("elif", window, cx);
25764 });
25765 cx.assert_editor_state(indoc! {"
25766 if [ \"$1\" = \"test\" ]; then
25767 echo \"foo bar\"
25768 elifˇ
25769 "});
25770
25771 // test `fi` auto outdents when typed inside `else` block
25772 cx.set_state(indoc! {"
25773 if [ \"$1\" = \"test\" ]; then
25774 echo \"foo bar\"
25775 else
25776 echo \"bar baz\"
25777 ˇ
25778 "});
25779 cx.update_editor(|editor, window, cx| {
25780 editor.handle_input("fi", window, cx);
25781 });
25782 cx.assert_editor_state(indoc! {"
25783 if [ \"$1\" = \"test\" ]; then
25784 echo \"foo bar\"
25785 else
25786 echo \"bar baz\"
25787 fiˇ
25788 "});
25789
25790 // test `done` auto outdents when typed inside `while` block
25791 cx.set_state(indoc! {"
25792 while read line; do
25793 echo \"$line\"
25794 ˇ
25795 "});
25796 cx.update_editor(|editor, window, cx| {
25797 editor.handle_input("done", window, cx);
25798 });
25799 cx.assert_editor_state(indoc! {"
25800 while read line; do
25801 echo \"$line\"
25802 doneˇ
25803 "});
25804
25805 // test `done` auto outdents when typed inside `for` block
25806 cx.set_state(indoc! {"
25807 for file in *.txt; do
25808 cat \"$file\"
25809 ˇ
25810 "});
25811 cx.update_editor(|editor, window, cx| {
25812 editor.handle_input("done", window, cx);
25813 });
25814 cx.assert_editor_state(indoc! {"
25815 for file in *.txt; do
25816 cat \"$file\"
25817 doneˇ
25818 "});
25819
25820 // test `esac` auto outdents when typed inside `case` block
25821 cx.set_state(indoc! {"
25822 case \"$1\" in
25823 start)
25824 echo \"foo bar\"
25825 ;;
25826 stop)
25827 echo \"bar baz\"
25828 ;;
25829 ˇ
25830 "});
25831 cx.update_editor(|editor, window, cx| {
25832 editor.handle_input("esac", window, cx);
25833 });
25834 cx.assert_editor_state(indoc! {"
25835 case \"$1\" in
25836 start)
25837 echo \"foo bar\"
25838 ;;
25839 stop)
25840 echo \"bar baz\"
25841 ;;
25842 esacˇ
25843 "});
25844
25845 // test `*)` auto outdents when typed inside `case` block
25846 cx.set_state(indoc! {"
25847 case \"$1\" in
25848 start)
25849 echo \"foo bar\"
25850 ;;
25851 ˇ
25852 "});
25853 cx.update_editor(|editor, window, cx| {
25854 editor.handle_input("*)", window, cx);
25855 });
25856 cx.assert_editor_state(indoc! {"
25857 case \"$1\" in
25858 start)
25859 echo \"foo bar\"
25860 ;;
25861 *)ˇ
25862 "});
25863
25864 // test `fi` outdents to correct level with nested if blocks
25865 cx.set_state(indoc! {"
25866 if [ \"$1\" = \"test\" ]; then
25867 echo \"outer if\"
25868 if [ \"$2\" = \"debug\" ]; then
25869 echo \"inner if\"
25870 ˇ
25871 "});
25872 cx.update_editor(|editor, window, cx| {
25873 editor.handle_input("fi", window, cx);
25874 });
25875 cx.assert_editor_state(indoc! {"
25876 if [ \"$1\" = \"test\" ]; then
25877 echo \"outer if\"
25878 if [ \"$2\" = \"debug\" ]; then
25879 echo \"inner if\"
25880 fiˇ
25881 "});
25882}
25883
25884#[gpui::test]
25885async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25886 init_test(cx, |_| {});
25887 update_test_language_settings(cx, |settings| {
25888 settings.defaults.extend_comment_on_newline = Some(false);
25889 });
25890 let mut cx = EditorTestContext::new(cx).await;
25891 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25892 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25893
25894 // test correct indent after newline on comment
25895 cx.set_state(indoc! {"
25896 # COMMENT:ˇ
25897 "});
25898 cx.update_editor(|editor, window, cx| {
25899 editor.newline(&Newline, window, cx);
25900 });
25901 cx.assert_editor_state(indoc! {"
25902 # COMMENT:
25903 ˇ
25904 "});
25905
25906 // test correct indent after newline after `then`
25907 cx.set_state(indoc! {"
25908
25909 if [ \"$1\" = \"test\" ]; thenˇ
25910 "});
25911 cx.update_editor(|editor, window, cx| {
25912 editor.newline(&Newline, window, cx);
25913 });
25914 cx.run_until_parked();
25915 cx.assert_editor_state(indoc! {"
25916
25917 if [ \"$1\" = \"test\" ]; then
25918 ˇ
25919 "});
25920
25921 // test correct indent after newline after `else`
25922 cx.set_state(indoc! {"
25923 if [ \"$1\" = \"test\" ]; then
25924 elseˇ
25925 "});
25926 cx.update_editor(|editor, window, cx| {
25927 editor.newline(&Newline, window, cx);
25928 });
25929 cx.run_until_parked();
25930 cx.assert_editor_state(indoc! {"
25931 if [ \"$1\" = \"test\" ]; then
25932 else
25933 ˇ
25934 "});
25935
25936 // test correct indent after newline after `elif`
25937 cx.set_state(indoc! {"
25938 if [ \"$1\" = \"test\" ]; then
25939 elifˇ
25940 "});
25941 cx.update_editor(|editor, window, cx| {
25942 editor.newline(&Newline, window, cx);
25943 });
25944 cx.run_until_parked();
25945 cx.assert_editor_state(indoc! {"
25946 if [ \"$1\" = \"test\" ]; then
25947 elif
25948 ˇ
25949 "});
25950
25951 // test correct indent after newline after `do`
25952 cx.set_state(indoc! {"
25953 for file in *.txt; doˇ
25954 "});
25955 cx.update_editor(|editor, window, cx| {
25956 editor.newline(&Newline, window, cx);
25957 });
25958 cx.run_until_parked();
25959 cx.assert_editor_state(indoc! {"
25960 for file in *.txt; do
25961 ˇ
25962 "});
25963
25964 // test correct indent after newline after case pattern
25965 cx.set_state(indoc! {"
25966 case \"$1\" in
25967 start)ˇ
25968 "});
25969 cx.update_editor(|editor, window, cx| {
25970 editor.newline(&Newline, window, cx);
25971 });
25972 cx.run_until_parked();
25973 cx.assert_editor_state(indoc! {"
25974 case \"$1\" in
25975 start)
25976 ˇ
25977 "});
25978
25979 // test correct indent after newline after case pattern
25980 cx.set_state(indoc! {"
25981 case \"$1\" in
25982 start)
25983 ;;
25984 *)ˇ
25985 "});
25986 cx.update_editor(|editor, window, cx| {
25987 editor.newline(&Newline, window, cx);
25988 });
25989 cx.run_until_parked();
25990 cx.assert_editor_state(indoc! {"
25991 case \"$1\" in
25992 start)
25993 ;;
25994 *)
25995 ˇ
25996 "});
25997
25998 // test correct indent after newline after function opening brace
25999 cx.set_state(indoc! {"
26000 function test() {ˇ}
26001 "});
26002 cx.update_editor(|editor, window, cx| {
26003 editor.newline(&Newline, window, cx);
26004 });
26005 cx.run_until_parked();
26006 cx.assert_editor_state(indoc! {"
26007 function test() {
26008 ˇ
26009 }
26010 "});
26011
26012 // test no extra indent after semicolon on same line
26013 cx.set_state(indoc! {"
26014 echo \"test\";ˇ
26015 "});
26016 cx.update_editor(|editor, window, cx| {
26017 editor.newline(&Newline, window, cx);
26018 });
26019 cx.run_until_parked();
26020 cx.assert_editor_state(indoc! {"
26021 echo \"test\";
26022 ˇ
26023 "});
26024}
26025
26026fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26027 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26028 point..point
26029}
26030
26031#[track_caller]
26032fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26033 let (text, ranges) = marked_text_ranges(marked_text, true);
26034 assert_eq!(editor.text(cx), text);
26035 assert_eq!(
26036 editor.selections.ranges(&editor.display_snapshot(cx)),
26037 ranges
26038 .iter()
26039 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26040 .collect::<Vec<_>>(),
26041 "Assert selections are {}",
26042 marked_text
26043 );
26044}
26045
26046pub fn handle_signature_help_request(
26047 cx: &mut EditorLspTestContext,
26048 mocked_response: lsp::SignatureHelp,
26049) -> impl Future<Output = ()> + use<> {
26050 let mut request =
26051 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26052 let mocked_response = mocked_response.clone();
26053 async move { Ok(Some(mocked_response)) }
26054 });
26055
26056 async move {
26057 request.next().await;
26058 }
26059}
26060
26061#[track_caller]
26062pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26063 cx.update_editor(|editor, _, _| {
26064 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26065 let entries = menu.entries.borrow();
26066 let entries = entries
26067 .iter()
26068 .map(|entry| entry.string.as_str())
26069 .collect::<Vec<_>>();
26070 assert_eq!(entries, expected);
26071 } else {
26072 panic!("Expected completions menu");
26073 }
26074 });
26075}
26076
26077#[gpui::test]
26078async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26079 init_test(cx, |_| {});
26080 let mut cx = EditorLspTestContext::new_rust(
26081 lsp::ServerCapabilities {
26082 completion_provider: Some(lsp::CompletionOptions {
26083 ..Default::default()
26084 }),
26085 ..Default::default()
26086 },
26087 cx,
26088 )
26089 .await;
26090 cx.lsp
26091 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26092 Ok(Some(lsp::CompletionResponse::Array(vec![
26093 lsp::CompletionItem {
26094 label: "unsafe".into(),
26095 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26096 range: lsp::Range {
26097 start: lsp::Position {
26098 line: 0,
26099 character: 9,
26100 },
26101 end: lsp::Position {
26102 line: 0,
26103 character: 11,
26104 },
26105 },
26106 new_text: "unsafe".to_string(),
26107 })),
26108 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26109 ..Default::default()
26110 },
26111 ])))
26112 });
26113
26114 cx.update_editor(|editor, _, cx| {
26115 editor.project().unwrap().update(cx, |project, cx| {
26116 project.snippets().update(cx, |snippets, _cx| {
26117 snippets.add_snippet_for_test(
26118 None,
26119 PathBuf::from("test_snippets.json"),
26120 vec![
26121 Arc::new(project::snippet_provider::Snippet {
26122 prefix: vec![
26123 "unlimited word count".to_string(),
26124 "unlimit word count".to_string(),
26125 "unlimited unknown".to_string(),
26126 ],
26127 body: "this is many words".to_string(),
26128 description: Some("description".to_string()),
26129 name: "multi-word snippet test".to_string(),
26130 }),
26131 Arc::new(project::snippet_provider::Snippet {
26132 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26133 body: "fewer words".to_string(),
26134 description: Some("alt description".to_string()),
26135 name: "other name".to_string(),
26136 }),
26137 Arc::new(project::snippet_provider::Snippet {
26138 prefix: vec!["ab aa".to_string()],
26139 body: "abcd".to_string(),
26140 description: None,
26141 name: "alphabet".to_string(),
26142 }),
26143 ],
26144 );
26145 });
26146 })
26147 });
26148
26149 let get_completions = |cx: &mut EditorLspTestContext| {
26150 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26151 Some(CodeContextMenu::Completions(context_menu)) => {
26152 let entries = context_menu.entries.borrow();
26153 entries
26154 .iter()
26155 .map(|entry| entry.string.clone())
26156 .collect_vec()
26157 }
26158 _ => vec![],
26159 })
26160 };
26161
26162 // snippets:
26163 // @foo
26164 // foo bar
26165 //
26166 // when typing:
26167 //
26168 // when typing:
26169 // - if I type a symbol "open the completions with snippets only"
26170 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26171 //
26172 // stuff we need:
26173 // - filtering logic change?
26174 // - remember how far back the completion started.
26175
26176 let test_cases: &[(&str, &[&str])] = &[
26177 (
26178 "un",
26179 &[
26180 "unsafe",
26181 "unlimit word count",
26182 "unlimited unknown",
26183 "unlimited word count",
26184 "unsnip",
26185 ],
26186 ),
26187 (
26188 "u ",
26189 &[
26190 "unlimit word count",
26191 "unlimited unknown",
26192 "unlimited word count",
26193 ],
26194 ),
26195 ("u a", &["ab aa", "unsafe"]), // unsAfe
26196 (
26197 "u u",
26198 &[
26199 "unsafe",
26200 "unlimit word count",
26201 "unlimited unknown", // ranked highest among snippets
26202 "unlimited word count",
26203 "unsnip",
26204 ],
26205 ),
26206 ("uw c", &["unlimit word count", "unlimited word count"]),
26207 (
26208 "u w",
26209 &[
26210 "unlimit word count",
26211 "unlimited word count",
26212 "unlimited unknown",
26213 ],
26214 ),
26215 ("u w ", &["unlimit word count", "unlimited word count"]),
26216 (
26217 "u ",
26218 &[
26219 "unlimit word count",
26220 "unlimited unknown",
26221 "unlimited word count",
26222 ],
26223 ),
26224 ("wor", &[]),
26225 ("uf", &["unsafe"]),
26226 ("af", &["unsafe"]),
26227 ("afu", &[]),
26228 (
26229 "ue",
26230 &["unsafe", "unlimited unknown", "unlimited word count"],
26231 ),
26232 ("@", &["@few"]),
26233 ("@few", &["@few"]),
26234 ("@ ", &[]),
26235 ("a@", &["@few"]),
26236 ("a@f", &["@few", "unsafe"]),
26237 ("a@fw", &["@few"]),
26238 ("a", &["ab aa", "unsafe"]),
26239 ("aa", &["ab aa"]),
26240 ("aaa", &["ab aa"]),
26241 ("ab", &["ab aa"]),
26242 ("ab ", &["ab aa"]),
26243 ("ab a", &["ab aa", "unsafe"]),
26244 ("ab ab", &["ab aa"]),
26245 ("ab ab aa", &["ab aa"]),
26246 ];
26247
26248 for &(input_to_simulate, expected_completions) in test_cases {
26249 cx.set_state("fn a() { ˇ }\n");
26250 for c in input_to_simulate.split("") {
26251 cx.simulate_input(c);
26252 cx.run_until_parked();
26253 }
26254 let expected_completions = expected_completions
26255 .iter()
26256 .map(|s| s.to_string())
26257 .collect_vec();
26258 assert_eq!(
26259 get_completions(&mut cx),
26260 expected_completions,
26261 "< actual / expected >, input = {input_to_simulate:?}",
26262 );
26263 }
26264}
26265
26266/// Handle completion request passing a marked string specifying where the completion
26267/// should be triggered from using '|' character, what range should be replaced, and what completions
26268/// should be returned using '<' and '>' to delimit the range.
26269///
26270/// Also see `handle_completion_request_with_insert_and_replace`.
26271#[track_caller]
26272pub fn handle_completion_request(
26273 marked_string: &str,
26274 completions: Vec<&'static str>,
26275 is_incomplete: bool,
26276 counter: Arc<AtomicUsize>,
26277 cx: &mut EditorLspTestContext,
26278) -> impl Future<Output = ()> {
26279 let complete_from_marker: TextRangeMarker = '|'.into();
26280 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26281 let (_, mut marked_ranges) = marked_text_ranges_by(
26282 marked_string,
26283 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26284 );
26285
26286 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26287 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26288 ));
26289 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26290 let replace_range =
26291 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26292
26293 let mut request =
26294 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26295 let completions = completions.clone();
26296 counter.fetch_add(1, atomic::Ordering::Release);
26297 async move {
26298 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26299 assert_eq!(
26300 params.text_document_position.position,
26301 complete_from_position
26302 );
26303 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26304 is_incomplete,
26305 item_defaults: None,
26306 items: completions
26307 .iter()
26308 .map(|completion_text| lsp::CompletionItem {
26309 label: completion_text.to_string(),
26310 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26311 range: replace_range,
26312 new_text: completion_text.to_string(),
26313 })),
26314 ..Default::default()
26315 })
26316 .collect(),
26317 })))
26318 }
26319 });
26320
26321 async move {
26322 request.next().await;
26323 }
26324}
26325
26326/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26327/// given instead, which also contains an `insert` range.
26328///
26329/// This function uses markers to define ranges:
26330/// - `|` marks the cursor position
26331/// - `<>` marks the replace range
26332/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26333pub fn handle_completion_request_with_insert_and_replace(
26334 cx: &mut EditorLspTestContext,
26335 marked_string: &str,
26336 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26337 counter: Arc<AtomicUsize>,
26338) -> impl Future<Output = ()> {
26339 let complete_from_marker: TextRangeMarker = '|'.into();
26340 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26341 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26342
26343 let (_, mut marked_ranges) = marked_text_ranges_by(
26344 marked_string,
26345 vec![
26346 complete_from_marker.clone(),
26347 replace_range_marker.clone(),
26348 insert_range_marker.clone(),
26349 ],
26350 );
26351
26352 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26353 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26354 ));
26355 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26356 let replace_range =
26357 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26358
26359 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26360 Some(ranges) if !ranges.is_empty() => {
26361 let range1 = ranges[0].clone();
26362 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26363 }
26364 _ => lsp::Range {
26365 start: replace_range.start,
26366 end: complete_from_position,
26367 },
26368 };
26369
26370 let mut request =
26371 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26372 let completions = completions.clone();
26373 counter.fetch_add(1, atomic::Ordering::Release);
26374 async move {
26375 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26376 assert_eq!(
26377 params.text_document_position.position, complete_from_position,
26378 "marker `|` position doesn't match",
26379 );
26380 Ok(Some(lsp::CompletionResponse::Array(
26381 completions
26382 .iter()
26383 .map(|(label, new_text)| lsp::CompletionItem {
26384 label: label.to_string(),
26385 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26386 lsp::InsertReplaceEdit {
26387 insert: insert_range,
26388 replace: replace_range,
26389 new_text: new_text.to_string(),
26390 },
26391 )),
26392 ..Default::default()
26393 })
26394 .collect(),
26395 )))
26396 }
26397 });
26398
26399 async move {
26400 request.next().await;
26401 }
26402}
26403
26404fn handle_resolve_completion_request(
26405 cx: &mut EditorLspTestContext,
26406 edits: Option<Vec<(&'static str, &'static str)>>,
26407) -> impl Future<Output = ()> {
26408 let edits = edits.map(|edits| {
26409 edits
26410 .iter()
26411 .map(|(marked_string, new_text)| {
26412 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26413 let replace_range = cx.to_lsp_range(
26414 MultiBufferOffset(marked_ranges[0].start)
26415 ..MultiBufferOffset(marked_ranges[0].end),
26416 );
26417 lsp::TextEdit::new(replace_range, new_text.to_string())
26418 })
26419 .collect::<Vec<_>>()
26420 });
26421
26422 let mut request =
26423 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26424 let edits = edits.clone();
26425 async move {
26426 Ok(lsp::CompletionItem {
26427 additional_text_edits: edits,
26428 ..Default::default()
26429 })
26430 }
26431 });
26432
26433 async move {
26434 request.next().await;
26435 }
26436}
26437
26438pub(crate) fn update_test_language_settings(
26439 cx: &mut TestAppContext,
26440 f: impl Fn(&mut AllLanguageSettingsContent),
26441) {
26442 cx.update(|cx| {
26443 SettingsStore::update_global(cx, |store, cx| {
26444 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26445 });
26446 });
26447}
26448
26449pub(crate) fn update_test_project_settings(
26450 cx: &mut TestAppContext,
26451 f: impl Fn(&mut ProjectSettingsContent),
26452) {
26453 cx.update(|cx| {
26454 SettingsStore::update_global(cx, |store, cx| {
26455 store.update_user_settings(cx, |settings| f(&mut settings.project));
26456 });
26457 });
26458}
26459
26460pub(crate) fn update_test_editor_settings(
26461 cx: &mut TestAppContext,
26462 f: impl Fn(&mut EditorSettingsContent),
26463) {
26464 cx.update(|cx| {
26465 SettingsStore::update_global(cx, |store, cx| {
26466 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26467 })
26468 })
26469}
26470
26471pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26472 cx.update(|cx| {
26473 assets::Assets.load_test_fonts(cx);
26474 let store = SettingsStore::test(cx);
26475 cx.set_global(store);
26476 theme::init(theme::LoadThemes::JustBase, cx);
26477 release_channel::init(semver::Version::new(0, 0, 0), cx);
26478 crate::init(cx);
26479 });
26480 zlog::init_test();
26481 update_test_language_settings(cx, f);
26482}
26483
26484#[track_caller]
26485fn assert_hunk_revert(
26486 not_reverted_text_with_selections: &str,
26487 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26488 expected_reverted_text_with_selections: &str,
26489 base_text: &str,
26490 cx: &mut EditorLspTestContext,
26491) {
26492 cx.set_state(not_reverted_text_with_selections);
26493 cx.set_head_text(base_text);
26494 cx.executor().run_until_parked();
26495
26496 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26497 let snapshot = editor.snapshot(window, cx);
26498 let reverted_hunk_statuses = snapshot
26499 .buffer_snapshot()
26500 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26501 .map(|hunk| hunk.status().kind)
26502 .collect::<Vec<_>>();
26503
26504 editor.git_restore(&Default::default(), window, cx);
26505 reverted_hunk_statuses
26506 });
26507 cx.executor().run_until_parked();
26508 cx.assert_editor_state(expected_reverted_text_with_selections);
26509 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26510}
26511
26512#[gpui::test(iterations = 10)]
26513async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26514 init_test(cx, |_| {});
26515
26516 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26517 let counter = diagnostic_requests.clone();
26518
26519 let fs = FakeFs::new(cx.executor());
26520 fs.insert_tree(
26521 path!("/a"),
26522 json!({
26523 "first.rs": "fn main() { let a = 5; }",
26524 "second.rs": "// Test file",
26525 }),
26526 )
26527 .await;
26528
26529 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26530 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26531 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26532
26533 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26534 language_registry.add(rust_lang());
26535 let mut fake_servers = language_registry.register_fake_lsp(
26536 "Rust",
26537 FakeLspAdapter {
26538 capabilities: lsp::ServerCapabilities {
26539 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26540 lsp::DiagnosticOptions {
26541 identifier: None,
26542 inter_file_dependencies: true,
26543 workspace_diagnostics: true,
26544 work_done_progress_options: Default::default(),
26545 },
26546 )),
26547 ..Default::default()
26548 },
26549 ..Default::default()
26550 },
26551 );
26552
26553 let editor = workspace
26554 .update(cx, |workspace, window, cx| {
26555 workspace.open_abs_path(
26556 PathBuf::from(path!("/a/first.rs")),
26557 OpenOptions::default(),
26558 window,
26559 cx,
26560 )
26561 })
26562 .unwrap()
26563 .await
26564 .unwrap()
26565 .downcast::<Editor>()
26566 .unwrap();
26567 let fake_server = fake_servers.next().await.unwrap();
26568 let server_id = fake_server.server.server_id();
26569 let mut first_request = fake_server
26570 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26571 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26572 let result_id = Some(new_result_id.to_string());
26573 assert_eq!(
26574 params.text_document.uri,
26575 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26576 );
26577 async move {
26578 Ok(lsp::DocumentDiagnosticReportResult::Report(
26579 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26580 related_documents: None,
26581 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26582 items: Vec::new(),
26583 result_id,
26584 },
26585 }),
26586 ))
26587 }
26588 });
26589
26590 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
26591 project.update(cx, |project, cx| {
26592 let buffer_id = editor
26593 .read(cx)
26594 .buffer()
26595 .read(cx)
26596 .as_singleton()
26597 .expect("created a singleton buffer")
26598 .read(cx)
26599 .remote_id();
26600 let buffer_result_id = project
26601 .lsp_store()
26602 .read(cx)
26603 .result_id(server_id, buffer_id, cx);
26604 assert_eq!(expected, buffer_result_id);
26605 });
26606 };
26607
26608 ensure_result_id(None, cx);
26609 cx.executor().advance_clock(Duration::from_millis(60));
26610 cx.executor().run_until_parked();
26611 assert_eq!(
26612 diagnostic_requests.load(atomic::Ordering::Acquire),
26613 1,
26614 "Opening file should trigger diagnostic request"
26615 );
26616 first_request
26617 .next()
26618 .await
26619 .expect("should have sent the first diagnostics pull request");
26620 ensure_result_id(Some("1".to_string()), cx);
26621
26622 // Editing should trigger diagnostics
26623 editor.update_in(cx, |editor, window, cx| {
26624 editor.handle_input("2", window, cx)
26625 });
26626 cx.executor().advance_clock(Duration::from_millis(60));
26627 cx.executor().run_until_parked();
26628 assert_eq!(
26629 diagnostic_requests.load(atomic::Ordering::Acquire),
26630 2,
26631 "Editing should trigger diagnostic request"
26632 );
26633 ensure_result_id(Some("2".to_string()), cx);
26634
26635 // Moving cursor should not trigger diagnostic request
26636 editor.update_in(cx, |editor, window, cx| {
26637 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26638 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26639 });
26640 });
26641 cx.executor().advance_clock(Duration::from_millis(60));
26642 cx.executor().run_until_parked();
26643 assert_eq!(
26644 diagnostic_requests.load(atomic::Ordering::Acquire),
26645 2,
26646 "Cursor movement should not trigger diagnostic request"
26647 );
26648 ensure_result_id(Some("2".to_string()), cx);
26649 // Multiple rapid edits should be debounced
26650 for _ in 0..5 {
26651 editor.update_in(cx, |editor, window, cx| {
26652 editor.handle_input("x", window, cx)
26653 });
26654 }
26655 cx.executor().advance_clock(Duration::from_millis(60));
26656 cx.executor().run_until_parked();
26657
26658 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26659 assert!(
26660 final_requests <= 4,
26661 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26662 );
26663 ensure_result_id(Some(final_requests.to_string()), cx);
26664}
26665
26666#[gpui::test]
26667async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26668 // Regression test for issue #11671
26669 // Previously, adding a cursor after moving multiple cursors would reset
26670 // the cursor count instead of adding to the existing cursors.
26671 init_test(cx, |_| {});
26672 let mut cx = EditorTestContext::new(cx).await;
26673
26674 // Create a simple buffer with cursor at start
26675 cx.set_state(indoc! {"
26676 ˇaaaa
26677 bbbb
26678 cccc
26679 dddd
26680 eeee
26681 ffff
26682 gggg
26683 hhhh"});
26684
26685 // Add 2 cursors below (so we have 3 total)
26686 cx.update_editor(|editor, window, cx| {
26687 editor.add_selection_below(&Default::default(), window, cx);
26688 editor.add_selection_below(&Default::default(), window, cx);
26689 });
26690
26691 // Verify we have 3 cursors
26692 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26693 assert_eq!(
26694 initial_count, 3,
26695 "Should have 3 cursors after adding 2 below"
26696 );
26697
26698 // Move down one line
26699 cx.update_editor(|editor, window, cx| {
26700 editor.move_down(&MoveDown, window, cx);
26701 });
26702
26703 // Add another cursor below
26704 cx.update_editor(|editor, window, cx| {
26705 editor.add_selection_below(&Default::default(), window, cx);
26706 });
26707
26708 // Should now have 4 cursors (3 original + 1 new)
26709 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26710 assert_eq!(
26711 final_count, 4,
26712 "Should have 4 cursors after moving and adding another"
26713 );
26714}
26715
26716#[gpui::test]
26717async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26718 init_test(cx, |_| {});
26719
26720 let mut cx = EditorTestContext::new(cx).await;
26721
26722 cx.set_state(indoc!(
26723 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26724 Second line here"#
26725 ));
26726
26727 cx.update_editor(|editor, window, cx| {
26728 // Enable soft wrapping with a narrow width to force soft wrapping and
26729 // confirm that more than 2 rows are being displayed.
26730 editor.set_wrap_width(Some(100.0.into()), cx);
26731 assert!(editor.display_text(cx).lines().count() > 2);
26732
26733 editor.add_selection_below(
26734 &AddSelectionBelow {
26735 skip_soft_wrap: true,
26736 },
26737 window,
26738 cx,
26739 );
26740
26741 assert_eq!(
26742 display_ranges(editor, cx),
26743 &[
26744 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26745 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26746 ]
26747 );
26748
26749 editor.add_selection_above(
26750 &AddSelectionAbove {
26751 skip_soft_wrap: true,
26752 },
26753 window,
26754 cx,
26755 );
26756
26757 assert_eq!(
26758 display_ranges(editor, cx),
26759 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26760 );
26761
26762 editor.add_selection_below(
26763 &AddSelectionBelow {
26764 skip_soft_wrap: false,
26765 },
26766 window,
26767 cx,
26768 );
26769
26770 assert_eq!(
26771 display_ranges(editor, cx),
26772 &[
26773 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26774 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26775 ]
26776 );
26777
26778 editor.add_selection_above(
26779 &AddSelectionAbove {
26780 skip_soft_wrap: false,
26781 },
26782 window,
26783 cx,
26784 );
26785
26786 assert_eq!(
26787 display_ranges(editor, cx),
26788 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26789 );
26790 });
26791}
26792
26793#[gpui::test(iterations = 10)]
26794async fn test_document_colors(cx: &mut TestAppContext) {
26795 let expected_color = Rgba {
26796 r: 0.33,
26797 g: 0.33,
26798 b: 0.33,
26799 a: 0.33,
26800 };
26801
26802 init_test(cx, |_| {});
26803
26804 let fs = FakeFs::new(cx.executor());
26805 fs.insert_tree(
26806 path!("/a"),
26807 json!({
26808 "first.rs": "fn main() { let a = 5; }",
26809 }),
26810 )
26811 .await;
26812
26813 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26814 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26815 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26816
26817 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26818 language_registry.add(rust_lang());
26819 let mut fake_servers = language_registry.register_fake_lsp(
26820 "Rust",
26821 FakeLspAdapter {
26822 capabilities: lsp::ServerCapabilities {
26823 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26824 ..lsp::ServerCapabilities::default()
26825 },
26826 name: "rust-analyzer",
26827 ..FakeLspAdapter::default()
26828 },
26829 );
26830 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26831 "Rust",
26832 FakeLspAdapter {
26833 capabilities: lsp::ServerCapabilities {
26834 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26835 ..lsp::ServerCapabilities::default()
26836 },
26837 name: "not-rust-analyzer",
26838 ..FakeLspAdapter::default()
26839 },
26840 );
26841
26842 let editor = workspace
26843 .update(cx, |workspace, window, cx| {
26844 workspace.open_abs_path(
26845 PathBuf::from(path!("/a/first.rs")),
26846 OpenOptions::default(),
26847 window,
26848 cx,
26849 )
26850 })
26851 .unwrap()
26852 .await
26853 .unwrap()
26854 .downcast::<Editor>()
26855 .unwrap();
26856 let fake_language_server = fake_servers.next().await.unwrap();
26857 let fake_language_server_without_capabilities =
26858 fake_servers_without_capabilities.next().await.unwrap();
26859 let requests_made = Arc::new(AtomicUsize::new(0));
26860 let closure_requests_made = Arc::clone(&requests_made);
26861 let mut color_request_handle = fake_language_server
26862 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26863 let requests_made = Arc::clone(&closure_requests_made);
26864 async move {
26865 assert_eq!(
26866 params.text_document.uri,
26867 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26868 );
26869 requests_made.fetch_add(1, atomic::Ordering::Release);
26870 Ok(vec![
26871 lsp::ColorInformation {
26872 range: lsp::Range {
26873 start: lsp::Position {
26874 line: 0,
26875 character: 0,
26876 },
26877 end: lsp::Position {
26878 line: 0,
26879 character: 1,
26880 },
26881 },
26882 color: lsp::Color {
26883 red: 0.33,
26884 green: 0.33,
26885 blue: 0.33,
26886 alpha: 0.33,
26887 },
26888 },
26889 lsp::ColorInformation {
26890 range: lsp::Range {
26891 start: lsp::Position {
26892 line: 0,
26893 character: 0,
26894 },
26895 end: lsp::Position {
26896 line: 0,
26897 character: 1,
26898 },
26899 },
26900 color: lsp::Color {
26901 red: 0.33,
26902 green: 0.33,
26903 blue: 0.33,
26904 alpha: 0.33,
26905 },
26906 },
26907 ])
26908 }
26909 });
26910
26911 let _handle = fake_language_server_without_capabilities
26912 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26913 panic!("Should not be called");
26914 });
26915 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26916 color_request_handle.next().await.unwrap();
26917 cx.run_until_parked();
26918 assert_eq!(
26919 1,
26920 requests_made.load(atomic::Ordering::Acquire),
26921 "Should query for colors once per editor open"
26922 );
26923 editor.update_in(cx, |editor, _, cx| {
26924 assert_eq!(
26925 vec![expected_color],
26926 extract_color_inlays(editor, cx),
26927 "Should have an initial inlay"
26928 );
26929 });
26930
26931 // opening another file in a split should not influence the LSP query counter
26932 workspace
26933 .update(cx, |workspace, window, cx| {
26934 assert_eq!(
26935 workspace.panes().len(),
26936 1,
26937 "Should have one pane with one editor"
26938 );
26939 workspace.move_item_to_pane_in_direction(
26940 &MoveItemToPaneInDirection {
26941 direction: SplitDirection::Right,
26942 focus: false,
26943 clone: true,
26944 },
26945 window,
26946 cx,
26947 );
26948 })
26949 .unwrap();
26950 cx.run_until_parked();
26951 workspace
26952 .update(cx, |workspace, _, cx| {
26953 let panes = workspace.panes();
26954 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26955 for pane in panes {
26956 let editor = pane
26957 .read(cx)
26958 .active_item()
26959 .and_then(|item| item.downcast::<Editor>())
26960 .expect("Should have opened an editor in each split");
26961 let editor_file = editor
26962 .read(cx)
26963 .buffer()
26964 .read(cx)
26965 .as_singleton()
26966 .expect("test deals with singleton buffers")
26967 .read(cx)
26968 .file()
26969 .expect("test buffese should have a file")
26970 .path();
26971 assert_eq!(
26972 editor_file.as_ref(),
26973 rel_path("first.rs"),
26974 "Both editors should be opened for the same file"
26975 )
26976 }
26977 })
26978 .unwrap();
26979
26980 cx.executor().advance_clock(Duration::from_millis(500));
26981 let save = editor.update_in(cx, |editor, window, cx| {
26982 editor.move_to_end(&MoveToEnd, window, cx);
26983 editor.handle_input("dirty", window, cx);
26984 editor.save(
26985 SaveOptions {
26986 format: true,
26987 autosave: true,
26988 },
26989 project.clone(),
26990 window,
26991 cx,
26992 )
26993 });
26994 save.await.unwrap();
26995
26996 color_request_handle.next().await.unwrap();
26997 cx.run_until_parked();
26998 assert_eq!(
26999 2,
27000 requests_made.load(atomic::Ordering::Acquire),
27001 "Should query for colors once per save (deduplicated) and once per formatting after save"
27002 );
27003
27004 drop(editor);
27005 let close = workspace
27006 .update(cx, |workspace, window, cx| {
27007 workspace.active_pane().update(cx, |pane, cx| {
27008 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27009 })
27010 })
27011 .unwrap();
27012 close.await.unwrap();
27013 let close = workspace
27014 .update(cx, |workspace, window, cx| {
27015 workspace.active_pane().update(cx, |pane, cx| {
27016 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27017 })
27018 })
27019 .unwrap();
27020 close.await.unwrap();
27021 assert_eq!(
27022 2,
27023 requests_made.load(atomic::Ordering::Acquire),
27024 "After saving and closing all editors, no extra requests should be made"
27025 );
27026 workspace
27027 .update(cx, |workspace, _, cx| {
27028 assert!(
27029 workspace.active_item(cx).is_none(),
27030 "Should close all editors"
27031 )
27032 })
27033 .unwrap();
27034
27035 workspace
27036 .update(cx, |workspace, window, cx| {
27037 workspace.active_pane().update(cx, |pane, cx| {
27038 pane.navigate_backward(&workspace::GoBack, window, cx);
27039 })
27040 })
27041 .unwrap();
27042 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27043 cx.run_until_parked();
27044 let editor = workspace
27045 .update(cx, |workspace, _, cx| {
27046 workspace
27047 .active_item(cx)
27048 .expect("Should have reopened the editor again after navigating back")
27049 .downcast::<Editor>()
27050 .expect("Should be an editor")
27051 })
27052 .unwrap();
27053
27054 assert_eq!(
27055 2,
27056 requests_made.load(atomic::Ordering::Acquire),
27057 "Cache should be reused on buffer close and reopen"
27058 );
27059 editor.update(cx, |editor, cx| {
27060 assert_eq!(
27061 vec![expected_color],
27062 extract_color_inlays(editor, cx),
27063 "Should have an initial inlay"
27064 );
27065 });
27066
27067 drop(color_request_handle);
27068 let closure_requests_made = Arc::clone(&requests_made);
27069 let mut empty_color_request_handle = fake_language_server
27070 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27071 let requests_made = Arc::clone(&closure_requests_made);
27072 async move {
27073 assert_eq!(
27074 params.text_document.uri,
27075 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27076 );
27077 requests_made.fetch_add(1, atomic::Ordering::Release);
27078 Ok(Vec::new())
27079 }
27080 });
27081 let save = editor.update_in(cx, |editor, window, cx| {
27082 editor.move_to_end(&MoveToEnd, window, cx);
27083 editor.handle_input("dirty_again", window, cx);
27084 editor.save(
27085 SaveOptions {
27086 format: false,
27087 autosave: true,
27088 },
27089 project.clone(),
27090 window,
27091 cx,
27092 )
27093 });
27094 save.await.unwrap();
27095
27096 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27097 empty_color_request_handle.next().await.unwrap();
27098 cx.run_until_parked();
27099 assert_eq!(
27100 3,
27101 requests_made.load(atomic::Ordering::Acquire),
27102 "Should query for colors once per save only, as formatting was not requested"
27103 );
27104 editor.update(cx, |editor, cx| {
27105 assert_eq!(
27106 Vec::<Rgba>::new(),
27107 extract_color_inlays(editor, cx),
27108 "Should clear all colors when the server returns an empty response"
27109 );
27110 });
27111}
27112
27113#[gpui::test]
27114async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27115 init_test(cx, |_| {});
27116 let (editor, cx) = cx.add_window_view(Editor::single_line);
27117 editor.update_in(cx, |editor, window, cx| {
27118 editor.set_text("oops\n\nwow\n", window, cx)
27119 });
27120 cx.run_until_parked();
27121 editor.update(cx, |editor, cx| {
27122 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27123 });
27124 editor.update(cx, |editor, cx| {
27125 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27126 });
27127 cx.run_until_parked();
27128 editor.update(cx, |editor, cx| {
27129 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27130 });
27131}
27132
27133#[gpui::test]
27134async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27135 init_test(cx, |_| {});
27136
27137 cx.update(|cx| {
27138 register_project_item::<Editor>(cx);
27139 });
27140
27141 let fs = FakeFs::new(cx.executor());
27142 fs.insert_tree("/root1", json!({})).await;
27143 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27144 .await;
27145
27146 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27147 let (workspace, cx) =
27148 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27149
27150 let worktree_id = project.update(cx, |project, cx| {
27151 project.worktrees(cx).next().unwrap().read(cx).id()
27152 });
27153
27154 let handle = workspace
27155 .update_in(cx, |workspace, window, cx| {
27156 let project_path = (worktree_id, rel_path("one.pdf"));
27157 workspace.open_path(project_path, None, true, window, cx)
27158 })
27159 .await
27160 .unwrap();
27161
27162 assert_eq!(
27163 handle.to_any_view().entity_type(),
27164 TypeId::of::<InvalidItemView>()
27165 );
27166}
27167
27168#[gpui::test]
27169async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27170 init_test(cx, |_| {});
27171
27172 let language = Arc::new(Language::new(
27173 LanguageConfig::default(),
27174 Some(tree_sitter_rust::LANGUAGE.into()),
27175 ));
27176
27177 // Test hierarchical sibling navigation
27178 let text = r#"
27179 fn outer() {
27180 if condition {
27181 let a = 1;
27182 }
27183 let b = 2;
27184 }
27185
27186 fn another() {
27187 let c = 3;
27188 }
27189 "#;
27190
27191 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27192 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27193 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27194
27195 // Wait for parsing to complete
27196 editor
27197 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27198 .await;
27199
27200 editor.update_in(cx, |editor, window, cx| {
27201 // Start by selecting "let a = 1;" inside the if block
27202 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27203 s.select_display_ranges([
27204 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27205 ]);
27206 });
27207
27208 let initial_selection = editor
27209 .selections
27210 .display_ranges(&editor.display_snapshot(cx));
27211 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27212
27213 // Test select next sibling - should move up levels to find the next sibling
27214 // Since "let a = 1;" has no siblings in the if block, it should move up
27215 // to find "let b = 2;" which is a sibling of the if block
27216 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27217 let next_selection = editor
27218 .selections
27219 .display_ranges(&editor.display_snapshot(cx));
27220
27221 // Should have a selection and it should be different from the initial
27222 assert_eq!(
27223 next_selection.len(),
27224 1,
27225 "Should have one selection after next"
27226 );
27227 assert_ne!(
27228 next_selection[0], initial_selection[0],
27229 "Next sibling selection should be different"
27230 );
27231
27232 // Test hierarchical navigation by going to the end of the current function
27233 // and trying to navigate to the next function
27234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27235 s.select_display_ranges([
27236 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27237 ]);
27238 });
27239
27240 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27241 let function_next_selection = editor
27242 .selections
27243 .display_ranges(&editor.display_snapshot(cx));
27244
27245 // Should move to the next function
27246 assert_eq!(
27247 function_next_selection.len(),
27248 1,
27249 "Should have one selection after function next"
27250 );
27251
27252 // Test select previous sibling navigation
27253 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27254 let prev_selection = editor
27255 .selections
27256 .display_ranges(&editor.display_snapshot(cx));
27257
27258 // Should have a selection and it should be different
27259 assert_eq!(
27260 prev_selection.len(),
27261 1,
27262 "Should have one selection after prev"
27263 );
27264 assert_ne!(
27265 prev_selection[0], function_next_selection[0],
27266 "Previous sibling selection should be different from next"
27267 );
27268 });
27269}
27270
27271#[gpui::test]
27272async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27273 init_test(cx, |_| {});
27274
27275 let mut cx = EditorTestContext::new(cx).await;
27276 cx.set_state(
27277 "let ˇvariable = 42;
27278let another = variable + 1;
27279let result = variable * 2;",
27280 );
27281
27282 // Set up document highlights manually (simulating LSP response)
27283 cx.update_editor(|editor, _window, cx| {
27284 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27285
27286 // Create highlights for "variable" occurrences
27287 let highlight_ranges = [
27288 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27289 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27290 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27291 ];
27292
27293 let anchor_ranges: Vec<_> = highlight_ranges
27294 .iter()
27295 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27296 .collect();
27297
27298 editor.highlight_background::<DocumentHighlightRead>(
27299 &anchor_ranges,
27300 |theme| theme.colors().editor_document_highlight_read_background,
27301 cx,
27302 );
27303 });
27304
27305 // Go to next highlight - should move to second "variable"
27306 cx.update_editor(|editor, window, cx| {
27307 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27308 });
27309 cx.assert_editor_state(
27310 "let variable = 42;
27311let another = ˇvariable + 1;
27312let result = variable * 2;",
27313 );
27314
27315 // Go to next highlight - should move to third "variable"
27316 cx.update_editor(|editor, window, cx| {
27317 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27318 });
27319 cx.assert_editor_state(
27320 "let variable = 42;
27321let another = variable + 1;
27322let result = ˇvariable * 2;",
27323 );
27324
27325 // Go to next highlight - should stay at third "variable" (no wrap-around)
27326 cx.update_editor(|editor, window, cx| {
27327 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27328 });
27329 cx.assert_editor_state(
27330 "let variable = 42;
27331let another = variable + 1;
27332let result = ˇvariable * 2;",
27333 );
27334
27335 // Now test going backwards from third position
27336 cx.update_editor(|editor, window, cx| {
27337 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27338 });
27339 cx.assert_editor_state(
27340 "let variable = 42;
27341let another = ˇvariable + 1;
27342let result = variable * 2;",
27343 );
27344
27345 // Go to previous highlight - should move to first "variable"
27346 cx.update_editor(|editor, window, cx| {
27347 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27348 });
27349 cx.assert_editor_state(
27350 "let ˇvariable = 42;
27351let another = variable + 1;
27352let result = variable * 2;",
27353 );
27354
27355 // Go to previous highlight - should stay on first "variable"
27356 cx.update_editor(|editor, window, cx| {
27357 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27358 });
27359 cx.assert_editor_state(
27360 "let ˇvariable = 42;
27361let another = variable + 1;
27362let result = variable * 2;",
27363 );
27364}
27365
27366#[gpui::test]
27367async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27368 cx: &mut gpui::TestAppContext,
27369) {
27370 init_test(cx, |_| {});
27371
27372 let url = "https://zed.dev";
27373
27374 let markdown_language = Arc::new(Language::new(
27375 LanguageConfig {
27376 name: "Markdown".into(),
27377 ..LanguageConfig::default()
27378 },
27379 None,
27380 ));
27381
27382 let mut cx = EditorTestContext::new(cx).await;
27383 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27384 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27385
27386 cx.update_editor(|editor, window, cx| {
27387 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27388 editor.paste(&Paste, window, cx);
27389 });
27390
27391 cx.assert_editor_state(&format!(
27392 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27393 ));
27394}
27395
27396#[gpui::test]
27397async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
27398 cx: &mut gpui::TestAppContext,
27399) {
27400 init_test(cx, |_| {});
27401
27402 let url = "https://zed.dev";
27403
27404 let markdown_language = Arc::new(Language::new(
27405 LanguageConfig {
27406 name: "Markdown".into(),
27407 ..LanguageConfig::default()
27408 },
27409 None,
27410 ));
27411
27412 let mut cx = EditorTestContext::new(cx).await;
27413 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27414 cx.set_state(&format!(
27415 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
27416 ));
27417
27418 cx.update_editor(|editor, window, cx| {
27419 editor.copy(&Copy, window, cx);
27420 });
27421
27422 cx.set_state(&format!(
27423 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
27424 ));
27425
27426 cx.update_editor(|editor, window, cx| {
27427 editor.paste(&Paste, window, cx);
27428 });
27429
27430 cx.assert_editor_state(&format!(
27431 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
27432 ));
27433}
27434
27435#[gpui::test]
27436async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
27437 cx: &mut gpui::TestAppContext,
27438) {
27439 init_test(cx, |_| {});
27440
27441 let url = "https://zed.dev";
27442
27443 let markdown_language = Arc::new(Language::new(
27444 LanguageConfig {
27445 name: "Markdown".into(),
27446 ..LanguageConfig::default()
27447 },
27448 None,
27449 ));
27450
27451 let mut cx = EditorTestContext::new(cx).await;
27452 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27453 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
27454
27455 cx.update_editor(|editor, window, cx| {
27456 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27457 editor.paste(&Paste, window, cx);
27458 });
27459
27460 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
27461}
27462
27463#[gpui::test]
27464async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
27465 cx: &mut gpui::TestAppContext,
27466) {
27467 init_test(cx, |_| {});
27468
27469 let text = "Awesome";
27470
27471 let markdown_language = Arc::new(Language::new(
27472 LanguageConfig {
27473 name: "Markdown".into(),
27474 ..LanguageConfig::default()
27475 },
27476 None,
27477 ));
27478
27479 let mut cx = EditorTestContext::new(cx).await;
27480 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27481 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
27482
27483 cx.update_editor(|editor, window, cx| {
27484 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
27485 editor.paste(&Paste, window, cx);
27486 });
27487
27488 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
27489}
27490
27491#[gpui::test]
27492async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
27493 cx: &mut gpui::TestAppContext,
27494) {
27495 init_test(cx, |_| {});
27496
27497 let url = "https://zed.dev";
27498
27499 let markdown_language = Arc::new(Language::new(
27500 LanguageConfig {
27501 name: "Rust".into(),
27502 ..LanguageConfig::default()
27503 },
27504 None,
27505 ));
27506
27507 let mut cx = EditorTestContext::new(cx).await;
27508 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27509 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
27510
27511 cx.update_editor(|editor, window, cx| {
27512 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27513 editor.paste(&Paste, window, cx);
27514 });
27515
27516 cx.assert_editor_state(&format!(
27517 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
27518 ));
27519}
27520
27521#[gpui::test]
27522async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
27523 cx: &mut TestAppContext,
27524) {
27525 init_test(cx, |_| {});
27526
27527 let url = "https://zed.dev";
27528
27529 let markdown_language = Arc::new(Language::new(
27530 LanguageConfig {
27531 name: "Markdown".into(),
27532 ..LanguageConfig::default()
27533 },
27534 None,
27535 ));
27536
27537 let (editor, cx) = cx.add_window_view(|window, cx| {
27538 let multi_buffer = MultiBuffer::build_multi(
27539 [
27540 ("this will embed -> link", vec![Point::row_range(0..1)]),
27541 ("this will replace -> link", vec![Point::row_range(0..1)]),
27542 ],
27543 cx,
27544 );
27545 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
27546 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27547 s.select_ranges(vec![
27548 Point::new(0, 19)..Point::new(0, 23),
27549 Point::new(1, 21)..Point::new(1, 25),
27550 ])
27551 });
27552 let first_buffer_id = multi_buffer
27553 .read(cx)
27554 .excerpt_buffer_ids()
27555 .into_iter()
27556 .next()
27557 .unwrap();
27558 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
27559 first_buffer.update(cx, |buffer, cx| {
27560 buffer.set_language(Some(markdown_language.clone()), cx);
27561 });
27562
27563 editor
27564 });
27565 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27566
27567 cx.update_editor(|editor, window, cx| {
27568 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27569 editor.paste(&Paste, window, cx);
27570 });
27571
27572 cx.assert_editor_state(&format!(
27573 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
27574 ));
27575}
27576
27577#[gpui::test]
27578async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
27579 init_test(cx, |_| {});
27580
27581 let fs = FakeFs::new(cx.executor());
27582 fs.insert_tree(
27583 path!("/project"),
27584 json!({
27585 "first.rs": "# First Document\nSome content here.",
27586 "second.rs": "Plain text content for second file.",
27587 }),
27588 )
27589 .await;
27590
27591 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
27592 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27593 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27594
27595 let language = rust_lang();
27596 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27597 language_registry.add(language.clone());
27598 let mut fake_servers = language_registry.register_fake_lsp(
27599 "Rust",
27600 FakeLspAdapter {
27601 ..FakeLspAdapter::default()
27602 },
27603 );
27604
27605 let buffer1 = project
27606 .update(cx, |project, cx| {
27607 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
27608 })
27609 .await
27610 .unwrap();
27611 let buffer2 = project
27612 .update(cx, |project, cx| {
27613 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
27614 })
27615 .await
27616 .unwrap();
27617
27618 let multi_buffer = cx.new(|cx| {
27619 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
27620 multi_buffer.set_excerpts_for_path(
27621 PathKey::for_buffer(&buffer1, cx),
27622 buffer1.clone(),
27623 [Point::zero()..buffer1.read(cx).max_point()],
27624 3,
27625 cx,
27626 );
27627 multi_buffer.set_excerpts_for_path(
27628 PathKey::for_buffer(&buffer2, cx),
27629 buffer2.clone(),
27630 [Point::zero()..buffer1.read(cx).max_point()],
27631 3,
27632 cx,
27633 );
27634 multi_buffer
27635 });
27636
27637 let (editor, cx) = cx.add_window_view(|window, cx| {
27638 Editor::new(
27639 EditorMode::full(),
27640 multi_buffer,
27641 Some(project.clone()),
27642 window,
27643 cx,
27644 )
27645 });
27646
27647 let fake_language_server = fake_servers.next().await.unwrap();
27648
27649 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
27650
27651 let save = editor.update_in(cx, |editor, window, cx| {
27652 assert!(editor.is_dirty(cx));
27653
27654 editor.save(
27655 SaveOptions {
27656 format: true,
27657 autosave: true,
27658 },
27659 project,
27660 window,
27661 cx,
27662 )
27663 });
27664 let (start_edit_tx, start_edit_rx) = oneshot::channel();
27665 let (done_edit_tx, done_edit_rx) = oneshot::channel();
27666 let mut done_edit_rx = Some(done_edit_rx);
27667 let mut start_edit_tx = Some(start_edit_tx);
27668
27669 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
27670 start_edit_tx.take().unwrap().send(()).unwrap();
27671 let done_edit_rx = done_edit_rx.take().unwrap();
27672 async move {
27673 done_edit_rx.await.unwrap();
27674 Ok(None)
27675 }
27676 });
27677
27678 start_edit_rx.await.unwrap();
27679 buffer2
27680 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
27681 .unwrap();
27682
27683 done_edit_tx.send(()).unwrap();
27684
27685 save.await.unwrap();
27686 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
27687}
27688
27689#[track_caller]
27690fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
27691 editor
27692 .all_inlays(cx)
27693 .into_iter()
27694 .filter_map(|inlay| inlay.get_color())
27695 .map(Rgba::from)
27696 .collect()
27697}
27698
27699#[gpui::test]
27700fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
27701 init_test(cx, |_| {});
27702
27703 let editor = cx.add_window(|window, cx| {
27704 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
27705 build_editor(buffer, window, cx)
27706 });
27707
27708 editor
27709 .update(cx, |editor, window, cx| {
27710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27711 s.select_display_ranges([
27712 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
27713 ])
27714 });
27715
27716 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
27717
27718 assert_eq!(
27719 editor.display_text(cx),
27720 "line1\nline2\nline2",
27721 "Duplicating last line upward should create duplicate above, not on same line"
27722 );
27723
27724 assert_eq!(
27725 editor
27726 .selections
27727 .display_ranges(&editor.display_snapshot(cx)),
27728 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27729 "Selection should move to the duplicated line"
27730 );
27731 })
27732 .unwrap();
27733}
27734
27735#[gpui::test]
27736async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27737 init_test(cx, |_| {});
27738
27739 let mut cx = EditorTestContext::new(cx).await;
27740
27741 cx.set_state("line1\nline2ˇ");
27742
27743 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27744
27745 let clipboard_text = cx
27746 .read_from_clipboard()
27747 .and_then(|item| item.text().as_deref().map(str::to_string));
27748
27749 assert_eq!(
27750 clipboard_text,
27751 Some("line2\n".to_string()),
27752 "Copying a line without trailing newline should include a newline"
27753 );
27754
27755 cx.set_state("line1\nˇ");
27756
27757 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27758
27759 cx.assert_editor_state("line1\nline2\nˇ");
27760}
27761
27762#[gpui::test]
27763async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27764 init_test(cx, |_| {});
27765
27766 let mut cx = EditorTestContext::new(cx).await;
27767
27768 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27769
27770 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27771
27772 let clipboard_text = cx
27773 .read_from_clipboard()
27774 .and_then(|item| item.text().as_deref().map(str::to_string));
27775
27776 assert_eq!(
27777 clipboard_text,
27778 Some("line1\nline2\nline3\n".to_string()),
27779 "Copying multiple lines should include a single newline between lines"
27780 );
27781
27782 cx.set_state("lineA\nˇ");
27783
27784 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27785
27786 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27787}
27788
27789#[gpui::test]
27790async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
27791 init_test(cx, |_| {});
27792
27793 let mut cx = EditorTestContext::new(cx).await;
27794
27795 cx.set_state("ˇline1\nˇline2\nˇline3\n");
27796
27797 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
27798
27799 let clipboard_text = cx
27800 .read_from_clipboard()
27801 .and_then(|item| item.text().as_deref().map(str::to_string));
27802
27803 assert_eq!(
27804 clipboard_text,
27805 Some("line1\nline2\nline3\n".to_string()),
27806 "Copying multiple lines should include a single newline between lines"
27807 );
27808
27809 cx.set_state("lineA\nˇ");
27810
27811 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27812
27813 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
27814}
27815
27816#[gpui::test]
27817async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27818 init_test(cx, |_| {});
27819
27820 let mut cx = EditorTestContext::new(cx).await;
27821
27822 cx.set_state("line1\nline2ˇ");
27823 cx.update_editor(|e, window, cx| {
27824 e.set_mode(EditorMode::SingleLine);
27825 assert!(e.key_context(window, cx).contains("end_of_input"));
27826 });
27827 cx.set_state("ˇline1\nline2");
27828 cx.update_editor(|e, window, cx| {
27829 assert!(!e.key_context(window, cx).contains("end_of_input"));
27830 });
27831 cx.set_state("line1ˇ\nline2");
27832 cx.update_editor(|e, window, cx| {
27833 assert!(!e.key_context(window, cx).contains("end_of_input"));
27834 });
27835}
27836
27837#[gpui::test]
27838async fn test_sticky_scroll(cx: &mut TestAppContext) {
27839 init_test(cx, |_| {});
27840 let mut cx = EditorTestContext::new(cx).await;
27841
27842 let buffer = indoc! {"
27843 ˇfn foo() {
27844 let abc = 123;
27845 }
27846 struct Bar;
27847 impl Bar {
27848 fn new() -> Self {
27849 Self
27850 }
27851 }
27852 fn baz() {
27853 }
27854 "};
27855 cx.set_state(&buffer);
27856
27857 cx.update_editor(|e, _, cx| {
27858 e.buffer()
27859 .read(cx)
27860 .as_singleton()
27861 .unwrap()
27862 .update(cx, |buffer, cx| {
27863 buffer.set_language(Some(rust_lang()), cx);
27864 })
27865 });
27866
27867 let mut sticky_headers = |offset: ScrollOffset| {
27868 cx.update_editor(|e, window, cx| {
27869 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27870 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27871 .into_iter()
27872 .map(
27873 |StickyHeader {
27874 start_point,
27875 offset,
27876 ..
27877 }| { (start_point, offset) },
27878 )
27879 .collect::<Vec<_>>()
27880 })
27881 };
27882
27883 let fn_foo = Point { row: 0, column: 0 };
27884 let impl_bar = Point { row: 4, column: 0 };
27885 let fn_new = Point { row: 5, column: 4 };
27886
27887 assert_eq!(sticky_headers(0.0), vec![]);
27888 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27889 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27890 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27891 assert_eq!(sticky_headers(2.0), vec![]);
27892 assert_eq!(sticky_headers(2.5), vec![]);
27893 assert_eq!(sticky_headers(3.0), vec![]);
27894 assert_eq!(sticky_headers(3.5), vec![]);
27895 assert_eq!(sticky_headers(4.0), vec![]);
27896 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27897 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27898 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27899 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27900 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27901 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27902 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27903 assert_eq!(sticky_headers(8.0), vec![]);
27904 assert_eq!(sticky_headers(8.5), vec![]);
27905 assert_eq!(sticky_headers(9.0), vec![]);
27906 assert_eq!(sticky_headers(9.5), vec![]);
27907 assert_eq!(sticky_headers(10.0), vec![]);
27908}
27909
27910#[gpui::test]
27911async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27912 init_test(cx, |_| {});
27913 cx.update(|cx| {
27914 SettingsStore::update_global(cx, |store, cx| {
27915 store.update_user_settings(cx, |settings| {
27916 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27917 enabled: Some(true),
27918 })
27919 });
27920 });
27921 });
27922 let mut cx = EditorTestContext::new(cx).await;
27923
27924 let line_height = cx.editor(|editor, window, _cx| {
27925 editor
27926 .style()
27927 .unwrap()
27928 .text
27929 .line_height_in_pixels(window.rem_size())
27930 });
27931
27932 let buffer = indoc! {"
27933 ˇfn foo() {
27934 let abc = 123;
27935 }
27936 struct Bar;
27937 impl Bar {
27938 fn new() -> Self {
27939 Self
27940 }
27941 }
27942 fn baz() {
27943 }
27944 "};
27945 cx.set_state(&buffer);
27946
27947 cx.update_editor(|e, _, cx| {
27948 e.buffer()
27949 .read(cx)
27950 .as_singleton()
27951 .unwrap()
27952 .update(cx, |buffer, cx| {
27953 buffer.set_language(Some(rust_lang()), cx);
27954 })
27955 });
27956
27957 let fn_foo = || empty_range(0, 0);
27958 let impl_bar = || empty_range(4, 0);
27959 let fn_new = || empty_range(5, 4);
27960
27961 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27962 cx.update_editor(|e, window, cx| {
27963 e.scroll(
27964 gpui::Point {
27965 x: 0.,
27966 y: scroll_offset,
27967 },
27968 None,
27969 window,
27970 cx,
27971 );
27972 });
27973 cx.simulate_click(
27974 gpui::Point {
27975 x: px(0.),
27976 y: click_offset as f32 * line_height,
27977 },
27978 Modifiers::none(),
27979 );
27980 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27981 };
27982
27983 assert_eq!(
27984 scroll_and_click(
27985 4.5, // impl Bar is halfway off the screen
27986 0.0 // click top of screen
27987 ),
27988 // scrolled to impl Bar
27989 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27990 );
27991
27992 assert_eq!(
27993 scroll_and_click(
27994 4.5, // impl Bar is halfway off the screen
27995 0.25 // click middle of impl Bar
27996 ),
27997 // scrolled to impl Bar
27998 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27999 );
28000
28001 assert_eq!(
28002 scroll_and_click(
28003 4.5, // impl Bar is halfway off the screen
28004 1.5 // click below impl Bar (e.g. fn new())
28005 ),
28006 // scrolled to fn new() - this is below the impl Bar header which has persisted
28007 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28008 );
28009
28010 assert_eq!(
28011 scroll_and_click(
28012 5.5, // fn new is halfway underneath impl Bar
28013 0.75 // click on the overlap of impl Bar and fn new()
28014 ),
28015 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28016 );
28017
28018 assert_eq!(
28019 scroll_and_click(
28020 5.5, // fn new is halfway underneath impl Bar
28021 1.25 // click on the visible part of fn new()
28022 ),
28023 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28024 );
28025
28026 assert_eq!(
28027 scroll_and_click(
28028 1.5, // fn foo is halfway off the screen
28029 0.0 // click top of screen
28030 ),
28031 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28032 );
28033
28034 assert_eq!(
28035 scroll_and_click(
28036 1.5, // fn foo is halfway off the screen
28037 0.75 // click visible part of let abc...
28038 )
28039 .0,
28040 // no change in scroll
28041 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28042 (gpui::Point { x: 0., y: 1.5 })
28043 );
28044}
28045
28046#[gpui::test]
28047async fn test_next_prev_reference(cx: &mut TestAppContext) {
28048 const CYCLE_POSITIONS: &[&'static str] = &[
28049 indoc! {"
28050 fn foo() {
28051 let ˇabc = 123;
28052 let x = abc + 1;
28053 let y = abc + 2;
28054 let z = abc + 2;
28055 }
28056 "},
28057 indoc! {"
28058 fn foo() {
28059 let abc = 123;
28060 let x = ˇabc + 1;
28061 let y = abc + 2;
28062 let z = abc + 2;
28063 }
28064 "},
28065 indoc! {"
28066 fn foo() {
28067 let abc = 123;
28068 let x = abc + 1;
28069 let y = ˇabc + 2;
28070 let z = abc + 2;
28071 }
28072 "},
28073 indoc! {"
28074 fn foo() {
28075 let abc = 123;
28076 let x = abc + 1;
28077 let y = abc + 2;
28078 let z = ˇabc + 2;
28079 }
28080 "},
28081 ];
28082
28083 init_test(cx, |_| {});
28084
28085 let mut cx = EditorLspTestContext::new_rust(
28086 lsp::ServerCapabilities {
28087 references_provider: Some(lsp::OneOf::Left(true)),
28088 ..Default::default()
28089 },
28090 cx,
28091 )
28092 .await;
28093
28094 // importantly, the cursor is in the middle
28095 cx.set_state(indoc! {"
28096 fn foo() {
28097 let aˇbc = 123;
28098 let x = abc + 1;
28099 let y = abc + 2;
28100 let z = abc + 2;
28101 }
28102 "});
28103
28104 let reference_ranges = [
28105 lsp::Position::new(1, 8),
28106 lsp::Position::new(2, 12),
28107 lsp::Position::new(3, 12),
28108 lsp::Position::new(4, 12),
28109 ]
28110 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28111
28112 cx.lsp
28113 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
28114 Ok(Some(
28115 reference_ranges
28116 .map(|range| lsp::Location {
28117 uri: params.text_document_position.text_document.uri.clone(),
28118 range,
28119 })
28120 .to_vec(),
28121 ))
28122 });
28123
28124 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
28125 cx.update_editor(|editor, window, cx| {
28126 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
28127 })
28128 .unwrap()
28129 .await
28130 .unwrap()
28131 };
28132
28133 _move(Direction::Next, 1, &mut cx).await;
28134 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28135
28136 _move(Direction::Next, 1, &mut cx).await;
28137 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28138
28139 _move(Direction::Next, 1, &mut cx).await;
28140 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28141
28142 // loops back to the start
28143 _move(Direction::Next, 1, &mut cx).await;
28144 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28145
28146 // loops back to the end
28147 _move(Direction::Prev, 1, &mut cx).await;
28148 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28149
28150 _move(Direction::Prev, 1, &mut cx).await;
28151 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28152
28153 _move(Direction::Prev, 1, &mut cx).await;
28154 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28155
28156 _move(Direction::Prev, 1, &mut cx).await;
28157 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28158
28159 _move(Direction::Next, 3, &mut cx).await;
28160 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28161
28162 _move(Direction::Prev, 2, &mut cx).await;
28163 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28164}
28165
28166#[gpui::test]
28167async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
28168 init_test(cx, |_| {});
28169
28170 let (editor, cx) = cx.add_window_view(|window, cx| {
28171 let multi_buffer = MultiBuffer::build_multi(
28172 [
28173 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28174 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28175 ],
28176 cx,
28177 );
28178 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28179 });
28180
28181 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28182 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28183
28184 cx.assert_excerpts_with_selections(indoc! {"
28185 [EXCERPT]
28186 ˇ1
28187 2
28188 3
28189 [EXCERPT]
28190 1
28191 2
28192 3
28193 "});
28194
28195 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28196 cx.update_editor(|editor, window, cx| {
28197 editor.change_selections(None.into(), window, cx, |s| {
28198 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28199 });
28200 });
28201 cx.assert_excerpts_with_selections(indoc! {"
28202 [EXCERPT]
28203 1
28204 2ˇ
28205 3
28206 [EXCERPT]
28207 1
28208 2
28209 3
28210 "});
28211
28212 cx.update_editor(|editor, window, cx| {
28213 editor
28214 .select_all_matches(&SelectAllMatches, window, cx)
28215 .unwrap();
28216 });
28217 cx.assert_excerpts_with_selections(indoc! {"
28218 [EXCERPT]
28219 1
28220 2ˇ
28221 3
28222 [EXCERPT]
28223 1
28224 2ˇ
28225 3
28226 "});
28227
28228 cx.update_editor(|editor, window, cx| {
28229 editor.handle_input("X", window, cx);
28230 });
28231 cx.assert_excerpts_with_selections(indoc! {"
28232 [EXCERPT]
28233 1
28234 Xˇ
28235 3
28236 [EXCERPT]
28237 1
28238 Xˇ
28239 3
28240 "});
28241
28242 // Scenario 2: Select "2", then fold second buffer before insertion
28243 cx.update_multibuffer(|mb, cx| {
28244 for buffer_id in buffer_ids.iter() {
28245 let buffer = mb.buffer(*buffer_id).unwrap();
28246 buffer.update(cx, |buffer, cx| {
28247 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28248 });
28249 }
28250 });
28251
28252 // Select "2" and select all matches
28253 cx.update_editor(|editor, window, cx| {
28254 editor.change_selections(None.into(), window, cx, |s| {
28255 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28256 });
28257 editor
28258 .select_all_matches(&SelectAllMatches, window, cx)
28259 .unwrap();
28260 });
28261
28262 // Fold second buffer - should remove selections from folded buffer
28263 cx.update_editor(|editor, _, cx| {
28264 editor.fold_buffer(buffer_ids[1], cx);
28265 });
28266 cx.assert_excerpts_with_selections(indoc! {"
28267 [EXCERPT]
28268 1
28269 2ˇ
28270 3
28271 [EXCERPT]
28272 [FOLDED]
28273 "});
28274
28275 // Insert text - should only affect first buffer
28276 cx.update_editor(|editor, window, cx| {
28277 editor.handle_input("Y", window, cx);
28278 });
28279 cx.update_editor(|editor, _, cx| {
28280 editor.unfold_buffer(buffer_ids[1], cx);
28281 });
28282 cx.assert_excerpts_with_selections(indoc! {"
28283 [EXCERPT]
28284 1
28285 Yˇ
28286 3
28287 [EXCERPT]
28288 1
28289 2
28290 3
28291 "});
28292
28293 // Scenario 3: Select "2", then fold first buffer before insertion
28294 cx.update_multibuffer(|mb, cx| {
28295 for buffer_id in buffer_ids.iter() {
28296 let buffer = mb.buffer(*buffer_id).unwrap();
28297 buffer.update(cx, |buffer, cx| {
28298 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28299 });
28300 }
28301 });
28302
28303 // Select "2" and select all matches
28304 cx.update_editor(|editor, window, cx| {
28305 editor.change_selections(None.into(), window, cx, |s| {
28306 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28307 });
28308 editor
28309 .select_all_matches(&SelectAllMatches, window, cx)
28310 .unwrap();
28311 });
28312
28313 // Fold first buffer - should remove selections from folded buffer
28314 cx.update_editor(|editor, _, cx| {
28315 editor.fold_buffer(buffer_ids[0], cx);
28316 });
28317 cx.assert_excerpts_with_selections(indoc! {"
28318 [EXCERPT]
28319 [FOLDED]
28320 [EXCERPT]
28321 1
28322 2ˇ
28323 3
28324 "});
28325
28326 // Insert text - should only affect second buffer
28327 cx.update_editor(|editor, window, cx| {
28328 editor.handle_input("Z", window, cx);
28329 });
28330 cx.update_editor(|editor, _, cx| {
28331 editor.unfold_buffer(buffer_ids[0], cx);
28332 });
28333 cx.assert_excerpts_with_selections(indoc! {"
28334 [EXCERPT]
28335 1
28336 2
28337 3
28338 [EXCERPT]
28339 1
28340 Zˇ
28341 3
28342 "});
28343
28344 // Edge case scenario: fold all buffers, then try to insert
28345 cx.update_editor(|editor, _, cx| {
28346 editor.fold_buffer(buffer_ids[0], cx);
28347 editor.fold_buffer(buffer_ids[1], cx);
28348 });
28349 cx.assert_excerpts_with_selections(indoc! {"
28350 [EXCERPT]
28351 ˇ[FOLDED]
28352 [EXCERPT]
28353 [FOLDED]
28354 "});
28355
28356 // Insert should work via default selection
28357 cx.update_editor(|editor, window, cx| {
28358 editor.handle_input("W", window, cx);
28359 });
28360 cx.update_editor(|editor, _, cx| {
28361 editor.unfold_buffer(buffer_ids[0], cx);
28362 editor.unfold_buffer(buffer_ids[1], cx);
28363 });
28364 cx.assert_excerpts_with_selections(indoc! {"
28365 [EXCERPT]
28366 Wˇ1
28367 2
28368 3
28369 [EXCERPT]
28370 1
28371 Z
28372 3
28373 "});
28374}
28375
28376// FIXME restore these tests in some form
28377// #[gpui::test]
28378// async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
28379// init_test(cx, |_| {});
28380// let mut leader_cx = EditorTestContext::new(cx).await;
28381
28382// let diff_base = indoc!(
28383// r#"
28384// one
28385// two
28386// three
28387// four
28388// five
28389// six
28390// "#
28391// );
28392
28393// let initial_state = indoc!(
28394// r#"
28395// ˇone
28396// two
28397// THREE
28398// four
28399// five
28400// six
28401// "#
28402// );
28403
28404// leader_cx.set_state(initial_state);
28405
28406// leader_cx.set_head_text(&diff_base);
28407// leader_cx.run_until_parked();
28408
28409// let follower = leader_cx.update_multibuffer(|leader, cx| {
28410// leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28411// leader.set_all_diff_hunks_expanded(cx);
28412// leader.get_or_create_follower(cx)
28413// });
28414// follower.update(cx, |follower, cx| {
28415// follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28416// follower.set_all_diff_hunks_expanded(cx);
28417// });
28418
28419// let follower_editor =
28420// leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
28421// // leader_cx.window.focus(&follower_editor.focus_handle(cx));
28422
28423// let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
28424// cx.run_until_parked();
28425
28426// leader_cx.assert_editor_state(initial_state);
28427// follower_cx.assert_editor_state(indoc! {
28428// r#"
28429// ˇone
28430// two
28431// three
28432// four
28433// five
28434// six
28435// "#
28436// });
28437
28438// follower_cx.editor(|editor, _window, cx| {
28439// assert!(editor.read_only(cx));
28440// });
28441
28442// leader_cx.update_editor(|editor, _window, cx| {
28443// editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
28444// });
28445// cx.run_until_parked();
28446
28447// leader_cx.assert_editor_state(indoc! {
28448// r#"
28449// ˇone
28450// two
28451// THREE
28452// four
28453// FIVE
28454// six
28455// "#
28456// });
28457
28458// follower_cx.assert_editor_state(indoc! {
28459// r#"
28460// ˇone
28461// two
28462// three
28463// four
28464// five
28465// six
28466// "#
28467// });
28468
28469// leader_cx.update_editor(|editor, _window, cx| {
28470// editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
28471// });
28472// cx.run_until_parked();
28473
28474// leader_cx.assert_editor_state(indoc! {
28475// r#"
28476// ˇone
28477// two
28478// THREE
28479// four
28480// FIVE
28481// six
28482// SEVEN"#
28483// });
28484
28485// follower_cx.assert_editor_state(indoc! {
28486// r#"
28487// ˇone
28488// two
28489// three
28490// four
28491// five
28492// six
28493// "#
28494// });
28495
28496// leader_cx.update_editor(|editor, window, cx| {
28497// editor.move_down(&MoveDown, window, cx);
28498// editor.refresh_selected_text_highlights(true, window, cx);
28499// });
28500// leader_cx.run_until_parked();
28501// }
28502
28503// #[gpui::test]
28504// async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
28505// init_test(cx, |_| {});
28506// let base_text = "base\n";
28507// let buffer_text = "buffer\n";
28508
28509// let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
28510// let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
28511
28512// let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
28513// let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
28514// let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
28515// let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
28516
28517// let leader = cx.new(|cx| {
28518// let mut leader = MultiBuffer::new(Capability::ReadWrite);
28519// leader.set_all_diff_hunks_expanded(cx);
28520// leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
28521// leader
28522// });
28523// let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
28524// follower.update(cx, |follower, _| {
28525// follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
28526// });
28527
28528// leader.update(cx, |leader, cx| {
28529// leader.insert_excerpts_after(
28530// ExcerptId::min(),
28531// extra_buffer_2.clone(),
28532// vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28533// cx,
28534// );
28535// leader.add_diff(extra_diff_2.clone(), cx);
28536
28537// leader.insert_excerpts_after(
28538// ExcerptId::min(),
28539// extra_buffer_1.clone(),
28540// vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28541// cx,
28542// );
28543// leader.add_diff(extra_diff_1.clone(), cx);
28544
28545// leader.insert_excerpts_after(
28546// ExcerptId::min(),
28547// buffer1.clone(),
28548// vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
28549// cx,
28550// );
28551// leader.add_diff(diff1.clone(), cx);
28552// });
28553
28554// cx.run_until_parked();
28555// let mut cx = cx.add_empty_window();
28556
28557// let leader_editor = cx
28558// .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
28559// let follower_editor = cx.new_window_entity(|window, cx| {
28560// Editor::for_multibuffer(follower.clone(), None, window, cx)
28561// });
28562
28563// let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
28564// leader_cx.assert_editor_state(indoc! {"
28565// ˇbuffer
28566
28567// dummy text 1
28568
28569// dummy text 2
28570// "});
28571// let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
28572// follower_cx.assert_editor_state(indoc! {"
28573// ˇbase
28574
28575// "});
28576// }
28577
28578#[gpui::test]
28579async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
28580 init_test(cx, |_| {});
28581
28582 let (editor, cx) = cx.add_window_view(|window, cx| {
28583 let multi_buffer = MultiBuffer::build_multi(
28584 [
28585 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28586 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
28587 ],
28588 cx,
28589 );
28590 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28591 });
28592
28593 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28594
28595 cx.assert_excerpts_with_selections(indoc! {"
28596 [EXCERPT]
28597 ˇ1
28598 2
28599 3
28600 [EXCERPT]
28601 1
28602 2
28603 3
28604 4
28605 5
28606 6
28607 7
28608 8
28609 9
28610 "});
28611
28612 cx.update_editor(|editor, window, cx| {
28613 editor.change_selections(None.into(), window, cx, |s| {
28614 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
28615 });
28616 });
28617
28618 cx.assert_excerpts_with_selections(indoc! {"
28619 [EXCERPT]
28620 1
28621 2
28622 3
28623 [EXCERPT]
28624 1
28625 2
28626 3
28627 4
28628 5
28629 6
28630 ˇ7
28631 8
28632 9
28633 "});
28634
28635 cx.update_editor(|editor, _window, cx| {
28636 editor.set_vertical_scroll_margin(0, cx);
28637 });
28638
28639 cx.update_editor(|editor, window, cx| {
28640 assert_eq!(editor.vertical_scroll_margin(), 0);
28641 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28642 assert_eq!(
28643 editor.snapshot(window, cx).scroll_position(),
28644 gpui::Point::new(0., 12.0)
28645 );
28646 });
28647
28648 cx.update_editor(|editor, _window, cx| {
28649 editor.set_vertical_scroll_margin(3, cx);
28650 });
28651
28652 cx.update_editor(|editor, window, cx| {
28653 assert_eq!(editor.vertical_scroll_margin(), 3);
28654 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
28655 assert_eq!(
28656 editor.snapshot(window, cx).scroll_position(),
28657 gpui::Point::new(0., 9.0)
28658 );
28659 });
28660}