1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionDelegate,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, Rgba, TestAppContext, UpdateGlobal, VisualTestContext,
21 WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::markdown_lang;
36use languages::rust_lang;
37use lsp::CompletionParams;
38use multi_buffer::{
39 IndentGuide, MultiBufferFilterMode, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
40};
41use parking_lot::Mutex;
42use pretty_assertions::{assert_eq, assert_ne};
43use project::{
44 FakeFs, Project,
45 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
46 project_settings::LspSettings,
47 trusted_worktrees::{PathTrust, TrustedWorktrees},
48};
49use serde_json::{self, json};
50use settings::{
51 AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
52 IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent,
53 SettingsStore,
54};
55use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
56use std::{
57 iter,
58 sync::atomic::{self, AtomicUsize},
59};
60use test::build_editor_with_project;
61use text::ToPoint as _;
62use unindent::Unindent;
63use util::{
64 assert_set_eq, path,
65 rel_path::rel_path,
66 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
67 uri,
68};
69use workspace::{
70 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
71 OpenOptions, ViewId,
72 invalid_item_view::InvalidItemView,
73 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
74 register_project_item,
75};
76
77fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
78 editor
79 .selections
80 .display_ranges(&editor.display_snapshot(cx))
81}
82
83#[gpui::test]
84fn test_edit_events(cx: &mut TestAppContext) {
85 init_test(cx, |_| {});
86
87 let buffer = cx.new(|cx| {
88 let mut buffer = language::Buffer::local("123456", cx);
89 buffer.set_group_interval(Duration::from_secs(1));
90 buffer
91 });
92
93 let events = Rc::new(RefCell::new(Vec::new()));
94 let editor1 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 let entity = cx.entity();
98 cx.subscribe_in(
99 &entity,
100 window,
101 move |_, _, event: &EditorEvent, _, _| match event {
102 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
103 EditorEvent::BufferEdited => {
104 events.borrow_mut().push(("editor1", "buffer edited"))
105 }
106 _ => {}
107 },
108 )
109 .detach();
110 Editor::for_buffer(buffer.clone(), None, window, cx)
111 }
112 });
113
114 let editor2 = cx.add_window({
115 let events = events.clone();
116 |window, cx| {
117 cx.subscribe_in(
118 &cx.entity(),
119 window,
120 move |_, _, event: &EditorEvent, _, _| match event {
121 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
122 EditorEvent::BufferEdited => {
123 events.borrow_mut().push(("editor2", "buffer edited"))
124 }
125 _ => {}
126 },
127 )
128 .detach();
129 Editor::for_buffer(buffer.clone(), None, window, cx)
130 }
131 });
132
133 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
134
135 // Mutating editor 1 will emit an `Edited` event only for that editor.
136 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
137 assert_eq!(
138 mem::take(&mut *events.borrow_mut()),
139 [
140 ("editor1", "edited"),
141 ("editor1", "buffer edited"),
142 ("editor2", "buffer edited"),
143 ]
144 );
145
146 // Mutating editor 2 will emit an `Edited` event only for that editor.
147 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
148 assert_eq!(
149 mem::take(&mut *events.borrow_mut()),
150 [
151 ("editor2", "edited"),
152 ("editor1", "buffer edited"),
153 ("editor2", "buffer edited"),
154 ]
155 );
156
157 // Undoing on editor 1 will emit an `Edited` event only for that editor.
158 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
159 assert_eq!(
160 mem::take(&mut *events.borrow_mut()),
161 [
162 ("editor1", "edited"),
163 ("editor1", "buffer edited"),
164 ("editor2", "buffer edited"),
165 ]
166 );
167
168 // Redoing on editor 1 will emit an `Edited` event only for that editor.
169 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
170 assert_eq!(
171 mem::take(&mut *events.borrow_mut()),
172 [
173 ("editor1", "edited"),
174 ("editor1", "buffer edited"),
175 ("editor2", "buffer edited"),
176 ]
177 );
178
179 // Undoing on editor 2 will emit an `Edited` event only for that editor.
180 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
181 assert_eq!(
182 mem::take(&mut *events.borrow_mut()),
183 [
184 ("editor2", "edited"),
185 ("editor1", "buffer edited"),
186 ("editor2", "buffer edited"),
187 ]
188 );
189
190 // Redoing on editor 2 will emit an `Edited` event only for that editor.
191 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
192 assert_eq!(
193 mem::take(&mut *events.borrow_mut()),
194 [
195 ("editor2", "edited"),
196 ("editor1", "buffer edited"),
197 ("editor2", "buffer edited"),
198 ]
199 );
200
201 // No event is emitted when the mutation is a no-op.
202 _ = editor2.update(cx, |editor, window, cx| {
203 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
204 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
205 });
206
207 editor.backspace(&Backspace, window, cx);
208 });
209 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
210}
211
212#[gpui::test]
213fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
214 init_test(cx, |_| {});
215
216 let mut now = Instant::now();
217 let group_interval = Duration::from_millis(1);
218 let buffer = cx.new(|cx| {
219 let mut buf = language::Buffer::local("123456", cx);
220 buf.set_group_interval(group_interval);
221 buf
222 });
223 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
224 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
225
226 _ = editor.update(cx, |editor, window, cx| {
227 editor.start_transaction_at(now, window, cx);
228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
229 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
230 });
231
232 editor.insert("cd", window, cx);
233 editor.end_transaction_at(now, cx);
234 assert_eq!(editor.text(cx), "12cd56");
235 assert_eq!(
236 editor.selections.ranges(&editor.display_snapshot(cx)),
237 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
238 );
239
240 editor.start_transaction_at(now, window, cx);
241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
242 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
243 });
244 editor.insert("e", window, cx);
245 editor.end_transaction_at(now, cx);
246 assert_eq!(editor.text(cx), "12cde6");
247 assert_eq!(
248 editor.selections.ranges(&editor.display_snapshot(cx)),
249 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
250 );
251
252 now += group_interval + Duration::from_millis(1);
253 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
254 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
255 });
256
257 // Simulate an edit in another editor
258 buffer.update(cx, |buffer, cx| {
259 buffer.start_transaction_at(now, cx);
260 buffer.edit(
261 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
262 None,
263 cx,
264 );
265 buffer.edit(
266 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
267 None,
268 cx,
269 );
270 buffer.end_transaction_at(now, cx);
271 });
272
273 assert_eq!(editor.text(cx), "ab2cde6");
274 assert_eq!(
275 editor.selections.ranges(&editor.display_snapshot(cx)),
276 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
277 );
278
279 // Last transaction happened past the group interval in a different editor.
280 // Undo it individually and don't restore selections.
281 editor.undo(&Undo, window, cx);
282 assert_eq!(editor.text(cx), "12cde6");
283 assert_eq!(
284 editor.selections.ranges(&editor.display_snapshot(cx)),
285 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
286 );
287
288 // First two transactions happened within the group interval in this editor.
289 // Undo them together and restore selections.
290 editor.undo(&Undo, window, cx);
291 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
292 assert_eq!(editor.text(cx), "123456");
293 assert_eq!(
294 editor.selections.ranges(&editor.display_snapshot(cx)),
295 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
296 );
297
298 // Redo the first two transactions together.
299 editor.redo(&Redo, window, cx);
300 assert_eq!(editor.text(cx), "12cde6");
301 assert_eq!(
302 editor.selections.ranges(&editor.display_snapshot(cx)),
303 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
304 );
305
306 // Redo the last transaction on its own.
307 editor.redo(&Redo, window, cx);
308 assert_eq!(editor.text(cx), "ab2cde6");
309 assert_eq!(
310 editor.selections.ranges(&editor.display_snapshot(cx)),
311 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
312 );
313
314 // Test empty transactions.
315 editor.start_transaction_at(now, window, cx);
316 editor.end_transaction_at(now, cx);
317 editor.undo(&Undo, window, cx);
318 assert_eq!(editor.text(cx), "12cde6");
319 });
320}
321
322#[gpui::test]
323fn test_ime_composition(cx: &mut TestAppContext) {
324 init_test(cx, |_| {});
325
326 let buffer = cx.new(|cx| {
327 let mut buffer = language::Buffer::local("abcde", cx);
328 // Ensure automatic grouping doesn't occur.
329 buffer.set_group_interval(Duration::ZERO);
330 buffer
331 });
332
333 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
334 cx.add_window(|window, cx| {
335 let mut editor = build_editor(buffer.clone(), window, cx);
336
337 // Start a new IME composition.
338 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
339 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
340 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
341 assert_eq!(editor.text(cx), "äbcde");
342 assert_eq!(
343 editor.marked_text_ranges(cx),
344 Some(vec![
345 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
346 ])
347 );
348
349 // Finalize IME composition.
350 editor.replace_text_in_range(None, "ā", window, cx);
351 assert_eq!(editor.text(cx), "ābcde");
352 assert_eq!(editor.marked_text_ranges(cx), None);
353
354 // IME composition edits are grouped and are undone/redone at once.
355 editor.undo(&Default::default(), window, cx);
356 assert_eq!(editor.text(cx), "abcde");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358 editor.redo(&Default::default(), window, cx);
359 assert_eq!(editor.text(cx), "ābcde");
360 assert_eq!(editor.marked_text_ranges(cx), None);
361
362 // Start a new IME composition.
363 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
364 assert_eq!(
365 editor.marked_text_ranges(cx),
366 Some(vec![
367 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
368 ])
369 );
370
371 // Undoing during an IME composition cancels it.
372 editor.undo(&Default::default(), window, cx);
373 assert_eq!(editor.text(cx), "ābcde");
374 assert_eq!(editor.marked_text_ranges(cx), None);
375
376 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
377 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
378 assert_eq!(editor.text(cx), "ābcdè");
379 assert_eq!(
380 editor.marked_text_ranges(cx),
381 Some(vec![
382 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
383 ])
384 );
385
386 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
387 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
388 assert_eq!(editor.text(cx), "ābcdę");
389 assert_eq!(editor.marked_text_ranges(cx), None);
390
391 // Start a new IME composition with multiple cursors.
392 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
393 s.select_ranges([
394 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
395 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
396 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
397 ])
398 });
399 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
400 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
401 assert_eq!(
402 editor.marked_text_ranges(cx),
403 Some(vec![
404 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
405 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
406 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
407 ])
408 );
409
410 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
411 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
412 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
413 assert_eq!(
414 editor.marked_text_ranges(cx),
415 Some(vec![
416 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
417 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
418 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
419 ])
420 );
421
422 // Finalize IME composition with multiple cursors.
423 editor.replace_text_in_range(Some(9..10), "2", window, cx);
424 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
425 assert_eq!(editor.marked_text_ranges(cx), None);
426
427 editor
428 });
429}
430
431#[gpui::test]
432fn test_selection_with_mouse(cx: &mut TestAppContext) {
433 init_test(cx, |_| {});
434
435 let editor = cx.add_window(|window, cx| {
436 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
437 build_editor(buffer, window, cx)
438 });
439
440 _ = editor.update(cx, |editor, window, cx| {
441 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
442 });
443 assert_eq!(
444 editor
445 .update(cx, |editor, _, cx| display_ranges(editor, cx))
446 .unwrap(),
447 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
448 );
449
450 _ = editor.update(cx, |editor, window, cx| {
451 editor.update_selection(
452 DisplayPoint::new(DisplayRow(3), 3),
453 0,
454 gpui::Point::<f32>::default(),
455 window,
456 cx,
457 );
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| display_ranges(editor, cx))
463 .unwrap(),
464 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
465 );
466
467 _ = editor.update(cx, |editor, window, cx| {
468 editor.update_selection(
469 DisplayPoint::new(DisplayRow(1), 1),
470 0,
471 gpui::Point::<f32>::default(),
472 window,
473 cx,
474 );
475 });
476
477 assert_eq!(
478 editor
479 .update(cx, |editor, _, cx| display_ranges(editor, cx))
480 .unwrap(),
481 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
482 );
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 editor.update_selection(
487 DisplayPoint::new(DisplayRow(3), 3),
488 0,
489 gpui::Point::<f32>::default(),
490 window,
491 cx,
492 );
493 });
494
495 assert_eq!(
496 editor
497 .update(cx, |editor, _, cx| display_ranges(editor, cx))
498 .unwrap(),
499 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
504 editor.update_selection(
505 DisplayPoint::new(DisplayRow(0), 0),
506 0,
507 gpui::Point::<f32>::default(),
508 window,
509 cx,
510 );
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| display_ranges(editor, cx))
516 .unwrap(),
517 [
518 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
519 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
520 ]
521 );
522
523 _ = editor.update(cx, |editor, window, cx| {
524 editor.end_selection(window, cx);
525 });
526
527 assert_eq!(
528 editor
529 .update(cx, |editor, _, cx| display_ranges(editor, cx))
530 .unwrap(),
531 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
532 );
533}
534
535#[gpui::test]
536fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
537 init_test(cx, |_| {});
538
539 let editor = cx.add_window(|window, cx| {
540 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
541 build_editor(buffer, window, cx)
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.end_selection(window, cx);
550 });
551
552 _ = editor.update(cx, |editor, window, cx| {
553 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
554 });
555
556 _ = editor.update(cx, |editor, window, cx| {
557 editor.end_selection(window, cx);
558 });
559
560 assert_eq!(
561 editor
562 .update(cx, |editor, _, cx| display_ranges(editor, cx))
563 .unwrap(),
564 [
565 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
566 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
567 ]
568 );
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.end_selection(window, cx);
576 });
577
578 assert_eq!(
579 editor
580 .update(cx, |editor, _, cx| display_ranges(editor, cx))
581 .unwrap(),
582 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
583 );
584}
585
586#[gpui::test]
587fn test_canceling_pending_selection(cx: &mut TestAppContext) {
588 init_test(cx, |_| {});
589
590 let editor = cx.add_window(|window, cx| {
591 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
592 build_editor(buffer, window, cx)
593 });
594
595 _ = editor.update(cx, |editor, window, cx| {
596 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
597 assert_eq!(
598 display_ranges(editor, cx),
599 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
600 );
601 });
602
603 _ = editor.update(cx, |editor, window, cx| {
604 editor.update_selection(
605 DisplayPoint::new(DisplayRow(3), 3),
606 0,
607 gpui::Point::<f32>::default(),
608 window,
609 cx,
610 );
611 assert_eq!(
612 display_ranges(editor, cx),
613 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
614 );
615 });
616
617 _ = editor.update(cx, |editor, window, cx| {
618 editor.cancel(&Cancel, window, cx);
619 editor.update_selection(
620 DisplayPoint::new(DisplayRow(1), 1),
621 0,
622 gpui::Point::<f32>::default(),
623 window,
624 cx,
625 );
626 assert_eq!(
627 display_ranges(editor, cx),
628 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
629 );
630 });
631}
632
633#[gpui::test]
634fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
635 init_test(cx, |_| {});
636
637 let editor = cx.add_window(|window, cx| {
638 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
639 build_editor(buffer, window, cx)
640 });
641
642 _ = editor.update(cx, |editor, window, cx| {
643 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
644 assert_eq!(
645 display_ranges(editor, cx),
646 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
647 );
648
649 editor.move_down(&Default::default(), window, cx);
650 assert_eq!(
651 display_ranges(editor, cx),
652 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
653 );
654
655 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
656 assert_eq!(
657 display_ranges(editor, cx),
658 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
659 );
660
661 editor.move_up(&Default::default(), window, cx);
662 assert_eq!(
663 display_ranges(editor, cx),
664 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
665 );
666 });
667}
668
669#[gpui::test]
670fn test_extending_selection(cx: &mut TestAppContext) {
671 init_test(cx, |_| {});
672
673 let editor = cx.add_window(|window, cx| {
674 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
675 build_editor(buffer, window, cx)
676 });
677
678 _ = editor.update(cx, |editor, window, cx| {
679 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
680 editor.end_selection(window, cx);
681 assert_eq!(
682 display_ranges(editor, cx),
683 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
684 );
685
686 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
687 editor.end_selection(window, cx);
688 assert_eq!(
689 display_ranges(editor, cx),
690 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
691 );
692
693 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
694 editor.end_selection(window, cx);
695 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
696 assert_eq!(
697 display_ranges(editor, cx),
698 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
699 );
700
701 editor.update_selection(
702 DisplayPoint::new(DisplayRow(0), 1),
703 0,
704 gpui::Point::<f32>::default(),
705 window,
706 cx,
707 );
708 editor.end_selection(window, cx);
709 assert_eq!(
710 display_ranges(editor, cx),
711 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
712 );
713
714 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
715 editor.end_selection(window, cx);
716 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
717 editor.end_selection(window, cx);
718 assert_eq!(
719 display_ranges(editor, cx),
720 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
721 );
722
723 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
724 assert_eq!(
725 display_ranges(editor, cx),
726 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
727 );
728
729 editor.update_selection(
730 DisplayPoint::new(DisplayRow(0), 6),
731 0,
732 gpui::Point::<f32>::default(),
733 window,
734 cx,
735 );
736 assert_eq!(
737 display_ranges(editor, cx),
738 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
739 );
740
741 editor.update_selection(
742 DisplayPoint::new(DisplayRow(0), 1),
743 0,
744 gpui::Point::<f32>::default(),
745 window,
746 cx,
747 );
748 editor.end_selection(window, cx);
749 assert_eq!(
750 display_ranges(editor, cx),
751 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
752 );
753 });
754}
755
756#[gpui::test]
757fn test_clone(cx: &mut TestAppContext) {
758 init_test(cx, |_| {});
759
760 let (text, selection_ranges) = marked_text_ranges(
761 indoc! {"
762 one
763 two
764 threeˇ
765 four
766 fiveˇ
767 "},
768 true,
769 );
770
771 let editor = cx.add_window(|window, cx| {
772 let buffer = MultiBuffer::build_simple(&text, cx);
773 build_editor(buffer, window, cx)
774 });
775
776 _ = editor.update(cx, |editor, window, cx| {
777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
778 s.select_ranges(
779 selection_ranges
780 .iter()
781 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
782 )
783 });
784 editor.fold_creases(
785 vec![
786 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
787 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
788 ],
789 true,
790 window,
791 cx,
792 );
793 });
794
795 let cloned_editor = editor
796 .update(cx, |editor, _, cx| {
797 cx.open_window(Default::default(), |window, cx| {
798 cx.new(|cx| editor.clone(window, cx))
799 })
800 })
801 .unwrap()
802 .unwrap();
803
804 let snapshot = editor
805 .update(cx, |e, window, cx| e.snapshot(window, cx))
806 .unwrap();
807 let cloned_snapshot = cloned_editor
808 .update(cx, |e, window, cx| e.snapshot(window, cx))
809 .unwrap();
810
811 assert_eq!(
812 cloned_editor
813 .update(cx, |e, _, cx| e.display_text(cx))
814 .unwrap(),
815 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
816 );
817 assert_eq!(
818 cloned_snapshot
819 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
820 .collect::<Vec<_>>(),
821 snapshot
822 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
823 .collect::<Vec<_>>(),
824 );
825 assert_set_eq!(
826 cloned_editor
827 .update(cx, |editor, _, cx| editor
828 .selections
829 .ranges::<Point>(&editor.display_snapshot(cx)))
830 .unwrap(),
831 editor
832 .update(cx, |editor, _, cx| editor
833 .selections
834 .ranges(&editor.display_snapshot(cx)))
835 .unwrap()
836 );
837 assert_set_eq!(
838 cloned_editor
839 .update(cx, |e, _window, cx| e
840 .selections
841 .display_ranges(&e.display_snapshot(cx)))
842 .unwrap(),
843 editor
844 .update(cx, |e, _, cx| e
845 .selections
846 .display_ranges(&e.display_snapshot(cx)))
847 .unwrap()
848 );
849}
850
851#[gpui::test]
852async fn test_navigation_history(cx: &mut TestAppContext) {
853 init_test(cx, |_| {});
854
855 use workspace::item::Item;
856
857 let fs = FakeFs::new(cx.executor());
858 let project = Project::test(fs, [], cx).await;
859 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
860 let pane = workspace
861 .update(cx, |workspace, _, _| workspace.active_pane().clone())
862 .unwrap();
863
864 _ = workspace.update(cx, |_v, window, cx| {
865 cx.new(|cx| {
866 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
867 let mut editor = build_editor(buffer, window, cx);
868 let handle = cx.entity();
869 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
870
871 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
872 editor.nav_history.as_mut().unwrap().pop_backward(cx)
873 }
874
875 // Move the cursor a small distance.
876 // Nothing is added to the navigation history.
877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
878 s.select_display_ranges([
879 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
880 ])
881 });
882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
883 s.select_display_ranges([
884 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
885 ])
886 });
887 assert!(pop_history(&mut editor, cx).is_none());
888
889 // Move the cursor a large distance.
890 // The history can jump back to the previous position.
891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
892 s.select_display_ranges([
893 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
894 ])
895 });
896 let nav_entry = pop_history(&mut editor, cx).unwrap();
897 editor.navigate(nav_entry.data.unwrap(), window, cx);
898 assert_eq!(nav_entry.item.id(), cx.entity_id());
899 assert_eq!(
900 editor
901 .selections
902 .display_ranges(&editor.display_snapshot(cx)),
903 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
904 );
905 assert!(pop_history(&mut editor, cx).is_none());
906
907 // Move the cursor a small distance via the mouse.
908 // Nothing is added to the navigation history.
909 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
910 editor.end_selection(window, cx);
911 assert_eq!(
912 editor
913 .selections
914 .display_ranges(&editor.display_snapshot(cx)),
915 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
916 );
917 assert!(pop_history(&mut editor, cx).is_none());
918
919 // Move the cursor a large distance via the mouse.
920 // The history can jump back to the previous position.
921 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
922 editor.end_selection(window, cx);
923 assert_eq!(
924 editor
925 .selections
926 .display_ranges(&editor.display_snapshot(cx)),
927 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
928 );
929 let nav_entry = pop_history(&mut editor, cx).unwrap();
930 editor.navigate(nav_entry.data.unwrap(), window, cx);
931 assert_eq!(nav_entry.item.id(), cx.entity_id());
932 assert_eq!(
933 editor
934 .selections
935 .display_ranges(&editor.display_snapshot(cx)),
936 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
937 );
938 assert!(pop_history(&mut editor, cx).is_none());
939
940 // Set scroll position to check later
941 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
942 let original_scroll_position = editor.scroll_manager.anchor();
943
944 // Jump to the end of the document and adjust scroll
945 editor.move_to_end(&MoveToEnd, window, cx);
946 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
947 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
948
949 let nav_entry = pop_history(&mut editor, cx).unwrap();
950 editor.navigate(nav_entry.data.unwrap(), window, cx);
951 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
952
953 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
954 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
955 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
956 let invalid_point = Point::new(9999, 0);
957 editor.navigate(
958 Box::new(NavigationData {
959 cursor_anchor: invalid_anchor,
960 cursor_position: invalid_point,
961 scroll_anchor: ScrollAnchor {
962 anchor: invalid_anchor,
963 offset: Default::default(),
964 },
965 scroll_top_row: invalid_point.row,
966 }),
967 window,
968 cx,
969 );
970 assert_eq!(
971 editor
972 .selections
973 .display_ranges(&editor.display_snapshot(cx)),
974 &[editor.max_point(cx)..editor.max_point(cx)]
975 );
976 assert_eq!(
977 editor.scroll_position(cx),
978 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
979 );
980
981 editor
982 })
983 });
984}
985
986#[gpui::test]
987fn test_cancel(cx: &mut TestAppContext) {
988 init_test(cx, |_| {});
989
990 let editor = cx.add_window(|window, cx| {
991 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
992 build_editor(buffer, window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
997 editor.update_selection(
998 DisplayPoint::new(DisplayRow(1), 1),
999 0,
1000 gpui::Point::<f32>::default(),
1001 window,
1002 cx,
1003 );
1004 editor.end_selection(window, cx);
1005
1006 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1007 editor.update_selection(
1008 DisplayPoint::new(DisplayRow(0), 3),
1009 0,
1010 gpui::Point::<f32>::default(),
1011 window,
1012 cx,
1013 );
1014 editor.end_selection(window, cx);
1015 assert_eq!(
1016 display_ranges(editor, cx),
1017 [
1018 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1019 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1020 ]
1021 );
1022 });
1023
1024 _ = editor.update(cx, |editor, window, cx| {
1025 editor.cancel(&Cancel, window, cx);
1026 assert_eq!(
1027 display_ranges(editor, cx),
1028 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1029 );
1030 });
1031
1032 _ = editor.update(cx, |editor, window, cx| {
1033 editor.cancel(&Cancel, window, cx);
1034 assert_eq!(
1035 display_ranges(editor, cx),
1036 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1037 );
1038 });
1039}
1040
1041#[gpui::test]
1042fn test_fold_action(cx: &mut TestAppContext) {
1043 init_test(cx, |_| {});
1044
1045 let editor = cx.add_window(|window, cx| {
1046 let buffer = MultiBuffer::build_simple(
1047 &"
1048 impl Foo {
1049 // Hello!
1050
1051 fn a() {
1052 1
1053 }
1054
1055 fn b() {
1056 2
1057 }
1058
1059 fn c() {
1060 3
1061 }
1062 }
1063 "
1064 .unindent(),
1065 cx,
1066 );
1067 build_editor(buffer, window, cx)
1068 });
1069
1070 _ = editor.update(cx, |editor, window, cx| {
1071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1072 s.select_display_ranges([
1073 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1074 ]);
1075 });
1076 editor.fold(&Fold, window, cx);
1077 assert_eq!(
1078 editor.display_text(cx),
1079 "
1080 impl Foo {
1081 // Hello!
1082
1083 fn a() {
1084 1
1085 }
1086
1087 fn b() {⋯
1088 }
1089
1090 fn c() {⋯
1091 }
1092 }
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.fold(&Fold, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 impl Foo {⋯
1102 }
1103 "
1104 .unindent(),
1105 );
1106
1107 editor.unfold_lines(&UnfoldLines, window, cx);
1108 assert_eq!(
1109 editor.display_text(cx),
1110 "
1111 impl Foo {
1112 // Hello!
1113
1114 fn a() {
1115 1
1116 }
1117
1118 fn b() {⋯
1119 }
1120
1121 fn c() {⋯
1122 }
1123 }
1124 "
1125 .unindent(),
1126 );
1127
1128 editor.unfold_lines(&UnfoldLines, window, cx);
1129 assert_eq!(
1130 editor.display_text(cx),
1131 editor.buffer.read(cx).read(cx).text()
1132 );
1133 });
1134}
1135
1136#[gpui::test]
1137fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1138 init_test(cx, |_| {});
1139
1140 let editor = cx.add_window(|window, cx| {
1141 let buffer = MultiBuffer::build_simple(
1142 &"
1143 class Foo:
1144 # Hello!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152 def c():
1153 print(3)
1154 "
1155 .unindent(),
1156 cx,
1157 );
1158 build_editor(buffer, window, cx)
1159 });
1160
1161 _ = editor.update(cx, |editor, window, cx| {
1162 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1163 s.select_display_ranges([
1164 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1165 ]);
1166 });
1167 editor.fold(&Fold, window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():
1175 print(1)
1176
1177 def b():⋯
1178
1179 def c():⋯
1180 "
1181 .unindent(),
1182 );
1183
1184 editor.fold(&Fold, window, cx);
1185 assert_eq!(
1186 editor.display_text(cx),
1187 "
1188 class Foo:⋯
1189 "
1190 .unindent(),
1191 );
1192
1193 editor.unfold_lines(&UnfoldLines, window, cx);
1194 assert_eq!(
1195 editor.display_text(cx),
1196 "
1197 class Foo:
1198 # Hello!
1199
1200 def a():
1201 print(1)
1202
1203 def b():⋯
1204
1205 def c():⋯
1206 "
1207 .unindent(),
1208 );
1209
1210 editor.unfold_lines(&UnfoldLines, window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 editor.buffer.read(cx).read(cx).text()
1214 );
1215 });
1216}
1217
1218#[gpui::test]
1219fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1220 init_test(cx, |_| {});
1221
1222 let editor = cx.add_window(|window, cx| {
1223 let buffer = MultiBuffer::build_simple(
1224 &"
1225 class Foo:
1226 # Hello!
1227
1228 def a():
1229 print(1)
1230
1231 def b():
1232 print(2)
1233
1234
1235 def c():
1236 print(3)
1237
1238
1239 "
1240 .unindent(),
1241 cx,
1242 );
1243 build_editor(buffer, window, cx)
1244 });
1245
1246 _ = editor.update(cx, |editor, window, cx| {
1247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1248 s.select_display_ranges([
1249 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1250 ]);
1251 });
1252 editor.fold(&Fold, window, cx);
1253 assert_eq!(
1254 editor.display_text(cx),
1255 "
1256 class Foo:
1257 # Hello!
1258
1259 def a():
1260 print(1)
1261
1262 def b():⋯
1263
1264
1265 def c():⋯
1266
1267
1268 "
1269 .unindent(),
1270 );
1271
1272 editor.fold(&Fold, window, cx);
1273 assert_eq!(
1274 editor.display_text(cx),
1275 "
1276 class Foo:⋯
1277
1278
1279 "
1280 .unindent(),
1281 );
1282
1283 editor.unfold_lines(&UnfoldLines, window, cx);
1284 assert_eq!(
1285 editor.display_text(cx),
1286 "
1287 class Foo:
1288 # Hello!
1289
1290 def a():
1291 print(1)
1292
1293 def b():⋯
1294
1295
1296 def c():⋯
1297
1298
1299 "
1300 .unindent(),
1301 );
1302
1303 editor.unfold_lines(&UnfoldLines, window, cx);
1304 assert_eq!(
1305 editor.display_text(cx),
1306 editor.buffer.read(cx).read(cx).text()
1307 );
1308 });
1309}
1310
1311#[gpui::test]
1312fn test_fold_at_level(cx: &mut TestAppContext) {
1313 init_test(cx, |_| {});
1314
1315 let editor = cx.add_window(|window, cx| {
1316 let buffer = MultiBuffer::build_simple(
1317 &"
1318 class Foo:
1319 # Hello!
1320
1321 def a():
1322 print(1)
1323
1324 def b():
1325 print(2)
1326
1327
1328 class Bar:
1329 # World!
1330
1331 def a():
1332 print(1)
1333
1334 def b():
1335 print(2)
1336
1337
1338 "
1339 .unindent(),
1340 cx,
1341 );
1342 build_editor(buffer, window, cx)
1343 });
1344
1345 _ = editor.update(cx, |editor, window, cx| {
1346 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1347 assert_eq!(
1348 editor.display_text(cx),
1349 "
1350 class Foo:
1351 # Hello!
1352
1353 def a():⋯
1354
1355 def b():⋯
1356
1357
1358 class Bar:
1359 # World!
1360
1361 def a():⋯
1362
1363 def b():⋯
1364
1365
1366 "
1367 .unindent(),
1368 );
1369
1370 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1371 assert_eq!(
1372 editor.display_text(cx),
1373 "
1374 class Foo:⋯
1375
1376
1377 class Bar:⋯
1378
1379
1380 "
1381 .unindent(),
1382 );
1383
1384 editor.unfold_all(&UnfoldAll, window, cx);
1385 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1386 assert_eq!(
1387 editor.display_text(cx),
1388 "
1389 class Foo:
1390 # Hello!
1391
1392 def a():
1393 print(1)
1394
1395 def b():
1396 print(2)
1397
1398
1399 class Bar:
1400 # World!
1401
1402 def a():
1403 print(1)
1404
1405 def b():
1406 print(2)
1407
1408
1409 "
1410 .unindent(),
1411 );
1412
1413 assert_eq!(
1414 editor.display_text(cx),
1415 editor.buffer.read(cx).read(cx).text()
1416 );
1417 let (_, positions) = marked_text_ranges(
1418 &"
1419 class Foo:
1420 # Hello!
1421
1422 def a():
1423 print(1)
1424
1425 def b():
1426 p«riˇ»nt(2)
1427
1428
1429 class Bar:
1430 # World!
1431
1432 def a():
1433 «ˇprint(1)
1434
1435 def b():
1436 print(2)»
1437
1438
1439 "
1440 .unindent(),
1441 true,
1442 );
1443
1444 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1445 s.select_ranges(
1446 positions
1447 .iter()
1448 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1449 )
1450 });
1451
1452 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1453 assert_eq!(
1454 editor.display_text(cx),
1455 "
1456 class Foo:
1457 # Hello!
1458
1459 def a():⋯
1460
1461 def b():
1462 print(2)
1463
1464
1465 class Bar:
1466 # World!
1467
1468 def a():
1469 print(1)
1470
1471 def b():
1472 print(2)
1473
1474
1475 "
1476 .unindent(),
1477 );
1478 });
1479}
1480
1481#[gpui::test]
1482fn test_move_cursor(cx: &mut TestAppContext) {
1483 init_test(cx, |_| {});
1484
1485 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1486 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1487
1488 buffer.update(cx, |buffer, cx| {
1489 buffer.edit(
1490 vec![
1491 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1492 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1493 ],
1494 None,
1495 cx,
1496 );
1497 });
1498 _ = editor.update(cx, |editor, window, cx| {
1499 assert_eq!(
1500 display_ranges(editor, cx),
1501 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1502 );
1503
1504 editor.move_down(&MoveDown, window, cx);
1505 assert_eq!(
1506 display_ranges(editor, cx),
1507 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1508 );
1509
1510 editor.move_right(&MoveRight, window, cx);
1511 assert_eq!(
1512 display_ranges(editor, cx),
1513 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1514 );
1515
1516 editor.move_left(&MoveLeft, window, cx);
1517 assert_eq!(
1518 display_ranges(editor, cx),
1519 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1520 );
1521
1522 editor.move_up(&MoveUp, window, cx);
1523 assert_eq!(
1524 display_ranges(editor, cx),
1525 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1526 );
1527
1528 editor.move_to_end(&MoveToEnd, window, cx);
1529 assert_eq!(
1530 display_ranges(editor, cx),
1531 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1532 );
1533
1534 editor.move_to_beginning(&MoveToBeginning, window, cx);
1535 assert_eq!(
1536 display_ranges(editor, cx),
1537 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1538 );
1539
1540 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1541 s.select_display_ranges([
1542 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1543 ]);
1544 });
1545 editor.select_to_beginning(&SelectToBeginning, window, cx);
1546 assert_eq!(
1547 display_ranges(editor, cx),
1548 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1549 );
1550
1551 editor.select_to_end(&SelectToEnd, window, cx);
1552 assert_eq!(
1553 display_ranges(editor, cx),
1554 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1555 );
1556 });
1557}
1558
1559#[gpui::test]
1560fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1561 init_test(cx, |_| {});
1562
1563 let editor = cx.add_window(|window, cx| {
1564 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1565 build_editor(buffer, window, cx)
1566 });
1567
1568 assert_eq!('🟥'.len_utf8(), 4);
1569 assert_eq!('α'.len_utf8(), 2);
1570
1571 _ = editor.update(cx, |editor, window, cx| {
1572 editor.fold_creases(
1573 vec![
1574 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1575 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1576 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1577 ],
1578 true,
1579 window,
1580 cx,
1581 );
1582 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1583
1584 editor.move_right(&MoveRight, window, cx);
1585 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1586 editor.move_right(&MoveRight, window, cx);
1587 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1588 editor.move_right(&MoveRight, window, cx);
1589 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1590
1591 editor.move_down(&MoveDown, window, cx);
1592 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1593 editor.move_left(&MoveLeft, window, cx);
1594 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1595 editor.move_left(&MoveLeft, window, cx);
1596 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1597 editor.move_left(&MoveLeft, window, cx);
1598 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1599
1600 editor.move_down(&MoveDown, window, cx);
1601 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1602 editor.move_right(&MoveRight, window, cx);
1603 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1604 editor.move_right(&MoveRight, window, cx);
1605 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1606 editor.move_right(&MoveRight, window, cx);
1607 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1608
1609 editor.move_up(&MoveUp, window, cx);
1610 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1611 editor.move_down(&MoveDown, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1613 editor.move_up(&MoveUp, window, cx);
1614 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1615
1616 editor.move_up(&MoveUp, window, cx);
1617 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1618 editor.move_left(&MoveLeft, window, cx);
1619 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1620 editor.move_left(&MoveLeft, window, cx);
1621 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1622 });
1623}
1624
1625#[gpui::test]
1626fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1627 init_test(cx, |_| {});
1628
1629 let editor = cx.add_window(|window, cx| {
1630 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1631 build_editor(buffer, window, cx)
1632 });
1633 _ = editor.update(cx, |editor, window, cx| {
1634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1635 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1636 });
1637
1638 // moving above start of document should move selection to start of document,
1639 // but the next move down should still be at the original goal_x
1640 editor.move_up(&MoveUp, window, cx);
1641 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1642
1643 editor.move_down(&MoveDown, window, cx);
1644 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1645
1646 editor.move_down(&MoveDown, window, cx);
1647 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1648
1649 editor.move_down(&MoveDown, window, cx);
1650 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1651
1652 editor.move_down(&MoveDown, window, cx);
1653 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1654
1655 // moving past end of document should not change goal_x
1656 editor.move_down(&MoveDown, window, cx);
1657 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1658
1659 editor.move_down(&MoveDown, window, cx);
1660 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1661
1662 editor.move_up(&MoveUp, window, cx);
1663 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1664
1665 editor.move_up(&MoveUp, window, cx);
1666 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1667
1668 editor.move_up(&MoveUp, window, cx);
1669 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1670 });
1671}
1672
1673#[gpui::test]
1674fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1675 init_test(cx, |_| {});
1676 let move_to_beg = MoveToBeginningOfLine {
1677 stop_at_soft_wraps: true,
1678 stop_at_indent: true,
1679 };
1680
1681 let delete_to_beg = DeleteToBeginningOfLine {
1682 stop_at_indent: false,
1683 };
1684
1685 let move_to_end = MoveToEndOfLine {
1686 stop_at_soft_wraps: true,
1687 };
1688
1689 let editor = cx.add_window(|window, cx| {
1690 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1691 build_editor(buffer, window, cx)
1692 });
1693 _ = editor.update(cx, |editor, window, cx| {
1694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1695 s.select_display_ranges([
1696 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1697 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1698 ]);
1699 });
1700 });
1701
1702 _ = editor.update(cx, |editor, window, cx| {
1703 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1704 assert_eq!(
1705 display_ranges(editor, cx),
1706 &[
1707 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1708 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1709 ]
1710 );
1711 });
1712
1713 _ = editor.update(cx, |editor, window, cx| {
1714 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1715 assert_eq!(
1716 display_ranges(editor, cx),
1717 &[
1718 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1719 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1720 ]
1721 );
1722 });
1723
1724 _ = editor.update(cx, |editor, window, cx| {
1725 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1726 assert_eq!(
1727 display_ranges(editor, cx),
1728 &[
1729 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1730 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1731 ]
1732 );
1733 });
1734
1735 _ = editor.update(cx, |editor, window, cx| {
1736 editor.move_to_end_of_line(&move_to_end, window, cx);
1737 assert_eq!(
1738 display_ranges(editor, cx),
1739 &[
1740 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1741 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1742 ]
1743 );
1744 });
1745
1746 // Moving to the end of line again is a no-op.
1747 _ = editor.update(cx, |editor, window, cx| {
1748 editor.move_to_end_of_line(&move_to_end, window, cx);
1749 assert_eq!(
1750 display_ranges(editor, cx),
1751 &[
1752 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1753 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1754 ]
1755 );
1756 });
1757
1758 _ = editor.update(cx, |editor, window, cx| {
1759 editor.move_left(&MoveLeft, window, cx);
1760 editor.select_to_beginning_of_line(
1761 &SelectToBeginningOfLine {
1762 stop_at_soft_wraps: true,
1763 stop_at_indent: true,
1764 },
1765 window,
1766 cx,
1767 );
1768 assert_eq!(
1769 display_ranges(editor, cx),
1770 &[
1771 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1772 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1773 ]
1774 );
1775 });
1776
1777 _ = editor.update(cx, |editor, window, cx| {
1778 editor.select_to_beginning_of_line(
1779 &SelectToBeginningOfLine {
1780 stop_at_soft_wraps: true,
1781 stop_at_indent: true,
1782 },
1783 window,
1784 cx,
1785 );
1786 assert_eq!(
1787 display_ranges(editor, cx),
1788 &[
1789 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1790 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1791 ]
1792 );
1793 });
1794
1795 _ = editor.update(cx, |editor, window, cx| {
1796 editor.select_to_beginning_of_line(
1797 &SelectToBeginningOfLine {
1798 stop_at_soft_wraps: true,
1799 stop_at_indent: true,
1800 },
1801 window,
1802 cx,
1803 );
1804 assert_eq!(
1805 display_ranges(editor, cx),
1806 &[
1807 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1808 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1809 ]
1810 );
1811 });
1812
1813 _ = editor.update(cx, |editor, window, cx| {
1814 editor.select_to_end_of_line(
1815 &SelectToEndOfLine {
1816 stop_at_soft_wraps: true,
1817 },
1818 window,
1819 cx,
1820 );
1821 assert_eq!(
1822 display_ranges(editor, cx),
1823 &[
1824 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1825 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1826 ]
1827 );
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1832 assert_eq!(editor.display_text(cx), "ab\n de");
1833 assert_eq!(
1834 display_ranges(editor, cx),
1835 &[
1836 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1837 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1838 ]
1839 );
1840 });
1841
1842 _ = editor.update(cx, |editor, window, cx| {
1843 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1844 assert_eq!(editor.display_text(cx), "\n");
1845 assert_eq!(
1846 display_ranges(editor, cx),
1847 &[
1848 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1849 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1850 ]
1851 );
1852 });
1853}
1854
1855#[gpui::test]
1856fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1857 init_test(cx, |_| {});
1858 let move_to_beg = MoveToBeginningOfLine {
1859 stop_at_soft_wraps: false,
1860 stop_at_indent: false,
1861 };
1862
1863 let move_to_end = MoveToEndOfLine {
1864 stop_at_soft_wraps: false,
1865 };
1866
1867 let editor = cx.add_window(|window, cx| {
1868 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1869 build_editor(buffer, window, cx)
1870 });
1871
1872 _ = editor.update(cx, |editor, window, cx| {
1873 editor.set_wrap_width(Some(140.0.into()), cx);
1874
1875 // We expect the following lines after wrapping
1876 // ```
1877 // thequickbrownfox
1878 // jumpedoverthelazydo
1879 // gs
1880 // ```
1881 // The final `gs` was soft-wrapped onto a new line.
1882 assert_eq!(
1883 "thequickbrownfox\njumpedoverthelaz\nydogs",
1884 editor.display_text(cx),
1885 );
1886
1887 // First, let's assert behavior on the first line, that was not soft-wrapped.
1888 // Start the cursor at the `k` on the first line
1889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1890 s.select_display_ranges([
1891 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1892 ]);
1893 });
1894
1895 // Moving to the beginning of the line should put us at the beginning of the line.
1896 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1897 assert_eq!(
1898 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1899 display_ranges(editor, cx)
1900 );
1901
1902 // Moving to the end of the line should put us at the end of the line.
1903 editor.move_to_end_of_line(&move_to_end, window, cx);
1904 assert_eq!(
1905 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1906 display_ranges(editor, cx)
1907 );
1908
1909 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1910 // Start the cursor at the last line (`y` that was wrapped to a new line)
1911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1912 s.select_display_ranges([
1913 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1914 ]);
1915 });
1916
1917 // Moving to the beginning of the line should put us at the start of the second line of
1918 // display text, i.e., the `j`.
1919 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1920 assert_eq!(
1921 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1922 display_ranges(editor, cx)
1923 );
1924
1925 // Moving to the beginning of the line again should be a no-op.
1926 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1927 assert_eq!(
1928 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1929 display_ranges(editor, cx)
1930 );
1931
1932 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1933 // next display line.
1934 editor.move_to_end_of_line(&move_to_end, window, cx);
1935 assert_eq!(
1936 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1937 display_ranges(editor, cx)
1938 );
1939
1940 // Moving to the end of the line again should be a no-op.
1941 editor.move_to_end_of_line(&move_to_end, window, cx);
1942 assert_eq!(
1943 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1944 display_ranges(editor, cx)
1945 );
1946 });
1947}
1948
1949#[gpui::test]
1950fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1951 init_test(cx, |_| {});
1952
1953 let move_to_beg = MoveToBeginningOfLine {
1954 stop_at_soft_wraps: true,
1955 stop_at_indent: true,
1956 };
1957
1958 let select_to_beg = SelectToBeginningOfLine {
1959 stop_at_soft_wraps: true,
1960 stop_at_indent: true,
1961 };
1962
1963 let delete_to_beg = DeleteToBeginningOfLine {
1964 stop_at_indent: true,
1965 };
1966
1967 let move_to_end = MoveToEndOfLine {
1968 stop_at_soft_wraps: false,
1969 };
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1978 s.select_display_ranges([
1979 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1980 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1981 ]);
1982 });
1983
1984 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1985 // and the second cursor at the first non-whitespace character in the line.
1986 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1987 assert_eq!(
1988 display_ranges(editor, cx),
1989 &[
1990 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1991 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1992 ]
1993 );
1994
1995 // Moving to the beginning of the line again should be a no-op for the first cursor,
1996 // and should move the second cursor to the beginning of the line.
1997 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1998 assert_eq!(
1999 display_ranges(editor, cx),
2000 &[
2001 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2002 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2003 ]
2004 );
2005
2006 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2007 // and should move the second cursor back to the first non-whitespace character in the line.
2008 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2009 assert_eq!(
2010 display_ranges(editor, cx),
2011 &[
2012 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2013 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2014 ]
2015 );
2016
2017 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2018 // and to the first non-whitespace character in the line for the second cursor.
2019 editor.move_to_end_of_line(&move_to_end, window, cx);
2020 editor.move_left(&MoveLeft, window, cx);
2021 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2022 assert_eq!(
2023 display_ranges(editor, cx),
2024 &[
2025 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2026 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2027 ]
2028 );
2029
2030 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2031 // and should select to the beginning of the line for the second cursor.
2032 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2033 assert_eq!(
2034 display_ranges(editor, cx),
2035 &[
2036 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2037 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2038 ]
2039 );
2040
2041 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2042 // and should delete to the first non-whitespace character in the line for the second cursor.
2043 editor.move_to_end_of_line(&move_to_end, window, cx);
2044 editor.move_left(&MoveLeft, window, cx);
2045 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2046 assert_eq!(editor.text(cx), "c\n f");
2047 });
2048}
2049
2050#[gpui::test]
2051fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2052 init_test(cx, |_| {});
2053
2054 let move_to_beg = MoveToBeginningOfLine {
2055 stop_at_soft_wraps: true,
2056 stop_at_indent: true,
2057 };
2058
2059 let editor = cx.add_window(|window, cx| {
2060 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2061 build_editor(buffer, window, cx)
2062 });
2063
2064 _ = editor.update(cx, |editor, window, cx| {
2065 // test cursor between line_start and indent_start
2066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2067 s.select_display_ranges([
2068 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2069 ]);
2070 });
2071
2072 // cursor should move to line_start
2073 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2074 assert_eq!(
2075 display_ranges(editor, cx),
2076 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2077 );
2078
2079 // cursor should move to indent_start
2080 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2081 assert_eq!(
2082 display_ranges(editor, cx),
2083 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2084 );
2085
2086 // cursor should move to back to line_start
2087 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2088 assert_eq!(
2089 display_ranges(editor, cx),
2090 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2091 );
2092 });
2093}
2094
2095#[gpui::test]
2096fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2097 init_test(cx, |_| {});
2098
2099 let editor = cx.add_window(|window, cx| {
2100 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2101 build_editor(buffer, window, cx)
2102 });
2103 _ = editor.update(cx, |editor, window, cx| {
2104 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2105 s.select_display_ranges([
2106 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2107 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2108 ])
2109 });
2110 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2111 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2112
2113 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2114 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2115
2116 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2117 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2118
2119 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2120 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2121
2122 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2123 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2124
2125 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2126 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2127
2128 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2129 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2130
2131 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2132 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2133
2134 editor.move_right(&MoveRight, window, cx);
2135 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2136 assert_selection_ranges(
2137 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2138 editor,
2139 cx,
2140 );
2141
2142 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2143 assert_selection_ranges(
2144 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2145 editor,
2146 cx,
2147 );
2148
2149 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2150 assert_selection_ranges(
2151 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2152 editor,
2153 cx,
2154 );
2155 });
2156}
2157
2158#[gpui::test]
2159fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2160 init_test(cx, |_| {});
2161
2162 let editor = cx.add_window(|window, cx| {
2163 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2164 build_editor(buffer, window, cx)
2165 });
2166
2167 _ = editor.update(cx, |editor, window, cx| {
2168 editor.set_wrap_width(Some(140.0.into()), cx);
2169 assert_eq!(
2170 editor.display_text(cx),
2171 "use one::{\n two::three::\n four::five\n};"
2172 );
2173
2174 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2175 s.select_display_ranges([
2176 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2177 ]);
2178 });
2179
2180 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2181 assert_eq!(
2182 display_ranges(editor, cx),
2183 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2184 );
2185
2186 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2187 assert_eq!(
2188 display_ranges(editor, cx),
2189 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2190 );
2191
2192 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2193 assert_eq!(
2194 display_ranges(editor, cx),
2195 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2196 );
2197
2198 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2199 assert_eq!(
2200 display_ranges(editor, cx),
2201 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2202 );
2203
2204 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2205 assert_eq!(
2206 display_ranges(editor, cx),
2207 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2208 );
2209
2210 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2211 assert_eq!(
2212 display_ranges(editor, cx),
2213 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2214 );
2215 });
2216}
2217
2218#[gpui::test]
2219async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2220 init_test(cx, |_| {});
2221 let mut cx = EditorTestContext::new(cx).await;
2222
2223 let line_height = cx.update_editor(|editor, window, cx| {
2224 editor
2225 .style(cx)
2226 .text
2227 .line_height_in_pixels(window.rem_size())
2228 });
2229 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2230
2231 cx.set_state(
2232 &r#"ˇone
2233 two
2234
2235 three
2236 fourˇ
2237 five
2238
2239 six"#
2240 .unindent(),
2241 );
2242
2243 cx.update_editor(|editor, window, cx| {
2244 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2245 });
2246 cx.assert_editor_state(
2247 &r#"one
2248 two
2249 ˇ
2250 three
2251 four
2252 five
2253 ˇ
2254 six"#
2255 .unindent(),
2256 );
2257
2258 cx.update_editor(|editor, window, cx| {
2259 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2260 });
2261 cx.assert_editor_state(
2262 &r#"one
2263 two
2264
2265 three
2266 four
2267 five
2268 ˇ
2269 sixˇ"#
2270 .unindent(),
2271 );
2272
2273 cx.update_editor(|editor, window, cx| {
2274 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2275 });
2276 cx.assert_editor_state(
2277 &r#"one
2278 two
2279
2280 three
2281 four
2282 five
2283
2284 sixˇ"#
2285 .unindent(),
2286 );
2287
2288 cx.update_editor(|editor, window, cx| {
2289 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2290 });
2291 cx.assert_editor_state(
2292 &r#"one
2293 two
2294
2295 three
2296 four
2297 five
2298 ˇ
2299 six"#
2300 .unindent(),
2301 );
2302
2303 cx.update_editor(|editor, window, cx| {
2304 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2305 });
2306 cx.assert_editor_state(
2307 &r#"one
2308 two
2309 ˇ
2310 three
2311 four
2312 five
2313
2314 six"#
2315 .unindent(),
2316 );
2317
2318 cx.update_editor(|editor, window, cx| {
2319 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2320 });
2321 cx.assert_editor_state(
2322 &r#"ˇone
2323 two
2324
2325 three
2326 four
2327 five
2328
2329 six"#
2330 .unindent(),
2331 );
2332}
2333
2334#[gpui::test]
2335async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2336 init_test(cx, |_| {});
2337 let mut cx = EditorTestContext::new(cx).await;
2338 let line_height = cx.update_editor(|editor, window, cx| {
2339 editor
2340 .style(cx)
2341 .text
2342 .line_height_in_pixels(window.rem_size())
2343 });
2344 let window = cx.window;
2345 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2346
2347 cx.set_state(
2348 r#"ˇone
2349 two
2350 three
2351 four
2352 five
2353 six
2354 seven
2355 eight
2356 nine
2357 ten
2358 "#,
2359 );
2360
2361 cx.update_editor(|editor, window, cx| {
2362 assert_eq!(
2363 editor.snapshot(window, cx).scroll_position(),
2364 gpui::Point::new(0., 0.)
2365 );
2366 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2367 assert_eq!(
2368 editor.snapshot(window, cx).scroll_position(),
2369 gpui::Point::new(0., 3.)
2370 );
2371 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2372 assert_eq!(
2373 editor.snapshot(window, cx).scroll_position(),
2374 gpui::Point::new(0., 6.)
2375 );
2376 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2377 assert_eq!(
2378 editor.snapshot(window, cx).scroll_position(),
2379 gpui::Point::new(0., 3.)
2380 );
2381
2382 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2383 assert_eq!(
2384 editor.snapshot(window, cx).scroll_position(),
2385 gpui::Point::new(0., 1.)
2386 );
2387 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2388 assert_eq!(
2389 editor.snapshot(window, cx).scroll_position(),
2390 gpui::Point::new(0., 3.)
2391 );
2392 });
2393}
2394
2395#[gpui::test]
2396async fn test_autoscroll(cx: &mut TestAppContext) {
2397 init_test(cx, |_| {});
2398 let mut cx = EditorTestContext::new(cx).await;
2399
2400 let line_height = cx.update_editor(|editor, window, cx| {
2401 editor.set_vertical_scroll_margin(2, cx);
2402 editor
2403 .style(cx)
2404 .text
2405 .line_height_in_pixels(window.rem_size())
2406 });
2407 let window = cx.window;
2408 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2409
2410 cx.set_state(
2411 r#"ˇone
2412 two
2413 three
2414 four
2415 five
2416 six
2417 seven
2418 eight
2419 nine
2420 ten
2421 "#,
2422 );
2423 cx.update_editor(|editor, window, cx| {
2424 assert_eq!(
2425 editor.snapshot(window, cx).scroll_position(),
2426 gpui::Point::new(0., 0.0)
2427 );
2428 });
2429
2430 // Add a cursor below the visible area. Since both cursors cannot fit
2431 // on screen, the editor autoscrolls to reveal the newest cursor, and
2432 // allows the vertical scroll margin below that cursor.
2433 cx.update_editor(|editor, window, cx| {
2434 editor.change_selections(Default::default(), window, cx, |selections| {
2435 selections.select_ranges([
2436 Point::new(0, 0)..Point::new(0, 0),
2437 Point::new(6, 0)..Point::new(6, 0),
2438 ]);
2439 })
2440 });
2441 cx.update_editor(|editor, window, cx| {
2442 assert_eq!(
2443 editor.snapshot(window, cx).scroll_position(),
2444 gpui::Point::new(0., 3.0)
2445 );
2446 });
2447
2448 // Move down. The editor cursor scrolls down to track the newest cursor.
2449 cx.update_editor(|editor, window, cx| {
2450 editor.move_down(&Default::default(), window, cx);
2451 });
2452 cx.update_editor(|editor, window, cx| {
2453 assert_eq!(
2454 editor.snapshot(window, cx).scroll_position(),
2455 gpui::Point::new(0., 4.0)
2456 );
2457 });
2458
2459 // Add a cursor above the visible area. Since both cursors fit on screen,
2460 // the editor scrolls to show both.
2461 cx.update_editor(|editor, window, cx| {
2462 editor.change_selections(Default::default(), window, cx, |selections| {
2463 selections.select_ranges([
2464 Point::new(1, 0)..Point::new(1, 0),
2465 Point::new(6, 0)..Point::new(6, 0),
2466 ]);
2467 })
2468 });
2469 cx.update_editor(|editor, window, cx| {
2470 assert_eq!(
2471 editor.snapshot(window, cx).scroll_position(),
2472 gpui::Point::new(0., 1.0)
2473 );
2474 });
2475}
2476
2477#[gpui::test]
2478async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2479 init_test(cx, |_| {});
2480 let mut cx = EditorTestContext::new(cx).await;
2481
2482 let line_height = cx.update_editor(|editor, window, cx| {
2483 editor
2484 .style(cx)
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]
5779async fn test_rotate_selections(cx: &mut TestAppContext) {
5780 init_test(cx, |_| {});
5781
5782 let mut cx = EditorTestContext::new(cx).await;
5783
5784 // Rotate text selections (horizontal)
5785 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5786 cx.update_editor(|e, window, cx| {
5787 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5788 });
5789 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
5790 cx.update_editor(|e, window, cx| {
5791 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5792 });
5793 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5794
5795 // Rotate text selections (vertical)
5796 cx.set_state(indoc! {"
5797 x=«1ˇ»
5798 y=«2ˇ»
5799 z=«3ˇ»
5800 "});
5801 cx.update_editor(|e, window, cx| {
5802 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5803 });
5804 cx.assert_editor_state(indoc! {"
5805 x=«3ˇ»
5806 y=«1ˇ»
5807 z=«2ˇ»
5808 "});
5809 cx.update_editor(|e, window, cx| {
5810 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5811 });
5812 cx.assert_editor_state(indoc! {"
5813 x=«1ˇ»
5814 y=«2ˇ»
5815 z=«3ˇ»
5816 "});
5817
5818 // Rotate text selections (vertical, different lengths)
5819 cx.set_state(indoc! {"
5820 x=\"«ˇ»\"
5821 y=\"«aˇ»\"
5822 z=\"«aaˇ»\"
5823 "});
5824 cx.update_editor(|e, window, cx| {
5825 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5826 });
5827 cx.assert_editor_state(indoc! {"
5828 x=\"«aaˇ»\"
5829 y=\"«ˇ»\"
5830 z=\"«aˇ»\"
5831 "});
5832 cx.update_editor(|e, window, cx| {
5833 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5834 });
5835 cx.assert_editor_state(indoc! {"
5836 x=\"«ˇ»\"
5837 y=\"«aˇ»\"
5838 z=\"«aaˇ»\"
5839 "});
5840
5841 // Rotate whole lines (cursor positions preserved)
5842 cx.set_state(indoc! {"
5843 ˇline123
5844 liˇne23
5845 line3ˇ
5846 "});
5847 cx.update_editor(|e, window, cx| {
5848 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5849 });
5850 cx.assert_editor_state(indoc! {"
5851 line3ˇ
5852 ˇline123
5853 liˇne23
5854 "});
5855 cx.update_editor(|e, window, cx| {
5856 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5857 });
5858 cx.assert_editor_state(indoc! {"
5859 ˇline123
5860 liˇne23
5861 line3ˇ
5862 "});
5863
5864 // Rotate whole lines, multiple cursors per line (positions preserved)
5865 cx.set_state(indoc! {"
5866 ˇliˇne123
5867 ˇline23
5868 ˇline3
5869 "});
5870 cx.update_editor(|e, window, cx| {
5871 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5872 });
5873 cx.assert_editor_state(indoc! {"
5874 ˇline3
5875 ˇliˇne123
5876 ˇline23
5877 "});
5878 cx.update_editor(|e, window, cx| {
5879 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5880 });
5881 cx.assert_editor_state(indoc! {"
5882 ˇliˇne123
5883 ˇline23
5884 ˇline3
5885 "});
5886}
5887
5888#[gpui::test]
5889fn test_move_line_up_down(cx: &mut TestAppContext) {
5890 init_test(cx, |_| {});
5891
5892 let editor = cx.add_window(|window, cx| {
5893 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5894 build_editor(buffer, window, cx)
5895 });
5896 _ = editor.update(cx, |editor, window, cx| {
5897 editor.fold_creases(
5898 vec![
5899 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5900 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5901 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5902 ],
5903 true,
5904 window,
5905 cx,
5906 );
5907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5908 s.select_display_ranges([
5909 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5910 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5911 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5912 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5913 ])
5914 });
5915 assert_eq!(
5916 editor.display_text(cx),
5917 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5918 );
5919
5920 editor.move_line_up(&MoveLineUp, window, cx);
5921 assert_eq!(
5922 editor.display_text(cx),
5923 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5924 );
5925 assert_eq!(
5926 display_ranges(editor, cx),
5927 vec![
5928 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5929 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5930 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5931 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5932 ]
5933 );
5934 });
5935
5936 _ = editor.update(cx, |editor, window, cx| {
5937 editor.move_line_down(&MoveLineDown, window, cx);
5938 assert_eq!(
5939 editor.display_text(cx),
5940 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5941 );
5942 assert_eq!(
5943 display_ranges(editor, cx),
5944 vec![
5945 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5946 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5947 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5948 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5949 ]
5950 );
5951 });
5952
5953 _ = editor.update(cx, |editor, window, cx| {
5954 editor.move_line_down(&MoveLineDown, window, cx);
5955 assert_eq!(
5956 editor.display_text(cx),
5957 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5958 );
5959 assert_eq!(
5960 display_ranges(editor, cx),
5961 vec![
5962 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5963 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5964 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5965 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5966 ]
5967 );
5968 });
5969
5970 _ = editor.update(cx, |editor, window, cx| {
5971 editor.move_line_up(&MoveLineUp, window, cx);
5972 assert_eq!(
5973 editor.display_text(cx),
5974 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5975 );
5976 assert_eq!(
5977 display_ranges(editor, cx),
5978 vec![
5979 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5980 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5981 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5982 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5983 ]
5984 );
5985 });
5986}
5987
5988#[gpui::test]
5989fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5990 init_test(cx, |_| {});
5991 let editor = cx.add_window(|window, cx| {
5992 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5993 build_editor(buffer, window, cx)
5994 });
5995 _ = editor.update(cx, |editor, window, cx| {
5996 editor.fold_creases(
5997 vec![Crease::simple(
5998 Point::new(6, 4)..Point::new(7, 4),
5999 FoldPlaceholder::test(),
6000 )],
6001 true,
6002 window,
6003 cx,
6004 );
6005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6006 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6007 });
6008 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6009 editor.move_line_up(&MoveLineUp, window, cx);
6010 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6011 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6012 });
6013}
6014
6015#[gpui::test]
6016fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6017 init_test(cx, |_| {});
6018
6019 let editor = cx.add_window(|window, cx| {
6020 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6021 build_editor(buffer, window, cx)
6022 });
6023 _ = editor.update(cx, |editor, window, cx| {
6024 let snapshot = editor.buffer.read(cx).snapshot(cx);
6025 editor.insert_blocks(
6026 [BlockProperties {
6027 style: BlockStyle::Fixed,
6028 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6029 height: Some(1),
6030 render: Arc::new(|_| div().into_any()),
6031 priority: 0,
6032 }],
6033 Some(Autoscroll::fit()),
6034 cx,
6035 );
6036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6037 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6038 });
6039 editor.move_line_down(&MoveLineDown, window, cx);
6040 });
6041}
6042
6043#[gpui::test]
6044async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6045 init_test(cx, |_| {});
6046
6047 let mut cx = EditorTestContext::new(cx).await;
6048 cx.set_state(
6049 &"
6050 ˇzero
6051 one
6052 two
6053 three
6054 four
6055 five
6056 "
6057 .unindent(),
6058 );
6059
6060 // Create a four-line block that replaces three lines of text.
6061 cx.update_editor(|editor, window, cx| {
6062 let snapshot = editor.snapshot(window, cx);
6063 let snapshot = &snapshot.buffer_snapshot();
6064 let placement = BlockPlacement::Replace(
6065 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6066 );
6067 editor.insert_blocks(
6068 [BlockProperties {
6069 placement,
6070 height: Some(4),
6071 style: BlockStyle::Sticky,
6072 render: Arc::new(|_| gpui::div().into_any_element()),
6073 priority: 0,
6074 }],
6075 None,
6076 cx,
6077 );
6078 });
6079
6080 // Move down so that the cursor touches the block.
6081 cx.update_editor(|editor, window, cx| {
6082 editor.move_down(&Default::default(), window, cx);
6083 });
6084 cx.assert_editor_state(
6085 &"
6086 zero
6087 «one
6088 two
6089 threeˇ»
6090 four
6091 five
6092 "
6093 .unindent(),
6094 );
6095
6096 // Move down past the block.
6097 cx.update_editor(|editor, window, cx| {
6098 editor.move_down(&Default::default(), window, cx);
6099 });
6100 cx.assert_editor_state(
6101 &"
6102 zero
6103 one
6104 two
6105 three
6106 ˇfour
6107 five
6108 "
6109 .unindent(),
6110 );
6111}
6112
6113#[gpui::test]
6114fn test_transpose(cx: &mut TestAppContext) {
6115 init_test(cx, |_| {});
6116
6117 _ = cx.add_window(|window, cx| {
6118 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6119 editor.set_style(EditorStyle::default(), window, cx);
6120 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6121 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6122 });
6123 editor.transpose(&Default::default(), window, cx);
6124 assert_eq!(editor.text(cx), "bac");
6125 assert_eq!(
6126 editor.selections.ranges(&editor.display_snapshot(cx)),
6127 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6128 );
6129
6130 editor.transpose(&Default::default(), window, cx);
6131 assert_eq!(editor.text(cx), "bca");
6132 assert_eq!(
6133 editor.selections.ranges(&editor.display_snapshot(cx)),
6134 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6135 );
6136
6137 editor.transpose(&Default::default(), window, cx);
6138 assert_eq!(editor.text(cx), "bac");
6139 assert_eq!(
6140 editor.selections.ranges(&editor.display_snapshot(cx)),
6141 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6142 );
6143
6144 editor
6145 });
6146
6147 _ = cx.add_window(|window, cx| {
6148 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6149 editor.set_style(EditorStyle::default(), window, cx);
6150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6151 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6152 });
6153 editor.transpose(&Default::default(), window, cx);
6154 assert_eq!(editor.text(cx), "acb\nde");
6155 assert_eq!(
6156 editor.selections.ranges(&editor.display_snapshot(cx)),
6157 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6158 );
6159
6160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6161 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6162 });
6163 editor.transpose(&Default::default(), window, cx);
6164 assert_eq!(editor.text(cx), "acbd\ne");
6165 assert_eq!(
6166 editor.selections.ranges(&editor.display_snapshot(cx)),
6167 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6168 );
6169
6170 editor.transpose(&Default::default(), window, cx);
6171 assert_eq!(editor.text(cx), "acbde\n");
6172 assert_eq!(
6173 editor.selections.ranges(&editor.display_snapshot(cx)),
6174 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6175 );
6176
6177 editor.transpose(&Default::default(), window, cx);
6178 assert_eq!(editor.text(cx), "acbd\ne");
6179 assert_eq!(
6180 editor.selections.ranges(&editor.display_snapshot(cx)),
6181 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6182 );
6183
6184 editor
6185 });
6186
6187 _ = cx.add_window(|window, cx| {
6188 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6189 editor.set_style(EditorStyle::default(), window, cx);
6190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6191 s.select_ranges([
6192 MultiBufferOffset(1)..MultiBufferOffset(1),
6193 MultiBufferOffset(2)..MultiBufferOffset(2),
6194 MultiBufferOffset(4)..MultiBufferOffset(4),
6195 ])
6196 });
6197 editor.transpose(&Default::default(), window, cx);
6198 assert_eq!(editor.text(cx), "bacd\ne");
6199 assert_eq!(
6200 editor.selections.ranges(&editor.display_snapshot(cx)),
6201 [
6202 MultiBufferOffset(2)..MultiBufferOffset(2),
6203 MultiBufferOffset(3)..MultiBufferOffset(3),
6204 MultiBufferOffset(5)..MultiBufferOffset(5)
6205 ]
6206 );
6207
6208 editor.transpose(&Default::default(), window, cx);
6209 assert_eq!(editor.text(cx), "bcade\n");
6210 assert_eq!(
6211 editor.selections.ranges(&editor.display_snapshot(cx)),
6212 [
6213 MultiBufferOffset(3)..MultiBufferOffset(3),
6214 MultiBufferOffset(4)..MultiBufferOffset(4),
6215 MultiBufferOffset(6)..MultiBufferOffset(6)
6216 ]
6217 );
6218
6219 editor.transpose(&Default::default(), window, cx);
6220 assert_eq!(editor.text(cx), "bcda\ne");
6221 assert_eq!(
6222 editor.selections.ranges(&editor.display_snapshot(cx)),
6223 [
6224 MultiBufferOffset(4)..MultiBufferOffset(4),
6225 MultiBufferOffset(6)..MultiBufferOffset(6)
6226 ]
6227 );
6228
6229 editor.transpose(&Default::default(), window, cx);
6230 assert_eq!(editor.text(cx), "bcade\n");
6231 assert_eq!(
6232 editor.selections.ranges(&editor.display_snapshot(cx)),
6233 [
6234 MultiBufferOffset(4)..MultiBufferOffset(4),
6235 MultiBufferOffset(6)..MultiBufferOffset(6)
6236 ]
6237 );
6238
6239 editor.transpose(&Default::default(), window, cx);
6240 assert_eq!(editor.text(cx), "bcaed\n");
6241 assert_eq!(
6242 editor.selections.ranges(&editor.display_snapshot(cx)),
6243 [
6244 MultiBufferOffset(5)..MultiBufferOffset(5),
6245 MultiBufferOffset(6)..MultiBufferOffset(6)
6246 ]
6247 );
6248
6249 editor
6250 });
6251
6252 _ = cx.add_window(|window, cx| {
6253 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6254 editor.set_style(EditorStyle::default(), window, cx);
6255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6256 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6257 });
6258 editor.transpose(&Default::default(), window, cx);
6259 assert_eq!(editor.text(cx), "🏀🍐✋");
6260 assert_eq!(
6261 editor.selections.ranges(&editor.display_snapshot(cx)),
6262 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6263 );
6264
6265 editor.transpose(&Default::default(), window, cx);
6266 assert_eq!(editor.text(cx), "🏀✋🍐");
6267 assert_eq!(
6268 editor.selections.ranges(&editor.display_snapshot(cx)),
6269 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6270 );
6271
6272 editor.transpose(&Default::default(), window, cx);
6273 assert_eq!(editor.text(cx), "🏀🍐✋");
6274 assert_eq!(
6275 editor.selections.ranges(&editor.display_snapshot(cx)),
6276 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6277 );
6278
6279 editor
6280 });
6281}
6282
6283#[gpui::test]
6284async fn test_rewrap(cx: &mut TestAppContext) {
6285 init_test(cx, |settings| {
6286 settings.languages.0.extend([
6287 (
6288 "Markdown".into(),
6289 LanguageSettingsContent {
6290 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6291 preferred_line_length: Some(40),
6292 ..Default::default()
6293 },
6294 ),
6295 (
6296 "Plain Text".into(),
6297 LanguageSettingsContent {
6298 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6299 preferred_line_length: Some(40),
6300 ..Default::default()
6301 },
6302 ),
6303 (
6304 "C++".into(),
6305 LanguageSettingsContent {
6306 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6307 preferred_line_length: Some(40),
6308 ..Default::default()
6309 },
6310 ),
6311 (
6312 "Python".into(),
6313 LanguageSettingsContent {
6314 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6315 preferred_line_length: Some(40),
6316 ..Default::default()
6317 },
6318 ),
6319 (
6320 "Rust".into(),
6321 LanguageSettingsContent {
6322 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6323 preferred_line_length: Some(40),
6324 ..Default::default()
6325 },
6326 ),
6327 ])
6328 });
6329
6330 let mut cx = EditorTestContext::new(cx).await;
6331
6332 let cpp_language = Arc::new(Language::new(
6333 LanguageConfig {
6334 name: "C++".into(),
6335 line_comments: vec!["// ".into()],
6336 ..LanguageConfig::default()
6337 },
6338 None,
6339 ));
6340 let python_language = Arc::new(Language::new(
6341 LanguageConfig {
6342 name: "Python".into(),
6343 line_comments: vec!["# ".into()],
6344 ..LanguageConfig::default()
6345 },
6346 None,
6347 ));
6348 let markdown_language = Arc::new(Language::new(
6349 LanguageConfig {
6350 name: "Markdown".into(),
6351 rewrap_prefixes: vec![
6352 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6353 regex::Regex::new("[-*+]\\s+").unwrap(),
6354 ],
6355 ..LanguageConfig::default()
6356 },
6357 None,
6358 ));
6359 let rust_language = Arc::new(
6360 Language::new(
6361 LanguageConfig {
6362 name: "Rust".into(),
6363 line_comments: vec!["// ".into(), "/// ".into()],
6364 ..LanguageConfig::default()
6365 },
6366 Some(tree_sitter_rust::LANGUAGE.into()),
6367 )
6368 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6369 .unwrap(),
6370 );
6371
6372 let plaintext_language = Arc::new(Language::new(
6373 LanguageConfig {
6374 name: "Plain Text".into(),
6375 ..LanguageConfig::default()
6376 },
6377 None,
6378 ));
6379
6380 // Test basic rewrapping of a long line with a cursor
6381 assert_rewrap(
6382 indoc! {"
6383 // ˇThis is a long comment that needs to be wrapped.
6384 "},
6385 indoc! {"
6386 // ˇThis is a long comment that needs to
6387 // be wrapped.
6388 "},
6389 cpp_language.clone(),
6390 &mut cx,
6391 );
6392
6393 // Test rewrapping a full selection
6394 assert_rewrap(
6395 indoc! {"
6396 «// This selected long comment needs to be wrapped.ˇ»"
6397 },
6398 indoc! {"
6399 «// This selected long comment needs to
6400 // be wrapped.ˇ»"
6401 },
6402 cpp_language.clone(),
6403 &mut cx,
6404 );
6405
6406 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6407 assert_rewrap(
6408 indoc! {"
6409 // ˇThis is the first line.
6410 // Thisˇ is the second line.
6411 // This is the thirdˇ line, all part of one paragraph.
6412 "},
6413 indoc! {"
6414 // ˇThis is the first line. Thisˇ is the
6415 // second line. This is the thirdˇ line,
6416 // all part of one paragraph.
6417 "},
6418 cpp_language.clone(),
6419 &mut cx,
6420 );
6421
6422 // Test multiple cursors in different paragraphs trigger separate rewraps
6423 assert_rewrap(
6424 indoc! {"
6425 // ˇThis is the first paragraph, first line.
6426 // ˇThis is the first paragraph, second line.
6427
6428 // ˇThis is the second paragraph, first line.
6429 // ˇThis is the second paragraph, second line.
6430 "},
6431 indoc! {"
6432 // ˇThis is the first paragraph, first
6433 // line. ˇThis is the first paragraph,
6434 // second line.
6435
6436 // ˇThis is the second paragraph, first
6437 // line. ˇThis is the second paragraph,
6438 // second line.
6439 "},
6440 cpp_language.clone(),
6441 &mut cx,
6442 );
6443
6444 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6445 assert_rewrap(
6446 indoc! {"
6447 «// A regular long long comment to be wrapped.
6448 /// A documentation long comment to be wrapped.ˇ»
6449 "},
6450 indoc! {"
6451 «// A regular long long comment to be
6452 // wrapped.
6453 /// A documentation long comment to be
6454 /// wrapped.ˇ»
6455 "},
6456 rust_language.clone(),
6457 &mut cx,
6458 );
6459
6460 // Test that change in indentation level trigger seperate rewraps
6461 assert_rewrap(
6462 indoc! {"
6463 fn foo() {
6464 «// This is a long comment at the base indent.
6465 // This is a long comment at the next indent.ˇ»
6466 }
6467 "},
6468 indoc! {"
6469 fn foo() {
6470 «// This is a long comment at the
6471 // base indent.
6472 // This is a long comment at the
6473 // next indent.ˇ»
6474 }
6475 "},
6476 rust_language.clone(),
6477 &mut cx,
6478 );
6479
6480 // Test that different comment prefix characters (e.g., '#') are handled correctly
6481 assert_rewrap(
6482 indoc! {"
6483 # ˇThis is a long comment using a pound sign.
6484 "},
6485 indoc! {"
6486 # ˇThis is a long comment using a pound
6487 # sign.
6488 "},
6489 python_language,
6490 &mut cx,
6491 );
6492
6493 // Test rewrapping only affects comments, not code even when selected
6494 assert_rewrap(
6495 indoc! {"
6496 «/// This doc comment is long and should be wrapped.
6497 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6498 "},
6499 indoc! {"
6500 «/// This doc comment is long and should
6501 /// be wrapped.
6502 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6503 "},
6504 rust_language.clone(),
6505 &mut cx,
6506 );
6507
6508 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6509 assert_rewrap(
6510 indoc! {"
6511 # Header
6512
6513 A long long long line of markdown text to wrap.ˇ
6514 "},
6515 indoc! {"
6516 # Header
6517
6518 A long long long line of markdown text
6519 to wrap.ˇ
6520 "},
6521 markdown_language.clone(),
6522 &mut cx,
6523 );
6524
6525 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6526 assert_rewrap(
6527 indoc! {"
6528 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6529 2. This is a numbered list item that is very long and needs to be wrapped properly.
6530 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6531 "},
6532 indoc! {"
6533 «1. This is a numbered list item that is
6534 very long and needs to be wrapped
6535 properly.
6536 2. This is a numbered list item that is
6537 very long and needs to be wrapped
6538 properly.
6539 - This is an unordered list item that is
6540 also very long and should not merge
6541 with the numbered item.ˇ»
6542 "},
6543 markdown_language.clone(),
6544 &mut cx,
6545 );
6546
6547 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6548 assert_rewrap(
6549 indoc! {"
6550 «1. This is a numbered list item that is
6551 very long and needs to be wrapped
6552 properly.
6553 2. This is a numbered list item that is
6554 very long and needs to be wrapped
6555 properly.
6556 - This is an unordered list item that is
6557 also very long and should not merge with
6558 the numbered item.ˇ»
6559 "},
6560 indoc! {"
6561 «1. This is a numbered list item that is
6562 very long and needs to be wrapped
6563 properly.
6564 2. This is a numbered list item that is
6565 very long and needs to be wrapped
6566 properly.
6567 - This is an unordered list item that is
6568 also very long and should not merge
6569 with the numbered item.ˇ»
6570 "},
6571 markdown_language.clone(),
6572 &mut cx,
6573 );
6574
6575 // Test that rewrapping maintain indents even when they already exists.
6576 assert_rewrap(
6577 indoc! {"
6578 «1. This is a numbered list
6579 item that is very long and needs to be wrapped properly.
6580 2. This is a numbered list
6581 item that is very long and needs to be wrapped properly.
6582 - This is an unordered list item that is also very long and
6583 should not merge with the numbered item.ˇ»
6584 "},
6585 indoc! {"
6586 «1. This is a numbered list item that is
6587 very long and needs to be wrapped
6588 properly.
6589 2. This is a numbered list item that is
6590 very long and needs to be wrapped
6591 properly.
6592 - This is an unordered list item that is
6593 also very long and should not merge
6594 with the numbered item.ˇ»
6595 "},
6596 markdown_language,
6597 &mut cx,
6598 );
6599
6600 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6601 assert_rewrap(
6602 indoc! {"
6603 ˇThis is a very long line of plain text that will be wrapped.
6604 "},
6605 indoc! {"
6606 ˇThis is a very long line of plain text
6607 that will be wrapped.
6608 "},
6609 plaintext_language.clone(),
6610 &mut cx,
6611 );
6612
6613 // Test that non-commented code acts as a paragraph boundary within a selection
6614 assert_rewrap(
6615 indoc! {"
6616 «// This is the first long comment block to be wrapped.
6617 fn my_func(a: u32);
6618 // This is the second long comment block to be wrapped.ˇ»
6619 "},
6620 indoc! {"
6621 «// This is the first long comment block
6622 // to be wrapped.
6623 fn my_func(a: u32);
6624 // This is the second long comment block
6625 // to be wrapped.ˇ»
6626 "},
6627 rust_language,
6628 &mut cx,
6629 );
6630
6631 // Test rewrapping multiple selections, including ones with blank lines or tabs
6632 assert_rewrap(
6633 indoc! {"
6634 «ˇThis is a very long line that will be wrapped.
6635
6636 This is another paragraph in the same selection.»
6637
6638 «\tThis is a very long indented line that will be wrapped.ˇ»
6639 "},
6640 indoc! {"
6641 «ˇThis is a very long line that will be
6642 wrapped.
6643
6644 This is another paragraph in the same
6645 selection.»
6646
6647 «\tThis is a very long indented line
6648 \tthat will be wrapped.ˇ»
6649 "},
6650 plaintext_language,
6651 &mut cx,
6652 );
6653
6654 // Test that an empty comment line acts as a paragraph boundary
6655 assert_rewrap(
6656 indoc! {"
6657 // ˇThis is a long comment that will be wrapped.
6658 //
6659 // And this is another long comment that will also be wrapped.ˇ
6660 "},
6661 indoc! {"
6662 // ˇThis is a long comment that will be
6663 // wrapped.
6664 //
6665 // And this is another long comment that
6666 // will also be wrapped.ˇ
6667 "},
6668 cpp_language,
6669 &mut cx,
6670 );
6671
6672 #[track_caller]
6673 fn assert_rewrap(
6674 unwrapped_text: &str,
6675 wrapped_text: &str,
6676 language: Arc<Language>,
6677 cx: &mut EditorTestContext,
6678 ) {
6679 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6680 cx.set_state(unwrapped_text);
6681 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6682 cx.assert_editor_state(wrapped_text);
6683 }
6684}
6685
6686#[gpui::test]
6687async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6688 init_test(cx, |settings| {
6689 settings.languages.0.extend([(
6690 "Rust".into(),
6691 LanguageSettingsContent {
6692 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6693 preferred_line_length: Some(40),
6694 ..Default::default()
6695 },
6696 )])
6697 });
6698
6699 let mut cx = EditorTestContext::new(cx).await;
6700
6701 let rust_lang = Arc::new(
6702 Language::new(
6703 LanguageConfig {
6704 name: "Rust".into(),
6705 line_comments: vec!["// ".into()],
6706 block_comment: Some(BlockCommentConfig {
6707 start: "/*".into(),
6708 end: "*/".into(),
6709 prefix: "* ".into(),
6710 tab_size: 1,
6711 }),
6712 documentation_comment: Some(BlockCommentConfig {
6713 start: "/**".into(),
6714 end: "*/".into(),
6715 prefix: "* ".into(),
6716 tab_size: 1,
6717 }),
6718
6719 ..LanguageConfig::default()
6720 },
6721 Some(tree_sitter_rust::LANGUAGE.into()),
6722 )
6723 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6724 .unwrap(),
6725 );
6726
6727 // regular block comment
6728 assert_rewrap(
6729 indoc! {"
6730 /*
6731 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6732 */
6733 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6734 "},
6735 indoc! {"
6736 /*
6737 *ˇ Lorem ipsum dolor sit amet,
6738 * consectetur adipiscing elit.
6739 */
6740 /*
6741 *ˇ Lorem ipsum dolor sit amet,
6742 * consectetur adipiscing elit.
6743 */
6744 "},
6745 rust_lang.clone(),
6746 &mut cx,
6747 );
6748
6749 // indent is respected
6750 assert_rewrap(
6751 indoc! {"
6752 {}
6753 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6754 "},
6755 indoc! {"
6756 {}
6757 /*
6758 *ˇ Lorem ipsum dolor sit amet,
6759 * consectetur adipiscing elit.
6760 */
6761 "},
6762 rust_lang.clone(),
6763 &mut cx,
6764 );
6765
6766 // short block comments with inline delimiters
6767 assert_rewrap(
6768 indoc! {"
6769 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6770 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6771 */
6772 /*
6773 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6774 "},
6775 indoc! {"
6776 /*
6777 *ˇ Lorem ipsum dolor sit amet,
6778 * consectetur adipiscing elit.
6779 */
6780 /*
6781 *ˇ Lorem ipsum dolor sit amet,
6782 * consectetur adipiscing elit.
6783 */
6784 /*
6785 *ˇ Lorem ipsum dolor sit amet,
6786 * consectetur adipiscing elit.
6787 */
6788 "},
6789 rust_lang.clone(),
6790 &mut cx,
6791 );
6792
6793 // multiline block comment with inline start/end delimiters
6794 assert_rewrap(
6795 indoc! {"
6796 /*ˇ Lorem ipsum dolor sit amet,
6797 * consectetur adipiscing elit. */
6798 "},
6799 indoc! {"
6800 /*
6801 *ˇ Lorem ipsum dolor sit amet,
6802 * consectetur adipiscing elit.
6803 */
6804 "},
6805 rust_lang.clone(),
6806 &mut cx,
6807 );
6808
6809 // block comment rewrap still respects paragraph bounds
6810 assert_rewrap(
6811 indoc! {"
6812 /*
6813 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6814 *
6815 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6816 */
6817 "},
6818 indoc! {"
6819 /*
6820 *ˇ Lorem ipsum dolor sit amet,
6821 * consectetur adipiscing elit.
6822 *
6823 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6824 */
6825 "},
6826 rust_lang.clone(),
6827 &mut cx,
6828 );
6829
6830 // documentation comments
6831 assert_rewrap(
6832 indoc! {"
6833 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6834 /**
6835 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6836 */
6837 "},
6838 indoc! {"
6839 /**
6840 *ˇ Lorem ipsum dolor sit amet,
6841 * consectetur adipiscing elit.
6842 */
6843 /**
6844 *ˇ Lorem ipsum dolor sit amet,
6845 * consectetur adipiscing elit.
6846 */
6847 "},
6848 rust_lang.clone(),
6849 &mut cx,
6850 );
6851
6852 // different, adjacent comments
6853 assert_rewrap(
6854 indoc! {"
6855 /**
6856 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6857 */
6858 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6859 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6860 "},
6861 indoc! {"
6862 /**
6863 *ˇ Lorem ipsum dolor sit amet,
6864 * consectetur adipiscing elit.
6865 */
6866 /*
6867 *ˇ Lorem ipsum dolor sit amet,
6868 * consectetur adipiscing elit.
6869 */
6870 //ˇ Lorem ipsum dolor sit amet,
6871 // consectetur adipiscing elit.
6872 "},
6873 rust_lang.clone(),
6874 &mut cx,
6875 );
6876
6877 // selection w/ single short block comment
6878 assert_rewrap(
6879 indoc! {"
6880 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6881 "},
6882 indoc! {"
6883 «/*
6884 * Lorem ipsum dolor sit amet,
6885 * consectetur adipiscing elit.
6886 */ˇ»
6887 "},
6888 rust_lang.clone(),
6889 &mut cx,
6890 );
6891
6892 // rewrapping a single comment w/ abutting comments
6893 assert_rewrap(
6894 indoc! {"
6895 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6896 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6897 "},
6898 indoc! {"
6899 /*
6900 * ˇLorem ipsum dolor sit amet,
6901 * consectetur adipiscing elit.
6902 */
6903 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6904 "},
6905 rust_lang.clone(),
6906 &mut cx,
6907 );
6908
6909 // selection w/ non-abutting short block comments
6910 assert_rewrap(
6911 indoc! {"
6912 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6913
6914 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6915 "},
6916 indoc! {"
6917 «/*
6918 * Lorem ipsum dolor sit amet,
6919 * consectetur adipiscing elit.
6920 */
6921
6922 /*
6923 * Lorem ipsum dolor sit amet,
6924 * consectetur adipiscing elit.
6925 */ˇ»
6926 "},
6927 rust_lang.clone(),
6928 &mut cx,
6929 );
6930
6931 // selection of multiline block comments
6932 assert_rewrap(
6933 indoc! {"
6934 «/* Lorem ipsum dolor sit amet,
6935 * consectetur adipiscing elit. */ˇ»
6936 "},
6937 indoc! {"
6938 «/*
6939 * Lorem ipsum dolor sit amet,
6940 * consectetur adipiscing elit.
6941 */ˇ»
6942 "},
6943 rust_lang.clone(),
6944 &mut cx,
6945 );
6946
6947 // partial selection of multiline block comments
6948 assert_rewrap(
6949 indoc! {"
6950 «/* Lorem ipsum dolor sit amet,ˇ»
6951 * consectetur adipiscing elit. */
6952 /* Lorem ipsum dolor sit amet,
6953 «* consectetur adipiscing elit. */ˇ»
6954 "},
6955 indoc! {"
6956 «/*
6957 * Lorem ipsum dolor sit amet,ˇ»
6958 * consectetur adipiscing elit. */
6959 /* Lorem ipsum dolor sit amet,
6960 «* consectetur adipiscing elit.
6961 */ˇ»
6962 "},
6963 rust_lang.clone(),
6964 &mut cx,
6965 );
6966
6967 // selection w/ abutting short block comments
6968 // TODO: should not be combined; should rewrap as 2 comments
6969 assert_rewrap(
6970 indoc! {"
6971 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6972 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6973 "},
6974 // desired behavior:
6975 // indoc! {"
6976 // «/*
6977 // * Lorem ipsum dolor sit amet,
6978 // * consectetur adipiscing elit.
6979 // */
6980 // /*
6981 // * Lorem ipsum dolor sit amet,
6982 // * consectetur adipiscing elit.
6983 // */ˇ»
6984 // "},
6985 // actual behaviour:
6986 indoc! {"
6987 «/*
6988 * Lorem ipsum dolor sit amet,
6989 * consectetur adipiscing elit. Lorem
6990 * ipsum dolor sit amet, consectetur
6991 * adipiscing elit.
6992 */ˇ»
6993 "},
6994 rust_lang.clone(),
6995 &mut cx,
6996 );
6997
6998 // TODO: same as above, but with delimiters on separate line
6999 // assert_rewrap(
7000 // indoc! {"
7001 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7002 // */
7003 // /*
7004 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7005 // "},
7006 // // desired:
7007 // // indoc! {"
7008 // // «/*
7009 // // * Lorem ipsum dolor sit amet,
7010 // // * consectetur adipiscing elit.
7011 // // */
7012 // // /*
7013 // // * Lorem ipsum dolor sit amet,
7014 // // * consectetur adipiscing elit.
7015 // // */ˇ»
7016 // // "},
7017 // // actual: (but with trailing w/s on the empty lines)
7018 // indoc! {"
7019 // «/*
7020 // * Lorem ipsum dolor sit amet,
7021 // * consectetur adipiscing elit.
7022 // *
7023 // */
7024 // /*
7025 // *
7026 // * Lorem ipsum dolor sit amet,
7027 // * consectetur adipiscing elit.
7028 // */ˇ»
7029 // "},
7030 // rust_lang.clone(),
7031 // &mut cx,
7032 // );
7033
7034 // TODO these are unhandled edge cases; not correct, just documenting known issues
7035 assert_rewrap(
7036 indoc! {"
7037 /*
7038 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7039 */
7040 /*
7041 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7042 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7043 "},
7044 // desired:
7045 // indoc! {"
7046 // /*
7047 // *ˇ Lorem ipsum dolor sit amet,
7048 // * consectetur adipiscing elit.
7049 // */
7050 // /*
7051 // *ˇ Lorem ipsum dolor sit amet,
7052 // * consectetur adipiscing elit.
7053 // */
7054 // /*
7055 // *ˇ Lorem ipsum dolor sit amet
7056 // */ /* consectetur adipiscing elit. */
7057 // "},
7058 // actual:
7059 indoc! {"
7060 /*
7061 //ˇ Lorem ipsum dolor sit amet,
7062 // consectetur adipiscing elit.
7063 */
7064 /*
7065 * //ˇ Lorem ipsum dolor sit amet,
7066 * consectetur adipiscing elit.
7067 */
7068 /*
7069 *ˇ Lorem ipsum dolor sit amet */ /*
7070 * consectetur adipiscing elit.
7071 */
7072 "},
7073 rust_lang,
7074 &mut cx,
7075 );
7076
7077 #[track_caller]
7078 fn assert_rewrap(
7079 unwrapped_text: &str,
7080 wrapped_text: &str,
7081 language: Arc<Language>,
7082 cx: &mut EditorTestContext,
7083 ) {
7084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7085 cx.set_state(unwrapped_text);
7086 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7087 cx.assert_editor_state(wrapped_text);
7088 }
7089}
7090
7091#[gpui::test]
7092async fn test_hard_wrap(cx: &mut TestAppContext) {
7093 init_test(cx, |_| {});
7094 let mut cx = EditorTestContext::new(cx).await;
7095
7096 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7097 cx.update_editor(|editor, _, cx| {
7098 editor.set_hard_wrap(Some(14), cx);
7099 });
7100
7101 cx.set_state(indoc!(
7102 "
7103 one two three ˇ
7104 "
7105 ));
7106 cx.simulate_input("four");
7107 cx.run_until_parked();
7108
7109 cx.assert_editor_state(indoc!(
7110 "
7111 one two three
7112 fourˇ
7113 "
7114 ));
7115
7116 cx.update_editor(|editor, window, cx| {
7117 editor.newline(&Default::default(), window, cx);
7118 });
7119 cx.run_until_parked();
7120 cx.assert_editor_state(indoc!(
7121 "
7122 one two three
7123 four
7124 ˇ
7125 "
7126 ));
7127
7128 cx.simulate_input("five");
7129 cx.run_until_parked();
7130 cx.assert_editor_state(indoc!(
7131 "
7132 one two three
7133 four
7134 fiveˇ
7135 "
7136 ));
7137
7138 cx.update_editor(|editor, window, cx| {
7139 editor.newline(&Default::default(), window, cx);
7140 });
7141 cx.run_until_parked();
7142 cx.simulate_input("# ");
7143 cx.run_until_parked();
7144 cx.assert_editor_state(indoc!(
7145 "
7146 one two three
7147 four
7148 five
7149 # ˇ
7150 "
7151 ));
7152
7153 cx.update_editor(|editor, window, cx| {
7154 editor.newline(&Default::default(), window, cx);
7155 });
7156 cx.run_until_parked();
7157 cx.assert_editor_state(indoc!(
7158 "
7159 one two three
7160 four
7161 five
7162 #\x20
7163 #ˇ
7164 "
7165 ));
7166
7167 cx.simulate_input(" 6");
7168 cx.run_until_parked();
7169 cx.assert_editor_state(indoc!(
7170 "
7171 one two three
7172 four
7173 five
7174 #
7175 # 6ˇ
7176 "
7177 ));
7178}
7179
7180#[gpui::test]
7181async fn test_cut_line_ends(cx: &mut TestAppContext) {
7182 init_test(cx, |_| {});
7183
7184 let mut cx = EditorTestContext::new(cx).await;
7185
7186 cx.set_state(indoc! {"The quick brownˇ"});
7187 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7188 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7189
7190 cx.set_state(indoc! {"The emacs foxˇ"});
7191 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7192 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7193
7194 cx.set_state(indoc! {"
7195 The quick« brownˇ»
7196 fox jumps overˇ
7197 the lazy dog"});
7198 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7199 cx.assert_editor_state(indoc! {"
7200 The quickˇ
7201 ˇthe lazy dog"});
7202
7203 cx.set_state(indoc! {"
7204 The quick« brownˇ»
7205 fox jumps overˇ
7206 the lazy dog"});
7207 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7208 cx.assert_editor_state(indoc! {"
7209 The quickˇ
7210 fox jumps overˇthe lazy dog"});
7211
7212 cx.set_state(indoc! {"
7213 The quick« brownˇ»
7214 fox jumps overˇ
7215 the lazy dog"});
7216 cx.update_editor(|e, window, cx| {
7217 e.cut_to_end_of_line(
7218 &CutToEndOfLine {
7219 stop_at_newlines: true,
7220 },
7221 window,
7222 cx,
7223 )
7224 });
7225 cx.assert_editor_state(indoc! {"
7226 The quickˇ
7227 fox jumps overˇ
7228 the lazy dog"});
7229
7230 cx.set_state(indoc! {"
7231 The quick« brownˇ»
7232 fox jumps overˇ
7233 the lazy dog"});
7234 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7235 cx.assert_editor_state(indoc! {"
7236 The quickˇ
7237 fox jumps overˇthe lazy dog"});
7238}
7239
7240#[gpui::test]
7241async fn test_clipboard(cx: &mut TestAppContext) {
7242 init_test(cx, |_| {});
7243
7244 let mut cx = EditorTestContext::new(cx).await;
7245
7246 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7247 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7248 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7249
7250 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7251 cx.set_state("two ˇfour ˇsix ˇ");
7252 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7253 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7254
7255 // Paste again but with only two cursors. Since the number of cursors doesn't
7256 // match the number of slices in the clipboard, the entire clipboard text
7257 // is pasted at each cursor.
7258 cx.set_state("ˇtwo one✅ four three six five ˇ");
7259 cx.update_editor(|e, window, cx| {
7260 e.handle_input("( ", window, cx);
7261 e.paste(&Paste, window, cx);
7262 e.handle_input(") ", window, cx);
7263 });
7264 cx.assert_editor_state(
7265 &([
7266 "( one✅ ",
7267 "three ",
7268 "five ) ˇtwo one✅ four three six five ( one✅ ",
7269 "three ",
7270 "five ) ˇ",
7271 ]
7272 .join("\n")),
7273 );
7274
7275 // Cut with three selections, one of which is full-line.
7276 cx.set_state(indoc! {"
7277 1«2ˇ»3
7278 4ˇ567
7279 «8ˇ»9"});
7280 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7281 cx.assert_editor_state(indoc! {"
7282 1ˇ3
7283 ˇ9"});
7284
7285 // Paste with three selections, noticing how the copied selection that was full-line
7286 // gets inserted before the second cursor.
7287 cx.set_state(indoc! {"
7288 1ˇ3
7289 9ˇ
7290 «oˇ»ne"});
7291 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7292 cx.assert_editor_state(indoc! {"
7293 12ˇ3
7294 4567
7295 9ˇ
7296 8ˇne"});
7297
7298 // Copy with a single cursor only, which writes the whole line into the clipboard.
7299 cx.set_state(indoc! {"
7300 The quick brown
7301 fox juˇmps over
7302 the lazy dog"});
7303 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7304 assert_eq!(
7305 cx.read_from_clipboard()
7306 .and_then(|item| item.text().as_deref().map(str::to_string)),
7307 Some("fox jumps over\n".to_string())
7308 );
7309
7310 // Paste with three selections, noticing how the copied full-line selection is inserted
7311 // before the empty selections but replaces the selection that is non-empty.
7312 cx.set_state(indoc! {"
7313 Tˇhe quick brown
7314 «foˇ»x jumps over
7315 tˇhe lazy dog"});
7316 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7317 cx.assert_editor_state(indoc! {"
7318 fox jumps over
7319 Tˇhe quick brown
7320 fox jumps over
7321 ˇx jumps over
7322 fox jumps over
7323 tˇhe lazy dog"});
7324}
7325
7326#[gpui::test]
7327async fn test_copy_trim(cx: &mut TestAppContext) {
7328 init_test(cx, |_| {});
7329
7330 let mut cx = EditorTestContext::new(cx).await;
7331 cx.set_state(
7332 r#" «for selection in selections.iter() {
7333 let mut start = selection.start;
7334 let mut end = selection.end;
7335 let is_entire_line = selection.is_empty();
7336 if is_entire_line {
7337 start = Point::new(start.row, 0);ˇ»
7338 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7339 }
7340 "#,
7341 );
7342 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7343 assert_eq!(
7344 cx.read_from_clipboard()
7345 .and_then(|item| item.text().as_deref().map(str::to_string)),
7346 Some(
7347 "for selection in selections.iter() {
7348 let mut start = selection.start;
7349 let mut end = selection.end;
7350 let is_entire_line = selection.is_empty();
7351 if is_entire_line {
7352 start = Point::new(start.row, 0);"
7353 .to_string()
7354 ),
7355 "Regular copying preserves all indentation selected",
7356 );
7357 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7358 assert_eq!(
7359 cx.read_from_clipboard()
7360 .and_then(|item| item.text().as_deref().map(str::to_string)),
7361 Some(
7362 "for selection in selections.iter() {
7363let mut start = selection.start;
7364let mut end = selection.end;
7365let is_entire_line = selection.is_empty();
7366if is_entire_line {
7367 start = Point::new(start.row, 0);"
7368 .to_string()
7369 ),
7370 "Copying with stripping should strip all leading whitespaces"
7371 );
7372
7373 cx.set_state(
7374 r#" « for selection in selections.iter() {
7375 let mut start = selection.start;
7376 let mut end = selection.end;
7377 let is_entire_line = selection.is_empty();
7378 if is_entire_line {
7379 start = Point::new(start.row, 0);ˇ»
7380 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7381 }
7382 "#,
7383 );
7384 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7385 assert_eq!(
7386 cx.read_from_clipboard()
7387 .and_then(|item| item.text().as_deref().map(str::to_string)),
7388 Some(
7389 " for selection in selections.iter() {
7390 let mut start = selection.start;
7391 let mut end = selection.end;
7392 let is_entire_line = selection.is_empty();
7393 if is_entire_line {
7394 start = Point::new(start.row, 0);"
7395 .to_string()
7396 ),
7397 "Regular copying preserves all indentation selected",
7398 );
7399 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7400 assert_eq!(
7401 cx.read_from_clipboard()
7402 .and_then(|item| item.text().as_deref().map(str::to_string)),
7403 Some(
7404 "for selection in selections.iter() {
7405let mut start = selection.start;
7406let mut end = selection.end;
7407let is_entire_line = selection.is_empty();
7408if is_entire_line {
7409 start = Point::new(start.row, 0);"
7410 .to_string()
7411 ),
7412 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7413 );
7414
7415 cx.set_state(
7416 r#" «ˇ for selection in selections.iter() {
7417 let mut start = selection.start;
7418 let mut end = selection.end;
7419 let is_entire_line = selection.is_empty();
7420 if is_entire_line {
7421 start = Point::new(start.row, 0);»
7422 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7423 }
7424 "#,
7425 );
7426 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7427 assert_eq!(
7428 cx.read_from_clipboard()
7429 .and_then(|item| item.text().as_deref().map(str::to_string)),
7430 Some(
7431 " for selection in selections.iter() {
7432 let mut start = selection.start;
7433 let mut end = selection.end;
7434 let is_entire_line = selection.is_empty();
7435 if is_entire_line {
7436 start = Point::new(start.row, 0);"
7437 .to_string()
7438 ),
7439 "Regular copying for reverse selection works the same",
7440 );
7441 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7442 assert_eq!(
7443 cx.read_from_clipboard()
7444 .and_then(|item| item.text().as_deref().map(str::to_string)),
7445 Some(
7446 "for selection in selections.iter() {
7447let mut start = selection.start;
7448let mut end = selection.end;
7449let is_entire_line = selection.is_empty();
7450if is_entire_line {
7451 start = Point::new(start.row, 0);"
7452 .to_string()
7453 ),
7454 "Copying with stripping for reverse selection works the same"
7455 );
7456
7457 cx.set_state(
7458 r#" for selection «in selections.iter() {
7459 let mut start = selection.start;
7460 let mut end = selection.end;
7461 let is_entire_line = selection.is_empty();
7462 if is_entire_line {
7463 start = Point::new(start.row, 0);ˇ»
7464 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7465 }
7466 "#,
7467 );
7468 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7469 assert_eq!(
7470 cx.read_from_clipboard()
7471 .and_then(|item| item.text().as_deref().map(str::to_string)),
7472 Some(
7473 "in selections.iter() {
7474 let mut start = selection.start;
7475 let mut end = selection.end;
7476 let is_entire_line = selection.is_empty();
7477 if is_entire_line {
7478 start = Point::new(start.row, 0);"
7479 .to_string()
7480 ),
7481 "When selecting past the indent, the copying works as usual",
7482 );
7483 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7484 assert_eq!(
7485 cx.read_from_clipboard()
7486 .and_then(|item| item.text().as_deref().map(str::to_string)),
7487 Some(
7488 "in selections.iter() {
7489 let mut start = selection.start;
7490 let mut end = selection.end;
7491 let is_entire_line = selection.is_empty();
7492 if is_entire_line {
7493 start = Point::new(start.row, 0);"
7494 .to_string()
7495 ),
7496 "When selecting past the indent, nothing is trimmed"
7497 );
7498
7499 cx.set_state(
7500 r#" «for selection in selections.iter() {
7501 let mut start = selection.start;
7502
7503 let mut end = selection.end;
7504 let is_entire_line = selection.is_empty();
7505 if is_entire_line {
7506 start = Point::new(start.row, 0);
7507ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7508 }
7509 "#,
7510 );
7511 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7512 assert_eq!(
7513 cx.read_from_clipboard()
7514 .and_then(|item| item.text().as_deref().map(str::to_string)),
7515 Some(
7516 "for selection in selections.iter() {
7517let mut start = selection.start;
7518
7519let mut end = selection.end;
7520let is_entire_line = selection.is_empty();
7521if is_entire_line {
7522 start = Point::new(start.row, 0);
7523"
7524 .to_string()
7525 ),
7526 "Copying with stripping should ignore empty lines"
7527 );
7528}
7529
7530#[gpui::test]
7531async fn test_paste_multiline(cx: &mut TestAppContext) {
7532 init_test(cx, |_| {});
7533
7534 let mut cx = EditorTestContext::new(cx).await;
7535 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7536
7537 // Cut an indented block, without the leading whitespace.
7538 cx.set_state(indoc! {"
7539 const a: B = (
7540 c(),
7541 «d(
7542 e,
7543 f
7544 )ˇ»
7545 );
7546 "});
7547 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7548 cx.assert_editor_state(indoc! {"
7549 const a: B = (
7550 c(),
7551 ˇ
7552 );
7553 "});
7554
7555 // Paste it at the same position.
7556 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7557 cx.assert_editor_state(indoc! {"
7558 const a: B = (
7559 c(),
7560 d(
7561 e,
7562 f
7563 )ˇ
7564 );
7565 "});
7566
7567 // Paste it at a line with a lower indent level.
7568 cx.set_state(indoc! {"
7569 ˇ
7570 const a: B = (
7571 c(),
7572 );
7573 "});
7574 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7575 cx.assert_editor_state(indoc! {"
7576 d(
7577 e,
7578 f
7579 )ˇ
7580 const a: B = (
7581 c(),
7582 );
7583 "});
7584
7585 // Cut an indented block, with the leading whitespace.
7586 cx.set_state(indoc! {"
7587 const a: B = (
7588 c(),
7589 « d(
7590 e,
7591 f
7592 )
7593 ˇ»);
7594 "});
7595 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7596 cx.assert_editor_state(indoc! {"
7597 const a: B = (
7598 c(),
7599 ˇ);
7600 "});
7601
7602 // Paste it at the same position.
7603 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7604 cx.assert_editor_state(indoc! {"
7605 const a: B = (
7606 c(),
7607 d(
7608 e,
7609 f
7610 )
7611 ˇ);
7612 "});
7613
7614 // Paste it at a line with a higher indent level.
7615 cx.set_state(indoc! {"
7616 const a: B = (
7617 c(),
7618 d(
7619 e,
7620 fˇ
7621 )
7622 );
7623 "});
7624 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7625 cx.assert_editor_state(indoc! {"
7626 const a: B = (
7627 c(),
7628 d(
7629 e,
7630 f d(
7631 e,
7632 f
7633 )
7634 ˇ
7635 )
7636 );
7637 "});
7638
7639 // Copy an indented block, starting mid-line
7640 cx.set_state(indoc! {"
7641 const a: B = (
7642 c(),
7643 somethin«g(
7644 e,
7645 f
7646 )ˇ»
7647 );
7648 "});
7649 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7650
7651 // Paste it on a line with a lower indent level
7652 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7653 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7654 cx.assert_editor_state(indoc! {"
7655 const a: B = (
7656 c(),
7657 something(
7658 e,
7659 f
7660 )
7661 );
7662 g(
7663 e,
7664 f
7665 )ˇ"});
7666}
7667
7668#[gpui::test]
7669async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7670 init_test(cx, |_| {});
7671
7672 cx.write_to_clipboard(ClipboardItem::new_string(
7673 " d(\n e\n );\n".into(),
7674 ));
7675
7676 let mut cx = EditorTestContext::new(cx).await;
7677 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7678
7679 cx.set_state(indoc! {"
7680 fn a() {
7681 b();
7682 if c() {
7683 ˇ
7684 }
7685 }
7686 "});
7687
7688 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7689 cx.assert_editor_state(indoc! {"
7690 fn a() {
7691 b();
7692 if c() {
7693 d(
7694 e
7695 );
7696 ˇ
7697 }
7698 }
7699 "});
7700
7701 cx.set_state(indoc! {"
7702 fn a() {
7703 b();
7704 ˇ
7705 }
7706 "});
7707
7708 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7709 cx.assert_editor_state(indoc! {"
7710 fn a() {
7711 b();
7712 d(
7713 e
7714 );
7715 ˇ
7716 }
7717 "});
7718}
7719
7720#[gpui::test]
7721fn test_select_all(cx: &mut TestAppContext) {
7722 init_test(cx, |_| {});
7723
7724 let editor = cx.add_window(|window, cx| {
7725 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7726 build_editor(buffer, window, cx)
7727 });
7728 _ = editor.update(cx, |editor, window, cx| {
7729 editor.select_all(&SelectAll, window, cx);
7730 assert_eq!(
7731 display_ranges(editor, cx),
7732 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7733 );
7734 });
7735}
7736
7737#[gpui::test]
7738fn test_select_line(cx: &mut TestAppContext) {
7739 init_test(cx, |_| {});
7740
7741 let editor = cx.add_window(|window, cx| {
7742 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7743 build_editor(buffer, window, cx)
7744 });
7745 _ = editor.update(cx, |editor, window, cx| {
7746 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7747 s.select_display_ranges([
7748 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7749 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7750 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7751 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7752 ])
7753 });
7754 editor.select_line(&SelectLine, window, cx);
7755 // Adjacent line selections should NOT merge (only overlapping ones do)
7756 assert_eq!(
7757 display_ranges(editor, cx),
7758 vec![
7759 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
7760 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
7761 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7762 ]
7763 );
7764 });
7765
7766 _ = editor.update(cx, |editor, window, cx| {
7767 editor.select_line(&SelectLine, window, cx);
7768 assert_eq!(
7769 display_ranges(editor, cx),
7770 vec![
7771 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7772 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7773 ]
7774 );
7775 });
7776
7777 _ = editor.update(cx, |editor, window, cx| {
7778 editor.select_line(&SelectLine, window, cx);
7779 // Adjacent but not overlapping, so they stay separate
7780 assert_eq!(
7781 display_ranges(editor, cx),
7782 vec![
7783 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
7784 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7785 ]
7786 );
7787 });
7788}
7789
7790#[gpui::test]
7791async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7792 init_test(cx, |_| {});
7793 let mut cx = EditorTestContext::new(cx).await;
7794
7795 #[track_caller]
7796 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7797 cx.set_state(initial_state);
7798 cx.update_editor(|e, window, cx| {
7799 e.split_selection_into_lines(&Default::default(), window, cx)
7800 });
7801 cx.assert_editor_state(expected_state);
7802 }
7803
7804 // Selection starts and ends at the middle of lines, left-to-right
7805 test(
7806 &mut cx,
7807 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7808 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7809 );
7810 // Same thing, right-to-left
7811 test(
7812 &mut cx,
7813 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7814 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7815 );
7816
7817 // Whole buffer, left-to-right, last line *doesn't* end with newline
7818 test(
7819 &mut cx,
7820 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7821 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7822 );
7823 // Same thing, right-to-left
7824 test(
7825 &mut cx,
7826 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7827 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7828 );
7829
7830 // Whole buffer, left-to-right, last line ends with newline
7831 test(
7832 &mut cx,
7833 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7834 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7835 );
7836 // Same thing, right-to-left
7837 test(
7838 &mut cx,
7839 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7840 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7841 );
7842
7843 // Starts at the end of a line, ends at the start of another
7844 test(
7845 &mut cx,
7846 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7847 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7848 );
7849}
7850
7851#[gpui::test]
7852async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7853 init_test(cx, |_| {});
7854
7855 let editor = cx.add_window(|window, cx| {
7856 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7857 build_editor(buffer, window, cx)
7858 });
7859
7860 // setup
7861 _ = editor.update(cx, |editor, window, cx| {
7862 editor.fold_creases(
7863 vec![
7864 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7865 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7866 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7867 ],
7868 true,
7869 window,
7870 cx,
7871 );
7872 assert_eq!(
7873 editor.display_text(cx),
7874 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7875 );
7876 });
7877
7878 _ = editor.update(cx, |editor, window, cx| {
7879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7880 s.select_display_ranges([
7881 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7882 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7883 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7884 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7885 ])
7886 });
7887 editor.split_selection_into_lines(&Default::default(), window, cx);
7888 assert_eq!(
7889 editor.display_text(cx),
7890 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7891 );
7892 });
7893 EditorTestContext::for_editor(editor, cx)
7894 .await
7895 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7896
7897 _ = editor.update(cx, |editor, window, cx| {
7898 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7899 s.select_display_ranges([
7900 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7901 ])
7902 });
7903 editor.split_selection_into_lines(&Default::default(), window, cx);
7904 assert_eq!(
7905 editor.display_text(cx),
7906 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7907 );
7908 assert_eq!(
7909 display_ranges(editor, cx),
7910 [
7911 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7912 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7913 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7914 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7915 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7916 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7917 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7918 ]
7919 );
7920 });
7921 EditorTestContext::for_editor(editor, cx)
7922 .await
7923 .assert_editor_state(
7924 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7925 );
7926}
7927
7928#[gpui::test]
7929async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7930 init_test(cx, |_| {});
7931
7932 let mut cx = EditorTestContext::new(cx).await;
7933
7934 cx.set_state(indoc!(
7935 r#"abc
7936 defˇghi
7937
7938 jk
7939 nlmo
7940 "#
7941 ));
7942
7943 cx.update_editor(|editor, window, cx| {
7944 editor.add_selection_above(&Default::default(), window, cx);
7945 });
7946
7947 cx.assert_editor_state(indoc!(
7948 r#"abcˇ
7949 defˇghi
7950
7951 jk
7952 nlmo
7953 "#
7954 ));
7955
7956 cx.update_editor(|editor, window, cx| {
7957 editor.add_selection_above(&Default::default(), window, cx);
7958 });
7959
7960 cx.assert_editor_state(indoc!(
7961 r#"abcˇ
7962 defˇghi
7963
7964 jk
7965 nlmo
7966 "#
7967 ));
7968
7969 cx.update_editor(|editor, window, cx| {
7970 editor.add_selection_below(&Default::default(), window, cx);
7971 });
7972
7973 cx.assert_editor_state(indoc!(
7974 r#"abc
7975 defˇghi
7976
7977 jk
7978 nlmo
7979 "#
7980 ));
7981
7982 cx.update_editor(|editor, window, cx| {
7983 editor.undo_selection(&Default::default(), window, cx);
7984 });
7985
7986 cx.assert_editor_state(indoc!(
7987 r#"abcˇ
7988 defˇghi
7989
7990 jk
7991 nlmo
7992 "#
7993 ));
7994
7995 cx.update_editor(|editor, window, cx| {
7996 editor.redo_selection(&Default::default(), window, cx);
7997 });
7998
7999 cx.assert_editor_state(indoc!(
8000 r#"abc
8001 defˇghi
8002
8003 jk
8004 nlmo
8005 "#
8006 ));
8007
8008 cx.update_editor(|editor, window, cx| {
8009 editor.add_selection_below(&Default::default(), window, cx);
8010 });
8011
8012 cx.assert_editor_state(indoc!(
8013 r#"abc
8014 defˇghi
8015 ˇ
8016 jk
8017 nlmo
8018 "#
8019 ));
8020
8021 cx.update_editor(|editor, window, cx| {
8022 editor.add_selection_below(&Default::default(), window, cx);
8023 });
8024
8025 cx.assert_editor_state(indoc!(
8026 r#"abc
8027 defˇghi
8028 ˇ
8029 jkˇ
8030 nlmo
8031 "#
8032 ));
8033
8034 cx.update_editor(|editor, window, cx| {
8035 editor.add_selection_below(&Default::default(), window, cx);
8036 });
8037
8038 cx.assert_editor_state(indoc!(
8039 r#"abc
8040 defˇghi
8041 ˇ
8042 jkˇ
8043 nlmˇo
8044 "#
8045 ));
8046
8047 cx.update_editor(|editor, window, cx| {
8048 editor.add_selection_below(&Default::default(), window, cx);
8049 });
8050
8051 cx.assert_editor_state(indoc!(
8052 r#"abc
8053 defˇghi
8054 ˇ
8055 jkˇ
8056 nlmˇo
8057 ˇ"#
8058 ));
8059
8060 // change selections
8061 cx.set_state(indoc!(
8062 r#"abc
8063 def«ˇg»hi
8064
8065 jk
8066 nlmo
8067 "#
8068 ));
8069
8070 cx.update_editor(|editor, window, cx| {
8071 editor.add_selection_below(&Default::default(), window, cx);
8072 });
8073
8074 cx.assert_editor_state(indoc!(
8075 r#"abc
8076 def«ˇg»hi
8077
8078 jk
8079 nlm«ˇo»
8080 "#
8081 ));
8082
8083 cx.update_editor(|editor, window, cx| {
8084 editor.add_selection_below(&Default::default(), window, cx);
8085 });
8086
8087 cx.assert_editor_state(indoc!(
8088 r#"abc
8089 def«ˇg»hi
8090
8091 jk
8092 nlm«ˇo»
8093 "#
8094 ));
8095
8096 cx.update_editor(|editor, window, cx| {
8097 editor.add_selection_above(&Default::default(), window, cx);
8098 });
8099
8100 cx.assert_editor_state(indoc!(
8101 r#"abc
8102 def«ˇg»hi
8103
8104 jk
8105 nlmo
8106 "#
8107 ));
8108
8109 cx.update_editor(|editor, window, cx| {
8110 editor.add_selection_above(&Default::default(), window, cx);
8111 });
8112
8113 cx.assert_editor_state(indoc!(
8114 r#"abc
8115 def«ˇg»hi
8116
8117 jk
8118 nlmo
8119 "#
8120 ));
8121
8122 // Change selections again
8123 cx.set_state(indoc!(
8124 r#"a«bc
8125 defgˇ»hi
8126
8127 jk
8128 nlmo
8129 "#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_below(&Default::default(), window, cx);
8134 });
8135
8136 cx.assert_editor_state(indoc!(
8137 r#"a«bcˇ»
8138 d«efgˇ»hi
8139
8140 j«kˇ»
8141 nlmo
8142 "#
8143 ));
8144
8145 cx.update_editor(|editor, window, cx| {
8146 editor.add_selection_below(&Default::default(), window, cx);
8147 });
8148 cx.assert_editor_state(indoc!(
8149 r#"a«bcˇ»
8150 d«efgˇ»hi
8151
8152 j«kˇ»
8153 n«lmoˇ»
8154 "#
8155 ));
8156 cx.update_editor(|editor, window, cx| {
8157 editor.add_selection_above(&Default::default(), window, cx);
8158 });
8159
8160 cx.assert_editor_state(indoc!(
8161 r#"a«bcˇ»
8162 d«efgˇ»hi
8163
8164 j«kˇ»
8165 nlmo
8166 "#
8167 ));
8168
8169 // Change selections again
8170 cx.set_state(indoc!(
8171 r#"abc
8172 d«ˇefghi
8173
8174 jk
8175 nlm»o
8176 "#
8177 ));
8178
8179 cx.update_editor(|editor, window, cx| {
8180 editor.add_selection_above(&Default::default(), window, cx);
8181 });
8182
8183 cx.assert_editor_state(indoc!(
8184 r#"a«ˇbc»
8185 d«ˇef»ghi
8186
8187 j«ˇk»
8188 n«ˇlm»o
8189 "#
8190 ));
8191
8192 cx.update_editor(|editor, window, cx| {
8193 editor.add_selection_below(&Default::default(), window, cx);
8194 });
8195
8196 cx.assert_editor_state(indoc!(
8197 r#"abc
8198 d«ˇef»ghi
8199
8200 j«ˇk»
8201 n«ˇlm»o
8202 "#
8203 ));
8204}
8205
8206#[gpui::test]
8207async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8208 init_test(cx, |_| {});
8209 let mut cx = EditorTestContext::new(cx).await;
8210
8211 cx.set_state(indoc!(
8212 r#"line onˇe
8213 liˇne two
8214 line three
8215 line four"#
8216 ));
8217
8218 cx.update_editor(|editor, window, cx| {
8219 editor.add_selection_below(&Default::default(), window, cx);
8220 });
8221
8222 // test multiple cursors expand in the same direction
8223 cx.assert_editor_state(indoc!(
8224 r#"line onˇe
8225 liˇne twˇo
8226 liˇne three
8227 line four"#
8228 ));
8229
8230 cx.update_editor(|editor, window, cx| {
8231 editor.add_selection_below(&Default::default(), window, cx);
8232 });
8233
8234 cx.update_editor(|editor, window, cx| {
8235 editor.add_selection_below(&Default::default(), window, cx);
8236 });
8237
8238 // test multiple cursors expand below overflow
8239 cx.assert_editor_state(indoc!(
8240 r#"line onˇe
8241 liˇne twˇo
8242 liˇne thˇree
8243 liˇne foˇur"#
8244 ));
8245
8246 cx.update_editor(|editor, window, cx| {
8247 editor.add_selection_above(&Default::default(), window, cx);
8248 });
8249
8250 // test multiple cursors retrieves back correctly
8251 cx.assert_editor_state(indoc!(
8252 r#"line onˇe
8253 liˇne twˇo
8254 liˇne thˇree
8255 line four"#
8256 ));
8257
8258 cx.update_editor(|editor, window, cx| {
8259 editor.add_selection_above(&Default::default(), window, cx);
8260 });
8261
8262 cx.update_editor(|editor, window, cx| {
8263 editor.add_selection_above(&Default::default(), window, cx);
8264 });
8265
8266 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8267 cx.assert_editor_state(indoc!(
8268 r#"liˇne onˇe
8269 liˇne two
8270 line three
8271 line four"#
8272 ));
8273
8274 cx.update_editor(|editor, window, cx| {
8275 editor.undo_selection(&Default::default(), window, cx);
8276 });
8277
8278 // test undo
8279 cx.assert_editor_state(indoc!(
8280 r#"line onˇe
8281 liˇne twˇo
8282 line three
8283 line four"#
8284 ));
8285
8286 cx.update_editor(|editor, window, cx| {
8287 editor.redo_selection(&Default::default(), window, cx);
8288 });
8289
8290 // test redo
8291 cx.assert_editor_state(indoc!(
8292 r#"liˇne onˇe
8293 liˇne two
8294 line three
8295 line four"#
8296 ));
8297
8298 cx.set_state(indoc!(
8299 r#"abcd
8300 ef«ghˇ»
8301 ijkl
8302 «mˇ»nop"#
8303 ));
8304
8305 cx.update_editor(|editor, window, cx| {
8306 editor.add_selection_above(&Default::default(), window, cx);
8307 });
8308
8309 // test multiple selections expand in the same direction
8310 cx.assert_editor_state(indoc!(
8311 r#"ab«cdˇ»
8312 ef«ghˇ»
8313 «iˇ»jkl
8314 «mˇ»nop"#
8315 ));
8316
8317 cx.update_editor(|editor, window, cx| {
8318 editor.add_selection_above(&Default::default(), window, cx);
8319 });
8320
8321 // test multiple selection upward overflow
8322 cx.assert_editor_state(indoc!(
8323 r#"ab«cdˇ»
8324 «eˇ»f«ghˇ»
8325 «iˇ»jkl
8326 «mˇ»nop"#
8327 ));
8328
8329 cx.update_editor(|editor, window, cx| {
8330 editor.add_selection_below(&Default::default(), window, cx);
8331 });
8332
8333 // test multiple selection retrieves back correctly
8334 cx.assert_editor_state(indoc!(
8335 r#"abcd
8336 ef«ghˇ»
8337 «iˇ»jkl
8338 «mˇ»nop"#
8339 ));
8340
8341 cx.update_editor(|editor, window, cx| {
8342 editor.add_selection_below(&Default::default(), window, cx);
8343 });
8344
8345 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8346 cx.assert_editor_state(indoc!(
8347 r#"abcd
8348 ef«ghˇ»
8349 ij«klˇ»
8350 «mˇ»nop"#
8351 ));
8352
8353 cx.update_editor(|editor, window, cx| {
8354 editor.undo_selection(&Default::default(), window, cx);
8355 });
8356
8357 // test undo
8358 cx.assert_editor_state(indoc!(
8359 r#"abcd
8360 ef«ghˇ»
8361 «iˇ»jkl
8362 «mˇ»nop"#
8363 ));
8364
8365 cx.update_editor(|editor, window, cx| {
8366 editor.redo_selection(&Default::default(), window, cx);
8367 });
8368
8369 // test redo
8370 cx.assert_editor_state(indoc!(
8371 r#"abcd
8372 ef«ghˇ»
8373 ij«klˇ»
8374 «mˇ»nop"#
8375 ));
8376}
8377
8378#[gpui::test]
8379async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8380 init_test(cx, |_| {});
8381 let mut cx = EditorTestContext::new(cx).await;
8382
8383 cx.set_state(indoc!(
8384 r#"line onˇe
8385 liˇne two
8386 line three
8387 line four"#
8388 ));
8389
8390 cx.update_editor(|editor, window, cx| {
8391 editor.add_selection_below(&Default::default(), window, cx);
8392 editor.add_selection_below(&Default::default(), window, cx);
8393 editor.add_selection_below(&Default::default(), window, cx);
8394 });
8395
8396 // initial state with two multi cursor groups
8397 cx.assert_editor_state(indoc!(
8398 r#"line onˇe
8399 liˇne twˇo
8400 liˇne thˇree
8401 liˇne foˇur"#
8402 ));
8403
8404 // add single cursor in middle - simulate opt click
8405 cx.update_editor(|editor, window, cx| {
8406 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8407 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8408 editor.end_selection(window, cx);
8409 });
8410
8411 cx.assert_editor_state(indoc!(
8412 r#"line onˇe
8413 liˇne twˇo
8414 liˇneˇ thˇree
8415 liˇne foˇur"#
8416 ));
8417
8418 cx.update_editor(|editor, window, cx| {
8419 editor.add_selection_above(&Default::default(), window, cx);
8420 });
8421
8422 // test new added selection expands above and existing selection shrinks
8423 cx.assert_editor_state(indoc!(
8424 r#"line onˇe
8425 liˇneˇ twˇo
8426 liˇneˇ thˇree
8427 line four"#
8428 ));
8429
8430 cx.update_editor(|editor, window, cx| {
8431 editor.add_selection_above(&Default::default(), window, cx);
8432 });
8433
8434 // test new added selection expands above and existing selection shrinks
8435 cx.assert_editor_state(indoc!(
8436 r#"lineˇ onˇe
8437 liˇneˇ twˇo
8438 lineˇ three
8439 line four"#
8440 ));
8441
8442 // intial state with two selection groups
8443 cx.set_state(indoc!(
8444 r#"abcd
8445 ef«ghˇ»
8446 ijkl
8447 «mˇ»nop"#
8448 ));
8449
8450 cx.update_editor(|editor, window, cx| {
8451 editor.add_selection_above(&Default::default(), window, cx);
8452 editor.add_selection_above(&Default::default(), window, cx);
8453 });
8454
8455 cx.assert_editor_state(indoc!(
8456 r#"ab«cdˇ»
8457 «eˇ»f«ghˇ»
8458 «iˇ»jkl
8459 «mˇ»nop"#
8460 ));
8461
8462 // add single selection in middle - simulate opt drag
8463 cx.update_editor(|editor, window, cx| {
8464 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8465 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8466 editor.update_selection(
8467 DisplayPoint::new(DisplayRow(2), 4),
8468 0,
8469 gpui::Point::<f32>::default(),
8470 window,
8471 cx,
8472 );
8473 editor.end_selection(window, cx);
8474 });
8475
8476 cx.assert_editor_state(indoc!(
8477 r#"ab«cdˇ»
8478 «eˇ»f«ghˇ»
8479 «iˇ»jk«lˇ»
8480 «mˇ»nop"#
8481 ));
8482
8483 cx.update_editor(|editor, window, cx| {
8484 editor.add_selection_below(&Default::default(), window, cx);
8485 });
8486
8487 // test new added selection expands below, others shrinks from above
8488 cx.assert_editor_state(indoc!(
8489 r#"abcd
8490 ef«ghˇ»
8491 «iˇ»jk«lˇ»
8492 «mˇ»no«pˇ»"#
8493 ));
8494}
8495
8496#[gpui::test]
8497async fn test_select_next(cx: &mut TestAppContext) {
8498 init_test(cx, |_| {});
8499 let mut cx = EditorTestContext::new(cx).await;
8500
8501 // Enable case sensitive search.
8502 update_test_editor_settings(&mut cx, |settings| {
8503 let mut search_settings = SearchSettingsContent::default();
8504 search_settings.case_sensitive = Some(true);
8505 settings.search = Some(search_settings);
8506 });
8507
8508 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8509
8510 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8511 .unwrap();
8512 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8513
8514 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8515 .unwrap();
8516 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8517
8518 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8519 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8520
8521 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8522 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8523
8524 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8525 .unwrap();
8526 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8527
8528 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8529 .unwrap();
8530 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8531
8532 // Test selection direction should be preserved
8533 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8534
8535 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8536 .unwrap();
8537 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8538
8539 // Test case sensitivity
8540 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8541 cx.update_editor(|e, window, cx| {
8542 e.select_next(&SelectNext::default(), window, cx).unwrap();
8543 });
8544 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8545
8546 // Disable case sensitive search.
8547 update_test_editor_settings(&mut cx, |settings| {
8548 let mut search_settings = SearchSettingsContent::default();
8549 search_settings.case_sensitive = Some(false);
8550 settings.search = Some(search_settings);
8551 });
8552
8553 cx.set_state("«ˇfoo»\nFOO\nFoo");
8554 cx.update_editor(|e, window, cx| {
8555 e.select_next(&SelectNext::default(), window, cx).unwrap();
8556 e.select_next(&SelectNext::default(), window, cx).unwrap();
8557 });
8558 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8559}
8560
8561#[gpui::test]
8562async fn test_select_all_matches(cx: &mut TestAppContext) {
8563 init_test(cx, |_| {});
8564 let mut cx = EditorTestContext::new(cx).await;
8565
8566 // Enable case sensitive search.
8567 update_test_editor_settings(&mut cx, |settings| {
8568 let mut search_settings = SearchSettingsContent::default();
8569 search_settings.case_sensitive = Some(true);
8570 settings.search = Some(search_settings);
8571 });
8572
8573 // Test caret-only selections
8574 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8575 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8576 .unwrap();
8577 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8578
8579 // Test left-to-right selections
8580 cx.set_state("abc\n«abcˇ»\nabc");
8581 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8582 .unwrap();
8583 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8584
8585 // Test right-to-left selections
8586 cx.set_state("abc\n«ˇabc»\nabc");
8587 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8588 .unwrap();
8589 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8590
8591 // Test selecting whitespace with caret selection
8592 cx.set_state("abc\nˇ abc\nabc");
8593 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8594 .unwrap();
8595 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8596
8597 // Test selecting whitespace with left-to-right selection
8598 cx.set_state("abc\n«ˇ »abc\nabc");
8599 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8600 .unwrap();
8601 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8602
8603 // Test no matches with right-to-left selection
8604 cx.set_state("abc\n« ˇ»abc\nabc");
8605 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8606 .unwrap();
8607 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8608
8609 // Test with a single word and clip_at_line_ends=true (#29823)
8610 cx.set_state("aˇbc");
8611 cx.update_editor(|e, window, cx| {
8612 e.set_clip_at_line_ends(true, cx);
8613 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8614 e.set_clip_at_line_ends(false, cx);
8615 });
8616 cx.assert_editor_state("«abcˇ»");
8617
8618 // Test case sensitivity
8619 cx.set_state("fˇoo\nFOO\nFoo");
8620 cx.update_editor(|e, window, cx| {
8621 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8622 });
8623 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8624
8625 // Disable case sensitive search.
8626 update_test_editor_settings(&mut cx, |settings| {
8627 let mut search_settings = SearchSettingsContent::default();
8628 search_settings.case_sensitive = Some(false);
8629 settings.search = Some(search_settings);
8630 });
8631
8632 cx.set_state("fˇoo\nFOO\nFoo");
8633 cx.update_editor(|e, window, cx| {
8634 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8635 });
8636 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8637}
8638
8639#[gpui::test]
8640async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8641 init_test(cx, |_| {});
8642
8643 let mut cx = EditorTestContext::new(cx).await;
8644
8645 let large_body_1 = "\nd".repeat(200);
8646 let large_body_2 = "\ne".repeat(200);
8647
8648 cx.set_state(&format!(
8649 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8650 ));
8651 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8652 let scroll_position = editor.scroll_position(cx);
8653 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8654 scroll_position
8655 });
8656
8657 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8658 .unwrap();
8659 cx.assert_editor_state(&format!(
8660 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8661 ));
8662 let scroll_position_after_selection =
8663 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8664 assert_eq!(
8665 initial_scroll_position, scroll_position_after_selection,
8666 "Scroll position should not change after selecting all matches"
8667 );
8668}
8669
8670#[gpui::test]
8671async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8672 init_test(cx, |_| {});
8673
8674 let mut cx = EditorLspTestContext::new_rust(
8675 lsp::ServerCapabilities {
8676 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8677 ..Default::default()
8678 },
8679 cx,
8680 )
8681 .await;
8682
8683 cx.set_state(indoc! {"
8684 line 1
8685 line 2
8686 linˇe 3
8687 line 4
8688 line 5
8689 "});
8690
8691 // Make an edit
8692 cx.update_editor(|editor, window, cx| {
8693 editor.handle_input("X", window, cx);
8694 });
8695
8696 // Move cursor to a different position
8697 cx.update_editor(|editor, window, cx| {
8698 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8699 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8700 });
8701 });
8702
8703 cx.assert_editor_state(indoc! {"
8704 line 1
8705 line 2
8706 linXe 3
8707 line 4
8708 liˇne 5
8709 "});
8710
8711 cx.lsp
8712 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8713 Ok(Some(vec![lsp::TextEdit::new(
8714 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8715 "PREFIX ".to_string(),
8716 )]))
8717 });
8718
8719 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8720 .unwrap()
8721 .await
8722 .unwrap();
8723
8724 cx.assert_editor_state(indoc! {"
8725 PREFIX line 1
8726 line 2
8727 linXe 3
8728 line 4
8729 liˇne 5
8730 "});
8731
8732 // Undo formatting
8733 cx.update_editor(|editor, window, cx| {
8734 editor.undo(&Default::default(), window, cx);
8735 });
8736
8737 // Verify cursor moved back to position after edit
8738 cx.assert_editor_state(indoc! {"
8739 line 1
8740 line 2
8741 linXˇe 3
8742 line 4
8743 line 5
8744 "});
8745}
8746
8747#[gpui::test]
8748async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8749 init_test(cx, |_| {});
8750
8751 let mut cx = EditorTestContext::new(cx).await;
8752
8753 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
8754 cx.update_editor(|editor, window, cx| {
8755 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8756 });
8757
8758 cx.set_state(indoc! {"
8759 line 1
8760 line 2
8761 linˇe 3
8762 line 4
8763 line 5
8764 line 6
8765 line 7
8766 line 8
8767 line 9
8768 line 10
8769 "});
8770
8771 let snapshot = cx.buffer_snapshot();
8772 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8773
8774 cx.update(|_, cx| {
8775 provider.update(cx, |provider, _| {
8776 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
8777 id: None,
8778 edits: vec![(edit_position..edit_position, "X".into())],
8779 edit_preview: None,
8780 }))
8781 })
8782 });
8783
8784 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8785 cx.update_editor(|editor, window, cx| {
8786 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8787 });
8788
8789 cx.assert_editor_state(indoc! {"
8790 line 1
8791 line 2
8792 lineXˇ 3
8793 line 4
8794 line 5
8795 line 6
8796 line 7
8797 line 8
8798 line 9
8799 line 10
8800 "});
8801
8802 cx.update_editor(|editor, window, cx| {
8803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8804 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8805 });
8806 });
8807
8808 cx.assert_editor_state(indoc! {"
8809 line 1
8810 line 2
8811 lineX 3
8812 line 4
8813 line 5
8814 line 6
8815 line 7
8816 line 8
8817 line 9
8818 liˇne 10
8819 "});
8820
8821 cx.update_editor(|editor, window, cx| {
8822 editor.undo(&Default::default(), window, cx);
8823 });
8824
8825 cx.assert_editor_state(indoc! {"
8826 line 1
8827 line 2
8828 lineˇ 3
8829 line 4
8830 line 5
8831 line 6
8832 line 7
8833 line 8
8834 line 9
8835 line 10
8836 "});
8837}
8838
8839#[gpui::test]
8840async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8841 init_test(cx, |_| {});
8842
8843 let mut cx = EditorTestContext::new(cx).await;
8844 cx.set_state(
8845 r#"let foo = 2;
8846lˇet foo = 2;
8847let fooˇ = 2;
8848let foo = 2;
8849let foo = ˇ2;"#,
8850 );
8851
8852 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8853 .unwrap();
8854 cx.assert_editor_state(
8855 r#"let foo = 2;
8856«letˇ» foo = 2;
8857let «fooˇ» = 2;
8858let foo = 2;
8859let foo = «2ˇ»;"#,
8860 );
8861
8862 // noop for multiple selections with different contents
8863 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8864 .unwrap();
8865 cx.assert_editor_state(
8866 r#"let foo = 2;
8867«letˇ» foo = 2;
8868let «fooˇ» = 2;
8869let foo = 2;
8870let foo = «2ˇ»;"#,
8871 );
8872
8873 // Test last selection direction should be preserved
8874 cx.set_state(
8875 r#"let foo = 2;
8876let foo = 2;
8877let «fooˇ» = 2;
8878let «ˇfoo» = 2;
8879let foo = 2;"#,
8880 );
8881
8882 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8883 .unwrap();
8884 cx.assert_editor_state(
8885 r#"let foo = 2;
8886let foo = 2;
8887let «fooˇ» = 2;
8888let «ˇfoo» = 2;
8889let «ˇfoo» = 2;"#,
8890 );
8891}
8892
8893#[gpui::test]
8894async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8895 init_test(cx, |_| {});
8896
8897 let mut cx =
8898 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8899
8900 cx.assert_editor_state(indoc! {"
8901 ˇbbb
8902 ccc
8903
8904 bbb
8905 ccc
8906 "});
8907 cx.dispatch_action(SelectPrevious::default());
8908 cx.assert_editor_state(indoc! {"
8909 «bbbˇ»
8910 ccc
8911
8912 bbb
8913 ccc
8914 "});
8915 cx.dispatch_action(SelectPrevious::default());
8916 cx.assert_editor_state(indoc! {"
8917 «bbbˇ»
8918 ccc
8919
8920 «bbbˇ»
8921 ccc
8922 "});
8923}
8924
8925#[gpui::test]
8926async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8927 init_test(cx, |_| {});
8928
8929 let mut cx = EditorTestContext::new(cx).await;
8930 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8931
8932 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8933 .unwrap();
8934 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8935
8936 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8937 .unwrap();
8938 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8939
8940 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8941 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8942
8943 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8944 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8945
8946 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8947 .unwrap();
8948 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8949
8950 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8951 .unwrap();
8952 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8953}
8954
8955#[gpui::test]
8956async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8957 init_test(cx, |_| {});
8958
8959 let mut cx = EditorTestContext::new(cx).await;
8960 cx.set_state("aˇ");
8961
8962 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8963 .unwrap();
8964 cx.assert_editor_state("«aˇ»");
8965 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8966 .unwrap();
8967 cx.assert_editor_state("«aˇ»");
8968}
8969
8970#[gpui::test]
8971async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8972 init_test(cx, |_| {});
8973
8974 let mut cx = EditorTestContext::new(cx).await;
8975 cx.set_state(
8976 r#"let foo = 2;
8977lˇet foo = 2;
8978let fooˇ = 2;
8979let foo = 2;
8980let foo = ˇ2;"#,
8981 );
8982
8983 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8984 .unwrap();
8985 cx.assert_editor_state(
8986 r#"let foo = 2;
8987«letˇ» foo = 2;
8988let «fooˇ» = 2;
8989let foo = 2;
8990let foo = «2ˇ»;"#,
8991 );
8992
8993 // noop for multiple selections with different contents
8994 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8995 .unwrap();
8996 cx.assert_editor_state(
8997 r#"let foo = 2;
8998«letˇ» foo = 2;
8999let «fooˇ» = 2;
9000let foo = 2;
9001let foo = «2ˇ»;"#,
9002 );
9003}
9004
9005#[gpui::test]
9006async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9007 init_test(cx, |_| {});
9008 let mut cx = EditorTestContext::new(cx).await;
9009
9010 // Enable case sensitive search.
9011 update_test_editor_settings(&mut cx, |settings| {
9012 let mut search_settings = SearchSettingsContent::default();
9013 search_settings.case_sensitive = Some(true);
9014 settings.search = Some(search_settings);
9015 });
9016
9017 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9018
9019 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9020 .unwrap();
9021 // selection direction is preserved
9022 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9023
9024 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9025 .unwrap();
9026 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9027
9028 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9029 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9030
9031 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9032 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9033
9034 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9035 .unwrap();
9036 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9037
9038 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9039 .unwrap();
9040 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9041
9042 // Test case sensitivity
9043 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9044 cx.update_editor(|e, window, cx| {
9045 e.select_previous(&SelectPrevious::default(), window, cx)
9046 .unwrap();
9047 e.select_previous(&SelectPrevious::default(), window, cx)
9048 .unwrap();
9049 });
9050 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9051
9052 // Disable case sensitive search.
9053 update_test_editor_settings(&mut cx, |settings| {
9054 let mut search_settings = SearchSettingsContent::default();
9055 search_settings.case_sensitive = Some(false);
9056 settings.search = Some(search_settings);
9057 });
9058
9059 cx.set_state("foo\nFOO\n«ˇFoo»");
9060 cx.update_editor(|e, window, cx| {
9061 e.select_previous(&SelectPrevious::default(), window, cx)
9062 .unwrap();
9063 e.select_previous(&SelectPrevious::default(), window, cx)
9064 .unwrap();
9065 });
9066 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9067}
9068
9069#[gpui::test]
9070async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9071 init_test(cx, |_| {});
9072
9073 let language = Arc::new(Language::new(
9074 LanguageConfig::default(),
9075 Some(tree_sitter_rust::LANGUAGE.into()),
9076 ));
9077
9078 let text = r#"
9079 use mod1::mod2::{mod3, mod4};
9080
9081 fn fn_1(param1: bool, param2: &str) {
9082 let var1 = "text";
9083 }
9084 "#
9085 .unindent();
9086
9087 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9088 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9089 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9090
9091 editor
9092 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9093 .await;
9094
9095 editor.update_in(cx, |editor, window, cx| {
9096 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9097 s.select_display_ranges([
9098 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9099 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9100 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9101 ]);
9102 });
9103 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9104 });
9105 editor.update(cx, |editor, cx| {
9106 assert_text_with_selections(
9107 editor,
9108 indoc! {r#"
9109 use mod1::mod2::{mod3, «mod4ˇ»};
9110
9111 fn fn_1«ˇ(param1: bool, param2: &str)» {
9112 let var1 = "«ˇtext»";
9113 }
9114 "#},
9115 cx,
9116 );
9117 });
9118
9119 editor.update_in(cx, |editor, window, cx| {
9120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9121 });
9122 editor.update(cx, |editor, cx| {
9123 assert_text_with_selections(
9124 editor,
9125 indoc! {r#"
9126 use mod1::mod2::«{mod3, mod4}ˇ»;
9127
9128 «ˇfn fn_1(param1: bool, param2: &str) {
9129 let var1 = "text";
9130 }»
9131 "#},
9132 cx,
9133 );
9134 });
9135
9136 editor.update_in(cx, |editor, window, cx| {
9137 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9138 });
9139 assert_eq!(
9140 editor.update(cx, |editor, cx| editor
9141 .selections
9142 .display_ranges(&editor.display_snapshot(cx))),
9143 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9144 );
9145
9146 // Trying to expand the selected syntax node one more time has no effect.
9147 editor.update_in(cx, |editor, window, cx| {
9148 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9149 });
9150 assert_eq!(
9151 editor.update(cx, |editor, cx| editor
9152 .selections
9153 .display_ranges(&editor.display_snapshot(cx))),
9154 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9155 );
9156
9157 editor.update_in(cx, |editor, window, cx| {
9158 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9159 });
9160 editor.update(cx, |editor, cx| {
9161 assert_text_with_selections(
9162 editor,
9163 indoc! {r#"
9164 use mod1::mod2::«{mod3, mod4}ˇ»;
9165
9166 «ˇfn fn_1(param1: bool, param2: &str) {
9167 let var1 = "text";
9168 }»
9169 "#},
9170 cx,
9171 );
9172 });
9173
9174 editor.update_in(cx, |editor, window, cx| {
9175 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9176 });
9177 editor.update(cx, |editor, cx| {
9178 assert_text_with_selections(
9179 editor,
9180 indoc! {r#"
9181 use mod1::mod2::{mod3, «mod4ˇ»};
9182
9183 fn fn_1«ˇ(param1: bool, param2: &str)» {
9184 let var1 = "«ˇtext»";
9185 }
9186 "#},
9187 cx,
9188 );
9189 });
9190
9191 editor.update_in(cx, |editor, window, cx| {
9192 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9193 });
9194 editor.update(cx, |editor, cx| {
9195 assert_text_with_selections(
9196 editor,
9197 indoc! {r#"
9198 use mod1::mod2::{mod3, moˇd4};
9199
9200 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9201 let var1 = "teˇxt";
9202 }
9203 "#},
9204 cx,
9205 );
9206 });
9207
9208 // Trying to shrink the selected syntax node one more time has no effect.
9209 editor.update_in(cx, |editor, window, cx| {
9210 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9211 });
9212 editor.update_in(cx, |editor, _, cx| {
9213 assert_text_with_selections(
9214 editor,
9215 indoc! {r#"
9216 use mod1::mod2::{mod3, moˇd4};
9217
9218 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9219 let var1 = "teˇxt";
9220 }
9221 "#},
9222 cx,
9223 );
9224 });
9225
9226 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9227 // a fold.
9228 editor.update_in(cx, |editor, window, cx| {
9229 editor.fold_creases(
9230 vec![
9231 Crease::simple(
9232 Point::new(0, 21)..Point::new(0, 24),
9233 FoldPlaceholder::test(),
9234 ),
9235 Crease::simple(
9236 Point::new(3, 20)..Point::new(3, 22),
9237 FoldPlaceholder::test(),
9238 ),
9239 ],
9240 true,
9241 window,
9242 cx,
9243 );
9244 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9245 });
9246 editor.update(cx, |editor, cx| {
9247 assert_text_with_selections(
9248 editor,
9249 indoc! {r#"
9250 use mod1::mod2::«{mod3, mod4}ˇ»;
9251
9252 fn fn_1«ˇ(param1: bool, param2: &str)» {
9253 let var1 = "«ˇtext»";
9254 }
9255 "#},
9256 cx,
9257 );
9258 });
9259}
9260
9261#[gpui::test]
9262async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9263 init_test(cx, |_| {});
9264
9265 let language = Arc::new(Language::new(
9266 LanguageConfig::default(),
9267 Some(tree_sitter_rust::LANGUAGE.into()),
9268 ));
9269
9270 let text = "let a = 2;";
9271
9272 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9273 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9274 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9275
9276 editor
9277 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9278 .await;
9279
9280 // Test case 1: Cursor at end of word
9281 editor.update_in(cx, |editor, window, cx| {
9282 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9283 s.select_display_ranges([
9284 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9285 ]);
9286 });
9287 });
9288 editor.update(cx, |editor, cx| {
9289 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9290 });
9291 editor.update_in(cx, |editor, window, cx| {
9292 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9293 });
9294 editor.update(cx, |editor, cx| {
9295 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9296 });
9297 editor.update_in(cx, |editor, window, cx| {
9298 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9299 });
9300 editor.update(cx, |editor, cx| {
9301 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9302 });
9303
9304 // Test case 2: Cursor at end of statement
9305 editor.update_in(cx, |editor, window, cx| {
9306 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9307 s.select_display_ranges([
9308 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9309 ]);
9310 });
9311 });
9312 editor.update(cx, |editor, cx| {
9313 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9314 });
9315 editor.update_in(cx, |editor, window, cx| {
9316 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9317 });
9318 editor.update(cx, |editor, cx| {
9319 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9320 });
9321}
9322
9323#[gpui::test]
9324async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9325 init_test(cx, |_| {});
9326
9327 let language = Arc::new(Language::new(
9328 LanguageConfig {
9329 name: "JavaScript".into(),
9330 ..Default::default()
9331 },
9332 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9333 ));
9334
9335 let text = r#"
9336 let a = {
9337 key: "value",
9338 };
9339 "#
9340 .unindent();
9341
9342 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9343 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9344 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9345
9346 editor
9347 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9348 .await;
9349
9350 // Test case 1: 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(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
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 // Test case 2: Cursor after ':'
9385 editor.update_in(cx, |editor, window, cx| {
9386 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9387 s.select_display_ranges([
9388 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9389 ]);
9390 });
9391 });
9392 editor.update(cx, |editor, cx| {
9393 assert_text_with_selections(
9394 editor,
9395 indoc! {r#"
9396 let a = {
9397 key:ˇ "value",
9398 };
9399 "#},
9400 cx,
9401 );
9402 });
9403 editor.update_in(cx, |editor, window, cx| {
9404 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9405 });
9406 editor.update(cx, |editor, cx| {
9407 assert_text_with_selections(
9408 editor,
9409 indoc! {r#"
9410 let a = {
9411 «ˇkey: "value"»,
9412 };
9413 "#},
9414 cx,
9415 );
9416 });
9417 editor.update_in(cx, |editor, window, cx| {
9418 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9419 });
9420 editor.update(cx, |editor, cx| {
9421 assert_text_with_selections(
9422 editor,
9423 indoc! {r#"
9424 let a = «ˇ{
9425 key: "value",
9426 }»;
9427 "#},
9428 cx,
9429 );
9430 });
9431
9432 // Test case 3: Cursor after ','
9433 editor.update_in(cx, |editor, window, cx| {
9434 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9435 s.select_display_ranges([
9436 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9437 ]);
9438 });
9439 });
9440 editor.update(cx, |editor, cx| {
9441 assert_text_with_selections(
9442 editor,
9443 indoc! {r#"
9444 let a = {
9445 key: "value",ˇ
9446 };
9447 "#},
9448 cx,
9449 );
9450 });
9451 editor.update_in(cx, |editor, window, cx| {
9452 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9453 });
9454 editor.update(cx, |editor, cx| {
9455 assert_text_with_selections(
9456 editor,
9457 indoc! {r#"
9458 let a = «ˇ{
9459 key: "value",
9460 }»;
9461 "#},
9462 cx,
9463 );
9464 });
9465
9466 // Test case 4: Cursor after ';'
9467 editor.update_in(cx, |editor, window, cx| {
9468 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9469 s.select_display_ranges([
9470 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9471 ]);
9472 });
9473 });
9474 editor.update(cx, |editor, cx| {
9475 assert_text_with_selections(
9476 editor,
9477 indoc! {r#"
9478 let a = {
9479 key: "value",
9480 };ˇ
9481 "#},
9482 cx,
9483 );
9484 });
9485 editor.update_in(cx, |editor, window, cx| {
9486 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9487 });
9488 editor.update(cx, |editor, cx| {
9489 assert_text_with_selections(
9490 editor,
9491 indoc! {r#"
9492 «ˇlet a = {
9493 key: "value",
9494 };
9495 »"#},
9496 cx,
9497 );
9498 });
9499}
9500
9501#[gpui::test]
9502async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9503 init_test(cx, |_| {});
9504
9505 let language = Arc::new(Language::new(
9506 LanguageConfig::default(),
9507 Some(tree_sitter_rust::LANGUAGE.into()),
9508 ));
9509
9510 let text = r#"
9511 use mod1::mod2::{mod3, mod4};
9512
9513 fn fn_1(param1: bool, param2: &str) {
9514 let var1 = "hello world";
9515 }
9516 "#
9517 .unindent();
9518
9519 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9520 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9521 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9522
9523 editor
9524 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9525 .await;
9526
9527 // Test 1: Cursor on a letter of a string word
9528 editor.update_in(cx, |editor, window, cx| {
9529 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9530 s.select_display_ranges([
9531 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9532 ]);
9533 });
9534 });
9535 editor.update_in(cx, |editor, window, cx| {
9536 assert_text_with_selections(
9537 editor,
9538 indoc! {r#"
9539 use mod1::mod2::{mod3, mod4};
9540
9541 fn fn_1(param1: bool, param2: &str) {
9542 let var1 = "hˇello world";
9543 }
9544 "#},
9545 cx,
9546 );
9547 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9548 assert_text_with_selections(
9549 editor,
9550 indoc! {r#"
9551 use mod1::mod2::{mod3, mod4};
9552
9553 fn fn_1(param1: bool, param2: &str) {
9554 let var1 = "«ˇhello» world";
9555 }
9556 "#},
9557 cx,
9558 );
9559 });
9560
9561 // Test 2: Partial selection within a word
9562 editor.update_in(cx, |editor, window, cx| {
9563 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9564 s.select_display_ranges([
9565 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9566 ]);
9567 });
9568 });
9569 editor.update_in(cx, |editor, window, cx| {
9570 assert_text_with_selections(
9571 editor,
9572 indoc! {r#"
9573 use mod1::mod2::{mod3, mod4};
9574
9575 fn fn_1(param1: bool, param2: &str) {
9576 let var1 = "h«elˇ»lo world";
9577 }
9578 "#},
9579 cx,
9580 );
9581 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9582 assert_text_with_selections(
9583 editor,
9584 indoc! {r#"
9585 use mod1::mod2::{mod3, mod4};
9586
9587 fn fn_1(param1: bool, param2: &str) {
9588 let var1 = "«ˇhello» world";
9589 }
9590 "#},
9591 cx,
9592 );
9593 });
9594
9595 // Test 3: Complete word already selected
9596 editor.update_in(cx, |editor, window, cx| {
9597 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9598 s.select_display_ranges([
9599 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9600 ]);
9601 });
9602 });
9603 editor.update_in(cx, |editor, window, cx| {
9604 assert_text_with_selections(
9605 editor,
9606 indoc! {r#"
9607 use mod1::mod2::{mod3, mod4};
9608
9609 fn fn_1(param1: bool, param2: &str) {
9610 let var1 = "«helloˇ» world";
9611 }
9612 "#},
9613 cx,
9614 );
9615 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9616 assert_text_with_selections(
9617 editor,
9618 indoc! {r#"
9619 use mod1::mod2::{mod3, mod4};
9620
9621 fn fn_1(param1: bool, param2: &str) {
9622 let var1 = "«hello worldˇ»";
9623 }
9624 "#},
9625 cx,
9626 );
9627 });
9628
9629 // Test 4: Selection spanning across words
9630 editor.update_in(cx, |editor, window, cx| {
9631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9632 s.select_display_ranges([
9633 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9634 ]);
9635 });
9636 });
9637 editor.update_in(cx, |editor, window, cx| {
9638 assert_text_with_selections(
9639 editor,
9640 indoc! {r#"
9641 use mod1::mod2::{mod3, mod4};
9642
9643 fn fn_1(param1: bool, param2: &str) {
9644 let var1 = "hel«lo woˇ»rld";
9645 }
9646 "#},
9647 cx,
9648 );
9649 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9650 assert_text_with_selections(
9651 editor,
9652 indoc! {r#"
9653 use mod1::mod2::{mod3, mod4};
9654
9655 fn fn_1(param1: bool, param2: &str) {
9656 let var1 = "«ˇhello world»";
9657 }
9658 "#},
9659 cx,
9660 );
9661 });
9662
9663 // Test 5: Expansion beyond string
9664 editor.update_in(cx, |editor, window, cx| {
9665 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9666 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9667 assert_text_with_selections(
9668 editor,
9669 indoc! {r#"
9670 use mod1::mod2::{mod3, mod4};
9671
9672 fn fn_1(param1: bool, param2: &str) {
9673 «ˇlet var1 = "hello world";»
9674 }
9675 "#},
9676 cx,
9677 );
9678 });
9679}
9680
9681#[gpui::test]
9682async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9683 init_test(cx, |_| {});
9684
9685 let mut cx = EditorTestContext::new(cx).await;
9686
9687 let language = Arc::new(Language::new(
9688 LanguageConfig::default(),
9689 Some(tree_sitter_rust::LANGUAGE.into()),
9690 ));
9691
9692 cx.update_buffer(|buffer, cx| {
9693 buffer.set_language(Some(language), cx);
9694 });
9695
9696 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9697 cx.update_editor(|editor, window, cx| {
9698 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9699 });
9700
9701 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9702
9703 cx.set_state(indoc! { r#"fn a() {
9704 // what
9705 // a
9706 // ˇlong
9707 // method
9708 // I
9709 // sure
9710 // hope
9711 // it
9712 // works
9713 }"# });
9714
9715 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9716 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9717 cx.update(|_, cx| {
9718 multi_buffer.update(cx, |multi_buffer, cx| {
9719 multi_buffer.set_excerpts_for_path(
9720 PathKey::for_buffer(&buffer, cx),
9721 buffer,
9722 [Point::new(1, 0)..Point::new(1, 0)],
9723 3,
9724 cx,
9725 );
9726 });
9727 });
9728
9729 let editor2 = cx.new_window_entity(|window, cx| {
9730 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9731 });
9732
9733 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9734 cx.update_editor(|editor, window, cx| {
9735 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9736 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9737 })
9738 });
9739
9740 cx.assert_editor_state(indoc! { "
9741 fn a() {
9742 // what
9743 // a
9744 ˇ // long
9745 // method"});
9746
9747 cx.update_editor(|editor, window, cx| {
9748 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9749 });
9750
9751 // Although we could potentially make the action work when the syntax node
9752 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9753 // did. Maybe we could also expand the excerpt to contain the range?
9754 cx.assert_editor_state(indoc! { "
9755 fn a() {
9756 // what
9757 // a
9758 ˇ // long
9759 // method"});
9760}
9761
9762#[gpui::test]
9763async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9764 init_test(cx, |_| {});
9765
9766 let base_text = r#"
9767 impl A {
9768 // this is an uncommitted comment
9769
9770 fn b() {
9771 c();
9772 }
9773
9774 // this is another uncommitted comment
9775
9776 fn d() {
9777 // e
9778 // f
9779 }
9780 }
9781
9782 fn g() {
9783 // h
9784 }
9785 "#
9786 .unindent();
9787
9788 let text = r#"
9789 ˇimpl A {
9790
9791 fn b() {
9792 c();
9793 }
9794
9795 fn d() {
9796 // e
9797 // f
9798 }
9799 }
9800
9801 fn g() {
9802 // h
9803 }
9804 "#
9805 .unindent();
9806
9807 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9808 cx.set_state(&text);
9809 cx.set_head_text(&base_text);
9810 cx.update_editor(|editor, window, cx| {
9811 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9812 });
9813
9814 cx.assert_state_with_diff(
9815 "
9816 ˇimpl A {
9817 - // this is an uncommitted comment
9818
9819 fn b() {
9820 c();
9821 }
9822
9823 - // this is another uncommitted comment
9824 -
9825 fn d() {
9826 // e
9827 // f
9828 }
9829 }
9830
9831 fn g() {
9832 // h
9833 }
9834 "
9835 .unindent(),
9836 );
9837
9838 let expected_display_text = "
9839 impl A {
9840 // this is an uncommitted comment
9841
9842 fn b() {
9843 ⋯
9844 }
9845
9846 // this is another uncommitted comment
9847
9848 fn d() {
9849 ⋯
9850 }
9851 }
9852
9853 fn g() {
9854 ⋯
9855 }
9856 "
9857 .unindent();
9858
9859 cx.update_editor(|editor, window, cx| {
9860 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9861 assert_eq!(editor.display_text(cx), expected_display_text);
9862 });
9863}
9864
9865#[gpui::test]
9866async fn test_autoindent(cx: &mut TestAppContext) {
9867 init_test(cx, |_| {});
9868
9869 let language = Arc::new(
9870 Language::new(
9871 LanguageConfig {
9872 brackets: BracketPairConfig {
9873 pairs: vec![
9874 BracketPair {
9875 start: "{".to_string(),
9876 end: "}".to_string(),
9877 close: false,
9878 surround: false,
9879 newline: true,
9880 },
9881 BracketPair {
9882 start: "(".to_string(),
9883 end: ")".to_string(),
9884 close: false,
9885 surround: false,
9886 newline: true,
9887 },
9888 ],
9889 ..Default::default()
9890 },
9891 ..Default::default()
9892 },
9893 Some(tree_sitter_rust::LANGUAGE.into()),
9894 )
9895 .with_indents_query(
9896 r#"
9897 (_ "(" ")" @end) @indent
9898 (_ "{" "}" @end) @indent
9899 "#,
9900 )
9901 .unwrap(),
9902 );
9903
9904 let text = "fn a() {}";
9905
9906 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9907 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9908 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9909 editor
9910 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9911 .await;
9912
9913 editor.update_in(cx, |editor, window, cx| {
9914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9915 s.select_ranges([
9916 MultiBufferOffset(5)..MultiBufferOffset(5),
9917 MultiBufferOffset(8)..MultiBufferOffset(8),
9918 MultiBufferOffset(9)..MultiBufferOffset(9),
9919 ])
9920 });
9921 editor.newline(&Newline, window, cx);
9922 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9923 assert_eq!(
9924 editor.selections.ranges(&editor.display_snapshot(cx)),
9925 &[
9926 Point::new(1, 4)..Point::new(1, 4),
9927 Point::new(3, 4)..Point::new(3, 4),
9928 Point::new(5, 0)..Point::new(5, 0)
9929 ]
9930 );
9931 });
9932}
9933
9934#[gpui::test]
9935async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9936 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9937
9938 let language = Arc::new(
9939 Language::new(
9940 LanguageConfig {
9941 brackets: BracketPairConfig {
9942 pairs: vec![
9943 BracketPair {
9944 start: "{".to_string(),
9945 end: "}".to_string(),
9946 close: false,
9947 surround: false,
9948 newline: true,
9949 },
9950 BracketPair {
9951 start: "(".to_string(),
9952 end: ")".to_string(),
9953 close: false,
9954 surround: false,
9955 newline: true,
9956 },
9957 ],
9958 ..Default::default()
9959 },
9960 ..Default::default()
9961 },
9962 Some(tree_sitter_rust::LANGUAGE.into()),
9963 )
9964 .with_indents_query(
9965 r#"
9966 (_ "(" ")" @end) @indent
9967 (_ "{" "}" @end) @indent
9968 "#,
9969 )
9970 .unwrap(),
9971 );
9972
9973 let text = "fn a() {}";
9974
9975 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9976 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9977 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9978 editor
9979 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9980 .await;
9981
9982 editor.update_in(cx, |editor, window, cx| {
9983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9984 s.select_ranges([
9985 MultiBufferOffset(5)..MultiBufferOffset(5),
9986 MultiBufferOffset(8)..MultiBufferOffset(8),
9987 MultiBufferOffset(9)..MultiBufferOffset(9),
9988 ])
9989 });
9990 editor.newline(&Newline, window, cx);
9991 assert_eq!(
9992 editor.text(cx),
9993 indoc!(
9994 "
9995 fn a(
9996
9997 ) {
9998
9999 }
10000 "
10001 )
10002 );
10003 assert_eq!(
10004 editor.selections.ranges(&editor.display_snapshot(cx)),
10005 &[
10006 Point::new(1, 0)..Point::new(1, 0),
10007 Point::new(3, 0)..Point::new(3, 0),
10008 Point::new(5, 0)..Point::new(5, 0)
10009 ]
10010 );
10011 });
10012}
10013
10014#[gpui::test]
10015async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10016 init_test(cx, |settings| {
10017 settings.defaults.auto_indent = Some(true);
10018 settings.languages.0.insert(
10019 "python".into(),
10020 LanguageSettingsContent {
10021 auto_indent: Some(false),
10022 ..Default::default()
10023 },
10024 );
10025 });
10026
10027 let mut cx = EditorTestContext::new(cx).await;
10028
10029 let injected_language = Arc::new(
10030 Language::new(
10031 LanguageConfig {
10032 brackets: BracketPairConfig {
10033 pairs: vec![
10034 BracketPair {
10035 start: "{".to_string(),
10036 end: "}".to_string(),
10037 close: false,
10038 surround: false,
10039 newline: true,
10040 },
10041 BracketPair {
10042 start: "(".to_string(),
10043 end: ")".to_string(),
10044 close: true,
10045 surround: false,
10046 newline: true,
10047 },
10048 ],
10049 ..Default::default()
10050 },
10051 name: "python".into(),
10052 ..Default::default()
10053 },
10054 Some(tree_sitter_python::LANGUAGE.into()),
10055 )
10056 .with_indents_query(
10057 r#"
10058 (_ "(" ")" @end) @indent
10059 (_ "{" "}" @end) @indent
10060 "#,
10061 )
10062 .unwrap(),
10063 );
10064
10065 let language = Arc::new(
10066 Language::new(
10067 LanguageConfig {
10068 brackets: BracketPairConfig {
10069 pairs: vec![
10070 BracketPair {
10071 start: "{".to_string(),
10072 end: "}".to_string(),
10073 close: false,
10074 surround: false,
10075 newline: true,
10076 },
10077 BracketPair {
10078 start: "(".to_string(),
10079 end: ")".to_string(),
10080 close: true,
10081 surround: false,
10082 newline: true,
10083 },
10084 ],
10085 ..Default::default()
10086 },
10087 name: LanguageName::new_static("rust"),
10088 ..Default::default()
10089 },
10090 Some(tree_sitter_rust::LANGUAGE.into()),
10091 )
10092 .with_indents_query(
10093 r#"
10094 (_ "(" ")" @end) @indent
10095 (_ "{" "}" @end) @indent
10096 "#,
10097 )
10098 .unwrap()
10099 .with_injection_query(
10100 r#"
10101 (macro_invocation
10102 macro: (identifier) @_macro_name
10103 (token_tree) @injection.content
10104 (#set! injection.language "python"))
10105 "#,
10106 )
10107 .unwrap(),
10108 );
10109
10110 cx.language_registry().add(injected_language);
10111 cx.language_registry().add(language.clone());
10112
10113 cx.update_buffer(|buffer, cx| {
10114 buffer.set_language(Some(language), cx);
10115 });
10116
10117 cx.set_state(r#"struct A {ˇ}"#);
10118
10119 cx.update_editor(|editor, window, cx| {
10120 editor.newline(&Default::default(), window, cx);
10121 });
10122
10123 cx.assert_editor_state(indoc!(
10124 "struct A {
10125 ˇ
10126 }"
10127 ));
10128
10129 cx.set_state(r#"select_biased!(ˇ)"#);
10130
10131 cx.update_editor(|editor, window, cx| {
10132 editor.newline(&Default::default(), window, cx);
10133 editor.handle_input("def ", window, cx);
10134 editor.handle_input("(", window, cx);
10135 editor.newline(&Default::default(), window, cx);
10136 editor.handle_input("a", window, cx);
10137 });
10138
10139 cx.assert_editor_state(indoc!(
10140 "select_biased!(
10141 def (
10142 aˇ
10143 )
10144 )"
10145 ));
10146}
10147
10148#[gpui::test]
10149async fn test_autoindent_selections(cx: &mut TestAppContext) {
10150 init_test(cx, |_| {});
10151
10152 {
10153 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10154 cx.set_state(indoc! {"
10155 impl A {
10156
10157 fn b() {}
10158
10159 «fn c() {
10160
10161 }ˇ»
10162 }
10163 "});
10164
10165 cx.update_editor(|editor, window, cx| {
10166 editor.autoindent(&Default::default(), window, cx);
10167 });
10168
10169 cx.assert_editor_state(indoc! {"
10170 impl A {
10171
10172 fn b() {}
10173
10174 «fn c() {
10175
10176 }ˇ»
10177 }
10178 "});
10179 }
10180
10181 {
10182 let mut cx = EditorTestContext::new_multibuffer(
10183 cx,
10184 [indoc! { "
10185 impl A {
10186 «
10187 // a
10188 fn b(){}
10189 »
10190 «
10191 }
10192 fn c(){}
10193 »
10194 "}],
10195 );
10196
10197 let buffer = cx.update_editor(|editor, _, cx| {
10198 let buffer = editor.buffer().update(cx, |buffer, _| {
10199 buffer.all_buffers().iter().next().unwrap().clone()
10200 });
10201 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10202 buffer
10203 });
10204
10205 cx.run_until_parked();
10206 cx.update_editor(|editor, window, cx| {
10207 editor.select_all(&Default::default(), window, cx);
10208 editor.autoindent(&Default::default(), window, cx)
10209 });
10210 cx.run_until_parked();
10211
10212 cx.update(|_, cx| {
10213 assert_eq!(
10214 buffer.read(cx).text(),
10215 indoc! { "
10216 impl A {
10217
10218 // a
10219 fn b(){}
10220
10221
10222 }
10223 fn c(){}
10224
10225 " }
10226 )
10227 });
10228 }
10229}
10230
10231#[gpui::test]
10232async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10233 init_test(cx, |_| {});
10234
10235 let mut cx = EditorTestContext::new(cx).await;
10236
10237 let language = Arc::new(Language::new(
10238 LanguageConfig {
10239 brackets: BracketPairConfig {
10240 pairs: vec![
10241 BracketPair {
10242 start: "{".to_string(),
10243 end: "}".to_string(),
10244 close: true,
10245 surround: true,
10246 newline: true,
10247 },
10248 BracketPair {
10249 start: "(".to_string(),
10250 end: ")".to_string(),
10251 close: true,
10252 surround: true,
10253 newline: true,
10254 },
10255 BracketPair {
10256 start: "/*".to_string(),
10257 end: " */".to_string(),
10258 close: true,
10259 surround: true,
10260 newline: true,
10261 },
10262 BracketPair {
10263 start: "[".to_string(),
10264 end: "]".to_string(),
10265 close: false,
10266 surround: false,
10267 newline: true,
10268 },
10269 BracketPair {
10270 start: "\"".to_string(),
10271 end: "\"".to_string(),
10272 close: true,
10273 surround: true,
10274 newline: false,
10275 },
10276 BracketPair {
10277 start: "<".to_string(),
10278 end: ">".to_string(),
10279 close: false,
10280 surround: true,
10281 newline: true,
10282 },
10283 ],
10284 ..Default::default()
10285 },
10286 autoclose_before: "})]".to_string(),
10287 ..Default::default()
10288 },
10289 Some(tree_sitter_rust::LANGUAGE.into()),
10290 ));
10291
10292 cx.language_registry().add(language.clone());
10293 cx.update_buffer(|buffer, cx| {
10294 buffer.set_language(Some(language), cx);
10295 });
10296
10297 cx.set_state(
10298 &r#"
10299 🏀ˇ
10300 εˇ
10301 ❤️ˇ
10302 "#
10303 .unindent(),
10304 );
10305
10306 // autoclose multiple nested brackets at multiple cursors
10307 cx.update_editor(|editor, window, cx| {
10308 editor.handle_input("{", window, cx);
10309 editor.handle_input("{", window, cx);
10310 editor.handle_input("{", window, cx);
10311 });
10312 cx.assert_editor_state(
10313 &"
10314 🏀{{{ˇ}}}
10315 ε{{{ˇ}}}
10316 ❤️{{{ˇ}}}
10317 "
10318 .unindent(),
10319 );
10320
10321 // insert a different closing bracket
10322 cx.update_editor(|editor, window, cx| {
10323 editor.handle_input(")", window, cx);
10324 });
10325 cx.assert_editor_state(
10326 &"
10327 🏀{{{)ˇ}}}
10328 ε{{{)ˇ}}}
10329 ❤️{{{)ˇ}}}
10330 "
10331 .unindent(),
10332 );
10333
10334 // skip over the auto-closed brackets when typing a closing bracket
10335 cx.update_editor(|editor, window, cx| {
10336 editor.move_right(&MoveRight, window, cx);
10337 editor.handle_input("}", window, cx);
10338 editor.handle_input("}", window, cx);
10339 editor.handle_input("}", window, cx);
10340 });
10341 cx.assert_editor_state(
10342 &"
10343 🏀{{{)}}}}ˇ
10344 ε{{{)}}}}ˇ
10345 ❤️{{{)}}}}ˇ
10346 "
10347 .unindent(),
10348 );
10349
10350 // autoclose multi-character pairs
10351 cx.set_state(
10352 &"
10353 ˇ
10354 ˇ
10355 "
10356 .unindent(),
10357 );
10358 cx.update_editor(|editor, window, cx| {
10359 editor.handle_input("/", window, cx);
10360 editor.handle_input("*", window, cx);
10361 });
10362 cx.assert_editor_state(
10363 &"
10364 /*ˇ */
10365 /*ˇ */
10366 "
10367 .unindent(),
10368 );
10369
10370 // one cursor autocloses a multi-character pair, one cursor
10371 // does not autoclose.
10372 cx.set_state(
10373 &"
10374 /ˇ
10375 ˇ
10376 "
10377 .unindent(),
10378 );
10379 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10380 cx.assert_editor_state(
10381 &"
10382 /*ˇ */
10383 *ˇ
10384 "
10385 .unindent(),
10386 );
10387
10388 // Don't autoclose if the next character isn't whitespace and isn't
10389 // listed in the language's "autoclose_before" section.
10390 cx.set_state("ˇa b");
10391 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10392 cx.assert_editor_state("{ˇa b");
10393
10394 // Don't autoclose if `close` is false for the bracket pair
10395 cx.set_state("ˇ");
10396 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10397 cx.assert_editor_state("[ˇ");
10398
10399 // Surround with brackets if text is selected
10400 cx.set_state("«aˇ» b");
10401 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10402 cx.assert_editor_state("{«aˇ»} b");
10403
10404 // Autoclose when not immediately after a word character
10405 cx.set_state("a ˇ");
10406 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10407 cx.assert_editor_state("a \"ˇ\"");
10408
10409 // Autoclose pair where the start and end characters are the same
10410 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10411 cx.assert_editor_state("a \"\"ˇ");
10412
10413 // Don't autoclose when immediately after a word character
10414 cx.set_state("aˇ");
10415 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10416 cx.assert_editor_state("a\"ˇ");
10417
10418 // Do autoclose when after a non-word character
10419 cx.set_state("{ˇ");
10420 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10421 cx.assert_editor_state("{\"ˇ\"");
10422
10423 // Non identical pairs autoclose regardless of preceding character
10424 cx.set_state("aˇ");
10425 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10426 cx.assert_editor_state("a{ˇ}");
10427
10428 // Don't autoclose pair if autoclose is disabled
10429 cx.set_state("ˇ");
10430 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10431 cx.assert_editor_state("<ˇ");
10432
10433 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10434 cx.set_state("«aˇ» b");
10435 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10436 cx.assert_editor_state("<«aˇ»> b");
10437}
10438
10439#[gpui::test]
10440async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10441 init_test(cx, |settings| {
10442 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10443 });
10444
10445 let mut cx = EditorTestContext::new(cx).await;
10446
10447 let language = Arc::new(Language::new(
10448 LanguageConfig {
10449 brackets: BracketPairConfig {
10450 pairs: vec![
10451 BracketPair {
10452 start: "{".to_string(),
10453 end: "}".to_string(),
10454 close: true,
10455 surround: true,
10456 newline: true,
10457 },
10458 BracketPair {
10459 start: "(".to_string(),
10460 end: ")".to_string(),
10461 close: true,
10462 surround: true,
10463 newline: true,
10464 },
10465 BracketPair {
10466 start: "[".to_string(),
10467 end: "]".to_string(),
10468 close: false,
10469 surround: false,
10470 newline: true,
10471 },
10472 ],
10473 ..Default::default()
10474 },
10475 autoclose_before: "})]".to_string(),
10476 ..Default::default()
10477 },
10478 Some(tree_sitter_rust::LANGUAGE.into()),
10479 ));
10480
10481 cx.language_registry().add(language.clone());
10482 cx.update_buffer(|buffer, cx| {
10483 buffer.set_language(Some(language), cx);
10484 });
10485
10486 cx.set_state(
10487 &"
10488 ˇ
10489 ˇ
10490 ˇ
10491 "
10492 .unindent(),
10493 );
10494
10495 // ensure only matching closing brackets are skipped over
10496 cx.update_editor(|editor, window, cx| {
10497 editor.handle_input("}", window, cx);
10498 editor.move_left(&MoveLeft, window, cx);
10499 editor.handle_input(")", window, cx);
10500 editor.move_left(&MoveLeft, window, cx);
10501 });
10502 cx.assert_editor_state(
10503 &"
10504 ˇ)}
10505 ˇ)}
10506 ˇ)}
10507 "
10508 .unindent(),
10509 );
10510
10511 // skip-over closing brackets at multiple cursors
10512 cx.update_editor(|editor, window, cx| {
10513 editor.handle_input(")", window, cx);
10514 editor.handle_input("}", window, cx);
10515 });
10516 cx.assert_editor_state(
10517 &"
10518 )}ˇ
10519 )}ˇ
10520 )}ˇ
10521 "
10522 .unindent(),
10523 );
10524
10525 // ignore non-close brackets
10526 cx.update_editor(|editor, window, cx| {
10527 editor.handle_input("]", window, cx);
10528 editor.move_left(&MoveLeft, window, cx);
10529 editor.handle_input("]", window, cx);
10530 });
10531 cx.assert_editor_state(
10532 &"
10533 )}]ˇ]
10534 )}]ˇ]
10535 )}]ˇ]
10536 "
10537 .unindent(),
10538 );
10539}
10540
10541#[gpui::test]
10542async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10543 init_test(cx, |_| {});
10544
10545 let mut cx = EditorTestContext::new(cx).await;
10546
10547 let html_language = Arc::new(
10548 Language::new(
10549 LanguageConfig {
10550 name: "HTML".into(),
10551 brackets: BracketPairConfig {
10552 pairs: vec![
10553 BracketPair {
10554 start: "<".into(),
10555 end: ">".into(),
10556 close: true,
10557 ..Default::default()
10558 },
10559 BracketPair {
10560 start: "{".into(),
10561 end: "}".into(),
10562 close: true,
10563 ..Default::default()
10564 },
10565 BracketPair {
10566 start: "(".into(),
10567 end: ")".into(),
10568 close: true,
10569 ..Default::default()
10570 },
10571 ],
10572 ..Default::default()
10573 },
10574 autoclose_before: "})]>".into(),
10575 ..Default::default()
10576 },
10577 Some(tree_sitter_html::LANGUAGE.into()),
10578 )
10579 .with_injection_query(
10580 r#"
10581 (script_element
10582 (raw_text) @injection.content
10583 (#set! injection.language "javascript"))
10584 "#,
10585 )
10586 .unwrap(),
10587 );
10588
10589 let javascript_language = Arc::new(Language::new(
10590 LanguageConfig {
10591 name: "JavaScript".into(),
10592 brackets: BracketPairConfig {
10593 pairs: vec![
10594 BracketPair {
10595 start: "/*".into(),
10596 end: " */".into(),
10597 close: true,
10598 ..Default::default()
10599 },
10600 BracketPair {
10601 start: "{".into(),
10602 end: "}".into(),
10603 close: true,
10604 ..Default::default()
10605 },
10606 BracketPair {
10607 start: "(".into(),
10608 end: ")".into(),
10609 close: true,
10610 ..Default::default()
10611 },
10612 ],
10613 ..Default::default()
10614 },
10615 autoclose_before: "})]>".into(),
10616 ..Default::default()
10617 },
10618 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10619 ));
10620
10621 cx.language_registry().add(html_language.clone());
10622 cx.language_registry().add(javascript_language);
10623 cx.executor().run_until_parked();
10624
10625 cx.update_buffer(|buffer, cx| {
10626 buffer.set_language(Some(html_language), cx);
10627 });
10628
10629 cx.set_state(
10630 &r#"
10631 <body>ˇ
10632 <script>
10633 var x = 1;ˇ
10634 </script>
10635 </body>ˇ
10636 "#
10637 .unindent(),
10638 );
10639
10640 // Precondition: different languages are active at different locations.
10641 cx.update_editor(|editor, window, cx| {
10642 let snapshot = editor.snapshot(window, cx);
10643 let cursors = editor
10644 .selections
10645 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10646 let languages = cursors
10647 .iter()
10648 .map(|c| snapshot.language_at(c.start).unwrap().name())
10649 .collect::<Vec<_>>();
10650 assert_eq!(
10651 languages,
10652 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10653 );
10654 });
10655
10656 // Angle brackets autoclose in HTML, but not JavaScript.
10657 cx.update_editor(|editor, window, cx| {
10658 editor.handle_input("<", window, cx);
10659 editor.handle_input("a", window, cx);
10660 });
10661 cx.assert_editor_state(
10662 &r#"
10663 <body><aˇ>
10664 <script>
10665 var x = 1;<aˇ
10666 </script>
10667 </body><aˇ>
10668 "#
10669 .unindent(),
10670 );
10671
10672 // Curly braces and parens autoclose in both HTML and JavaScript.
10673 cx.update_editor(|editor, window, cx| {
10674 editor.handle_input(" b=", window, cx);
10675 editor.handle_input("{", window, cx);
10676 editor.handle_input("c", window, cx);
10677 editor.handle_input("(", window, cx);
10678 });
10679 cx.assert_editor_state(
10680 &r#"
10681 <body><a b={c(ˇ)}>
10682 <script>
10683 var x = 1;<a b={c(ˇ)}
10684 </script>
10685 </body><a b={c(ˇ)}>
10686 "#
10687 .unindent(),
10688 );
10689
10690 // Brackets that were already autoclosed are skipped.
10691 cx.update_editor(|editor, window, cx| {
10692 editor.handle_input(")", window, cx);
10693 editor.handle_input("d", window, cx);
10694 editor.handle_input("}", window, cx);
10695 });
10696 cx.assert_editor_state(
10697 &r#"
10698 <body><a b={c()d}ˇ>
10699 <script>
10700 var x = 1;<a b={c()d}ˇ
10701 </script>
10702 </body><a b={c()d}ˇ>
10703 "#
10704 .unindent(),
10705 );
10706 cx.update_editor(|editor, window, cx| {
10707 editor.handle_input(">", window, cx);
10708 });
10709 cx.assert_editor_state(
10710 &r#"
10711 <body><a b={c()d}>ˇ
10712 <script>
10713 var x = 1;<a b={c()d}>ˇ
10714 </script>
10715 </body><a b={c()d}>ˇ
10716 "#
10717 .unindent(),
10718 );
10719
10720 // Reset
10721 cx.set_state(
10722 &r#"
10723 <body>ˇ
10724 <script>
10725 var x = 1;ˇ
10726 </script>
10727 </body>ˇ
10728 "#
10729 .unindent(),
10730 );
10731
10732 cx.update_editor(|editor, window, cx| {
10733 editor.handle_input("<", window, cx);
10734 });
10735 cx.assert_editor_state(
10736 &r#"
10737 <body><ˇ>
10738 <script>
10739 var x = 1;<ˇ
10740 </script>
10741 </body><ˇ>
10742 "#
10743 .unindent(),
10744 );
10745
10746 // When backspacing, the closing angle brackets are removed.
10747 cx.update_editor(|editor, window, cx| {
10748 editor.backspace(&Backspace, window, cx);
10749 });
10750 cx.assert_editor_state(
10751 &r#"
10752 <body>ˇ
10753 <script>
10754 var x = 1;ˇ
10755 </script>
10756 </body>ˇ
10757 "#
10758 .unindent(),
10759 );
10760
10761 // Block comments autoclose in JavaScript, but not HTML.
10762 cx.update_editor(|editor, window, cx| {
10763 editor.handle_input("/", window, cx);
10764 editor.handle_input("*", window, cx);
10765 });
10766 cx.assert_editor_state(
10767 &r#"
10768 <body>/*ˇ
10769 <script>
10770 var x = 1;/*ˇ */
10771 </script>
10772 </body>/*ˇ
10773 "#
10774 .unindent(),
10775 );
10776}
10777
10778#[gpui::test]
10779async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10780 init_test(cx, |_| {});
10781
10782 let mut cx = EditorTestContext::new(cx).await;
10783
10784 let rust_language = Arc::new(
10785 Language::new(
10786 LanguageConfig {
10787 name: "Rust".into(),
10788 brackets: serde_json::from_value(json!([
10789 { "start": "{", "end": "}", "close": true, "newline": true },
10790 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10791 ]))
10792 .unwrap(),
10793 autoclose_before: "})]>".into(),
10794 ..Default::default()
10795 },
10796 Some(tree_sitter_rust::LANGUAGE.into()),
10797 )
10798 .with_override_query("(string_literal) @string")
10799 .unwrap(),
10800 );
10801
10802 cx.language_registry().add(rust_language.clone());
10803 cx.update_buffer(|buffer, cx| {
10804 buffer.set_language(Some(rust_language), cx);
10805 });
10806
10807 cx.set_state(
10808 &r#"
10809 let x = ˇ
10810 "#
10811 .unindent(),
10812 );
10813
10814 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10815 cx.update_editor(|editor, window, cx| {
10816 editor.handle_input("\"", window, cx);
10817 });
10818 cx.assert_editor_state(
10819 &r#"
10820 let x = "ˇ"
10821 "#
10822 .unindent(),
10823 );
10824
10825 // Inserting another quotation mark. The cursor moves across the existing
10826 // automatically-inserted quotation mark.
10827 cx.update_editor(|editor, window, cx| {
10828 editor.handle_input("\"", window, cx);
10829 });
10830 cx.assert_editor_state(
10831 &r#"
10832 let x = ""ˇ
10833 "#
10834 .unindent(),
10835 );
10836
10837 // Reset
10838 cx.set_state(
10839 &r#"
10840 let x = ˇ
10841 "#
10842 .unindent(),
10843 );
10844
10845 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10846 cx.update_editor(|editor, window, cx| {
10847 editor.handle_input("\"", window, cx);
10848 editor.handle_input(" ", window, cx);
10849 editor.move_left(&Default::default(), window, cx);
10850 editor.handle_input("\\", window, cx);
10851 editor.handle_input("\"", window, cx);
10852 });
10853 cx.assert_editor_state(
10854 &r#"
10855 let x = "\"ˇ "
10856 "#
10857 .unindent(),
10858 );
10859
10860 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10861 // mark. Nothing is inserted.
10862 cx.update_editor(|editor, window, cx| {
10863 editor.move_right(&Default::default(), window, cx);
10864 editor.handle_input("\"", window, cx);
10865 });
10866 cx.assert_editor_state(
10867 &r#"
10868 let x = "\" "ˇ
10869 "#
10870 .unindent(),
10871 );
10872}
10873
10874#[gpui::test]
10875async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
10876 init_test(cx, |_| {});
10877
10878 let mut cx = EditorTestContext::new(cx).await;
10879 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
10880
10881 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10882
10883 // Double quote inside single-quoted string
10884 cx.set_state(indoc! {r#"
10885 def main():
10886 items = ['"', ˇ]
10887 "#});
10888 cx.update_editor(|editor, window, cx| {
10889 editor.handle_input("\"", window, cx);
10890 });
10891 cx.assert_editor_state(indoc! {r#"
10892 def main():
10893 items = ['"', "ˇ"]
10894 "#});
10895
10896 // Two double quotes inside single-quoted string
10897 cx.set_state(indoc! {r#"
10898 def main():
10899 items = ['""', ˇ]
10900 "#});
10901 cx.update_editor(|editor, window, cx| {
10902 editor.handle_input("\"", window, cx);
10903 });
10904 cx.assert_editor_state(indoc! {r#"
10905 def main():
10906 items = ['""', "ˇ"]
10907 "#});
10908
10909 // Single quote inside double-quoted string
10910 cx.set_state(indoc! {r#"
10911 def main():
10912 items = ["'", ˇ]
10913 "#});
10914 cx.update_editor(|editor, window, cx| {
10915 editor.handle_input("'", window, cx);
10916 });
10917 cx.assert_editor_state(indoc! {r#"
10918 def main():
10919 items = ["'", 'ˇ']
10920 "#});
10921
10922 // Two single quotes inside double-quoted string
10923 cx.set_state(indoc! {r#"
10924 def main():
10925 items = ["''", ˇ]
10926 "#});
10927 cx.update_editor(|editor, window, cx| {
10928 editor.handle_input("'", window, cx);
10929 });
10930 cx.assert_editor_state(indoc! {r#"
10931 def main():
10932 items = ["''", 'ˇ']
10933 "#});
10934
10935 // Mixed quotes on same line
10936 cx.set_state(indoc! {r#"
10937 def main():
10938 items = ['"""', "'''''", ˇ]
10939 "#});
10940 cx.update_editor(|editor, window, cx| {
10941 editor.handle_input("\"", window, cx);
10942 });
10943 cx.assert_editor_state(indoc! {r#"
10944 def main():
10945 items = ['"""', "'''''", "ˇ"]
10946 "#});
10947 cx.update_editor(|editor, window, cx| {
10948 editor.move_right(&MoveRight, window, cx);
10949 });
10950 cx.update_editor(|editor, window, cx| {
10951 editor.handle_input(", ", window, cx);
10952 });
10953 cx.update_editor(|editor, window, cx| {
10954 editor.handle_input("'", window, cx);
10955 });
10956 cx.assert_editor_state(indoc! {r#"
10957 def main():
10958 items = ['"""', "'''''", "", 'ˇ']
10959 "#});
10960}
10961
10962#[gpui::test]
10963async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
10964 init_test(cx, |_| {});
10965
10966 let mut cx = EditorTestContext::new(cx).await;
10967 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
10968 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10969
10970 cx.set_state(indoc! {r#"
10971 def main():
10972 items = ["🎉", ˇ]
10973 "#});
10974 cx.update_editor(|editor, window, cx| {
10975 editor.handle_input("\"", window, cx);
10976 });
10977 cx.assert_editor_state(indoc! {r#"
10978 def main():
10979 items = ["🎉", "ˇ"]
10980 "#});
10981}
10982
10983#[gpui::test]
10984async fn test_surround_with_pair(cx: &mut TestAppContext) {
10985 init_test(cx, |_| {});
10986
10987 let language = Arc::new(Language::new(
10988 LanguageConfig {
10989 brackets: BracketPairConfig {
10990 pairs: vec![
10991 BracketPair {
10992 start: "{".to_string(),
10993 end: "}".to_string(),
10994 close: true,
10995 surround: true,
10996 newline: true,
10997 },
10998 BracketPair {
10999 start: "/* ".to_string(),
11000 end: "*/".to_string(),
11001 close: true,
11002 surround: true,
11003 ..Default::default()
11004 },
11005 ],
11006 ..Default::default()
11007 },
11008 ..Default::default()
11009 },
11010 Some(tree_sitter_rust::LANGUAGE.into()),
11011 ));
11012
11013 let text = r#"
11014 a
11015 b
11016 c
11017 "#
11018 .unindent();
11019
11020 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11021 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11022 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11023 editor
11024 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11025 .await;
11026
11027 editor.update_in(cx, |editor, window, cx| {
11028 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11029 s.select_display_ranges([
11030 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11031 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11032 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11033 ])
11034 });
11035
11036 editor.handle_input("{", window, cx);
11037 editor.handle_input("{", window, cx);
11038 editor.handle_input("{", window, cx);
11039 assert_eq!(
11040 editor.text(cx),
11041 "
11042 {{{a}}}
11043 {{{b}}}
11044 {{{c}}}
11045 "
11046 .unindent()
11047 );
11048 assert_eq!(
11049 display_ranges(editor, cx),
11050 [
11051 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11052 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11053 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11054 ]
11055 );
11056
11057 editor.undo(&Undo, window, cx);
11058 editor.undo(&Undo, window, cx);
11059 editor.undo(&Undo, window, cx);
11060 assert_eq!(
11061 editor.text(cx),
11062 "
11063 a
11064 b
11065 c
11066 "
11067 .unindent()
11068 );
11069 assert_eq!(
11070 display_ranges(editor, cx),
11071 [
11072 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11073 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11074 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11075 ]
11076 );
11077
11078 // Ensure inserting the first character of a multi-byte bracket pair
11079 // doesn't surround the selections with the bracket.
11080 editor.handle_input("/", window, cx);
11081 assert_eq!(
11082 editor.text(cx),
11083 "
11084 /
11085 /
11086 /
11087 "
11088 .unindent()
11089 );
11090 assert_eq!(
11091 display_ranges(editor, cx),
11092 [
11093 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11094 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11095 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11096 ]
11097 );
11098
11099 editor.undo(&Undo, window, cx);
11100 assert_eq!(
11101 editor.text(cx),
11102 "
11103 a
11104 b
11105 c
11106 "
11107 .unindent()
11108 );
11109 assert_eq!(
11110 display_ranges(editor, cx),
11111 [
11112 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11113 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11114 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11115 ]
11116 );
11117
11118 // Ensure inserting the last character of a multi-byte bracket pair
11119 // doesn't surround the selections with the bracket.
11120 editor.handle_input("*", window, cx);
11121 assert_eq!(
11122 editor.text(cx),
11123 "
11124 *
11125 *
11126 *
11127 "
11128 .unindent()
11129 );
11130 assert_eq!(
11131 display_ranges(editor, cx),
11132 [
11133 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11134 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11135 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11136 ]
11137 );
11138 });
11139}
11140
11141#[gpui::test]
11142async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11143 init_test(cx, |_| {});
11144
11145 let language = Arc::new(Language::new(
11146 LanguageConfig {
11147 brackets: BracketPairConfig {
11148 pairs: vec![BracketPair {
11149 start: "{".to_string(),
11150 end: "}".to_string(),
11151 close: true,
11152 surround: true,
11153 newline: true,
11154 }],
11155 ..Default::default()
11156 },
11157 autoclose_before: "}".to_string(),
11158 ..Default::default()
11159 },
11160 Some(tree_sitter_rust::LANGUAGE.into()),
11161 ));
11162
11163 let text = r#"
11164 a
11165 b
11166 c
11167 "#
11168 .unindent();
11169
11170 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11171 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11172 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11173 editor
11174 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11175 .await;
11176
11177 editor.update_in(cx, |editor, window, cx| {
11178 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11179 s.select_ranges([
11180 Point::new(0, 1)..Point::new(0, 1),
11181 Point::new(1, 1)..Point::new(1, 1),
11182 Point::new(2, 1)..Point::new(2, 1),
11183 ])
11184 });
11185
11186 editor.handle_input("{", window, cx);
11187 editor.handle_input("{", window, cx);
11188 editor.handle_input("_", window, cx);
11189 assert_eq!(
11190 editor.text(cx),
11191 "
11192 a{{_}}
11193 b{{_}}
11194 c{{_}}
11195 "
11196 .unindent()
11197 );
11198 assert_eq!(
11199 editor
11200 .selections
11201 .ranges::<Point>(&editor.display_snapshot(cx)),
11202 [
11203 Point::new(0, 4)..Point::new(0, 4),
11204 Point::new(1, 4)..Point::new(1, 4),
11205 Point::new(2, 4)..Point::new(2, 4)
11206 ]
11207 );
11208
11209 editor.backspace(&Default::default(), window, cx);
11210 editor.backspace(&Default::default(), window, cx);
11211 assert_eq!(
11212 editor.text(cx),
11213 "
11214 a{}
11215 b{}
11216 c{}
11217 "
11218 .unindent()
11219 );
11220 assert_eq!(
11221 editor
11222 .selections
11223 .ranges::<Point>(&editor.display_snapshot(cx)),
11224 [
11225 Point::new(0, 2)..Point::new(0, 2),
11226 Point::new(1, 2)..Point::new(1, 2),
11227 Point::new(2, 2)..Point::new(2, 2)
11228 ]
11229 );
11230
11231 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11232 assert_eq!(
11233 editor.text(cx),
11234 "
11235 a
11236 b
11237 c
11238 "
11239 .unindent()
11240 );
11241 assert_eq!(
11242 editor
11243 .selections
11244 .ranges::<Point>(&editor.display_snapshot(cx)),
11245 [
11246 Point::new(0, 1)..Point::new(0, 1),
11247 Point::new(1, 1)..Point::new(1, 1),
11248 Point::new(2, 1)..Point::new(2, 1)
11249 ]
11250 );
11251 });
11252}
11253
11254#[gpui::test]
11255async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11256 init_test(cx, |settings| {
11257 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11258 });
11259
11260 let mut cx = EditorTestContext::new(cx).await;
11261
11262 let language = Arc::new(Language::new(
11263 LanguageConfig {
11264 brackets: BracketPairConfig {
11265 pairs: vec![
11266 BracketPair {
11267 start: "{".to_string(),
11268 end: "}".to_string(),
11269 close: true,
11270 surround: true,
11271 newline: true,
11272 },
11273 BracketPair {
11274 start: "(".to_string(),
11275 end: ")".to_string(),
11276 close: true,
11277 surround: true,
11278 newline: true,
11279 },
11280 BracketPair {
11281 start: "[".to_string(),
11282 end: "]".to_string(),
11283 close: false,
11284 surround: true,
11285 newline: true,
11286 },
11287 ],
11288 ..Default::default()
11289 },
11290 autoclose_before: "})]".to_string(),
11291 ..Default::default()
11292 },
11293 Some(tree_sitter_rust::LANGUAGE.into()),
11294 ));
11295
11296 cx.language_registry().add(language.clone());
11297 cx.update_buffer(|buffer, cx| {
11298 buffer.set_language(Some(language), cx);
11299 });
11300
11301 cx.set_state(
11302 &"
11303 {(ˇ)}
11304 [[ˇ]]
11305 {(ˇ)}
11306 "
11307 .unindent(),
11308 );
11309
11310 cx.update_editor(|editor, window, cx| {
11311 editor.backspace(&Default::default(), window, cx);
11312 editor.backspace(&Default::default(), window, cx);
11313 });
11314
11315 cx.assert_editor_state(
11316 &"
11317 ˇ
11318 ˇ]]
11319 ˇ
11320 "
11321 .unindent(),
11322 );
11323
11324 cx.update_editor(|editor, window, cx| {
11325 editor.handle_input("{", window, cx);
11326 editor.handle_input("{", window, cx);
11327 editor.move_right(&MoveRight, window, cx);
11328 editor.move_right(&MoveRight, window, cx);
11329 editor.move_left(&MoveLeft, window, cx);
11330 editor.move_left(&MoveLeft, window, cx);
11331 editor.backspace(&Default::default(), window, cx);
11332 });
11333
11334 cx.assert_editor_state(
11335 &"
11336 {ˇ}
11337 {ˇ}]]
11338 {ˇ}
11339 "
11340 .unindent(),
11341 );
11342
11343 cx.update_editor(|editor, window, cx| {
11344 editor.backspace(&Default::default(), window, cx);
11345 });
11346
11347 cx.assert_editor_state(
11348 &"
11349 ˇ
11350 ˇ]]
11351 ˇ
11352 "
11353 .unindent(),
11354 );
11355}
11356
11357#[gpui::test]
11358async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11359 init_test(cx, |_| {});
11360
11361 let language = Arc::new(Language::new(
11362 LanguageConfig::default(),
11363 Some(tree_sitter_rust::LANGUAGE.into()),
11364 ));
11365
11366 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11367 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11368 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11369 editor
11370 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11371 .await;
11372
11373 editor.update_in(cx, |editor, window, cx| {
11374 editor.set_auto_replace_emoji_shortcode(true);
11375
11376 editor.handle_input("Hello ", window, cx);
11377 editor.handle_input(":wave", window, cx);
11378 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11379
11380 editor.handle_input(":", window, cx);
11381 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11382
11383 editor.handle_input(" :smile", window, cx);
11384 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11385
11386 editor.handle_input(":", window, cx);
11387 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11388
11389 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11390 editor.handle_input(":wave", window, cx);
11391 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11392
11393 editor.handle_input(":", window, cx);
11394 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11395
11396 editor.handle_input(":1", window, cx);
11397 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11398
11399 editor.handle_input(":", window, cx);
11400 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11401
11402 // Ensure shortcode does not get replaced when it is part of a word
11403 editor.handle_input(" Test:wave", window, cx);
11404 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11405
11406 editor.handle_input(":", window, cx);
11407 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11408
11409 editor.set_auto_replace_emoji_shortcode(false);
11410
11411 // Ensure shortcode does not get replaced when auto replace is off
11412 editor.handle_input(" :wave", window, cx);
11413 assert_eq!(
11414 editor.text(cx),
11415 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11416 );
11417
11418 editor.handle_input(":", window, cx);
11419 assert_eq!(
11420 editor.text(cx),
11421 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11422 );
11423 });
11424}
11425
11426#[gpui::test]
11427async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11428 init_test(cx, |_| {});
11429
11430 let (text, insertion_ranges) = marked_text_ranges(
11431 indoc! {"
11432 ˇ
11433 "},
11434 false,
11435 );
11436
11437 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11438 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11439
11440 _ = editor.update_in(cx, |editor, window, cx| {
11441 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11442
11443 editor
11444 .insert_snippet(
11445 &insertion_ranges
11446 .iter()
11447 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11448 .collect::<Vec<_>>(),
11449 snippet,
11450 window,
11451 cx,
11452 )
11453 .unwrap();
11454
11455 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11456 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11457 assert_eq!(editor.text(cx), expected_text);
11458 assert_eq!(
11459 editor.selections.ranges(&editor.display_snapshot(cx)),
11460 selection_ranges
11461 .iter()
11462 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11463 .collect::<Vec<_>>()
11464 );
11465 }
11466
11467 assert(
11468 editor,
11469 cx,
11470 indoc! {"
11471 type «» =•
11472 "},
11473 );
11474
11475 assert!(editor.context_menu_visible(), "There should be a matches");
11476 });
11477}
11478
11479#[gpui::test]
11480async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11481 init_test(cx, |_| {});
11482
11483 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11484 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11485 assert_eq!(editor.text(cx), expected_text);
11486 assert_eq!(
11487 editor.selections.ranges(&editor.display_snapshot(cx)),
11488 selection_ranges
11489 .iter()
11490 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11491 .collect::<Vec<_>>()
11492 );
11493 }
11494
11495 let (text, insertion_ranges) = marked_text_ranges(
11496 indoc! {"
11497 ˇ
11498 "},
11499 false,
11500 );
11501
11502 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11503 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11504
11505 _ = editor.update_in(cx, |editor, window, cx| {
11506 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11507
11508 editor
11509 .insert_snippet(
11510 &insertion_ranges
11511 .iter()
11512 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11513 .collect::<Vec<_>>(),
11514 snippet,
11515 window,
11516 cx,
11517 )
11518 .unwrap();
11519
11520 assert_state(
11521 editor,
11522 cx,
11523 indoc! {"
11524 type «» = ;•
11525 "},
11526 );
11527
11528 assert!(
11529 editor.context_menu_visible(),
11530 "Context menu should be visible for placeholder choices"
11531 );
11532
11533 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11534
11535 assert_state(
11536 editor,
11537 cx,
11538 indoc! {"
11539 type = «»;•
11540 "},
11541 );
11542
11543 assert!(
11544 !editor.context_menu_visible(),
11545 "Context menu should be hidden after moving to next tabstop"
11546 );
11547
11548 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11549
11550 assert_state(
11551 editor,
11552 cx,
11553 indoc! {"
11554 type = ; ˇ
11555 "},
11556 );
11557
11558 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11559
11560 assert_state(
11561 editor,
11562 cx,
11563 indoc! {"
11564 type = ; ˇ
11565 "},
11566 );
11567 });
11568
11569 _ = editor.update_in(cx, |editor, window, cx| {
11570 editor.select_all(&SelectAll, window, cx);
11571 editor.backspace(&Backspace, window, cx);
11572
11573 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11574 let insertion_ranges = editor
11575 .selections
11576 .all(&editor.display_snapshot(cx))
11577 .iter()
11578 .map(|s| s.range())
11579 .collect::<Vec<_>>();
11580
11581 editor
11582 .insert_snippet(&insertion_ranges, snippet, window, cx)
11583 .unwrap();
11584
11585 assert_state(editor, cx, "fn «» = value;•");
11586
11587 assert!(
11588 editor.context_menu_visible(),
11589 "Context menu should be visible for placeholder choices"
11590 );
11591
11592 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11593
11594 assert_state(editor, cx, "fn = «valueˇ»;•");
11595
11596 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11597
11598 assert_state(editor, cx, "fn «» = value;•");
11599
11600 assert!(
11601 editor.context_menu_visible(),
11602 "Context menu should be visible again after returning to first tabstop"
11603 );
11604
11605 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11606
11607 assert_state(editor, cx, "fn «» = value;•");
11608 });
11609}
11610
11611#[gpui::test]
11612async fn test_snippets(cx: &mut TestAppContext) {
11613 init_test(cx, |_| {});
11614
11615 let mut cx = EditorTestContext::new(cx).await;
11616
11617 cx.set_state(indoc! {"
11618 a.ˇ b
11619 a.ˇ b
11620 a.ˇ b
11621 "});
11622
11623 cx.update_editor(|editor, window, cx| {
11624 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11625 let insertion_ranges = editor
11626 .selections
11627 .all(&editor.display_snapshot(cx))
11628 .iter()
11629 .map(|s| s.range())
11630 .collect::<Vec<_>>();
11631 editor
11632 .insert_snippet(&insertion_ranges, snippet, window, cx)
11633 .unwrap();
11634 });
11635
11636 cx.assert_editor_state(indoc! {"
11637 a.f(«oneˇ», two, «threeˇ») b
11638 a.f(«oneˇ», two, «threeˇ») b
11639 a.f(«oneˇ», two, «threeˇ») b
11640 "});
11641
11642 // Can't move earlier than the first tab stop
11643 cx.update_editor(|editor, window, cx| {
11644 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11645 });
11646 cx.assert_editor_state(indoc! {"
11647 a.f(«oneˇ», two, «threeˇ») b
11648 a.f(«oneˇ», two, «threeˇ») b
11649 a.f(«oneˇ», two, «threeˇ») b
11650 "});
11651
11652 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11653 cx.assert_editor_state(indoc! {"
11654 a.f(one, «twoˇ», three) b
11655 a.f(one, «twoˇ», three) b
11656 a.f(one, «twoˇ», three) b
11657 "});
11658
11659 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11660 cx.assert_editor_state(indoc! {"
11661 a.f(«oneˇ», two, «threeˇ») b
11662 a.f(«oneˇ», two, «threeˇ») b
11663 a.f(«oneˇ», two, «threeˇ») b
11664 "});
11665
11666 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11667 cx.assert_editor_state(indoc! {"
11668 a.f(one, «twoˇ», three) b
11669 a.f(one, «twoˇ», three) b
11670 a.f(one, «twoˇ», three) b
11671 "});
11672 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11673 cx.assert_editor_state(indoc! {"
11674 a.f(one, two, three)ˇ b
11675 a.f(one, two, three)ˇ b
11676 a.f(one, two, three)ˇ b
11677 "});
11678
11679 // As soon as the last tab stop is reached, snippet state is gone
11680 cx.update_editor(|editor, window, cx| {
11681 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11682 });
11683 cx.assert_editor_state(indoc! {"
11684 a.f(one, two, three)ˇ b
11685 a.f(one, two, three)ˇ b
11686 a.f(one, two, three)ˇ b
11687 "});
11688}
11689
11690#[gpui::test]
11691async fn test_snippet_indentation(cx: &mut TestAppContext) {
11692 init_test(cx, |_| {});
11693
11694 let mut cx = EditorTestContext::new(cx).await;
11695
11696 cx.update_editor(|editor, window, cx| {
11697 let snippet = Snippet::parse(indoc! {"
11698 /*
11699 * Multiline comment with leading indentation
11700 *
11701 * $1
11702 */
11703 $0"})
11704 .unwrap();
11705 let insertion_ranges = editor
11706 .selections
11707 .all(&editor.display_snapshot(cx))
11708 .iter()
11709 .map(|s| s.range())
11710 .collect::<Vec<_>>();
11711 editor
11712 .insert_snippet(&insertion_ranges, snippet, window, cx)
11713 .unwrap();
11714 });
11715
11716 cx.assert_editor_state(indoc! {"
11717 /*
11718 * Multiline comment with leading indentation
11719 *
11720 * ˇ
11721 */
11722 "});
11723
11724 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11725 cx.assert_editor_state(indoc! {"
11726 /*
11727 * Multiline comment with leading indentation
11728 *
11729 *•
11730 */
11731 ˇ"});
11732}
11733
11734#[gpui::test]
11735async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11736 init_test(cx, |_| {});
11737
11738 let mut cx = EditorTestContext::new(cx).await;
11739 cx.update_editor(|editor, _, cx| {
11740 editor.project().unwrap().update(cx, |project, cx| {
11741 project.snippets().update(cx, |snippets, _cx| {
11742 let snippet = project::snippet_provider::Snippet {
11743 prefix: vec!["multi word".to_string()],
11744 body: "this is many words".to_string(),
11745 description: Some("description".to_string()),
11746 name: "multi-word snippet test".to_string(),
11747 };
11748 snippets.add_snippet_for_test(
11749 None,
11750 PathBuf::from("test_snippets.json"),
11751 vec![Arc::new(snippet)],
11752 );
11753 });
11754 })
11755 });
11756
11757 for (input_to_simulate, should_match_snippet) in [
11758 ("m", true),
11759 ("m ", true),
11760 ("m w", true),
11761 ("aa m w", true),
11762 ("aa m g", false),
11763 ] {
11764 cx.set_state("ˇ");
11765 cx.simulate_input(input_to_simulate); // fails correctly
11766
11767 cx.update_editor(|editor, _, _| {
11768 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11769 else {
11770 assert!(!should_match_snippet); // no completions! don't even show the menu
11771 return;
11772 };
11773 assert!(context_menu.visible());
11774 let completions = context_menu.completions.borrow();
11775
11776 assert_eq!(!completions.is_empty(), should_match_snippet);
11777 });
11778 }
11779}
11780
11781#[gpui::test]
11782async fn test_document_format_during_save(cx: &mut TestAppContext) {
11783 init_test(cx, |_| {});
11784
11785 let fs = FakeFs::new(cx.executor());
11786 fs.insert_file(path!("/file.rs"), Default::default()).await;
11787
11788 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11789
11790 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11791 language_registry.add(rust_lang());
11792 let mut fake_servers = language_registry.register_fake_lsp(
11793 "Rust",
11794 FakeLspAdapter {
11795 capabilities: lsp::ServerCapabilities {
11796 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11797 ..Default::default()
11798 },
11799 ..Default::default()
11800 },
11801 );
11802
11803 let buffer = project
11804 .update(cx, |project, cx| {
11805 project.open_local_buffer(path!("/file.rs"), cx)
11806 })
11807 .await
11808 .unwrap();
11809
11810 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11811 let (editor, cx) = cx.add_window_view(|window, cx| {
11812 build_editor_with_project(project.clone(), buffer, window, cx)
11813 });
11814 editor.update_in(cx, |editor, window, cx| {
11815 editor.set_text("one\ntwo\nthree\n", window, cx)
11816 });
11817 assert!(cx.read(|cx| editor.is_dirty(cx)));
11818
11819 cx.executor().start_waiting();
11820 let fake_server = fake_servers.next().await.unwrap();
11821
11822 {
11823 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11824 move |params, _| async move {
11825 assert_eq!(
11826 params.text_document.uri,
11827 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11828 );
11829 assert_eq!(params.options.tab_size, 4);
11830 Ok(Some(vec![lsp::TextEdit::new(
11831 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11832 ", ".to_string(),
11833 )]))
11834 },
11835 );
11836 let save = editor
11837 .update_in(cx, |editor, window, cx| {
11838 editor.save(
11839 SaveOptions {
11840 format: true,
11841 autosave: false,
11842 },
11843 project.clone(),
11844 window,
11845 cx,
11846 )
11847 })
11848 .unwrap();
11849 cx.executor().start_waiting();
11850 save.await;
11851
11852 assert_eq!(
11853 editor.update(cx, |editor, cx| editor.text(cx)),
11854 "one, two\nthree\n"
11855 );
11856 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11857 }
11858
11859 {
11860 editor.update_in(cx, |editor, window, cx| {
11861 editor.set_text("one\ntwo\nthree\n", window, cx)
11862 });
11863 assert!(cx.read(|cx| editor.is_dirty(cx)));
11864
11865 // Ensure we can still save even if formatting hangs.
11866 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11867 move |params, _| async move {
11868 assert_eq!(
11869 params.text_document.uri,
11870 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11871 );
11872 futures::future::pending::<()>().await;
11873 unreachable!()
11874 },
11875 );
11876 let save = editor
11877 .update_in(cx, |editor, window, cx| {
11878 editor.save(
11879 SaveOptions {
11880 format: true,
11881 autosave: false,
11882 },
11883 project.clone(),
11884 window,
11885 cx,
11886 )
11887 })
11888 .unwrap();
11889 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11890 cx.executor().start_waiting();
11891 save.await;
11892 assert_eq!(
11893 editor.update(cx, |editor, cx| editor.text(cx)),
11894 "one\ntwo\nthree\n"
11895 );
11896 }
11897
11898 // Set rust language override and assert overridden tabsize is sent to language server
11899 update_test_language_settings(cx, |settings| {
11900 settings.languages.0.insert(
11901 "Rust".into(),
11902 LanguageSettingsContent {
11903 tab_size: NonZeroU32::new(8),
11904 ..Default::default()
11905 },
11906 );
11907 });
11908
11909 {
11910 editor.update_in(cx, |editor, window, cx| {
11911 editor.set_text("somehting_new\n", window, cx)
11912 });
11913 assert!(cx.read(|cx| editor.is_dirty(cx)));
11914 let _formatting_request_signal = fake_server
11915 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11916 assert_eq!(
11917 params.text_document.uri,
11918 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11919 );
11920 assert_eq!(params.options.tab_size, 8);
11921 Ok(Some(vec![]))
11922 });
11923 let save = editor
11924 .update_in(cx, |editor, window, cx| {
11925 editor.save(
11926 SaveOptions {
11927 format: true,
11928 autosave: false,
11929 },
11930 project.clone(),
11931 window,
11932 cx,
11933 )
11934 })
11935 .unwrap();
11936 cx.executor().start_waiting();
11937 save.await;
11938 }
11939}
11940
11941#[gpui::test]
11942async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11943 init_test(cx, |settings| {
11944 settings.defaults.ensure_final_newline_on_save = Some(false);
11945 });
11946
11947 let fs = FakeFs::new(cx.executor());
11948 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11949
11950 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11951
11952 let buffer = project
11953 .update(cx, |project, cx| {
11954 project.open_local_buffer(path!("/file.txt"), cx)
11955 })
11956 .await
11957 .unwrap();
11958
11959 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11960 let (editor, cx) = cx.add_window_view(|window, cx| {
11961 build_editor_with_project(project.clone(), buffer, window, cx)
11962 });
11963 editor.update_in(cx, |editor, window, cx| {
11964 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11965 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11966 });
11967 });
11968 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11969
11970 editor.update_in(cx, |editor, window, cx| {
11971 editor.handle_input("\n", window, cx)
11972 });
11973 cx.run_until_parked();
11974 save(&editor, &project, cx).await;
11975 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11976
11977 editor.update_in(cx, |editor, window, cx| {
11978 editor.undo(&Default::default(), window, cx);
11979 });
11980 save(&editor, &project, cx).await;
11981 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11982
11983 editor.update_in(cx, |editor, window, cx| {
11984 editor.redo(&Default::default(), window, cx);
11985 });
11986 cx.run_until_parked();
11987 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11988
11989 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11990 let save = editor
11991 .update_in(cx, |editor, window, cx| {
11992 editor.save(
11993 SaveOptions {
11994 format: true,
11995 autosave: false,
11996 },
11997 project.clone(),
11998 window,
11999 cx,
12000 )
12001 })
12002 .unwrap();
12003 cx.executor().start_waiting();
12004 save.await;
12005 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12006 }
12007}
12008
12009#[gpui::test]
12010async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12011 init_test(cx, |_| {});
12012
12013 let cols = 4;
12014 let rows = 10;
12015 let sample_text_1 = sample_text(rows, cols, 'a');
12016 assert_eq!(
12017 sample_text_1,
12018 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12019 );
12020 let sample_text_2 = sample_text(rows, cols, 'l');
12021 assert_eq!(
12022 sample_text_2,
12023 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12024 );
12025 let sample_text_3 = sample_text(rows, cols, 'v');
12026 assert_eq!(
12027 sample_text_3,
12028 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12029 );
12030
12031 let fs = FakeFs::new(cx.executor());
12032 fs.insert_tree(
12033 path!("/a"),
12034 json!({
12035 "main.rs": sample_text_1,
12036 "other.rs": sample_text_2,
12037 "lib.rs": sample_text_3,
12038 }),
12039 )
12040 .await;
12041
12042 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12043 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12044 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12045
12046 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12047 language_registry.add(rust_lang());
12048 let mut fake_servers = language_registry.register_fake_lsp(
12049 "Rust",
12050 FakeLspAdapter {
12051 capabilities: lsp::ServerCapabilities {
12052 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12053 ..Default::default()
12054 },
12055 ..Default::default()
12056 },
12057 );
12058
12059 let worktree = project.update(cx, |project, cx| {
12060 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12061 assert_eq!(worktrees.len(), 1);
12062 worktrees.pop().unwrap()
12063 });
12064 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12065
12066 let buffer_1 = project
12067 .update(cx, |project, cx| {
12068 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12069 })
12070 .await
12071 .unwrap();
12072 let buffer_2 = project
12073 .update(cx, |project, cx| {
12074 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12075 })
12076 .await
12077 .unwrap();
12078 let buffer_3 = project
12079 .update(cx, |project, cx| {
12080 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12081 })
12082 .await
12083 .unwrap();
12084
12085 let multi_buffer = cx.new(|cx| {
12086 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12087 multi_buffer.push_excerpts(
12088 buffer_1.clone(),
12089 [
12090 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12091 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12092 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12093 ],
12094 cx,
12095 );
12096 multi_buffer.push_excerpts(
12097 buffer_2.clone(),
12098 [
12099 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12100 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12101 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12102 ],
12103 cx,
12104 );
12105 multi_buffer.push_excerpts(
12106 buffer_3.clone(),
12107 [
12108 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12109 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12110 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12111 ],
12112 cx,
12113 );
12114 multi_buffer
12115 });
12116 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12117 Editor::new(
12118 EditorMode::full(),
12119 multi_buffer,
12120 Some(project.clone()),
12121 window,
12122 cx,
12123 )
12124 });
12125
12126 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12127 editor.change_selections(
12128 SelectionEffects::scroll(Autoscroll::Next),
12129 window,
12130 cx,
12131 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12132 );
12133 editor.insert("|one|two|three|", window, cx);
12134 });
12135 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12136 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12137 editor.change_selections(
12138 SelectionEffects::scroll(Autoscroll::Next),
12139 window,
12140 cx,
12141 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12142 );
12143 editor.insert("|four|five|six|", window, cx);
12144 });
12145 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12146
12147 // First two buffers should be edited, but not the third one.
12148 assert_eq!(
12149 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12150 "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}",
12151 );
12152 buffer_1.update(cx, |buffer, _| {
12153 assert!(buffer.is_dirty());
12154 assert_eq!(
12155 buffer.text(),
12156 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12157 )
12158 });
12159 buffer_2.update(cx, |buffer, _| {
12160 assert!(buffer.is_dirty());
12161 assert_eq!(
12162 buffer.text(),
12163 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12164 )
12165 });
12166 buffer_3.update(cx, |buffer, _| {
12167 assert!(!buffer.is_dirty());
12168 assert_eq!(buffer.text(), sample_text_3,)
12169 });
12170 cx.executor().run_until_parked();
12171
12172 cx.executor().start_waiting();
12173 let save = multi_buffer_editor
12174 .update_in(cx, |editor, window, cx| {
12175 editor.save(
12176 SaveOptions {
12177 format: true,
12178 autosave: false,
12179 },
12180 project.clone(),
12181 window,
12182 cx,
12183 )
12184 })
12185 .unwrap();
12186
12187 let fake_server = fake_servers.next().await.unwrap();
12188 fake_server
12189 .server
12190 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12191 Ok(Some(vec![lsp::TextEdit::new(
12192 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12193 format!("[{} formatted]", params.text_document.uri),
12194 )]))
12195 })
12196 .detach();
12197 save.await;
12198
12199 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12200 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12201 assert_eq!(
12202 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12203 uri!(
12204 "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}"
12205 ),
12206 );
12207 buffer_1.update(cx, |buffer, _| {
12208 assert!(!buffer.is_dirty());
12209 assert_eq!(
12210 buffer.text(),
12211 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12212 )
12213 });
12214 buffer_2.update(cx, |buffer, _| {
12215 assert!(!buffer.is_dirty());
12216 assert_eq!(
12217 buffer.text(),
12218 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12219 )
12220 });
12221 buffer_3.update(cx, |buffer, _| {
12222 assert!(!buffer.is_dirty());
12223 assert_eq!(buffer.text(), sample_text_3,)
12224 });
12225}
12226
12227#[gpui::test]
12228async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12229 init_test(cx, |_| {});
12230
12231 let fs = FakeFs::new(cx.executor());
12232 fs.insert_tree(
12233 path!("/dir"),
12234 json!({
12235 "file1.rs": "fn main() { println!(\"hello\"); }",
12236 "file2.rs": "fn test() { println!(\"test\"); }",
12237 "file3.rs": "fn other() { println!(\"other\"); }\n",
12238 }),
12239 )
12240 .await;
12241
12242 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12243 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12244 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12245
12246 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12247 language_registry.add(rust_lang());
12248
12249 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12250 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12251
12252 // Open three buffers
12253 let buffer_1 = project
12254 .update(cx, |project, cx| {
12255 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12256 })
12257 .await
12258 .unwrap();
12259 let buffer_2 = project
12260 .update(cx, |project, cx| {
12261 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12262 })
12263 .await
12264 .unwrap();
12265 let buffer_3 = project
12266 .update(cx, |project, cx| {
12267 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12268 })
12269 .await
12270 .unwrap();
12271
12272 // Create a multi-buffer with all three buffers
12273 let multi_buffer = cx.new(|cx| {
12274 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12275 multi_buffer.push_excerpts(
12276 buffer_1.clone(),
12277 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12278 cx,
12279 );
12280 multi_buffer.push_excerpts(
12281 buffer_2.clone(),
12282 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12283 cx,
12284 );
12285 multi_buffer.push_excerpts(
12286 buffer_3.clone(),
12287 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12288 cx,
12289 );
12290 multi_buffer
12291 });
12292
12293 let editor = cx.new_window_entity(|window, cx| {
12294 Editor::new(
12295 EditorMode::full(),
12296 multi_buffer,
12297 Some(project.clone()),
12298 window,
12299 cx,
12300 )
12301 });
12302
12303 // Edit only the first buffer
12304 editor.update_in(cx, |editor, window, cx| {
12305 editor.change_selections(
12306 SelectionEffects::scroll(Autoscroll::Next),
12307 window,
12308 cx,
12309 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12310 );
12311 editor.insert("// edited", window, cx);
12312 });
12313
12314 // Verify that only buffer 1 is dirty
12315 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12316 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12317 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12318
12319 // Get write counts after file creation (files were created with initial content)
12320 // We expect each file to have been written once during creation
12321 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12322 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12323 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12324
12325 // Perform autosave
12326 let save_task = editor.update_in(cx, |editor, window, cx| {
12327 editor.save(
12328 SaveOptions {
12329 format: true,
12330 autosave: true,
12331 },
12332 project.clone(),
12333 window,
12334 cx,
12335 )
12336 });
12337 save_task.await.unwrap();
12338
12339 // Only the dirty buffer should have been saved
12340 assert_eq!(
12341 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12342 1,
12343 "Buffer 1 was dirty, so it should have been written once during autosave"
12344 );
12345 assert_eq!(
12346 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12347 0,
12348 "Buffer 2 was clean, so it should not have been written during autosave"
12349 );
12350 assert_eq!(
12351 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12352 0,
12353 "Buffer 3 was clean, so it should not have been written during autosave"
12354 );
12355
12356 // Verify buffer states after autosave
12357 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12358 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12359 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12360
12361 // Now perform a manual save (format = true)
12362 let save_task = editor.update_in(cx, |editor, window, cx| {
12363 editor.save(
12364 SaveOptions {
12365 format: true,
12366 autosave: false,
12367 },
12368 project.clone(),
12369 window,
12370 cx,
12371 )
12372 });
12373 save_task.await.unwrap();
12374
12375 // During manual save, clean buffers don't get written to disk
12376 // They just get did_save called for language server notifications
12377 assert_eq!(
12378 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12379 1,
12380 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12381 );
12382 assert_eq!(
12383 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12384 0,
12385 "Buffer 2 should not have been written at all"
12386 );
12387 assert_eq!(
12388 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12389 0,
12390 "Buffer 3 should not have been written at all"
12391 );
12392}
12393
12394async fn setup_range_format_test(
12395 cx: &mut TestAppContext,
12396) -> (
12397 Entity<Project>,
12398 Entity<Editor>,
12399 &mut gpui::VisualTestContext,
12400 lsp::FakeLanguageServer,
12401) {
12402 init_test(cx, |_| {});
12403
12404 let fs = FakeFs::new(cx.executor());
12405 fs.insert_file(path!("/file.rs"), Default::default()).await;
12406
12407 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12408
12409 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12410 language_registry.add(rust_lang());
12411 let mut fake_servers = language_registry.register_fake_lsp(
12412 "Rust",
12413 FakeLspAdapter {
12414 capabilities: lsp::ServerCapabilities {
12415 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12416 ..lsp::ServerCapabilities::default()
12417 },
12418 ..FakeLspAdapter::default()
12419 },
12420 );
12421
12422 let buffer = project
12423 .update(cx, |project, cx| {
12424 project.open_local_buffer(path!("/file.rs"), cx)
12425 })
12426 .await
12427 .unwrap();
12428
12429 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12430 let (editor, cx) = cx.add_window_view(|window, cx| {
12431 build_editor_with_project(project.clone(), buffer, window, cx)
12432 });
12433
12434 cx.executor().start_waiting();
12435 let fake_server = fake_servers.next().await.unwrap();
12436
12437 (project, editor, cx, fake_server)
12438}
12439
12440#[gpui::test]
12441async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12442 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12443
12444 editor.update_in(cx, |editor, window, cx| {
12445 editor.set_text("one\ntwo\nthree\n", window, cx)
12446 });
12447 assert!(cx.read(|cx| editor.is_dirty(cx)));
12448
12449 let save = editor
12450 .update_in(cx, |editor, window, cx| {
12451 editor.save(
12452 SaveOptions {
12453 format: true,
12454 autosave: false,
12455 },
12456 project.clone(),
12457 window,
12458 cx,
12459 )
12460 })
12461 .unwrap();
12462 fake_server
12463 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12464 assert_eq!(
12465 params.text_document.uri,
12466 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12467 );
12468 assert_eq!(params.options.tab_size, 4);
12469 Ok(Some(vec![lsp::TextEdit::new(
12470 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12471 ", ".to_string(),
12472 )]))
12473 })
12474 .next()
12475 .await;
12476 cx.executor().start_waiting();
12477 save.await;
12478 assert_eq!(
12479 editor.update(cx, |editor, cx| editor.text(cx)),
12480 "one, two\nthree\n"
12481 );
12482 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12483}
12484
12485#[gpui::test]
12486async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12487 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12488
12489 editor.update_in(cx, |editor, window, cx| {
12490 editor.set_text("one\ntwo\nthree\n", window, cx)
12491 });
12492 assert!(cx.read(|cx| editor.is_dirty(cx)));
12493
12494 // Test that save still works when formatting hangs
12495 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12496 move |params, _| async move {
12497 assert_eq!(
12498 params.text_document.uri,
12499 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12500 );
12501 futures::future::pending::<()>().await;
12502 unreachable!()
12503 },
12504 );
12505 let save = editor
12506 .update_in(cx, |editor, window, cx| {
12507 editor.save(
12508 SaveOptions {
12509 format: true,
12510 autosave: false,
12511 },
12512 project.clone(),
12513 window,
12514 cx,
12515 )
12516 })
12517 .unwrap();
12518 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12519 cx.executor().start_waiting();
12520 save.await;
12521 assert_eq!(
12522 editor.update(cx, |editor, cx| editor.text(cx)),
12523 "one\ntwo\nthree\n"
12524 );
12525 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12526}
12527
12528#[gpui::test]
12529async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12530 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12531
12532 // Buffer starts clean, no formatting should be requested
12533 let save = editor
12534 .update_in(cx, |editor, window, cx| {
12535 editor.save(
12536 SaveOptions {
12537 format: false,
12538 autosave: false,
12539 },
12540 project.clone(),
12541 window,
12542 cx,
12543 )
12544 })
12545 .unwrap();
12546 let _pending_format_request = fake_server
12547 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12548 panic!("Should not be invoked");
12549 })
12550 .next();
12551 cx.executor().start_waiting();
12552 save.await;
12553 cx.run_until_parked();
12554}
12555
12556#[gpui::test]
12557async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12558 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12559
12560 // Set Rust language override and assert overridden tabsize is sent to language server
12561 update_test_language_settings(cx, |settings| {
12562 settings.languages.0.insert(
12563 "Rust".into(),
12564 LanguageSettingsContent {
12565 tab_size: NonZeroU32::new(8),
12566 ..Default::default()
12567 },
12568 );
12569 });
12570
12571 editor.update_in(cx, |editor, window, cx| {
12572 editor.set_text("something_new\n", window, cx)
12573 });
12574 assert!(cx.read(|cx| editor.is_dirty(cx)));
12575 let save = editor
12576 .update_in(cx, |editor, window, cx| {
12577 editor.save(
12578 SaveOptions {
12579 format: true,
12580 autosave: false,
12581 },
12582 project.clone(),
12583 window,
12584 cx,
12585 )
12586 })
12587 .unwrap();
12588 fake_server
12589 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12590 assert_eq!(
12591 params.text_document.uri,
12592 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12593 );
12594 assert_eq!(params.options.tab_size, 8);
12595 Ok(Some(Vec::new()))
12596 })
12597 .next()
12598 .await;
12599 save.await;
12600}
12601
12602#[gpui::test]
12603async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12604 init_test(cx, |settings| {
12605 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12606 settings::LanguageServerFormatterSpecifier::Current,
12607 )))
12608 });
12609
12610 let fs = FakeFs::new(cx.executor());
12611 fs.insert_file(path!("/file.rs"), Default::default()).await;
12612
12613 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12614
12615 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12616 language_registry.add(Arc::new(Language::new(
12617 LanguageConfig {
12618 name: "Rust".into(),
12619 matcher: LanguageMatcher {
12620 path_suffixes: vec!["rs".to_string()],
12621 ..Default::default()
12622 },
12623 ..LanguageConfig::default()
12624 },
12625 Some(tree_sitter_rust::LANGUAGE.into()),
12626 )));
12627 update_test_language_settings(cx, |settings| {
12628 // Enable Prettier formatting for the same buffer, and ensure
12629 // LSP is called instead of Prettier.
12630 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12631 });
12632 let mut fake_servers = language_registry.register_fake_lsp(
12633 "Rust",
12634 FakeLspAdapter {
12635 capabilities: lsp::ServerCapabilities {
12636 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12637 ..Default::default()
12638 },
12639 ..Default::default()
12640 },
12641 );
12642
12643 let buffer = project
12644 .update(cx, |project, cx| {
12645 project.open_local_buffer(path!("/file.rs"), cx)
12646 })
12647 .await
12648 .unwrap();
12649
12650 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12651 let (editor, cx) = cx.add_window_view(|window, cx| {
12652 build_editor_with_project(project.clone(), buffer, window, cx)
12653 });
12654 editor.update_in(cx, |editor, window, cx| {
12655 editor.set_text("one\ntwo\nthree\n", window, cx)
12656 });
12657
12658 cx.executor().start_waiting();
12659 let fake_server = fake_servers.next().await.unwrap();
12660
12661 let format = editor
12662 .update_in(cx, |editor, window, cx| {
12663 editor.perform_format(
12664 project.clone(),
12665 FormatTrigger::Manual,
12666 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12667 window,
12668 cx,
12669 )
12670 })
12671 .unwrap();
12672 fake_server
12673 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12674 assert_eq!(
12675 params.text_document.uri,
12676 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12677 );
12678 assert_eq!(params.options.tab_size, 4);
12679 Ok(Some(vec![lsp::TextEdit::new(
12680 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12681 ", ".to_string(),
12682 )]))
12683 })
12684 .next()
12685 .await;
12686 cx.executor().start_waiting();
12687 format.await;
12688 assert_eq!(
12689 editor.update(cx, |editor, cx| editor.text(cx)),
12690 "one, two\nthree\n"
12691 );
12692
12693 editor.update_in(cx, |editor, window, cx| {
12694 editor.set_text("one\ntwo\nthree\n", window, cx)
12695 });
12696 // Ensure we don't lock if formatting hangs.
12697 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12698 move |params, _| async move {
12699 assert_eq!(
12700 params.text_document.uri,
12701 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12702 );
12703 futures::future::pending::<()>().await;
12704 unreachable!()
12705 },
12706 );
12707 let format = editor
12708 .update_in(cx, |editor, window, cx| {
12709 editor.perform_format(
12710 project,
12711 FormatTrigger::Manual,
12712 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12713 window,
12714 cx,
12715 )
12716 })
12717 .unwrap();
12718 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12719 cx.executor().start_waiting();
12720 format.await;
12721 assert_eq!(
12722 editor.update(cx, |editor, cx| editor.text(cx)),
12723 "one\ntwo\nthree\n"
12724 );
12725}
12726
12727#[gpui::test]
12728async fn test_multiple_formatters(cx: &mut TestAppContext) {
12729 init_test(cx, |settings| {
12730 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12731 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12732 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12733 Formatter::CodeAction("code-action-1".into()),
12734 Formatter::CodeAction("code-action-2".into()),
12735 ]))
12736 });
12737
12738 let fs = FakeFs::new(cx.executor());
12739 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12740 .await;
12741
12742 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12743 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12744 language_registry.add(rust_lang());
12745
12746 let mut fake_servers = language_registry.register_fake_lsp(
12747 "Rust",
12748 FakeLspAdapter {
12749 capabilities: lsp::ServerCapabilities {
12750 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12751 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12752 commands: vec!["the-command-for-code-action-1".into()],
12753 ..Default::default()
12754 }),
12755 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12756 ..Default::default()
12757 },
12758 ..Default::default()
12759 },
12760 );
12761
12762 let buffer = project
12763 .update(cx, |project, cx| {
12764 project.open_local_buffer(path!("/file.rs"), cx)
12765 })
12766 .await
12767 .unwrap();
12768
12769 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12770 let (editor, cx) = cx.add_window_view(|window, cx| {
12771 build_editor_with_project(project.clone(), buffer, window, cx)
12772 });
12773
12774 cx.executor().start_waiting();
12775
12776 let fake_server = fake_servers.next().await.unwrap();
12777 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12778 move |_params, _| async move {
12779 Ok(Some(vec![lsp::TextEdit::new(
12780 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12781 "applied-formatting\n".to_string(),
12782 )]))
12783 },
12784 );
12785 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12786 move |params, _| async move {
12787 let requested_code_actions = params.context.only.expect("Expected code action request");
12788 assert_eq!(requested_code_actions.len(), 1);
12789
12790 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12791 let code_action = match requested_code_actions[0].as_str() {
12792 "code-action-1" => lsp::CodeAction {
12793 kind: Some("code-action-1".into()),
12794 edit: Some(lsp::WorkspaceEdit::new(
12795 [(
12796 uri,
12797 vec![lsp::TextEdit::new(
12798 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12799 "applied-code-action-1-edit\n".to_string(),
12800 )],
12801 )]
12802 .into_iter()
12803 .collect(),
12804 )),
12805 command: Some(lsp::Command {
12806 command: "the-command-for-code-action-1".into(),
12807 ..Default::default()
12808 }),
12809 ..Default::default()
12810 },
12811 "code-action-2" => lsp::CodeAction {
12812 kind: Some("code-action-2".into()),
12813 edit: Some(lsp::WorkspaceEdit::new(
12814 [(
12815 uri,
12816 vec![lsp::TextEdit::new(
12817 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12818 "applied-code-action-2-edit\n".to_string(),
12819 )],
12820 )]
12821 .into_iter()
12822 .collect(),
12823 )),
12824 ..Default::default()
12825 },
12826 req => panic!("Unexpected code action request: {:?}", req),
12827 };
12828 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12829 code_action,
12830 )]))
12831 },
12832 );
12833
12834 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12835 move |params, _| async move { Ok(params) }
12836 });
12837
12838 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12839 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12840 let fake = fake_server.clone();
12841 let lock = command_lock.clone();
12842 move |params, _| {
12843 assert_eq!(params.command, "the-command-for-code-action-1");
12844 let fake = fake.clone();
12845 let lock = lock.clone();
12846 async move {
12847 lock.lock().await;
12848 fake.server
12849 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12850 label: None,
12851 edit: lsp::WorkspaceEdit {
12852 changes: Some(
12853 [(
12854 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12855 vec![lsp::TextEdit {
12856 range: lsp::Range::new(
12857 lsp::Position::new(0, 0),
12858 lsp::Position::new(0, 0),
12859 ),
12860 new_text: "applied-code-action-1-command\n".into(),
12861 }],
12862 )]
12863 .into_iter()
12864 .collect(),
12865 ),
12866 ..Default::default()
12867 },
12868 })
12869 .await
12870 .into_response()
12871 .unwrap();
12872 Ok(Some(json!(null)))
12873 }
12874 }
12875 });
12876
12877 cx.executor().start_waiting();
12878 editor
12879 .update_in(cx, |editor, window, cx| {
12880 editor.perform_format(
12881 project.clone(),
12882 FormatTrigger::Manual,
12883 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12884 window,
12885 cx,
12886 )
12887 })
12888 .unwrap()
12889 .await;
12890 editor.update(cx, |editor, cx| {
12891 assert_eq!(
12892 editor.text(cx),
12893 r#"
12894 applied-code-action-2-edit
12895 applied-code-action-1-command
12896 applied-code-action-1-edit
12897 applied-formatting
12898 one
12899 two
12900 three
12901 "#
12902 .unindent()
12903 );
12904 });
12905
12906 editor.update_in(cx, |editor, window, cx| {
12907 editor.undo(&Default::default(), window, cx);
12908 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12909 });
12910
12911 // Perform a manual edit while waiting for an LSP command
12912 // that's being run as part of a formatting code action.
12913 let lock_guard = command_lock.lock().await;
12914 let format = editor
12915 .update_in(cx, |editor, window, cx| {
12916 editor.perform_format(
12917 project.clone(),
12918 FormatTrigger::Manual,
12919 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12920 window,
12921 cx,
12922 )
12923 })
12924 .unwrap();
12925 cx.run_until_parked();
12926 editor.update(cx, |editor, cx| {
12927 assert_eq!(
12928 editor.text(cx),
12929 r#"
12930 applied-code-action-1-edit
12931 applied-formatting
12932 one
12933 two
12934 three
12935 "#
12936 .unindent()
12937 );
12938
12939 editor.buffer.update(cx, |buffer, cx| {
12940 let ix = buffer.len(cx);
12941 buffer.edit([(ix..ix, "edited\n")], None, cx);
12942 });
12943 });
12944
12945 // Allow the LSP command to proceed. Because the buffer was edited,
12946 // the second code action will not be run.
12947 drop(lock_guard);
12948 format.await;
12949 editor.update_in(cx, |editor, window, cx| {
12950 assert_eq!(
12951 editor.text(cx),
12952 r#"
12953 applied-code-action-1-command
12954 applied-code-action-1-edit
12955 applied-formatting
12956 one
12957 two
12958 three
12959 edited
12960 "#
12961 .unindent()
12962 );
12963
12964 // The manual edit is undone first, because it is the last thing the user did
12965 // (even though the command completed afterwards).
12966 editor.undo(&Default::default(), window, cx);
12967 assert_eq!(
12968 editor.text(cx),
12969 r#"
12970 applied-code-action-1-command
12971 applied-code-action-1-edit
12972 applied-formatting
12973 one
12974 two
12975 three
12976 "#
12977 .unindent()
12978 );
12979
12980 // All the formatting (including the command, which completed after the manual edit)
12981 // is undone together.
12982 editor.undo(&Default::default(), window, cx);
12983 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12984 });
12985}
12986
12987#[gpui::test]
12988async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12989 init_test(cx, |settings| {
12990 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12991 settings::LanguageServerFormatterSpecifier::Current,
12992 )]))
12993 });
12994
12995 let fs = FakeFs::new(cx.executor());
12996 fs.insert_file(path!("/file.ts"), Default::default()).await;
12997
12998 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12999
13000 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13001 language_registry.add(Arc::new(Language::new(
13002 LanguageConfig {
13003 name: "TypeScript".into(),
13004 matcher: LanguageMatcher {
13005 path_suffixes: vec!["ts".to_string()],
13006 ..Default::default()
13007 },
13008 ..LanguageConfig::default()
13009 },
13010 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13011 )));
13012 update_test_language_settings(cx, |settings| {
13013 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13014 });
13015 let mut fake_servers = language_registry.register_fake_lsp(
13016 "TypeScript",
13017 FakeLspAdapter {
13018 capabilities: lsp::ServerCapabilities {
13019 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13020 ..Default::default()
13021 },
13022 ..Default::default()
13023 },
13024 );
13025
13026 let buffer = project
13027 .update(cx, |project, cx| {
13028 project.open_local_buffer(path!("/file.ts"), cx)
13029 })
13030 .await
13031 .unwrap();
13032
13033 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13034 let (editor, cx) = cx.add_window_view(|window, cx| {
13035 build_editor_with_project(project.clone(), buffer, window, cx)
13036 });
13037 editor.update_in(cx, |editor, window, cx| {
13038 editor.set_text(
13039 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13040 window,
13041 cx,
13042 )
13043 });
13044
13045 cx.executor().start_waiting();
13046 let fake_server = fake_servers.next().await.unwrap();
13047
13048 let format = editor
13049 .update_in(cx, |editor, window, cx| {
13050 editor.perform_code_action_kind(
13051 project.clone(),
13052 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13053 window,
13054 cx,
13055 )
13056 })
13057 .unwrap();
13058 fake_server
13059 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13060 assert_eq!(
13061 params.text_document.uri,
13062 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13063 );
13064 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13065 lsp::CodeAction {
13066 title: "Organize Imports".to_string(),
13067 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13068 edit: Some(lsp::WorkspaceEdit {
13069 changes: Some(
13070 [(
13071 params.text_document.uri.clone(),
13072 vec![lsp::TextEdit::new(
13073 lsp::Range::new(
13074 lsp::Position::new(1, 0),
13075 lsp::Position::new(2, 0),
13076 ),
13077 "".to_string(),
13078 )],
13079 )]
13080 .into_iter()
13081 .collect(),
13082 ),
13083 ..Default::default()
13084 }),
13085 ..Default::default()
13086 },
13087 )]))
13088 })
13089 .next()
13090 .await;
13091 cx.executor().start_waiting();
13092 format.await;
13093 assert_eq!(
13094 editor.update(cx, |editor, cx| editor.text(cx)),
13095 "import { a } from 'module';\n\nconst x = a;\n"
13096 );
13097
13098 editor.update_in(cx, |editor, window, cx| {
13099 editor.set_text(
13100 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13101 window,
13102 cx,
13103 )
13104 });
13105 // Ensure we don't lock if code action hangs.
13106 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13107 move |params, _| async move {
13108 assert_eq!(
13109 params.text_document.uri,
13110 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13111 );
13112 futures::future::pending::<()>().await;
13113 unreachable!()
13114 },
13115 );
13116 let format = editor
13117 .update_in(cx, |editor, window, cx| {
13118 editor.perform_code_action_kind(
13119 project,
13120 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13121 window,
13122 cx,
13123 )
13124 })
13125 .unwrap();
13126 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13127 cx.executor().start_waiting();
13128 format.await;
13129 assert_eq!(
13130 editor.update(cx, |editor, cx| editor.text(cx)),
13131 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13132 );
13133}
13134
13135#[gpui::test]
13136async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13137 init_test(cx, |_| {});
13138
13139 let mut cx = EditorLspTestContext::new_rust(
13140 lsp::ServerCapabilities {
13141 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13142 ..Default::default()
13143 },
13144 cx,
13145 )
13146 .await;
13147
13148 cx.set_state(indoc! {"
13149 one.twoˇ
13150 "});
13151
13152 // The format request takes a long time. When it completes, it inserts
13153 // a newline and an indent before the `.`
13154 cx.lsp
13155 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13156 let executor = cx.background_executor().clone();
13157 async move {
13158 executor.timer(Duration::from_millis(100)).await;
13159 Ok(Some(vec![lsp::TextEdit {
13160 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13161 new_text: "\n ".into(),
13162 }]))
13163 }
13164 });
13165
13166 // Submit a format request.
13167 let format_1 = cx
13168 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13169 .unwrap();
13170 cx.executor().run_until_parked();
13171
13172 // Submit a second format request.
13173 let format_2 = cx
13174 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13175 .unwrap();
13176 cx.executor().run_until_parked();
13177
13178 // Wait for both format requests to complete
13179 cx.executor().advance_clock(Duration::from_millis(200));
13180 cx.executor().start_waiting();
13181 format_1.await.unwrap();
13182 cx.executor().start_waiting();
13183 format_2.await.unwrap();
13184
13185 // The formatting edits only happens once.
13186 cx.assert_editor_state(indoc! {"
13187 one
13188 .twoˇ
13189 "});
13190}
13191
13192#[gpui::test]
13193async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13194 init_test(cx, |settings| {
13195 settings.defaults.formatter = Some(FormatterList::default())
13196 });
13197
13198 let mut cx = EditorLspTestContext::new_rust(
13199 lsp::ServerCapabilities {
13200 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13201 ..Default::default()
13202 },
13203 cx,
13204 )
13205 .await;
13206
13207 // Record which buffer changes have been sent to the language server
13208 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13209 cx.lsp
13210 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13211 let buffer_changes = buffer_changes.clone();
13212 move |params, _| {
13213 buffer_changes.lock().extend(
13214 params
13215 .content_changes
13216 .into_iter()
13217 .map(|e| (e.range.unwrap(), e.text)),
13218 );
13219 }
13220 });
13221 // Handle formatting requests to the language server.
13222 cx.lsp
13223 .set_request_handler::<lsp::request::Formatting, _, _>({
13224 let buffer_changes = buffer_changes.clone();
13225 move |_, _| {
13226 let buffer_changes = buffer_changes.clone();
13227 // Insert blank lines between each line of the buffer.
13228 async move {
13229 // When formatting is requested, trailing whitespace has already been stripped,
13230 // and the trailing newline has already been added.
13231 assert_eq!(
13232 &buffer_changes.lock()[1..],
13233 &[
13234 (
13235 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13236 "".into()
13237 ),
13238 (
13239 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13240 "".into()
13241 ),
13242 (
13243 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13244 "\n".into()
13245 ),
13246 ]
13247 );
13248
13249 Ok(Some(vec![
13250 lsp::TextEdit {
13251 range: lsp::Range::new(
13252 lsp::Position::new(1, 0),
13253 lsp::Position::new(1, 0),
13254 ),
13255 new_text: "\n".into(),
13256 },
13257 lsp::TextEdit {
13258 range: lsp::Range::new(
13259 lsp::Position::new(2, 0),
13260 lsp::Position::new(2, 0),
13261 ),
13262 new_text: "\n".into(),
13263 },
13264 ]))
13265 }
13266 }
13267 });
13268
13269 // Set up a buffer white some trailing whitespace and no trailing newline.
13270 cx.set_state(
13271 &[
13272 "one ", //
13273 "twoˇ", //
13274 "three ", //
13275 "four", //
13276 ]
13277 .join("\n"),
13278 );
13279 cx.run_until_parked();
13280
13281 // Submit a format request.
13282 let format = cx
13283 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13284 .unwrap();
13285
13286 cx.run_until_parked();
13287 // After formatting the buffer, the trailing whitespace is stripped,
13288 // a newline is appended, and the edits provided by the language server
13289 // have been applied.
13290 format.await.unwrap();
13291
13292 cx.assert_editor_state(
13293 &[
13294 "one", //
13295 "", //
13296 "twoˇ", //
13297 "", //
13298 "three", //
13299 "four", //
13300 "", //
13301 ]
13302 .join("\n"),
13303 );
13304
13305 // Undoing the formatting undoes the trailing whitespace removal, the
13306 // trailing newline, and the LSP edits.
13307 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13308 cx.assert_editor_state(
13309 &[
13310 "one ", //
13311 "twoˇ", //
13312 "three ", //
13313 "four", //
13314 ]
13315 .join("\n"),
13316 );
13317}
13318
13319#[gpui::test]
13320async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13321 cx: &mut TestAppContext,
13322) {
13323 init_test(cx, |_| {});
13324
13325 cx.update(|cx| {
13326 cx.update_global::<SettingsStore, _>(|settings, cx| {
13327 settings.update_user_settings(cx, |settings| {
13328 settings.editor.auto_signature_help = Some(true);
13329 });
13330 });
13331 });
13332
13333 let mut cx = EditorLspTestContext::new_rust(
13334 lsp::ServerCapabilities {
13335 signature_help_provider: Some(lsp::SignatureHelpOptions {
13336 ..Default::default()
13337 }),
13338 ..Default::default()
13339 },
13340 cx,
13341 )
13342 .await;
13343
13344 let language = Language::new(
13345 LanguageConfig {
13346 name: "Rust".into(),
13347 brackets: BracketPairConfig {
13348 pairs: vec![
13349 BracketPair {
13350 start: "{".to_string(),
13351 end: "}".to_string(),
13352 close: true,
13353 surround: true,
13354 newline: true,
13355 },
13356 BracketPair {
13357 start: "(".to_string(),
13358 end: ")".to_string(),
13359 close: true,
13360 surround: true,
13361 newline: true,
13362 },
13363 BracketPair {
13364 start: "/*".to_string(),
13365 end: " */".to_string(),
13366 close: true,
13367 surround: true,
13368 newline: true,
13369 },
13370 BracketPair {
13371 start: "[".to_string(),
13372 end: "]".to_string(),
13373 close: false,
13374 surround: false,
13375 newline: true,
13376 },
13377 BracketPair {
13378 start: "\"".to_string(),
13379 end: "\"".to_string(),
13380 close: true,
13381 surround: true,
13382 newline: false,
13383 },
13384 BracketPair {
13385 start: "<".to_string(),
13386 end: ">".to_string(),
13387 close: false,
13388 surround: true,
13389 newline: true,
13390 },
13391 ],
13392 ..Default::default()
13393 },
13394 autoclose_before: "})]".to_string(),
13395 ..Default::default()
13396 },
13397 Some(tree_sitter_rust::LANGUAGE.into()),
13398 );
13399 let language = Arc::new(language);
13400
13401 cx.language_registry().add(language.clone());
13402 cx.update_buffer(|buffer, cx| {
13403 buffer.set_language(Some(language), cx);
13404 });
13405
13406 cx.set_state(
13407 &r#"
13408 fn main() {
13409 sampleˇ
13410 }
13411 "#
13412 .unindent(),
13413 );
13414
13415 cx.update_editor(|editor, window, cx| {
13416 editor.handle_input("(", window, cx);
13417 });
13418 cx.assert_editor_state(
13419 &"
13420 fn main() {
13421 sample(ˇ)
13422 }
13423 "
13424 .unindent(),
13425 );
13426
13427 let mocked_response = lsp::SignatureHelp {
13428 signatures: vec![lsp::SignatureInformation {
13429 label: "fn sample(param1: u8, param2: u8)".to_string(),
13430 documentation: None,
13431 parameters: Some(vec![
13432 lsp::ParameterInformation {
13433 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13434 documentation: None,
13435 },
13436 lsp::ParameterInformation {
13437 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13438 documentation: None,
13439 },
13440 ]),
13441 active_parameter: None,
13442 }],
13443 active_signature: Some(0),
13444 active_parameter: Some(0),
13445 };
13446 handle_signature_help_request(&mut cx, mocked_response).await;
13447
13448 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13449 .await;
13450
13451 cx.editor(|editor, _, _| {
13452 let signature_help_state = editor.signature_help_state.popover().cloned();
13453 let signature = signature_help_state.unwrap();
13454 assert_eq!(
13455 signature.signatures[signature.current_signature].label,
13456 "fn sample(param1: u8, param2: u8)"
13457 );
13458 });
13459}
13460
13461#[gpui::test]
13462async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13463 init_test(cx, |_| {});
13464
13465 cx.update(|cx| {
13466 cx.update_global::<SettingsStore, _>(|settings, cx| {
13467 settings.update_user_settings(cx, |settings| {
13468 settings.editor.auto_signature_help = Some(false);
13469 settings.editor.show_signature_help_after_edits = Some(false);
13470 });
13471 });
13472 });
13473
13474 let mut cx = EditorLspTestContext::new_rust(
13475 lsp::ServerCapabilities {
13476 signature_help_provider: Some(lsp::SignatureHelpOptions {
13477 ..Default::default()
13478 }),
13479 ..Default::default()
13480 },
13481 cx,
13482 )
13483 .await;
13484
13485 let language = Language::new(
13486 LanguageConfig {
13487 name: "Rust".into(),
13488 brackets: BracketPairConfig {
13489 pairs: vec![
13490 BracketPair {
13491 start: "{".to_string(),
13492 end: "}".to_string(),
13493 close: true,
13494 surround: true,
13495 newline: true,
13496 },
13497 BracketPair {
13498 start: "(".to_string(),
13499 end: ")".to_string(),
13500 close: true,
13501 surround: true,
13502 newline: true,
13503 },
13504 BracketPair {
13505 start: "/*".to_string(),
13506 end: " */".to_string(),
13507 close: true,
13508 surround: true,
13509 newline: true,
13510 },
13511 BracketPair {
13512 start: "[".to_string(),
13513 end: "]".to_string(),
13514 close: false,
13515 surround: false,
13516 newline: true,
13517 },
13518 BracketPair {
13519 start: "\"".to_string(),
13520 end: "\"".to_string(),
13521 close: true,
13522 surround: true,
13523 newline: false,
13524 },
13525 BracketPair {
13526 start: "<".to_string(),
13527 end: ">".to_string(),
13528 close: false,
13529 surround: true,
13530 newline: true,
13531 },
13532 ],
13533 ..Default::default()
13534 },
13535 autoclose_before: "})]".to_string(),
13536 ..Default::default()
13537 },
13538 Some(tree_sitter_rust::LANGUAGE.into()),
13539 );
13540 let language = Arc::new(language);
13541
13542 cx.language_registry().add(language.clone());
13543 cx.update_buffer(|buffer, cx| {
13544 buffer.set_language(Some(language), cx);
13545 });
13546
13547 // Ensure that signature_help is not called when no signature help is enabled.
13548 cx.set_state(
13549 &r#"
13550 fn main() {
13551 sampleˇ
13552 }
13553 "#
13554 .unindent(),
13555 );
13556 cx.update_editor(|editor, window, cx| {
13557 editor.handle_input("(", window, cx);
13558 });
13559 cx.assert_editor_state(
13560 &"
13561 fn main() {
13562 sample(ˇ)
13563 }
13564 "
13565 .unindent(),
13566 );
13567 cx.editor(|editor, _, _| {
13568 assert!(editor.signature_help_state.task().is_none());
13569 });
13570
13571 let mocked_response = lsp::SignatureHelp {
13572 signatures: vec![lsp::SignatureInformation {
13573 label: "fn sample(param1: u8, param2: u8)".to_string(),
13574 documentation: None,
13575 parameters: Some(vec![
13576 lsp::ParameterInformation {
13577 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13578 documentation: None,
13579 },
13580 lsp::ParameterInformation {
13581 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13582 documentation: None,
13583 },
13584 ]),
13585 active_parameter: None,
13586 }],
13587 active_signature: Some(0),
13588 active_parameter: Some(0),
13589 };
13590
13591 // Ensure that signature_help is called when enabled afte edits
13592 cx.update(|_, cx| {
13593 cx.update_global::<SettingsStore, _>(|settings, cx| {
13594 settings.update_user_settings(cx, |settings| {
13595 settings.editor.auto_signature_help = Some(false);
13596 settings.editor.show_signature_help_after_edits = Some(true);
13597 });
13598 });
13599 });
13600 cx.set_state(
13601 &r#"
13602 fn main() {
13603 sampleˇ
13604 }
13605 "#
13606 .unindent(),
13607 );
13608 cx.update_editor(|editor, window, cx| {
13609 editor.handle_input("(", window, cx);
13610 });
13611 cx.assert_editor_state(
13612 &"
13613 fn main() {
13614 sample(ˇ)
13615 }
13616 "
13617 .unindent(),
13618 );
13619 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13620 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13621 .await;
13622 cx.update_editor(|editor, _, _| {
13623 let signature_help_state = editor.signature_help_state.popover().cloned();
13624 assert!(signature_help_state.is_some());
13625 let signature = signature_help_state.unwrap();
13626 assert_eq!(
13627 signature.signatures[signature.current_signature].label,
13628 "fn sample(param1: u8, param2: u8)"
13629 );
13630 editor.signature_help_state = SignatureHelpState::default();
13631 });
13632
13633 // Ensure that signature_help is called when auto signature help override is enabled
13634 cx.update(|_, cx| {
13635 cx.update_global::<SettingsStore, _>(|settings, cx| {
13636 settings.update_user_settings(cx, |settings| {
13637 settings.editor.auto_signature_help = Some(true);
13638 settings.editor.show_signature_help_after_edits = Some(false);
13639 });
13640 });
13641 });
13642 cx.set_state(
13643 &r#"
13644 fn main() {
13645 sampleˇ
13646 }
13647 "#
13648 .unindent(),
13649 );
13650 cx.update_editor(|editor, window, cx| {
13651 editor.handle_input("(", window, cx);
13652 });
13653 cx.assert_editor_state(
13654 &"
13655 fn main() {
13656 sample(ˇ)
13657 }
13658 "
13659 .unindent(),
13660 );
13661 handle_signature_help_request(&mut cx, mocked_response).await;
13662 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13663 .await;
13664 cx.editor(|editor, _, _| {
13665 let signature_help_state = editor.signature_help_state.popover().cloned();
13666 assert!(signature_help_state.is_some());
13667 let signature = signature_help_state.unwrap();
13668 assert_eq!(
13669 signature.signatures[signature.current_signature].label,
13670 "fn sample(param1: u8, param2: u8)"
13671 );
13672 });
13673}
13674
13675#[gpui::test]
13676async fn test_signature_help(cx: &mut TestAppContext) {
13677 init_test(cx, |_| {});
13678 cx.update(|cx| {
13679 cx.update_global::<SettingsStore, _>(|settings, cx| {
13680 settings.update_user_settings(cx, |settings| {
13681 settings.editor.auto_signature_help = Some(true);
13682 });
13683 });
13684 });
13685
13686 let mut cx = EditorLspTestContext::new_rust(
13687 lsp::ServerCapabilities {
13688 signature_help_provider: Some(lsp::SignatureHelpOptions {
13689 ..Default::default()
13690 }),
13691 ..Default::default()
13692 },
13693 cx,
13694 )
13695 .await;
13696
13697 // A test that directly calls `show_signature_help`
13698 cx.update_editor(|editor, window, cx| {
13699 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13700 });
13701
13702 let mocked_response = lsp::SignatureHelp {
13703 signatures: vec![lsp::SignatureInformation {
13704 label: "fn sample(param1: u8, param2: u8)".to_string(),
13705 documentation: None,
13706 parameters: Some(vec![
13707 lsp::ParameterInformation {
13708 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13709 documentation: None,
13710 },
13711 lsp::ParameterInformation {
13712 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13713 documentation: None,
13714 },
13715 ]),
13716 active_parameter: None,
13717 }],
13718 active_signature: Some(0),
13719 active_parameter: Some(0),
13720 };
13721 handle_signature_help_request(&mut cx, mocked_response).await;
13722
13723 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13724 .await;
13725
13726 cx.editor(|editor, _, _| {
13727 let signature_help_state = editor.signature_help_state.popover().cloned();
13728 assert!(signature_help_state.is_some());
13729 let signature = signature_help_state.unwrap();
13730 assert_eq!(
13731 signature.signatures[signature.current_signature].label,
13732 "fn sample(param1: u8, param2: u8)"
13733 );
13734 });
13735
13736 // When exiting outside from inside the brackets, `signature_help` is closed.
13737 cx.set_state(indoc! {"
13738 fn main() {
13739 sample(ˇ);
13740 }
13741
13742 fn sample(param1: u8, param2: u8) {}
13743 "});
13744
13745 cx.update_editor(|editor, window, cx| {
13746 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13747 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13748 });
13749 });
13750
13751 let mocked_response = lsp::SignatureHelp {
13752 signatures: Vec::new(),
13753 active_signature: None,
13754 active_parameter: None,
13755 };
13756 handle_signature_help_request(&mut cx, mocked_response).await;
13757
13758 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13759 .await;
13760
13761 cx.editor(|editor, _, _| {
13762 assert!(!editor.signature_help_state.is_shown());
13763 });
13764
13765 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13766 cx.set_state(indoc! {"
13767 fn main() {
13768 sample(ˇ);
13769 }
13770
13771 fn sample(param1: u8, param2: u8) {}
13772 "});
13773
13774 let mocked_response = lsp::SignatureHelp {
13775 signatures: vec![lsp::SignatureInformation {
13776 label: "fn sample(param1: u8, param2: u8)".to_string(),
13777 documentation: None,
13778 parameters: Some(vec![
13779 lsp::ParameterInformation {
13780 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13781 documentation: None,
13782 },
13783 lsp::ParameterInformation {
13784 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13785 documentation: None,
13786 },
13787 ]),
13788 active_parameter: None,
13789 }],
13790 active_signature: Some(0),
13791 active_parameter: Some(0),
13792 };
13793 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13794 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13795 .await;
13796 cx.editor(|editor, _, _| {
13797 assert!(editor.signature_help_state.is_shown());
13798 });
13799
13800 // Restore the popover with more parameter input
13801 cx.set_state(indoc! {"
13802 fn main() {
13803 sample(param1, param2ˇ);
13804 }
13805
13806 fn sample(param1: u8, param2: u8) {}
13807 "});
13808
13809 let mocked_response = lsp::SignatureHelp {
13810 signatures: vec![lsp::SignatureInformation {
13811 label: "fn sample(param1: u8, param2: u8)".to_string(),
13812 documentation: None,
13813 parameters: Some(vec![
13814 lsp::ParameterInformation {
13815 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13816 documentation: None,
13817 },
13818 lsp::ParameterInformation {
13819 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13820 documentation: None,
13821 },
13822 ]),
13823 active_parameter: None,
13824 }],
13825 active_signature: Some(0),
13826 active_parameter: Some(1),
13827 };
13828 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13829 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13830 .await;
13831
13832 // When selecting a range, the popover is gone.
13833 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13834 cx.update_editor(|editor, window, cx| {
13835 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13836 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13837 })
13838 });
13839 cx.assert_editor_state(indoc! {"
13840 fn main() {
13841 sample(param1, «ˇparam2»);
13842 }
13843
13844 fn sample(param1: u8, param2: u8) {}
13845 "});
13846 cx.editor(|editor, _, _| {
13847 assert!(!editor.signature_help_state.is_shown());
13848 });
13849
13850 // When unselecting again, the popover is back if within the brackets.
13851 cx.update_editor(|editor, window, cx| {
13852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13853 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13854 })
13855 });
13856 cx.assert_editor_state(indoc! {"
13857 fn main() {
13858 sample(param1, ˇparam2);
13859 }
13860
13861 fn sample(param1: u8, param2: u8) {}
13862 "});
13863 handle_signature_help_request(&mut cx, mocked_response).await;
13864 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13865 .await;
13866 cx.editor(|editor, _, _| {
13867 assert!(editor.signature_help_state.is_shown());
13868 });
13869
13870 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13871 cx.update_editor(|editor, window, cx| {
13872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13873 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13874 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13875 })
13876 });
13877 cx.assert_editor_state(indoc! {"
13878 fn main() {
13879 sample(param1, ˇparam2);
13880 }
13881
13882 fn sample(param1: u8, param2: u8) {}
13883 "});
13884
13885 let mocked_response = lsp::SignatureHelp {
13886 signatures: vec![lsp::SignatureInformation {
13887 label: "fn sample(param1: u8, param2: u8)".to_string(),
13888 documentation: None,
13889 parameters: Some(vec![
13890 lsp::ParameterInformation {
13891 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13892 documentation: None,
13893 },
13894 lsp::ParameterInformation {
13895 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13896 documentation: None,
13897 },
13898 ]),
13899 active_parameter: None,
13900 }],
13901 active_signature: Some(0),
13902 active_parameter: Some(1),
13903 };
13904 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13905 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13906 .await;
13907 cx.update_editor(|editor, _, cx| {
13908 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13909 });
13910 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13911 .await;
13912 cx.update_editor(|editor, window, cx| {
13913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13914 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13915 })
13916 });
13917 cx.assert_editor_state(indoc! {"
13918 fn main() {
13919 sample(param1, «ˇparam2»);
13920 }
13921
13922 fn sample(param1: u8, param2: u8) {}
13923 "});
13924 cx.update_editor(|editor, window, cx| {
13925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13926 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13927 })
13928 });
13929 cx.assert_editor_state(indoc! {"
13930 fn main() {
13931 sample(param1, ˇparam2);
13932 }
13933
13934 fn sample(param1: u8, param2: u8) {}
13935 "});
13936 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13937 .await;
13938}
13939
13940#[gpui::test]
13941async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13942 init_test(cx, |_| {});
13943
13944 let mut cx = EditorLspTestContext::new_rust(
13945 lsp::ServerCapabilities {
13946 signature_help_provider: Some(lsp::SignatureHelpOptions {
13947 ..Default::default()
13948 }),
13949 ..Default::default()
13950 },
13951 cx,
13952 )
13953 .await;
13954
13955 cx.set_state(indoc! {"
13956 fn main() {
13957 overloadedˇ
13958 }
13959 "});
13960
13961 cx.update_editor(|editor, window, cx| {
13962 editor.handle_input("(", window, cx);
13963 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13964 });
13965
13966 // Mock response with 3 signatures
13967 let mocked_response = lsp::SignatureHelp {
13968 signatures: vec![
13969 lsp::SignatureInformation {
13970 label: "fn overloaded(x: i32)".to_string(),
13971 documentation: None,
13972 parameters: Some(vec![lsp::ParameterInformation {
13973 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13974 documentation: None,
13975 }]),
13976 active_parameter: None,
13977 },
13978 lsp::SignatureInformation {
13979 label: "fn overloaded(x: i32, y: i32)".to_string(),
13980 documentation: None,
13981 parameters: Some(vec![
13982 lsp::ParameterInformation {
13983 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13984 documentation: None,
13985 },
13986 lsp::ParameterInformation {
13987 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13988 documentation: None,
13989 },
13990 ]),
13991 active_parameter: None,
13992 },
13993 lsp::SignatureInformation {
13994 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13995 documentation: None,
13996 parameters: Some(vec![
13997 lsp::ParameterInformation {
13998 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13999 documentation: None,
14000 },
14001 lsp::ParameterInformation {
14002 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14003 documentation: None,
14004 },
14005 lsp::ParameterInformation {
14006 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14007 documentation: None,
14008 },
14009 ]),
14010 active_parameter: None,
14011 },
14012 ],
14013 active_signature: Some(1),
14014 active_parameter: Some(0),
14015 };
14016 handle_signature_help_request(&mut cx, mocked_response).await;
14017
14018 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14019 .await;
14020
14021 // Verify we have multiple signatures and the right one is selected
14022 cx.editor(|editor, _, _| {
14023 let popover = editor.signature_help_state.popover().cloned().unwrap();
14024 assert_eq!(popover.signatures.len(), 3);
14025 // active_signature was 1, so that should be the current
14026 assert_eq!(popover.current_signature, 1);
14027 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14028 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14029 assert_eq!(
14030 popover.signatures[2].label,
14031 "fn overloaded(x: i32, y: i32, z: i32)"
14032 );
14033 });
14034
14035 // Test navigation functionality
14036 cx.update_editor(|editor, window, cx| {
14037 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14038 });
14039
14040 cx.editor(|editor, _, _| {
14041 let popover = editor.signature_help_state.popover().cloned().unwrap();
14042 assert_eq!(popover.current_signature, 2);
14043 });
14044
14045 // Test wrap around
14046 cx.update_editor(|editor, window, cx| {
14047 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14048 });
14049
14050 cx.editor(|editor, _, _| {
14051 let popover = editor.signature_help_state.popover().cloned().unwrap();
14052 assert_eq!(popover.current_signature, 0);
14053 });
14054
14055 // Test previous navigation
14056 cx.update_editor(|editor, window, cx| {
14057 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14058 });
14059
14060 cx.editor(|editor, _, _| {
14061 let popover = editor.signature_help_state.popover().cloned().unwrap();
14062 assert_eq!(popover.current_signature, 2);
14063 });
14064}
14065
14066#[gpui::test]
14067async fn test_completion_mode(cx: &mut TestAppContext) {
14068 init_test(cx, |_| {});
14069 let mut cx = EditorLspTestContext::new_rust(
14070 lsp::ServerCapabilities {
14071 completion_provider: Some(lsp::CompletionOptions {
14072 resolve_provider: Some(true),
14073 ..Default::default()
14074 }),
14075 ..Default::default()
14076 },
14077 cx,
14078 )
14079 .await;
14080
14081 struct Run {
14082 run_description: &'static str,
14083 initial_state: String,
14084 buffer_marked_text: String,
14085 completion_label: &'static str,
14086 completion_text: &'static str,
14087 expected_with_insert_mode: String,
14088 expected_with_replace_mode: String,
14089 expected_with_replace_subsequence_mode: String,
14090 expected_with_replace_suffix_mode: String,
14091 }
14092
14093 let runs = [
14094 Run {
14095 run_description: "Start of word matches completion text",
14096 initial_state: "before ediˇ after".into(),
14097 buffer_marked_text: "before <edi|> after".into(),
14098 completion_label: "editor",
14099 completion_text: "editor",
14100 expected_with_insert_mode: "before editorˇ after".into(),
14101 expected_with_replace_mode: "before editorˇ after".into(),
14102 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14103 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14104 },
14105 Run {
14106 run_description: "Accept same text at the middle of the word",
14107 initial_state: "before ediˇtor after".into(),
14108 buffer_marked_text: "before <edi|tor> after".into(),
14109 completion_label: "editor",
14110 completion_text: "editor",
14111 expected_with_insert_mode: "before editorˇtor after".into(),
14112 expected_with_replace_mode: "before editorˇ after".into(),
14113 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14114 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14115 },
14116 Run {
14117 run_description: "End of word matches completion text -- cursor at end",
14118 initial_state: "before torˇ after".into(),
14119 buffer_marked_text: "before <tor|> after".into(),
14120 completion_label: "editor",
14121 completion_text: "editor",
14122 expected_with_insert_mode: "before editorˇ after".into(),
14123 expected_with_replace_mode: "before editorˇ after".into(),
14124 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14125 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14126 },
14127 Run {
14128 run_description: "End of word matches completion text -- cursor at start",
14129 initial_state: "before ˇtor after".into(),
14130 buffer_marked_text: "before <|tor> after".into(),
14131 completion_label: "editor",
14132 completion_text: "editor",
14133 expected_with_insert_mode: "before editorˇtor after".into(),
14134 expected_with_replace_mode: "before editorˇ after".into(),
14135 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14136 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14137 },
14138 Run {
14139 run_description: "Prepend text containing whitespace",
14140 initial_state: "pˇfield: bool".into(),
14141 buffer_marked_text: "<p|field>: bool".into(),
14142 completion_label: "pub ",
14143 completion_text: "pub ",
14144 expected_with_insert_mode: "pub ˇfield: bool".into(),
14145 expected_with_replace_mode: "pub ˇ: bool".into(),
14146 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14147 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14148 },
14149 Run {
14150 run_description: "Add element to start of list",
14151 initial_state: "[element_ˇelement_2]".into(),
14152 buffer_marked_text: "[<element_|element_2>]".into(),
14153 completion_label: "element_1",
14154 completion_text: "element_1",
14155 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14156 expected_with_replace_mode: "[element_1ˇ]".into(),
14157 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14158 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14159 },
14160 Run {
14161 run_description: "Add element to start of list -- first and second elements are equal",
14162 initial_state: "[elˇelement]".into(),
14163 buffer_marked_text: "[<el|element>]".into(),
14164 completion_label: "element",
14165 completion_text: "element",
14166 expected_with_insert_mode: "[elementˇelement]".into(),
14167 expected_with_replace_mode: "[elementˇ]".into(),
14168 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14169 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14170 },
14171 Run {
14172 run_description: "Ends with matching suffix",
14173 initial_state: "SubˇError".into(),
14174 buffer_marked_text: "<Sub|Error>".into(),
14175 completion_label: "SubscriptionError",
14176 completion_text: "SubscriptionError",
14177 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14178 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14179 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14180 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14181 },
14182 Run {
14183 run_description: "Suffix is a subsequence -- contiguous",
14184 initial_state: "SubˇErr".into(),
14185 buffer_marked_text: "<Sub|Err>".into(),
14186 completion_label: "SubscriptionError",
14187 completion_text: "SubscriptionError",
14188 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14189 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14190 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14191 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14192 },
14193 Run {
14194 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14195 initial_state: "Suˇscrirr".into(),
14196 buffer_marked_text: "<Su|scrirr>".into(),
14197 completion_label: "SubscriptionError",
14198 completion_text: "SubscriptionError",
14199 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14200 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14201 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14202 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14203 },
14204 Run {
14205 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14206 initial_state: "foo(indˇix)".into(),
14207 buffer_marked_text: "foo(<ind|ix>)".into(),
14208 completion_label: "node_index",
14209 completion_text: "node_index",
14210 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14211 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14212 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14213 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14214 },
14215 Run {
14216 run_description: "Replace range ends before cursor - should extend to cursor",
14217 initial_state: "before editˇo after".into(),
14218 buffer_marked_text: "before <{ed}>it|o after".into(),
14219 completion_label: "editor",
14220 completion_text: "editor",
14221 expected_with_insert_mode: "before editorˇo after".into(),
14222 expected_with_replace_mode: "before editorˇo after".into(),
14223 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14224 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14225 },
14226 Run {
14227 run_description: "Uses label for suffix matching",
14228 initial_state: "before ediˇtor after".into(),
14229 buffer_marked_text: "before <edi|tor> after".into(),
14230 completion_label: "editor",
14231 completion_text: "editor()",
14232 expected_with_insert_mode: "before editor()ˇtor after".into(),
14233 expected_with_replace_mode: "before editor()ˇ after".into(),
14234 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14235 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14236 },
14237 Run {
14238 run_description: "Case insensitive subsequence and suffix matching",
14239 initial_state: "before EDiˇtoR after".into(),
14240 buffer_marked_text: "before <EDi|toR> after".into(),
14241 completion_label: "editor",
14242 completion_text: "editor",
14243 expected_with_insert_mode: "before editorˇtoR after".into(),
14244 expected_with_replace_mode: "before editorˇ after".into(),
14245 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14246 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14247 },
14248 ];
14249
14250 for run in runs {
14251 let run_variations = [
14252 (LspInsertMode::Insert, run.expected_with_insert_mode),
14253 (LspInsertMode::Replace, run.expected_with_replace_mode),
14254 (
14255 LspInsertMode::ReplaceSubsequence,
14256 run.expected_with_replace_subsequence_mode,
14257 ),
14258 (
14259 LspInsertMode::ReplaceSuffix,
14260 run.expected_with_replace_suffix_mode,
14261 ),
14262 ];
14263
14264 for (lsp_insert_mode, expected_text) in run_variations {
14265 eprintln!(
14266 "run = {:?}, mode = {lsp_insert_mode:.?}",
14267 run.run_description,
14268 );
14269
14270 update_test_language_settings(&mut cx, |settings| {
14271 settings.defaults.completions = Some(CompletionSettingsContent {
14272 lsp_insert_mode: Some(lsp_insert_mode),
14273 words: Some(WordsCompletionMode::Disabled),
14274 words_min_length: Some(0),
14275 ..Default::default()
14276 });
14277 });
14278
14279 cx.set_state(&run.initial_state);
14280 cx.update_editor(|editor, window, cx| {
14281 editor.show_completions(&ShowCompletions, window, cx);
14282 });
14283
14284 let counter = Arc::new(AtomicUsize::new(0));
14285 handle_completion_request_with_insert_and_replace(
14286 &mut cx,
14287 &run.buffer_marked_text,
14288 vec![(run.completion_label, run.completion_text)],
14289 counter.clone(),
14290 )
14291 .await;
14292 cx.condition(|editor, _| editor.context_menu_visible())
14293 .await;
14294 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14295
14296 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14297 editor
14298 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14299 .unwrap()
14300 });
14301 cx.assert_editor_state(&expected_text);
14302 handle_resolve_completion_request(&mut cx, None).await;
14303 apply_additional_edits.await.unwrap();
14304 }
14305 }
14306}
14307
14308#[gpui::test]
14309async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14310 init_test(cx, |_| {});
14311 let mut cx = EditorLspTestContext::new_rust(
14312 lsp::ServerCapabilities {
14313 completion_provider: Some(lsp::CompletionOptions {
14314 resolve_provider: Some(true),
14315 ..Default::default()
14316 }),
14317 ..Default::default()
14318 },
14319 cx,
14320 )
14321 .await;
14322
14323 let initial_state = "SubˇError";
14324 let buffer_marked_text = "<Sub|Error>";
14325 let completion_text = "SubscriptionError";
14326 let expected_with_insert_mode = "SubscriptionErrorˇError";
14327 let expected_with_replace_mode = "SubscriptionErrorˇ";
14328
14329 update_test_language_settings(&mut cx, |settings| {
14330 settings.defaults.completions = Some(CompletionSettingsContent {
14331 words: Some(WordsCompletionMode::Disabled),
14332 words_min_length: Some(0),
14333 // set the opposite here to ensure that the action is overriding the default behavior
14334 lsp_insert_mode: Some(LspInsertMode::Insert),
14335 ..Default::default()
14336 });
14337 });
14338
14339 cx.set_state(initial_state);
14340 cx.update_editor(|editor, window, cx| {
14341 editor.show_completions(&ShowCompletions, window, cx);
14342 });
14343
14344 let counter = Arc::new(AtomicUsize::new(0));
14345 handle_completion_request_with_insert_and_replace(
14346 &mut cx,
14347 buffer_marked_text,
14348 vec![(completion_text, completion_text)],
14349 counter.clone(),
14350 )
14351 .await;
14352 cx.condition(|editor, _| editor.context_menu_visible())
14353 .await;
14354 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14355
14356 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14357 editor
14358 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14359 .unwrap()
14360 });
14361 cx.assert_editor_state(expected_with_replace_mode);
14362 handle_resolve_completion_request(&mut cx, None).await;
14363 apply_additional_edits.await.unwrap();
14364
14365 update_test_language_settings(&mut cx, |settings| {
14366 settings.defaults.completions = Some(CompletionSettingsContent {
14367 words: Some(WordsCompletionMode::Disabled),
14368 words_min_length: Some(0),
14369 // set the opposite here to ensure that the action is overriding the default behavior
14370 lsp_insert_mode: Some(LspInsertMode::Replace),
14371 ..Default::default()
14372 });
14373 });
14374
14375 cx.set_state(initial_state);
14376 cx.update_editor(|editor, window, cx| {
14377 editor.show_completions(&ShowCompletions, window, cx);
14378 });
14379 handle_completion_request_with_insert_and_replace(
14380 &mut cx,
14381 buffer_marked_text,
14382 vec![(completion_text, completion_text)],
14383 counter.clone(),
14384 )
14385 .await;
14386 cx.condition(|editor, _| editor.context_menu_visible())
14387 .await;
14388 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14389
14390 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14391 editor
14392 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14393 .unwrap()
14394 });
14395 cx.assert_editor_state(expected_with_insert_mode);
14396 handle_resolve_completion_request(&mut cx, None).await;
14397 apply_additional_edits.await.unwrap();
14398}
14399
14400#[gpui::test]
14401async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14402 init_test(cx, |_| {});
14403 let mut cx = EditorLspTestContext::new_rust(
14404 lsp::ServerCapabilities {
14405 completion_provider: Some(lsp::CompletionOptions {
14406 resolve_provider: Some(true),
14407 ..Default::default()
14408 }),
14409 ..Default::default()
14410 },
14411 cx,
14412 )
14413 .await;
14414
14415 // scenario: surrounding text matches completion text
14416 let completion_text = "to_offset";
14417 let initial_state = indoc! {"
14418 1. buf.to_offˇsuffix
14419 2. buf.to_offˇsuf
14420 3. buf.to_offˇfix
14421 4. buf.to_offˇ
14422 5. into_offˇensive
14423 6. ˇsuffix
14424 7. let ˇ //
14425 8. aaˇzz
14426 9. buf.to_off«zzzzzˇ»suffix
14427 10. buf.«ˇzzzzz»suffix
14428 11. to_off«ˇzzzzz»
14429
14430 buf.to_offˇsuffix // newest cursor
14431 "};
14432 let completion_marked_buffer = indoc! {"
14433 1. buf.to_offsuffix
14434 2. buf.to_offsuf
14435 3. buf.to_offfix
14436 4. buf.to_off
14437 5. into_offensive
14438 6. suffix
14439 7. let //
14440 8. aazz
14441 9. buf.to_offzzzzzsuffix
14442 10. buf.zzzzzsuffix
14443 11. to_offzzzzz
14444
14445 buf.<to_off|suffix> // newest cursor
14446 "};
14447 let expected = indoc! {"
14448 1. buf.to_offsetˇ
14449 2. buf.to_offsetˇsuf
14450 3. buf.to_offsetˇfix
14451 4. buf.to_offsetˇ
14452 5. into_offsetˇensive
14453 6. to_offsetˇsuffix
14454 7. let to_offsetˇ //
14455 8. aato_offsetˇzz
14456 9. buf.to_offsetˇ
14457 10. buf.to_offsetˇsuffix
14458 11. to_offsetˇ
14459
14460 buf.to_offsetˇ // newest cursor
14461 "};
14462 cx.set_state(initial_state);
14463 cx.update_editor(|editor, window, cx| {
14464 editor.show_completions(&ShowCompletions, window, cx);
14465 });
14466 handle_completion_request_with_insert_and_replace(
14467 &mut cx,
14468 completion_marked_buffer,
14469 vec![(completion_text, completion_text)],
14470 Arc::new(AtomicUsize::new(0)),
14471 )
14472 .await;
14473 cx.condition(|editor, _| editor.context_menu_visible())
14474 .await;
14475 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14476 editor
14477 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14478 .unwrap()
14479 });
14480 cx.assert_editor_state(expected);
14481 handle_resolve_completion_request(&mut cx, None).await;
14482 apply_additional_edits.await.unwrap();
14483
14484 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14485 let completion_text = "foo_and_bar";
14486 let initial_state = indoc! {"
14487 1. ooanbˇ
14488 2. zooanbˇ
14489 3. ooanbˇz
14490 4. zooanbˇz
14491 5. ooanˇ
14492 6. oanbˇ
14493
14494 ooanbˇ
14495 "};
14496 let completion_marked_buffer = indoc! {"
14497 1. ooanb
14498 2. zooanb
14499 3. ooanbz
14500 4. zooanbz
14501 5. ooan
14502 6. oanb
14503
14504 <ooanb|>
14505 "};
14506 let expected = indoc! {"
14507 1. foo_and_barˇ
14508 2. zfoo_and_barˇ
14509 3. foo_and_barˇz
14510 4. zfoo_and_barˇz
14511 5. ooanfoo_and_barˇ
14512 6. oanbfoo_and_barˇ
14513
14514 foo_and_barˇ
14515 "};
14516 cx.set_state(initial_state);
14517 cx.update_editor(|editor, window, cx| {
14518 editor.show_completions(&ShowCompletions, window, cx);
14519 });
14520 handle_completion_request_with_insert_and_replace(
14521 &mut cx,
14522 completion_marked_buffer,
14523 vec![(completion_text, completion_text)],
14524 Arc::new(AtomicUsize::new(0)),
14525 )
14526 .await;
14527 cx.condition(|editor, _| editor.context_menu_visible())
14528 .await;
14529 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14530 editor
14531 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14532 .unwrap()
14533 });
14534 cx.assert_editor_state(expected);
14535 handle_resolve_completion_request(&mut cx, None).await;
14536 apply_additional_edits.await.unwrap();
14537
14538 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14539 // (expects the same as if it was inserted at the end)
14540 let completion_text = "foo_and_bar";
14541 let initial_state = indoc! {"
14542 1. ooˇanb
14543 2. zooˇanb
14544 3. ooˇanbz
14545 4. zooˇanbz
14546
14547 ooˇanb
14548 "};
14549 let completion_marked_buffer = indoc! {"
14550 1. ooanb
14551 2. zooanb
14552 3. ooanbz
14553 4. zooanbz
14554
14555 <oo|anb>
14556 "};
14557 let expected = indoc! {"
14558 1. foo_and_barˇ
14559 2. zfoo_and_barˇ
14560 3. foo_and_barˇz
14561 4. zfoo_and_barˇz
14562
14563 foo_and_barˇ
14564 "};
14565 cx.set_state(initial_state);
14566 cx.update_editor(|editor, window, cx| {
14567 editor.show_completions(&ShowCompletions, window, cx);
14568 });
14569 handle_completion_request_with_insert_and_replace(
14570 &mut cx,
14571 completion_marked_buffer,
14572 vec![(completion_text, completion_text)],
14573 Arc::new(AtomicUsize::new(0)),
14574 )
14575 .await;
14576 cx.condition(|editor, _| editor.context_menu_visible())
14577 .await;
14578 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14579 editor
14580 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14581 .unwrap()
14582 });
14583 cx.assert_editor_state(expected);
14584 handle_resolve_completion_request(&mut cx, None).await;
14585 apply_additional_edits.await.unwrap();
14586}
14587
14588// This used to crash
14589#[gpui::test]
14590async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14591 init_test(cx, |_| {});
14592
14593 let buffer_text = indoc! {"
14594 fn main() {
14595 10.satu;
14596
14597 //
14598 // separate cursors so they open in different excerpts (manually reproducible)
14599 //
14600
14601 10.satu20;
14602 }
14603 "};
14604 let multibuffer_text_with_selections = indoc! {"
14605 fn main() {
14606 10.satuˇ;
14607
14608 //
14609
14610 //
14611
14612 10.satuˇ20;
14613 }
14614 "};
14615 let expected_multibuffer = indoc! {"
14616 fn main() {
14617 10.saturating_sub()ˇ;
14618
14619 //
14620
14621 //
14622
14623 10.saturating_sub()ˇ;
14624 }
14625 "};
14626
14627 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14628 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14629
14630 let fs = FakeFs::new(cx.executor());
14631 fs.insert_tree(
14632 path!("/a"),
14633 json!({
14634 "main.rs": buffer_text,
14635 }),
14636 )
14637 .await;
14638
14639 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14640 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14641 language_registry.add(rust_lang());
14642 let mut fake_servers = language_registry.register_fake_lsp(
14643 "Rust",
14644 FakeLspAdapter {
14645 capabilities: lsp::ServerCapabilities {
14646 completion_provider: Some(lsp::CompletionOptions {
14647 resolve_provider: None,
14648 ..lsp::CompletionOptions::default()
14649 }),
14650 ..lsp::ServerCapabilities::default()
14651 },
14652 ..FakeLspAdapter::default()
14653 },
14654 );
14655 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14656 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14657 let buffer = project
14658 .update(cx, |project, cx| {
14659 project.open_local_buffer(path!("/a/main.rs"), cx)
14660 })
14661 .await
14662 .unwrap();
14663
14664 let multi_buffer = cx.new(|cx| {
14665 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14666 multi_buffer.push_excerpts(
14667 buffer.clone(),
14668 [ExcerptRange::new(0..first_excerpt_end)],
14669 cx,
14670 );
14671 multi_buffer.push_excerpts(
14672 buffer.clone(),
14673 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14674 cx,
14675 );
14676 multi_buffer
14677 });
14678
14679 let editor = workspace
14680 .update(cx, |_, window, cx| {
14681 cx.new(|cx| {
14682 Editor::new(
14683 EditorMode::Full {
14684 scale_ui_elements_with_buffer_font_size: false,
14685 show_active_line_background: false,
14686 sizing_behavior: SizingBehavior::Default,
14687 },
14688 multi_buffer.clone(),
14689 Some(project.clone()),
14690 window,
14691 cx,
14692 )
14693 })
14694 })
14695 .unwrap();
14696
14697 let pane = workspace
14698 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14699 .unwrap();
14700 pane.update_in(cx, |pane, window, cx| {
14701 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14702 });
14703
14704 let fake_server = fake_servers.next().await.unwrap();
14705
14706 editor.update_in(cx, |editor, window, cx| {
14707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14708 s.select_ranges([
14709 Point::new(1, 11)..Point::new(1, 11),
14710 Point::new(7, 11)..Point::new(7, 11),
14711 ])
14712 });
14713
14714 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14715 });
14716
14717 editor.update_in(cx, |editor, window, cx| {
14718 editor.show_completions(&ShowCompletions, window, cx);
14719 });
14720
14721 fake_server
14722 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14723 let completion_item = lsp::CompletionItem {
14724 label: "saturating_sub()".into(),
14725 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14726 lsp::InsertReplaceEdit {
14727 new_text: "saturating_sub()".to_owned(),
14728 insert: lsp::Range::new(
14729 lsp::Position::new(7, 7),
14730 lsp::Position::new(7, 11),
14731 ),
14732 replace: lsp::Range::new(
14733 lsp::Position::new(7, 7),
14734 lsp::Position::new(7, 13),
14735 ),
14736 },
14737 )),
14738 ..lsp::CompletionItem::default()
14739 };
14740
14741 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14742 })
14743 .next()
14744 .await
14745 .unwrap();
14746
14747 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14748 .await;
14749
14750 editor
14751 .update_in(cx, |editor, window, cx| {
14752 editor
14753 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14754 .unwrap()
14755 })
14756 .await
14757 .unwrap();
14758
14759 editor.update(cx, |editor, cx| {
14760 assert_text_with_selections(editor, expected_multibuffer, cx);
14761 })
14762}
14763
14764#[gpui::test]
14765async fn test_completion(cx: &mut TestAppContext) {
14766 init_test(cx, |_| {});
14767
14768 let mut cx = EditorLspTestContext::new_rust(
14769 lsp::ServerCapabilities {
14770 completion_provider: Some(lsp::CompletionOptions {
14771 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14772 resolve_provider: Some(true),
14773 ..Default::default()
14774 }),
14775 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14776 ..Default::default()
14777 },
14778 cx,
14779 )
14780 .await;
14781 let counter = Arc::new(AtomicUsize::new(0));
14782
14783 cx.set_state(indoc! {"
14784 oneˇ
14785 two
14786 three
14787 "});
14788 cx.simulate_keystroke(".");
14789 handle_completion_request(
14790 indoc! {"
14791 one.|<>
14792 two
14793 three
14794 "},
14795 vec!["first_completion", "second_completion"],
14796 true,
14797 counter.clone(),
14798 &mut cx,
14799 )
14800 .await;
14801 cx.condition(|editor, _| editor.context_menu_visible())
14802 .await;
14803 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14804
14805 let _handler = handle_signature_help_request(
14806 &mut cx,
14807 lsp::SignatureHelp {
14808 signatures: vec![lsp::SignatureInformation {
14809 label: "test signature".to_string(),
14810 documentation: None,
14811 parameters: Some(vec![lsp::ParameterInformation {
14812 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14813 documentation: None,
14814 }]),
14815 active_parameter: None,
14816 }],
14817 active_signature: None,
14818 active_parameter: None,
14819 },
14820 );
14821 cx.update_editor(|editor, window, cx| {
14822 assert!(
14823 !editor.signature_help_state.is_shown(),
14824 "No signature help was called for"
14825 );
14826 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14827 });
14828 cx.run_until_parked();
14829 cx.update_editor(|editor, _, _| {
14830 assert!(
14831 !editor.signature_help_state.is_shown(),
14832 "No signature help should be shown when completions menu is open"
14833 );
14834 });
14835
14836 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14837 editor.context_menu_next(&Default::default(), window, cx);
14838 editor
14839 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14840 .unwrap()
14841 });
14842 cx.assert_editor_state(indoc! {"
14843 one.second_completionˇ
14844 two
14845 three
14846 "});
14847
14848 handle_resolve_completion_request(
14849 &mut cx,
14850 Some(vec![
14851 (
14852 //This overlaps with the primary completion edit which is
14853 //misbehavior from the LSP spec, test that we filter it out
14854 indoc! {"
14855 one.second_ˇcompletion
14856 two
14857 threeˇ
14858 "},
14859 "overlapping additional edit",
14860 ),
14861 (
14862 indoc! {"
14863 one.second_completion
14864 two
14865 threeˇ
14866 "},
14867 "\nadditional edit",
14868 ),
14869 ]),
14870 )
14871 .await;
14872 apply_additional_edits.await.unwrap();
14873 cx.assert_editor_state(indoc! {"
14874 one.second_completionˇ
14875 two
14876 three
14877 additional edit
14878 "});
14879
14880 cx.set_state(indoc! {"
14881 one.second_completion
14882 twoˇ
14883 threeˇ
14884 additional edit
14885 "});
14886 cx.simulate_keystroke(" ");
14887 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14888 cx.simulate_keystroke("s");
14889 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14890
14891 cx.assert_editor_state(indoc! {"
14892 one.second_completion
14893 two sˇ
14894 three sˇ
14895 additional edit
14896 "});
14897 handle_completion_request(
14898 indoc! {"
14899 one.second_completion
14900 two s
14901 three <s|>
14902 additional edit
14903 "},
14904 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14905 true,
14906 counter.clone(),
14907 &mut cx,
14908 )
14909 .await;
14910 cx.condition(|editor, _| editor.context_menu_visible())
14911 .await;
14912 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14913
14914 cx.simulate_keystroke("i");
14915
14916 handle_completion_request(
14917 indoc! {"
14918 one.second_completion
14919 two si
14920 three <si|>
14921 additional edit
14922 "},
14923 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14924 true,
14925 counter.clone(),
14926 &mut cx,
14927 )
14928 .await;
14929 cx.condition(|editor, _| editor.context_menu_visible())
14930 .await;
14931 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14932
14933 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14934 editor
14935 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14936 .unwrap()
14937 });
14938 cx.assert_editor_state(indoc! {"
14939 one.second_completion
14940 two sixth_completionˇ
14941 three sixth_completionˇ
14942 additional edit
14943 "});
14944
14945 apply_additional_edits.await.unwrap();
14946
14947 update_test_language_settings(&mut cx, |settings| {
14948 settings.defaults.show_completions_on_input = Some(false);
14949 });
14950 cx.set_state("editorˇ");
14951 cx.simulate_keystroke(".");
14952 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14953 cx.simulate_keystrokes("c l o");
14954 cx.assert_editor_state("editor.cloˇ");
14955 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14956 cx.update_editor(|editor, window, cx| {
14957 editor.show_completions(&ShowCompletions, window, cx);
14958 });
14959 handle_completion_request(
14960 "editor.<clo|>",
14961 vec!["close", "clobber"],
14962 true,
14963 counter.clone(),
14964 &mut cx,
14965 )
14966 .await;
14967 cx.condition(|editor, _| editor.context_menu_visible())
14968 .await;
14969 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14970
14971 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14972 editor
14973 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14974 .unwrap()
14975 });
14976 cx.assert_editor_state("editor.clobberˇ");
14977 handle_resolve_completion_request(&mut cx, None).await;
14978 apply_additional_edits.await.unwrap();
14979}
14980
14981#[gpui::test]
14982async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
14983 init_test(cx, |_| {});
14984
14985 let fs = FakeFs::new(cx.executor());
14986 fs.insert_tree(
14987 path!("/a"),
14988 json!({
14989 "main.rs": "",
14990 }),
14991 )
14992 .await;
14993
14994 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14995 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14996 language_registry.add(rust_lang());
14997 let command_calls = Arc::new(AtomicUsize::new(0));
14998 let registered_command = "_the/command";
14999
15000 let closure_command_calls = command_calls.clone();
15001 let mut fake_servers = language_registry.register_fake_lsp(
15002 "Rust",
15003 FakeLspAdapter {
15004 capabilities: lsp::ServerCapabilities {
15005 completion_provider: Some(lsp::CompletionOptions {
15006 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15007 ..lsp::CompletionOptions::default()
15008 }),
15009 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15010 commands: vec![registered_command.to_owned()],
15011 ..lsp::ExecuteCommandOptions::default()
15012 }),
15013 ..lsp::ServerCapabilities::default()
15014 },
15015 initializer: Some(Box::new(move |fake_server| {
15016 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15017 move |params, _| async move {
15018 Ok(Some(lsp::CompletionResponse::Array(vec![
15019 lsp::CompletionItem {
15020 label: "registered_command".to_owned(),
15021 text_edit: gen_text_edit(¶ms, ""),
15022 command: Some(lsp::Command {
15023 title: registered_command.to_owned(),
15024 command: "_the/command".to_owned(),
15025 arguments: Some(vec![serde_json::Value::Bool(true)]),
15026 }),
15027 ..lsp::CompletionItem::default()
15028 },
15029 lsp::CompletionItem {
15030 label: "unregistered_command".to_owned(),
15031 text_edit: gen_text_edit(¶ms, ""),
15032 command: Some(lsp::Command {
15033 title: "????????????".to_owned(),
15034 command: "????????????".to_owned(),
15035 arguments: Some(vec![serde_json::Value::Null]),
15036 }),
15037 ..lsp::CompletionItem::default()
15038 },
15039 ])))
15040 },
15041 );
15042 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15043 let command_calls = closure_command_calls.clone();
15044 move |params, _| {
15045 assert_eq!(params.command, registered_command);
15046 let command_calls = command_calls.clone();
15047 async move {
15048 command_calls.fetch_add(1, atomic::Ordering::Release);
15049 Ok(Some(json!(null)))
15050 }
15051 }
15052 });
15053 })),
15054 ..FakeLspAdapter::default()
15055 },
15056 );
15057 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15058 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15059 let editor = workspace
15060 .update(cx, |workspace, window, cx| {
15061 workspace.open_abs_path(
15062 PathBuf::from(path!("/a/main.rs")),
15063 OpenOptions::default(),
15064 window,
15065 cx,
15066 )
15067 })
15068 .unwrap()
15069 .await
15070 .unwrap()
15071 .downcast::<Editor>()
15072 .unwrap();
15073 let _fake_server = fake_servers.next().await.unwrap();
15074
15075 editor.update_in(cx, |editor, window, cx| {
15076 cx.focus_self(window);
15077 editor.move_to_end(&MoveToEnd, window, cx);
15078 editor.handle_input(".", window, cx);
15079 });
15080 cx.run_until_parked();
15081 editor.update(cx, |editor, _| {
15082 assert!(editor.context_menu_visible());
15083 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15084 {
15085 let completion_labels = menu
15086 .completions
15087 .borrow()
15088 .iter()
15089 .map(|c| c.label.text.clone())
15090 .collect::<Vec<_>>();
15091 assert_eq!(
15092 completion_labels,
15093 &["registered_command", "unregistered_command",],
15094 );
15095 } else {
15096 panic!("expected completion menu to be open");
15097 }
15098 });
15099
15100 editor
15101 .update_in(cx, |editor, window, cx| {
15102 editor
15103 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15104 .unwrap()
15105 })
15106 .await
15107 .unwrap();
15108 cx.run_until_parked();
15109 assert_eq!(
15110 command_calls.load(atomic::Ordering::Acquire),
15111 1,
15112 "For completion with a registered command, Zed should send a command execution request",
15113 );
15114
15115 editor.update_in(cx, |editor, window, cx| {
15116 cx.focus_self(window);
15117 editor.handle_input(".", window, cx);
15118 });
15119 cx.run_until_parked();
15120 editor.update(cx, |editor, _| {
15121 assert!(editor.context_menu_visible());
15122 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15123 {
15124 let completion_labels = menu
15125 .completions
15126 .borrow()
15127 .iter()
15128 .map(|c| c.label.text.clone())
15129 .collect::<Vec<_>>();
15130 assert_eq!(
15131 completion_labels,
15132 &["registered_command", "unregistered_command",],
15133 );
15134 } else {
15135 panic!("expected completion menu to be open");
15136 }
15137 });
15138 editor
15139 .update_in(cx, |editor, window, cx| {
15140 editor.context_menu_next(&Default::default(), window, cx);
15141 editor
15142 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15143 .unwrap()
15144 })
15145 .await
15146 .unwrap();
15147 cx.run_until_parked();
15148 assert_eq!(
15149 command_calls.load(atomic::Ordering::Acquire),
15150 1,
15151 "For completion with an unregistered command, Zed should not send a command execution request",
15152 );
15153}
15154
15155#[gpui::test]
15156async fn test_completion_reuse(cx: &mut TestAppContext) {
15157 init_test(cx, |_| {});
15158
15159 let mut cx = EditorLspTestContext::new_rust(
15160 lsp::ServerCapabilities {
15161 completion_provider: Some(lsp::CompletionOptions {
15162 trigger_characters: Some(vec![".".to_string()]),
15163 ..Default::default()
15164 }),
15165 ..Default::default()
15166 },
15167 cx,
15168 )
15169 .await;
15170
15171 let counter = Arc::new(AtomicUsize::new(0));
15172 cx.set_state("objˇ");
15173 cx.simulate_keystroke(".");
15174
15175 // Initial completion request returns complete results
15176 let is_incomplete = false;
15177 handle_completion_request(
15178 "obj.|<>",
15179 vec!["a", "ab", "abc"],
15180 is_incomplete,
15181 counter.clone(),
15182 &mut cx,
15183 )
15184 .await;
15185 cx.run_until_parked();
15186 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15187 cx.assert_editor_state("obj.ˇ");
15188 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15189
15190 // Type "a" - filters existing completions
15191 cx.simulate_keystroke("a");
15192 cx.run_until_parked();
15193 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15194 cx.assert_editor_state("obj.aˇ");
15195 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15196
15197 // Type "b" - filters existing completions
15198 cx.simulate_keystroke("b");
15199 cx.run_until_parked();
15200 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15201 cx.assert_editor_state("obj.abˇ");
15202 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15203
15204 // Type "c" - filters existing completions
15205 cx.simulate_keystroke("c");
15206 cx.run_until_parked();
15207 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15208 cx.assert_editor_state("obj.abcˇ");
15209 check_displayed_completions(vec!["abc"], &mut cx);
15210
15211 // Backspace to delete "c" - filters existing completions
15212 cx.update_editor(|editor, window, cx| {
15213 editor.backspace(&Backspace, window, cx);
15214 });
15215 cx.run_until_parked();
15216 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15217 cx.assert_editor_state("obj.abˇ");
15218 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15219
15220 // Moving cursor to the left dismisses menu.
15221 cx.update_editor(|editor, window, cx| {
15222 editor.move_left(&MoveLeft, window, cx);
15223 });
15224 cx.run_until_parked();
15225 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15226 cx.assert_editor_state("obj.aˇb");
15227 cx.update_editor(|editor, _, _| {
15228 assert_eq!(editor.context_menu_visible(), false);
15229 });
15230
15231 // Type "b" - new request
15232 cx.simulate_keystroke("b");
15233 let is_incomplete = false;
15234 handle_completion_request(
15235 "obj.<ab|>a",
15236 vec!["ab", "abc"],
15237 is_incomplete,
15238 counter.clone(),
15239 &mut cx,
15240 )
15241 .await;
15242 cx.run_until_parked();
15243 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15244 cx.assert_editor_state("obj.abˇb");
15245 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15246
15247 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15248 cx.update_editor(|editor, window, cx| {
15249 editor.backspace(&Backspace, window, cx);
15250 });
15251 let is_incomplete = false;
15252 handle_completion_request(
15253 "obj.<a|>b",
15254 vec!["a", "ab", "abc"],
15255 is_incomplete,
15256 counter.clone(),
15257 &mut cx,
15258 )
15259 .await;
15260 cx.run_until_parked();
15261 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15262 cx.assert_editor_state("obj.aˇb");
15263 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15264
15265 // Backspace to delete "a" - dismisses menu.
15266 cx.update_editor(|editor, window, cx| {
15267 editor.backspace(&Backspace, window, cx);
15268 });
15269 cx.run_until_parked();
15270 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15271 cx.assert_editor_state("obj.ˇb");
15272 cx.update_editor(|editor, _, _| {
15273 assert_eq!(editor.context_menu_visible(), false);
15274 });
15275}
15276
15277#[gpui::test]
15278async fn test_word_completion(cx: &mut TestAppContext) {
15279 let lsp_fetch_timeout_ms = 10;
15280 init_test(cx, |language_settings| {
15281 language_settings.defaults.completions = Some(CompletionSettingsContent {
15282 words_min_length: Some(0),
15283 lsp_fetch_timeout_ms: Some(10),
15284 lsp_insert_mode: Some(LspInsertMode::Insert),
15285 ..Default::default()
15286 });
15287 });
15288
15289 let mut cx = EditorLspTestContext::new_rust(
15290 lsp::ServerCapabilities {
15291 completion_provider: Some(lsp::CompletionOptions {
15292 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15293 ..lsp::CompletionOptions::default()
15294 }),
15295 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15296 ..lsp::ServerCapabilities::default()
15297 },
15298 cx,
15299 )
15300 .await;
15301
15302 let throttle_completions = Arc::new(AtomicBool::new(false));
15303
15304 let lsp_throttle_completions = throttle_completions.clone();
15305 let _completion_requests_handler =
15306 cx.lsp
15307 .server
15308 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15309 let lsp_throttle_completions = lsp_throttle_completions.clone();
15310 let cx = cx.clone();
15311 async move {
15312 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15313 cx.background_executor()
15314 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15315 .await;
15316 }
15317 Ok(Some(lsp::CompletionResponse::Array(vec![
15318 lsp::CompletionItem {
15319 label: "first".into(),
15320 ..lsp::CompletionItem::default()
15321 },
15322 lsp::CompletionItem {
15323 label: "last".into(),
15324 ..lsp::CompletionItem::default()
15325 },
15326 ])))
15327 }
15328 });
15329
15330 cx.set_state(indoc! {"
15331 oneˇ
15332 two
15333 three
15334 "});
15335 cx.simulate_keystroke(".");
15336 cx.executor().run_until_parked();
15337 cx.condition(|editor, _| editor.context_menu_visible())
15338 .await;
15339 cx.update_editor(|editor, window, cx| {
15340 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15341 {
15342 assert_eq!(
15343 completion_menu_entries(menu),
15344 &["first", "last"],
15345 "When LSP server is fast to reply, no fallback word completions are used"
15346 );
15347 } else {
15348 panic!("expected completion menu to be open");
15349 }
15350 editor.cancel(&Cancel, window, cx);
15351 });
15352 cx.executor().run_until_parked();
15353 cx.condition(|editor, _| !editor.context_menu_visible())
15354 .await;
15355
15356 throttle_completions.store(true, atomic::Ordering::Release);
15357 cx.simulate_keystroke(".");
15358 cx.executor()
15359 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15360 cx.executor().run_until_parked();
15361 cx.condition(|editor, _| editor.context_menu_visible())
15362 .await;
15363 cx.update_editor(|editor, _, _| {
15364 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15365 {
15366 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15367 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15368 } else {
15369 panic!("expected completion menu to be open");
15370 }
15371 });
15372}
15373
15374#[gpui::test]
15375async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15376 init_test(cx, |language_settings| {
15377 language_settings.defaults.completions = Some(CompletionSettingsContent {
15378 words: Some(WordsCompletionMode::Enabled),
15379 words_min_length: Some(0),
15380 lsp_insert_mode: Some(LspInsertMode::Insert),
15381 ..Default::default()
15382 });
15383 });
15384
15385 let mut cx = EditorLspTestContext::new_rust(
15386 lsp::ServerCapabilities {
15387 completion_provider: Some(lsp::CompletionOptions {
15388 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15389 ..lsp::CompletionOptions::default()
15390 }),
15391 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15392 ..lsp::ServerCapabilities::default()
15393 },
15394 cx,
15395 )
15396 .await;
15397
15398 let _completion_requests_handler =
15399 cx.lsp
15400 .server
15401 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15402 Ok(Some(lsp::CompletionResponse::Array(vec![
15403 lsp::CompletionItem {
15404 label: "first".into(),
15405 ..lsp::CompletionItem::default()
15406 },
15407 lsp::CompletionItem {
15408 label: "last".into(),
15409 ..lsp::CompletionItem::default()
15410 },
15411 ])))
15412 });
15413
15414 cx.set_state(indoc! {"ˇ
15415 first
15416 last
15417 second
15418 "});
15419 cx.simulate_keystroke(".");
15420 cx.executor().run_until_parked();
15421 cx.condition(|editor, _| editor.context_menu_visible())
15422 .await;
15423 cx.update_editor(|editor, _, _| {
15424 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15425 {
15426 assert_eq!(
15427 completion_menu_entries(menu),
15428 &["first", "last", "second"],
15429 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15430 );
15431 } else {
15432 panic!("expected completion menu to be open");
15433 }
15434 });
15435}
15436
15437#[gpui::test]
15438async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15439 init_test(cx, |language_settings| {
15440 language_settings.defaults.completions = Some(CompletionSettingsContent {
15441 words: Some(WordsCompletionMode::Disabled),
15442 words_min_length: Some(0),
15443 lsp_insert_mode: Some(LspInsertMode::Insert),
15444 ..Default::default()
15445 });
15446 });
15447
15448 let mut cx = EditorLspTestContext::new_rust(
15449 lsp::ServerCapabilities {
15450 completion_provider: Some(lsp::CompletionOptions {
15451 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15452 ..lsp::CompletionOptions::default()
15453 }),
15454 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15455 ..lsp::ServerCapabilities::default()
15456 },
15457 cx,
15458 )
15459 .await;
15460
15461 let _completion_requests_handler =
15462 cx.lsp
15463 .server
15464 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15465 panic!("LSP completions should not be queried when dealing with word completions")
15466 });
15467
15468 cx.set_state(indoc! {"ˇ
15469 first
15470 last
15471 second
15472 "});
15473 cx.update_editor(|editor, window, cx| {
15474 editor.show_word_completions(&ShowWordCompletions, window, cx);
15475 });
15476 cx.executor().run_until_parked();
15477 cx.condition(|editor, _| editor.context_menu_visible())
15478 .await;
15479 cx.update_editor(|editor, _, _| {
15480 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15481 {
15482 assert_eq!(
15483 completion_menu_entries(menu),
15484 &["first", "last", "second"],
15485 "`ShowWordCompletions` action should show word completions"
15486 );
15487 } else {
15488 panic!("expected completion menu to be open");
15489 }
15490 });
15491
15492 cx.simulate_keystroke("l");
15493 cx.executor().run_until_parked();
15494 cx.condition(|editor, _| editor.context_menu_visible())
15495 .await;
15496 cx.update_editor(|editor, _, _| {
15497 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15498 {
15499 assert_eq!(
15500 completion_menu_entries(menu),
15501 &["last"],
15502 "After showing word completions, further editing should filter them and not query the LSP"
15503 );
15504 } else {
15505 panic!("expected completion menu to be open");
15506 }
15507 });
15508}
15509
15510#[gpui::test]
15511async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15512 init_test(cx, |language_settings| {
15513 language_settings.defaults.completions = Some(CompletionSettingsContent {
15514 words_min_length: Some(0),
15515 lsp: Some(false),
15516 lsp_insert_mode: Some(LspInsertMode::Insert),
15517 ..Default::default()
15518 });
15519 });
15520
15521 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15522
15523 cx.set_state(indoc! {"ˇ
15524 0_usize
15525 let
15526 33
15527 4.5f32
15528 "});
15529 cx.update_editor(|editor, window, cx| {
15530 editor.show_completions(&ShowCompletions, window, cx);
15531 });
15532 cx.executor().run_until_parked();
15533 cx.condition(|editor, _| editor.context_menu_visible())
15534 .await;
15535 cx.update_editor(|editor, window, cx| {
15536 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15537 {
15538 assert_eq!(
15539 completion_menu_entries(menu),
15540 &["let"],
15541 "With no digits in the completion query, no digits should be in the word completions"
15542 );
15543 } else {
15544 panic!("expected completion menu to be open");
15545 }
15546 editor.cancel(&Cancel, window, cx);
15547 });
15548
15549 cx.set_state(indoc! {"3ˇ
15550 0_usize
15551 let
15552 3
15553 33.35f32
15554 "});
15555 cx.update_editor(|editor, window, cx| {
15556 editor.show_completions(&ShowCompletions, window, cx);
15557 });
15558 cx.executor().run_until_parked();
15559 cx.condition(|editor, _| editor.context_menu_visible())
15560 .await;
15561 cx.update_editor(|editor, _, _| {
15562 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15563 {
15564 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15565 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15566 } else {
15567 panic!("expected completion menu to be open");
15568 }
15569 });
15570}
15571
15572#[gpui::test]
15573async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15574 init_test(cx, |language_settings| {
15575 language_settings.defaults.completions = Some(CompletionSettingsContent {
15576 words: Some(WordsCompletionMode::Enabled),
15577 words_min_length: Some(3),
15578 lsp_insert_mode: Some(LspInsertMode::Insert),
15579 ..Default::default()
15580 });
15581 });
15582
15583 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15584 cx.set_state(indoc! {"ˇ
15585 wow
15586 wowen
15587 wowser
15588 "});
15589 cx.simulate_keystroke("w");
15590 cx.executor().run_until_parked();
15591 cx.update_editor(|editor, _, _| {
15592 if editor.context_menu.borrow_mut().is_some() {
15593 panic!(
15594 "expected completion menu to be hidden, as words completion threshold is not met"
15595 );
15596 }
15597 });
15598
15599 cx.update_editor(|editor, window, cx| {
15600 editor.show_word_completions(&ShowWordCompletions, window, cx);
15601 });
15602 cx.executor().run_until_parked();
15603 cx.update_editor(|editor, window, cx| {
15604 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15605 {
15606 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");
15607 } else {
15608 panic!("expected completion menu to be open after the word completions are called with an action");
15609 }
15610
15611 editor.cancel(&Cancel, window, cx);
15612 });
15613 cx.update_editor(|editor, _, _| {
15614 if editor.context_menu.borrow_mut().is_some() {
15615 panic!("expected completion menu to be hidden after canceling");
15616 }
15617 });
15618
15619 cx.simulate_keystroke("o");
15620 cx.executor().run_until_parked();
15621 cx.update_editor(|editor, _, _| {
15622 if editor.context_menu.borrow_mut().is_some() {
15623 panic!(
15624 "expected completion menu to be hidden, as words completion threshold is not met still"
15625 );
15626 }
15627 });
15628
15629 cx.simulate_keystroke("w");
15630 cx.executor().run_until_parked();
15631 cx.update_editor(|editor, _, _| {
15632 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15633 {
15634 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15635 } else {
15636 panic!("expected completion menu to be open after the word completions threshold is met");
15637 }
15638 });
15639}
15640
15641#[gpui::test]
15642async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15643 init_test(cx, |language_settings| {
15644 language_settings.defaults.completions = Some(CompletionSettingsContent {
15645 words: Some(WordsCompletionMode::Enabled),
15646 words_min_length: Some(0),
15647 lsp_insert_mode: Some(LspInsertMode::Insert),
15648 ..Default::default()
15649 });
15650 });
15651
15652 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15653 cx.update_editor(|editor, _, _| {
15654 editor.disable_word_completions();
15655 });
15656 cx.set_state(indoc! {"ˇ
15657 wow
15658 wowen
15659 wowser
15660 "});
15661 cx.simulate_keystroke("w");
15662 cx.executor().run_until_parked();
15663 cx.update_editor(|editor, _, _| {
15664 if editor.context_menu.borrow_mut().is_some() {
15665 panic!(
15666 "expected completion menu to be hidden, as words completion are disabled for this editor"
15667 );
15668 }
15669 });
15670
15671 cx.update_editor(|editor, window, cx| {
15672 editor.show_word_completions(&ShowWordCompletions, window, cx);
15673 });
15674 cx.executor().run_until_parked();
15675 cx.update_editor(|editor, _, _| {
15676 if editor.context_menu.borrow_mut().is_some() {
15677 panic!(
15678 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15679 );
15680 }
15681 });
15682}
15683
15684#[gpui::test]
15685async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15686 init_test(cx, |language_settings| {
15687 language_settings.defaults.completions = Some(CompletionSettingsContent {
15688 words: Some(WordsCompletionMode::Disabled),
15689 words_min_length: Some(0),
15690 lsp_insert_mode: Some(LspInsertMode::Insert),
15691 ..Default::default()
15692 });
15693 });
15694
15695 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15696 cx.update_editor(|editor, _, _| {
15697 editor.set_completion_provider(None);
15698 });
15699 cx.set_state(indoc! {"ˇ
15700 wow
15701 wowen
15702 wowser
15703 "});
15704 cx.simulate_keystroke("w");
15705 cx.executor().run_until_parked();
15706 cx.update_editor(|editor, _, _| {
15707 if editor.context_menu.borrow_mut().is_some() {
15708 panic!("expected completion menu to be hidden, as disabled in settings");
15709 }
15710 });
15711}
15712
15713fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15714 let position = || lsp::Position {
15715 line: params.text_document_position.position.line,
15716 character: params.text_document_position.position.character,
15717 };
15718 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15719 range: lsp::Range {
15720 start: position(),
15721 end: position(),
15722 },
15723 new_text: text.to_string(),
15724 }))
15725}
15726
15727#[gpui::test]
15728async fn test_multiline_completion(cx: &mut TestAppContext) {
15729 init_test(cx, |_| {});
15730
15731 let fs = FakeFs::new(cx.executor());
15732 fs.insert_tree(
15733 path!("/a"),
15734 json!({
15735 "main.ts": "a",
15736 }),
15737 )
15738 .await;
15739
15740 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15741 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15742 let typescript_language = Arc::new(Language::new(
15743 LanguageConfig {
15744 name: "TypeScript".into(),
15745 matcher: LanguageMatcher {
15746 path_suffixes: vec!["ts".to_string()],
15747 ..LanguageMatcher::default()
15748 },
15749 line_comments: vec!["// ".into()],
15750 ..LanguageConfig::default()
15751 },
15752 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15753 ));
15754 language_registry.add(typescript_language.clone());
15755 let mut fake_servers = language_registry.register_fake_lsp(
15756 "TypeScript",
15757 FakeLspAdapter {
15758 capabilities: lsp::ServerCapabilities {
15759 completion_provider: Some(lsp::CompletionOptions {
15760 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15761 ..lsp::CompletionOptions::default()
15762 }),
15763 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15764 ..lsp::ServerCapabilities::default()
15765 },
15766 // Emulate vtsls label generation
15767 label_for_completion: Some(Box::new(|item, _| {
15768 let text = if let Some(description) = item
15769 .label_details
15770 .as_ref()
15771 .and_then(|label_details| label_details.description.as_ref())
15772 {
15773 format!("{} {}", item.label, description)
15774 } else if let Some(detail) = &item.detail {
15775 format!("{} {}", item.label, detail)
15776 } else {
15777 item.label.clone()
15778 };
15779 Some(language::CodeLabel::plain(text, None))
15780 })),
15781 ..FakeLspAdapter::default()
15782 },
15783 );
15784 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15785 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15786 let worktree_id = workspace
15787 .update(cx, |workspace, _window, cx| {
15788 workspace.project().update(cx, |project, cx| {
15789 project.worktrees(cx).next().unwrap().read(cx).id()
15790 })
15791 })
15792 .unwrap();
15793 let _buffer = project
15794 .update(cx, |project, cx| {
15795 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15796 })
15797 .await
15798 .unwrap();
15799 let editor = workspace
15800 .update(cx, |workspace, window, cx| {
15801 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15802 })
15803 .unwrap()
15804 .await
15805 .unwrap()
15806 .downcast::<Editor>()
15807 .unwrap();
15808 let fake_server = fake_servers.next().await.unwrap();
15809
15810 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15811 let multiline_label_2 = "a\nb\nc\n";
15812 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15813 let multiline_description = "d\ne\nf\n";
15814 let multiline_detail_2 = "g\nh\ni\n";
15815
15816 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15817 move |params, _| async move {
15818 Ok(Some(lsp::CompletionResponse::Array(vec![
15819 lsp::CompletionItem {
15820 label: multiline_label.to_string(),
15821 text_edit: gen_text_edit(¶ms, "new_text_1"),
15822 ..lsp::CompletionItem::default()
15823 },
15824 lsp::CompletionItem {
15825 label: "single line label 1".to_string(),
15826 detail: Some(multiline_detail.to_string()),
15827 text_edit: gen_text_edit(¶ms, "new_text_2"),
15828 ..lsp::CompletionItem::default()
15829 },
15830 lsp::CompletionItem {
15831 label: "single line label 2".to_string(),
15832 label_details: Some(lsp::CompletionItemLabelDetails {
15833 description: Some(multiline_description.to_string()),
15834 detail: None,
15835 }),
15836 text_edit: gen_text_edit(¶ms, "new_text_2"),
15837 ..lsp::CompletionItem::default()
15838 },
15839 lsp::CompletionItem {
15840 label: multiline_label_2.to_string(),
15841 detail: Some(multiline_detail_2.to_string()),
15842 text_edit: gen_text_edit(¶ms, "new_text_3"),
15843 ..lsp::CompletionItem::default()
15844 },
15845 lsp::CompletionItem {
15846 label: "Label with many spaces and \t but without newlines".to_string(),
15847 detail: Some(
15848 "Details with many spaces and \t but without newlines".to_string(),
15849 ),
15850 text_edit: gen_text_edit(¶ms, "new_text_4"),
15851 ..lsp::CompletionItem::default()
15852 },
15853 ])))
15854 },
15855 );
15856
15857 editor.update_in(cx, |editor, window, cx| {
15858 cx.focus_self(window);
15859 editor.move_to_end(&MoveToEnd, window, cx);
15860 editor.handle_input(".", window, cx);
15861 });
15862 cx.run_until_parked();
15863 completion_handle.next().await.unwrap();
15864
15865 editor.update(cx, |editor, _| {
15866 assert!(editor.context_menu_visible());
15867 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15868 {
15869 let completion_labels = menu
15870 .completions
15871 .borrow()
15872 .iter()
15873 .map(|c| c.label.text.clone())
15874 .collect::<Vec<_>>();
15875 assert_eq!(
15876 completion_labels,
15877 &[
15878 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15879 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15880 "single line label 2 d e f ",
15881 "a b c g h i ",
15882 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15883 ],
15884 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15885 );
15886
15887 for completion in menu
15888 .completions
15889 .borrow()
15890 .iter() {
15891 assert_eq!(
15892 completion.label.filter_range,
15893 0..completion.label.text.len(),
15894 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15895 );
15896 }
15897 } else {
15898 panic!("expected completion menu to be open");
15899 }
15900 });
15901}
15902
15903#[gpui::test]
15904async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15905 init_test(cx, |_| {});
15906 let mut cx = EditorLspTestContext::new_rust(
15907 lsp::ServerCapabilities {
15908 completion_provider: Some(lsp::CompletionOptions {
15909 trigger_characters: Some(vec![".".to_string()]),
15910 ..Default::default()
15911 }),
15912 ..Default::default()
15913 },
15914 cx,
15915 )
15916 .await;
15917 cx.lsp
15918 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15919 Ok(Some(lsp::CompletionResponse::Array(vec![
15920 lsp::CompletionItem {
15921 label: "first".into(),
15922 ..Default::default()
15923 },
15924 lsp::CompletionItem {
15925 label: "last".into(),
15926 ..Default::default()
15927 },
15928 ])))
15929 });
15930 cx.set_state("variableˇ");
15931 cx.simulate_keystroke(".");
15932 cx.executor().run_until_parked();
15933
15934 cx.update_editor(|editor, _, _| {
15935 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15936 {
15937 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15938 } else {
15939 panic!("expected completion menu to be open");
15940 }
15941 });
15942
15943 cx.update_editor(|editor, window, cx| {
15944 editor.move_page_down(&MovePageDown::default(), window, cx);
15945 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15946 {
15947 assert!(
15948 menu.selected_item == 1,
15949 "expected PageDown to select the last item from the context menu"
15950 );
15951 } else {
15952 panic!("expected completion menu to stay open after PageDown");
15953 }
15954 });
15955
15956 cx.update_editor(|editor, window, cx| {
15957 editor.move_page_up(&MovePageUp::default(), window, cx);
15958 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15959 {
15960 assert!(
15961 menu.selected_item == 0,
15962 "expected PageUp to select the first item from the context menu"
15963 );
15964 } else {
15965 panic!("expected completion menu to stay open after PageUp");
15966 }
15967 });
15968}
15969
15970#[gpui::test]
15971async fn test_as_is_completions(cx: &mut TestAppContext) {
15972 init_test(cx, |_| {});
15973 let mut cx = EditorLspTestContext::new_rust(
15974 lsp::ServerCapabilities {
15975 completion_provider: Some(lsp::CompletionOptions {
15976 ..Default::default()
15977 }),
15978 ..Default::default()
15979 },
15980 cx,
15981 )
15982 .await;
15983 cx.lsp
15984 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15985 Ok(Some(lsp::CompletionResponse::Array(vec![
15986 lsp::CompletionItem {
15987 label: "unsafe".into(),
15988 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15989 range: lsp::Range {
15990 start: lsp::Position {
15991 line: 1,
15992 character: 2,
15993 },
15994 end: lsp::Position {
15995 line: 1,
15996 character: 3,
15997 },
15998 },
15999 new_text: "unsafe".to_string(),
16000 })),
16001 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16002 ..Default::default()
16003 },
16004 ])))
16005 });
16006 cx.set_state("fn a() {}\n nˇ");
16007 cx.executor().run_until_parked();
16008 cx.update_editor(|editor, window, cx| {
16009 editor.trigger_completion_on_input("n", true, window, cx)
16010 });
16011 cx.executor().run_until_parked();
16012
16013 cx.update_editor(|editor, window, cx| {
16014 editor.confirm_completion(&Default::default(), window, cx)
16015 });
16016 cx.executor().run_until_parked();
16017 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16018}
16019
16020#[gpui::test]
16021async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16022 init_test(cx, |_| {});
16023 let language =
16024 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16025 let mut cx = EditorLspTestContext::new(
16026 language,
16027 lsp::ServerCapabilities {
16028 completion_provider: Some(lsp::CompletionOptions {
16029 ..lsp::CompletionOptions::default()
16030 }),
16031 ..lsp::ServerCapabilities::default()
16032 },
16033 cx,
16034 )
16035 .await;
16036
16037 cx.set_state(
16038 "#ifndef BAR_H
16039#define BAR_H
16040
16041#include <stdbool.h>
16042
16043int fn_branch(bool do_branch1, bool do_branch2);
16044
16045#endif // BAR_H
16046ˇ",
16047 );
16048 cx.executor().run_until_parked();
16049 cx.update_editor(|editor, window, cx| {
16050 editor.handle_input("#", window, cx);
16051 });
16052 cx.executor().run_until_parked();
16053 cx.update_editor(|editor, window, cx| {
16054 editor.handle_input("i", window, cx);
16055 });
16056 cx.executor().run_until_parked();
16057 cx.update_editor(|editor, window, cx| {
16058 editor.handle_input("n", window, cx);
16059 });
16060 cx.executor().run_until_parked();
16061 cx.assert_editor_state(
16062 "#ifndef BAR_H
16063#define BAR_H
16064
16065#include <stdbool.h>
16066
16067int fn_branch(bool do_branch1, bool do_branch2);
16068
16069#endif // BAR_H
16070#inˇ",
16071 );
16072
16073 cx.lsp
16074 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16075 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16076 is_incomplete: false,
16077 item_defaults: None,
16078 items: vec![lsp::CompletionItem {
16079 kind: Some(lsp::CompletionItemKind::SNIPPET),
16080 label_details: Some(lsp::CompletionItemLabelDetails {
16081 detail: Some("header".to_string()),
16082 description: None,
16083 }),
16084 label: " include".to_string(),
16085 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16086 range: lsp::Range {
16087 start: lsp::Position {
16088 line: 8,
16089 character: 1,
16090 },
16091 end: lsp::Position {
16092 line: 8,
16093 character: 1,
16094 },
16095 },
16096 new_text: "include \"$0\"".to_string(),
16097 })),
16098 sort_text: Some("40b67681include".to_string()),
16099 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16100 filter_text: Some("include".to_string()),
16101 insert_text: Some("include \"$0\"".to_string()),
16102 ..lsp::CompletionItem::default()
16103 }],
16104 })))
16105 });
16106 cx.update_editor(|editor, window, cx| {
16107 editor.show_completions(&ShowCompletions, window, cx);
16108 });
16109 cx.executor().run_until_parked();
16110 cx.update_editor(|editor, window, cx| {
16111 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16112 });
16113 cx.executor().run_until_parked();
16114 cx.assert_editor_state(
16115 "#ifndef BAR_H
16116#define BAR_H
16117
16118#include <stdbool.h>
16119
16120int fn_branch(bool do_branch1, bool do_branch2);
16121
16122#endif // BAR_H
16123#include \"ˇ\"",
16124 );
16125
16126 cx.lsp
16127 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16128 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16129 is_incomplete: true,
16130 item_defaults: None,
16131 items: vec![lsp::CompletionItem {
16132 kind: Some(lsp::CompletionItemKind::FILE),
16133 label: "AGL/".to_string(),
16134 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16135 range: lsp::Range {
16136 start: lsp::Position {
16137 line: 8,
16138 character: 10,
16139 },
16140 end: lsp::Position {
16141 line: 8,
16142 character: 11,
16143 },
16144 },
16145 new_text: "AGL/".to_string(),
16146 })),
16147 sort_text: Some("40b67681AGL/".to_string()),
16148 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16149 filter_text: Some("AGL/".to_string()),
16150 insert_text: Some("AGL/".to_string()),
16151 ..lsp::CompletionItem::default()
16152 }],
16153 })))
16154 });
16155 cx.update_editor(|editor, window, cx| {
16156 editor.show_completions(&ShowCompletions, window, cx);
16157 });
16158 cx.executor().run_until_parked();
16159 cx.update_editor(|editor, window, cx| {
16160 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16161 });
16162 cx.executor().run_until_parked();
16163 cx.assert_editor_state(
16164 r##"#ifndef BAR_H
16165#define BAR_H
16166
16167#include <stdbool.h>
16168
16169int fn_branch(bool do_branch1, bool do_branch2);
16170
16171#endif // BAR_H
16172#include "AGL/ˇ"##,
16173 );
16174
16175 cx.update_editor(|editor, window, cx| {
16176 editor.handle_input("\"", window, cx);
16177 });
16178 cx.executor().run_until_parked();
16179 cx.assert_editor_state(
16180 r##"#ifndef BAR_H
16181#define BAR_H
16182
16183#include <stdbool.h>
16184
16185int fn_branch(bool do_branch1, bool do_branch2);
16186
16187#endif // BAR_H
16188#include "AGL/"ˇ"##,
16189 );
16190}
16191
16192#[gpui::test]
16193async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16194 init_test(cx, |_| {});
16195
16196 let mut cx = EditorLspTestContext::new_rust(
16197 lsp::ServerCapabilities {
16198 completion_provider: Some(lsp::CompletionOptions {
16199 trigger_characters: Some(vec![".".to_string()]),
16200 resolve_provider: Some(true),
16201 ..Default::default()
16202 }),
16203 ..Default::default()
16204 },
16205 cx,
16206 )
16207 .await;
16208
16209 cx.set_state("fn main() { let a = 2ˇ; }");
16210 cx.simulate_keystroke(".");
16211 let completion_item = lsp::CompletionItem {
16212 label: "Some".into(),
16213 kind: Some(lsp::CompletionItemKind::SNIPPET),
16214 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16215 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16216 kind: lsp::MarkupKind::Markdown,
16217 value: "```rust\nSome(2)\n```".to_string(),
16218 })),
16219 deprecated: Some(false),
16220 sort_text: Some("Some".to_string()),
16221 filter_text: Some("Some".to_string()),
16222 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16223 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16224 range: lsp::Range {
16225 start: lsp::Position {
16226 line: 0,
16227 character: 22,
16228 },
16229 end: lsp::Position {
16230 line: 0,
16231 character: 22,
16232 },
16233 },
16234 new_text: "Some(2)".to_string(),
16235 })),
16236 additional_text_edits: Some(vec![lsp::TextEdit {
16237 range: lsp::Range {
16238 start: lsp::Position {
16239 line: 0,
16240 character: 20,
16241 },
16242 end: lsp::Position {
16243 line: 0,
16244 character: 22,
16245 },
16246 },
16247 new_text: "".to_string(),
16248 }]),
16249 ..Default::default()
16250 };
16251
16252 let closure_completion_item = completion_item.clone();
16253 let counter = Arc::new(AtomicUsize::new(0));
16254 let counter_clone = counter.clone();
16255 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16256 let task_completion_item = closure_completion_item.clone();
16257 counter_clone.fetch_add(1, atomic::Ordering::Release);
16258 async move {
16259 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16260 is_incomplete: true,
16261 item_defaults: None,
16262 items: vec![task_completion_item],
16263 })))
16264 }
16265 });
16266
16267 cx.condition(|editor, _| editor.context_menu_visible())
16268 .await;
16269 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16270 assert!(request.next().await.is_some());
16271 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16272
16273 cx.simulate_keystrokes("S o m");
16274 cx.condition(|editor, _| editor.context_menu_visible())
16275 .await;
16276 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16277 assert!(request.next().await.is_some());
16278 assert!(request.next().await.is_some());
16279 assert!(request.next().await.is_some());
16280 request.close();
16281 assert!(request.next().await.is_none());
16282 assert_eq!(
16283 counter.load(atomic::Ordering::Acquire),
16284 4,
16285 "With the completions menu open, only one LSP request should happen per input"
16286 );
16287}
16288
16289#[gpui::test]
16290async fn test_toggle_comment(cx: &mut TestAppContext) {
16291 init_test(cx, |_| {});
16292 let mut cx = EditorTestContext::new(cx).await;
16293 let language = Arc::new(Language::new(
16294 LanguageConfig {
16295 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16296 ..Default::default()
16297 },
16298 Some(tree_sitter_rust::LANGUAGE.into()),
16299 ));
16300 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16301
16302 // If multiple selections intersect a line, the line is only toggled once.
16303 cx.set_state(indoc! {"
16304 fn a() {
16305 «//b();
16306 ˇ»// «c();
16307 //ˇ» d();
16308 }
16309 "});
16310
16311 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16312
16313 cx.assert_editor_state(indoc! {"
16314 fn a() {
16315 «b();
16316 ˇ»«c();
16317 ˇ» d();
16318 }
16319 "});
16320
16321 // The comment prefix is inserted at the same column for every line in a
16322 // selection.
16323 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16324
16325 cx.assert_editor_state(indoc! {"
16326 fn a() {
16327 // «b();
16328 ˇ»// «c();
16329 ˇ» // d();
16330 }
16331 "});
16332
16333 // If a selection ends at the beginning of a line, that line is not toggled.
16334 cx.set_selections_state(indoc! {"
16335 fn a() {
16336 // b();
16337 «// c();
16338 ˇ» // d();
16339 }
16340 "});
16341
16342 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16343
16344 cx.assert_editor_state(indoc! {"
16345 fn a() {
16346 // b();
16347 «c();
16348 ˇ» // d();
16349 }
16350 "});
16351
16352 // If a selection span a single line and is empty, the line is toggled.
16353 cx.set_state(indoc! {"
16354 fn a() {
16355 a();
16356 b();
16357 ˇ
16358 }
16359 "});
16360
16361 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16362
16363 cx.assert_editor_state(indoc! {"
16364 fn a() {
16365 a();
16366 b();
16367 //•ˇ
16368 }
16369 "});
16370
16371 // If a selection span multiple lines, empty lines are not toggled.
16372 cx.set_state(indoc! {"
16373 fn a() {
16374 «a();
16375
16376 c();ˇ»
16377 }
16378 "});
16379
16380 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16381
16382 cx.assert_editor_state(indoc! {"
16383 fn a() {
16384 // «a();
16385
16386 // c();ˇ»
16387 }
16388 "});
16389
16390 // If a selection includes multiple comment prefixes, all lines are uncommented.
16391 cx.set_state(indoc! {"
16392 fn a() {
16393 «// a();
16394 /// b();
16395 //! c();ˇ»
16396 }
16397 "});
16398
16399 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16400
16401 cx.assert_editor_state(indoc! {"
16402 fn a() {
16403 «a();
16404 b();
16405 c();ˇ»
16406 }
16407 "});
16408}
16409
16410#[gpui::test]
16411async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16412 init_test(cx, |_| {});
16413 let mut cx = EditorTestContext::new(cx).await;
16414 let language = Arc::new(Language::new(
16415 LanguageConfig {
16416 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16417 ..Default::default()
16418 },
16419 Some(tree_sitter_rust::LANGUAGE.into()),
16420 ));
16421 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16422
16423 let toggle_comments = &ToggleComments {
16424 advance_downwards: false,
16425 ignore_indent: true,
16426 };
16427
16428 // If multiple selections intersect a line, the line is only toggled once.
16429 cx.set_state(indoc! {"
16430 fn a() {
16431 // «b();
16432 // c();
16433 // ˇ» d();
16434 }
16435 "});
16436
16437 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16438
16439 cx.assert_editor_state(indoc! {"
16440 fn a() {
16441 «b();
16442 c();
16443 ˇ» d();
16444 }
16445 "});
16446
16447 // The comment prefix is inserted at the beginning of each line
16448 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16449
16450 cx.assert_editor_state(indoc! {"
16451 fn a() {
16452 // «b();
16453 // c();
16454 // ˇ» d();
16455 }
16456 "});
16457
16458 // If a selection ends at the beginning of a line, that line is not toggled.
16459 cx.set_selections_state(indoc! {"
16460 fn a() {
16461 // b();
16462 // «c();
16463 ˇ»// d();
16464 }
16465 "});
16466
16467 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16468
16469 cx.assert_editor_state(indoc! {"
16470 fn a() {
16471 // b();
16472 «c();
16473 ˇ»// d();
16474 }
16475 "});
16476
16477 // If a selection span a single line and is empty, the line is toggled.
16478 cx.set_state(indoc! {"
16479 fn a() {
16480 a();
16481 b();
16482 ˇ
16483 }
16484 "});
16485
16486 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16487
16488 cx.assert_editor_state(indoc! {"
16489 fn a() {
16490 a();
16491 b();
16492 //ˇ
16493 }
16494 "});
16495
16496 // If a selection span multiple lines, empty lines are not toggled.
16497 cx.set_state(indoc! {"
16498 fn a() {
16499 «a();
16500
16501 c();ˇ»
16502 }
16503 "});
16504
16505 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16506
16507 cx.assert_editor_state(indoc! {"
16508 fn a() {
16509 // «a();
16510
16511 // c();ˇ»
16512 }
16513 "});
16514
16515 // If a selection includes multiple comment prefixes, all lines are uncommented.
16516 cx.set_state(indoc! {"
16517 fn a() {
16518 // «a();
16519 /// b();
16520 //! c();ˇ»
16521 }
16522 "});
16523
16524 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16525
16526 cx.assert_editor_state(indoc! {"
16527 fn a() {
16528 «a();
16529 b();
16530 c();ˇ»
16531 }
16532 "});
16533}
16534
16535#[gpui::test]
16536async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16537 init_test(cx, |_| {});
16538
16539 let language = Arc::new(Language::new(
16540 LanguageConfig {
16541 line_comments: vec!["// ".into()],
16542 ..Default::default()
16543 },
16544 Some(tree_sitter_rust::LANGUAGE.into()),
16545 ));
16546
16547 let mut cx = EditorTestContext::new(cx).await;
16548
16549 cx.language_registry().add(language.clone());
16550 cx.update_buffer(|buffer, cx| {
16551 buffer.set_language(Some(language), cx);
16552 });
16553
16554 let toggle_comments = &ToggleComments {
16555 advance_downwards: true,
16556 ignore_indent: false,
16557 };
16558
16559 // Single cursor on one line -> advance
16560 // Cursor moves horizontally 3 characters as well on non-blank line
16561 cx.set_state(indoc!(
16562 "fn a() {
16563 ˇdog();
16564 cat();
16565 }"
16566 ));
16567 cx.update_editor(|editor, window, cx| {
16568 editor.toggle_comments(toggle_comments, window, cx);
16569 });
16570 cx.assert_editor_state(indoc!(
16571 "fn a() {
16572 // dog();
16573 catˇ();
16574 }"
16575 ));
16576
16577 // Single selection on one line -> don't advance
16578 cx.set_state(indoc!(
16579 "fn a() {
16580 «dog()ˇ»;
16581 cat();
16582 }"
16583 ));
16584 cx.update_editor(|editor, window, cx| {
16585 editor.toggle_comments(toggle_comments, window, cx);
16586 });
16587 cx.assert_editor_state(indoc!(
16588 "fn a() {
16589 // «dog()ˇ»;
16590 cat();
16591 }"
16592 ));
16593
16594 // Multiple cursors on one line -> advance
16595 cx.set_state(indoc!(
16596 "fn a() {
16597 ˇdˇog();
16598 cat();
16599 }"
16600 ));
16601 cx.update_editor(|editor, window, cx| {
16602 editor.toggle_comments(toggle_comments, window, cx);
16603 });
16604 cx.assert_editor_state(indoc!(
16605 "fn a() {
16606 // dog();
16607 catˇ(ˇ);
16608 }"
16609 ));
16610
16611 // Multiple cursors on one line, with selection -> don't advance
16612 cx.set_state(indoc!(
16613 "fn a() {
16614 ˇdˇog«()ˇ»;
16615 cat();
16616 }"
16617 ));
16618 cx.update_editor(|editor, window, cx| {
16619 editor.toggle_comments(toggle_comments, window, cx);
16620 });
16621 cx.assert_editor_state(indoc!(
16622 "fn a() {
16623 // ˇdˇog«()ˇ»;
16624 cat();
16625 }"
16626 ));
16627
16628 // Single cursor on one line -> advance
16629 // Cursor moves to column 0 on blank line
16630 cx.set_state(indoc!(
16631 "fn a() {
16632 ˇdog();
16633
16634 cat();
16635 }"
16636 ));
16637 cx.update_editor(|editor, window, cx| {
16638 editor.toggle_comments(toggle_comments, window, cx);
16639 });
16640 cx.assert_editor_state(indoc!(
16641 "fn a() {
16642 // dog();
16643 ˇ
16644 cat();
16645 }"
16646 ));
16647
16648 // Single cursor on one line -> advance
16649 // Cursor starts and ends at column 0
16650 cx.set_state(indoc!(
16651 "fn a() {
16652 ˇ dog();
16653 cat();
16654 }"
16655 ));
16656 cx.update_editor(|editor, window, cx| {
16657 editor.toggle_comments(toggle_comments, window, cx);
16658 });
16659 cx.assert_editor_state(indoc!(
16660 "fn a() {
16661 // dog();
16662 ˇ cat();
16663 }"
16664 ));
16665}
16666
16667#[gpui::test]
16668async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16669 init_test(cx, |_| {});
16670
16671 let mut cx = EditorTestContext::new(cx).await;
16672
16673 let html_language = Arc::new(
16674 Language::new(
16675 LanguageConfig {
16676 name: "HTML".into(),
16677 block_comment: Some(BlockCommentConfig {
16678 start: "<!-- ".into(),
16679 prefix: "".into(),
16680 end: " -->".into(),
16681 tab_size: 0,
16682 }),
16683 ..Default::default()
16684 },
16685 Some(tree_sitter_html::LANGUAGE.into()),
16686 )
16687 .with_injection_query(
16688 r#"
16689 (script_element
16690 (raw_text) @injection.content
16691 (#set! injection.language "javascript"))
16692 "#,
16693 )
16694 .unwrap(),
16695 );
16696
16697 let javascript_language = Arc::new(Language::new(
16698 LanguageConfig {
16699 name: "JavaScript".into(),
16700 line_comments: vec!["// ".into()],
16701 ..Default::default()
16702 },
16703 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16704 ));
16705
16706 cx.language_registry().add(html_language.clone());
16707 cx.language_registry().add(javascript_language);
16708 cx.update_buffer(|buffer, cx| {
16709 buffer.set_language(Some(html_language), cx);
16710 });
16711
16712 // Toggle comments for empty selections
16713 cx.set_state(
16714 &r#"
16715 <p>A</p>ˇ
16716 <p>B</p>ˇ
16717 <p>C</p>ˇ
16718 "#
16719 .unindent(),
16720 );
16721 cx.update_editor(|editor, window, cx| {
16722 editor.toggle_comments(&ToggleComments::default(), window, cx)
16723 });
16724 cx.assert_editor_state(
16725 &r#"
16726 <!-- <p>A</p>ˇ -->
16727 <!-- <p>B</p>ˇ -->
16728 <!-- <p>C</p>ˇ -->
16729 "#
16730 .unindent(),
16731 );
16732 cx.update_editor(|editor, window, cx| {
16733 editor.toggle_comments(&ToggleComments::default(), window, cx)
16734 });
16735 cx.assert_editor_state(
16736 &r#"
16737 <p>A</p>ˇ
16738 <p>B</p>ˇ
16739 <p>C</p>ˇ
16740 "#
16741 .unindent(),
16742 );
16743
16744 // Toggle comments for mixture of empty and non-empty selections, where
16745 // multiple selections occupy a given line.
16746 cx.set_state(
16747 &r#"
16748 <p>A«</p>
16749 <p>ˇ»B</p>ˇ
16750 <p>C«</p>
16751 <p>ˇ»D</p>ˇ
16752 "#
16753 .unindent(),
16754 );
16755
16756 cx.update_editor(|editor, window, cx| {
16757 editor.toggle_comments(&ToggleComments::default(), window, cx)
16758 });
16759 cx.assert_editor_state(
16760 &r#"
16761 <!-- <p>A«</p>
16762 <p>ˇ»B</p>ˇ -->
16763 <!-- <p>C«</p>
16764 <p>ˇ»D</p>ˇ -->
16765 "#
16766 .unindent(),
16767 );
16768 cx.update_editor(|editor, window, cx| {
16769 editor.toggle_comments(&ToggleComments::default(), window, cx)
16770 });
16771 cx.assert_editor_state(
16772 &r#"
16773 <p>A«</p>
16774 <p>ˇ»B</p>ˇ
16775 <p>C«</p>
16776 <p>ˇ»D</p>ˇ
16777 "#
16778 .unindent(),
16779 );
16780
16781 // Toggle comments when different languages are active for different
16782 // selections.
16783 cx.set_state(
16784 &r#"
16785 ˇ<script>
16786 ˇvar x = new Y();
16787 ˇ</script>
16788 "#
16789 .unindent(),
16790 );
16791 cx.executor().run_until_parked();
16792 cx.update_editor(|editor, window, cx| {
16793 editor.toggle_comments(&ToggleComments::default(), window, cx)
16794 });
16795 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16796 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16797 cx.assert_editor_state(
16798 &r#"
16799 <!-- ˇ<script> -->
16800 // ˇvar x = new Y();
16801 <!-- ˇ</script> -->
16802 "#
16803 .unindent(),
16804 );
16805}
16806
16807#[gpui::test]
16808fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16809 init_test(cx, |_| {});
16810
16811 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16812 let multibuffer = cx.new(|cx| {
16813 let mut multibuffer = MultiBuffer::new(ReadWrite);
16814 multibuffer.push_excerpts(
16815 buffer.clone(),
16816 [
16817 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16818 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16819 ],
16820 cx,
16821 );
16822 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16823 multibuffer
16824 });
16825
16826 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16827 editor.update_in(cx, |editor, window, cx| {
16828 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16829 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16830 s.select_ranges([
16831 Point::new(0, 0)..Point::new(0, 0),
16832 Point::new(1, 0)..Point::new(1, 0),
16833 ])
16834 });
16835
16836 editor.handle_input("X", window, cx);
16837 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16838 assert_eq!(
16839 editor.selections.ranges(&editor.display_snapshot(cx)),
16840 [
16841 Point::new(0, 1)..Point::new(0, 1),
16842 Point::new(1, 1)..Point::new(1, 1),
16843 ]
16844 );
16845
16846 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16848 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16849 });
16850 editor.backspace(&Default::default(), window, cx);
16851 assert_eq!(editor.text(cx), "Xa\nbbb");
16852 assert_eq!(
16853 editor.selections.ranges(&editor.display_snapshot(cx)),
16854 [Point::new(1, 0)..Point::new(1, 0)]
16855 );
16856
16857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16858 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16859 });
16860 editor.backspace(&Default::default(), window, cx);
16861 assert_eq!(editor.text(cx), "X\nbb");
16862 assert_eq!(
16863 editor.selections.ranges(&editor.display_snapshot(cx)),
16864 [Point::new(0, 1)..Point::new(0, 1)]
16865 );
16866 });
16867}
16868
16869#[gpui::test]
16870fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16871 init_test(cx, |_| {});
16872
16873 let markers = vec![('[', ']').into(), ('(', ')').into()];
16874 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16875 indoc! {"
16876 [aaaa
16877 (bbbb]
16878 cccc)",
16879 },
16880 markers.clone(),
16881 );
16882 let excerpt_ranges = markers.into_iter().map(|marker| {
16883 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16884 ExcerptRange::new(context)
16885 });
16886 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16887 let multibuffer = cx.new(|cx| {
16888 let mut multibuffer = MultiBuffer::new(ReadWrite);
16889 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16890 multibuffer
16891 });
16892
16893 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16894 editor.update_in(cx, |editor, window, cx| {
16895 let (expected_text, selection_ranges) = marked_text_ranges(
16896 indoc! {"
16897 aaaa
16898 bˇbbb
16899 bˇbbˇb
16900 cccc"
16901 },
16902 true,
16903 );
16904 assert_eq!(editor.text(cx), expected_text);
16905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16906 s.select_ranges(
16907 selection_ranges
16908 .iter()
16909 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16910 )
16911 });
16912
16913 editor.handle_input("X", window, cx);
16914
16915 let (expected_text, expected_selections) = marked_text_ranges(
16916 indoc! {"
16917 aaaa
16918 bXˇbbXb
16919 bXˇbbXˇb
16920 cccc"
16921 },
16922 false,
16923 );
16924 assert_eq!(editor.text(cx), expected_text);
16925 assert_eq!(
16926 editor.selections.ranges(&editor.display_snapshot(cx)),
16927 expected_selections
16928 .iter()
16929 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16930 .collect::<Vec<_>>()
16931 );
16932
16933 editor.newline(&Newline, window, cx);
16934 let (expected_text, expected_selections) = marked_text_ranges(
16935 indoc! {"
16936 aaaa
16937 bX
16938 ˇbbX
16939 b
16940 bX
16941 ˇbbX
16942 ˇb
16943 cccc"
16944 },
16945 false,
16946 );
16947 assert_eq!(editor.text(cx), expected_text);
16948 assert_eq!(
16949 editor.selections.ranges(&editor.display_snapshot(cx)),
16950 expected_selections
16951 .iter()
16952 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16953 .collect::<Vec<_>>()
16954 );
16955 });
16956}
16957
16958#[gpui::test]
16959fn test_refresh_selections(cx: &mut TestAppContext) {
16960 init_test(cx, |_| {});
16961
16962 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16963 let mut excerpt1_id = None;
16964 let multibuffer = cx.new(|cx| {
16965 let mut multibuffer = MultiBuffer::new(ReadWrite);
16966 excerpt1_id = multibuffer
16967 .push_excerpts(
16968 buffer.clone(),
16969 [
16970 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16971 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16972 ],
16973 cx,
16974 )
16975 .into_iter()
16976 .next();
16977 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16978 multibuffer
16979 });
16980
16981 let editor = cx.add_window(|window, cx| {
16982 let mut editor = build_editor(multibuffer.clone(), window, cx);
16983 let snapshot = editor.snapshot(window, cx);
16984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16985 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16986 });
16987 editor.begin_selection(
16988 Point::new(2, 1).to_display_point(&snapshot),
16989 true,
16990 1,
16991 window,
16992 cx,
16993 );
16994 assert_eq!(
16995 editor.selections.ranges(&editor.display_snapshot(cx)),
16996 [
16997 Point::new(1, 3)..Point::new(1, 3),
16998 Point::new(2, 1)..Point::new(2, 1),
16999 ]
17000 );
17001 editor
17002 });
17003
17004 // Refreshing selections is a no-op when excerpts haven't changed.
17005 _ = editor.update(cx, |editor, window, cx| {
17006 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17007 assert_eq!(
17008 editor.selections.ranges(&editor.display_snapshot(cx)),
17009 [
17010 Point::new(1, 3)..Point::new(1, 3),
17011 Point::new(2, 1)..Point::new(2, 1),
17012 ]
17013 );
17014 });
17015
17016 multibuffer.update(cx, |multibuffer, cx| {
17017 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17018 });
17019 _ = editor.update(cx, |editor, window, cx| {
17020 // Removing an excerpt causes the first selection to become degenerate.
17021 assert_eq!(
17022 editor.selections.ranges(&editor.display_snapshot(cx)),
17023 [
17024 Point::new(0, 0)..Point::new(0, 0),
17025 Point::new(0, 1)..Point::new(0, 1)
17026 ]
17027 );
17028
17029 // Refreshing selections will relocate the first selection to the original buffer
17030 // location.
17031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17032 assert_eq!(
17033 editor.selections.ranges(&editor.display_snapshot(cx)),
17034 [
17035 Point::new(0, 1)..Point::new(0, 1),
17036 Point::new(0, 3)..Point::new(0, 3)
17037 ]
17038 );
17039 assert!(editor.selections.pending_anchor().is_some());
17040 });
17041}
17042
17043#[gpui::test]
17044fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17045 init_test(cx, |_| {});
17046
17047 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17048 let mut excerpt1_id = None;
17049 let multibuffer = cx.new(|cx| {
17050 let mut multibuffer = MultiBuffer::new(ReadWrite);
17051 excerpt1_id = multibuffer
17052 .push_excerpts(
17053 buffer.clone(),
17054 [
17055 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17056 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17057 ],
17058 cx,
17059 )
17060 .into_iter()
17061 .next();
17062 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17063 multibuffer
17064 });
17065
17066 let editor = cx.add_window(|window, cx| {
17067 let mut editor = build_editor(multibuffer.clone(), window, cx);
17068 let snapshot = editor.snapshot(window, cx);
17069 editor.begin_selection(
17070 Point::new(1, 3).to_display_point(&snapshot),
17071 false,
17072 1,
17073 window,
17074 cx,
17075 );
17076 assert_eq!(
17077 editor.selections.ranges(&editor.display_snapshot(cx)),
17078 [Point::new(1, 3)..Point::new(1, 3)]
17079 );
17080 editor
17081 });
17082
17083 multibuffer.update(cx, |multibuffer, cx| {
17084 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17085 });
17086 _ = editor.update(cx, |editor, window, cx| {
17087 assert_eq!(
17088 editor.selections.ranges(&editor.display_snapshot(cx)),
17089 [Point::new(0, 0)..Point::new(0, 0)]
17090 );
17091
17092 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17094 assert_eq!(
17095 editor.selections.ranges(&editor.display_snapshot(cx)),
17096 [Point::new(0, 3)..Point::new(0, 3)]
17097 );
17098 assert!(editor.selections.pending_anchor().is_some());
17099 });
17100}
17101
17102#[gpui::test]
17103async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17104 init_test(cx, |_| {});
17105
17106 let language = Arc::new(
17107 Language::new(
17108 LanguageConfig {
17109 brackets: BracketPairConfig {
17110 pairs: vec![
17111 BracketPair {
17112 start: "{".to_string(),
17113 end: "}".to_string(),
17114 close: true,
17115 surround: true,
17116 newline: true,
17117 },
17118 BracketPair {
17119 start: "/* ".to_string(),
17120 end: " */".to_string(),
17121 close: true,
17122 surround: true,
17123 newline: true,
17124 },
17125 ],
17126 ..Default::default()
17127 },
17128 ..Default::default()
17129 },
17130 Some(tree_sitter_rust::LANGUAGE.into()),
17131 )
17132 .with_indents_query("")
17133 .unwrap(),
17134 );
17135
17136 let text = concat!(
17137 "{ }\n", //
17138 " x\n", //
17139 " /* */\n", //
17140 "x\n", //
17141 "{{} }\n", //
17142 );
17143
17144 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17145 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17146 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17147 editor
17148 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17149 .await;
17150
17151 editor.update_in(cx, |editor, window, cx| {
17152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17153 s.select_display_ranges([
17154 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17155 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17156 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17157 ])
17158 });
17159 editor.newline(&Newline, window, cx);
17160
17161 assert_eq!(
17162 editor.buffer().read(cx).read(cx).text(),
17163 concat!(
17164 "{ \n", // Suppress rustfmt
17165 "\n", //
17166 "}\n", //
17167 " x\n", //
17168 " /* \n", //
17169 " \n", //
17170 " */\n", //
17171 "x\n", //
17172 "{{} \n", //
17173 "}\n", //
17174 )
17175 );
17176 });
17177}
17178
17179#[gpui::test]
17180fn test_highlighted_ranges(cx: &mut TestAppContext) {
17181 init_test(cx, |_| {});
17182
17183 let editor = cx.add_window(|window, cx| {
17184 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17185 build_editor(buffer, window, cx)
17186 });
17187
17188 _ = editor.update(cx, |editor, window, cx| {
17189 struct Type1;
17190 struct Type2;
17191
17192 let buffer = editor.buffer.read(cx).snapshot(cx);
17193
17194 let anchor_range =
17195 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17196
17197 editor.highlight_background::<Type1>(
17198 &[
17199 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17200 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17201 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17202 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17203 ],
17204 |_, _| Hsla::red(),
17205 cx,
17206 );
17207 editor.highlight_background::<Type2>(
17208 &[
17209 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17210 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17211 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17212 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17213 ],
17214 |_, _| Hsla::green(),
17215 cx,
17216 );
17217
17218 let snapshot = editor.snapshot(window, cx);
17219 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17220 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17221 &snapshot,
17222 cx.theme(),
17223 );
17224 assert_eq!(
17225 highlighted_ranges,
17226 &[
17227 (
17228 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17229 Hsla::green(),
17230 ),
17231 (
17232 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17233 Hsla::red(),
17234 ),
17235 (
17236 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17237 Hsla::green(),
17238 ),
17239 (
17240 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17241 Hsla::red(),
17242 ),
17243 ]
17244 );
17245 assert_eq!(
17246 editor.sorted_background_highlights_in_range(
17247 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17248 &snapshot,
17249 cx.theme(),
17250 ),
17251 &[(
17252 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17253 Hsla::red(),
17254 )]
17255 );
17256 });
17257}
17258
17259#[gpui::test]
17260async fn test_following(cx: &mut TestAppContext) {
17261 init_test(cx, |_| {});
17262
17263 let fs = FakeFs::new(cx.executor());
17264 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17265
17266 let buffer = project.update(cx, |project, cx| {
17267 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17268 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17269 });
17270 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17271 let follower = cx.update(|cx| {
17272 cx.open_window(
17273 WindowOptions {
17274 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17275 gpui::Point::new(px(0.), px(0.)),
17276 gpui::Point::new(px(10.), px(80.)),
17277 ))),
17278 ..Default::default()
17279 },
17280 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17281 )
17282 .unwrap()
17283 });
17284
17285 let is_still_following = Rc::new(RefCell::new(true));
17286 let follower_edit_event_count = Rc::new(RefCell::new(0));
17287 let pending_update = Rc::new(RefCell::new(None));
17288 let leader_entity = leader.root(cx).unwrap();
17289 let follower_entity = follower.root(cx).unwrap();
17290 _ = follower.update(cx, {
17291 let update = pending_update.clone();
17292 let is_still_following = is_still_following.clone();
17293 let follower_edit_event_count = follower_edit_event_count.clone();
17294 |_, window, cx| {
17295 cx.subscribe_in(
17296 &leader_entity,
17297 window,
17298 move |_, leader, event, window, cx| {
17299 leader.read(cx).add_event_to_update_proto(
17300 event,
17301 &mut update.borrow_mut(),
17302 window,
17303 cx,
17304 );
17305 },
17306 )
17307 .detach();
17308
17309 cx.subscribe_in(
17310 &follower_entity,
17311 window,
17312 move |_, _, event: &EditorEvent, _window, _cx| {
17313 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17314 *is_still_following.borrow_mut() = false;
17315 }
17316
17317 if let EditorEvent::BufferEdited = event {
17318 *follower_edit_event_count.borrow_mut() += 1;
17319 }
17320 },
17321 )
17322 .detach();
17323 }
17324 });
17325
17326 // Update the selections only
17327 _ = leader.update(cx, |leader, window, cx| {
17328 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17329 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17330 });
17331 });
17332 follower
17333 .update(cx, |follower, window, cx| {
17334 follower.apply_update_proto(
17335 &project,
17336 pending_update.borrow_mut().take().unwrap(),
17337 window,
17338 cx,
17339 )
17340 })
17341 .unwrap()
17342 .await
17343 .unwrap();
17344 _ = follower.update(cx, |follower, _, cx| {
17345 assert_eq!(
17346 follower.selections.ranges(&follower.display_snapshot(cx)),
17347 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17348 );
17349 });
17350 assert!(*is_still_following.borrow());
17351 assert_eq!(*follower_edit_event_count.borrow(), 0);
17352
17353 // Update the scroll position only
17354 _ = leader.update(cx, |leader, window, cx| {
17355 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17356 });
17357 follower
17358 .update(cx, |follower, window, cx| {
17359 follower.apply_update_proto(
17360 &project,
17361 pending_update.borrow_mut().take().unwrap(),
17362 window,
17363 cx,
17364 )
17365 })
17366 .unwrap()
17367 .await
17368 .unwrap();
17369 assert_eq!(
17370 follower
17371 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17372 .unwrap(),
17373 gpui::Point::new(1.5, 3.5)
17374 );
17375 assert!(*is_still_following.borrow());
17376 assert_eq!(*follower_edit_event_count.borrow(), 0);
17377
17378 // Update the selections and scroll position. The follower's scroll position is updated
17379 // via autoscroll, not via the leader's exact scroll position.
17380 _ = leader.update(cx, |leader, window, cx| {
17381 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17382 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17383 });
17384 leader.request_autoscroll(Autoscroll::newest(), cx);
17385 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17386 });
17387 follower
17388 .update(cx, |follower, window, cx| {
17389 follower.apply_update_proto(
17390 &project,
17391 pending_update.borrow_mut().take().unwrap(),
17392 window,
17393 cx,
17394 )
17395 })
17396 .unwrap()
17397 .await
17398 .unwrap();
17399 _ = follower.update(cx, |follower, _, cx| {
17400 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17401 assert_eq!(
17402 follower.selections.ranges(&follower.display_snapshot(cx)),
17403 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17404 );
17405 });
17406 assert!(*is_still_following.borrow());
17407
17408 // Creating a pending selection that precedes another selection
17409 _ = leader.update(cx, |leader, window, cx| {
17410 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17411 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17412 });
17413 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17414 });
17415 follower
17416 .update(cx, |follower, window, cx| {
17417 follower.apply_update_proto(
17418 &project,
17419 pending_update.borrow_mut().take().unwrap(),
17420 window,
17421 cx,
17422 )
17423 })
17424 .unwrap()
17425 .await
17426 .unwrap();
17427 _ = follower.update(cx, |follower, _, cx| {
17428 assert_eq!(
17429 follower.selections.ranges(&follower.display_snapshot(cx)),
17430 vec![
17431 MultiBufferOffset(0)..MultiBufferOffset(0),
17432 MultiBufferOffset(1)..MultiBufferOffset(1)
17433 ]
17434 );
17435 });
17436 assert!(*is_still_following.borrow());
17437
17438 // Extend the pending selection so that it surrounds another selection
17439 _ = leader.update(cx, |leader, window, cx| {
17440 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17441 });
17442 follower
17443 .update(cx, |follower, window, cx| {
17444 follower.apply_update_proto(
17445 &project,
17446 pending_update.borrow_mut().take().unwrap(),
17447 window,
17448 cx,
17449 )
17450 })
17451 .unwrap()
17452 .await
17453 .unwrap();
17454 _ = follower.update(cx, |follower, _, cx| {
17455 assert_eq!(
17456 follower.selections.ranges(&follower.display_snapshot(cx)),
17457 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17458 );
17459 });
17460
17461 // Scrolling locally breaks the follow
17462 _ = follower.update(cx, |follower, window, cx| {
17463 let top_anchor = follower
17464 .buffer()
17465 .read(cx)
17466 .read(cx)
17467 .anchor_after(MultiBufferOffset(0));
17468 follower.set_scroll_anchor(
17469 ScrollAnchor {
17470 anchor: top_anchor,
17471 offset: gpui::Point::new(0.0, 0.5),
17472 },
17473 window,
17474 cx,
17475 );
17476 });
17477 assert!(!(*is_still_following.borrow()));
17478}
17479
17480#[gpui::test]
17481async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17482 init_test(cx, |_| {});
17483
17484 let fs = FakeFs::new(cx.executor());
17485 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17486 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17487 let pane = workspace
17488 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17489 .unwrap();
17490
17491 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17492
17493 let leader = pane.update_in(cx, |_, window, cx| {
17494 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17495 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17496 });
17497
17498 // Start following the editor when it has no excerpts.
17499 let mut state_message =
17500 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17501 let workspace_entity = workspace.root(cx).unwrap();
17502 let follower_1 = cx
17503 .update_window(*workspace.deref(), |_, window, cx| {
17504 Editor::from_state_proto(
17505 workspace_entity,
17506 ViewId {
17507 creator: CollaboratorId::PeerId(PeerId::default()),
17508 id: 0,
17509 },
17510 &mut state_message,
17511 window,
17512 cx,
17513 )
17514 })
17515 .unwrap()
17516 .unwrap()
17517 .await
17518 .unwrap();
17519
17520 let update_message = Rc::new(RefCell::new(None));
17521 follower_1.update_in(cx, {
17522 let update = update_message.clone();
17523 |_, window, cx| {
17524 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17525 leader.read(cx).add_event_to_update_proto(
17526 event,
17527 &mut update.borrow_mut(),
17528 window,
17529 cx,
17530 );
17531 })
17532 .detach();
17533 }
17534 });
17535
17536 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17537 (
17538 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17539 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17540 )
17541 });
17542
17543 // Insert some excerpts.
17544 leader.update(cx, |leader, cx| {
17545 leader.buffer.update(cx, |multibuffer, cx| {
17546 multibuffer.set_excerpts_for_path(
17547 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17548 buffer_1.clone(),
17549 vec![
17550 Point::row_range(0..3),
17551 Point::row_range(1..6),
17552 Point::row_range(12..15),
17553 ],
17554 0,
17555 cx,
17556 );
17557 multibuffer.set_excerpts_for_path(
17558 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17559 buffer_2.clone(),
17560 vec![Point::row_range(0..6), Point::row_range(8..12)],
17561 0,
17562 cx,
17563 );
17564 });
17565 });
17566
17567 // Apply the update of adding the excerpts.
17568 follower_1
17569 .update_in(cx, |follower, window, cx| {
17570 follower.apply_update_proto(
17571 &project,
17572 update_message.borrow().clone().unwrap(),
17573 window,
17574 cx,
17575 )
17576 })
17577 .await
17578 .unwrap();
17579 assert_eq!(
17580 follower_1.update(cx, |editor, cx| editor.text(cx)),
17581 leader.update(cx, |editor, cx| editor.text(cx))
17582 );
17583 update_message.borrow_mut().take();
17584
17585 // Start following separately after it already has excerpts.
17586 let mut state_message =
17587 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17588 let workspace_entity = workspace.root(cx).unwrap();
17589 let follower_2 = cx
17590 .update_window(*workspace.deref(), |_, window, cx| {
17591 Editor::from_state_proto(
17592 workspace_entity,
17593 ViewId {
17594 creator: CollaboratorId::PeerId(PeerId::default()),
17595 id: 0,
17596 },
17597 &mut state_message,
17598 window,
17599 cx,
17600 )
17601 })
17602 .unwrap()
17603 .unwrap()
17604 .await
17605 .unwrap();
17606 assert_eq!(
17607 follower_2.update(cx, |editor, cx| editor.text(cx)),
17608 leader.update(cx, |editor, cx| editor.text(cx))
17609 );
17610
17611 // Remove some excerpts.
17612 leader.update(cx, |leader, cx| {
17613 leader.buffer.update(cx, |multibuffer, cx| {
17614 let excerpt_ids = multibuffer.excerpt_ids();
17615 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17616 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17617 });
17618 });
17619
17620 // Apply the update of removing the excerpts.
17621 follower_1
17622 .update_in(cx, |follower, window, cx| {
17623 follower.apply_update_proto(
17624 &project,
17625 update_message.borrow().clone().unwrap(),
17626 window,
17627 cx,
17628 )
17629 })
17630 .await
17631 .unwrap();
17632 follower_2
17633 .update_in(cx, |follower, window, cx| {
17634 follower.apply_update_proto(
17635 &project,
17636 update_message.borrow().clone().unwrap(),
17637 window,
17638 cx,
17639 )
17640 })
17641 .await
17642 .unwrap();
17643 update_message.borrow_mut().take();
17644 assert_eq!(
17645 follower_1.update(cx, |editor, cx| editor.text(cx)),
17646 leader.update(cx, |editor, cx| editor.text(cx))
17647 );
17648}
17649
17650#[gpui::test]
17651async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17652 init_test(cx, |_| {});
17653
17654 let mut cx = EditorTestContext::new(cx).await;
17655 let lsp_store =
17656 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17657
17658 cx.set_state(indoc! {"
17659 ˇfn func(abc def: i32) -> u32 {
17660 }
17661 "});
17662
17663 cx.update(|_, cx| {
17664 lsp_store.update(cx, |lsp_store, cx| {
17665 lsp_store
17666 .update_diagnostics(
17667 LanguageServerId(0),
17668 lsp::PublishDiagnosticsParams {
17669 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17670 version: None,
17671 diagnostics: vec![
17672 lsp::Diagnostic {
17673 range: lsp::Range::new(
17674 lsp::Position::new(0, 11),
17675 lsp::Position::new(0, 12),
17676 ),
17677 severity: Some(lsp::DiagnosticSeverity::ERROR),
17678 ..Default::default()
17679 },
17680 lsp::Diagnostic {
17681 range: lsp::Range::new(
17682 lsp::Position::new(0, 12),
17683 lsp::Position::new(0, 15),
17684 ),
17685 severity: Some(lsp::DiagnosticSeverity::ERROR),
17686 ..Default::default()
17687 },
17688 lsp::Diagnostic {
17689 range: lsp::Range::new(
17690 lsp::Position::new(0, 25),
17691 lsp::Position::new(0, 28),
17692 ),
17693 severity: Some(lsp::DiagnosticSeverity::ERROR),
17694 ..Default::default()
17695 },
17696 ],
17697 },
17698 None,
17699 DiagnosticSourceKind::Pushed,
17700 &[],
17701 cx,
17702 )
17703 .unwrap()
17704 });
17705 });
17706
17707 executor.run_until_parked();
17708
17709 cx.update_editor(|editor, window, cx| {
17710 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17711 });
17712
17713 cx.assert_editor_state(indoc! {"
17714 fn func(abc def: i32) -> ˇu32 {
17715 }
17716 "});
17717
17718 cx.update_editor(|editor, window, cx| {
17719 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17720 });
17721
17722 cx.assert_editor_state(indoc! {"
17723 fn func(abc ˇdef: i32) -> u32 {
17724 }
17725 "});
17726
17727 cx.update_editor(|editor, window, cx| {
17728 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17729 });
17730
17731 cx.assert_editor_state(indoc! {"
17732 fn func(abcˇ def: i32) -> u32 {
17733 }
17734 "});
17735
17736 cx.update_editor(|editor, window, cx| {
17737 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17738 });
17739
17740 cx.assert_editor_state(indoc! {"
17741 fn func(abc def: i32) -> ˇu32 {
17742 }
17743 "});
17744}
17745
17746#[gpui::test]
17747async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17748 init_test(cx, |_| {});
17749
17750 let mut cx = EditorTestContext::new(cx).await;
17751
17752 let diff_base = r#"
17753 use some::mod;
17754
17755 const A: u32 = 42;
17756
17757 fn main() {
17758 println!("hello");
17759
17760 println!("world");
17761 }
17762 "#
17763 .unindent();
17764
17765 // Edits are modified, removed, modified, added
17766 cx.set_state(
17767 &r#"
17768 use some::modified;
17769
17770 ˇ
17771 fn main() {
17772 println!("hello there");
17773
17774 println!("around the");
17775 println!("world");
17776 }
17777 "#
17778 .unindent(),
17779 );
17780
17781 cx.set_head_text(&diff_base);
17782 executor.run_until_parked();
17783
17784 cx.update_editor(|editor, window, cx| {
17785 //Wrap around the bottom of the buffer
17786 for _ in 0..3 {
17787 editor.go_to_next_hunk(&GoToHunk, window, cx);
17788 }
17789 });
17790
17791 cx.assert_editor_state(
17792 &r#"
17793 ˇuse some::modified;
17794
17795
17796 fn main() {
17797 println!("hello there");
17798
17799 println!("around the");
17800 println!("world");
17801 }
17802 "#
17803 .unindent(),
17804 );
17805
17806 cx.update_editor(|editor, window, cx| {
17807 //Wrap around the top of the buffer
17808 for _ in 0..2 {
17809 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17810 }
17811 });
17812
17813 cx.assert_editor_state(
17814 &r#"
17815 use some::modified;
17816
17817
17818 fn main() {
17819 ˇ println!("hello there");
17820
17821 println!("around the");
17822 println!("world");
17823 }
17824 "#
17825 .unindent(),
17826 );
17827
17828 cx.update_editor(|editor, window, cx| {
17829 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17830 });
17831
17832 cx.assert_editor_state(
17833 &r#"
17834 use some::modified;
17835
17836 ˇ
17837 fn main() {
17838 println!("hello there");
17839
17840 println!("around the");
17841 println!("world");
17842 }
17843 "#
17844 .unindent(),
17845 );
17846
17847 cx.update_editor(|editor, window, cx| {
17848 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17849 });
17850
17851 cx.assert_editor_state(
17852 &r#"
17853 ˇuse some::modified;
17854
17855
17856 fn main() {
17857 println!("hello there");
17858
17859 println!("around the");
17860 println!("world");
17861 }
17862 "#
17863 .unindent(),
17864 );
17865
17866 cx.update_editor(|editor, window, cx| {
17867 for _ in 0..2 {
17868 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17869 }
17870 });
17871
17872 cx.assert_editor_state(
17873 &r#"
17874 use some::modified;
17875
17876
17877 fn main() {
17878 ˇ println!("hello there");
17879
17880 println!("around the");
17881 println!("world");
17882 }
17883 "#
17884 .unindent(),
17885 );
17886
17887 cx.update_editor(|editor, window, cx| {
17888 editor.fold(&Fold, window, cx);
17889 });
17890
17891 cx.update_editor(|editor, window, cx| {
17892 editor.go_to_next_hunk(&GoToHunk, window, cx);
17893 });
17894
17895 cx.assert_editor_state(
17896 &r#"
17897 ˇuse some::modified;
17898
17899
17900 fn main() {
17901 println!("hello there");
17902
17903 println!("around the");
17904 println!("world");
17905 }
17906 "#
17907 .unindent(),
17908 );
17909}
17910
17911#[test]
17912fn test_split_words() {
17913 fn split(text: &str) -> Vec<&str> {
17914 split_words(text).collect()
17915 }
17916
17917 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17918 assert_eq!(split("hello_world"), &["hello_", "world"]);
17919 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17920 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17921 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17922 assert_eq!(split("helloworld"), &["helloworld"]);
17923
17924 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17925}
17926
17927#[test]
17928fn test_split_words_for_snippet_prefix() {
17929 fn split(text: &str) -> Vec<&str> {
17930 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17931 }
17932
17933 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17934 assert_eq!(split("hello_world"), &["hello_world"]);
17935 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17936 assert_eq!(split("Hello_World"), &["Hello_World"]);
17937 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17938 assert_eq!(split("helloworld"), &["helloworld"]);
17939 assert_eq!(
17940 split("this@is!@#$^many . symbols"),
17941 &[
17942 "symbols",
17943 " symbols",
17944 ". symbols",
17945 " . symbols",
17946 " . symbols",
17947 " . symbols",
17948 "many . symbols",
17949 "^many . symbols",
17950 "$^many . symbols",
17951 "#$^many . symbols",
17952 "@#$^many . symbols",
17953 "!@#$^many . symbols",
17954 "is!@#$^many . symbols",
17955 "@is!@#$^many . symbols",
17956 "this@is!@#$^many . symbols",
17957 ],
17958 );
17959 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17960}
17961
17962#[gpui::test]
17963async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17964 init_test(cx, |_| {});
17965
17966 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17967
17968 #[track_caller]
17969 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17970 let _state_context = cx.set_state(before);
17971 cx.run_until_parked();
17972 cx.update_editor(|editor, window, cx| {
17973 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17974 });
17975 cx.run_until_parked();
17976 cx.assert_editor_state(after);
17977 }
17978
17979 // Outside bracket jumps to outside of matching bracket
17980 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17981 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17982
17983 // Inside bracket jumps to inside of matching bracket
17984 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17985 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17986
17987 // When outside a bracket and inside, favor jumping to the inside bracket
17988 assert(
17989 "console.log('foo', [1, 2, 3]ˇ);",
17990 "console.log('foo', ˇ[1, 2, 3]);",
17991 &mut cx,
17992 );
17993 assert(
17994 "console.log(ˇ'foo', [1, 2, 3]);",
17995 "console.log('foo'ˇ, [1, 2, 3]);",
17996 &mut cx,
17997 );
17998
17999 // Bias forward if two options are equally likely
18000 assert(
18001 "let result = curried_fun()ˇ();",
18002 "let result = curried_fun()()ˇ;",
18003 &mut cx,
18004 );
18005
18006 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18007 assert(
18008 indoc! {"
18009 function test() {
18010 console.log('test')ˇ
18011 }"},
18012 indoc! {"
18013 function test() {
18014 console.logˇ('test')
18015 }"},
18016 &mut cx,
18017 );
18018}
18019
18020#[gpui::test]
18021async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18022 init_test(cx, |_| {});
18023 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18024 language_registry.add(markdown_lang());
18025 language_registry.add(rust_lang());
18026 let buffer = cx.new(|cx| {
18027 let mut buffer = language::Buffer::local(
18028 indoc! {"
18029 ```rs
18030 impl Worktree {
18031 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18032 }
18033 }
18034 ```
18035 "},
18036 cx,
18037 );
18038 buffer.set_language_registry(language_registry.clone());
18039 buffer.set_language(Some(markdown_lang()), cx);
18040 buffer
18041 });
18042 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18043 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18044 cx.executor().run_until_parked();
18045 _ = editor.update(cx, |editor, window, cx| {
18046 // Case 1: Test outer enclosing brackets
18047 select_ranges(
18048 editor,
18049 &indoc! {"
18050 ```rs
18051 impl Worktree {
18052 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18053 }
18054 }ˇ
18055 ```
18056 "},
18057 window,
18058 cx,
18059 );
18060 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18061 assert_text_with_selections(
18062 editor,
18063 &indoc! {"
18064 ```rs
18065 impl Worktree ˇ{
18066 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18067 }
18068 }
18069 ```
18070 "},
18071 cx,
18072 );
18073 // Case 2: Test inner enclosing brackets
18074 select_ranges(
18075 editor,
18076 &indoc! {"
18077 ```rs
18078 impl Worktree {
18079 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18080 }ˇ
18081 }
18082 ```
18083 "},
18084 window,
18085 cx,
18086 );
18087 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18088 assert_text_with_selections(
18089 editor,
18090 &indoc! {"
18091 ```rs
18092 impl Worktree {
18093 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18094 }
18095 }
18096 ```
18097 "},
18098 cx,
18099 );
18100 });
18101}
18102
18103#[gpui::test]
18104async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18105 init_test(cx, |_| {});
18106
18107 let fs = FakeFs::new(cx.executor());
18108 fs.insert_tree(
18109 path!("/a"),
18110 json!({
18111 "main.rs": "fn main() { let a = 5; }",
18112 "other.rs": "// Test file",
18113 }),
18114 )
18115 .await;
18116 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18117
18118 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18119 language_registry.add(Arc::new(Language::new(
18120 LanguageConfig {
18121 name: "Rust".into(),
18122 matcher: LanguageMatcher {
18123 path_suffixes: vec!["rs".to_string()],
18124 ..Default::default()
18125 },
18126 brackets: BracketPairConfig {
18127 pairs: vec![BracketPair {
18128 start: "{".to_string(),
18129 end: "}".to_string(),
18130 close: true,
18131 surround: true,
18132 newline: true,
18133 }],
18134 disabled_scopes_by_bracket_ix: Vec::new(),
18135 },
18136 ..Default::default()
18137 },
18138 Some(tree_sitter_rust::LANGUAGE.into()),
18139 )));
18140 let mut fake_servers = language_registry.register_fake_lsp(
18141 "Rust",
18142 FakeLspAdapter {
18143 capabilities: lsp::ServerCapabilities {
18144 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18145 first_trigger_character: "{".to_string(),
18146 more_trigger_character: None,
18147 }),
18148 ..Default::default()
18149 },
18150 ..Default::default()
18151 },
18152 );
18153
18154 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18155
18156 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18157
18158 let worktree_id = workspace
18159 .update(cx, |workspace, _, cx| {
18160 workspace.project().update(cx, |project, cx| {
18161 project.worktrees(cx).next().unwrap().read(cx).id()
18162 })
18163 })
18164 .unwrap();
18165
18166 let buffer = project
18167 .update(cx, |project, cx| {
18168 project.open_local_buffer(path!("/a/main.rs"), cx)
18169 })
18170 .await
18171 .unwrap();
18172 let editor_handle = workspace
18173 .update(cx, |workspace, window, cx| {
18174 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18175 })
18176 .unwrap()
18177 .await
18178 .unwrap()
18179 .downcast::<Editor>()
18180 .unwrap();
18181
18182 cx.executor().start_waiting();
18183 let fake_server = fake_servers.next().await.unwrap();
18184
18185 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18186 |params, _| async move {
18187 assert_eq!(
18188 params.text_document_position.text_document.uri,
18189 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18190 );
18191 assert_eq!(
18192 params.text_document_position.position,
18193 lsp::Position::new(0, 21),
18194 );
18195
18196 Ok(Some(vec![lsp::TextEdit {
18197 new_text: "]".to_string(),
18198 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18199 }]))
18200 },
18201 );
18202
18203 editor_handle.update_in(cx, |editor, window, cx| {
18204 window.focus(&editor.focus_handle(cx));
18205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18206 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18207 });
18208 editor.handle_input("{", window, cx);
18209 });
18210
18211 cx.executor().run_until_parked();
18212
18213 buffer.update(cx, |buffer, _| {
18214 assert_eq!(
18215 buffer.text(),
18216 "fn main() { let a = {5}; }",
18217 "No extra braces from on type formatting should appear in the buffer"
18218 )
18219 });
18220}
18221
18222#[gpui::test(iterations = 20, seeds(31))]
18223async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18224 init_test(cx, |_| {});
18225
18226 let mut cx = EditorLspTestContext::new_rust(
18227 lsp::ServerCapabilities {
18228 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18229 first_trigger_character: ".".to_string(),
18230 more_trigger_character: None,
18231 }),
18232 ..Default::default()
18233 },
18234 cx,
18235 )
18236 .await;
18237
18238 cx.update_buffer(|buffer, _| {
18239 // This causes autoindent to be async.
18240 buffer.set_sync_parse_timeout(Duration::ZERO)
18241 });
18242
18243 cx.set_state("fn c() {\n d()ˇ\n}\n");
18244 cx.simulate_keystroke("\n");
18245 cx.run_until_parked();
18246
18247 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18248 let mut request =
18249 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18250 let buffer_cloned = buffer_cloned.clone();
18251 async move {
18252 buffer_cloned.update(&mut cx, |buffer, _| {
18253 assert_eq!(
18254 buffer.text(),
18255 "fn c() {\n d()\n .\n}\n",
18256 "OnTypeFormatting should triggered after autoindent applied"
18257 )
18258 })?;
18259
18260 Ok(Some(vec![]))
18261 }
18262 });
18263
18264 cx.simulate_keystroke(".");
18265 cx.run_until_parked();
18266
18267 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18268 assert!(request.next().await.is_some());
18269 request.close();
18270 assert!(request.next().await.is_none());
18271}
18272
18273#[gpui::test]
18274async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18275 init_test(cx, |_| {});
18276
18277 let fs = FakeFs::new(cx.executor());
18278 fs.insert_tree(
18279 path!("/a"),
18280 json!({
18281 "main.rs": "fn main() { let a = 5; }",
18282 "other.rs": "// Test file",
18283 }),
18284 )
18285 .await;
18286
18287 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18288
18289 let server_restarts = Arc::new(AtomicUsize::new(0));
18290 let closure_restarts = Arc::clone(&server_restarts);
18291 let language_server_name = "test language server";
18292 let language_name: LanguageName = "Rust".into();
18293
18294 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18295 language_registry.add(Arc::new(Language::new(
18296 LanguageConfig {
18297 name: language_name.clone(),
18298 matcher: LanguageMatcher {
18299 path_suffixes: vec!["rs".to_string()],
18300 ..Default::default()
18301 },
18302 ..Default::default()
18303 },
18304 Some(tree_sitter_rust::LANGUAGE.into()),
18305 )));
18306 let mut fake_servers = language_registry.register_fake_lsp(
18307 "Rust",
18308 FakeLspAdapter {
18309 name: language_server_name,
18310 initialization_options: Some(json!({
18311 "testOptionValue": true
18312 })),
18313 initializer: Some(Box::new(move |fake_server| {
18314 let task_restarts = Arc::clone(&closure_restarts);
18315 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18316 task_restarts.fetch_add(1, atomic::Ordering::Release);
18317 futures::future::ready(Ok(()))
18318 });
18319 })),
18320 ..Default::default()
18321 },
18322 );
18323
18324 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18325 let _buffer = project
18326 .update(cx, |project, cx| {
18327 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18328 })
18329 .await
18330 .unwrap();
18331 let _fake_server = fake_servers.next().await.unwrap();
18332 update_test_language_settings(cx, |language_settings| {
18333 language_settings.languages.0.insert(
18334 language_name.clone().0,
18335 LanguageSettingsContent {
18336 tab_size: NonZeroU32::new(8),
18337 ..Default::default()
18338 },
18339 );
18340 });
18341 cx.executor().run_until_parked();
18342 assert_eq!(
18343 server_restarts.load(atomic::Ordering::Acquire),
18344 0,
18345 "Should not restart LSP server on an unrelated change"
18346 );
18347
18348 update_test_project_settings(cx, |project_settings| {
18349 project_settings.lsp.insert(
18350 "Some other server name".into(),
18351 LspSettings {
18352 binary: None,
18353 settings: None,
18354 initialization_options: Some(json!({
18355 "some other init value": false
18356 })),
18357 enable_lsp_tasks: false,
18358 fetch: None,
18359 },
18360 );
18361 });
18362 cx.executor().run_until_parked();
18363 assert_eq!(
18364 server_restarts.load(atomic::Ordering::Acquire),
18365 0,
18366 "Should not restart LSP server on an unrelated LSP settings change"
18367 );
18368
18369 update_test_project_settings(cx, |project_settings| {
18370 project_settings.lsp.insert(
18371 language_server_name.into(),
18372 LspSettings {
18373 binary: None,
18374 settings: None,
18375 initialization_options: Some(json!({
18376 "anotherInitValue": false
18377 })),
18378 enable_lsp_tasks: false,
18379 fetch: None,
18380 },
18381 );
18382 });
18383 cx.executor().run_until_parked();
18384 assert_eq!(
18385 server_restarts.load(atomic::Ordering::Acquire),
18386 1,
18387 "Should restart LSP server on a related LSP settings change"
18388 );
18389
18390 update_test_project_settings(cx, |project_settings| {
18391 project_settings.lsp.insert(
18392 language_server_name.into(),
18393 LspSettings {
18394 binary: None,
18395 settings: None,
18396 initialization_options: Some(json!({
18397 "anotherInitValue": false
18398 })),
18399 enable_lsp_tasks: false,
18400 fetch: None,
18401 },
18402 );
18403 });
18404 cx.executor().run_until_parked();
18405 assert_eq!(
18406 server_restarts.load(atomic::Ordering::Acquire),
18407 1,
18408 "Should not restart LSP server on a related LSP settings change that is the same"
18409 );
18410
18411 update_test_project_settings(cx, |project_settings| {
18412 project_settings.lsp.insert(
18413 language_server_name.into(),
18414 LspSettings {
18415 binary: None,
18416 settings: None,
18417 initialization_options: None,
18418 enable_lsp_tasks: false,
18419 fetch: None,
18420 },
18421 );
18422 });
18423 cx.executor().run_until_parked();
18424 assert_eq!(
18425 server_restarts.load(atomic::Ordering::Acquire),
18426 2,
18427 "Should restart LSP server on another related LSP settings change"
18428 );
18429}
18430
18431#[gpui::test]
18432async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18433 init_test(cx, |_| {});
18434
18435 let mut cx = EditorLspTestContext::new_rust(
18436 lsp::ServerCapabilities {
18437 completion_provider: Some(lsp::CompletionOptions {
18438 trigger_characters: Some(vec![".".to_string()]),
18439 resolve_provider: Some(true),
18440 ..Default::default()
18441 }),
18442 ..Default::default()
18443 },
18444 cx,
18445 )
18446 .await;
18447
18448 cx.set_state("fn main() { let a = 2ˇ; }");
18449 cx.simulate_keystroke(".");
18450 let completion_item = lsp::CompletionItem {
18451 label: "some".into(),
18452 kind: Some(lsp::CompletionItemKind::SNIPPET),
18453 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18454 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18455 kind: lsp::MarkupKind::Markdown,
18456 value: "```rust\nSome(2)\n```".to_string(),
18457 })),
18458 deprecated: Some(false),
18459 sort_text: Some("fffffff2".to_string()),
18460 filter_text: Some("some".to_string()),
18461 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18462 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18463 range: lsp::Range {
18464 start: lsp::Position {
18465 line: 0,
18466 character: 22,
18467 },
18468 end: lsp::Position {
18469 line: 0,
18470 character: 22,
18471 },
18472 },
18473 new_text: "Some(2)".to_string(),
18474 })),
18475 additional_text_edits: Some(vec![lsp::TextEdit {
18476 range: lsp::Range {
18477 start: lsp::Position {
18478 line: 0,
18479 character: 20,
18480 },
18481 end: lsp::Position {
18482 line: 0,
18483 character: 22,
18484 },
18485 },
18486 new_text: "".to_string(),
18487 }]),
18488 ..Default::default()
18489 };
18490
18491 let closure_completion_item = completion_item.clone();
18492 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18493 let task_completion_item = closure_completion_item.clone();
18494 async move {
18495 Ok(Some(lsp::CompletionResponse::Array(vec![
18496 task_completion_item,
18497 ])))
18498 }
18499 });
18500
18501 request.next().await;
18502
18503 cx.condition(|editor, _| editor.context_menu_visible())
18504 .await;
18505 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18506 editor
18507 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18508 .unwrap()
18509 });
18510 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18511
18512 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18513 let task_completion_item = completion_item.clone();
18514 async move { Ok(task_completion_item) }
18515 })
18516 .next()
18517 .await
18518 .unwrap();
18519 apply_additional_edits.await.unwrap();
18520 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18521}
18522
18523#[gpui::test]
18524async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18525 init_test(cx, |_| {});
18526
18527 let mut cx = EditorLspTestContext::new_rust(
18528 lsp::ServerCapabilities {
18529 completion_provider: Some(lsp::CompletionOptions {
18530 trigger_characters: Some(vec![".".to_string()]),
18531 resolve_provider: Some(true),
18532 ..Default::default()
18533 }),
18534 ..Default::default()
18535 },
18536 cx,
18537 )
18538 .await;
18539
18540 cx.set_state("fn main() { let a = 2ˇ; }");
18541 cx.simulate_keystroke(".");
18542
18543 let item1 = lsp::CompletionItem {
18544 label: "method id()".to_string(),
18545 filter_text: Some("id".to_string()),
18546 detail: None,
18547 documentation: None,
18548 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18549 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18550 new_text: ".id".to_string(),
18551 })),
18552 ..lsp::CompletionItem::default()
18553 };
18554
18555 let item2 = lsp::CompletionItem {
18556 label: "other".to_string(),
18557 filter_text: Some("other".to_string()),
18558 detail: None,
18559 documentation: None,
18560 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18561 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18562 new_text: ".other".to_string(),
18563 })),
18564 ..lsp::CompletionItem::default()
18565 };
18566
18567 let item1 = item1.clone();
18568 cx.set_request_handler::<lsp::request::Completion, _, _>({
18569 let item1 = item1.clone();
18570 move |_, _, _| {
18571 let item1 = item1.clone();
18572 let item2 = item2.clone();
18573 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18574 }
18575 })
18576 .next()
18577 .await;
18578
18579 cx.condition(|editor, _| editor.context_menu_visible())
18580 .await;
18581 cx.update_editor(|editor, _, _| {
18582 let context_menu = editor.context_menu.borrow_mut();
18583 let context_menu = context_menu
18584 .as_ref()
18585 .expect("Should have the context menu deployed");
18586 match context_menu {
18587 CodeContextMenu::Completions(completions_menu) => {
18588 let completions = completions_menu.completions.borrow_mut();
18589 assert_eq!(
18590 completions
18591 .iter()
18592 .map(|completion| &completion.label.text)
18593 .collect::<Vec<_>>(),
18594 vec!["method id()", "other"]
18595 )
18596 }
18597 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18598 }
18599 });
18600
18601 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18602 let item1 = item1.clone();
18603 move |_, item_to_resolve, _| {
18604 let item1 = item1.clone();
18605 async move {
18606 if item1 == item_to_resolve {
18607 Ok(lsp::CompletionItem {
18608 label: "method id()".to_string(),
18609 filter_text: Some("id".to_string()),
18610 detail: Some("Now resolved!".to_string()),
18611 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18612 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18613 range: lsp::Range::new(
18614 lsp::Position::new(0, 22),
18615 lsp::Position::new(0, 22),
18616 ),
18617 new_text: ".id".to_string(),
18618 })),
18619 ..lsp::CompletionItem::default()
18620 })
18621 } else {
18622 Ok(item_to_resolve)
18623 }
18624 }
18625 }
18626 })
18627 .next()
18628 .await
18629 .unwrap();
18630 cx.run_until_parked();
18631
18632 cx.update_editor(|editor, window, cx| {
18633 editor.context_menu_next(&Default::default(), window, cx);
18634 });
18635
18636 cx.update_editor(|editor, _, _| {
18637 let context_menu = editor.context_menu.borrow_mut();
18638 let context_menu = context_menu
18639 .as_ref()
18640 .expect("Should have the context menu deployed");
18641 match context_menu {
18642 CodeContextMenu::Completions(completions_menu) => {
18643 let completions = completions_menu.completions.borrow_mut();
18644 assert_eq!(
18645 completions
18646 .iter()
18647 .map(|completion| &completion.label.text)
18648 .collect::<Vec<_>>(),
18649 vec!["method id() Now resolved!", "other"],
18650 "Should update first completion label, but not second as the filter text did not match."
18651 );
18652 }
18653 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18654 }
18655 });
18656}
18657
18658#[gpui::test]
18659async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18660 init_test(cx, |_| {});
18661 let mut cx = EditorLspTestContext::new_rust(
18662 lsp::ServerCapabilities {
18663 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18664 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18665 completion_provider: Some(lsp::CompletionOptions {
18666 resolve_provider: Some(true),
18667 ..Default::default()
18668 }),
18669 ..Default::default()
18670 },
18671 cx,
18672 )
18673 .await;
18674 cx.set_state(indoc! {"
18675 struct TestStruct {
18676 field: i32
18677 }
18678
18679 fn mainˇ() {
18680 let unused_var = 42;
18681 let test_struct = TestStruct { field: 42 };
18682 }
18683 "});
18684 let symbol_range = cx.lsp_range(indoc! {"
18685 struct TestStruct {
18686 field: i32
18687 }
18688
18689 «fn main»() {
18690 let unused_var = 42;
18691 let test_struct = TestStruct { field: 42 };
18692 }
18693 "});
18694 let mut hover_requests =
18695 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18696 Ok(Some(lsp::Hover {
18697 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18698 kind: lsp::MarkupKind::Markdown,
18699 value: "Function documentation".to_string(),
18700 }),
18701 range: Some(symbol_range),
18702 }))
18703 });
18704
18705 // Case 1: Test that code action menu hide hover popover
18706 cx.dispatch_action(Hover);
18707 hover_requests.next().await;
18708 cx.condition(|editor, _| editor.hover_state.visible()).await;
18709 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18710 move |_, _, _| async move {
18711 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18712 lsp::CodeAction {
18713 title: "Remove unused variable".to_string(),
18714 kind: Some(CodeActionKind::QUICKFIX),
18715 edit: Some(lsp::WorkspaceEdit {
18716 changes: Some(
18717 [(
18718 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18719 vec![lsp::TextEdit {
18720 range: lsp::Range::new(
18721 lsp::Position::new(5, 4),
18722 lsp::Position::new(5, 27),
18723 ),
18724 new_text: "".to_string(),
18725 }],
18726 )]
18727 .into_iter()
18728 .collect(),
18729 ),
18730 ..Default::default()
18731 }),
18732 ..Default::default()
18733 },
18734 )]))
18735 },
18736 );
18737 cx.update_editor(|editor, window, cx| {
18738 editor.toggle_code_actions(
18739 &ToggleCodeActions {
18740 deployed_from: None,
18741 quick_launch: false,
18742 },
18743 window,
18744 cx,
18745 );
18746 });
18747 code_action_requests.next().await;
18748 cx.run_until_parked();
18749 cx.condition(|editor, _| editor.context_menu_visible())
18750 .await;
18751 cx.update_editor(|editor, _, _| {
18752 assert!(
18753 !editor.hover_state.visible(),
18754 "Hover popover should be hidden when code action menu is shown"
18755 );
18756 // Hide code actions
18757 editor.context_menu.take();
18758 });
18759
18760 // Case 2: Test that code completions hide hover popover
18761 cx.dispatch_action(Hover);
18762 hover_requests.next().await;
18763 cx.condition(|editor, _| editor.hover_state.visible()).await;
18764 let counter = Arc::new(AtomicUsize::new(0));
18765 let mut completion_requests =
18766 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18767 let counter = counter.clone();
18768 async move {
18769 counter.fetch_add(1, atomic::Ordering::Release);
18770 Ok(Some(lsp::CompletionResponse::Array(vec![
18771 lsp::CompletionItem {
18772 label: "main".into(),
18773 kind: Some(lsp::CompletionItemKind::FUNCTION),
18774 detail: Some("() -> ()".to_string()),
18775 ..Default::default()
18776 },
18777 lsp::CompletionItem {
18778 label: "TestStruct".into(),
18779 kind: Some(lsp::CompletionItemKind::STRUCT),
18780 detail: Some("struct TestStruct".to_string()),
18781 ..Default::default()
18782 },
18783 ])))
18784 }
18785 });
18786 cx.update_editor(|editor, window, cx| {
18787 editor.show_completions(&ShowCompletions, window, cx);
18788 });
18789 completion_requests.next().await;
18790 cx.condition(|editor, _| editor.context_menu_visible())
18791 .await;
18792 cx.update_editor(|editor, _, _| {
18793 assert!(
18794 !editor.hover_state.visible(),
18795 "Hover popover should be hidden when completion menu is shown"
18796 );
18797 });
18798}
18799
18800#[gpui::test]
18801async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18802 init_test(cx, |_| {});
18803
18804 let mut cx = EditorLspTestContext::new_rust(
18805 lsp::ServerCapabilities {
18806 completion_provider: Some(lsp::CompletionOptions {
18807 trigger_characters: Some(vec![".".to_string()]),
18808 resolve_provider: Some(true),
18809 ..Default::default()
18810 }),
18811 ..Default::default()
18812 },
18813 cx,
18814 )
18815 .await;
18816
18817 cx.set_state("fn main() { let a = 2ˇ; }");
18818 cx.simulate_keystroke(".");
18819
18820 let unresolved_item_1 = lsp::CompletionItem {
18821 label: "id".to_string(),
18822 filter_text: Some("id".to_string()),
18823 detail: None,
18824 documentation: None,
18825 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18826 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18827 new_text: ".id".to_string(),
18828 })),
18829 ..lsp::CompletionItem::default()
18830 };
18831 let resolved_item_1 = lsp::CompletionItem {
18832 additional_text_edits: Some(vec![lsp::TextEdit {
18833 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18834 new_text: "!!".to_string(),
18835 }]),
18836 ..unresolved_item_1.clone()
18837 };
18838 let unresolved_item_2 = lsp::CompletionItem {
18839 label: "other".to_string(),
18840 filter_text: Some("other".to_string()),
18841 detail: None,
18842 documentation: None,
18843 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18844 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18845 new_text: ".other".to_string(),
18846 })),
18847 ..lsp::CompletionItem::default()
18848 };
18849 let resolved_item_2 = lsp::CompletionItem {
18850 additional_text_edits: Some(vec![lsp::TextEdit {
18851 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18852 new_text: "??".to_string(),
18853 }]),
18854 ..unresolved_item_2.clone()
18855 };
18856
18857 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18858 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18859 cx.lsp
18860 .server
18861 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18862 let unresolved_item_1 = unresolved_item_1.clone();
18863 let resolved_item_1 = resolved_item_1.clone();
18864 let unresolved_item_2 = unresolved_item_2.clone();
18865 let resolved_item_2 = resolved_item_2.clone();
18866 let resolve_requests_1 = resolve_requests_1.clone();
18867 let resolve_requests_2 = resolve_requests_2.clone();
18868 move |unresolved_request, _| {
18869 let unresolved_item_1 = unresolved_item_1.clone();
18870 let resolved_item_1 = resolved_item_1.clone();
18871 let unresolved_item_2 = unresolved_item_2.clone();
18872 let resolved_item_2 = resolved_item_2.clone();
18873 let resolve_requests_1 = resolve_requests_1.clone();
18874 let resolve_requests_2 = resolve_requests_2.clone();
18875 async move {
18876 if unresolved_request == unresolved_item_1 {
18877 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18878 Ok(resolved_item_1.clone())
18879 } else if unresolved_request == unresolved_item_2 {
18880 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18881 Ok(resolved_item_2.clone())
18882 } else {
18883 panic!("Unexpected completion item {unresolved_request:?}")
18884 }
18885 }
18886 }
18887 })
18888 .detach();
18889
18890 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18891 let unresolved_item_1 = unresolved_item_1.clone();
18892 let unresolved_item_2 = unresolved_item_2.clone();
18893 async move {
18894 Ok(Some(lsp::CompletionResponse::Array(vec![
18895 unresolved_item_1,
18896 unresolved_item_2,
18897 ])))
18898 }
18899 })
18900 .next()
18901 .await;
18902
18903 cx.condition(|editor, _| editor.context_menu_visible())
18904 .await;
18905 cx.update_editor(|editor, _, _| {
18906 let context_menu = editor.context_menu.borrow_mut();
18907 let context_menu = context_menu
18908 .as_ref()
18909 .expect("Should have the context menu deployed");
18910 match context_menu {
18911 CodeContextMenu::Completions(completions_menu) => {
18912 let completions = completions_menu.completions.borrow_mut();
18913 assert_eq!(
18914 completions
18915 .iter()
18916 .map(|completion| &completion.label.text)
18917 .collect::<Vec<_>>(),
18918 vec!["id", "other"]
18919 )
18920 }
18921 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18922 }
18923 });
18924 cx.run_until_parked();
18925
18926 cx.update_editor(|editor, window, cx| {
18927 editor.context_menu_next(&ContextMenuNext, window, cx);
18928 });
18929 cx.run_until_parked();
18930 cx.update_editor(|editor, window, cx| {
18931 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18932 });
18933 cx.run_until_parked();
18934 cx.update_editor(|editor, window, cx| {
18935 editor.context_menu_next(&ContextMenuNext, window, cx);
18936 });
18937 cx.run_until_parked();
18938 cx.update_editor(|editor, window, cx| {
18939 editor
18940 .compose_completion(&ComposeCompletion::default(), window, cx)
18941 .expect("No task returned")
18942 })
18943 .await
18944 .expect("Completion failed");
18945 cx.run_until_parked();
18946
18947 cx.update_editor(|editor, _, cx| {
18948 assert_eq!(
18949 resolve_requests_1.load(atomic::Ordering::Acquire),
18950 1,
18951 "Should always resolve once despite multiple selections"
18952 );
18953 assert_eq!(
18954 resolve_requests_2.load(atomic::Ordering::Acquire),
18955 1,
18956 "Should always resolve once after multiple selections and applying the completion"
18957 );
18958 assert_eq!(
18959 editor.text(cx),
18960 "fn main() { let a = ??.other; }",
18961 "Should use resolved data when applying the completion"
18962 );
18963 });
18964}
18965
18966#[gpui::test]
18967async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18968 init_test(cx, |_| {});
18969
18970 let item_0 = lsp::CompletionItem {
18971 label: "abs".into(),
18972 insert_text: Some("abs".into()),
18973 data: Some(json!({ "very": "special"})),
18974 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18975 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18976 lsp::InsertReplaceEdit {
18977 new_text: "abs".to_string(),
18978 insert: lsp::Range::default(),
18979 replace: lsp::Range::default(),
18980 },
18981 )),
18982 ..lsp::CompletionItem::default()
18983 };
18984 let items = iter::once(item_0.clone())
18985 .chain((11..51).map(|i| lsp::CompletionItem {
18986 label: format!("item_{}", i),
18987 insert_text: Some(format!("item_{}", i)),
18988 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18989 ..lsp::CompletionItem::default()
18990 }))
18991 .collect::<Vec<_>>();
18992
18993 let default_commit_characters = vec!["?".to_string()];
18994 let default_data = json!({ "default": "data"});
18995 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18996 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18997 let default_edit_range = lsp::Range {
18998 start: lsp::Position {
18999 line: 0,
19000 character: 5,
19001 },
19002 end: lsp::Position {
19003 line: 0,
19004 character: 5,
19005 },
19006 };
19007
19008 let mut cx = EditorLspTestContext::new_rust(
19009 lsp::ServerCapabilities {
19010 completion_provider: Some(lsp::CompletionOptions {
19011 trigger_characters: Some(vec![".".to_string()]),
19012 resolve_provider: Some(true),
19013 ..Default::default()
19014 }),
19015 ..Default::default()
19016 },
19017 cx,
19018 )
19019 .await;
19020
19021 cx.set_state("fn main() { let a = 2ˇ; }");
19022 cx.simulate_keystroke(".");
19023
19024 let completion_data = default_data.clone();
19025 let completion_characters = default_commit_characters.clone();
19026 let completion_items = items.clone();
19027 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19028 let default_data = completion_data.clone();
19029 let default_commit_characters = completion_characters.clone();
19030 let items = completion_items.clone();
19031 async move {
19032 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19033 items,
19034 item_defaults: Some(lsp::CompletionListItemDefaults {
19035 data: Some(default_data.clone()),
19036 commit_characters: Some(default_commit_characters.clone()),
19037 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19038 default_edit_range,
19039 )),
19040 insert_text_format: Some(default_insert_text_format),
19041 insert_text_mode: Some(default_insert_text_mode),
19042 }),
19043 ..lsp::CompletionList::default()
19044 })))
19045 }
19046 })
19047 .next()
19048 .await;
19049
19050 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19051 cx.lsp
19052 .server
19053 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19054 let closure_resolved_items = resolved_items.clone();
19055 move |item_to_resolve, _| {
19056 let closure_resolved_items = closure_resolved_items.clone();
19057 async move {
19058 closure_resolved_items.lock().push(item_to_resolve.clone());
19059 Ok(item_to_resolve)
19060 }
19061 }
19062 })
19063 .detach();
19064
19065 cx.condition(|editor, _| editor.context_menu_visible())
19066 .await;
19067 cx.run_until_parked();
19068 cx.update_editor(|editor, _, _| {
19069 let menu = editor.context_menu.borrow_mut();
19070 match menu.as_ref().expect("should have the completions menu") {
19071 CodeContextMenu::Completions(completions_menu) => {
19072 assert_eq!(
19073 completions_menu
19074 .entries
19075 .borrow()
19076 .iter()
19077 .map(|mat| mat.string.clone())
19078 .collect::<Vec<String>>(),
19079 items
19080 .iter()
19081 .map(|completion| completion.label.clone())
19082 .collect::<Vec<String>>()
19083 );
19084 }
19085 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19086 }
19087 });
19088 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19089 // with 4 from the end.
19090 assert_eq!(
19091 *resolved_items.lock(),
19092 [&items[0..16], &items[items.len() - 4..items.len()]]
19093 .concat()
19094 .iter()
19095 .cloned()
19096 .map(|mut item| {
19097 if item.data.is_none() {
19098 item.data = Some(default_data.clone());
19099 }
19100 item
19101 })
19102 .collect::<Vec<lsp::CompletionItem>>(),
19103 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19104 );
19105 resolved_items.lock().clear();
19106
19107 cx.update_editor(|editor, window, cx| {
19108 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19109 });
19110 cx.run_until_parked();
19111 // Completions that have already been resolved are skipped.
19112 assert_eq!(
19113 *resolved_items.lock(),
19114 items[items.len() - 17..items.len() - 4]
19115 .iter()
19116 .cloned()
19117 .map(|mut item| {
19118 if item.data.is_none() {
19119 item.data = Some(default_data.clone());
19120 }
19121 item
19122 })
19123 .collect::<Vec<lsp::CompletionItem>>()
19124 );
19125 resolved_items.lock().clear();
19126}
19127
19128#[gpui::test]
19129async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19130 init_test(cx, |_| {});
19131
19132 let mut cx = EditorLspTestContext::new(
19133 Language::new(
19134 LanguageConfig {
19135 matcher: LanguageMatcher {
19136 path_suffixes: vec!["jsx".into()],
19137 ..Default::default()
19138 },
19139 overrides: [(
19140 "element".into(),
19141 LanguageConfigOverride {
19142 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19143 ..Default::default()
19144 },
19145 )]
19146 .into_iter()
19147 .collect(),
19148 ..Default::default()
19149 },
19150 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19151 )
19152 .with_override_query("(jsx_self_closing_element) @element")
19153 .unwrap(),
19154 lsp::ServerCapabilities {
19155 completion_provider: Some(lsp::CompletionOptions {
19156 trigger_characters: Some(vec![":".to_string()]),
19157 ..Default::default()
19158 }),
19159 ..Default::default()
19160 },
19161 cx,
19162 )
19163 .await;
19164
19165 cx.lsp
19166 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19167 Ok(Some(lsp::CompletionResponse::Array(vec![
19168 lsp::CompletionItem {
19169 label: "bg-blue".into(),
19170 ..Default::default()
19171 },
19172 lsp::CompletionItem {
19173 label: "bg-red".into(),
19174 ..Default::default()
19175 },
19176 lsp::CompletionItem {
19177 label: "bg-yellow".into(),
19178 ..Default::default()
19179 },
19180 ])))
19181 });
19182
19183 cx.set_state(r#"<p class="bgˇ" />"#);
19184
19185 // Trigger completion when typing a dash, because the dash is an extra
19186 // word character in the 'element' scope, which contains the cursor.
19187 cx.simulate_keystroke("-");
19188 cx.executor().run_until_parked();
19189 cx.update_editor(|editor, _, _| {
19190 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19191 {
19192 assert_eq!(
19193 completion_menu_entries(menu),
19194 &["bg-blue", "bg-red", "bg-yellow"]
19195 );
19196 } else {
19197 panic!("expected completion menu to be open");
19198 }
19199 });
19200
19201 cx.simulate_keystroke("l");
19202 cx.executor().run_until_parked();
19203 cx.update_editor(|editor, _, _| {
19204 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19205 {
19206 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19207 } else {
19208 panic!("expected completion menu to be open");
19209 }
19210 });
19211
19212 // When filtering completions, consider the character after the '-' to
19213 // be the start of a subword.
19214 cx.set_state(r#"<p class="yelˇ" />"#);
19215 cx.simulate_keystroke("l");
19216 cx.executor().run_until_parked();
19217 cx.update_editor(|editor, _, _| {
19218 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19219 {
19220 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19221 } else {
19222 panic!("expected completion menu to be open");
19223 }
19224 });
19225}
19226
19227fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19228 let entries = menu.entries.borrow();
19229 entries.iter().map(|mat| mat.string.clone()).collect()
19230}
19231
19232#[gpui::test]
19233async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19234 init_test(cx, |settings| {
19235 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19236 });
19237
19238 let fs = FakeFs::new(cx.executor());
19239 fs.insert_file(path!("/file.ts"), Default::default()).await;
19240
19241 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19242 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19243
19244 language_registry.add(Arc::new(Language::new(
19245 LanguageConfig {
19246 name: "TypeScript".into(),
19247 matcher: LanguageMatcher {
19248 path_suffixes: vec!["ts".to_string()],
19249 ..Default::default()
19250 },
19251 ..Default::default()
19252 },
19253 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19254 )));
19255 update_test_language_settings(cx, |settings| {
19256 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19257 });
19258
19259 let test_plugin = "test_plugin";
19260 let _ = language_registry.register_fake_lsp(
19261 "TypeScript",
19262 FakeLspAdapter {
19263 prettier_plugins: vec![test_plugin],
19264 ..Default::default()
19265 },
19266 );
19267
19268 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19269 let buffer = project
19270 .update(cx, |project, cx| {
19271 project.open_local_buffer(path!("/file.ts"), cx)
19272 })
19273 .await
19274 .unwrap();
19275
19276 let buffer_text = "one\ntwo\nthree\n";
19277 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19278 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19279 editor.update_in(cx, |editor, window, cx| {
19280 editor.set_text(buffer_text, window, cx)
19281 });
19282
19283 editor
19284 .update_in(cx, |editor, window, cx| {
19285 editor.perform_format(
19286 project.clone(),
19287 FormatTrigger::Manual,
19288 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19289 window,
19290 cx,
19291 )
19292 })
19293 .unwrap()
19294 .await;
19295 assert_eq!(
19296 editor.update(cx, |editor, cx| editor.text(cx)),
19297 buffer_text.to_string() + prettier_format_suffix,
19298 "Test prettier formatting was not applied to the original buffer text",
19299 );
19300
19301 update_test_language_settings(cx, |settings| {
19302 settings.defaults.formatter = Some(FormatterList::default())
19303 });
19304 let format = editor.update_in(cx, |editor, window, cx| {
19305 editor.perform_format(
19306 project.clone(),
19307 FormatTrigger::Manual,
19308 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19309 window,
19310 cx,
19311 )
19312 });
19313 format.await.unwrap();
19314 assert_eq!(
19315 editor.update(cx, |editor, cx| editor.text(cx)),
19316 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19317 "Autoformatting (via test prettier) was not applied to the original buffer text",
19318 );
19319}
19320
19321#[gpui::test]
19322async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19323 init_test(cx, |settings| {
19324 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19325 });
19326
19327 let fs = FakeFs::new(cx.executor());
19328 fs.insert_file(path!("/file.settings"), Default::default())
19329 .await;
19330
19331 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19332 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19333
19334 let ts_lang = Arc::new(Language::new(
19335 LanguageConfig {
19336 name: "TypeScript".into(),
19337 matcher: LanguageMatcher {
19338 path_suffixes: vec!["ts".to_string()],
19339 ..LanguageMatcher::default()
19340 },
19341 prettier_parser_name: Some("typescript".to_string()),
19342 ..LanguageConfig::default()
19343 },
19344 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19345 ));
19346
19347 language_registry.add(ts_lang.clone());
19348
19349 update_test_language_settings(cx, |settings| {
19350 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19351 });
19352
19353 let test_plugin = "test_plugin";
19354 let _ = language_registry.register_fake_lsp(
19355 "TypeScript",
19356 FakeLspAdapter {
19357 prettier_plugins: vec![test_plugin],
19358 ..Default::default()
19359 },
19360 );
19361
19362 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19363 let buffer = project
19364 .update(cx, |project, cx| {
19365 project.open_local_buffer(path!("/file.settings"), cx)
19366 })
19367 .await
19368 .unwrap();
19369
19370 project.update(cx, |project, cx| {
19371 project.set_language_for_buffer(&buffer, ts_lang, cx)
19372 });
19373
19374 let buffer_text = "one\ntwo\nthree\n";
19375 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19376 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19377 editor.update_in(cx, |editor, window, cx| {
19378 editor.set_text(buffer_text, window, cx)
19379 });
19380
19381 editor
19382 .update_in(cx, |editor, window, cx| {
19383 editor.perform_format(
19384 project.clone(),
19385 FormatTrigger::Manual,
19386 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19387 window,
19388 cx,
19389 )
19390 })
19391 .unwrap()
19392 .await;
19393 assert_eq!(
19394 editor.update(cx, |editor, cx| editor.text(cx)),
19395 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19396 "Test prettier formatting was not applied to the original buffer text",
19397 );
19398
19399 update_test_language_settings(cx, |settings| {
19400 settings.defaults.formatter = Some(FormatterList::default())
19401 });
19402 let format = editor.update_in(cx, |editor, window, cx| {
19403 editor.perform_format(
19404 project.clone(),
19405 FormatTrigger::Manual,
19406 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19407 window,
19408 cx,
19409 )
19410 });
19411 format.await.unwrap();
19412
19413 assert_eq!(
19414 editor.update(cx, |editor, cx| editor.text(cx)),
19415 buffer_text.to_string()
19416 + prettier_format_suffix
19417 + "\ntypescript\n"
19418 + prettier_format_suffix
19419 + "\ntypescript",
19420 "Autoformatting (via test prettier) was not applied to the original buffer text",
19421 );
19422}
19423
19424#[gpui::test]
19425async fn test_addition_reverts(cx: &mut TestAppContext) {
19426 init_test(cx, |_| {});
19427 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19428 let base_text = indoc! {r#"
19429 struct Row;
19430 struct Row1;
19431 struct Row2;
19432
19433 struct Row4;
19434 struct Row5;
19435 struct Row6;
19436
19437 struct Row8;
19438 struct Row9;
19439 struct Row10;"#};
19440
19441 // When addition hunks are not adjacent to carets, no hunk revert is performed
19442 assert_hunk_revert(
19443 indoc! {r#"struct Row;
19444 struct Row1;
19445 struct Row1.1;
19446 struct Row1.2;
19447 struct Row2;ˇ
19448
19449 struct Row4;
19450 struct Row5;
19451 struct Row6;
19452
19453 struct Row8;
19454 ˇstruct Row9;
19455 struct Row9.1;
19456 struct Row9.2;
19457 struct Row9.3;
19458 struct Row10;"#},
19459 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19460 indoc! {r#"struct Row;
19461 struct Row1;
19462 struct Row1.1;
19463 struct Row1.2;
19464 struct Row2;ˇ
19465
19466 struct Row4;
19467 struct Row5;
19468 struct Row6;
19469
19470 struct Row8;
19471 ˇstruct Row9;
19472 struct Row9.1;
19473 struct Row9.2;
19474 struct Row9.3;
19475 struct Row10;"#},
19476 base_text,
19477 &mut cx,
19478 );
19479 // Same for selections
19480 assert_hunk_revert(
19481 indoc! {r#"struct Row;
19482 struct Row1;
19483 struct Row2;
19484 struct Row2.1;
19485 struct Row2.2;
19486 «ˇ
19487 struct Row4;
19488 struct» Row5;
19489 «struct Row6;
19490 ˇ»
19491 struct Row9.1;
19492 struct Row9.2;
19493 struct Row9.3;
19494 struct Row8;
19495 struct Row9;
19496 struct Row10;"#},
19497 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19498 indoc! {r#"struct Row;
19499 struct Row1;
19500 struct Row2;
19501 struct Row2.1;
19502 struct Row2.2;
19503 «ˇ
19504 struct Row4;
19505 struct» Row5;
19506 «struct Row6;
19507 ˇ»
19508 struct Row9.1;
19509 struct Row9.2;
19510 struct Row9.3;
19511 struct Row8;
19512 struct Row9;
19513 struct Row10;"#},
19514 base_text,
19515 &mut cx,
19516 );
19517
19518 // When carets and selections intersect the addition hunks, those are reverted.
19519 // Adjacent carets got merged.
19520 assert_hunk_revert(
19521 indoc! {r#"struct Row;
19522 ˇ// something on the top
19523 struct Row1;
19524 struct Row2;
19525 struct Roˇw3.1;
19526 struct Row2.2;
19527 struct Row2.3;ˇ
19528
19529 struct Row4;
19530 struct ˇRow5.1;
19531 struct Row5.2;
19532 struct «Rowˇ»5.3;
19533 struct Row5;
19534 struct Row6;
19535 ˇ
19536 struct Row9.1;
19537 struct «Rowˇ»9.2;
19538 struct «ˇRow»9.3;
19539 struct Row8;
19540 struct Row9;
19541 «ˇ// something on bottom»
19542 struct Row10;"#},
19543 vec![
19544 DiffHunkStatusKind::Added,
19545 DiffHunkStatusKind::Added,
19546 DiffHunkStatusKind::Added,
19547 DiffHunkStatusKind::Added,
19548 DiffHunkStatusKind::Added,
19549 ],
19550 indoc! {r#"struct Row;
19551 ˇstruct Row1;
19552 struct Row2;
19553 ˇ
19554 struct Row4;
19555 ˇstruct Row5;
19556 struct Row6;
19557 ˇ
19558 ˇstruct Row8;
19559 struct Row9;
19560 ˇstruct Row10;"#},
19561 base_text,
19562 &mut cx,
19563 );
19564}
19565
19566#[gpui::test]
19567async fn test_modification_reverts(cx: &mut TestAppContext) {
19568 init_test(cx, |_| {});
19569 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19570 let base_text = indoc! {r#"
19571 struct Row;
19572 struct Row1;
19573 struct Row2;
19574
19575 struct Row4;
19576 struct Row5;
19577 struct Row6;
19578
19579 struct Row8;
19580 struct Row9;
19581 struct Row10;"#};
19582
19583 // Modification hunks behave the same as the addition ones.
19584 assert_hunk_revert(
19585 indoc! {r#"struct Row;
19586 struct Row1;
19587 struct Row33;
19588 ˇ
19589 struct Row4;
19590 struct Row5;
19591 struct Row6;
19592 ˇ
19593 struct Row99;
19594 struct Row9;
19595 struct Row10;"#},
19596 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19597 indoc! {r#"struct Row;
19598 struct Row1;
19599 struct Row33;
19600 ˇ
19601 struct Row4;
19602 struct Row5;
19603 struct Row6;
19604 ˇ
19605 struct Row99;
19606 struct Row9;
19607 struct Row10;"#},
19608 base_text,
19609 &mut cx,
19610 );
19611 assert_hunk_revert(
19612 indoc! {r#"struct Row;
19613 struct Row1;
19614 struct Row33;
19615 «ˇ
19616 struct Row4;
19617 struct» Row5;
19618 «struct Row6;
19619 ˇ»
19620 struct Row99;
19621 struct Row9;
19622 struct Row10;"#},
19623 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19624 indoc! {r#"struct Row;
19625 struct Row1;
19626 struct Row33;
19627 «ˇ
19628 struct Row4;
19629 struct» Row5;
19630 «struct Row6;
19631 ˇ»
19632 struct Row99;
19633 struct Row9;
19634 struct Row10;"#},
19635 base_text,
19636 &mut cx,
19637 );
19638
19639 assert_hunk_revert(
19640 indoc! {r#"ˇstruct Row1.1;
19641 struct Row1;
19642 «ˇstr»uct Row22;
19643
19644 struct ˇRow44;
19645 struct Row5;
19646 struct «Rˇ»ow66;ˇ
19647
19648 «struˇ»ct Row88;
19649 struct Row9;
19650 struct Row1011;ˇ"#},
19651 vec![
19652 DiffHunkStatusKind::Modified,
19653 DiffHunkStatusKind::Modified,
19654 DiffHunkStatusKind::Modified,
19655 DiffHunkStatusKind::Modified,
19656 DiffHunkStatusKind::Modified,
19657 DiffHunkStatusKind::Modified,
19658 ],
19659 indoc! {r#"struct Row;
19660 ˇstruct Row1;
19661 struct Row2;
19662 ˇ
19663 struct Row4;
19664 ˇstruct Row5;
19665 struct Row6;
19666 ˇ
19667 struct Row8;
19668 ˇstruct Row9;
19669 struct Row10;ˇ"#},
19670 base_text,
19671 &mut cx,
19672 );
19673}
19674
19675#[gpui::test]
19676async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19677 init_test(cx, |_| {});
19678 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19679 let base_text = indoc! {r#"
19680 one
19681
19682 two
19683 three
19684 "#};
19685
19686 cx.set_head_text(base_text);
19687 cx.set_state("\nˇ\n");
19688 cx.executor().run_until_parked();
19689 cx.update_editor(|editor, _window, cx| {
19690 editor.expand_selected_diff_hunks(cx);
19691 });
19692 cx.executor().run_until_parked();
19693 cx.update_editor(|editor, window, cx| {
19694 editor.backspace(&Default::default(), window, cx);
19695 });
19696 cx.run_until_parked();
19697 cx.assert_state_with_diff(
19698 indoc! {r#"
19699
19700 - two
19701 - threeˇ
19702 +
19703 "#}
19704 .to_string(),
19705 );
19706}
19707
19708#[gpui::test]
19709async fn test_deletion_reverts(cx: &mut TestAppContext) {
19710 init_test(cx, |_| {});
19711 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19712 let base_text = indoc! {r#"struct Row;
19713struct Row1;
19714struct Row2;
19715
19716struct Row4;
19717struct Row5;
19718struct Row6;
19719
19720struct Row8;
19721struct Row9;
19722struct Row10;"#};
19723
19724 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19725 assert_hunk_revert(
19726 indoc! {r#"struct Row;
19727 struct Row2;
19728
19729 ˇstruct Row4;
19730 struct Row5;
19731 struct Row6;
19732 ˇ
19733 struct Row8;
19734 struct Row10;"#},
19735 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19736 indoc! {r#"struct Row;
19737 struct Row2;
19738
19739 ˇstruct Row4;
19740 struct Row5;
19741 struct Row6;
19742 ˇ
19743 struct Row8;
19744 struct Row10;"#},
19745 base_text,
19746 &mut cx,
19747 );
19748 assert_hunk_revert(
19749 indoc! {r#"struct Row;
19750 struct Row2;
19751
19752 «ˇstruct Row4;
19753 struct» Row5;
19754 «struct Row6;
19755 ˇ»
19756 struct Row8;
19757 struct Row10;"#},
19758 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19759 indoc! {r#"struct Row;
19760 struct Row2;
19761
19762 «ˇstruct Row4;
19763 struct» Row5;
19764 «struct Row6;
19765 ˇ»
19766 struct Row8;
19767 struct Row10;"#},
19768 base_text,
19769 &mut cx,
19770 );
19771
19772 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19773 assert_hunk_revert(
19774 indoc! {r#"struct Row;
19775 ˇstruct Row2;
19776
19777 struct Row4;
19778 struct Row5;
19779 struct Row6;
19780
19781 struct Row8;ˇ
19782 struct Row10;"#},
19783 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19784 indoc! {r#"struct Row;
19785 struct Row1;
19786 ˇstruct Row2;
19787
19788 struct Row4;
19789 struct Row5;
19790 struct Row6;
19791
19792 struct Row8;ˇ
19793 struct Row9;
19794 struct Row10;"#},
19795 base_text,
19796 &mut cx,
19797 );
19798 assert_hunk_revert(
19799 indoc! {r#"struct Row;
19800 struct Row2«ˇ;
19801 struct Row4;
19802 struct» Row5;
19803 «struct Row6;
19804
19805 struct Row8;ˇ»
19806 struct Row10;"#},
19807 vec![
19808 DiffHunkStatusKind::Deleted,
19809 DiffHunkStatusKind::Deleted,
19810 DiffHunkStatusKind::Deleted,
19811 ],
19812 indoc! {r#"struct Row;
19813 struct Row1;
19814 struct Row2«ˇ;
19815
19816 struct Row4;
19817 struct» Row5;
19818 «struct Row6;
19819
19820 struct Row8;ˇ»
19821 struct Row9;
19822 struct Row10;"#},
19823 base_text,
19824 &mut cx,
19825 );
19826}
19827
19828#[gpui::test]
19829async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19830 init_test(cx, |_| {});
19831
19832 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19833 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19834 let base_text_3 =
19835 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19836
19837 let text_1 = edit_first_char_of_every_line(base_text_1);
19838 let text_2 = edit_first_char_of_every_line(base_text_2);
19839 let text_3 = edit_first_char_of_every_line(base_text_3);
19840
19841 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19842 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19843 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19844
19845 let multibuffer = cx.new(|cx| {
19846 let mut multibuffer = MultiBuffer::new(ReadWrite);
19847 multibuffer.push_excerpts(
19848 buffer_1.clone(),
19849 [
19850 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19851 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19852 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19853 ],
19854 cx,
19855 );
19856 multibuffer.push_excerpts(
19857 buffer_2.clone(),
19858 [
19859 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19860 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19861 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19862 ],
19863 cx,
19864 );
19865 multibuffer.push_excerpts(
19866 buffer_3.clone(),
19867 [
19868 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19869 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19870 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19871 ],
19872 cx,
19873 );
19874 multibuffer
19875 });
19876
19877 let fs = FakeFs::new(cx.executor());
19878 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19879 let (editor, cx) = cx
19880 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19881 editor.update_in(cx, |editor, _window, cx| {
19882 for (buffer, diff_base) in [
19883 (buffer_1.clone(), base_text_1),
19884 (buffer_2.clone(), base_text_2),
19885 (buffer_3.clone(), base_text_3),
19886 ] {
19887 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19888 editor
19889 .buffer
19890 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19891 }
19892 });
19893 cx.executor().run_until_parked();
19894
19895 editor.update_in(cx, |editor, window, cx| {
19896 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}");
19897 editor.select_all(&SelectAll, window, cx);
19898 editor.git_restore(&Default::default(), window, cx);
19899 });
19900 cx.executor().run_until_parked();
19901
19902 // When all ranges are selected, all buffer hunks are reverted.
19903 editor.update(cx, |editor, cx| {
19904 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");
19905 });
19906 buffer_1.update(cx, |buffer, _| {
19907 assert_eq!(buffer.text(), base_text_1);
19908 });
19909 buffer_2.update(cx, |buffer, _| {
19910 assert_eq!(buffer.text(), base_text_2);
19911 });
19912 buffer_3.update(cx, |buffer, _| {
19913 assert_eq!(buffer.text(), base_text_3);
19914 });
19915
19916 editor.update_in(cx, |editor, window, cx| {
19917 editor.undo(&Default::default(), window, cx);
19918 });
19919
19920 editor.update_in(cx, |editor, window, cx| {
19921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19922 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19923 });
19924 editor.git_restore(&Default::default(), window, cx);
19925 });
19926
19927 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19928 // but not affect buffer_2 and its related excerpts.
19929 editor.update(cx, |editor, cx| {
19930 assert_eq!(
19931 editor.text(cx),
19932 "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}"
19933 );
19934 });
19935 buffer_1.update(cx, |buffer, _| {
19936 assert_eq!(buffer.text(), base_text_1);
19937 });
19938 buffer_2.update(cx, |buffer, _| {
19939 assert_eq!(
19940 buffer.text(),
19941 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19942 );
19943 });
19944 buffer_3.update(cx, |buffer, _| {
19945 assert_eq!(
19946 buffer.text(),
19947 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19948 );
19949 });
19950
19951 fn edit_first_char_of_every_line(text: &str) -> String {
19952 text.split('\n')
19953 .map(|line| format!("X{}", &line[1..]))
19954 .collect::<Vec<_>>()
19955 .join("\n")
19956 }
19957}
19958
19959#[gpui::test]
19960async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19961 init_test(cx, |_| {});
19962
19963 let cols = 4;
19964 let rows = 10;
19965 let sample_text_1 = sample_text(rows, cols, 'a');
19966 assert_eq!(
19967 sample_text_1,
19968 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19969 );
19970 let sample_text_2 = sample_text(rows, cols, 'l');
19971 assert_eq!(
19972 sample_text_2,
19973 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19974 );
19975 let sample_text_3 = sample_text(rows, cols, 'v');
19976 assert_eq!(
19977 sample_text_3,
19978 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19979 );
19980
19981 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19982 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19983 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19984
19985 let multi_buffer = cx.new(|cx| {
19986 let mut multibuffer = MultiBuffer::new(ReadWrite);
19987 multibuffer.push_excerpts(
19988 buffer_1.clone(),
19989 [
19990 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19991 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19992 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19993 ],
19994 cx,
19995 );
19996 multibuffer.push_excerpts(
19997 buffer_2.clone(),
19998 [
19999 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20000 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20001 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20002 ],
20003 cx,
20004 );
20005 multibuffer.push_excerpts(
20006 buffer_3.clone(),
20007 [
20008 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20009 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20010 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20011 ],
20012 cx,
20013 );
20014 multibuffer
20015 });
20016
20017 let fs = FakeFs::new(cx.executor());
20018 fs.insert_tree(
20019 "/a",
20020 json!({
20021 "main.rs": sample_text_1,
20022 "other.rs": sample_text_2,
20023 "lib.rs": sample_text_3,
20024 }),
20025 )
20026 .await;
20027 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20028 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20029 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20030 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20031 Editor::new(
20032 EditorMode::full(),
20033 multi_buffer,
20034 Some(project.clone()),
20035 window,
20036 cx,
20037 )
20038 });
20039 let multibuffer_item_id = workspace
20040 .update(cx, |workspace, window, cx| {
20041 assert!(
20042 workspace.active_item(cx).is_none(),
20043 "active item should be None before the first item is added"
20044 );
20045 workspace.add_item_to_active_pane(
20046 Box::new(multi_buffer_editor.clone()),
20047 None,
20048 true,
20049 window,
20050 cx,
20051 );
20052 let active_item = workspace
20053 .active_item(cx)
20054 .expect("should have an active item after adding the multi buffer");
20055 assert_eq!(
20056 active_item.buffer_kind(cx),
20057 ItemBufferKind::Multibuffer,
20058 "A multi buffer was expected to active after adding"
20059 );
20060 active_item.item_id()
20061 })
20062 .unwrap();
20063 cx.executor().run_until_parked();
20064
20065 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20066 editor.change_selections(
20067 SelectionEffects::scroll(Autoscroll::Next),
20068 window,
20069 cx,
20070 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20071 );
20072 editor.open_excerpts(&OpenExcerpts, window, cx);
20073 });
20074 cx.executor().run_until_parked();
20075 let first_item_id = workspace
20076 .update(cx, |workspace, window, cx| {
20077 let active_item = workspace
20078 .active_item(cx)
20079 .expect("should have an active item after navigating into the 1st buffer");
20080 let first_item_id = active_item.item_id();
20081 assert_ne!(
20082 first_item_id, multibuffer_item_id,
20083 "Should navigate into the 1st buffer and activate it"
20084 );
20085 assert_eq!(
20086 active_item.buffer_kind(cx),
20087 ItemBufferKind::Singleton,
20088 "New active item should be a singleton buffer"
20089 );
20090 assert_eq!(
20091 active_item
20092 .act_as::<Editor>(cx)
20093 .expect("should have navigated into an editor for the 1st buffer")
20094 .read(cx)
20095 .text(cx),
20096 sample_text_1
20097 );
20098
20099 workspace
20100 .go_back(workspace.active_pane().downgrade(), window, cx)
20101 .detach_and_log_err(cx);
20102
20103 first_item_id
20104 })
20105 .unwrap();
20106 cx.executor().run_until_parked();
20107 workspace
20108 .update(cx, |workspace, _, cx| {
20109 let active_item = workspace
20110 .active_item(cx)
20111 .expect("should have an active item after navigating back");
20112 assert_eq!(
20113 active_item.item_id(),
20114 multibuffer_item_id,
20115 "Should navigate back to the multi buffer"
20116 );
20117 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20118 })
20119 .unwrap();
20120
20121 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20122 editor.change_selections(
20123 SelectionEffects::scroll(Autoscroll::Next),
20124 window,
20125 cx,
20126 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20127 );
20128 editor.open_excerpts(&OpenExcerpts, window, cx);
20129 });
20130 cx.executor().run_until_parked();
20131 let second_item_id = workspace
20132 .update(cx, |workspace, window, cx| {
20133 let active_item = workspace
20134 .active_item(cx)
20135 .expect("should have an active item after navigating into the 2nd buffer");
20136 let second_item_id = active_item.item_id();
20137 assert_ne!(
20138 second_item_id, multibuffer_item_id,
20139 "Should navigate away from the multibuffer"
20140 );
20141 assert_ne!(
20142 second_item_id, first_item_id,
20143 "Should navigate into the 2nd buffer and activate it"
20144 );
20145 assert_eq!(
20146 active_item.buffer_kind(cx),
20147 ItemBufferKind::Singleton,
20148 "New active item should be a singleton buffer"
20149 );
20150 assert_eq!(
20151 active_item
20152 .act_as::<Editor>(cx)
20153 .expect("should have navigated into an editor")
20154 .read(cx)
20155 .text(cx),
20156 sample_text_2
20157 );
20158
20159 workspace
20160 .go_back(workspace.active_pane().downgrade(), window, cx)
20161 .detach_and_log_err(cx);
20162
20163 second_item_id
20164 })
20165 .unwrap();
20166 cx.executor().run_until_parked();
20167 workspace
20168 .update(cx, |workspace, _, cx| {
20169 let active_item = workspace
20170 .active_item(cx)
20171 .expect("should have an active item after navigating back from the 2nd buffer");
20172 assert_eq!(
20173 active_item.item_id(),
20174 multibuffer_item_id,
20175 "Should navigate back from the 2nd buffer to the multi buffer"
20176 );
20177 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20178 })
20179 .unwrap();
20180
20181 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20182 editor.change_selections(
20183 SelectionEffects::scroll(Autoscroll::Next),
20184 window,
20185 cx,
20186 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20187 );
20188 editor.open_excerpts(&OpenExcerpts, window, cx);
20189 });
20190 cx.executor().run_until_parked();
20191 workspace
20192 .update(cx, |workspace, window, cx| {
20193 let active_item = workspace
20194 .active_item(cx)
20195 .expect("should have an active item after navigating into the 3rd buffer");
20196 let third_item_id = active_item.item_id();
20197 assert_ne!(
20198 third_item_id, multibuffer_item_id,
20199 "Should navigate into the 3rd buffer and activate it"
20200 );
20201 assert_ne!(third_item_id, first_item_id);
20202 assert_ne!(third_item_id, second_item_id);
20203 assert_eq!(
20204 active_item.buffer_kind(cx),
20205 ItemBufferKind::Singleton,
20206 "New active item should be a singleton buffer"
20207 );
20208 assert_eq!(
20209 active_item
20210 .act_as::<Editor>(cx)
20211 .expect("should have navigated into an editor")
20212 .read(cx)
20213 .text(cx),
20214 sample_text_3
20215 );
20216
20217 workspace
20218 .go_back(workspace.active_pane().downgrade(), window, cx)
20219 .detach_and_log_err(cx);
20220 })
20221 .unwrap();
20222 cx.executor().run_until_parked();
20223 workspace
20224 .update(cx, |workspace, _, cx| {
20225 let active_item = workspace
20226 .active_item(cx)
20227 .expect("should have an active item after navigating back from the 3rd buffer");
20228 assert_eq!(
20229 active_item.item_id(),
20230 multibuffer_item_id,
20231 "Should navigate back from the 3rd buffer to the multi buffer"
20232 );
20233 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20234 })
20235 .unwrap();
20236}
20237
20238#[gpui::test]
20239async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20240 init_test(cx, |_| {});
20241
20242 let mut cx = EditorTestContext::new(cx).await;
20243
20244 let diff_base = r#"
20245 use some::mod;
20246
20247 const A: u32 = 42;
20248
20249 fn main() {
20250 println!("hello");
20251
20252 println!("world");
20253 }
20254 "#
20255 .unindent();
20256
20257 cx.set_state(
20258 &r#"
20259 use some::modified;
20260
20261 ˇ
20262 fn main() {
20263 println!("hello there");
20264
20265 println!("around the");
20266 println!("world");
20267 }
20268 "#
20269 .unindent(),
20270 );
20271
20272 cx.set_head_text(&diff_base);
20273 executor.run_until_parked();
20274
20275 cx.update_editor(|editor, window, cx| {
20276 editor.go_to_next_hunk(&GoToHunk, window, cx);
20277 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20278 });
20279 executor.run_until_parked();
20280 cx.assert_state_with_diff(
20281 r#"
20282 use some::modified;
20283
20284
20285 fn main() {
20286 - println!("hello");
20287 + ˇ println!("hello there");
20288
20289 println!("around the");
20290 println!("world");
20291 }
20292 "#
20293 .unindent(),
20294 );
20295
20296 cx.update_editor(|editor, window, cx| {
20297 for _ in 0..2 {
20298 editor.go_to_next_hunk(&GoToHunk, window, cx);
20299 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20300 }
20301 });
20302 executor.run_until_parked();
20303 cx.assert_state_with_diff(
20304 r#"
20305 - use some::mod;
20306 + ˇuse some::modified;
20307
20308
20309 fn main() {
20310 - println!("hello");
20311 + println!("hello there");
20312
20313 + println!("around the");
20314 println!("world");
20315 }
20316 "#
20317 .unindent(),
20318 );
20319
20320 cx.update_editor(|editor, window, cx| {
20321 editor.go_to_next_hunk(&GoToHunk, window, cx);
20322 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20323 });
20324 executor.run_until_parked();
20325 cx.assert_state_with_diff(
20326 r#"
20327 - use some::mod;
20328 + use some::modified;
20329
20330 - const A: u32 = 42;
20331 ˇ
20332 fn main() {
20333 - println!("hello");
20334 + println!("hello there");
20335
20336 + println!("around the");
20337 println!("world");
20338 }
20339 "#
20340 .unindent(),
20341 );
20342
20343 cx.update_editor(|editor, window, cx| {
20344 editor.cancel(&Cancel, window, cx);
20345 });
20346
20347 cx.assert_state_with_diff(
20348 r#"
20349 use some::modified;
20350
20351 ˇ
20352 fn main() {
20353 println!("hello there");
20354
20355 println!("around the");
20356 println!("world");
20357 }
20358 "#
20359 .unindent(),
20360 );
20361}
20362
20363#[gpui::test]
20364async fn test_diff_base_change_with_expanded_diff_hunks(
20365 executor: BackgroundExecutor,
20366 cx: &mut TestAppContext,
20367) {
20368 init_test(cx, |_| {});
20369
20370 let mut cx = EditorTestContext::new(cx).await;
20371
20372 let diff_base = r#"
20373 use some::mod1;
20374 use some::mod2;
20375
20376 const A: u32 = 42;
20377 const B: u32 = 42;
20378 const C: u32 = 42;
20379
20380 fn main() {
20381 println!("hello");
20382
20383 println!("world");
20384 }
20385 "#
20386 .unindent();
20387
20388 cx.set_state(
20389 &r#"
20390 use some::mod2;
20391
20392 const A: u32 = 42;
20393 const C: u32 = 42;
20394
20395 fn main(ˇ) {
20396 //println!("hello");
20397
20398 println!("world");
20399 //
20400 //
20401 }
20402 "#
20403 .unindent(),
20404 );
20405
20406 cx.set_head_text(&diff_base);
20407 executor.run_until_parked();
20408
20409 cx.update_editor(|editor, window, cx| {
20410 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20411 });
20412 executor.run_until_parked();
20413 cx.assert_state_with_diff(
20414 r#"
20415 - use some::mod1;
20416 use some::mod2;
20417
20418 const A: u32 = 42;
20419 - const B: u32 = 42;
20420 const C: u32 = 42;
20421
20422 fn main(ˇ) {
20423 - println!("hello");
20424 + //println!("hello");
20425
20426 println!("world");
20427 + //
20428 + //
20429 }
20430 "#
20431 .unindent(),
20432 );
20433
20434 cx.set_head_text("new diff base!");
20435 executor.run_until_parked();
20436 cx.assert_state_with_diff(
20437 r#"
20438 - new diff base!
20439 + use some::mod2;
20440 +
20441 + const A: u32 = 42;
20442 + const C: u32 = 42;
20443 +
20444 + fn main(ˇ) {
20445 + //println!("hello");
20446 +
20447 + println!("world");
20448 + //
20449 + //
20450 + }
20451 "#
20452 .unindent(),
20453 );
20454}
20455
20456#[gpui::test]
20457async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20458 init_test(cx, |_| {});
20459
20460 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20461 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20462 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20463 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20464 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20465 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20466
20467 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20468 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20469 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20470
20471 let multi_buffer = cx.new(|cx| {
20472 let mut multibuffer = MultiBuffer::new(ReadWrite);
20473 multibuffer.push_excerpts(
20474 buffer_1.clone(),
20475 [
20476 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20477 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20478 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20479 ],
20480 cx,
20481 );
20482 multibuffer.push_excerpts(
20483 buffer_2.clone(),
20484 [
20485 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20486 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20487 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20488 ],
20489 cx,
20490 );
20491 multibuffer.push_excerpts(
20492 buffer_3.clone(),
20493 [
20494 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20495 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20496 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20497 ],
20498 cx,
20499 );
20500 multibuffer
20501 });
20502
20503 let editor =
20504 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20505 editor
20506 .update(cx, |editor, _window, cx| {
20507 for (buffer, diff_base) in [
20508 (buffer_1.clone(), file_1_old),
20509 (buffer_2.clone(), file_2_old),
20510 (buffer_3.clone(), file_3_old),
20511 ] {
20512 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20513 editor
20514 .buffer
20515 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20516 }
20517 })
20518 .unwrap();
20519
20520 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20521 cx.run_until_parked();
20522
20523 cx.assert_editor_state(
20524 &"
20525 ˇaaa
20526 ccc
20527 ddd
20528
20529 ggg
20530 hhh
20531
20532
20533 lll
20534 mmm
20535 NNN
20536
20537 qqq
20538 rrr
20539
20540 uuu
20541 111
20542 222
20543 333
20544
20545 666
20546 777
20547
20548 000
20549 !!!"
20550 .unindent(),
20551 );
20552
20553 cx.update_editor(|editor, window, cx| {
20554 editor.select_all(&SelectAll, window, cx);
20555 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20556 });
20557 cx.executor().run_until_parked();
20558
20559 cx.assert_state_with_diff(
20560 "
20561 «aaa
20562 - bbb
20563 ccc
20564 ddd
20565
20566 ggg
20567 hhh
20568
20569
20570 lll
20571 mmm
20572 - nnn
20573 + NNN
20574
20575 qqq
20576 rrr
20577
20578 uuu
20579 111
20580 222
20581 333
20582
20583 + 666
20584 777
20585
20586 000
20587 !!!ˇ»"
20588 .unindent(),
20589 );
20590}
20591
20592#[gpui::test]
20593async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20594 init_test(cx, |_| {});
20595
20596 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20597 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20598
20599 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20600 let multi_buffer = cx.new(|cx| {
20601 let mut multibuffer = MultiBuffer::new(ReadWrite);
20602 multibuffer.push_excerpts(
20603 buffer.clone(),
20604 [
20605 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20606 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20607 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20608 ],
20609 cx,
20610 );
20611 multibuffer
20612 });
20613
20614 let editor =
20615 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20616 editor
20617 .update(cx, |editor, _window, cx| {
20618 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20619 editor
20620 .buffer
20621 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20622 })
20623 .unwrap();
20624
20625 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20626 cx.run_until_parked();
20627
20628 cx.update_editor(|editor, window, cx| {
20629 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20630 });
20631 cx.executor().run_until_parked();
20632
20633 // When the start of a hunk coincides with the start of its excerpt,
20634 // the hunk is expanded. When the start of a hunk is earlier than
20635 // the start of its excerpt, the hunk is not expanded.
20636 cx.assert_state_with_diff(
20637 "
20638 ˇaaa
20639 - bbb
20640 + BBB
20641
20642 - ddd
20643 - eee
20644 + DDD
20645 + EEE
20646 fff
20647
20648 iii
20649 "
20650 .unindent(),
20651 );
20652}
20653
20654#[gpui::test]
20655async fn test_edits_around_expanded_insertion_hunks(
20656 executor: BackgroundExecutor,
20657 cx: &mut TestAppContext,
20658) {
20659 init_test(cx, |_| {});
20660
20661 let mut cx = EditorTestContext::new(cx).await;
20662
20663 let diff_base = r#"
20664 use some::mod1;
20665 use some::mod2;
20666
20667 const A: u32 = 42;
20668
20669 fn main() {
20670 println!("hello");
20671
20672 println!("world");
20673 }
20674 "#
20675 .unindent();
20676 executor.run_until_parked();
20677 cx.set_state(
20678 &r#"
20679 use some::mod1;
20680 use some::mod2;
20681
20682 const A: u32 = 42;
20683 const B: u32 = 42;
20684 const C: u32 = 42;
20685 ˇ
20686
20687 fn main() {
20688 println!("hello");
20689
20690 println!("world");
20691 }
20692 "#
20693 .unindent(),
20694 );
20695
20696 cx.set_head_text(&diff_base);
20697 executor.run_until_parked();
20698
20699 cx.update_editor(|editor, window, cx| {
20700 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20701 });
20702 executor.run_until_parked();
20703
20704 cx.assert_state_with_diff(
20705 r#"
20706 use some::mod1;
20707 use some::mod2;
20708
20709 const A: u32 = 42;
20710 + const B: u32 = 42;
20711 + const C: u32 = 42;
20712 + ˇ
20713
20714 fn main() {
20715 println!("hello");
20716
20717 println!("world");
20718 }
20719 "#
20720 .unindent(),
20721 );
20722
20723 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20724 executor.run_until_parked();
20725
20726 cx.assert_state_with_diff(
20727 r#"
20728 use some::mod1;
20729 use some::mod2;
20730
20731 const A: u32 = 42;
20732 + const B: u32 = 42;
20733 + const C: u32 = 42;
20734 + const D: u32 = 42;
20735 + ˇ
20736
20737 fn main() {
20738 println!("hello");
20739
20740 println!("world");
20741 }
20742 "#
20743 .unindent(),
20744 );
20745
20746 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20747 executor.run_until_parked();
20748
20749 cx.assert_state_with_diff(
20750 r#"
20751 use some::mod1;
20752 use some::mod2;
20753
20754 const A: u32 = 42;
20755 + const B: u32 = 42;
20756 + const C: u32 = 42;
20757 + const D: u32 = 42;
20758 + const E: u32 = 42;
20759 + ˇ
20760
20761 fn main() {
20762 println!("hello");
20763
20764 println!("world");
20765 }
20766 "#
20767 .unindent(),
20768 );
20769
20770 cx.update_editor(|editor, window, cx| {
20771 editor.delete_line(&DeleteLine, window, cx);
20772 });
20773 executor.run_until_parked();
20774
20775 cx.assert_state_with_diff(
20776 r#"
20777 use some::mod1;
20778 use some::mod2;
20779
20780 const A: u32 = 42;
20781 + const B: u32 = 42;
20782 + const C: u32 = 42;
20783 + const D: u32 = 42;
20784 + const E: u32 = 42;
20785 ˇ
20786 fn main() {
20787 println!("hello");
20788
20789 println!("world");
20790 }
20791 "#
20792 .unindent(),
20793 );
20794
20795 cx.update_editor(|editor, window, cx| {
20796 editor.move_up(&MoveUp, window, cx);
20797 editor.delete_line(&DeleteLine, window, cx);
20798 editor.move_up(&MoveUp, window, cx);
20799 editor.delete_line(&DeleteLine, window, cx);
20800 editor.move_up(&MoveUp, window, cx);
20801 editor.delete_line(&DeleteLine, window, cx);
20802 });
20803 executor.run_until_parked();
20804 cx.assert_state_with_diff(
20805 r#"
20806 use some::mod1;
20807 use some::mod2;
20808
20809 const A: u32 = 42;
20810 + const B: u32 = 42;
20811 ˇ
20812 fn main() {
20813 println!("hello");
20814
20815 println!("world");
20816 }
20817 "#
20818 .unindent(),
20819 );
20820
20821 cx.update_editor(|editor, window, cx| {
20822 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20823 editor.delete_line(&DeleteLine, window, cx);
20824 });
20825 executor.run_until_parked();
20826 cx.assert_state_with_diff(
20827 r#"
20828 ˇ
20829 fn main() {
20830 println!("hello");
20831
20832 println!("world");
20833 }
20834 "#
20835 .unindent(),
20836 );
20837}
20838
20839#[gpui::test]
20840async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20841 init_test(cx, |_| {});
20842
20843 let mut cx = EditorTestContext::new(cx).await;
20844 cx.set_head_text(indoc! { "
20845 one
20846 two
20847 three
20848 four
20849 five
20850 "
20851 });
20852 cx.set_state(indoc! { "
20853 one
20854 ˇthree
20855 five
20856 "});
20857 cx.run_until_parked();
20858 cx.update_editor(|editor, window, cx| {
20859 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20860 });
20861 cx.assert_state_with_diff(
20862 indoc! { "
20863 one
20864 - two
20865 ˇthree
20866 - four
20867 five
20868 "}
20869 .to_string(),
20870 );
20871 cx.update_editor(|editor, window, cx| {
20872 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20873 });
20874
20875 cx.assert_state_with_diff(
20876 indoc! { "
20877 one
20878 ˇthree
20879 five
20880 "}
20881 .to_string(),
20882 );
20883
20884 cx.set_state(indoc! { "
20885 one
20886 ˇTWO
20887 three
20888 four
20889 five
20890 "});
20891 cx.run_until_parked();
20892 cx.update_editor(|editor, window, cx| {
20893 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20894 });
20895
20896 cx.assert_state_with_diff(
20897 indoc! { "
20898 one
20899 - two
20900 + ˇTWO
20901 three
20902 four
20903 five
20904 "}
20905 .to_string(),
20906 );
20907 cx.update_editor(|editor, window, cx| {
20908 editor.move_up(&Default::default(), window, cx);
20909 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20910 });
20911 cx.assert_state_with_diff(
20912 indoc! { "
20913 one
20914 ˇTWO
20915 three
20916 four
20917 five
20918 "}
20919 .to_string(),
20920 );
20921}
20922
20923#[gpui::test]
20924async fn test_edits_around_expanded_deletion_hunks(
20925 executor: BackgroundExecutor,
20926 cx: &mut TestAppContext,
20927) {
20928 init_test(cx, |_| {});
20929
20930 let mut cx = EditorTestContext::new(cx).await;
20931
20932 let diff_base = r#"
20933 use some::mod1;
20934 use some::mod2;
20935
20936 const A: u32 = 42;
20937 const B: u32 = 42;
20938 const C: u32 = 42;
20939
20940
20941 fn main() {
20942 println!("hello");
20943
20944 println!("world");
20945 }
20946 "#
20947 .unindent();
20948 executor.run_until_parked();
20949 cx.set_state(
20950 &r#"
20951 use some::mod1;
20952 use some::mod2;
20953
20954 ˇconst B: u32 = 42;
20955 const C: u32 = 42;
20956
20957
20958 fn main() {
20959 println!("hello");
20960
20961 println!("world");
20962 }
20963 "#
20964 .unindent(),
20965 );
20966
20967 cx.set_head_text(&diff_base);
20968 executor.run_until_parked();
20969
20970 cx.update_editor(|editor, window, cx| {
20971 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20972 });
20973 executor.run_until_parked();
20974
20975 cx.assert_state_with_diff(
20976 r#"
20977 use some::mod1;
20978 use some::mod2;
20979
20980 - const A: u32 = 42;
20981 ˇconst B: u32 = 42;
20982 const C: u32 = 42;
20983
20984
20985 fn main() {
20986 println!("hello");
20987
20988 println!("world");
20989 }
20990 "#
20991 .unindent(),
20992 );
20993
20994 cx.update_editor(|editor, window, cx| {
20995 editor.delete_line(&DeleteLine, window, cx);
20996 });
20997 executor.run_until_parked();
20998 cx.assert_state_with_diff(
20999 r#"
21000 use some::mod1;
21001 use some::mod2;
21002
21003 - const A: u32 = 42;
21004 - const B: u32 = 42;
21005 ˇconst C: u32 = 42;
21006
21007
21008 fn main() {
21009 println!("hello");
21010
21011 println!("world");
21012 }
21013 "#
21014 .unindent(),
21015 );
21016
21017 cx.update_editor(|editor, window, cx| {
21018 editor.delete_line(&DeleteLine, window, cx);
21019 });
21020 executor.run_until_parked();
21021 cx.assert_state_with_diff(
21022 r#"
21023 use some::mod1;
21024 use some::mod2;
21025
21026 - const A: u32 = 42;
21027 - const B: u32 = 42;
21028 - const C: u32 = 42;
21029 ˇ
21030
21031 fn main() {
21032 println!("hello");
21033
21034 println!("world");
21035 }
21036 "#
21037 .unindent(),
21038 );
21039
21040 cx.update_editor(|editor, window, cx| {
21041 editor.handle_input("replacement", window, cx);
21042 });
21043 executor.run_until_parked();
21044 cx.assert_state_with_diff(
21045 r#"
21046 use some::mod1;
21047 use some::mod2;
21048
21049 - const A: u32 = 42;
21050 - const B: u32 = 42;
21051 - const C: u32 = 42;
21052 -
21053 + replacementˇ
21054
21055 fn main() {
21056 println!("hello");
21057
21058 println!("world");
21059 }
21060 "#
21061 .unindent(),
21062 );
21063}
21064
21065#[gpui::test]
21066async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21067 init_test(cx, |_| {});
21068
21069 let mut cx = EditorTestContext::new(cx).await;
21070
21071 let base_text = r#"
21072 one
21073 two
21074 three
21075 four
21076 five
21077 "#
21078 .unindent();
21079 executor.run_until_parked();
21080 cx.set_state(
21081 &r#"
21082 one
21083 two
21084 fˇour
21085 five
21086 "#
21087 .unindent(),
21088 );
21089
21090 cx.set_head_text(&base_text);
21091 executor.run_until_parked();
21092
21093 cx.update_editor(|editor, window, cx| {
21094 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21095 });
21096 executor.run_until_parked();
21097
21098 cx.assert_state_with_diff(
21099 r#"
21100 one
21101 two
21102 - three
21103 fˇour
21104 five
21105 "#
21106 .unindent(),
21107 );
21108
21109 cx.update_editor(|editor, window, cx| {
21110 editor.backspace(&Backspace, window, cx);
21111 editor.backspace(&Backspace, window, cx);
21112 });
21113 executor.run_until_parked();
21114 cx.assert_state_with_diff(
21115 r#"
21116 one
21117 two
21118 - threeˇ
21119 - four
21120 + our
21121 five
21122 "#
21123 .unindent(),
21124 );
21125}
21126
21127#[gpui::test]
21128async fn test_edit_after_expanded_modification_hunk(
21129 executor: BackgroundExecutor,
21130 cx: &mut TestAppContext,
21131) {
21132 init_test(cx, |_| {});
21133
21134 let mut cx = EditorTestContext::new(cx).await;
21135
21136 let diff_base = r#"
21137 use some::mod1;
21138 use some::mod2;
21139
21140 const A: u32 = 42;
21141 const B: u32 = 42;
21142 const C: u32 = 42;
21143 const D: u32 = 42;
21144
21145
21146 fn main() {
21147 println!("hello");
21148
21149 println!("world");
21150 }"#
21151 .unindent();
21152
21153 cx.set_state(
21154 &r#"
21155 use some::mod1;
21156 use some::mod2;
21157
21158 const A: u32 = 42;
21159 const B: u32 = 42;
21160 const C: u32 = 43ˇ
21161 const D: u32 = 42;
21162
21163
21164 fn main() {
21165 println!("hello");
21166
21167 println!("world");
21168 }"#
21169 .unindent(),
21170 );
21171
21172 cx.set_head_text(&diff_base);
21173 executor.run_until_parked();
21174 cx.update_editor(|editor, window, cx| {
21175 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21176 });
21177 executor.run_until_parked();
21178
21179 cx.assert_state_with_diff(
21180 r#"
21181 use some::mod1;
21182 use some::mod2;
21183
21184 const A: u32 = 42;
21185 const B: u32 = 42;
21186 - const C: u32 = 42;
21187 + const C: u32 = 43ˇ
21188 const D: u32 = 42;
21189
21190
21191 fn main() {
21192 println!("hello");
21193
21194 println!("world");
21195 }"#
21196 .unindent(),
21197 );
21198
21199 cx.update_editor(|editor, window, cx| {
21200 editor.handle_input("\nnew_line\n", window, cx);
21201 });
21202 executor.run_until_parked();
21203
21204 cx.assert_state_with_diff(
21205 r#"
21206 use some::mod1;
21207 use some::mod2;
21208
21209 const A: u32 = 42;
21210 const B: u32 = 42;
21211 - const C: u32 = 42;
21212 + const C: u32 = 43
21213 + new_line
21214 + ˇ
21215 const D: u32 = 42;
21216
21217
21218 fn main() {
21219 println!("hello");
21220
21221 println!("world");
21222 }"#
21223 .unindent(),
21224 );
21225}
21226
21227#[gpui::test]
21228async fn test_stage_and_unstage_added_file_hunk(
21229 executor: BackgroundExecutor,
21230 cx: &mut TestAppContext,
21231) {
21232 init_test(cx, |_| {});
21233
21234 let mut cx = EditorTestContext::new(cx).await;
21235 cx.update_editor(|editor, _, cx| {
21236 editor.set_expand_all_diff_hunks(cx);
21237 });
21238
21239 let working_copy = r#"
21240 ˇfn main() {
21241 println!("hello, world!");
21242 }
21243 "#
21244 .unindent();
21245
21246 cx.set_state(&working_copy);
21247 executor.run_until_parked();
21248
21249 cx.assert_state_with_diff(
21250 r#"
21251 + ˇfn main() {
21252 + println!("hello, world!");
21253 + }
21254 "#
21255 .unindent(),
21256 );
21257 cx.assert_index_text(None);
21258
21259 cx.update_editor(|editor, window, cx| {
21260 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21261 });
21262 executor.run_until_parked();
21263 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21264 cx.assert_state_with_diff(
21265 r#"
21266 + ˇfn main() {
21267 + println!("hello, world!");
21268 + }
21269 "#
21270 .unindent(),
21271 );
21272
21273 cx.update_editor(|editor, window, cx| {
21274 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21275 });
21276 executor.run_until_parked();
21277 cx.assert_index_text(None);
21278}
21279
21280async fn setup_indent_guides_editor(
21281 text: &str,
21282 cx: &mut TestAppContext,
21283) -> (BufferId, EditorTestContext) {
21284 init_test(cx, |_| {});
21285
21286 let mut cx = EditorTestContext::new(cx).await;
21287
21288 let buffer_id = cx.update_editor(|editor, window, cx| {
21289 editor.set_text(text, window, cx);
21290 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21291
21292 buffer_ids[0]
21293 });
21294
21295 (buffer_id, cx)
21296}
21297
21298fn assert_indent_guides(
21299 range: Range<u32>,
21300 expected: Vec<IndentGuide>,
21301 active_indices: Option<Vec<usize>>,
21302 cx: &mut EditorTestContext,
21303) {
21304 let indent_guides = cx.update_editor(|editor, window, cx| {
21305 let snapshot = editor.snapshot(window, cx).display_snapshot;
21306 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21307 editor,
21308 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21309 true,
21310 &snapshot,
21311 cx,
21312 );
21313
21314 indent_guides.sort_by(|a, b| {
21315 a.depth.cmp(&b.depth).then(
21316 a.start_row
21317 .cmp(&b.start_row)
21318 .then(a.end_row.cmp(&b.end_row)),
21319 )
21320 });
21321 indent_guides
21322 });
21323
21324 if let Some(expected) = active_indices {
21325 let active_indices = cx.update_editor(|editor, window, cx| {
21326 let snapshot = editor.snapshot(window, cx).display_snapshot;
21327 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21328 });
21329
21330 assert_eq!(
21331 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21332 expected,
21333 "Active indent guide indices do not match"
21334 );
21335 }
21336
21337 assert_eq!(indent_guides, expected, "Indent guides do not match");
21338}
21339
21340fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21341 IndentGuide {
21342 buffer_id,
21343 start_row: MultiBufferRow(start_row),
21344 end_row: MultiBufferRow(end_row),
21345 depth,
21346 tab_size: 4,
21347 settings: IndentGuideSettings {
21348 enabled: true,
21349 line_width: 1,
21350 active_line_width: 1,
21351 coloring: IndentGuideColoring::default(),
21352 background_coloring: IndentGuideBackgroundColoring::default(),
21353 },
21354 }
21355}
21356
21357#[gpui::test]
21358async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21359 let (buffer_id, mut cx) = setup_indent_guides_editor(
21360 &"
21361 fn main() {
21362 let a = 1;
21363 }"
21364 .unindent(),
21365 cx,
21366 )
21367 .await;
21368
21369 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21370}
21371
21372#[gpui::test]
21373async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21374 let (buffer_id, mut cx) = setup_indent_guides_editor(
21375 &"
21376 fn main() {
21377 let a = 1;
21378 let b = 2;
21379 }"
21380 .unindent(),
21381 cx,
21382 )
21383 .await;
21384
21385 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21386}
21387
21388#[gpui::test]
21389async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21390 let (buffer_id, mut cx) = setup_indent_guides_editor(
21391 &"
21392 fn main() {
21393 let a = 1;
21394 if a == 3 {
21395 let b = 2;
21396 } else {
21397 let c = 3;
21398 }
21399 }"
21400 .unindent(),
21401 cx,
21402 )
21403 .await;
21404
21405 assert_indent_guides(
21406 0..8,
21407 vec![
21408 indent_guide(buffer_id, 1, 6, 0),
21409 indent_guide(buffer_id, 3, 3, 1),
21410 indent_guide(buffer_id, 5, 5, 1),
21411 ],
21412 None,
21413 &mut cx,
21414 );
21415}
21416
21417#[gpui::test]
21418async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21419 let (buffer_id, mut cx) = setup_indent_guides_editor(
21420 &"
21421 fn main() {
21422 let a = 1;
21423 let b = 2;
21424 let c = 3;
21425 }"
21426 .unindent(),
21427 cx,
21428 )
21429 .await;
21430
21431 assert_indent_guides(
21432 0..5,
21433 vec![
21434 indent_guide(buffer_id, 1, 3, 0),
21435 indent_guide(buffer_id, 2, 2, 1),
21436 ],
21437 None,
21438 &mut cx,
21439 );
21440}
21441
21442#[gpui::test]
21443async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21444 let (buffer_id, mut cx) = setup_indent_guides_editor(
21445 &"
21446 fn main() {
21447 let a = 1;
21448
21449 let c = 3;
21450 }"
21451 .unindent(),
21452 cx,
21453 )
21454 .await;
21455
21456 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21457}
21458
21459#[gpui::test]
21460async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21461 let (buffer_id, mut cx) = setup_indent_guides_editor(
21462 &"
21463 fn main() {
21464 let a = 1;
21465
21466 let c = 3;
21467
21468 if a == 3 {
21469 let b = 2;
21470 } else {
21471 let c = 3;
21472 }
21473 }"
21474 .unindent(),
21475 cx,
21476 )
21477 .await;
21478
21479 assert_indent_guides(
21480 0..11,
21481 vec![
21482 indent_guide(buffer_id, 1, 9, 0),
21483 indent_guide(buffer_id, 6, 6, 1),
21484 indent_guide(buffer_id, 8, 8, 1),
21485 ],
21486 None,
21487 &mut cx,
21488 );
21489}
21490
21491#[gpui::test]
21492async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21493 let (buffer_id, mut cx) = setup_indent_guides_editor(
21494 &"
21495 fn main() {
21496 let a = 1;
21497
21498 let c = 3;
21499
21500 if a == 3 {
21501 let b = 2;
21502 } else {
21503 let c = 3;
21504 }
21505 }"
21506 .unindent(),
21507 cx,
21508 )
21509 .await;
21510
21511 assert_indent_guides(
21512 1..11,
21513 vec![
21514 indent_guide(buffer_id, 1, 9, 0),
21515 indent_guide(buffer_id, 6, 6, 1),
21516 indent_guide(buffer_id, 8, 8, 1),
21517 ],
21518 None,
21519 &mut cx,
21520 );
21521}
21522
21523#[gpui::test]
21524async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21525 let (buffer_id, mut cx) = setup_indent_guides_editor(
21526 &"
21527 fn main() {
21528 let a = 1;
21529
21530 let c = 3;
21531
21532 if a == 3 {
21533 let b = 2;
21534 } else {
21535 let c = 3;
21536 }
21537 }"
21538 .unindent(),
21539 cx,
21540 )
21541 .await;
21542
21543 assert_indent_guides(
21544 1..10,
21545 vec![
21546 indent_guide(buffer_id, 1, 9, 0),
21547 indent_guide(buffer_id, 6, 6, 1),
21548 indent_guide(buffer_id, 8, 8, 1),
21549 ],
21550 None,
21551 &mut cx,
21552 );
21553}
21554
21555#[gpui::test]
21556async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21557 let (buffer_id, mut cx) = setup_indent_guides_editor(
21558 &"
21559 fn main() {
21560 if a {
21561 b(
21562 c,
21563 d,
21564 )
21565 } else {
21566 e(
21567 f
21568 )
21569 }
21570 }"
21571 .unindent(),
21572 cx,
21573 )
21574 .await;
21575
21576 assert_indent_guides(
21577 0..11,
21578 vec![
21579 indent_guide(buffer_id, 1, 10, 0),
21580 indent_guide(buffer_id, 2, 5, 1),
21581 indent_guide(buffer_id, 7, 9, 1),
21582 indent_guide(buffer_id, 3, 4, 2),
21583 indent_guide(buffer_id, 8, 8, 2),
21584 ],
21585 None,
21586 &mut cx,
21587 );
21588
21589 cx.update_editor(|editor, window, cx| {
21590 editor.fold_at(MultiBufferRow(2), window, cx);
21591 assert_eq!(
21592 editor.display_text(cx),
21593 "
21594 fn main() {
21595 if a {
21596 b(⋯
21597 )
21598 } else {
21599 e(
21600 f
21601 )
21602 }
21603 }"
21604 .unindent()
21605 );
21606 });
21607
21608 assert_indent_guides(
21609 0..11,
21610 vec![
21611 indent_guide(buffer_id, 1, 10, 0),
21612 indent_guide(buffer_id, 2, 5, 1),
21613 indent_guide(buffer_id, 7, 9, 1),
21614 indent_guide(buffer_id, 8, 8, 2),
21615 ],
21616 None,
21617 &mut cx,
21618 );
21619}
21620
21621#[gpui::test]
21622async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21623 let (buffer_id, mut cx) = setup_indent_guides_editor(
21624 &"
21625 block1
21626 block2
21627 block3
21628 block4
21629 block2
21630 block1
21631 block1"
21632 .unindent(),
21633 cx,
21634 )
21635 .await;
21636
21637 assert_indent_guides(
21638 1..10,
21639 vec![
21640 indent_guide(buffer_id, 1, 4, 0),
21641 indent_guide(buffer_id, 2, 3, 1),
21642 indent_guide(buffer_id, 3, 3, 2),
21643 ],
21644 None,
21645 &mut cx,
21646 );
21647}
21648
21649#[gpui::test]
21650async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21651 let (buffer_id, mut cx) = setup_indent_guides_editor(
21652 &"
21653 block1
21654 block2
21655 block3
21656
21657 block1
21658 block1"
21659 .unindent(),
21660 cx,
21661 )
21662 .await;
21663
21664 assert_indent_guides(
21665 0..6,
21666 vec![
21667 indent_guide(buffer_id, 1, 2, 0),
21668 indent_guide(buffer_id, 2, 2, 1),
21669 ],
21670 None,
21671 &mut cx,
21672 );
21673}
21674
21675#[gpui::test]
21676async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21677 let (buffer_id, mut cx) = setup_indent_guides_editor(
21678 &"
21679 function component() {
21680 \treturn (
21681 \t\t\t
21682 \t\t<div>
21683 \t\t\t<abc></abc>
21684 \t\t</div>
21685 \t)
21686 }"
21687 .unindent(),
21688 cx,
21689 )
21690 .await;
21691
21692 assert_indent_guides(
21693 0..8,
21694 vec![
21695 indent_guide(buffer_id, 1, 6, 0),
21696 indent_guide(buffer_id, 2, 5, 1),
21697 indent_guide(buffer_id, 4, 4, 2),
21698 ],
21699 None,
21700 &mut cx,
21701 );
21702}
21703
21704#[gpui::test]
21705async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21706 let (buffer_id, mut cx) = setup_indent_guides_editor(
21707 &"
21708 function component() {
21709 \treturn (
21710 \t
21711 \t\t<div>
21712 \t\t\t<abc></abc>
21713 \t\t</div>
21714 \t)
21715 }"
21716 .unindent(),
21717 cx,
21718 )
21719 .await;
21720
21721 assert_indent_guides(
21722 0..8,
21723 vec![
21724 indent_guide(buffer_id, 1, 6, 0),
21725 indent_guide(buffer_id, 2, 5, 1),
21726 indent_guide(buffer_id, 4, 4, 2),
21727 ],
21728 None,
21729 &mut cx,
21730 );
21731}
21732
21733#[gpui::test]
21734async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21735 let (buffer_id, mut cx) = setup_indent_guides_editor(
21736 &"
21737 block1
21738
21739
21740
21741 block2
21742 "
21743 .unindent(),
21744 cx,
21745 )
21746 .await;
21747
21748 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21749}
21750
21751#[gpui::test]
21752async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21753 let (buffer_id, mut cx) = setup_indent_guides_editor(
21754 &"
21755 def a:
21756 \tb = 3
21757 \tif True:
21758 \t\tc = 4
21759 \t\td = 5
21760 \tprint(b)
21761 "
21762 .unindent(),
21763 cx,
21764 )
21765 .await;
21766
21767 assert_indent_guides(
21768 0..6,
21769 vec![
21770 indent_guide(buffer_id, 1, 5, 0),
21771 indent_guide(buffer_id, 3, 4, 1),
21772 ],
21773 None,
21774 &mut cx,
21775 );
21776}
21777
21778#[gpui::test]
21779async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21780 let (buffer_id, mut cx) = setup_indent_guides_editor(
21781 &"
21782 fn main() {
21783 let a = 1;
21784 }"
21785 .unindent(),
21786 cx,
21787 )
21788 .await;
21789
21790 cx.update_editor(|editor, window, cx| {
21791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21792 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21793 });
21794 });
21795
21796 assert_indent_guides(
21797 0..3,
21798 vec![indent_guide(buffer_id, 1, 1, 0)],
21799 Some(vec![0]),
21800 &mut cx,
21801 );
21802}
21803
21804#[gpui::test]
21805async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21806 let (buffer_id, mut cx) = setup_indent_guides_editor(
21807 &"
21808 fn main() {
21809 if 1 == 2 {
21810 let a = 1;
21811 }
21812 }"
21813 .unindent(),
21814 cx,
21815 )
21816 .await;
21817
21818 cx.update_editor(|editor, window, cx| {
21819 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21820 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21821 });
21822 });
21823
21824 assert_indent_guides(
21825 0..4,
21826 vec![
21827 indent_guide(buffer_id, 1, 3, 0),
21828 indent_guide(buffer_id, 2, 2, 1),
21829 ],
21830 Some(vec![1]),
21831 &mut cx,
21832 );
21833
21834 cx.update_editor(|editor, window, cx| {
21835 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21836 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21837 });
21838 });
21839
21840 assert_indent_guides(
21841 0..4,
21842 vec![
21843 indent_guide(buffer_id, 1, 3, 0),
21844 indent_guide(buffer_id, 2, 2, 1),
21845 ],
21846 Some(vec![1]),
21847 &mut cx,
21848 );
21849
21850 cx.update_editor(|editor, window, cx| {
21851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21852 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21853 });
21854 });
21855
21856 assert_indent_guides(
21857 0..4,
21858 vec![
21859 indent_guide(buffer_id, 1, 3, 0),
21860 indent_guide(buffer_id, 2, 2, 1),
21861 ],
21862 Some(vec![0]),
21863 &mut cx,
21864 );
21865}
21866
21867#[gpui::test]
21868async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21869 let (buffer_id, mut cx) = setup_indent_guides_editor(
21870 &"
21871 fn main() {
21872 let a = 1;
21873
21874 let b = 2;
21875 }"
21876 .unindent(),
21877 cx,
21878 )
21879 .await;
21880
21881 cx.update_editor(|editor, window, cx| {
21882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21883 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21884 });
21885 });
21886
21887 assert_indent_guides(
21888 0..5,
21889 vec![indent_guide(buffer_id, 1, 3, 0)],
21890 Some(vec![0]),
21891 &mut cx,
21892 );
21893}
21894
21895#[gpui::test]
21896async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21897 let (buffer_id, mut cx) = setup_indent_guides_editor(
21898 &"
21899 def m:
21900 a = 1
21901 pass"
21902 .unindent(),
21903 cx,
21904 )
21905 .await;
21906
21907 cx.update_editor(|editor, window, cx| {
21908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21909 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21910 });
21911 });
21912
21913 assert_indent_guides(
21914 0..3,
21915 vec![indent_guide(buffer_id, 1, 2, 0)],
21916 Some(vec![0]),
21917 &mut cx,
21918 );
21919}
21920
21921#[gpui::test]
21922async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21923 init_test(cx, |_| {});
21924 let mut cx = EditorTestContext::new(cx).await;
21925 let text = indoc! {
21926 "
21927 impl A {
21928 fn b() {
21929 0;
21930 3;
21931 5;
21932 6;
21933 7;
21934 }
21935 }
21936 "
21937 };
21938 let base_text = indoc! {
21939 "
21940 impl A {
21941 fn b() {
21942 0;
21943 1;
21944 2;
21945 3;
21946 4;
21947 }
21948 fn c() {
21949 5;
21950 6;
21951 7;
21952 }
21953 }
21954 "
21955 };
21956
21957 cx.update_editor(|editor, window, cx| {
21958 editor.set_text(text, window, cx);
21959
21960 editor.buffer().update(cx, |multibuffer, cx| {
21961 let buffer = multibuffer.as_singleton().unwrap();
21962 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21963
21964 multibuffer.set_all_diff_hunks_expanded(cx);
21965 multibuffer.add_diff(diff, cx);
21966
21967 buffer.read(cx).remote_id()
21968 })
21969 });
21970 cx.run_until_parked();
21971
21972 cx.assert_state_with_diff(
21973 indoc! { "
21974 impl A {
21975 fn b() {
21976 0;
21977 - 1;
21978 - 2;
21979 3;
21980 - 4;
21981 - }
21982 - fn c() {
21983 5;
21984 6;
21985 7;
21986 }
21987 }
21988 ˇ"
21989 }
21990 .to_string(),
21991 );
21992
21993 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21994 editor
21995 .snapshot(window, cx)
21996 .buffer_snapshot()
21997 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21998 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21999 .collect::<Vec<_>>()
22000 });
22001 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22002 assert_eq!(
22003 actual_guides,
22004 vec![
22005 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22006 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22007 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22008 ]
22009 );
22010}
22011
22012#[gpui::test]
22013async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22014 init_test(cx, |_| {});
22015 let mut cx = EditorTestContext::new(cx).await;
22016
22017 let diff_base = r#"
22018 a
22019 b
22020 c
22021 "#
22022 .unindent();
22023
22024 cx.set_state(
22025 &r#"
22026 ˇA
22027 b
22028 C
22029 "#
22030 .unindent(),
22031 );
22032 cx.set_head_text(&diff_base);
22033 cx.update_editor(|editor, window, cx| {
22034 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22035 });
22036 executor.run_until_parked();
22037
22038 let both_hunks_expanded = r#"
22039 - a
22040 + ˇA
22041 b
22042 - c
22043 + C
22044 "#
22045 .unindent();
22046
22047 cx.assert_state_with_diff(both_hunks_expanded.clone());
22048
22049 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22050 let snapshot = editor.snapshot(window, cx);
22051 let hunks = editor
22052 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22053 .collect::<Vec<_>>();
22054 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22055 hunks
22056 .into_iter()
22057 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22058 .collect::<Vec<_>>()
22059 });
22060 assert_eq!(hunk_ranges.len(), 2);
22061
22062 cx.update_editor(|editor, _, cx| {
22063 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22064 });
22065 executor.run_until_parked();
22066
22067 let second_hunk_expanded = r#"
22068 ˇA
22069 b
22070 - c
22071 + C
22072 "#
22073 .unindent();
22074
22075 cx.assert_state_with_diff(second_hunk_expanded);
22076
22077 cx.update_editor(|editor, _, cx| {
22078 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22079 });
22080 executor.run_until_parked();
22081
22082 cx.assert_state_with_diff(both_hunks_expanded.clone());
22083
22084 cx.update_editor(|editor, _, cx| {
22085 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22086 });
22087 executor.run_until_parked();
22088
22089 let first_hunk_expanded = r#"
22090 - a
22091 + ˇA
22092 b
22093 C
22094 "#
22095 .unindent();
22096
22097 cx.assert_state_with_diff(first_hunk_expanded);
22098
22099 cx.update_editor(|editor, _, cx| {
22100 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22101 });
22102 executor.run_until_parked();
22103
22104 cx.assert_state_with_diff(both_hunks_expanded);
22105
22106 cx.set_state(
22107 &r#"
22108 ˇA
22109 b
22110 "#
22111 .unindent(),
22112 );
22113 cx.run_until_parked();
22114
22115 // TODO this cursor position seems bad
22116 cx.assert_state_with_diff(
22117 r#"
22118 - ˇa
22119 + A
22120 b
22121 "#
22122 .unindent(),
22123 );
22124
22125 cx.update_editor(|editor, window, cx| {
22126 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22127 });
22128
22129 cx.assert_state_with_diff(
22130 r#"
22131 - ˇa
22132 + A
22133 b
22134 - c
22135 "#
22136 .unindent(),
22137 );
22138
22139 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22140 let snapshot = editor.snapshot(window, cx);
22141 let hunks = editor
22142 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22143 .collect::<Vec<_>>();
22144 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22145 hunks
22146 .into_iter()
22147 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22148 .collect::<Vec<_>>()
22149 });
22150 assert_eq!(hunk_ranges.len(), 2);
22151
22152 cx.update_editor(|editor, _, cx| {
22153 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22154 });
22155 executor.run_until_parked();
22156
22157 cx.assert_state_with_diff(
22158 r#"
22159 - ˇa
22160 + A
22161 b
22162 "#
22163 .unindent(),
22164 );
22165}
22166
22167#[gpui::test]
22168async fn test_toggle_deletion_hunk_at_start_of_file(
22169 executor: BackgroundExecutor,
22170 cx: &mut TestAppContext,
22171) {
22172 init_test(cx, |_| {});
22173 let mut cx = EditorTestContext::new(cx).await;
22174
22175 let diff_base = r#"
22176 a
22177 b
22178 c
22179 "#
22180 .unindent();
22181
22182 cx.set_state(
22183 &r#"
22184 ˇb
22185 c
22186 "#
22187 .unindent(),
22188 );
22189 cx.set_head_text(&diff_base);
22190 cx.update_editor(|editor, window, cx| {
22191 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22192 });
22193 executor.run_until_parked();
22194
22195 let hunk_expanded = r#"
22196 - a
22197 ˇb
22198 c
22199 "#
22200 .unindent();
22201
22202 cx.assert_state_with_diff(hunk_expanded.clone());
22203
22204 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22205 let snapshot = editor.snapshot(window, cx);
22206 let hunks = editor
22207 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22208 .collect::<Vec<_>>();
22209 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22210 hunks
22211 .into_iter()
22212 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22213 .collect::<Vec<_>>()
22214 });
22215 assert_eq!(hunk_ranges.len(), 1);
22216
22217 cx.update_editor(|editor, _, cx| {
22218 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22219 });
22220 executor.run_until_parked();
22221
22222 let hunk_collapsed = r#"
22223 ˇb
22224 c
22225 "#
22226 .unindent();
22227
22228 cx.assert_state_with_diff(hunk_collapsed);
22229
22230 cx.update_editor(|editor, _, cx| {
22231 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22232 });
22233 executor.run_until_parked();
22234
22235 cx.assert_state_with_diff(hunk_expanded);
22236}
22237
22238#[gpui::test]
22239async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22240 executor: BackgroundExecutor,
22241 cx: &mut TestAppContext,
22242) {
22243 init_test(cx, |_| {});
22244 let mut cx = EditorTestContext::new(cx).await;
22245
22246 cx.set_state("ˇnew\nsecond\nthird\n");
22247 cx.set_head_text("old\nsecond\nthird\n");
22248 cx.update_editor(|editor, window, cx| {
22249 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22250 });
22251 executor.run_until_parked();
22252 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22253
22254 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22255 cx.update_editor(|editor, window, cx| {
22256 let snapshot = editor.snapshot(window, cx);
22257 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22258 let hunks = editor
22259 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22260 .collect::<Vec<_>>();
22261 assert_eq!(hunks.len(), 1);
22262 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22263 editor.toggle_single_diff_hunk(hunk_range, cx)
22264 });
22265 executor.run_until_parked();
22266 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22267
22268 // Keep the editor scrolled to the top so the full hunk remains visible.
22269 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22270}
22271
22272#[gpui::test]
22273async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22274 init_test(cx, |_| {});
22275
22276 let fs = FakeFs::new(cx.executor());
22277 fs.insert_tree(
22278 path!("/test"),
22279 json!({
22280 ".git": {},
22281 "file-1": "ONE\n",
22282 "file-2": "TWO\n",
22283 "file-3": "THREE\n",
22284 }),
22285 )
22286 .await;
22287
22288 fs.set_head_for_repo(
22289 path!("/test/.git").as_ref(),
22290 &[
22291 ("file-1", "one\n".into()),
22292 ("file-2", "two\n".into()),
22293 ("file-3", "three\n".into()),
22294 ],
22295 "deadbeef",
22296 );
22297
22298 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22299 let mut buffers = vec![];
22300 for i in 1..=3 {
22301 let buffer = project
22302 .update(cx, |project, cx| {
22303 let path = format!(path!("/test/file-{}"), i);
22304 project.open_local_buffer(path, cx)
22305 })
22306 .await
22307 .unwrap();
22308 buffers.push(buffer);
22309 }
22310
22311 let multibuffer = cx.new(|cx| {
22312 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22313 multibuffer.set_all_diff_hunks_expanded(cx);
22314 for buffer in &buffers {
22315 let snapshot = buffer.read(cx).snapshot();
22316 multibuffer.set_excerpts_for_path(
22317 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22318 buffer.clone(),
22319 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22320 2,
22321 cx,
22322 );
22323 }
22324 multibuffer
22325 });
22326
22327 let editor = cx.add_window(|window, cx| {
22328 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22329 });
22330 cx.run_until_parked();
22331
22332 let snapshot = editor
22333 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22334 .unwrap();
22335 let hunks = snapshot
22336 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22337 .map(|hunk| match hunk {
22338 DisplayDiffHunk::Unfolded {
22339 display_row_range, ..
22340 } => display_row_range,
22341 DisplayDiffHunk::Folded { .. } => unreachable!(),
22342 })
22343 .collect::<Vec<_>>();
22344 assert_eq!(
22345 hunks,
22346 [
22347 DisplayRow(2)..DisplayRow(4),
22348 DisplayRow(7)..DisplayRow(9),
22349 DisplayRow(12)..DisplayRow(14),
22350 ]
22351 );
22352}
22353
22354#[gpui::test]
22355async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22356 init_test(cx, |_| {});
22357
22358 let mut cx = EditorTestContext::new(cx).await;
22359 cx.set_head_text(indoc! { "
22360 one
22361 two
22362 three
22363 four
22364 five
22365 "
22366 });
22367 cx.set_index_text(indoc! { "
22368 one
22369 two
22370 three
22371 four
22372 five
22373 "
22374 });
22375 cx.set_state(indoc! {"
22376 one
22377 TWO
22378 ˇTHREE
22379 FOUR
22380 five
22381 "});
22382 cx.run_until_parked();
22383 cx.update_editor(|editor, window, cx| {
22384 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22385 });
22386 cx.run_until_parked();
22387 cx.assert_index_text(Some(indoc! {"
22388 one
22389 TWO
22390 THREE
22391 FOUR
22392 five
22393 "}));
22394 cx.set_state(indoc! { "
22395 one
22396 TWO
22397 ˇTHREE-HUNDRED
22398 FOUR
22399 five
22400 "});
22401 cx.run_until_parked();
22402 cx.update_editor(|editor, window, cx| {
22403 let snapshot = editor.snapshot(window, cx);
22404 let hunks = editor
22405 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22406 .collect::<Vec<_>>();
22407 assert_eq!(hunks.len(), 1);
22408 assert_eq!(
22409 hunks[0].status(),
22410 DiffHunkStatus {
22411 kind: DiffHunkStatusKind::Modified,
22412 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22413 }
22414 );
22415
22416 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22417 });
22418 cx.run_until_parked();
22419 cx.assert_index_text(Some(indoc! {"
22420 one
22421 TWO
22422 THREE-HUNDRED
22423 FOUR
22424 five
22425 "}));
22426}
22427
22428#[gpui::test]
22429fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22430 init_test(cx, |_| {});
22431
22432 let editor = cx.add_window(|window, cx| {
22433 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22434 build_editor(buffer, window, cx)
22435 });
22436
22437 let render_args = Arc::new(Mutex::new(None));
22438 let snapshot = editor
22439 .update(cx, |editor, window, cx| {
22440 let snapshot = editor.buffer().read(cx).snapshot(cx);
22441 let range =
22442 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22443
22444 struct RenderArgs {
22445 row: MultiBufferRow,
22446 folded: bool,
22447 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22448 }
22449
22450 let crease = Crease::inline(
22451 range,
22452 FoldPlaceholder::test(),
22453 {
22454 let toggle_callback = render_args.clone();
22455 move |row, folded, callback, _window, _cx| {
22456 *toggle_callback.lock() = Some(RenderArgs {
22457 row,
22458 folded,
22459 callback,
22460 });
22461 div()
22462 }
22463 },
22464 |_row, _folded, _window, _cx| div(),
22465 );
22466
22467 editor.insert_creases(Some(crease), cx);
22468 let snapshot = editor.snapshot(window, cx);
22469 let _div =
22470 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22471 snapshot
22472 })
22473 .unwrap();
22474
22475 let render_args = render_args.lock().take().unwrap();
22476 assert_eq!(render_args.row, MultiBufferRow(1));
22477 assert!(!render_args.folded);
22478 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22479
22480 cx.update_window(*editor, |_, window, cx| {
22481 (render_args.callback)(true, window, cx)
22482 })
22483 .unwrap();
22484 let snapshot = editor
22485 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22486 .unwrap();
22487 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22488
22489 cx.update_window(*editor, |_, window, cx| {
22490 (render_args.callback)(false, window, cx)
22491 })
22492 .unwrap();
22493 let snapshot = editor
22494 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22495 .unwrap();
22496 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22497}
22498
22499#[gpui::test]
22500async fn test_input_text(cx: &mut TestAppContext) {
22501 init_test(cx, |_| {});
22502 let mut cx = EditorTestContext::new(cx).await;
22503
22504 cx.set_state(
22505 &r#"ˇone
22506 two
22507
22508 three
22509 fourˇ
22510 five
22511
22512 siˇx"#
22513 .unindent(),
22514 );
22515
22516 cx.dispatch_action(HandleInput(String::new()));
22517 cx.assert_editor_state(
22518 &r#"ˇone
22519 two
22520
22521 three
22522 fourˇ
22523 five
22524
22525 siˇx"#
22526 .unindent(),
22527 );
22528
22529 cx.dispatch_action(HandleInput("AAAA".to_string()));
22530 cx.assert_editor_state(
22531 &r#"AAAAˇone
22532 two
22533
22534 three
22535 fourAAAAˇ
22536 five
22537
22538 siAAAAˇx"#
22539 .unindent(),
22540 );
22541}
22542
22543#[gpui::test]
22544async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22545 init_test(cx, |_| {});
22546
22547 let mut cx = EditorTestContext::new(cx).await;
22548 cx.set_state(
22549 r#"let foo = 1;
22550let foo = 2;
22551let foo = 3;
22552let fooˇ = 4;
22553let foo = 5;
22554let foo = 6;
22555let foo = 7;
22556let foo = 8;
22557let foo = 9;
22558let foo = 10;
22559let foo = 11;
22560let foo = 12;
22561let foo = 13;
22562let foo = 14;
22563let foo = 15;"#,
22564 );
22565
22566 cx.update_editor(|e, window, cx| {
22567 assert_eq!(
22568 e.next_scroll_position,
22569 NextScrollCursorCenterTopBottom::Center,
22570 "Default next scroll direction is center",
22571 );
22572
22573 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22574 assert_eq!(
22575 e.next_scroll_position,
22576 NextScrollCursorCenterTopBottom::Top,
22577 "After center, next scroll direction should be top",
22578 );
22579
22580 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22581 assert_eq!(
22582 e.next_scroll_position,
22583 NextScrollCursorCenterTopBottom::Bottom,
22584 "After top, next scroll direction should be bottom",
22585 );
22586
22587 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22588 assert_eq!(
22589 e.next_scroll_position,
22590 NextScrollCursorCenterTopBottom::Center,
22591 "After bottom, scrolling should start over",
22592 );
22593
22594 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22595 assert_eq!(
22596 e.next_scroll_position,
22597 NextScrollCursorCenterTopBottom::Top,
22598 "Scrolling continues if retriggered fast enough"
22599 );
22600 });
22601
22602 cx.executor()
22603 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22604 cx.executor().run_until_parked();
22605 cx.update_editor(|e, _, _| {
22606 assert_eq!(
22607 e.next_scroll_position,
22608 NextScrollCursorCenterTopBottom::Center,
22609 "If scrolling is not triggered fast enough, it should reset"
22610 );
22611 });
22612}
22613
22614#[gpui::test]
22615async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22616 init_test(cx, |_| {});
22617 let mut cx = EditorLspTestContext::new_rust(
22618 lsp::ServerCapabilities {
22619 definition_provider: Some(lsp::OneOf::Left(true)),
22620 references_provider: Some(lsp::OneOf::Left(true)),
22621 ..lsp::ServerCapabilities::default()
22622 },
22623 cx,
22624 )
22625 .await;
22626
22627 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22628 let go_to_definition = cx
22629 .lsp
22630 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22631 move |params, _| async move {
22632 if empty_go_to_definition {
22633 Ok(None)
22634 } else {
22635 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22636 uri: params.text_document_position_params.text_document.uri,
22637 range: lsp::Range::new(
22638 lsp::Position::new(4, 3),
22639 lsp::Position::new(4, 6),
22640 ),
22641 })))
22642 }
22643 },
22644 );
22645 let references = cx
22646 .lsp
22647 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22648 Ok(Some(vec![lsp::Location {
22649 uri: params.text_document_position.text_document.uri,
22650 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22651 }]))
22652 });
22653 (go_to_definition, references)
22654 };
22655
22656 cx.set_state(
22657 &r#"fn one() {
22658 let mut a = ˇtwo();
22659 }
22660
22661 fn two() {}"#
22662 .unindent(),
22663 );
22664 set_up_lsp_handlers(false, &mut cx);
22665 let navigated = cx
22666 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22667 .await
22668 .expect("Failed to navigate to definition");
22669 assert_eq!(
22670 navigated,
22671 Navigated::Yes,
22672 "Should have navigated to definition from the GetDefinition response"
22673 );
22674 cx.assert_editor_state(
22675 &r#"fn one() {
22676 let mut a = two();
22677 }
22678
22679 fn «twoˇ»() {}"#
22680 .unindent(),
22681 );
22682
22683 let editors = cx.update_workspace(|workspace, _, cx| {
22684 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22685 });
22686 cx.update_editor(|_, _, test_editor_cx| {
22687 assert_eq!(
22688 editors.len(),
22689 1,
22690 "Initially, only one, test, editor should be open in the workspace"
22691 );
22692 assert_eq!(
22693 test_editor_cx.entity(),
22694 editors.last().expect("Asserted len is 1").clone()
22695 );
22696 });
22697
22698 set_up_lsp_handlers(true, &mut cx);
22699 let navigated = cx
22700 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22701 .await
22702 .expect("Failed to navigate to lookup references");
22703 assert_eq!(
22704 navigated,
22705 Navigated::Yes,
22706 "Should have navigated to references as a fallback after empty GoToDefinition response"
22707 );
22708 // We should not change the selections in the existing file,
22709 // if opening another milti buffer with the references
22710 cx.assert_editor_state(
22711 &r#"fn one() {
22712 let mut a = two();
22713 }
22714
22715 fn «twoˇ»() {}"#
22716 .unindent(),
22717 );
22718 let editors = cx.update_workspace(|workspace, _, cx| {
22719 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22720 });
22721 cx.update_editor(|_, _, test_editor_cx| {
22722 assert_eq!(
22723 editors.len(),
22724 2,
22725 "After falling back to references search, we open a new editor with the results"
22726 );
22727 let references_fallback_text = editors
22728 .into_iter()
22729 .find(|new_editor| *new_editor != test_editor_cx.entity())
22730 .expect("Should have one non-test editor now")
22731 .read(test_editor_cx)
22732 .text(test_editor_cx);
22733 assert_eq!(
22734 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22735 "Should use the range from the references response and not the GoToDefinition one"
22736 );
22737 });
22738}
22739
22740#[gpui::test]
22741async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22742 init_test(cx, |_| {});
22743 cx.update(|cx| {
22744 let mut editor_settings = EditorSettings::get_global(cx).clone();
22745 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22746 EditorSettings::override_global(editor_settings, cx);
22747 });
22748 let mut cx = EditorLspTestContext::new_rust(
22749 lsp::ServerCapabilities {
22750 definition_provider: Some(lsp::OneOf::Left(true)),
22751 references_provider: Some(lsp::OneOf::Left(true)),
22752 ..lsp::ServerCapabilities::default()
22753 },
22754 cx,
22755 )
22756 .await;
22757 let original_state = r#"fn one() {
22758 let mut a = ˇtwo();
22759 }
22760
22761 fn two() {}"#
22762 .unindent();
22763 cx.set_state(&original_state);
22764
22765 let mut go_to_definition = cx
22766 .lsp
22767 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22768 move |_, _| async move { Ok(None) },
22769 );
22770 let _references = cx
22771 .lsp
22772 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22773 panic!("Should not call for references with no go to definition fallback")
22774 });
22775
22776 let navigated = cx
22777 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22778 .await
22779 .expect("Failed to navigate to lookup references");
22780 go_to_definition
22781 .next()
22782 .await
22783 .expect("Should have called the go_to_definition handler");
22784
22785 assert_eq!(
22786 navigated,
22787 Navigated::No,
22788 "Should have navigated to references as a fallback after empty GoToDefinition response"
22789 );
22790 cx.assert_editor_state(&original_state);
22791 let editors = cx.update_workspace(|workspace, _, cx| {
22792 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22793 });
22794 cx.update_editor(|_, _, _| {
22795 assert_eq!(
22796 editors.len(),
22797 1,
22798 "After unsuccessful fallback, no other editor should have been opened"
22799 );
22800 });
22801}
22802
22803#[gpui::test]
22804async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22805 init_test(cx, |_| {});
22806 let mut cx = EditorLspTestContext::new_rust(
22807 lsp::ServerCapabilities {
22808 references_provider: Some(lsp::OneOf::Left(true)),
22809 ..lsp::ServerCapabilities::default()
22810 },
22811 cx,
22812 )
22813 .await;
22814
22815 cx.set_state(
22816 &r#"
22817 fn one() {
22818 let mut a = two();
22819 }
22820
22821 fn ˇtwo() {}"#
22822 .unindent(),
22823 );
22824 cx.lsp
22825 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22826 Ok(Some(vec![
22827 lsp::Location {
22828 uri: params.text_document_position.text_document.uri.clone(),
22829 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22830 },
22831 lsp::Location {
22832 uri: params.text_document_position.text_document.uri,
22833 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22834 },
22835 ]))
22836 });
22837 let navigated = cx
22838 .update_editor(|editor, window, cx| {
22839 editor.find_all_references(&FindAllReferences::default(), window, cx)
22840 })
22841 .unwrap()
22842 .await
22843 .expect("Failed to navigate to references");
22844 assert_eq!(
22845 navigated,
22846 Navigated::Yes,
22847 "Should have navigated to references from the FindAllReferences response"
22848 );
22849 cx.assert_editor_state(
22850 &r#"fn one() {
22851 let mut a = two();
22852 }
22853
22854 fn ˇtwo() {}"#
22855 .unindent(),
22856 );
22857
22858 let editors = cx.update_workspace(|workspace, _, cx| {
22859 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22860 });
22861 cx.update_editor(|_, _, _| {
22862 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22863 });
22864
22865 cx.set_state(
22866 &r#"fn one() {
22867 let mut a = ˇtwo();
22868 }
22869
22870 fn two() {}"#
22871 .unindent(),
22872 );
22873 let navigated = cx
22874 .update_editor(|editor, window, cx| {
22875 editor.find_all_references(&FindAllReferences::default(), window, cx)
22876 })
22877 .unwrap()
22878 .await
22879 .expect("Failed to navigate to references");
22880 assert_eq!(
22881 navigated,
22882 Navigated::Yes,
22883 "Should have navigated to references from the FindAllReferences response"
22884 );
22885 cx.assert_editor_state(
22886 &r#"fn one() {
22887 let mut a = ˇtwo();
22888 }
22889
22890 fn two() {}"#
22891 .unindent(),
22892 );
22893 let editors = cx.update_workspace(|workspace, _, cx| {
22894 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22895 });
22896 cx.update_editor(|_, _, _| {
22897 assert_eq!(
22898 editors.len(),
22899 2,
22900 "should have re-used the previous multibuffer"
22901 );
22902 });
22903
22904 cx.set_state(
22905 &r#"fn one() {
22906 let mut a = ˇtwo();
22907 }
22908 fn three() {}
22909 fn two() {}"#
22910 .unindent(),
22911 );
22912 cx.lsp
22913 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22914 Ok(Some(vec![
22915 lsp::Location {
22916 uri: params.text_document_position.text_document.uri.clone(),
22917 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22918 },
22919 lsp::Location {
22920 uri: params.text_document_position.text_document.uri,
22921 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22922 },
22923 ]))
22924 });
22925 let navigated = cx
22926 .update_editor(|editor, window, cx| {
22927 editor.find_all_references(&FindAllReferences::default(), window, cx)
22928 })
22929 .unwrap()
22930 .await
22931 .expect("Failed to navigate to references");
22932 assert_eq!(
22933 navigated,
22934 Navigated::Yes,
22935 "Should have navigated to references from the FindAllReferences response"
22936 );
22937 cx.assert_editor_state(
22938 &r#"fn one() {
22939 let mut a = ˇtwo();
22940 }
22941 fn three() {}
22942 fn two() {}"#
22943 .unindent(),
22944 );
22945 let editors = cx.update_workspace(|workspace, _, cx| {
22946 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22947 });
22948 cx.update_editor(|_, _, _| {
22949 assert_eq!(
22950 editors.len(),
22951 3,
22952 "should have used a new multibuffer as offsets changed"
22953 );
22954 });
22955}
22956#[gpui::test]
22957async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22958 init_test(cx, |_| {});
22959
22960 let language = Arc::new(Language::new(
22961 LanguageConfig::default(),
22962 Some(tree_sitter_rust::LANGUAGE.into()),
22963 ));
22964
22965 let text = r#"
22966 #[cfg(test)]
22967 mod tests() {
22968 #[test]
22969 fn runnable_1() {
22970 let a = 1;
22971 }
22972
22973 #[test]
22974 fn runnable_2() {
22975 let a = 1;
22976 let b = 2;
22977 }
22978 }
22979 "#
22980 .unindent();
22981
22982 let fs = FakeFs::new(cx.executor());
22983 fs.insert_file("/file.rs", Default::default()).await;
22984
22985 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22986 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22987 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22988 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22989 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22990
22991 let editor = cx.new_window_entity(|window, cx| {
22992 Editor::new(
22993 EditorMode::full(),
22994 multi_buffer,
22995 Some(project.clone()),
22996 window,
22997 cx,
22998 )
22999 });
23000
23001 editor.update_in(cx, |editor, window, cx| {
23002 let snapshot = editor.buffer().read(cx).snapshot(cx);
23003 editor.tasks.insert(
23004 (buffer.read(cx).remote_id(), 3),
23005 RunnableTasks {
23006 templates: vec![],
23007 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23008 column: 0,
23009 extra_variables: HashMap::default(),
23010 context_range: BufferOffset(43)..BufferOffset(85),
23011 },
23012 );
23013 editor.tasks.insert(
23014 (buffer.read(cx).remote_id(), 8),
23015 RunnableTasks {
23016 templates: vec![],
23017 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23018 column: 0,
23019 extra_variables: HashMap::default(),
23020 context_range: BufferOffset(86)..BufferOffset(191),
23021 },
23022 );
23023
23024 // Test finding task when cursor is inside function body
23025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23026 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23027 });
23028 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23029 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23030
23031 // Test finding task when cursor is on function name
23032 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23033 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23034 });
23035 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23036 assert_eq!(row, 8, "Should find task when cursor is on function name");
23037 });
23038}
23039
23040#[gpui::test]
23041async fn test_folding_buffers(cx: &mut TestAppContext) {
23042 init_test(cx, |_| {});
23043
23044 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23045 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23046 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23047
23048 let fs = FakeFs::new(cx.executor());
23049 fs.insert_tree(
23050 path!("/a"),
23051 json!({
23052 "first.rs": sample_text_1,
23053 "second.rs": sample_text_2,
23054 "third.rs": sample_text_3,
23055 }),
23056 )
23057 .await;
23058 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23059 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23060 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23061 let worktree = project.update(cx, |project, cx| {
23062 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23063 assert_eq!(worktrees.len(), 1);
23064 worktrees.pop().unwrap()
23065 });
23066 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23067
23068 let buffer_1 = project
23069 .update(cx, |project, cx| {
23070 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23071 })
23072 .await
23073 .unwrap();
23074 let buffer_2 = project
23075 .update(cx, |project, cx| {
23076 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23077 })
23078 .await
23079 .unwrap();
23080 let buffer_3 = project
23081 .update(cx, |project, cx| {
23082 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23083 })
23084 .await
23085 .unwrap();
23086
23087 let multi_buffer = cx.new(|cx| {
23088 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23089 multi_buffer.push_excerpts(
23090 buffer_1.clone(),
23091 [
23092 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23093 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23094 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23095 ],
23096 cx,
23097 );
23098 multi_buffer.push_excerpts(
23099 buffer_2.clone(),
23100 [
23101 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23102 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23103 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23104 ],
23105 cx,
23106 );
23107 multi_buffer.push_excerpts(
23108 buffer_3.clone(),
23109 [
23110 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23111 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23112 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23113 ],
23114 cx,
23115 );
23116 multi_buffer
23117 });
23118 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23119 Editor::new(
23120 EditorMode::full(),
23121 multi_buffer.clone(),
23122 Some(project.clone()),
23123 window,
23124 cx,
23125 )
23126 });
23127
23128 assert_eq!(
23129 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23130 "\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",
23131 );
23132
23133 multi_buffer_editor.update(cx, |editor, cx| {
23134 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23135 });
23136 assert_eq!(
23137 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23138 "\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",
23139 "After folding the first buffer, its text should not be displayed"
23140 );
23141
23142 multi_buffer_editor.update(cx, |editor, cx| {
23143 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23144 });
23145 assert_eq!(
23146 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23147 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23148 "After folding the second buffer, its text should not be displayed"
23149 );
23150
23151 multi_buffer_editor.update(cx, |editor, cx| {
23152 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23153 });
23154 assert_eq!(
23155 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23156 "\n\n\n\n\n",
23157 "After folding the third buffer, its text should not be displayed"
23158 );
23159
23160 // Emulate selection inside the fold logic, that should work
23161 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23162 editor
23163 .snapshot(window, cx)
23164 .next_line_boundary(Point::new(0, 4));
23165 });
23166
23167 multi_buffer_editor.update(cx, |editor, cx| {
23168 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23169 });
23170 assert_eq!(
23171 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23172 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23173 "After unfolding the second buffer, its text should be displayed"
23174 );
23175
23176 // Typing inside of buffer 1 causes that buffer to be unfolded.
23177 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23178 assert_eq!(
23179 multi_buffer
23180 .read(cx)
23181 .snapshot(cx)
23182 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23183 .collect::<String>(),
23184 "bbbb"
23185 );
23186 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23187 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23188 });
23189 editor.handle_input("B", window, cx);
23190 });
23191
23192 assert_eq!(
23193 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23194 "\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",
23195 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23196 );
23197
23198 multi_buffer_editor.update(cx, |editor, cx| {
23199 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23200 });
23201 assert_eq!(
23202 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23203 "\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",
23204 "After unfolding the all buffers, all original text should be displayed"
23205 );
23206}
23207
23208#[gpui::test]
23209async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23210 init_test(cx, |_| {});
23211
23212 let sample_text_1 = "1111\n2222\n3333".to_string();
23213 let sample_text_2 = "4444\n5555\n6666".to_string();
23214 let sample_text_3 = "7777\n8888\n9999".to_string();
23215
23216 let fs = FakeFs::new(cx.executor());
23217 fs.insert_tree(
23218 path!("/a"),
23219 json!({
23220 "first.rs": sample_text_1,
23221 "second.rs": sample_text_2,
23222 "third.rs": sample_text_3,
23223 }),
23224 )
23225 .await;
23226 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23227 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23228 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23229 let worktree = project.update(cx, |project, cx| {
23230 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23231 assert_eq!(worktrees.len(), 1);
23232 worktrees.pop().unwrap()
23233 });
23234 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23235
23236 let buffer_1 = project
23237 .update(cx, |project, cx| {
23238 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23239 })
23240 .await
23241 .unwrap();
23242 let buffer_2 = project
23243 .update(cx, |project, cx| {
23244 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23245 })
23246 .await
23247 .unwrap();
23248 let buffer_3 = project
23249 .update(cx, |project, cx| {
23250 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23251 })
23252 .await
23253 .unwrap();
23254
23255 let multi_buffer = cx.new(|cx| {
23256 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23257 multi_buffer.push_excerpts(
23258 buffer_1.clone(),
23259 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23260 cx,
23261 );
23262 multi_buffer.push_excerpts(
23263 buffer_2.clone(),
23264 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23265 cx,
23266 );
23267 multi_buffer.push_excerpts(
23268 buffer_3.clone(),
23269 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23270 cx,
23271 );
23272 multi_buffer
23273 });
23274
23275 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23276 Editor::new(
23277 EditorMode::full(),
23278 multi_buffer,
23279 Some(project.clone()),
23280 window,
23281 cx,
23282 )
23283 });
23284
23285 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23286 assert_eq!(
23287 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23288 full_text,
23289 );
23290
23291 multi_buffer_editor.update(cx, |editor, cx| {
23292 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23293 });
23294 assert_eq!(
23295 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23296 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23297 "After folding the first buffer, its text should not be displayed"
23298 );
23299
23300 multi_buffer_editor.update(cx, |editor, cx| {
23301 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23302 });
23303
23304 assert_eq!(
23305 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23306 "\n\n\n\n\n\n7777\n8888\n9999",
23307 "After folding the second buffer, its text should not be displayed"
23308 );
23309
23310 multi_buffer_editor.update(cx, |editor, cx| {
23311 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23312 });
23313 assert_eq!(
23314 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23315 "\n\n\n\n\n",
23316 "After folding the third buffer, its text should not be displayed"
23317 );
23318
23319 multi_buffer_editor.update(cx, |editor, cx| {
23320 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23321 });
23322 assert_eq!(
23323 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23324 "\n\n\n\n4444\n5555\n6666\n\n",
23325 "After unfolding the second buffer, its text should be displayed"
23326 );
23327
23328 multi_buffer_editor.update(cx, |editor, cx| {
23329 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23330 });
23331 assert_eq!(
23332 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23333 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23334 "After unfolding the first buffer, its text should be displayed"
23335 );
23336
23337 multi_buffer_editor.update(cx, |editor, cx| {
23338 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23339 });
23340 assert_eq!(
23341 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23342 full_text,
23343 "After unfolding all buffers, all original text should be displayed"
23344 );
23345}
23346
23347#[gpui::test]
23348async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23349 init_test(cx, |_| {});
23350
23351 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23352
23353 let fs = FakeFs::new(cx.executor());
23354 fs.insert_tree(
23355 path!("/a"),
23356 json!({
23357 "main.rs": sample_text,
23358 }),
23359 )
23360 .await;
23361 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23362 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23363 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23364 let worktree = project.update(cx, |project, cx| {
23365 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23366 assert_eq!(worktrees.len(), 1);
23367 worktrees.pop().unwrap()
23368 });
23369 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23370
23371 let buffer_1 = project
23372 .update(cx, |project, cx| {
23373 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23374 })
23375 .await
23376 .unwrap();
23377
23378 let multi_buffer = cx.new(|cx| {
23379 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23380 multi_buffer.push_excerpts(
23381 buffer_1.clone(),
23382 [ExcerptRange::new(
23383 Point::new(0, 0)
23384 ..Point::new(
23385 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23386 0,
23387 ),
23388 )],
23389 cx,
23390 );
23391 multi_buffer
23392 });
23393 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23394 Editor::new(
23395 EditorMode::full(),
23396 multi_buffer,
23397 Some(project.clone()),
23398 window,
23399 cx,
23400 )
23401 });
23402
23403 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23404 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23405 enum TestHighlight {}
23406 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23407 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23408 editor.highlight_text::<TestHighlight>(
23409 vec![highlight_range.clone()],
23410 HighlightStyle::color(Hsla::green()),
23411 cx,
23412 );
23413 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23414 s.select_ranges(Some(highlight_range))
23415 });
23416 });
23417
23418 let full_text = format!("\n\n{sample_text}");
23419 assert_eq!(
23420 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23421 full_text,
23422 );
23423}
23424
23425#[gpui::test]
23426async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23427 init_test(cx, |_| {});
23428 cx.update(|cx| {
23429 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23430 "keymaps/default-linux.json",
23431 cx,
23432 )
23433 .unwrap();
23434 cx.bind_keys(default_key_bindings);
23435 });
23436
23437 let (editor, cx) = cx.add_window_view(|window, cx| {
23438 let multi_buffer = MultiBuffer::build_multi(
23439 [
23440 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23441 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23442 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23443 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23444 ],
23445 cx,
23446 );
23447 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23448
23449 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23450 // fold all but the second buffer, so that we test navigating between two
23451 // adjacent folded buffers, as well as folded buffers at the start and
23452 // end the multibuffer
23453 editor.fold_buffer(buffer_ids[0], cx);
23454 editor.fold_buffer(buffer_ids[2], cx);
23455 editor.fold_buffer(buffer_ids[3], cx);
23456
23457 editor
23458 });
23459 cx.simulate_resize(size(px(1000.), px(1000.)));
23460
23461 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23462 cx.assert_excerpts_with_selections(indoc! {"
23463 [EXCERPT]
23464 ˇ[FOLDED]
23465 [EXCERPT]
23466 a1
23467 b1
23468 [EXCERPT]
23469 [FOLDED]
23470 [EXCERPT]
23471 [FOLDED]
23472 "
23473 });
23474 cx.simulate_keystroke("down");
23475 cx.assert_excerpts_with_selections(indoc! {"
23476 [EXCERPT]
23477 [FOLDED]
23478 [EXCERPT]
23479 ˇa1
23480 b1
23481 [EXCERPT]
23482 [FOLDED]
23483 [EXCERPT]
23484 [FOLDED]
23485 "
23486 });
23487 cx.simulate_keystroke("down");
23488 cx.assert_excerpts_with_selections(indoc! {"
23489 [EXCERPT]
23490 [FOLDED]
23491 [EXCERPT]
23492 a1
23493 ˇb1
23494 [EXCERPT]
23495 [FOLDED]
23496 [EXCERPT]
23497 [FOLDED]
23498 "
23499 });
23500 cx.simulate_keystroke("down");
23501 cx.assert_excerpts_with_selections(indoc! {"
23502 [EXCERPT]
23503 [FOLDED]
23504 [EXCERPT]
23505 a1
23506 b1
23507 ˇ[EXCERPT]
23508 [FOLDED]
23509 [EXCERPT]
23510 [FOLDED]
23511 "
23512 });
23513 cx.simulate_keystroke("down");
23514 cx.assert_excerpts_with_selections(indoc! {"
23515 [EXCERPT]
23516 [FOLDED]
23517 [EXCERPT]
23518 a1
23519 b1
23520 [EXCERPT]
23521 ˇ[FOLDED]
23522 [EXCERPT]
23523 [FOLDED]
23524 "
23525 });
23526 for _ in 0..5 {
23527 cx.simulate_keystroke("down");
23528 cx.assert_excerpts_with_selections(indoc! {"
23529 [EXCERPT]
23530 [FOLDED]
23531 [EXCERPT]
23532 a1
23533 b1
23534 [EXCERPT]
23535 [FOLDED]
23536 [EXCERPT]
23537 ˇ[FOLDED]
23538 "
23539 });
23540 }
23541
23542 cx.simulate_keystroke("up");
23543 cx.assert_excerpts_with_selections(indoc! {"
23544 [EXCERPT]
23545 [FOLDED]
23546 [EXCERPT]
23547 a1
23548 b1
23549 [EXCERPT]
23550 ˇ[FOLDED]
23551 [EXCERPT]
23552 [FOLDED]
23553 "
23554 });
23555 cx.simulate_keystroke("up");
23556 cx.assert_excerpts_with_selections(indoc! {"
23557 [EXCERPT]
23558 [FOLDED]
23559 [EXCERPT]
23560 a1
23561 b1
23562 ˇ[EXCERPT]
23563 [FOLDED]
23564 [EXCERPT]
23565 [FOLDED]
23566 "
23567 });
23568 cx.simulate_keystroke("up");
23569 cx.assert_excerpts_with_selections(indoc! {"
23570 [EXCERPT]
23571 [FOLDED]
23572 [EXCERPT]
23573 a1
23574 ˇb1
23575 [EXCERPT]
23576 [FOLDED]
23577 [EXCERPT]
23578 [FOLDED]
23579 "
23580 });
23581 cx.simulate_keystroke("up");
23582 cx.assert_excerpts_with_selections(indoc! {"
23583 [EXCERPT]
23584 [FOLDED]
23585 [EXCERPT]
23586 ˇa1
23587 b1
23588 [EXCERPT]
23589 [FOLDED]
23590 [EXCERPT]
23591 [FOLDED]
23592 "
23593 });
23594 for _ in 0..5 {
23595 cx.simulate_keystroke("up");
23596 cx.assert_excerpts_with_selections(indoc! {"
23597 [EXCERPT]
23598 ˇ[FOLDED]
23599 [EXCERPT]
23600 a1
23601 b1
23602 [EXCERPT]
23603 [FOLDED]
23604 [EXCERPT]
23605 [FOLDED]
23606 "
23607 });
23608 }
23609}
23610
23611#[gpui::test]
23612async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23613 init_test(cx, |_| {});
23614
23615 // Simple insertion
23616 assert_highlighted_edits(
23617 "Hello, world!",
23618 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23619 true,
23620 cx,
23621 |highlighted_edits, cx| {
23622 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23623 assert_eq!(highlighted_edits.highlights.len(), 1);
23624 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23625 assert_eq!(
23626 highlighted_edits.highlights[0].1.background_color,
23627 Some(cx.theme().status().created_background)
23628 );
23629 },
23630 )
23631 .await;
23632
23633 // Replacement
23634 assert_highlighted_edits(
23635 "This is a test.",
23636 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23637 false,
23638 cx,
23639 |highlighted_edits, cx| {
23640 assert_eq!(highlighted_edits.text, "That is a test.");
23641 assert_eq!(highlighted_edits.highlights.len(), 1);
23642 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23643 assert_eq!(
23644 highlighted_edits.highlights[0].1.background_color,
23645 Some(cx.theme().status().created_background)
23646 );
23647 },
23648 )
23649 .await;
23650
23651 // Multiple edits
23652 assert_highlighted_edits(
23653 "Hello, world!",
23654 vec![
23655 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23656 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23657 ],
23658 false,
23659 cx,
23660 |highlighted_edits, cx| {
23661 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23662 assert_eq!(highlighted_edits.highlights.len(), 2);
23663 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23664 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23665 assert_eq!(
23666 highlighted_edits.highlights[0].1.background_color,
23667 Some(cx.theme().status().created_background)
23668 );
23669 assert_eq!(
23670 highlighted_edits.highlights[1].1.background_color,
23671 Some(cx.theme().status().created_background)
23672 );
23673 },
23674 )
23675 .await;
23676
23677 // Multiple lines with edits
23678 assert_highlighted_edits(
23679 "First line\nSecond line\nThird line\nFourth line",
23680 vec![
23681 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23682 (
23683 Point::new(2, 0)..Point::new(2, 10),
23684 "New third line".to_string(),
23685 ),
23686 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23687 ],
23688 false,
23689 cx,
23690 |highlighted_edits, cx| {
23691 assert_eq!(
23692 highlighted_edits.text,
23693 "Second modified\nNew third line\nFourth updated line"
23694 );
23695 assert_eq!(highlighted_edits.highlights.len(), 3);
23696 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23697 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23698 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23699 for highlight in &highlighted_edits.highlights {
23700 assert_eq!(
23701 highlight.1.background_color,
23702 Some(cx.theme().status().created_background)
23703 );
23704 }
23705 },
23706 )
23707 .await;
23708}
23709
23710#[gpui::test]
23711async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23712 init_test(cx, |_| {});
23713
23714 // Deletion
23715 assert_highlighted_edits(
23716 "Hello, world!",
23717 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23718 true,
23719 cx,
23720 |highlighted_edits, cx| {
23721 assert_eq!(highlighted_edits.text, "Hello, world!");
23722 assert_eq!(highlighted_edits.highlights.len(), 1);
23723 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23724 assert_eq!(
23725 highlighted_edits.highlights[0].1.background_color,
23726 Some(cx.theme().status().deleted_background)
23727 );
23728 },
23729 )
23730 .await;
23731
23732 // Insertion
23733 assert_highlighted_edits(
23734 "Hello, world!",
23735 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23736 true,
23737 cx,
23738 |highlighted_edits, cx| {
23739 assert_eq!(highlighted_edits.highlights.len(), 1);
23740 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23741 assert_eq!(
23742 highlighted_edits.highlights[0].1.background_color,
23743 Some(cx.theme().status().created_background)
23744 );
23745 },
23746 )
23747 .await;
23748}
23749
23750async fn assert_highlighted_edits(
23751 text: &str,
23752 edits: Vec<(Range<Point>, String)>,
23753 include_deletions: bool,
23754 cx: &mut TestAppContext,
23755 assertion_fn: impl Fn(HighlightedText, &App),
23756) {
23757 let window = cx.add_window(|window, cx| {
23758 let buffer = MultiBuffer::build_simple(text, cx);
23759 Editor::new(EditorMode::full(), buffer, None, window, cx)
23760 });
23761 let cx = &mut VisualTestContext::from_window(*window, cx);
23762
23763 let (buffer, snapshot) = window
23764 .update(cx, |editor, _window, cx| {
23765 (
23766 editor.buffer().clone(),
23767 editor.buffer().read(cx).snapshot(cx),
23768 )
23769 })
23770 .unwrap();
23771
23772 let edits = edits
23773 .into_iter()
23774 .map(|(range, edit)| {
23775 (
23776 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23777 edit,
23778 )
23779 })
23780 .collect::<Vec<_>>();
23781
23782 let text_anchor_edits = edits
23783 .clone()
23784 .into_iter()
23785 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23786 .collect::<Vec<_>>();
23787
23788 let edit_preview = window
23789 .update(cx, |_, _window, cx| {
23790 buffer
23791 .read(cx)
23792 .as_singleton()
23793 .unwrap()
23794 .read(cx)
23795 .preview_edits(text_anchor_edits.into(), cx)
23796 })
23797 .unwrap()
23798 .await;
23799
23800 cx.update(|_window, cx| {
23801 let highlighted_edits = edit_prediction_edit_text(
23802 snapshot.as_singleton().unwrap().2,
23803 &edits,
23804 &edit_preview,
23805 include_deletions,
23806 cx,
23807 );
23808 assertion_fn(highlighted_edits, cx)
23809 });
23810}
23811
23812#[track_caller]
23813fn assert_breakpoint(
23814 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23815 path: &Arc<Path>,
23816 expected: Vec<(u32, Breakpoint)>,
23817) {
23818 if expected.is_empty() {
23819 assert!(!breakpoints.contains_key(path), "{}", path.display());
23820 } else {
23821 let mut breakpoint = breakpoints
23822 .get(path)
23823 .unwrap()
23824 .iter()
23825 .map(|breakpoint| {
23826 (
23827 breakpoint.row,
23828 Breakpoint {
23829 message: breakpoint.message.clone(),
23830 state: breakpoint.state,
23831 condition: breakpoint.condition.clone(),
23832 hit_condition: breakpoint.hit_condition.clone(),
23833 },
23834 )
23835 })
23836 .collect::<Vec<_>>();
23837
23838 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23839
23840 assert_eq!(expected, breakpoint);
23841 }
23842}
23843
23844fn add_log_breakpoint_at_cursor(
23845 editor: &mut Editor,
23846 log_message: &str,
23847 window: &mut Window,
23848 cx: &mut Context<Editor>,
23849) {
23850 let (anchor, bp) = editor
23851 .breakpoints_at_cursors(window, cx)
23852 .first()
23853 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23854 .unwrap_or_else(|| {
23855 let snapshot = editor.snapshot(window, cx);
23856 let cursor_position: Point =
23857 editor.selections.newest(&snapshot.display_snapshot).head();
23858
23859 let breakpoint_position = snapshot
23860 .buffer_snapshot()
23861 .anchor_before(Point::new(cursor_position.row, 0));
23862
23863 (breakpoint_position, Breakpoint::new_log(log_message))
23864 });
23865
23866 editor.edit_breakpoint_at_anchor(
23867 anchor,
23868 bp,
23869 BreakpointEditAction::EditLogMessage(log_message.into()),
23870 cx,
23871 );
23872}
23873
23874#[gpui::test]
23875async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23876 init_test(cx, |_| {});
23877
23878 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23879 let fs = FakeFs::new(cx.executor());
23880 fs.insert_tree(
23881 path!("/a"),
23882 json!({
23883 "main.rs": sample_text,
23884 }),
23885 )
23886 .await;
23887 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23888 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23889 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23890
23891 let fs = FakeFs::new(cx.executor());
23892 fs.insert_tree(
23893 path!("/a"),
23894 json!({
23895 "main.rs": sample_text,
23896 }),
23897 )
23898 .await;
23899 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23900 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23901 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23902 let worktree_id = workspace
23903 .update(cx, |workspace, _window, cx| {
23904 workspace.project().update(cx, |project, cx| {
23905 project.worktrees(cx).next().unwrap().read(cx).id()
23906 })
23907 })
23908 .unwrap();
23909
23910 let buffer = project
23911 .update(cx, |project, cx| {
23912 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23913 })
23914 .await
23915 .unwrap();
23916
23917 let (editor, cx) = cx.add_window_view(|window, cx| {
23918 Editor::new(
23919 EditorMode::full(),
23920 MultiBuffer::build_from_buffer(buffer, cx),
23921 Some(project.clone()),
23922 window,
23923 cx,
23924 )
23925 });
23926
23927 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23928 let abs_path = project.read_with(cx, |project, cx| {
23929 project
23930 .absolute_path(&project_path, cx)
23931 .map(Arc::from)
23932 .unwrap()
23933 });
23934
23935 // assert we can add breakpoint on the first line
23936 editor.update_in(cx, |editor, window, cx| {
23937 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23938 editor.move_to_end(&MoveToEnd, window, cx);
23939 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23940 });
23941
23942 let breakpoints = editor.update(cx, |editor, cx| {
23943 editor
23944 .breakpoint_store()
23945 .as_ref()
23946 .unwrap()
23947 .read(cx)
23948 .all_source_breakpoints(cx)
23949 });
23950
23951 assert_eq!(1, breakpoints.len());
23952 assert_breakpoint(
23953 &breakpoints,
23954 &abs_path,
23955 vec![
23956 (0, Breakpoint::new_standard()),
23957 (3, Breakpoint::new_standard()),
23958 ],
23959 );
23960
23961 editor.update_in(cx, |editor, window, cx| {
23962 editor.move_to_beginning(&MoveToBeginning, window, cx);
23963 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23964 });
23965
23966 let breakpoints = editor.update(cx, |editor, cx| {
23967 editor
23968 .breakpoint_store()
23969 .as_ref()
23970 .unwrap()
23971 .read(cx)
23972 .all_source_breakpoints(cx)
23973 });
23974
23975 assert_eq!(1, breakpoints.len());
23976 assert_breakpoint(
23977 &breakpoints,
23978 &abs_path,
23979 vec![(3, Breakpoint::new_standard())],
23980 );
23981
23982 editor.update_in(cx, |editor, window, cx| {
23983 editor.move_to_end(&MoveToEnd, window, cx);
23984 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23985 });
23986
23987 let breakpoints = editor.update(cx, |editor, cx| {
23988 editor
23989 .breakpoint_store()
23990 .as_ref()
23991 .unwrap()
23992 .read(cx)
23993 .all_source_breakpoints(cx)
23994 });
23995
23996 assert_eq!(0, breakpoints.len());
23997 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23998}
23999
24000#[gpui::test]
24001async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24002 init_test(cx, |_| {});
24003
24004 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24005
24006 let fs = FakeFs::new(cx.executor());
24007 fs.insert_tree(
24008 path!("/a"),
24009 json!({
24010 "main.rs": sample_text,
24011 }),
24012 )
24013 .await;
24014 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24015 let (workspace, cx) =
24016 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24017
24018 let worktree_id = workspace.update(cx, |workspace, cx| {
24019 workspace.project().update(cx, |project, cx| {
24020 project.worktrees(cx).next().unwrap().read(cx).id()
24021 })
24022 });
24023
24024 let buffer = project
24025 .update(cx, |project, cx| {
24026 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24027 })
24028 .await
24029 .unwrap();
24030
24031 let (editor, cx) = cx.add_window_view(|window, cx| {
24032 Editor::new(
24033 EditorMode::full(),
24034 MultiBuffer::build_from_buffer(buffer, cx),
24035 Some(project.clone()),
24036 window,
24037 cx,
24038 )
24039 });
24040
24041 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24042 let abs_path = project.read_with(cx, |project, cx| {
24043 project
24044 .absolute_path(&project_path, cx)
24045 .map(Arc::from)
24046 .unwrap()
24047 });
24048
24049 editor.update_in(cx, |editor, window, cx| {
24050 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24051 });
24052
24053 let breakpoints = editor.update(cx, |editor, cx| {
24054 editor
24055 .breakpoint_store()
24056 .as_ref()
24057 .unwrap()
24058 .read(cx)
24059 .all_source_breakpoints(cx)
24060 });
24061
24062 assert_breakpoint(
24063 &breakpoints,
24064 &abs_path,
24065 vec![(0, Breakpoint::new_log("hello world"))],
24066 );
24067
24068 // Removing a log message from a log breakpoint should remove it
24069 editor.update_in(cx, |editor, window, cx| {
24070 add_log_breakpoint_at_cursor(editor, "", window, cx);
24071 });
24072
24073 let breakpoints = editor.update(cx, |editor, cx| {
24074 editor
24075 .breakpoint_store()
24076 .as_ref()
24077 .unwrap()
24078 .read(cx)
24079 .all_source_breakpoints(cx)
24080 });
24081
24082 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24083
24084 editor.update_in(cx, |editor, window, cx| {
24085 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24086 editor.move_to_end(&MoveToEnd, window, cx);
24087 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24088 // Not adding a log message to a standard breakpoint shouldn't remove it
24089 add_log_breakpoint_at_cursor(editor, "", window, cx);
24090 });
24091
24092 let breakpoints = editor.update(cx, |editor, cx| {
24093 editor
24094 .breakpoint_store()
24095 .as_ref()
24096 .unwrap()
24097 .read(cx)
24098 .all_source_breakpoints(cx)
24099 });
24100
24101 assert_breakpoint(
24102 &breakpoints,
24103 &abs_path,
24104 vec![
24105 (0, Breakpoint::new_standard()),
24106 (3, Breakpoint::new_standard()),
24107 ],
24108 );
24109
24110 editor.update_in(cx, |editor, window, cx| {
24111 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24112 });
24113
24114 let breakpoints = editor.update(cx, |editor, cx| {
24115 editor
24116 .breakpoint_store()
24117 .as_ref()
24118 .unwrap()
24119 .read(cx)
24120 .all_source_breakpoints(cx)
24121 });
24122
24123 assert_breakpoint(
24124 &breakpoints,
24125 &abs_path,
24126 vec![
24127 (0, Breakpoint::new_standard()),
24128 (3, Breakpoint::new_log("hello world")),
24129 ],
24130 );
24131
24132 editor.update_in(cx, |editor, window, cx| {
24133 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24134 });
24135
24136 let breakpoints = editor.update(cx, |editor, cx| {
24137 editor
24138 .breakpoint_store()
24139 .as_ref()
24140 .unwrap()
24141 .read(cx)
24142 .all_source_breakpoints(cx)
24143 });
24144
24145 assert_breakpoint(
24146 &breakpoints,
24147 &abs_path,
24148 vec![
24149 (0, Breakpoint::new_standard()),
24150 (3, Breakpoint::new_log("hello Earth!!")),
24151 ],
24152 );
24153}
24154
24155/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24156/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24157/// or when breakpoints were placed out of order. This tests for a regression too
24158#[gpui::test]
24159async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24160 init_test(cx, |_| {});
24161
24162 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24163 let fs = FakeFs::new(cx.executor());
24164 fs.insert_tree(
24165 path!("/a"),
24166 json!({
24167 "main.rs": sample_text,
24168 }),
24169 )
24170 .await;
24171 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24172 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24173 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24174
24175 let fs = FakeFs::new(cx.executor());
24176 fs.insert_tree(
24177 path!("/a"),
24178 json!({
24179 "main.rs": sample_text,
24180 }),
24181 )
24182 .await;
24183 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24184 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24185 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24186 let worktree_id = workspace
24187 .update(cx, |workspace, _window, cx| {
24188 workspace.project().update(cx, |project, cx| {
24189 project.worktrees(cx).next().unwrap().read(cx).id()
24190 })
24191 })
24192 .unwrap();
24193
24194 let buffer = project
24195 .update(cx, |project, cx| {
24196 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24197 })
24198 .await
24199 .unwrap();
24200
24201 let (editor, cx) = cx.add_window_view(|window, cx| {
24202 Editor::new(
24203 EditorMode::full(),
24204 MultiBuffer::build_from_buffer(buffer, cx),
24205 Some(project.clone()),
24206 window,
24207 cx,
24208 )
24209 });
24210
24211 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24212 let abs_path = project.read_with(cx, |project, cx| {
24213 project
24214 .absolute_path(&project_path, cx)
24215 .map(Arc::from)
24216 .unwrap()
24217 });
24218
24219 // assert we can add breakpoint on the first line
24220 editor.update_in(cx, |editor, window, cx| {
24221 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24222 editor.move_to_end(&MoveToEnd, window, cx);
24223 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24224 editor.move_up(&MoveUp, window, cx);
24225 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24226 });
24227
24228 let breakpoints = editor.update(cx, |editor, cx| {
24229 editor
24230 .breakpoint_store()
24231 .as_ref()
24232 .unwrap()
24233 .read(cx)
24234 .all_source_breakpoints(cx)
24235 });
24236
24237 assert_eq!(1, breakpoints.len());
24238 assert_breakpoint(
24239 &breakpoints,
24240 &abs_path,
24241 vec![
24242 (0, Breakpoint::new_standard()),
24243 (2, Breakpoint::new_standard()),
24244 (3, Breakpoint::new_standard()),
24245 ],
24246 );
24247
24248 editor.update_in(cx, |editor, window, cx| {
24249 editor.move_to_beginning(&MoveToBeginning, window, cx);
24250 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24251 editor.move_to_end(&MoveToEnd, window, cx);
24252 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24253 // Disabling a breakpoint that doesn't exist should do nothing
24254 editor.move_up(&MoveUp, window, cx);
24255 editor.move_up(&MoveUp, window, cx);
24256 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24257 });
24258
24259 let breakpoints = editor.update(cx, |editor, cx| {
24260 editor
24261 .breakpoint_store()
24262 .as_ref()
24263 .unwrap()
24264 .read(cx)
24265 .all_source_breakpoints(cx)
24266 });
24267
24268 let disable_breakpoint = {
24269 let mut bp = Breakpoint::new_standard();
24270 bp.state = BreakpointState::Disabled;
24271 bp
24272 };
24273
24274 assert_eq!(1, breakpoints.len());
24275 assert_breakpoint(
24276 &breakpoints,
24277 &abs_path,
24278 vec![
24279 (0, disable_breakpoint.clone()),
24280 (2, Breakpoint::new_standard()),
24281 (3, disable_breakpoint.clone()),
24282 ],
24283 );
24284
24285 editor.update_in(cx, |editor, window, cx| {
24286 editor.move_to_beginning(&MoveToBeginning, window, cx);
24287 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24288 editor.move_to_end(&MoveToEnd, window, cx);
24289 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24290 editor.move_up(&MoveUp, window, cx);
24291 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24292 });
24293
24294 let breakpoints = editor.update(cx, |editor, cx| {
24295 editor
24296 .breakpoint_store()
24297 .as_ref()
24298 .unwrap()
24299 .read(cx)
24300 .all_source_breakpoints(cx)
24301 });
24302
24303 assert_eq!(1, breakpoints.len());
24304 assert_breakpoint(
24305 &breakpoints,
24306 &abs_path,
24307 vec![
24308 (0, Breakpoint::new_standard()),
24309 (2, disable_breakpoint),
24310 (3, Breakpoint::new_standard()),
24311 ],
24312 );
24313}
24314
24315#[gpui::test]
24316async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24317 init_test(cx, |_| {});
24318 let capabilities = lsp::ServerCapabilities {
24319 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24320 prepare_provider: Some(true),
24321 work_done_progress_options: Default::default(),
24322 })),
24323 ..Default::default()
24324 };
24325 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24326
24327 cx.set_state(indoc! {"
24328 struct Fˇoo {}
24329 "});
24330
24331 cx.update_editor(|editor, _, cx| {
24332 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24333 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24334 editor.highlight_background::<DocumentHighlightRead>(
24335 &[highlight_range],
24336 |_, theme| theme.colors().editor_document_highlight_read_background,
24337 cx,
24338 );
24339 });
24340
24341 let mut prepare_rename_handler = cx
24342 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24343 move |_, _, _| async move {
24344 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24345 start: lsp::Position {
24346 line: 0,
24347 character: 7,
24348 },
24349 end: lsp::Position {
24350 line: 0,
24351 character: 10,
24352 },
24353 })))
24354 },
24355 );
24356 let prepare_rename_task = cx
24357 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24358 .expect("Prepare rename was not started");
24359 prepare_rename_handler.next().await.unwrap();
24360 prepare_rename_task.await.expect("Prepare rename failed");
24361
24362 let mut rename_handler =
24363 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24364 let edit = lsp::TextEdit {
24365 range: lsp::Range {
24366 start: lsp::Position {
24367 line: 0,
24368 character: 7,
24369 },
24370 end: lsp::Position {
24371 line: 0,
24372 character: 10,
24373 },
24374 },
24375 new_text: "FooRenamed".to_string(),
24376 };
24377 Ok(Some(lsp::WorkspaceEdit::new(
24378 // Specify the same edit twice
24379 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24380 )))
24381 });
24382 let rename_task = cx
24383 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24384 .expect("Confirm rename was not started");
24385 rename_handler.next().await.unwrap();
24386 rename_task.await.expect("Confirm rename failed");
24387 cx.run_until_parked();
24388
24389 // Despite two edits, only one is actually applied as those are identical
24390 cx.assert_editor_state(indoc! {"
24391 struct FooRenamedˇ {}
24392 "});
24393}
24394
24395#[gpui::test]
24396async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24397 init_test(cx, |_| {});
24398 // These capabilities indicate that the server does not support prepare rename.
24399 let capabilities = lsp::ServerCapabilities {
24400 rename_provider: Some(lsp::OneOf::Left(true)),
24401 ..Default::default()
24402 };
24403 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24404
24405 cx.set_state(indoc! {"
24406 struct Fˇoo {}
24407 "});
24408
24409 cx.update_editor(|editor, _window, cx| {
24410 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24411 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24412 editor.highlight_background::<DocumentHighlightRead>(
24413 &[highlight_range],
24414 |_, theme| theme.colors().editor_document_highlight_read_background,
24415 cx,
24416 );
24417 });
24418
24419 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24420 .expect("Prepare rename was not started")
24421 .await
24422 .expect("Prepare rename failed");
24423
24424 let mut rename_handler =
24425 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24426 let edit = lsp::TextEdit {
24427 range: lsp::Range {
24428 start: lsp::Position {
24429 line: 0,
24430 character: 7,
24431 },
24432 end: lsp::Position {
24433 line: 0,
24434 character: 10,
24435 },
24436 },
24437 new_text: "FooRenamed".to_string(),
24438 };
24439 Ok(Some(lsp::WorkspaceEdit::new(
24440 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24441 )))
24442 });
24443 let rename_task = cx
24444 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24445 .expect("Confirm rename was not started");
24446 rename_handler.next().await.unwrap();
24447 rename_task.await.expect("Confirm rename failed");
24448 cx.run_until_parked();
24449
24450 // Correct range is renamed, as `surrounding_word` is used to find it.
24451 cx.assert_editor_state(indoc! {"
24452 struct FooRenamedˇ {}
24453 "});
24454}
24455
24456#[gpui::test]
24457async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24458 init_test(cx, |_| {});
24459 let mut cx = EditorTestContext::new(cx).await;
24460
24461 let language = Arc::new(
24462 Language::new(
24463 LanguageConfig::default(),
24464 Some(tree_sitter_html::LANGUAGE.into()),
24465 )
24466 .with_brackets_query(
24467 r#"
24468 ("<" @open "/>" @close)
24469 ("</" @open ">" @close)
24470 ("<" @open ">" @close)
24471 ("\"" @open "\"" @close)
24472 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24473 "#,
24474 )
24475 .unwrap(),
24476 );
24477 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24478
24479 cx.set_state(indoc! {"
24480 <span>ˇ</span>
24481 "});
24482 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24483 cx.assert_editor_state(indoc! {"
24484 <span>
24485 ˇ
24486 </span>
24487 "});
24488
24489 cx.set_state(indoc! {"
24490 <span><span></span>ˇ</span>
24491 "});
24492 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24493 cx.assert_editor_state(indoc! {"
24494 <span><span></span>
24495 ˇ</span>
24496 "});
24497
24498 cx.set_state(indoc! {"
24499 <span>ˇ
24500 </span>
24501 "});
24502 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24503 cx.assert_editor_state(indoc! {"
24504 <span>
24505 ˇ
24506 </span>
24507 "});
24508}
24509
24510#[gpui::test(iterations = 10)]
24511async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24512 init_test(cx, |_| {});
24513
24514 let fs = FakeFs::new(cx.executor());
24515 fs.insert_tree(
24516 path!("/dir"),
24517 json!({
24518 "a.ts": "a",
24519 }),
24520 )
24521 .await;
24522
24523 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24524 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24525 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24526
24527 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24528 language_registry.add(Arc::new(Language::new(
24529 LanguageConfig {
24530 name: "TypeScript".into(),
24531 matcher: LanguageMatcher {
24532 path_suffixes: vec!["ts".to_string()],
24533 ..Default::default()
24534 },
24535 ..Default::default()
24536 },
24537 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24538 )));
24539 let mut fake_language_servers = language_registry.register_fake_lsp(
24540 "TypeScript",
24541 FakeLspAdapter {
24542 capabilities: lsp::ServerCapabilities {
24543 code_lens_provider: Some(lsp::CodeLensOptions {
24544 resolve_provider: Some(true),
24545 }),
24546 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24547 commands: vec!["_the/command".to_string()],
24548 ..lsp::ExecuteCommandOptions::default()
24549 }),
24550 ..lsp::ServerCapabilities::default()
24551 },
24552 ..FakeLspAdapter::default()
24553 },
24554 );
24555
24556 let editor = workspace
24557 .update(cx, |workspace, window, cx| {
24558 workspace.open_abs_path(
24559 PathBuf::from(path!("/dir/a.ts")),
24560 OpenOptions::default(),
24561 window,
24562 cx,
24563 )
24564 })
24565 .unwrap()
24566 .await
24567 .unwrap()
24568 .downcast::<Editor>()
24569 .unwrap();
24570 cx.executor().run_until_parked();
24571
24572 let fake_server = fake_language_servers.next().await.unwrap();
24573
24574 let buffer = editor.update(cx, |editor, cx| {
24575 editor
24576 .buffer()
24577 .read(cx)
24578 .as_singleton()
24579 .expect("have opened a single file by path")
24580 });
24581
24582 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24583 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24584 drop(buffer_snapshot);
24585 let actions = cx
24586 .update_window(*workspace, |_, window, cx| {
24587 project.code_actions(&buffer, anchor..anchor, window, cx)
24588 })
24589 .unwrap();
24590
24591 fake_server
24592 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24593 Ok(Some(vec![
24594 lsp::CodeLens {
24595 range: lsp::Range::default(),
24596 command: Some(lsp::Command {
24597 title: "Code lens command".to_owned(),
24598 command: "_the/command".to_owned(),
24599 arguments: None,
24600 }),
24601 data: None,
24602 },
24603 lsp::CodeLens {
24604 range: lsp::Range::default(),
24605 command: Some(lsp::Command {
24606 title: "Command not in capabilities".to_owned(),
24607 command: "not in capabilities".to_owned(),
24608 arguments: None,
24609 }),
24610 data: None,
24611 },
24612 lsp::CodeLens {
24613 range: lsp::Range {
24614 start: lsp::Position {
24615 line: 1,
24616 character: 1,
24617 },
24618 end: lsp::Position {
24619 line: 1,
24620 character: 1,
24621 },
24622 },
24623 command: Some(lsp::Command {
24624 title: "Command not in range".to_owned(),
24625 command: "_the/command".to_owned(),
24626 arguments: None,
24627 }),
24628 data: None,
24629 },
24630 ]))
24631 })
24632 .next()
24633 .await;
24634
24635 let actions = actions.await.unwrap();
24636 assert_eq!(
24637 actions.len(),
24638 1,
24639 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24640 );
24641 let action = actions[0].clone();
24642 let apply = project.update(cx, |project, cx| {
24643 project.apply_code_action(buffer.clone(), action, true, cx)
24644 });
24645
24646 // Resolving the code action does not populate its edits. In absence of
24647 // edits, we must execute the given command.
24648 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24649 |mut lens, _| async move {
24650 let lens_command = lens.command.as_mut().expect("should have a command");
24651 assert_eq!(lens_command.title, "Code lens command");
24652 lens_command.arguments = Some(vec![json!("the-argument")]);
24653 Ok(lens)
24654 },
24655 );
24656
24657 // While executing the command, the language server sends the editor
24658 // a `workspaceEdit` request.
24659 fake_server
24660 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24661 let fake = fake_server.clone();
24662 move |params, _| {
24663 assert_eq!(params.command, "_the/command");
24664 let fake = fake.clone();
24665 async move {
24666 fake.server
24667 .request::<lsp::request::ApplyWorkspaceEdit>(
24668 lsp::ApplyWorkspaceEditParams {
24669 label: None,
24670 edit: lsp::WorkspaceEdit {
24671 changes: Some(
24672 [(
24673 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24674 vec![lsp::TextEdit {
24675 range: lsp::Range::new(
24676 lsp::Position::new(0, 0),
24677 lsp::Position::new(0, 0),
24678 ),
24679 new_text: "X".into(),
24680 }],
24681 )]
24682 .into_iter()
24683 .collect(),
24684 ),
24685 ..lsp::WorkspaceEdit::default()
24686 },
24687 },
24688 )
24689 .await
24690 .into_response()
24691 .unwrap();
24692 Ok(Some(json!(null)))
24693 }
24694 }
24695 })
24696 .next()
24697 .await;
24698
24699 // Applying the code lens command returns a project transaction containing the edits
24700 // sent by the language server in its `workspaceEdit` request.
24701 let transaction = apply.await.unwrap();
24702 assert!(transaction.0.contains_key(&buffer));
24703 buffer.update(cx, |buffer, cx| {
24704 assert_eq!(buffer.text(), "Xa");
24705 buffer.undo(cx);
24706 assert_eq!(buffer.text(), "a");
24707 });
24708
24709 let actions_after_edits = cx
24710 .update_window(*workspace, |_, window, cx| {
24711 project.code_actions(&buffer, anchor..anchor, window, cx)
24712 })
24713 .unwrap()
24714 .await
24715 .unwrap();
24716 assert_eq!(
24717 actions, actions_after_edits,
24718 "For the same selection, same code lens actions should be returned"
24719 );
24720
24721 let _responses =
24722 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24723 panic!("No more code lens requests are expected");
24724 });
24725 editor.update_in(cx, |editor, window, cx| {
24726 editor.select_all(&SelectAll, window, cx);
24727 });
24728 cx.executor().run_until_parked();
24729 let new_actions = cx
24730 .update_window(*workspace, |_, window, cx| {
24731 project.code_actions(&buffer, anchor..anchor, window, cx)
24732 })
24733 .unwrap()
24734 .await
24735 .unwrap();
24736 assert_eq!(
24737 actions, new_actions,
24738 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24739 );
24740}
24741
24742#[gpui::test]
24743async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24744 init_test(cx, |_| {});
24745
24746 let fs = FakeFs::new(cx.executor());
24747 let main_text = r#"fn main() {
24748println!("1");
24749println!("2");
24750println!("3");
24751println!("4");
24752println!("5");
24753}"#;
24754 let lib_text = "mod foo {}";
24755 fs.insert_tree(
24756 path!("/a"),
24757 json!({
24758 "lib.rs": lib_text,
24759 "main.rs": main_text,
24760 }),
24761 )
24762 .await;
24763
24764 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24765 let (workspace, cx) =
24766 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24767 let worktree_id = workspace.update(cx, |workspace, cx| {
24768 workspace.project().update(cx, |project, cx| {
24769 project.worktrees(cx).next().unwrap().read(cx).id()
24770 })
24771 });
24772
24773 let expected_ranges = vec![
24774 Point::new(0, 0)..Point::new(0, 0),
24775 Point::new(1, 0)..Point::new(1, 1),
24776 Point::new(2, 0)..Point::new(2, 2),
24777 Point::new(3, 0)..Point::new(3, 3),
24778 ];
24779
24780 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24781 let editor_1 = workspace
24782 .update_in(cx, |workspace, window, cx| {
24783 workspace.open_path(
24784 (worktree_id, rel_path("main.rs")),
24785 Some(pane_1.downgrade()),
24786 true,
24787 window,
24788 cx,
24789 )
24790 })
24791 .unwrap()
24792 .await
24793 .downcast::<Editor>()
24794 .unwrap();
24795 pane_1.update(cx, |pane, cx| {
24796 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24797 open_editor.update(cx, |editor, cx| {
24798 assert_eq!(
24799 editor.display_text(cx),
24800 main_text,
24801 "Original main.rs text on initial open",
24802 );
24803 assert_eq!(
24804 editor
24805 .selections
24806 .all::<Point>(&editor.display_snapshot(cx))
24807 .into_iter()
24808 .map(|s| s.range())
24809 .collect::<Vec<_>>(),
24810 vec![Point::zero()..Point::zero()],
24811 "Default selections on initial open",
24812 );
24813 })
24814 });
24815 editor_1.update_in(cx, |editor, window, cx| {
24816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24817 s.select_ranges(expected_ranges.clone());
24818 });
24819 });
24820
24821 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24822 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24823 });
24824 let editor_2 = workspace
24825 .update_in(cx, |workspace, window, cx| {
24826 workspace.open_path(
24827 (worktree_id, rel_path("main.rs")),
24828 Some(pane_2.downgrade()),
24829 true,
24830 window,
24831 cx,
24832 )
24833 })
24834 .unwrap()
24835 .await
24836 .downcast::<Editor>()
24837 .unwrap();
24838 pane_2.update(cx, |pane, cx| {
24839 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24840 open_editor.update(cx, |editor, cx| {
24841 assert_eq!(
24842 editor.display_text(cx),
24843 main_text,
24844 "Original main.rs text on initial open in another panel",
24845 );
24846 assert_eq!(
24847 editor
24848 .selections
24849 .all::<Point>(&editor.display_snapshot(cx))
24850 .into_iter()
24851 .map(|s| s.range())
24852 .collect::<Vec<_>>(),
24853 vec![Point::zero()..Point::zero()],
24854 "Default selections on initial open in another panel",
24855 );
24856 })
24857 });
24858
24859 editor_2.update_in(cx, |editor, window, cx| {
24860 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24861 });
24862
24863 let _other_editor_1 = workspace
24864 .update_in(cx, |workspace, window, cx| {
24865 workspace.open_path(
24866 (worktree_id, rel_path("lib.rs")),
24867 Some(pane_1.downgrade()),
24868 true,
24869 window,
24870 cx,
24871 )
24872 })
24873 .unwrap()
24874 .await
24875 .downcast::<Editor>()
24876 .unwrap();
24877 pane_1
24878 .update_in(cx, |pane, window, cx| {
24879 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24880 })
24881 .await
24882 .unwrap();
24883 drop(editor_1);
24884 pane_1.update(cx, |pane, cx| {
24885 pane.active_item()
24886 .unwrap()
24887 .downcast::<Editor>()
24888 .unwrap()
24889 .update(cx, |editor, cx| {
24890 assert_eq!(
24891 editor.display_text(cx),
24892 lib_text,
24893 "Other file should be open and active",
24894 );
24895 });
24896 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24897 });
24898
24899 let _other_editor_2 = workspace
24900 .update_in(cx, |workspace, window, cx| {
24901 workspace.open_path(
24902 (worktree_id, rel_path("lib.rs")),
24903 Some(pane_2.downgrade()),
24904 true,
24905 window,
24906 cx,
24907 )
24908 })
24909 .unwrap()
24910 .await
24911 .downcast::<Editor>()
24912 .unwrap();
24913 pane_2
24914 .update_in(cx, |pane, window, cx| {
24915 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24916 })
24917 .await
24918 .unwrap();
24919 drop(editor_2);
24920 pane_2.update(cx, |pane, cx| {
24921 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24922 open_editor.update(cx, |editor, cx| {
24923 assert_eq!(
24924 editor.display_text(cx),
24925 lib_text,
24926 "Other file should be open and active in another panel too",
24927 );
24928 });
24929 assert_eq!(
24930 pane.items().count(),
24931 1,
24932 "No other editors should be open in another pane",
24933 );
24934 });
24935
24936 let _editor_1_reopened = workspace
24937 .update_in(cx, |workspace, window, cx| {
24938 workspace.open_path(
24939 (worktree_id, rel_path("main.rs")),
24940 Some(pane_1.downgrade()),
24941 true,
24942 window,
24943 cx,
24944 )
24945 })
24946 .unwrap()
24947 .await
24948 .downcast::<Editor>()
24949 .unwrap();
24950 let _editor_2_reopened = workspace
24951 .update_in(cx, |workspace, window, cx| {
24952 workspace.open_path(
24953 (worktree_id, rel_path("main.rs")),
24954 Some(pane_2.downgrade()),
24955 true,
24956 window,
24957 cx,
24958 )
24959 })
24960 .unwrap()
24961 .await
24962 .downcast::<Editor>()
24963 .unwrap();
24964 pane_1.update(cx, |pane, cx| {
24965 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24966 open_editor.update(cx, |editor, cx| {
24967 assert_eq!(
24968 editor.display_text(cx),
24969 main_text,
24970 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24971 );
24972 assert_eq!(
24973 editor
24974 .selections
24975 .all::<Point>(&editor.display_snapshot(cx))
24976 .into_iter()
24977 .map(|s| s.range())
24978 .collect::<Vec<_>>(),
24979 expected_ranges,
24980 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24981 );
24982 })
24983 });
24984 pane_2.update(cx, |pane, cx| {
24985 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24986 open_editor.update(cx, |editor, cx| {
24987 assert_eq!(
24988 editor.display_text(cx),
24989 r#"fn main() {
24990⋯rintln!("1");
24991⋯intln!("2");
24992⋯ntln!("3");
24993println!("4");
24994println!("5");
24995}"#,
24996 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24997 );
24998 assert_eq!(
24999 editor
25000 .selections
25001 .all::<Point>(&editor.display_snapshot(cx))
25002 .into_iter()
25003 .map(|s| s.range())
25004 .collect::<Vec<_>>(),
25005 vec![Point::zero()..Point::zero()],
25006 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25007 );
25008 })
25009 });
25010}
25011
25012#[gpui::test]
25013async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25014 init_test(cx, |_| {});
25015
25016 let fs = FakeFs::new(cx.executor());
25017 let main_text = r#"fn main() {
25018println!("1");
25019println!("2");
25020println!("3");
25021println!("4");
25022println!("5");
25023}"#;
25024 let lib_text = "mod foo {}";
25025 fs.insert_tree(
25026 path!("/a"),
25027 json!({
25028 "lib.rs": lib_text,
25029 "main.rs": main_text,
25030 }),
25031 )
25032 .await;
25033
25034 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25035 let (workspace, cx) =
25036 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25037 let worktree_id = workspace.update(cx, |workspace, cx| {
25038 workspace.project().update(cx, |project, cx| {
25039 project.worktrees(cx).next().unwrap().read(cx).id()
25040 })
25041 });
25042
25043 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25044 let editor = workspace
25045 .update_in(cx, |workspace, window, cx| {
25046 workspace.open_path(
25047 (worktree_id, rel_path("main.rs")),
25048 Some(pane.downgrade()),
25049 true,
25050 window,
25051 cx,
25052 )
25053 })
25054 .unwrap()
25055 .await
25056 .downcast::<Editor>()
25057 .unwrap();
25058 pane.update(cx, |pane, cx| {
25059 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25060 open_editor.update(cx, |editor, cx| {
25061 assert_eq!(
25062 editor.display_text(cx),
25063 main_text,
25064 "Original main.rs text on initial open",
25065 );
25066 })
25067 });
25068 editor.update_in(cx, |editor, window, cx| {
25069 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25070 });
25071
25072 cx.update_global(|store: &mut SettingsStore, cx| {
25073 store.update_user_settings(cx, |s| {
25074 s.workspace.restore_on_file_reopen = Some(false);
25075 });
25076 });
25077 editor.update_in(cx, |editor, window, cx| {
25078 editor.fold_ranges(
25079 vec![
25080 Point::new(1, 0)..Point::new(1, 1),
25081 Point::new(2, 0)..Point::new(2, 2),
25082 Point::new(3, 0)..Point::new(3, 3),
25083 ],
25084 false,
25085 window,
25086 cx,
25087 );
25088 });
25089 pane.update_in(cx, |pane, window, cx| {
25090 pane.close_all_items(&CloseAllItems::default(), window, cx)
25091 })
25092 .await
25093 .unwrap();
25094 pane.update(cx, |pane, _| {
25095 assert!(pane.active_item().is_none());
25096 });
25097 cx.update_global(|store: &mut SettingsStore, cx| {
25098 store.update_user_settings(cx, |s| {
25099 s.workspace.restore_on_file_reopen = Some(true);
25100 });
25101 });
25102
25103 let _editor_reopened = workspace
25104 .update_in(cx, |workspace, window, cx| {
25105 workspace.open_path(
25106 (worktree_id, rel_path("main.rs")),
25107 Some(pane.downgrade()),
25108 true,
25109 window,
25110 cx,
25111 )
25112 })
25113 .unwrap()
25114 .await
25115 .downcast::<Editor>()
25116 .unwrap();
25117 pane.update(cx, |pane, cx| {
25118 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25119 open_editor.update(cx, |editor, cx| {
25120 assert_eq!(
25121 editor.display_text(cx),
25122 main_text,
25123 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25124 );
25125 })
25126 });
25127}
25128
25129#[gpui::test]
25130async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25131 struct EmptyModalView {
25132 focus_handle: gpui::FocusHandle,
25133 }
25134 impl EventEmitter<DismissEvent> for EmptyModalView {}
25135 impl Render for EmptyModalView {
25136 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25137 div()
25138 }
25139 }
25140 impl Focusable for EmptyModalView {
25141 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25142 self.focus_handle.clone()
25143 }
25144 }
25145 impl workspace::ModalView for EmptyModalView {}
25146 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25147 EmptyModalView {
25148 focus_handle: cx.focus_handle(),
25149 }
25150 }
25151
25152 init_test(cx, |_| {});
25153
25154 let fs = FakeFs::new(cx.executor());
25155 let project = Project::test(fs, [], cx).await;
25156 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25157 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25158 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25159 let editor = cx.new_window_entity(|window, cx| {
25160 Editor::new(
25161 EditorMode::full(),
25162 buffer,
25163 Some(project.clone()),
25164 window,
25165 cx,
25166 )
25167 });
25168 workspace
25169 .update(cx, |workspace, window, cx| {
25170 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25171 })
25172 .unwrap();
25173 editor.update_in(cx, |editor, window, cx| {
25174 editor.open_context_menu(&OpenContextMenu, window, cx);
25175 assert!(editor.mouse_context_menu.is_some());
25176 });
25177 workspace
25178 .update(cx, |workspace, window, cx| {
25179 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25180 })
25181 .unwrap();
25182 cx.read(|cx| {
25183 assert!(editor.read(cx).mouse_context_menu.is_none());
25184 });
25185}
25186
25187fn set_linked_edit_ranges(
25188 opening: (Point, Point),
25189 closing: (Point, Point),
25190 editor: &mut Editor,
25191 cx: &mut Context<Editor>,
25192) {
25193 let Some((buffer, _)) = editor
25194 .buffer
25195 .read(cx)
25196 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25197 else {
25198 panic!("Failed to get buffer for selection position");
25199 };
25200 let buffer = buffer.read(cx);
25201 let buffer_id = buffer.remote_id();
25202 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25203 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25204 let mut linked_ranges = HashMap::default();
25205 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25206 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25207}
25208
25209#[gpui::test]
25210async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25211 init_test(cx, |_| {});
25212
25213 let fs = FakeFs::new(cx.executor());
25214 fs.insert_file(path!("/file.html"), Default::default())
25215 .await;
25216
25217 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25218
25219 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25220 let html_language = Arc::new(Language::new(
25221 LanguageConfig {
25222 name: "HTML".into(),
25223 matcher: LanguageMatcher {
25224 path_suffixes: vec!["html".to_string()],
25225 ..LanguageMatcher::default()
25226 },
25227 brackets: BracketPairConfig {
25228 pairs: vec![BracketPair {
25229 start: "<".into(),
25230 end: ">".into(),
25231 close: true,
25232 ..Default::default()
25233 }],
25234 ..Default::default()
25235 },
25236 ..Default::default()
25237 },
25238 Some(tree_sitter_html::LANGUAGE.into()),
25239 ));
25240 language_registry.add(html_language);
25241 let mut fake_servers = language_registry.register_fake_lsp(
25242 "HTML",
25243 FakeLspAdapter {
25244 capabilities: lsp::ServerCapabilities {
25245 completion_provider: Some(lsp::CompletionOptions {
25246 resolve_provider: Some(true),
25247 ..Default::default()
25248 }),
25249 ..Default::default()
25250 },
25251 ..Default::default()
25252 },
25253 );
25254
25255 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25256 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25257
25258 let worktree_id = workspace
25259 .update(cx, |workspace, _window, cx| {
25260 workspace.project().update(cx, |project, cx| {
25261 project.worktrees(cx).next().unwrap().read(cx).id()
25262 })
25263 })
25264 .unwrap();
25265 project
25266 .update(cx, |project, cx| {
25267 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25268 })
25269 .await
25270 .unwrap();
25271 let editor = workspace
25272 .update(cx, |workspace, window, cx| {
25273 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25274 })
25275 .unwrap()
25276 .await
25277 .unwrap()
25278 .downcast::<Editor>()
25279 .unwrap();
25280
25281 let fake_server = fake_servers.next().await.unwrap();
25282 editor.update_in(cx, |editor, window, cx| {
25283 editor.set_text("<ad></ad>", window, cx);
25284 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25285 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25286 });
25287 set_linked_edit_ranges(
25288 (Point::new(0, 1), Point::new(0, 3)),
25289 (Point::new(0, 6), Point::new(0, 8)),
25290 editor,
25291 cx,
25292 );
25293 });
25294 let mut completion_handle =
25295 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25296 Ok(Some(lsp::CompletionResponse::Array(vec![
25297 lsp::CompletionItem {
25298 label: "head".to_string(),
25299 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25300 lsp::InsertReplaceEdit {
25301 new_text: "head".to_string(),
25302 insert: lsp::Range::new(
25303 lsp::Position::new(0, 1),
25304 lsp::Position::new(0, 3),
25305 ),
25306 replace: lsp::Range::new(
25307 lsp::Position::new(0, 1),
25308 lsp::Position::new(0, 3),
25309 ),
25310 },
25311 )),
25312 ..Default::default()
25313 },
25314 ])))
25315 });
25316 editor.update_in(cx, |editor, window, cx| {
25317 editor.show_completions(&ShowCompletions, window, cx);
25318 });
25319 cx.run_until_parked();
25320 completion_handle.next().await.unwrap();
25321 editor.update(cx, |editor, _| {
25322 assert!(
25323 editor.context_menu_visible(),
25324 "Completion menu should be visible"
25325 );
25326 });
25327 editor.update_in(cx, |editor, window, cx| {
25328 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25329 });
25330 cx.executor().run_until_parked();
25331 editor.update(cx, |editor, cx| {
25332 assert_eq!(editor.text(cx), "<head></head>");
25333 });
25334}
25335
25336#[gpui::test]
25337async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25338 init_test(cx, |_| {});
25339
25340 let mut cx = EditorTestContext::new(cx).await;
25341 let language = Arc::new(Language::new(
25342 LanguageConfig {
25343 name: "TSX".into(),
25344 matcher: LanguageMatcher {
25345 path_suffixes: vec!["tsx".to_string()],
25346 ..LanguageMatcher::default()
25347 },
25348 brackets: BracketPairConfig {
25349 pairs: vec![BracketPair {
25350 start: "<".into(),
25351 end: ">".into(),
25352 close: true,
25353 ..Default::default()
25354 }],
25355 ..Default::default()
25356 },
25357 linked_edit_characters: HashSet::from_iter(['.']),
25358 ..Default::default()
25359 },
25360 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25361 ));
25362 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25363
25364 // Test typing > does not extend linked pair
25365 cx.set_state("<divˇ<div></div>");
25366 cx.update_editor(|editor, _, cx| {
25367 set_linked_edit_ranges(
25368 (Point::new(0, 1), Point::new(0, 4)),
25369 (Point::new(0, 11), Point::new(0, 14)),
25370 editor,
25371 cx,
25372 );
25373 });
25374 cx.update_editor(|editor, window, cx| {
25375 editor.handle_input(">", window, cx);
25376 });
25377 cx.assert_editor_state("<div>ˇ<div></div>");
25378
25379 // Test typing . do extend linked pair
25380 cx.set_state("<Animatedˇ></Animated>");
25381 cx.update_editor(|editor, _, cx| {
25382 set_linked_edit_ranges(
25383 (Point::new(0, 1), Point::new(0, 9)),
25384 (Point::new(0, 12), Point::new(0, 20)),
25385 editor,
25386 cx,
25387 );
25388 });
25389 cx.update_editor(|editor, window, cx| {
25390 editor.handle_input(".", window, cx);
25391 });
25392 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25393 cx.update_editor(|editor, _, cx| {
25394 set_linked_edit_ranges(
25395 (Point::new(0, 1), Point::new(0, 10)),
25396 (Point::new(0, 13), Point::new(0, 21)),
25397 editor,
25398 cx,
25399 );
25400 });
25401 cx.update_editor(|editor, window, cx| {
25402 editor.handle_input("V", window, cx);
25403 });
25404 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25405}
25406
25407#[gpui::test]
25408async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25409 init_test(cx, |_| {});
25410
25411 let fs = FakeFs::new(cx.executor());
25412 fs.insert_tree(
25413 path!("/root"),
25414 json!({
25415 "a": {
25416 "main.rs": "fn main() {}",
25417 },
25418 "foo": {
25419 "bar": {
25420 "external_file.rs": "pub mod external {}",
25421 }
25422 }
25423 }),
25424 )
25425 .await;
25426
25427 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25428 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25429 language_registry.add(rust_lang());
25430 let _fake_servers = language_registry.register_fake_lsp(
25431 "Rust",
25432 FakeLspAdapter {
25433 ..FakeLspAdapter::default()
25434 },
25435 );
25436 let (workspace, cx) =
25437 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25438 let worktree_id = workspace.update(cx, |workspace, cx| {
25439 workspace.project().update(cx, |project, cx| {
25440 project.worktrees(cx).next().unwrap().read(cx).id()
25441 })
25442 });
25443
25444 let assert_language_servers_count =
25445 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25446 project.update(cx, |project, cx| {
25447 let current = project
25448 .lsp_store()
25449 .read(cx)
25450 .as_local()
25451 .unwrap()
25452 .language_servers
25453 .len();
25454 assert_eq!(expected, current, "{context}");
25455 });
25456 };
25457
25458 assert_language_servers_count(
25459 0,
25460 "No servers should be running before any file is open",
25461 cx,
25462 );
25463 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25464 let main_editor = workspace
25465 .update_in(cx, |workspace, window, cx| {
25466 workspace.open_path(
25467 (worktree_id, rel_path("main.rs")),
25468 Some(pane.downgrade()),
25469 true,
25470 window,
25471 cx,
25472 )
25473 })
25474 .unwrap()
25475 .await
25476 .downcast::<Editor>()
25477 .unwrap();
25478 pane.update(cx, |pane, cx| {
25479 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25480 open_editor.update(cx, |editor, cx| {
25481 assert_eq!(
25482 editor.display_text(cx),
25483 "fn main() {}",
25484 "Original main.rs text on initial open",
25485 );
25486 });
25487 assert_eq!(open_editor, main_editor);
25488 });
25489 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25490
25491 let external_editor = workspace
25492 .update_in(cx, |workspace, window, cx| {
25493 workspace.open_abs_path(
25494 PathBuf::from("/root/foo/bar/external_file.rs"),
25495 OpenOptions::default(),
25496 window,
25497 cx,
25498 )
25499 })
25500 .await
25501 .expect("opening external file")
25502 .downcast::<Editor>()
25503 .expect("downcasted external file's open element to editor");
25504 pane.update(cx, |pane, cx| {
25505 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25506 open_editor.update(cx, |editor, cx| {
25507 assert_eq!(
25508 editor.display_text(cx),
25509 "pub mod external {}",
25510 "External file is open now",
25511 );
25512 });
25513 assert_eq!(open_editor, external_editor);
25514 });
25515 assert_language_servers_count(
25516 1,
25517 "Second, external, *.rs file should join the existing server",
25518 cx,
25519 );
25520
25521 pane.update_in(cx, |pane, window, cx| {
25522 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25523 })
25524 .await
25525 .unwrap();
25526 pane.update_in(cx, |pane, window, cx| {
25527 pane.navigate_backward(&Default::default(), window, cx);
25528 });
25529 cx.run_until_parked();
25530 pane.update(cx, |pane, cx| {
25531 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25532 open_editor.update(cx, |editor, cx| {
25533 assert_eq!(
25534 editor.display_text(cx),
25535 "pub mod external {}",
25536 "External file is open now",
25537 );
25538 });
25539 });
25540 assert_language_servers_count(
25541 1,
25542 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25543 cx,
25544 );
25545
25546 cx.update(|_, cx| {
25547 workspace::reload(cx);
25548 });
25549 assert_language_servers_count(
25550 1,
25551 "After reloading the worktree with local and external files opened, only one project should be started",
25552 cx,
25553 );
25554}
25555
25556#[gpui::test]
25557async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25558 init_test(cx, |_| {});
25559
25560 let mut cx = EditorTestContext::new(cx).await;
25561 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25562 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25563
25564 // test cursor move to start of each line on tab
25565 // for `if`, `elif`, `else`, `while`, `with` and `for`
25566 cx.set_state(indoc! {"
25567 def main():
25568 ˇ for item in items:
25569 ˇ while item.active:
25570 ˇ if item.value > 10:
25571 ˇ continue
25572 ˇ elif item.value < 0:
25573 ˇ break
25574 ˇ else:
25575 ˇ with item.context() as ctx:
25576 ˇ yield count
25577 ˇ else:
25578 ˇ log('while else')
25579 ˇ else:
25580 ˇ log('for else')
25581 "});
25582 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25583 cx.wait_for_autoindent_applied().await;
25584 cx.assert_editor_state(indoc! {"
25585 def main():
25586 ˇfor item in items:
25587 ˇwhile item.active:
25588 ˇif item.value > 10:
25589 ˇcontinue
25590 ˇelif item.value < 0:
25591 ˇbreak
25592 ˇelse:
25593 ˇwith item.context() as ctx:
25594 ˇyield count
25595 ˇelse:
25596 ˇlog('while else')
25597 ˇelse:
25598 ˇlog('for else')
25599 "});
25600 // test relative indent is preserved when tab
25601 // for `if`, `elif`, `else`, `while`, `with` and `for`
25602 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25603 cx.wait_for_autoindent_applied().await;
25604 cx.assert_editor_state(indoc! {"
25605 def main():
25606 ˇfor item in items:
25607 ˇwhile item.active:
25608 ˇif item.value > 10:
25609 ˇcontinue
25610 ˇelif item.value < 0:
25611 ˇbreak
25612 ˇelse:
25613 ˇwith item.context() as ctx:
25614 ˇyield count
25615 ˇelse:
25616 ˇlog('while else')
25617 ˇelse:
25618 ˇlog('for else')
25619 "});
25620
25621 // test cursor move to start of each line on tab
25622 // for `try`, `except`, `else`, `finally`, `match` and `def`
25623 cx.set_state(indoc! {"
25624 def main():
25625 ˇ try:
25626 ˇ fetch()
25627 ˇ except ValueError:
25628 ˇ handle_error()
25629 ˇ else:
25630 ˇ match value:
25631 ˇ case _:
25632 ˇ finally:
25633 ˇ def status():
25634 ˇ return 0
25635 "});
25636 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25637 cx.wait_for_autoindent_applied().await;
25638 cx.assert_editor_state(indoc! {"
25639 def main():
25640 ˇtry:
25641 ˇfetch()
25642 ˇexcept ValueError:
25643 ˇhandle_error()
25644 ˇelse:
25645 ˇmatch value:
25646 ˇcase _:
25647 ˇfinally:
25648 ˇdef status():
25649 ˇreturn 0
25650 "});
25651 // test relative indent is preserved when tab
25652 // for `try`, `except`, `else`, `finally`, `match` and `def`
25653 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25654 cx.wait_for_autoindent_applied().await;
25655 cx.assert_editor_state(indoc! {"
25656 def main():
25657 ˇtry:
25658 ˇfetch()
25659 ˇexcept ValueError:
25660 ˇhandle_error()
25661 ˇelse:
25662 ˇmatch value:
25663 ˇcase _:
25664 ˇfinally:
25665 ˇdef status():
25666 ˇreturn 0
25667 "});
25668}
25669
25670#[gpui::test]
25671async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25672 init_test(cx, |_| {});
25673
25674 let mut cx = EditorTestContext::new(cx).await;
25675 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25676 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25677
25678 // test `else` auto outdents when typed inside `if` block
25679 cx.set_state(indoc! {"
25680 def main():
25681 if i == 2:
25682 return
25683 ˇ
25684 "});
25685 cx.update_editor(|editor, window, cx| {
25686 editor.handle_input("else:", window, cx);
25687 });
25688 cx.wait_for_autoindent_applied().await;
25689 cx.assert_editor_state(indoc! {"
25690 def main():
25691 if i == 2:
25692 return
25693 else:ˇ
25694 "});
25695
25696 // test `except` auto outdents when typed inside `try` block
25697 cx.set_state(indoc! {"
25698 def main():
25699 try:
25700 i = 2
25701 ˇ
25702 "});
25703 cx.update_editor(|editor, window, cx| {
25704 editor.handle_input("except:", window, cx);
25705 });
25706 cx.wait_for_autoindent_applied().await;
25707 cx.assert_editor_state(indoc! {"
25708 def main():
25709 try:
25710 i = 2
25711 except:ˇ
25712 "});
25713
25714 // test `else` auto outdents when typed inside `except` block
25715 cx.set_state(indoc! {"
25716 def main():
25717 try:
25718 i = 2
25719 except:
25720 j = 2
25721 ˇ
25722 "});
25723 cx.update_editor(|editor, window, cx| {
25724 editor.handle_input("else:", window, cx);
25725 });
25726 cx.wait_for_autoindent_applied().await;
25727 cx.assert_editor_state(indoc! {"
25728 def main():
25729 try:
25730 i = 2
25731 except:
25732 j = 2
25733 else:ˇ
25734 "});
25735
25736 // test `finally` auto outdents when typed inside `else` block
25737 cx.set_state(indoc! {"
25738 def main():
25739 try:
25740 i = 2
25741 except:
25742 j = 2
25743 else:
25744 k = 2
25745 ˇ
25746 "});
25747 cx.update_editor(|editor, window, cx| {
25748 editor.handle_input("finally:", window, cx);
25749 });
25750 cx.wait_for_autoindent_applied().await;
25751 cx.assert_editor_state(indoc! {"
25752 def main():
25753 try:
25754 i = 2
25755 except:
25756 j = 2
25757 else:
25758 k = 2
25759 finally:ˇ
25760 "});
25761
25762 // test `else` does not outdents when typed inside `except` block right after for block
25763 cx.set_state(indoc! {"
25764 def main():
25765 try:
25766 i = 2
25767 except:
25768 for i in range(n):
25769 pass
25770 ˇ
25771 "});
25772 cx.update_editor(|editor, window, cx| {
25773 editor.handle_input("else:", window, cx);
25774 });
25775 cx.wait_for_autoindent_applied().await;
25776 cx.assert_editor_state(indoc! {"
25777 def main():
25778 try:
25779 i = 2
25780 except:
25781 for i in range(n):
25782 pass
25783 else:ˇ
25784 "});
25785
25786 // test `finally` auto outdents when typed inside `else` block right after for block
25787 cx.set_state(indoc! {"
25788 def main():
25789 try:
25790 i = 2
25791 except:
25792 j = 2
25793 else:
25794 for i in range(n):
25795 pass
25796 ˇ
25797 "});
25798 cx.update_editor(|editor, window, cx| {
25799 editor.handle_input("finally:", window, cx);
25800 });
25801 cx.wait_for_autoindent_applied().await;
25802 cx.assert_editor_state(indoc! {"
25803 def main():
25804 try:
25805 i = 2
25806 except:
25807 j = 2
25808 else:
25809 for i in range(n):
25810 pass
25811 finally:ˇ
25812 "});
25813
25814 // test `except` outdents to inner "try" block
25815 cx.set_state(indoc! {"
25816 def main():
25817 try:
25818 i = 2
25819 if i == 2:
25820 try:
25821 i = 3
25822 ˇ
25823 "});
25824 cx.update_editor(|editor, window, cx| {
25825 editor.handle_input("except:", window, cx);
25826 });
25827 cx.wait_for_autoindent_applied().await;
25828 cx.assert_editor_state(indoc! {"
25829 def main():
25830 try:
25831 i = 2
25832 if i == 2:
25833 try:
25834 i = 3
25835 except:ˇ
25836 "});
25837
25838 // test `except` outdents to outer "try" block
25839 cx.set_state(indoc! {"
25840 def main():
25841 try:
25842 i = 2
25843 if i == 2:
25844 try:
25845 i = 3
25846 ˇ
25847 "});
25848 cx.update_editor(|editor, window, cx| {
25849 editor.handle_input("except:", window, cx);
25850 });
25851 cx.wait_for_autoindent_applied().await;
25852 cx.assert_editor_state(indoc! {"
25853 def main():
25854 try:
25855 i = 2
25856 if i == 2:
25857 try:
25858 i = 3
25859 except:ˇ
25860 "});
25861
25862 // test `else` stays at correct indent when typed after `for` block
25863 cx.set_state(indoc! {"
25864 def main():
25865 for i in range(10):
25866 if i == 3:
25867 break
25868 ˇ
25869 "});
25870 cx.update_editor(|editor, window, cx| {
25871 editor.handle_input("else:", window, cx);
25872 });
25873 cx.wait_for_autoindent_applied().await;
25874 cx.assert_editor_state(indoc! {"
25875 def main():
25876 for i in range(10):
25877 if i == 3:
25878 break
25879 else:ˇ
25880 "});
25881
25882 // test does not outdent on typing after line with square brackets
25883 cx.set_state(indoc! {"
25884 def f() -> list[str]:
25885 ˇ
25886 "});
25887 cx.update_editor(|editor, window, cx| {
25888 editor.handle_input("a", window, cx);
25889 });
25890 cx.wait_for_autoindent_applied().await;
25891 cx.assert_editor_state(indoc! {"
25892 def f() -> list[str]:
25893 aˇ
25894 "});
25895
25896 // test does not outdent on typing : after case keyword
25897 cx.set_state(indoc! {"
25898 match 1:
25899 caseˇ
25900 "});
25901 cx.update_editor(|editor, window, cx| {
25902 editor.handle_input(":", window, cx);
25903 });
25904 cx.wait_for_autoindent_applied().await;
25905 cx.assert_editor_state(indoc! {"
25906 match 1:
25907 case:ˇ
25908 "});
25909}
25910
25911#[gpui::test]
25912async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25913 init_test(cx, |_| {});
25914 update_test_language_settings(cx, |settings| {
25915 settings.defaults.extend_comment_on_newline = Some(false);
25916 });
25917 let mut cx = EditorTestContext::new(cx).await;
25918 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25919 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25920
25921 // test correct indent after newline on comment
25922 cx.set_state(indoc! {"
25923 # COMMENT:ˇ
25924 "});
25925 cx.update_editor(|editor, window, cx| {
25926 editor.newline(&Newline, window, cx);
25927 });
25928 cx.wait_for_autoindent_applied().await;
25929 cx.assert_editor_state(indoc! {"
25930 # COMMENT:
25931 ˇ
25932 "});
25933
25934 // test correct indent after newline in brackets
25935 cx.set_state(indoc! {"
25936 {ˇ}
25937 "});
25938 cx.update_editor(|editor, window, cx| {
25939 editor.newline(&Newline, window, cx);
25940 });
25941 cx.wait_for_autoindent_applied().await;
25942 cx.assert_editor_state(indoc! {"
25943 {
25944 ˇ
25945 }
25946 "});
25947
25948 cx.set_state(indoc! {"
25949 (ˇ)
25950 "});
25951 cx.update_editor(|editor, window, cx| {
25952 editor.newline(&Newline, window, cx);
25953 });
25954 cx.run_until_parked();
25955 cx.assert_editor_state(indoc! {"
25956 (
25957 ˇ
25958 )
25959 "});
25960
25961 // do not indent after empty lists or dictionaries
25962 cx.set_state(indoc! {"
25963 a = []ˇ
25964 "});
25965 cx.update_editor(|editor, window, cx| {
25966 editor.newline(&Newline, window, cx);
25967 });
25968 cx.run_until_parked();
25969 cx.assert_editor_state(indoc! {"
25970 a = []
25971 ˇ
25972 "});
25973}
25974
25975#[gpui::test]
25976async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
25977 init_test(cx, |_| {});
25978
25979 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
25980 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
25981 language_registry.add(markdown_lang());
25982 language_registry.add(python_lang);
25983
25984 let mut cx = EditorTestContext::new(cx).await;
25985 cx.update_buffer(|buffer, cx| {
25986 buffer.set_language_registry(language_registry);
25987 buffer.set_language(Some(markdown_lang()), cx);
25988 });
25989
25990 // Test that `else:` correctly outdents to match `if:` inside the Python code block
25991 cx.set_state(indoc! {"
25992 # Heading
25993
25994 ```python
25995 def main():
25996 if condition:
25997 pass
25998 ˇ
25999 ```
26000 "});
26001 cx.update_editor(|editor, window, cx| {
26002 editor.handle_input("else:", window, cx);
26003 });
26004 cx.run_until_parked();
26005 cx.assert_editor_state(indoc! {"
26006 # Heading
26007
26008 ```python
26009 def main():
26010 if condition:
26011 pass
26012 else:ˇ
26013 ```
26014 "});
26015}
26016
26017#[gpui::test]
26018async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26019 init_test(cx, |_| {});
26020
26021 let mut cx = EditorTestContext::new(cx).await;
26022 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26023 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26024
26025 // test cursor move to start of each line on tab
26026 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26027 cx.set_state(indoc! {"
26028 function main() {
26029 ˇ for item in $items; do
26030 ˇ while [ -n \"$item\" ]; do
26031 ˇ if [ \"$value\" -gt 10 ]; then
26032 ˇ continue
26033 ˇ elif [ \"$value\" -lt 0 ]; then
26034 ˇ break
26035 ˇ else
26036 ˇ echo \"$item\"
26037 ˇ fi
26038 ˇ done
26039 ˇ done
26040 ˇ}
26041 "});
26042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26043 cx.wait_for_autoindent_applied().await;
26044 cx.assert_editor_state(indoc! {"
26045 function main() {
26046 ˇfor item in $items; do
26047 ˇwhile [ -n \"$item\" ]; do
26048 ˇif [ \"$value\" -gt 10 ]; then
26049 ˇcontinue
26050 ˇelif [ \"$value\" -lt 0 ]; then
26051 ˇbreak
26052 ˇelse
26053 ˇecho \"$item\"
26054 ˇfi
26055 ˇdone
26056 ˇdone
26057 ˇ}
26058 "});
26059 // test relative indent is preserved when tab
26060 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26061 cx.wait_for_autoindent_applied().await;
26062 cx.assert_editor_state(indoc! {"
26063 function main() {
26064 ˇfor item in $items; do
26065 ˇwhile [ -n \"$item\" ]; do
26066 ˇif [ \"$value\" -gt 10 ]; then
26067 ˇcontinue
26068 ˇelif [ \"$value\" -lt 0 ]; then
26069 ˇbreak
26070 ˇelse
26071 ˇecho \"$item\"
26072 ˇfi
26073 ˇdone
26074 ˇdone
26075 ˇ}
26076 "});
26077
26078 // test cursor move to start of each line on tab
26079 // for `case` statement with patterns
26080 cx.set_state(indoc! {"
26081 function handle() {
26082 ˇ case \"$1\" in
26083 ˇ start)
26084 ˇ echo \"a\"
26085 ˇ ;;
26086 ˇ stop)
26087 ˇ echo \"b\"
26088 ˇ ;;
26089 ˇ *)
26090 ˇ echo \"c\"
26091 ˇ ;;
26092 ˇ esac
26093 ˇ}
26094 "});
26095 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26096 cx.wait_for_autoindent_applied().await;
26097 cx.assert_editor_state(indoc! {"
26098 function handle() {
26099 ˇcase \"$1\" in
26100 ˇstart)
26101 ˇecho \"a\"
26102 ˇ;;
26103 ˇstop)
26104 ˇecho \"b\"
26105 ˇ;;
26106 ˇ*)
26107 ˇecho \"c\"
26108 ˇ;;
26109 ˇesac
26110 ˇ}
26111 "});
26112}
26113
26114#[gpui::test]
26115async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26116 init_test(cx, |_| {});
26117
26118 let mut cx = EditorTestContext::new(cx).await;
26119 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26120 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26121
26122 // test indents on comment insert
26123 cx.set_state(indoc! {"
26124 function main() {
26125 ˇ for item in $items; do
26126 ˇ while [ -n \"$item\" ]; do
26127 ˇ if [ \"$value\" -gt 10 ]; then
26128 ˇ continue
26129 ˇ elif [ \"$value\" -lt 0 ]; then
26130 ˇ break
26131 ˇ else
26132 ˇ echo \"$item\"
26133 ˇ fi
26134 ˇ done
26135 ˇ done
26136 ˇ}
26137 "});
26138 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26139 cx.wait_for_autoindent_applied().await;
26140 cx.assert_editor_state(indoc! {"
26141 function main() {
26142 #ˇ for item in $items; do
26143 #ˇ while [ -n \"$item\" ]; do
26144 #ˇ if [ \"$value\" -gt 10 ]; then
26145 #ˇ continue
26146 #ˇ elif [ \"$value\" -lt 0 ]; then
26147 #ˇ break
26148 #ˇ else
26149 #ˇ echo \"$item\"
26150 #ˇ fi
26151 #ˇ done
26152 #ˇ done
26153 #ˇ}
26154 "});
26155}
26156
26157#[gpui::test]
26158async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26159 init_test(cx, |_| {});
26160
26161 let mut cx = EditorTestContext::new(cx).await;
26162 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26163 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26164
26165 // test `else` auto outdents when typed inside `if` block
26166 cx.set_state(indoc! {"
26167 if [ \"$1\" = \"test\" ]; then
26168 echo \"foo bar\"
26169 ˇ
26170 "});
26171 cx.update_editor(|editor, window, cx| {
26172 editor.handle_input("else", window, cx);
26173 });
26174 cx.wait_for_autoindent_applied().await;
26175 cx.assert_editor_state(indoc! {"
26176 if [ \"$1\" = \"test\" ]; then
26177 echo \"foo bar\"
26178 elseˇ
26179 "});
26180
26181 // test `elif` auto outdents when typed inside `if` block
26182 cx.set_state(indoc! {"
26183 if [ \"$1\" = \"test\" ]; then
26184 echo \"foo bar\"
26185 ˇ
26186 "});
26187 cx.update_editor(|editor, window, cx| {
26188 editor.handle_input("elif", window, cx);
26189 });
26190 cx.wait_for_autoindent_applied().await;
26191 cx.assert_editor_state(indoc! {"
26192 if [ \"$1\" = \"test\" ]; then
26193 echo \"foo bar\"
26194 elifˇ
26195 "});
26196
26197 // test `fi` auto outdents when typed inside `else` block
26198 cx.set_state(indoc! {"
26199 if [ \"$1\" = \"test\" ]; then
26200 echo \"foo bar\"
26201 else
26202 echo \"bar baz\"
26203 ˇ
26204 "});
26205 cx.update_editor(|editor, window, cx| {
26206 editor.handle_input("fi", window, cx);
26207 });
26208 cx.wait_for_autoindent_applied().await;
26209 cx.assert_editor_state(indoc! {"
26210 if [ \"$1\" = \"test\" ]; then
26211 echo \"foo bar\"
26212 else
26213 echo \"bar baz\"
26214 fiˇ
26215 "});
26216
26217 // test `done` auto outdents when typed inside `while` block
26218 cx.set_state(indoc! {"
26219 while read line; do
26220 echo \"$line\"
26221 ˇ
26222 "});
26223 cx.update_editor(|editor, window, cx| {
26224 editor.handle_input("done", window, cx);
26225 });
26226 cx.wait_for_autoindent_applied().await;
26227 cx.assert_editor_state(indoc! {"
26228 while read line; do
26229 echo \"$line\"
26230 doneˇ
26231 "});
26232
26233 // test `done` auto outdents when typed inside `for` block
26234 cx.set_state(indoc! {"
26235 for file in *.txt; do
26236 cat \"$file\"
26237 ˇ
26238 "});
26239 cx.update_editor(|editor, window, cx| {
26240 editor.handle_input("done", window, cx);
26241 });
26242 cx.wait_for_autoindent_applied().await;
26243 cx.assert_editor_state(indoc! {"
26244 for file in *.txt; do
26245 cat \"$file\"
26246 doneˇ
26247 "});
26248
26249 // test `esac` auto outdents when typed inside `case` block
26250 cx.set_state(indoc! {"
26251 case \"$1\" in
26252 start)
26253 echo \"foo bar\"
26254 ;;
26255 stop)
26256 echo \"bar baz\"
26257 ;;
26258 ˇ
26259 "});
26260 cx.update_editor(|editor, window, cx| {
26261 editor.handle_input("esac", window, cx);
26262 });
26263 cx.wait_for_autoindent_applied().await;
26264 cx.assert_editor_state(indoc! {"
26265 case \"$1\" in
26266 start)
26267 echo \"foo bar\"
26268 ;;
26269 stop)
26270 echo \"bar baz\"
26271 ;;
26272 esacˇ
26273 "});
26274
26275 // test `*)` auto outdents when typed inside `case` block
26276 cx.set_state(indoc! {"
26277 case \"$1\" in
26278 start)
26279 echo \"foo bar\"
26280 ;;
26281 ˇ
26282 "});
26283 cx.update_editor(|editor, window, cx| {
26284 editor.handle_input("*)", window, cx);
26285 });
26286 cx.wait_for_autoindent_applied().await;
26287 cx.assert_editor_state(indoc! {"
26288 case \"$1\" in
26289 start)
26290 echo \"foo bar\"
26291 ;;
26292 *)ˇ
26293 "});
26294
26295 // test `fi` outdents to correct level with nested if blocks
26296 cx.set_state(indoc! {"
26297 if [ \"$1\" = \"test\" ]; then
26298 echo \"outer if\"
26299 if [ \"$2\" = \"debug\" ]; then
26300 echo \"inner if\"
26301 ˇ
26302 "});
26303 cx.update_editor(|editor, window, cx| {
26304 editor.handle_input("fi", window, cx);
26305 });
26306 cx.wait_for_autoindent_applied().await;
26307 cx.assert_editor_state(indoc! {"
26308 if [ \"$1\" = \"test\" ]; then
26309 echo \"outer if\"
26310 if [ \"$2\" = \"debug\" ]; then
26311 echo \"inner if\"
26312 fiˇ
26313 "});
26314}
26315
26316#[gpui::test]
26317async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26318 init_test(cx, |_| {});
26319 update_test_language_settings(cx, |settings| {
26320 settings.defaults.extend_comment_on_newline = Some(false);
26321 });
26322 let mut cx = EditorTestContext::new(cx).await;
26323 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26324 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26325
26326 // test correct indent after newline on comment
26327 cx.set_state(indoc! {"
26328 # COMMENT:ˇ
26329 "});
26330 cx.update_editor(|editor, window, cx| {
26331 editor.newline(&Newline, window, cx);
26332 });
26333 cx.wait_for_autoindent_applied().await;
26334 cx.assert_editor_state(indoc! {"
26335 # COMMENT:
26336 ˇ
26337 "});
26338
26339 // test correct indent after newline after `then`
26340 cx.set_state(indoc! {"
26341
26342 if [ \"$1\" = \"test\" ]; thenˇ
26343 "});
26344 cx.update_editor(|editor, window, cx| {
26345 editor.newline(&Newline, window, cx);
26346 });
26347 cx.wait_for_autoindent_applied().await;
26348 cx.assert_editor_state(indoc! {"
26349
26350 if [ \"$1\" = \"test\" ]; then
26351 ˇ
26352 "});
26353
26354 // test correct indent after newline after `else`
26355 cx.set_state(indoc! {"
26356 if [ \"$1\" = \"test\" ]; then
26357 elseˇ
26358 "});
26359 cx.update_editor(|editor, window, cx| {
26360 editor.newline(&Newline, window, cx);
26361 });
26362 cx.wait_for_autoindent_applied().await;
26363 cx.assert_editor_state(indoc! {"
26364 if [ \"$1\" = \"test\" ]; then
26365 else
26366 ˇ
26367 "});
26368
26369 // test correct indent after newline after `elif`
26370 cx.set_state(indoc! {"
26371 if [ \"$1\" = \"test\" ]; then
26372 elifˇ
26373 "});
26374 cx.update_editor(|editor, window, cx| {
26375 editor.newline(&Newline, window, cx);
26376 });
26377 cx.wait_for_autoindent_applied().await;
26378 cx.assert_editor_state(indoc! {"
26379 if [ \"$1\" = \"test\" ]; then
26380 elif
26381 ˇ
26382 "});
26383
26384 // test correct indent after newline after `do`
26385 cx.set_state(indoc! {"
26386 for file in *.txt; doˇ
26387 "});
26388 cx.update_editor(|editor, window, cx| {
26389 editor.newline(&Newline, window, cx);
26390 });
26391 cx.wait_for_autoindent_applied().await;
26392 cx.assert_editor_state(indoc! {"
26393 for file in *.txt; do
26394 ˇ
26395 "});
26396
26397 // test correct indent after newline after case pattern
26398 cx.set_state(indoc! {"
26399 case \"$1\" in
26400 start)ˇ
26401 "});
26402 cx.update_editor(|editor, window, cx| {
26403 editor.newline(&Newline, window, cx);
26404 });
26405 cx.wait_for_autoindent_applied().await;
26406 cx.assert_editor_state(indoc! {"
26407 case \"$1\" in
26408 start)
26409 ˇ
26410 "});
26411
26412 // test correct indent after newline after case pattern
26413 cx.set_state(indoc! {"
26414 case \"$1\" in
26415 start)
26416 ;;
26417 *)ˇ
26418 "});
26419 cx.update_editor(|editor, window, cx| {
26420 editor.newline(&Newline, window, cx);
26421 });
26422 cx.wait_for_autoindent_applied().await;
26423 cx.assert_editor_state(indoc! {"
26424 case \"$1\" in
26425 start)
26426 ;;
26427 *)
26428 ˇ
26429 "});
26430
26431 // test correct indent after newline after function opening brace
26432 cx.set_state(indoc! {"
26433 function test() {ˇ}
26434 "});
26435 cx.update_editor(|editor, window, cx| {
26436 editor.newline(&Newline, window, cx);
26437 });
26438 cx.wait_for_autoindent_applied().await;
26439 cx.assert_editor_state(indoc! {"
26440 function test() {
26441 ˇ
26442 }
26443 "});
26444
26445 // test no extra indent after semicolon on same line
26446 cx.set_state(indoc! {"
26447 echo \"test\";ˇ
26448 "});
26449 cx.update_editor(|editor, window, cx| {
26450 editor.newline(&Newline, window, cx);
26451 });
26452 cx.wait_for_autoindent_applied().await;
26453 cx.assert_editor_state(indoc! {"
26454 echo \"test\";
26455 ˇ
26456 "});
26457}
26458
26459fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26460 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26461 point..point
26462}
26463
26464#[track_caller]
26465fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26466 let (text, ranges) = marked_text_ranges(marked_text, true);
26467 assert_eq!(editor.text(cx), text);
26468 assert_eq!(
26469 editor.selections.ranges(&editor.display_snapshot(cx)),
26470 ranges
26471 .iter()
26472 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26473 .collect::<Vec<_>>(),
26474 "Assert selections are {}",
26475 marked_text
26476 );
26477}
26478
26479pub fn handle_signature_help_request(
26480 cx: &mut EditorLspTestContext,
26481 mocked_response: lsp::SignatureHelp,
26482) -> impl Future<Output = ()> + use<> {
26483 let mut request =
26484 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26485 let mocked_response = mocked_response.clone();
26486 async move { Ok(Some(mocked_response)) }
26487 });
26488
26489 async move {
26490 request.next().await;
26491 }
26492}
26493
26494#[track_caller]
26495pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26496 cx.update_editor(|editor, _, _| {
26497 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26498 let entries = menu.entries.borrow();
26499 let entries = entries
26500 .iter()
26501 .map(|entry| entry.string.as_str())
26502 .collect::<Vec<_>>();
26503 assert_eq!(entries, expected);
26504 } else {
26505 panic!("Expected completions menu");
26506 }
26507 });
26508}
26509
26510#[gpui::test]
26511async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26512 init_test(cx, |_| {});
26513 let mut cx = EditorLspTestContext::new_rust(
26514 lsp::ServerCapabilities {
26515 completion_provider: Some(lsp::CompletionOptions {
26516 ..Default::default()
26517 }),
26518 ..Default::default()
26519 },
26520 cx,
26521 )
26522 .await;
26523 cx.lsp
26524 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26525 Ok(Some(lsp::CompletionResponse::Array(vec![
26526 lsp::CompletionItem {
26527 label: "unsafe".into(),
26528 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26529 range: lsp::Range {
26530 start: lsp::Position {
26531 line: 0,
26532 character: 9,
26533 },
26534 end: lsp::Position {
26535 line: 0,
26536 character: 11,
26537 },
26538 },
26539 new_text: "unsafe".to_string(),
26540 })),
26541 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26542 ..Default::default()
26543 },
26544 ])))
26545 });
26546
26547 cx.update_editor(|editor, _, cx| {
26548 editor.project().unwrap().update(cx, |project, cx| {
26549 project.snippets().update(cx, |snippets, _cx| {
26550 snippets.add_snippet_for_test(
26551 None,
26552 PathBuf::from("test_snippets.json"),
26553 vec![
26554 Arc::new(project::snippet_provider::Snippet {
26555 prefix: vec![
26556 "unlimited word count".to_string(),
26557 "unlimit word count".to_string(),
26558 "unlimited unknown".to_string(),
26559 ],
26560 body: "this is many words".to_string(),
26561 description: Some("description".to_string()),
26562 name: "multi-word snippet test".to_string(),
26563 }),
26564 Arc::new(project::snippet_provider::Snippet {
26565 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26566 body: "fewer words".to_string(),
26567 description: Some("alt description".to_string()),
26568 name: "other name".to_string(),
26569 }),
26570 Arc::new(project::snippet_provider::Snippet {
26571 prefix: vec!["ab aa".to_string()],
26572 body: "abcd".to_string(),
26573 description: None,
26574 name: "alphabet".to_string(),
26575 }),
26576 ],
26577 );
26578 });
26579 })
26580 });
26581
26582 let get_completions = |cx: &mut EditorLspTestContext| {
26583 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26584 Some(CodeContextMenu::Completions(context_menu)) => {
26585 let entries = context_menu.entries.borrow();
26586 entries
26587 .iter()
26588 .map(|entry| entry.string.clone())
26589 .collect_vec()
26590 }
26591 _ => vec![],
26592 })
26593 };
26594
26595 // snippets:
26596 // @foo
26597 // foo bar
26598 //
26599 // when typing:
26600 //
26601 // when typing:
26602 // - if I type a symbol "open the completions with snippets only"
26603 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26604 //
26605 // stuff we need:
26606 // - filtering logic change?
26607 // - remember how far back the completion started.
26608
26609 let test_cases: &[(&str, &[&str])] = &[
26610 (
26611 "un",
26612 &[
26613 "unsafe",
26614 "unlimit word count",
26615 "unlimited unknown",
26616 "unlimited word count",
26617 "unsnip",
26618 ],
26619 ),
26620 (
26621 "u ",
26622 &[
26623 "unlimit word count",
26624 "unlimited unknown",
26625 "unlimited word count",
26626 ],
26627 ),
26628 ("u a", &["ab aa", "unsafe"]), // unsAfe
26629 (
26630 "u u",
26631 &[
26632 "unsafe",
26633 "unlimit word count",
26634 "unlimited unknown", // ranked highest among snippets
26635 "unlimited word count",
26636 "unsnip",
26637 ],
26638 ),
26639 ("uw c", &["unlimit word count", "unlimited word count"]),
26640 (
26641 "u w",
26642 &[
26643 "unlimit word count",
26644 "unlimited word count",
26645 "unlimited unknown",
26646 ],
26647 ),
26648 ("u w ", &["unlimit word count", "unlimited word count"]),
26649 (
26650 "u ",
26651 &[
26652 "unlimit word count",
26653 "unlimited unknown",
26654 "unlimited word count",
26655 ],
26656 ),
26657 ("wor", &[]),
26658 ("uf", &["unsafe"]),
26659 ("af", &["unsafe"]),
26660 ("afu", &[]),
26661 (
26662 "ue",
26663 &["unsafe", "unlimited unknown", "unlimited word count"],
26664 ),
26665 ("@", &["@few"]),
26666 ("@few", &["@few"]),
26667 ("@ ", &[]),
26668 ("a@", &["@few"]),
26669 ("a@f", &["@few", "unsafe"]),
26670 ("a@fw", &["@few"]),
26671 ("a", &["ab aa", "unsafe"]),
26672 ("aa", &["ab aa"]),
26673 ("aaa", &["ab aa"]),
26674 ("ab", &["ab aa"]),
26675 ("ab ", &["ab aa"]),
26676 ("ab a", &["ab aa", "unsafe"]),
26677 ("ab ab", &["ab aa"]),
26678 ("ab ab aa", &["ab aa"]),
26679 ];
26680
26681 for &(input_to_simulate, expected_completions) in test_cases {
26682 cx.set_state("fn a() { ˇ }\n");
26683 for c in input_to_simulate.split("") {
26684 cx.simulate_input(c);
26685 cx.run_until_parked();
26686 }
26687 let expected_completions = expected_completions
26688 .iter()
26689 .map(|s| s.to_string())
26690 .collect_vec();
26691 assert_eq!(
26692 get_completions(&mut cx),
26693 expected_completions,
26694 "< actual / expected >, input = {input_to_simulate:?}",
26695 );
26696 }
26697}
26698
26699/// Handle completion request passing a marked string specifying where the completion
26700/// should be triggered from using '|' character, what range should be replaced, and what completions
26701/// should be returned using '<' and '>' to delimit the range.
26702///
26703/// Also see `handle_completion_request_with_insert_and_replace`.
26704#[track_caller]
26705pub fn handle_completion_request(
26706 marked_string: &str,
26707 completions: Vec<&'static str>,
26708 is_incomplete: bool,
26709 counter: Arc<AtomicUsize>,
26710 cx: &mut EditorLspTestContext,
26711) -> impl Future<Output = ()> {
26712 let complete_from_marker: TextRangeMarker = '|'.into();
26713 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26714 let (_, mut marked_ranges) = marked_text_ranges_by(
26715 marked_string,
26716 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26717 );
26718
26719 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26720 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26721 ));
26722 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26723 let replace_range =
26724 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26725
26726 let mut request =
26727 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26728 let completions = completions.clone();
26729 counter.fetch_add(1, atomic::Ordering::Release);
26730 async move {
26731 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26732 assert_eq!(
26733 params.text_document_position.position,
26734 complete_from_position
26735 );
26736 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26737 is_incomplete,
26738 item_defaults: None,
26739 items: completions
26740 .iter()
26741 .map(|completion_text| lsp::CompletionItem {
26742 label: completion_text.to_string(),
26743 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26744 range: replace_range,
26745 new_text: completion_text.to_string(),
26746 })),
26747 ..Default::default()
26748 })
26749 .collect(),
26750 })))
26751 }
26752 });
26753
26754 async move {
26755 request.next().await;
26756 }
26757}
26758
26759/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26760/// given instead, which also contains an `insert` range.
26761///
26762/// This function uses markers to define ranges:
26763/// - `|` marks the cursor position
26764/// - `<>` marks the replace range
26765/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26766pub fn handle_completion_request_with_insert_and_replace(
26767 cx: &mut EditorLspTestContext,
26768 marked_string: &str,
26769 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26770 counter: Arc<AtomicUsize>,
26771) -> impl Future<Output = ()> {
26772 let complete_from_marker: TextRangeMarker = '|'.into();
26773 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26774 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26775
26776 let (_, mut marked_ranges) = marked_text_ranges_by(
26777 marked_string,
26778 vec![
26779 complete_from_marker.clone(),
26780 replace_range_marker.clone(),
26781 insert_range_marker.clone(),
26782 ],
26783 );
26784
26785 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26786 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26787 ));
26788 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26789 let replace_range =
26790 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26791
26792 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26793 Some(ranges) if !ranges.is_empty() => {
26794 let range1 = ranges[0].clone();
26795 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26796 }
26797 _ => lsp::Range {
26798 start: replace_range.start,
26799 end: complete_from_position,
26800 },
26801 };
26802
26803 let mut request =
26804 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26805 let completions = completions.clone();
26806 counter.fetch_add(1, atomic::Ordering::Release);
26807 async move {
26808 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26809 assert_eq!(
26810 params.text_document_position.position, complete_from_position,
26811 "marker `|` position doesn't match",
26812 );
26813 Ok(Some(lsp::CompletionResponse::Array(
26814 completions
26815 .iter()
26816 .map(|(label, new_text)| lsp::CompletionItem {
26817 label: label.to_string(),
26818 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26819 lsp::InsertReplaceEdit {
26820 insert: insert_range,
26821 replace: replace_range,
26822 new_text: new_text.to_string(),
26823 },
26824 )),
26825 ..Default::default()
26826 })
26827 .collect(),
26828 )))
26829 }
26830 });
26831
26832 async move {
26833 request.next().await;
26834 }
26835}
26836
26837fn handle_resolve_completion_request(
26838 cx: &mut EditorLspTestContext,
26839 edits: Option<Vec<(&'static str, &'static str)>>,
26840) -> impl Future<Output = ()> {
26841 let edits = edits.map(|edits| {
26842 edits
26843 .iter()
26844 .map(|(marked_string, new_text)| {
26845 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26846 let replace_range = cx.to_lsp_range(
26847 MultiBufferOffset(marked_ranges[0].start)
26848 ..MultiBufferOffset(marked_ranges[0].end),
26849 );
26850 lsp::TextEdit::new(replace_range, new_text.to_string())
26851 })
26852 .collect::<Vec<_>>()
26853 });
26854
26855 let mut request =
26856 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26857 let edits = edits.clone();
26858 async move {
26859 Ok(lsp::CompletionItem {
26860 additional_text_edits: edits,
26861 ..Default::default()
26862 })
26863 }
26864 });
26865
26866 async move {
26867 request.next().await;
26868 }
26869}
26870
26871pub(crate) fn update_test_language_settings(
26872 cx: &mut TestAppContext,
26873 f: impl Fn(&mut AllLanguageSettingsContent),
26874) {
26875 cx.update(|cx| {
26876 SettingsStore::update_global(cx, |store, cx| {
26877 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26878 });
26879 });
26880}
26881
26882pub(crate) fn update_test_project_settings(
26883 cx: &mut TestAppContext,
26884 f: impl Fn(&mut ProjectSettingsContent),
26885) {
26886 cx.update(|cx| {
26887 SettingsStore::update_global(cx, |store, cx| {
26888 store.update_user_settings(cx, |settings| f(&mut settings.project));
26889 });
26890 });
26891}
26892
26893pub(crate) fn update_test_editor_settings(
26894 cx: &mut TestAppContext,
26895 f: impl Fn(&mut EditorSettingsContent),
26896) {
26897 cx.update(|cx| {
26898 SettingsStore::update_global(cx, |store, cx| {
26899 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26900 })
26901 })
26902}
26903
26904pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26905 cx.update(|cx| {
26906 assets::Assets.load_test_fonts(cx);
26907 let store = SettingsStore::test(cx);
26908 cx.set_global(store);
26909 theme::init(theme::LoadThemes::JustBase, cx);
26910 release_channel::init(semver::Version::new(0, 0, 0), cx);
26911 crate::init(cx);
26912 });
26913 zlog::init_test();
26914 update_test_language_settings(cx, f);
26915}
26916
26917#[track_caller]
26918fn assert_hunk_revert(
26919 not_reverted_text_with_selections: &str,
26920 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26921 expected_reverted_text_with_selections: &str,
26922 base_text: &str,
26923 cx: &mut EditorLspTestContext,
26924) {
26925 cx.set_state(not_reverted_text_with_selections);
26926 cx.set_head_text(base_text);
26927 cx.executor().run_until_parked();
26928
26929 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26930 let snapshot = editor.snapshot(window, cx);
26931 let reverted_hunk_statuses = snapshot
26932 .buffer_snapshot()
26933 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
26934 .map(|hunk| hunk.status().kind)
26935 .collect::<Vec<_>>();
26936
26937 editor.git_restore(&Default::default(), window, cx);
26938 reverted_hunk_statuses
26939 });
26940 cx.executor().run_until_parked();
26941 cx.assert_editor_state(expected_reverted_text_with_selections);
26942 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26943}
26944
26945#[gpui::test(iterations = 10)]
26946async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26947 init_test(cx, |_| {});
26948
26949 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26950 let counter = diagnostic_requests.clone();
26951
26952 let fs = FakeFs::new(cx.executor());
26953 fs.insert_tree(
26954 path!("/a"),
26955 json!({
26956 "first.rs": "fn main() { let a = 5; }",
26957 "second.rs": "// Test file",
26958 }),
26959 )
26960 .await;
26961
26962 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26963 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26964 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26965
26966 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26967 language_registry.add(rust_lang());
26968 let mut fake_servers = language_registry.register_fake_lsp(
26969 "Rust",
26970 FakeLspAdapter {
26971 capabilities: lsp::ServerCapabilities {
26972 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26973 lsp::DiagnosticOptions {
26974 identifier: None,
26975 inter_file_dependencies: true,
26976 workspace_diagnostics: true,
26977 work_done_progress_options: Default::default(),
26978 },
26979 )),
26980 ..Default::default()
26981 },
26982 ..Default::default()
26983 },
26984 );
26985
26986 let editor = workspace
26987 .update(cx, |workspace, window, cx| {
26988 workspace.open_abs_path(
26989 PathBuf::from(path!("/a/first.rs")),
26990 OpenOptions::default(),
26991 window,
26992 cx,
26993 )
26994 })
26995 .unwrap()
26996 .await
26997 .unwrap()
26998 .downcast::<Editor>()
26999 .unwrap();
27000 let fake_server = fake_servers.next().await.unwrap();
27001 let server_id = fake_server.server.server_id();
27002 let mut first_request = fake_server
27003 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27004 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27005 let result_id = Some(new_result_id.to_string());
27006 assert_eq!(
27007 params.text_document.uri,
27008 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27009 );
27010 async move {
27011 Ok(lsp::DocumentDiagnosticReportResult::Report(
27012 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27013 related_documents: None,
27014 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27015 items: Vec::new(),
27016 result_id,
27017 },
27018 }),
27019 ))
27020 }
27021 });
27022
27023 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27024 project.update(cx, |project, cx| {
27025 let buffer_id = editor
27026 .read(cx)
27027 .buffer()
27028 .read(cx)
27029 .as_singleton()
27030 .expect("created a singleton buffer")
27031 .read(cx)
27032 .remote_id();
27033 let buffer_result_id = project
27034 .lsp_store()
27035 .read(cx)
27036 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27037 assert_eq!(expected, buffer_result_id);
27038 });
27039 };
27040
27041 ensure_result_id(None, cx);
27042 cx.executor().advance_clock(Duration::from_millis(60));
27043 cx.executor().run_until_parked();
27044 assert_eq!(
27045 diagnostic_requests.load(atomic::Ordering::Acquire),
27046 1,
27047 "Opening file should trigger diagnostic request"
27048 );
27049 first_request
27050 .next()
27051 .await
27052 .expect("should have sent the first diagnostics pull request");
27053 ensure_result_id(Some(SharedString::new("1")), cx);
27054
27055 // Editing should trigger diagnostics
27056 editor.update_in(cx, |editor, window, cx| {
27057 editor.handle_input("2", window, cx)
27058 });
27059 cx.executor().advance_clock(Duration::from_millis(60));
27060 cx.executor().run_until_parked();
27061 assert_eq!(
27062 diagnostic_requests.load(atomic::Ordering::Acquire),
27063 2,
27064 "Editing should trigger diagnostic request"
27065 );
27066 ensure_result_id(Some(SharedString::new("2")), cx);
27067
27068 // Moving cursor should not trigger diagnostic request
27069 editor.update_in(cx, |editor, window, cx| {
27070 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27071 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27072 });
27073 });
27074 cx.executor().advance_clock(Duration::from_millis(60));
27075 cx.executor().run_until_parked();
27076 assert_eq!(
27077 diagnostic_requests.load(atomic::Ordering::Acquire),
27078 2,
27079 "Cursor movement should not trigger diagnostic request"
27080 );
27081 ensure_result_id(Some(SharedString::new("2")), cx);
27082 // Multiple rapid edits should be debounced
27083 for _ in 0..5 {
27084 editor.update_in(cx, |editor, window, cx| {
27085 editor.handle_input("x", window, cx)
27086 });
27087 }
27088 cx.executor().advance_clock(Duration::from_millis(60));
27089 cx.executor().run_until_parked();
27090
27091 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27092 assert!(
27093 final_requests <= 4,
27094 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27095 );
27096 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27097}
27098
27099#[gpui::test]
27100async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27101 // Regression test for issue #11671
27102 // Previously, adding a cursor after moving multiple cursors would reset
27103 // the cursor count instead of adding to the existing cursors.
27104 init_test(cx, |_| {});
27105 let mut cx = EditorTestContext::new(cx).await;
27106
27107 // Create a simple buffer with cursor at start
27108 cx.set_state(indoc! {"
27109 ˇaaaa
27110 bbbb
27111 cccc
27112 dddd
27113 eeee
27114 ffff
27115 gggg
27116 hhhh"});
27117
27118 // Add 2 cursors below (so we have 3 total)
27119 cx.update_editor(|editor, window, cx| {
27120 editor.add_selection_below(&Default::default(), window, cx);
27121 editor.add_selection_below(&Default::default(), window, cx);
27122 });
27123
27124 // Verify we have 3 cursors
27125 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27126 assert_eq!(
27127 initial_count, 3,
27128 "Should have 3 cursors after adding 2 below"
27129 );
27130
27131 // Move down one line
27132 cx.update_editor(|editor, window, cx| {
27133 editor.move_down(&MoveDown, window, cx);
27134 });
27135
27136 // Add another cursor below
27137 cx.update_editor(|editor, window, cx| {
27138 editor.add_selection_below(&Default::default(), window, cx);
27139 });
27140
27141 // Should now have 4 cursors (3 original + 1 new)
27142 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27143 assert_eq!(
27144 final_count, 4,
27145 "Should have 4 cursors after moving and adding another"
27146 );
27147}
27148
27149#[gpui::test]
27150async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27151 init_test(cx, |_| {});
27152
27153 let mut cx = EditorTestContext::new(cx).await;
27154
27155 cx.set_state(indoc!(
27156 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27157 Second line here"#
27158 ));
27159
27160 cx.update_editor(|editor, window, cx| {
27161 // Enable soft wrapping with a narrow width to force soft wrapping and
27162 // confirm that more than 2 rows are being displayed.
27163 editor.set_wrap_width(Some(100.0.into()), cx);
27164 assert!(editor.display_text(cx).lines().count() > 2);
27165
27166 editor.add_selection_below(
27167 &AddSelectionBelow {
27168 skip_soft_wrap: true,
27169 },
27170 window,
27171 cx,
27172 );
27173
27174 assert_eq!(
27175 display_ranges(editor, cx),
27176 &[
27177 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27178 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27179 ]
27180 );
27181
27182 editor.add_selection_above(
27183 &AddSelectionAbove {
27184 skip_soft_wrap: true,
27185 },
27186 window,
27187 cx,
27188 );
27189
27190 assert_eq!(
27191 display_ranges(editor, cx),
27192 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27193 );
27194
27195 editor.add_selection_below(
27196 &AddSelectionBelow {
27197 skip_soft_wrap: false,
27198 },
27199 window,
27200 cx,
27201 );
27202
27203 assert_eq!(
27204 display_ranges(editor, cx),
27205 &[
27206 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27207 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27208 ]
27209 );
27210
27211 editor.add_selection_above(
27212 &AddSelectionAbove {
27213 skip_soft_wrap: false,
27214 },
27215 window,
27216 cx,
27217 );
27218
27219 assert_eq!(
27220 display_ranges(editor, cx),
27221 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27222 );
27223 });
27224}
27225
27226#[gpui::test]
27227async fn test_insert_snippet(cx: &mut TestAppContext) {
27228 init_test(cx, |_| {});
27229 let mut cx = EditorTestContext::new(cx).await;
27230
27231 cx.update_editor(|editor, _, cx| {
27232 editor.project().unwrap().update(cx, |project, cx| {
27233 project.snippets().update(cx, |snippets, _cx| {
27234 let snippet = project::snippet_provider::Snippet {
27235 prefix: vec![], // no prefix needed!
27236 body: "an Unspecified".to_string(),
27237 description: Some("shhhh it's a secret".to_string()),
27238 name: "super secret snippet".to_string(),
27239 };
27240 snippets.add_snippet_for_test(
27241 None,
27242 PathBuf::from("test_snippets.json"),
27243 vec![Arc::new(snippet)],
27244 );
27245
27246 let snippet = project::snippet_provider::Snippet {
27247 prefix: vec![], // no prefix needed!
27248 body: " Location".to_string(),
27249 description: Some("the word 'location'".to_string()),
27250 name: "location word".to_string(),
27251 };
27252 snippets.add_snippet_for_test(
27253 Some("Markdown".to_string()),
27254 PathBuf::from("test_snippets.json"),
27255 vec![Arc::new(snippet)],
27256 );
27257 });
27258 })
27259 });
27260
27261 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27262
27263 cx.update_editor(|editor, window, cx| {
27264 editor.insert_snippet_at_selections(
27265 &InsertSnippet {
27266 language: None,
27267 name: Some("super secret snippet".to_string()),
27268 snippet: None,
27269 },
27270 window,
27271 cx,
27272 );
27273
27274 // Language is specified in the action,
27275 // so the buffer language does not need to match
27276 editor.insert_snippet_at_selections(
27277 &InsertSnippet {
27278 language: Some("Markdown".to_string()),
27279 name: Some("location word".to_string()),
27280 snippet: None,
27281 },
27282 window,
27283 cx,
27284 );
27285
27286 editor.insert_snippet_at_selections(
27287 &InsertSnippet {
27288 language: None,
27289 name: None,
27290 snippet: Some("$0 after".to_string()),
27291 },
27292 window,
27293 cx,
27294 );
27295 });
27296
27297 cx.assert_editor_state(
27298 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27299 );
27300}
27301
27302#[gpui::test(iterations = 10)]
27303async fn test_document_colors(cx: &mut TestAppContext) {
27304 let expected_color = Rgba {
27305 r: 0.33,
27306 g: 0.33,
27307 b: 0.33,
27308 a: 0.33,
27309 };
27310
27311 init_test(cx, |_| {});
27312
27313 let fs = FakeFs::new(cx.executor());
27314 fs.insert_tree(
27315 path!("/a"),
27316 json!({
27317 "first.rs": "fn main() { let a = 5; }",
27318 }),
27319 )
27320 .await;
27321
27322 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27323 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27324 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27325
27326 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27327 language_registry.add(rust_lang());
27328 let mut fake_servers = language_registry.register_fake_lsp(
27329 "Rust",
27330 FakeLspAdapter {
27331 capabilities: lsp::ServerCapabilities {
27332 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27333 ..lsp::ServerCapabilities::default()
27334 },
27335 name: "rust-analyzer",
27336 ..FakeLspAdapter::default()
27337 },
27338 );
27339 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27340 "Rust",
27341 FakeLspAdapter {
27342 capabilities: lsp::ServerCapabilities {
27343 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27344 ..lsp::ServerCapabilities::default()
27345 },
27346 name: "not-rust-analyzer",
27347 ..FakeLspAdapter::default()
27348 },
27349 );
27350
27351 let editor = workspace
27352 .update(cx, |workspace, window, cx| {
27353 workspace.open_abs_path(
27354 PathBuf::from(path!("/a/first.rs")),
27355 OpenOptions::default(),
27356 window,
27357 cx,
27358 )
27359 })
27360 .unwrap()
27361 .await
27362 .unwrap()
27363 .downcast::<Editor>()
27364 .unwrap();
27365 let fake_language_server = fake_servers.next().await.unwrap();
27366 let fake_language_server_without_capabilities =
27367 fake_servers_without_capabilities.next().await.unwrap();
27368 let requests_made = Arc::new(AtomicUsize::new(0));
27369 let closure_requests_made = Arc::clone(&requests_made);
27370 let mut color_request_handle = fake_language_server
27371 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27372 let requests_made = Arc::clone(&closure_requests_made);
27373 async move {
27374 assert_eq!(
27375 params.text_document.uri,
27376 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27377 );
27378 requests_made.fetch_add(1, atomic::Ordering::Release);
27379 Ok(vec![
27380 lsp::ColorInformation {
27381 range: lsp::Range {
27382 start: lsp::Position {
27383 line: 0,
27384 character: 0,
27385 },
27386 end: lsp::Position {
27387 line: 0,
27388 character: 1,
27389 },
27390 },
27391 color: lsp::Color {
27392 red: 0.33,
27393 green: 0.33,
27394 blue: 0.33,
27395 alpha: 0.33,
27396 },
27397 },
27398 lsp::ColorInformation {
27399 range: lsp::Range {
27400 start: lsp::Position {
27401 line: 0,
27402 character: 0,
27403 },
27404 end: lsp::Position {
27405 line: 0,
27406 character: 1,
27407 },
27408 },
27409 color: lsp::Color {
27410 red: 0.33,
27411 green: 0.33,
27412 blue: 0.33,
27413 alpha: 0.33,
27414 },
27415 },
27416 ])
27417 }
27418 });
27419
27420 let _handle = fake_language_server_without_capabilities
27421 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27422 panic!("Should not be called");
27423 });
27424 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27425 color_request_handle.next().await.unwrap();
27426 cx.run_until_parked();
27427 assert_eq!(
27428 1,
27429 requests_made.load(atomic::Ordering::Acquire),
27430 "Should query for colors once per editor open"
27431 );
27432 editor.update_in(cx, |editor, _, cx| {
27433 assert_eq!(
27434 vec![expected_color],
27435 extract_color_inlays(editor, cx),
27436 "Should have an initial inlay"
27437 );
27438 });
27439
27440 // opening another file in a split should not influence the LSP query counter
27441 workspace
27442 .update(cx, |workspace, window, cx| {
27443 assert_eq!(
27444 workspace.panes().len(),
27445 1,
27446 "Should have one pane with one editor"
27447 );
27448 workspace.move_item_to_pane_in_direction(
27449 &MoveItemToPaneInDirection {
27450 direction: SplitDirection::Right,
27451 focus: false,
27452 clone: true,
27453 },
27454 window,
27455 cx,
27456 );
27457 })
27458 .unwrap();
27459 cx.run_until_parked();
27460 workspace
27461 .update(cx, |workspace, _, cx| {
27462 let panes = workspace.panes();
27463 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27464 for pane in panes {
27465 let editor = pane
27466 .read(cx)
27467 .active_item()
27468 .and_then(|item| item.downcast::<Editor>())
27469 .expect("Should have opened an editor in each split");
27470 let editor_file = editor
27471 .read(cx)
27472 .buffer()
27473 .read(cx)
27474 .as_singleton()
27475 .expect("test deals with singleton buffers")
27476 .read(cx)
27477 .file()
27478 .expect("test buffese should have a file")
27479 .path();
27480 assert_eq!(
27481 editor_file.as_ref(),
27482 rel_path("first.rs"),
27483 "Both editors should be opened for the same file"
27484 )
27485 }
27486 })
27487 .unwrap();
27488
27489 cx.executor().advance_clock(Duration::from_millis(500));
27490 let save = editor.update_in(cx, |editor, window, cx| {
27491 editor.move_to_end(&MoveToEnd, window, cx);
27492 editor.handle_input("dirty", window, cx);
27493 editor.save(
27494 SaveOptions {
27495 format: true,
27496 autosave: true,
27497 },
27498 project.clone(),
27499 window,
27500 cx,
27501 )
27502 });
27503 save.await.unwrap();
27504
27505 color_request_handle.next().await.unwrap();
27506 cx.run_until_parked();
27507 assert_eq!(
27508 2,
27509 requests_made.load(atomic::Ordering::Acquire),
27510 "Should query for colors once per save (deduplicated) and once per formatting after save"
27511 );
27512
27513 drop(editor);
27514 let close = workspace
27515 .update(cx, |workspace, window, cx| {
27516 workspace.active_pane().update(cx, |pane, cx| {
27517 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27518 })
27519 })
27520 .unwrap();
27521 close.await.unwrap();
27522 let close = workspace
27523 .update(cx, |workspace, window, cx| {
27524 workspace.active_pane().update(cx, |pane, cx| {
27525 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27526 })
27527 })
27528 .unwrap();
27529 close.await.unwrap();
27530 assert_eq!(
27531 2,
27532 requests_made.load(atomic::Ordering::Acquire),
27533 "After saving and closing all editors, no extra requests should be made"
27534 );
27535 workspace
27536 .update(cx, |workspace, _, cx| {
27537 assert!(
27538 workspace.active_item(cx).is_none(),
27539 "Should close all editors"
27540 )
27541 })
27542 .unwrap();
27543
27544 workspace
27545 .update(cx, |workspace, window, cx| {
27546 workspace.active_pane().update(cx, |pane, cx| {
27547 pane.navigate_backward(&workspace::GoBack, window, cx);
27548 })
27549 })
27550 .unwrap();
27551 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27552 cx.run_until_parked();
27553 let editor = workspace
27554 .update(cx, |workspace, _, cx| {
27555 workspace
27556 .active_item(cx)
27557 .expect("Should have reopened the editor again after navigating back")
27558 .downcast::<Editor>()
27559 .expect("Should be an editor")
27560 })
27561 .unwrap();
27562
27563 assert_eq!(
27564 2,
27565 requests_made.load(atomic::Ordering::Acquire),
27566 "Cache should be reused on buffer close and reopen"
27567 );
27568 editor.update(cx, |editor, cx| {
27569 assert_eq!(
27570 vec![expected_color],
27571 extract_color_inlays(editor, cx),
27572 "Should have an initial inlay"
27573 );
27574 });
27575
27576 drop(color_request_handle);
27577 let closure_requests_made = Arc::clone(&requests_made);
27578 let mut empty_color_request_handle = fake_language_server
27579 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27580 let requests_made = Arc::clone(&closure_requests_made);
27581 async move {
27582 assert_eq!(
27583 params.text_document.uri,
27584 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27585 );
27586 requests_made.fetch_add(1, atomic::Ordering::Release);
27587 Ok(Vec::new())
27588 }
27589 });
27590 let save = editor.update_in(cx, |editor, window, cx| {
27591 editor.move_to_end(&MoveToEnd, window, cx);
27592 editor.handle_input("dirty_again", window, cx);
27593 editor.save(
27594 SaveOptions {
27595 format: false,
27596 autosave: true,
27597 },
27598 project.clone(),
27599 window,
27600 cx,
27601 )
27602 });
27603 save.await.unwrap();
27604
27605 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27606 empty_color_request_handle.next().await.unwrap();
27607 cx.run_until_parked();
27608 assert_eq!(
27609 3,
27610 requests_made.load(atomic::Ordering::Acquire),
27611 "Should query for colors once per save only, as formatting was not requested"
27612 );
27613 editor.update(cx, |editor, cx| {
27614 assert_eq!(
27615 Vec::<Rgba>::new(),
27616 extract_color_inlays(editor, cx),
27617 "Should clear all colors when the server returns an empty response"
27618 );
27619 });
27620}
27621
27622#[gpui::test]
27623async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27624 init_test(cx, |_| {});
27625 let (editor, cx) = cx.add_window_view(Editor::single_line);
27626 editor.update_in(cx, |editor, window, cx| {
27627 editor.set_text("oops\n\nwow\n", window, cx)
27628 });
27629 cx.run_until_parked();
27630 editor.update(cx, |editor, cx| {
27631 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27632 });
27633 editor.update(cx, |editor, cx| {
27634 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27635 });
27636 cx.run_until_parked();
27637 editor.update(cx, |editor, cx| {
27638 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27639 });
27640}
27641
27642#[gpui::test]
27643async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27644 init_test(cx, |_| {});
27645
27646 cx.update(|cx| {
27647 register_project_item::<Editor>(cx);
27648 });
27649
27650 let fs = FakeFs::new(cx.executor());
27651 fs.insert_tree("/root1", json!({})).await;
27652 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27653 .await;
27654
27655 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27656 let (workspace, cx) =
27657 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27658
27659 let worktree_id = project.update(cx, |project, cx| {
27660 project.worktrees(cx).next().unwrap().read(cx).id()
27661 });
27662
27663 let handle = workspace
27664 .update_in(cx, |workspace, window, cx| {
27665 let project_path = (worktree_id, rel_path("one.pdf"));
27666 workspace.open_path(project_path, None, true, window, cx)
27667 })
27668 .await
27669 .unwrap();
27670
27671 assert_eq!(
27672 handle.to_any_view().entity_type(),
27673 TypeId::of::<InvalidItemView>()
27674 );
27675}
27676
27677#[gpui::test]
27678async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27679 init_test(cx, |_| {});
27680
27681 let language = Arc::new(Language::new(
27682 LanguageConfig::default(),
27683 Some(tree_sitter_rust::LANGUAGE.into()),
27684 ));
27685
27686 // Test hierarchical sibling navigation
27687 let text = r#"
27688 fn outer() {
27689 if condition {
27690 let a = 1;
27691 }
27692 let b = 2;
27693 }
27694
27695 fn another() {
27696 let c = 3;
27697 }
27698 "#;
27699
27700 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27701 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27702 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27703
27704 // Wait for parsing to complete
27705 editor
27706 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27707 .await;
27708
27709 editor.update_in(cx, |editor, window, cx| {
27710 // Start by selecting "let a = 1;" inside the if block
27711 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27712 s.select_display_ranges([
27713 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27714 ]);
27715 });
27716
27717 let initial_selection = editor
27718 .selections
27719 .display_ranges(&editor.display_snapshot(cx));
27720 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27721
27722 // Test select next sibling - should move up levels to find the next sibling
27723 // Since "let a = 1;" has no siblings in the if block, it should move up
27724 // to find "let b = 2;" which is a sibling of the if block
27725 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27726 let next_selection = editor
27727 .selections
27728 .display_ranges(&editor.display_snapshot(cx));
27729
27730 // Should have a selection and it should be different from the initial
27731 assert_eq!(
27732 next_selection.len(),
27733 1,
27734 "Should have one selection after next"
27735 );
27736 assert_ne!(
27737 next_selection[0], initial_selection[0],
27738 "Next sibling selection should be different"
27739 );
27740
27741 // Test hierarchical navigation by going to the end of the current function
27742 // and trying to navigate to the next function
27743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27744 s.select_display_ranges([
27745 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27746 ]);
27747 });
27748
27749 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27750 let function_next_selection = editor
27751 .selections
27752 .display_ranges(&editor.display_snapshot(cx));
27753
27754 // Should move to the next function
27755 assert_eq!(
27756 function_next_selection.len(),
27757 1,
27758 "Should have one selection after function next"
27759 );
27760
27761 // Test select previous sibling navigation
27762 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27763 let prev_selection = editor
27764 .selections
27765 .display_ranges(&editor.display_snapshot(cx));
27766
27767 // Should have a selection and it should be different
27768 assert_eq!(
27769 prev_selection.len(),
27770 1,
27771 "Should have one selection after prev"
27772 );
27773 assert_ne!(
27774 prev_selection[0], function_next_selection[0],
27775 "Previous sibling selection should be different from next"
27776 );
27777 });
27778}
27779
27780#[gpui::test]
27781async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27782 init_test(cx, |_| {});
27783
27784 let mut cx = EditorTestContext::new(cx).await;
27785 cx.set_state(
27786 "let ˇvariable = 42;
27787let another = variable + 1;
27788let result = variable * 2;",
27789 );
27790
27791 // Set up document highlights manually (simulating LSP response)
27792 cx.update_editor(|editor, _window, cx| {
27793 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27794
27795 // Create highlights for "variable" occurrences
27796 let highlight_ranges = [
27797 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27798 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27799 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27800 ];
27801
27802 let anchor_ranges: Vec<_> = highlight_ranges
27803 .iter()
27804 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27805 .collect();
27806
27807 editor.highlight_background::<DocumentHighlightRead>(
27808 &anchor_ranges,
27809 |_, theme| theme.colors().editor_document_highlight_read_background,
27810 cx,
27811 );
27812 });
27813
27814 // Go to next highlight - should move to second "variable"
27815 cx.update_editor(|editor, window, cx| {
27816 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27817 });
27818 cx.assert_editor_state(
27819 "let variable = 42;
27820let another = ˇvariable + 1;
27821let result = variable * 2;",
27822 );
27823
27824 // Go to next highlight - should move to third "variable"
27825 cx.update_editor(|editor, window, cx| {
27826 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27827 });
27828 cx.assert_editor_state(
27829 "let variable = 42;
27830let another = variable + 1;
27831let result = ˇvariable * 2;",
27832 );
27833
27834 // Go to next highlight - should stay at third "variable" (no wrap-around)
27835 cx.update_editor(|editor, window, cx| {
27836 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27837 });
27838 cx.assert_editor_state(
27839 "let variable = 42;
27840let another = variable + 1;
27841let result = ˇvariable * 2;",
27842 );
27843
27844 // Now test going backwards from third position
27845 cx.update_editor(|editor, window, cx| {
27846 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27847 });
27848 cx.assert_editor_state(
27849 "let variable = 42;
27850let another = ˇvariable + 1;
27851let result = variable * 2;",
27852 );
27853
27854 // Go to previous highlight - should move to first "variable"
27855 cx.update_editor(|editor, window, cx| {
27856 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27857 });
27858 cx.assert_editor_state(
27859 "let ˇvariable = 42;
27860let another = variable + 1;
27861let result = variable * 2;",
27862 );
27863
27864 // Go to previous highlight - should stay on first "variable"
27865 cx.update_editor(|editor, window, cx| {
27866 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27867 });
27868 cx.assert_editor_state(
27869 "let ˇvariable = 42;
27870let another = variable + 1;
27871let result = variable * 2;",
27872 );
27873}
27874
27875#[gpui::test]
27876async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27877 cx: &mut gpui::TestAppContext,
27878) {
27879 init_test(cx, |_| {});
27880
27881 let url = "https://zed.dev";
27882
27883 let markdown_language = Arc::new(Language::new(
27884 LanguageConfig {
27885 name: "Markdown".into(),
27886 ..LanguageConfig::default()
27887 },
27888 None,
27889 ));
27890
27891 let mut cx = EditorTestContext::new(cx).await;
27892 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27893 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27894
27895 cx.update_editor(|editor, window, cx| {
27896 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27897 editor.paste(&Paste, window, cx);
27898 });
27899
27900 cx.assert_editor_state(&format!(
27901 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27902 ));
27903}
27904
27905#[gpui::test]
27906async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
27907 init_test(cx, |_| {});
27908
27909 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
27910 let mut cx = EditorTestContext::new(cx).await;
27911
27912 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27913
27914 // Case 1: Test if adding a character with multi cursors preserves nested list indents
27915 cx.set_state(&indoc! {"
27916 - [ ] Item 1
27917 - [ ] Item 1.a
27918 - [ˇ] Item 2
27919 - [ˇ] Item 2.a
27920 - [ˇ] Item 2.b
27921 "
27922 });
27923 cx.update_editor(|editor, window, cx| {
27924 editor.handle_input("x", window, cx);
27925 });
27926 cx.run_until_parked();
27927 cx.assert_editor_state(indoc! {"
27928 - [ ] Item 1
27929 - [ ] Item 1.a
27930 - [xˇ] Item 2
27931 - [xˇ] Item 2.a
27932 - [xˇ] Item 2.b
27933 "
27934 });
27935
27936 // Case 2: Test adding new line after nested list preserves indent of previous line
27937 cx.set_state(&indoc! {"
27938 - [ ] Item 1
27939 - [ ] Item 1.a
27940 - [x] Item 2
27941 - [x] Item 2.a
27942 - [x] Item 2.bˇ"
27943 });
27944 cx.update_editor(|editor, window, cx| {
27945 editor.newline(&Newline, window, cx);
27946 });
27947 cx.assert_editor_state(indoc! {"
27948 - [ ] Item 1
27949 - [ ] Item 1.a
27950 - [x] Item 2
27951 - [x] Item 2.a
27952 - [x] Item 2.b
27953 ˇ"
27954 });
27955
27956 // Case 3: Test adding a new nested list item preserves indent
27957 cx.set_state(&indoc! {"
27958 - [ ] Item 1
27959 - [ ] Item 1.a
27960 - [x] Item 2
27961 - [x] Item 2.a
27962 - [x] Item 2.b
27963 ˇ"
27964 });
27965 cx.update_editor(|editor, window, cx| {
27966 editor.handle_input("-", window, cx);
27967 });
27968 cx.run_until_parked();
27969 cx.assert_editor_state(indoc! {"
27970 - [ ] Item 1
27971 - [ ] Item 1.a
27972 - [x] Item 2
27973 - [x] Item 2.a
27974 - [x] Item 2.b
27975 -ˇ"
27976 });
27977 cx.update_editor(|editor, window, cx| {
27978 editor.handle_input(" [x] Item 2.c", window, cx);
27979 });
27980 cx.run_until_parked();
27981 cx.assert_editor_state(indoc! {"
27982 - [ ] Item 1
27983 - [ ] Item 1.a
27984 - [x] Item 2
27985 - [x] Item 2.a
27986 - [x] Item 2.b
27987 - [x] Item 2.cˇ"
27988 });
27989
27990 // Case 4: Test adding new line after nested ordered list preserves indent of previous line
27991 cx.set_state(indoc! {"
27992 1. Item 1
27993 1. Item 1.a
27994 2. Item 2
27995 1. Item 2.a
27996 2. Item 2.bˇ"
27997 });
27998 cx.update_editor(|editor, window, cx| {
27999 editor.newline(&Newline, window, cx);
28000 });
28001 cx.assert_editor_state(indoc! {"
28002 1. Item 1
28003 1. Item 1.a
28004 2. Item 2
28005 1. Item 2.a
28006 2. Item 2.b
28007 ˇ"
28008 });
28009
28010 // Case 5: Adding new ordered list item preserves indent
28011 cx.set_state(indoc! {"
28012 1. Item 1
28013 1. Item 1.a
28014 2. Item 2
28015 1. Item 2.a
28016 2. Item 2.b
28017 ˇ"
28018 });
28019 cx.update_editor(|editor, window, cx| {
28020 editor.handle_input("3", window, cx);
28021 });
28022 cx.run_until_parked();
28023 cx.assert_editor_state(indoc! {"
28024 1. Item 1
28025 1. Item 1.a
28026 2. Item 2
28027 1. Item 2.a
28028 2. Item 2.b
28029 3ˇ"
28030 });
28031 cx.update_editor(|editor, window, cx| {
28032 editor.handle_input(".", window, cx);
28033 });
28034 cx.run_until_parked();
28035 cx.assert_editor_state(indoc! {"
28036 1. Item 1
28037 1. Item 1.a
28038 2. Item 2
28039 1. Item 2.a
28040 2. Item 2.b
28041 3.ˇ"
28042 });
28043 cx.update_editor(|editor, window, cx| {
28044 editor.handle_input(" Item 2.c", window, cx);
28045 });
28046 cx.run_until_parked();
28047 cx.assert_editor_state(indoc! {"
28048 1. Item 1
28049 1. Item 1.a
28050 2. Item 2
28051 1. Item 2.a
28052 2. Item 2.b
28053 3. Item 2.cˇ"
28054 });
28055
28056 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28057 cx.set_state(indoc! {"
28058 - Item 1
28059 - Item 1.a
28060 - Item 1.a
28061 ˇ"});
28062 cx.update_editor(|editor, window, cx| {
28063 editor.handle_input("-", window, cx);
28064 });
28065 cx.run_until_parked();
28066 cx.assert_editor_state(indoc! {"
28067 - Item 1
28068 - Item 1.a
28069 - Item 1.a
28070 -ˇ"});
28071
28072 // Case 7: Test blockquote newline preserves something
28073 cx.set_state(indoc! {"
28074 > Item 1ˇ"
28075 });
28076 cx.update_editor(|editor, window, cx| {
28077 editor.newline(&Newline, window, cx);
28078 });
28079 cx.assert_editor_state(indoc! {"
28080 > Item 1
28081 ˇ"
28082 });
28083}
28084
28085#[gpui::test]
28086async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28087 cx: &mut gpui::TestAppContext,
28088) {
28089 init_test(cx, |_| {});
28090
28091 let url = "https://zed.dev";
28092
28093 let markdown_language = Arc::new(Language::new(
28094 LanguageConfig {
28095 name: "Markdown".into(),
28096 ..LanguageConfig::default()
28097 },
28098 None,
28099 ));
28100
28101 let mut cx = EditorTestContext::new(cx).await;
28102 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28103 cx.set_state(&format!(
28104 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28105 ));
28106
28107 cx.update_editor(|editor, window, cx| {
28108 editor.copy(&Copy, window, cx);
28109 });
28110
28111 cx.set_state(&format!(
28112 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28113 ));
28114
28115 cx.update_editor(|editor, window, cx| {
28116 editor.paste(&Paste, window, cx);
28117 });
28118
28119 cx.assert_editor_state(&format!(
28120 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28121 ));
28122}
28123
28124#[gpui::test]
28125async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28126 cx: &mut gpui::TestAppContext,
28127) {
28128 init_test(cx, |_| {});
28129
28130 let url = "https://zed.dev";
28131
28132 let markdown_language = Arc::new(Language::new(
28133 LanguageConfig {
28134 name: "Markdown".into(),
28135 ..LanguageConfig::default()
28136 },
28137 None,
28138 ));
28139
28140 let mut cx = EditorTestContext::new(cx).await;
28141 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28142 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28143
28144 cx.update_editor(|editor, window, cx| {
28145 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28146 editor.paste(&Paste, window, cx);
28147 });
28148
28149 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28150}
28151
28152#[gpui::test]
28153async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28154 cx: &mut gpui::TestAppContext,
28155) {
28156 init_test(cx, |_| {});
28157
28158 let text = "Awesome";
28159
28160 let markdown_language = Arc::new(Language::new(
28161 LanguageConfig {
28162 name: "Markdown".into(),
28163 ..LanguageConfig::default()
28164 },
28165 None,
28166 ));
28167
28168 let mut cx = EditorTestContext::new(cx).await;
28169 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28170 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28171
28172 cx.update_editor(|editor, window, cx| {
28173 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28174 editor.paste(&Paste, window, cx);
28175 });
28176
28177 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28178}
28179
28180#[gpui::test]
28181async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28182 cx: &mut gpui::TestAppContext,
28183) {
28184 init_test(cx, |_| {});
28185
28186 let url = "https://zed.dev";
28187
28188 let markdown_language = Arc::new(Language::new(
28189 LanguageConfig {
28190 name: "Rust".into(),
28191 ..LanguageConfig::default()
28192 },
28193 None,
28194 ));
28195
28196 let mut cx = EditorTestContext::new(cx).await;
28197 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28198 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28199
28200 cx.update_editor(|editor, window, cx| {
28201 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28202 editor.paste(&Paste, window, cx);
28203 });
28204
28205 cx.assert_editor_state(&format!(
28206 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28207 ));
28208}
28209
28210#[gpui::test]
28211async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28212 cx: &mut TestAppContext,
28213) {
28214 init_test(cx, |_| {});
28215
28216 let url = "https://zed.dev";
28217
28218 let markdown_language = Arc::new(Language::new(
28219 LanguageConfig {
28220 name: "Markdown".into(),
28221 ..LanguageConfig::default()
28222 },
28223 None,
28224 ));
28225
28226 let (editor, cx) = cx.add_window_view(|window, cx| {
28227 let multi_buffer = MultiBuffer::build_multi(
28228 [
28229 ("this will embed -> link", vec![Point::row_range(0..1)]),
28230 ("this will replace -> link", vec![Point::row_range(0..1)]),
28231 ],
28232 cx,
28233 );
28234 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28236 s.select_ranges(vec![
28237 Point::new(0, 19)..Point::new(0, 23),
28238 Point::new(1, 21)..Point::new(1, 25),
28239 ])
28240 });
28241 let first_buffer_id = multi_buffer
28242 .read(cx)
28243 .excerpt_buffer_ids()
28244 .into_iter()
28245 .next()
28246 .unwrap();
28247 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28248 first_buffer.update(cx, |buffer, cx| {
28249 buffer.set_language(Some(markdown_language.clone()), cx);
28250 });
28251
28252 editor
28253 });
28254 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28255
28256 cx.update_editor(|editor, window, cx| {
28257 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28258 editor.paste(&Paste, window, cx);
28259 });
28260
28261 cx.assert_editor_state(&format!(
28262 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28263 ));
28264}
28265
28266#[gpui::test]
28267async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28268 init_test(cx, |_| {});
28269
28270 let fs = FakeFs::new(cx.executor());
28271 fs.insert_tree(
28272 path!("/project"),
28273 json!({
28274 "first.rs": "# First Document\nSome content here.",
28275 "second.rs": "Plain text content for second file.",
28276 }),
28277 )
28278 .await;
28279
28280 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28281 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28282 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28283
28284 let language = rust_lang();
28285 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28286 language_registry.add(language.clone());
28287 let mut fake_servers = language_registry.register_fake_lsp(
28288 "Rust",
28289 FakeLspAdapter {
28290 ..FakeLspAdapter::default()
28291 },
28292 );
28293
28294 let buffer1 = project
28295 .update(cx, |project, cx| {
28296 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28297 })
28298 .await
28299 .unwrap();
28300 let buffer2 = project
28301 .update(cx, |project, cx| {
28302 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28303 })
28304 .await
28305 .unwrap();
28306
28307 let multi_buffer = cx.new(|cx| {
28308 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28309 multi_buffer.set_excerpts_for_path(
28310 PathKey::for_buffer(&buffer1, cx),
28311 buffer1.clone(),
28312 [Point::zero()..buffer1.read(cx).max_point()],
28313 3,
28314 cx,
28315 );
28316 multi_buffer.set_excerpts_for_path(
28317 PathKey::for_buffer(&buffer2, cx),
28318 buffer2.clone(),
28319 [Point::zero()..buffer1.read(cx).max_point()],
28320 3,
28321 cx,
28322 );
28323 multi_buffer
28324 });
28325
28326 let (editor, cx) = cx.add_window_view(|window, cx| {
28327 Editor::new(
28328 EditorMode::full(),
28329 multi_buffer,
28330 Some(project.clone()),
28331 window,
28332 cx,
28333 )
28334 });
28335
28336 let fake_language_server = fake_servers.next().await.unwrap();
28337
28338 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28339
28340 let save = editor.update_in(cx, |editor, window, cx| {
28341 assert!(editor.is_dirty(cx));
28342
28343 editor.save(
28344 SaveOptions {
28345 format: true,
28346 autosave: true,
28347 },
28348 project,
28349 window,
28350 cx,
28351 )
28352 });
28353 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28354 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28355 let mut done_edit_rx = Some(done_edit_rx);
28356 let mut start_edit_tx = Some(start_edit_tx);
28357
28358 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28359 start_edit_tx.take().unwrap().send(()).unwrap();
28360 let done_edit_rx = done_edit_rx.take().unwrap();
28361 async move {
28362 done_edit_rx.await.unwrap();
28363 Ok(None)
28364 }
28365 });
28366
28367 start_edit_rx.await.unwrap();
28368 buffer2
28369 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28370 .unwrap();
28371
28372 done_edit_tx.send(()).unwrap();
28373
28374 save.await.unwrap();
28375 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28376}
28377
28378#[track_caller]
28379fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28380 editor
28381 .all_inlays(cx)
28382 .into_iter()
28383 .filter_map(|inlay| inlay.get_color())
28384 .map(Rgba::from)
28385 .collect()
28386}
28387
28388#[gpui::test]
28389fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28390 init_test(cx, |_| {});
28391
28392 let editor = cx.add_window(|window, cx| {
28393 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28394 build_editor(buffer, window, cx)
28395 });
28396
28397 editor
28398 .update(cx, |editor, window, cx| {
28399 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28400 s.select_display_ranges([
28401 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28402 ])
28403 });
28404
28405 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28406
28407 assert_eq!(
28408 editor.display_text(cx),
28409 "line1\nline2\nline2",
28410 "Duplicating last line upward should create duplicate above, not on same line"
28411 );
28412
28413 assert_eq!(
28414 editor
28415 .selections
28416 .display_ranges(&editor.display_snapshot(cx)),
28417 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28418 "Selection should move to the duplicated line"
28419 );
28420 })
28421 .unwrap();
28422}
28423
28424#[gpui::test]
28425async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28426 init_test(cx, |_| {});
28427
28428 let mut cx = EditorTestContext::new(cx).await;
28429
28430 cx.set_state("line1\nline2ˇ");
28431
28432 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28433
28434 let clipboard_text = cx
28435 .read_from_clipboard()
28436 .and_then(|item| item.text().as_deref().map(str::to_string));
28437
28438 assert_eq!(
28439 clipboard_text,
28440 Some("line2\n".to_string()),
28441 "Copying a line without trailing newline should include a newline"
28442 );
28443
28444 cx.set_state("line1\nˇ");
28445
28446 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28447
28448 cx.assert_editor_state("line1\nline2\nˇ");
28449}
28450
28451#[gpui::test]
28452async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28453 init_test(cx, |_| {});
28454
28455 let mut cx = EditorTestContext::new(cx).await;
28456
28457 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28458
28459 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28460
28461 let clipboard_text = cx
28462 .read_from_clipboard()
28463 .and_then(|item| item.text().as_deref().map(str::to_string));
28464
28465 assert_eq!(
28466 clipboard_text,
28467 Some("line1\nline2\nline3\n".to_string()),
28468 "Copying multiple lines should include a single newline between lines"
28469 );
28470
28471 cx.set_state("lineA\nˇ");
28472
28473 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28474
28475 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28476}
28477
28478#[gpui::test]
28479async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28480 init_test(cx, |_| {});
28481
28482 let mut cx = EditorTestContext::new(cx).await;
28483
28484 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28485
28486 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28487
28488 let clipboard_text = cx
28489 .read_from_clipboard()
28490 .and_then(|item| item.text().as_deref().map(str::to_string));
28491
28492 assert_eq!(
28493 clipboard_text,
28494 Some("line1\nline2\nline3\n".to_string()),
28495 "Copying multiple lines should include a single newline between lines"
28496 );
28497
28498 cx.set_state("lineA\nˇ");
28499
28500 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28501
28502 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28503}
28504
28505#[gpui::test]
28506async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28507 init_test(cx, |_| {});
28508
28509 let mut cx = EditorTestContext::new(cx).await;
28510
28511 cx.set_state("line1\nline2ˇ");
28512 cx.update_editor(|e, window, cx| {
28513 e.set_mode(EditorMode::SingleLine);
28514 assert!(e.key_context(window, cx).contains("end_of_input"));
28515 });
28516 cx.set_state("ˇline1\nline2");
28517 cx.update_editor(|e, window, cx| {
28518 assert!(!e.key_context(window, cx).contains("end_of_input"));
28519 });
28520 cx.set_state("line1ˇ\nline2");
28521 cx.update_editor(|e, window, cx| {
28522 assert!(!e.key_context(window, cx).contains("end_of_input"));
28523 });
28524}
28525
28526#[gpui::test]
28527async fn test_sticky_scroll(cx: &mut TestAppContext) {
28528 init_test(cx, |_| {});
28529 let mut cx = EditorTestContext::new(cx).await;
28530
28531 let buffer = indoc! {"
28532 ˇfn foo() {
28533 let abc = 123;
28534 }
28535 struct Bar;
28536 impl Bar {
28537 fn new() -> Self {
28538 Self
28539 }
28540 }
28541 fn baz() {
28542 }
28543 "};
28544 cx.set_state(&buffer);
28545
28546 cx.update_editor(|e, _, cx| {
28547 e.buffer()
28548 .read(cx)
28549 .as_singleton()
28550 .unwrap()
28551 .update(cx, |buffer, cx| {
28552 buffer.set_language(Some(rust_lang()), cx);
28553 })
28554 });
28555
28556 let mut sticky_headers = |offset: ScrollOffset| {
28557 cx.update_editor(|e, window, cx| {
28558 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28559 let style = e.style(cx).clone();
28560 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28561 .into_iter()
28562 .map(
28563 |StickyHeader {
28564 start_point,
28565 offset,
28566 ..
28567 }| { (start_point, offset) },
28568 )
28569 .collect::<Vec<_>>()
28570 })
28571 };
28572
28573 let fn_foo = Point { row: 0, column: 0 };
28574 let impl_bar = Point { row: 4, column: 0 };
28575 let fn_new = Point { row: 5, column: 4 };
28576
28577 assert_eq!(sticky_headers(0.0), vec![]);
28578 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28579 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28580 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28581 assert_eq!(sticky_headers(2.0), vec![]);
28582 assert_eq!(sticky_headers(2.5), vec![]);
28583 assert_eq!(sticky_headers(3.0), vec![]);
28584 assert_eq!(sticky_headers(3.5), vec![]);
28585 assert_eq!(sticky_headers(4.0), vec![]);
28586 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28587 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28588 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28589 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28590 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28591 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28592 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28593 assert_eq!(sticky_headers(8.0), vec![]);
28594 assert_eq!(sticky_headers(8.5), vec![]);
28595 assert_eq!(sticky_headers(9.0), vec![]);
28596 assert_eq!(sticky_headers(9.5), vec![]);
28597 assert_eq!(sticky_headers(10.0), vec![]);
28598}
28599
28600#[gpui::test]
28601async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
28602 init_test(cx, |_| {});
28603 cx.update(|cx| {
28604 SettingsStore::update_global(cx, |store, cx| {
28605 store.update_user_settings(cx, |settings| {
28606 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
28607 enabled: Some(true),
28608 })
28609 });
28610 });
28611 });
28612 let mut cx = EditorTestContext::new(cx).await;
28613
28614 let line_height = cx.update_editor(|editor, window, cx| {
28615 editor
28616 .style(cx)
28617 .text
28618 .line_height_in_pixels(window.rem_size())
28619 });
28620
28621 let buffer = indoc! {"
28622 ˇfn foo() {
28623 let abc = 123;
28624 }
28625 struct Bar;
28626 impl Bar {
28627 fn new() -> Self {
28628 Self
28629 }
28630 }
28631 fn baz() {
28632 }
28633 "};
28634 cx.set_state(&buffer);
28635
28636 cx.update_editor(|e, _, cx| {
28637 e.buffer()
28638 .read(cx)
28639 .as_singleton()
28640 .unwrap()
28641 .update(cx, |buffer, cx| {
28642 buffer.set_language(Some(rust_lang()), cx);
28643 })
28644 });
28645
28646 let fn_foo = || empty_range(0, 0);
28647 let impl_bar = || empty_range(4, 0);
28648 let fn_new = || empty_range(5, 4);
28649
28650 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
28651 cx.update_editor(|e, window, cx| {
28652 e.scroll(
28653 gpui::Point {
28654 x: 0.,
28655 y: scroll_offset,
28656 },
28657 None,
28658 window,
28659 cx,
28660 );
28661 });
28662 cx.simulate_click(
28663 gpui::Point {
28664 x: px(0.),
28665 y: click_offset as f32 * line_height,
28666 },
28667 Modifiers::none(),
28668 );
28669 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
28670 };
28671
28672 assert_eq!(
28673 scroll_and_click(
28674 4.5, // impl Bar is halfway off the screen
28675 0.0 // click top of screen
28676 ),
28677 // scrolled to impl Bar
28678 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28679 );
28680
28681 assert_eq!(
28682 scroll_and_click(
28683 4.5, // impl Bar is halfway off the screen
28684 0.25 // click middle of impl Bar
28685 ),
28686 // scrolled to impl Bar
28687 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28688 );
28689
28690 assert_eq!(
28691 scroll_and_click(
28692 4.5, // impl Bar is halfway off the screen
28693 1.5 // click below impl Bar (e.g. fn new())
28694 ),
28695 // scrolled to fn new() - this is below the impl Bar header which has persisted
28696 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28697 );
28698
28699 assert_eq!(
28700 scroll_and_click(
28701 5.5, // fn new is halfway underneath impl Bar
28702 0.75 // click on the overlap of impl Bar and fn new()
28703 ),
28704 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28705 );
28706
28707 assert_eq!(
28708 scroll_and_click(
28709 5.5, // fn new is halfway underneath impl Bar
28710 1.25 // click on the visible part of fn new()
28711 ),
28712 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28713 );
28714
28715 assert_eq!(
28716 scroll_and_click(
28717 1.5, // fn foo is halfway off the screen
28718 0.0 // click top of screen
28719 ),
28720 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28721 );
28722
28723 assert_eq!(
28724 scroll_and_click(
28725 1.5, // fn foo is halfway off the screen
28726 0.75 // click visible part of let abc...
28727 )
28728 .0,
28729 // no change in scroll
28730 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28731 (gpui::Point { x: 0., y: 1.5 })
28732 );
28733}
28734
28735#[gpui::test]
28736async fn test_next_prev_reference(cx: &mut TestAppContext) {
28737 const CYCLE_POSITIONS: &[&'static str] = &[
28738 indoc! {"
28739 fn foo() {
28740 let ˇabc = 123;
28741 let x = abc + 1;
28742 let y = abc + 2;
28743 let z = abc + 2;
28744 }
28745 "},
28746 indoc! {"
28747 fn foo() {
28748 let abc = 123;
28749 let x = ˇabc + 1;
28750 let y = abc + 2;
28751 let z = abc + 2;
28752 }
28753 "},
28754 indoc! {"
28755 fn foo() {
28756 let abc = 123;
28757 let x = abc + 1;
28758 let y = ˇabc + 2;
28759 let z = abc + 2;
28760 }
28761 "},
28762 indoc! {"
28763 fn foo() {
28764 let abc = 123;
28765 let x = abc + 1;
28766 let y = abc + 2;
28767 let z = ˇabc + 2;
28768 }
28769 "},
28770 ];
28771
28772 init_test(cx, |_| {});
28773
28774 let mut cx = EditorLspTestContext::new_rust(
28775 lsp::ServerCapabilities {
28776 references_provider: Some(lsp::OneOf::Left(true)),
28777 ..Default::default()
28778 },
28779 cx,
28780 )
28781 .await;
28782
28783 // importantly, the cursor is in the middle
28784 cx.set_state(indoc! {"
28785 fn foo() {
28786 let aˇbc = 123;
28787 let x = abc + 1;
28788 let y = abc + 2;
28789 let z = abc + 2;
28790 }
28791 "});
28792
28793 let reference_ranges = [
28794 lsp::Position::new(1, 8),
28795 lsp::Position::new(2, 12),
28796 lsp::Position::new(3, 12),
28797 lsp::Position::new(4, 12),
28798 ]
28799 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28800
28801 cx.lsp
28802 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
28803 Ok(Some(
28804 reference_ranges
28805 .map(|range| lsp::Location {
28806 uri: params.text_document_position.text_document.uri.clone(),
28807 range,
28808 })
28809 .to_vec(),
28810 ))
28811 });
28812
28813 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
28814 cx.update_editor(|editor, window, cx| {
28815 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
28816 })
28817 .unwrap()
28818 .await
28819 .unwrap()
28820 };
28821
28822 _move(Direction::Next, 1, &mut cx).await;
28823 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28824
28825 _move(Direction::Next, 1, &mut cx).await;
28826 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28827
28828 _move(Direction::Next, 1, &mut cx).await;
28829 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28830
28831 // loops back to the start
28832 _move(Direction::Next, 1, &mut cx).await;
28833 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28834
28835 // loops back to the end
28836 _move(Direction::Prev, 1, &mut cx).await;
28837 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28838
28839 _move(Direction::Prev, 1, &mut cx).await;
28840 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28841
28842 _move(Direction::Prev, 1, &mut cx).await;
28843 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28844
28845 _move(Direction::Prev, 1, &mut cx).await;
28846 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28847
28848 _move(Direction::Next, 3, &mut cx).await;
28849 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28850
28851 _move(Direction::Prev, 2, &mut cx).await;
28852 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28853}
28854
28855#[gpui::test]
28856async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
28857 init_test(cx, |_| {});
28858
28859 let (editor, cx) = cx.add_window_view(|window, cx| {
28860 let multi_buffer = MultiBuffer::build_multi(
28861 [
28862 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28863 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
28864 ],
28865 cx,
28866 );
28867 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
28868 });
28869
28870 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28871 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
28872
28873 cx.assert_excerpts_with_selections(indoc! {"
28874 [EXCERPT]
28875 ˇ1
28876 2
28877 3
28878 [EXCERPT]
28879 1
28880 2
28881 3
28882 "});
28883
28884 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
28885 cx.update_editor(|editor, window, cx| {
28886 editor.change_selections(None.into(), window, cx, |s| {
28887 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28888 });
28889 });
28890 cx.assert_excerpts_with_selections(indoc! {"
28891 [EXCERPT]
28892 1
28893 2ˇ
28894 3
28895 [EXCERPT]
28896 1
28897 2
28898 3
28899 "});
28900
28901 cx.update_editor(|editor, window, cx| {
28902 editor
28903 .select_all_matches(&SelectAllMatches, window, cx)
28904 .unwrap();
28905 });
28906 cx.assert_excerpts_with_selections(indoc! {"
28907 [EXCERPT]
28908 1
28909 2ˇ
28910 3
28911 [EXCERPT]
28912 1
28913 2ˇ
28914 3
28915 "});
28916
28917 cx.update_editor(|editor, window, cx| {
28918 editor.handle_input("X", window, cx);
28919 });
28920 cx.assert_excerpts_with_selections(indoc! {"
28921 [EXCERPT]
28922 1
28923 Xˇ
28924 3
28925 [EXCERPT]
28926 1
28927 Xˇ
28928 3
28929 "});
28930
28931 // Scenario 2: Select "2", then fold second buffer before insertion
28932 cx.update_multibuffer(|mb, cx| {
28933 for buffer_id in buffer_ids.iter() {
28934 let buffer = mb.buffer(*buffer_id).unwrap();
28935 buffer.update(cx, |buffer, cx| {
28936 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28937 });
28938 }
28939 });
28940
28941 // Select "2" and select all matches
28942 cx.update_editor(|editor, window, cx| {
28943 editor.change_selections(None.into(), window, cx, |s| {
28944 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28945 });
28946 editor
28947 .select_all_matches(&SelectAllMatches, window, cx)
28948 .unwrap();
28949 });
28950
28951 // Fold second buffer - should remove selections from folded buffer
28952 cx.update_editor(|editor, _, cx| {
28953 editor.fold_buffer(buffer_ids[1], cx);
28954 });
28955 cx.assert_excerpts_with_selections(indoc! {"
28956 [EXCERPT]
28957 1
28958 2ˇ
28959 3
28960 [EXCERPT]
28961 [FOLDED]
28962 "});
28963
28964 // Insert text - should only affect first buffer
28965 cx.update_editor(|editor, window, cx| {
28966 editor.handle_input("Y", window, cx);
28967 });
28968 cx.update_editor(|editor, _, cx| {
28969 editor.unfold_buffer(buffer_ids[1], cx);
28970 });
28971 cx.assert_excerpts_with_selections(indoc! {"
28972 [EXCERPT]
28973 1
28974 Yˇ
28975 3
28976 [EXCERPT]
28977 1
28978 2
28979 3
28980 "});
28981
28982 // Scenario 3: Select "2", then fold first buffer before insertion
28983 cx.update_multibuffer(|mb, cx| {
28984 for buffer_id in buffer_ids.iter() {
28985 let buffer = mb.buffer(*buffer_id).unwrap();
28986 buffer.update(cx, |buffer, cx| {
28987 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
28988 });
28989 }
28990 });
28991
28992 // Select "2" and select all matches
28993 cx.update_editor(|editor, window, cx| {
28994 editor.change_selections(None.into(), window, cx, |s| {
28995 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
28996 });
28997 editor
28998 .select_all_matches(&SelectAllMatches, window, cx)
28999 .unwrap();
29000 });
29001
29002 // Fold first buffer - should remove selections from folded buffer
29003 cx.update_editor(|editor, _, cx| {
29004 editor.fold_buffer(buffer_ids[0], cx);
29005 });
29006 cx.assert_excerpts_with_selections(indoc! {"
29007 [EXCERPT]
29008 [FOLDED]
29009 [EXCERPT]
29010 1
29011 2ˇ
29012 3
29013 "});
29014
29015 // Insert text - should only affect second buffer
29016 cx.update_editor(|editor, window, cx| {
29017 editor.handle_input("Z", window, cx);
29018 });
29019 cx.update_editor(|editor, _, cx| {
29020 editor.unfold_buffer(buffer_ids[0], cx);
29021 });
29022 cx.assert_excerpts_with_selections(indoc! {"
29023 [EXCERPT]
29024 1
29025 2
29026 3
29027 [EXCERPT]
29028 1
29029 Zˇ
29030 3
29031 "});
29032
29033 // Test correct folded header is selected upon fold
29034 cx.update_editor(|editor, _, cx| {
29035 editor.fold_buffer(buffer_ids[0], cx);
29036 editor.fold_buffer(buffer_ids[1], cx);
29037 });
29038 cx.assert_excerpts_with_selections(indoc! {"
29039 [EXCERPT]
29040 [FOLDED]
29041 [EXCERPT]
29042 ˇ[FOLDED]
29043 "});
29044
29045 // Test selection inside folded buffer unfolds it on type
29046 cx.update_editor(|editor, window, cx| {
29047 editor.handle_input("W", window, cx);
29048 });
29049 cx.update_editor(|editor, _, cx| {
29050 editor.unfold_buffer(buffer_ids[0], cx);
29051 });
29052 cx.assert_excerpts_with_selections(indoc! {"
29053 [EXCERPT]
29054 1
29055 2
29056 3
29057 [EXCERPT]
29058 Wˇ1
29059 Z
29060 3
29061 "});
29062}
29063
29064#[gpui::test]
29065async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
29066 init_test(cx, |_| {});
29067 let mut leader_cx = EditorTestContext::new(cx).await;
29068
29069 let diff_base = indoc!(
29070 r#"
29071 one
29072 two
29073 three
29074 four
29075 five
29076 six
29077 "#
29078 );
29079
29080 let initial_state = indoc!(
29081 r#"
29082 ˇone
29083 two
29084 THREE
29085 four
29086 five
29087 six
29088 "#
29089 );
29090
29091 leader_cx.set_state(initial_state);
29092
29093 leader_cx.set_head_text(&diff_base);
29094 leader_cx.run_until_parked();
29095
29096 let follower = leader_cx.update_multibuffer(|leader, cx| {
29097 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29098 leader.set_all_diff_hunks_expanded(cx);
29099 leader.get_or_create_follower(cx)
29100 });
29101 follower.update(cx, |follower, cx| {
29102 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29103 follower.set_all_diff_hunks_expanded(cx);
29104 });
29105
29106 let follower_editor =
29107 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
29108 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
29109
29110 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
29111 cx.run_until_parked();
29112
29113 leader_cx.assert_editor_state(initial_state);
29114 follower_cx.assert_editor_state(indoc! {
29115 r#"
29116 ˇone
29117 two
29118 three
29119 four
29120 five
29121 six
29122 "#
29123 });
29124
29125 follower_cx.editor(|editor, _window, cx| {
29126 assert!(editor.read_only(cx));
29127 });
29128
29129 leader_cx.update_editor(|editor, _window, cx| {
29130 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
29131 });
29132 cx.run_until_parked();
29133
29134 leader_cx.assert_editor_state(indoc! {
29135 r#"
29136 ˇone
29137 two
29138 THREE
29139 four
29140 FIVE
29141 six
29142 "#
29143 });
29144
29145 follower_cx.assert_editor_state(indoc! {
29146 r#"
29147 ˇone
29148 two
29149 three
29150 four
29151 five
29152 six
29153 "#
29154 });
29155
29156 leader_cx.update_editor(|editor, _window, cx| {
29157 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
29158 });
29159 cx.run_until_parked();
29160
29161 leader_cx.assert_editor_state(indoc! {
29162 r#"
29163 ˇone
29164 two
29165 THREE
29166 four
29167 FIVE
29168 six
29169 SEVEN"#
29170 });
29171
29172 follower_cx.assert_editor_state(indoc! {
29173 r#"
29174 ˇone
29175 two
29176 three
29177 four
29178 five
29179 six
29180 "#
29181 });
29182
29183 leader_cx.update_editor(|editor, window, cx| {
29184 editor.move_down(&MoveDown, window, cx);
29185 editor.refresh_selected_text_highlights(true, window, cx);
29186 });
29187 leader_cx.run_until_parked();
29188}
29189
29190#[gpui::test]
29191async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
29192 init_test(cx, |_| {});
29193 let base_text = "base\n";
29194 let buffer_text = "buffer\n";
29195
29196 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
29197 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
29198
29199 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
29200 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
29201 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
29202 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
29203
29204 let leader = cx.new(|cx| {
29205 let mut leader = MultiBuffer::new(Capability::ReadWrite);
29206 leader.set_all_diff_hunks_expanded(cx);
29207 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29208 leader
29209 });
29210 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
29211 follower.update(cx, |follower, _| {
29212 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29213 });
29214
29215 leader.update(cx, |leader, cx| {
29216 leader.insert_excerpts_after(
29217 ExcerptId::min(),
29218 extra_buffer_2.clone(),
29219 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29220 cx,
29221 );
29222 leader.add_diff(extra_diff_2.clone(), cx);
29223
29224 leader.insert_excerpts_after(
29225 ExcerptId::min(),
29226 extra_buffer_1.clone(),
29227 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29228 cx,
29229 );
29230 leader.add_diff(extra_diff_1.clone(), cx);
29231
29232 leader.insert_excerpts_after(
29233 ExcerptId::min(),
29234 buffer1.clone(),
29235 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29236 cx,
29237 );
29238 leader.add_diff(diff1.clone(), cx);
29239 });
29240
29241 cx.run_until_parked();
29242 let mut cx = cx.add_empty_window();
29243
29244 let leader_editor = cx
29245 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
29246 let follower_editor = cx.new_window_entity(|window, cx| {
29247 Editor::for_multibuffer(follower.clone(), None, window, cx)
29248 });
29249
29250 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
29251 leader_cx.assert_editor_state(indoc! {"
29252 ˇbuffer
29253
29254 dummy text 1
29255
29256 dummy text 2
29257 "});
29258 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
29259 follower_cx.assert_editor_state(indoc! {"
29260 ˇbase
29261
29262
29263 "});
29264}
29265
29266#[gpui::test]
29267async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29268 init_test(cx, |_| {});
29269
29270 let (editor, cx) = cx.add_window_view(|window, cx| {
29271 let multi_buffer = MultiBuffer::build_multi(
29272 [
29273 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29274 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29275 ],
29276 cx,
29277 );
29278 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29279 });
29280
29281 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29282
29283 cx.assert_excerpts_with_selections(indoc! {"
29284 [EXCERPT]
29285 ˇ1
29286 2
29287 3
29288 [EXCERPT]
29289 1
29290 2
29291 3
29292 4
29293 5
29294 6
29295 7
29296 8
29297 9
29298 "});
29299
29300 cx.update_editor(|editor, window, cx| {
29301 editor.change_selections(None.into(), window, cx, |s| {
29302 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29303 });
29304 });
29305
29306 cx.assert_excerpts_with_selections(indoc! {"
29307 [EXCERPT]
29308 1
29309 2
29310 3
29311 [EXCERPT]
29312 1
29313 2
29314 3
29315 4
29316 5
29317 6
29318 ˇ7
29319 8
29320 9
29321 "});
29322
29323 cx.update_editor(|editor, _window, cx| {
29324 editor.set_vertical_scroll_margin(0, cx);
29325 });
29326
29327 cx.update_editor(|editor, window, cx| {
29328 assert_eq!(editor.vertical_scroll_margin(), 0);
29329 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29330 assert_eq!(
29331 editor.snapshot(window, cx).scroll_position(),
29332 gpui::Point::new(0., 12.0)
29333 );
29334 });
29335
29336 cx.update_editor(|editor, _window, cx| {
29337 editor.set_vertical_scroll_margin(3, cx);
29338 });
29339
29340 cx.update_editor(|editor, window, cx| {
29341 assert_eq!(editor.vertical_scroll_margin(), 3);
29342 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29343 assert_eq!(
29344 editor.snapshot(window, cx).scroll_position(),
29345 gpui::Point::new(0., 9.0)
29346 );
29347 });
29348}
29349
29350#[gpui::test]
29351async fn test_find_references_single_case(cx: &mut TestAppContext) {
29352 init_test(cx, |_| {});
29353 let mut cx = EditorLspTestContext::new_rust(
29354 lsp::ServerCapabilities {
29355 references_provider: Some(lsp::OneOf::Left(true)),
29356 ..lsp::ServerCapabilities::default()
29357 },
29358 cx,
29359 )
29360 .await;
29361
29362 let before = indoc!(
29363 r#"
29364 fn main() {
29365 let aˇbc = 123;
29366 let xyz = abc;
29367 }
29368 "#
29369 );
29370 let after = indoc!(
29371 r#"
29372 fn main() {
29373 let abc = 123;
29374 let xyz = ˇabc;
29375 }
29376 "#
29377 );
29378
29379 cx.lsp
29380 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29381 Ok(Some(vec![
29382 lsp::Location {
29383 uri: params.text_document_position.text_document.uri.clone(),
29384 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29385 },
29386 lsp::Location {
29387 uri: params.text_document_position.text_document.uri,
29388 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29389 },
29390 ]))
29391 });
29392
29393 cx.set_state(before);
29394
29395 let action = FindAllReferences {
29396 always_open_multibuffer: false,
29397 };
29398
29399 let navigated = cx
29400 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29401 .expect("should have spawned a task")
29402 .await
29403 .unwrap();
29404
29405 assert_eq!(navigated, Navigated::No);
29406
29407 cx.run_until_parked();
29408
29409 cx.assert_editor_state(after);
29410}
29411
29412#[gpui::test]
29413async fn test_local_worktree_trust(cx: &mut TestAppContext) {
29414 init_test(cx, |_| {});
29415 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
29416
29417 cx.update(|cx| {
29418 SettingsStore::update_global(cx, |store, cx| {
29419 store.update_user_settings(cx, |settings| {
29420 settings.project.all_languages.defaults.inlay_hints =
29421 Some(InlayHintSettingsContent {
29422 enabled: Some(true),
29423 ..InlayHintSettingsContent::default()
29424 });
29425 });
29426 });
29427 });
29428
29429 let fs = FakeFs::new(cx.executor());
29430 fs.insert_tree(
29431 path!("/project"),
29432 json!({
29433 ".zed": {
29434 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
29435 },
29436 "main.rs": "fn main() {}"
29437 }),
29438 )
29439 .await;
29440
29441 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
29442 let server_name = "override-rust-analyzer";
29443 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
29444
29445 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
29446 language_registry.add(rust_lang());
29447
29448 let capabilities = lsp::ServerCapabilities {
29449 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
29450 ..lsp::ServerCapabilities::default()
29451 };
29452 let mut fake_language_servers = language_registry.register_fake_lsp(
29453 "Rust",
29454 FakeLspAdapter {
29455 name: server_name,
29456 capabilities,
29457 initializer: Some(Box::new({
29458 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
29459 move |fake_server| {
29460 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
29461 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
29462 move |_params, _| {
29463 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
29464 async move {
29465 Ok(Some(vec![lsp::InlayHint {
29466 position: lsp::Position::new(0, 0),
29467 label: lsp::InlayHintLabel::String("hint".to_string()),
29468 kind: None,
29469 text_edits: None,
29470 tooltip: None,
29471 padding_left: None,
29472 padding_right: None,
29473 data: None,
29474 }]))
29475 }
29476 },
29477 );
29478 }
29479 })),
29480 ..FakeLspAdapter::default()
29481 },
29482 );
29483
29484 cx.run_until_parked();
29485
29486 let worktree_id = project.read_with(cx, |project, cx| {
29487 project
29488 .worktrees(cx)
29489 .next()
29490 .map(|wt| wt.read(cx).id())
29491 .expect("should have a worktree")
29492 });
29493
29494 let trusted_worktrees =
29495 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
29496
29497 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
29498 assert!(!can_trust, "worktree should be restricted initially");
29499
29500 let buffer_before_approval = project
29501 .update(cx, |project, cx| {
29502 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
29503 })
29504 .await
29505 .unwrap();
29506
29507 let (editor, cx) = cx.add_window_view(|window, cx| {
29508 Editor::new(
29509 EditorMode::full(),
29510 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
29511 Some(project.clone()),
29512 window,
29513 cx,
29514 )
29515 });
29516 cx.run_until_parked();
29517 let fake_language_server = fake_language_servers.next();
29518
29519 cx.read(|cx| {
29520 let file = buffer_before_approval.read(cx).file();
29521 assert_eq!(
29522 language::language_settings::language_settings(Some("Rust".into()), file, cx)
29523 .language_servers,
29524 ["...".to_string()],
29525 "local .zed/settings.json must not apply before trust approval"
29526 )
29527 });
29528
29529 editor.update_in(cx, |editor, window, cx| {
29530 editor.handle_input("1", window, cx);
29531 });
29532 cx.run_until_parked();
29533 cx.executor()
29534 .advance_clock(std::time::Duration::from_secs(1));
29535 assert_eq!(
29536 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
29537 0,
29538 "inlay hints must not be queried before trust approval"
29539 );
29540
29541 trusted_worktrees.update(cx, |store, cx| {
29542 store.trust(
29543 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
29544 None,
29545 cx,
29546 );
29547 });
29548 cx.run_until_parked();
29549
29550 cx.read(|cx| {
29551 let file = buffer_before_approval.read(cx).file();
29552 assert_eq!(
29553 language::language_settings::language_settings(Some("Rust".into()), file, cx)
29554 .language_servers,
29555 ["override-rust-analyzer".to_string()],
29556 "local .zed/settings.json should apply after trust approval"
29557 )
29558 });
29559 let _fake_language_server = fake_language_server.await.unwrap();
29560 editor.update_in(cx, |editor, window, cx| {
29561 editor.handle_input("1", window, cx);
29562 });
29563 cx.run_until_parked();
29564 cx.executor()
29565 .advance_clock(std::time::Duration::from_secs(1));
29566 assert!(
29567 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
29568 "inlay hints should be queried after trust approval"
29569 );
29570
29571 let can_trust_after =
29572 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
29573 assert!(can_trust_after, "worktree should be trusted after trust()");
29574}
29575
29576#[gpui::test]
29577fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
29578 // This test reproduces a bug where drawing an editor at a position above the viewport
29579 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
29580 // causes an infinite loop in blocks_in_range.
29581 //
29582 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
29583 // the content mask intersection produces visible_bounds with origin at the viewport top.
29584 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
29585 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
29586 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
29587 init_test(cx, |_| {});
29588
29589 let window = cx.add_window(|_, _| gpui::Empty);
29590 let mut cx = VisualTestContext::from_window(*window, cx);
29591
29592 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
29593 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
29594
29595 // Simulate a small viewport (500x500 pixels at origin 0,0)
29596 cx.simulate_resize(gpui::size(px(500.), px(500.)));
29597
29598 // Draw the editor at a very negative Y position, simulating an editor that's been
29599 // scrolled way above the visible viewport (like in a List that has scrolled past it).
29600 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
29601 // This should NOT hang - it should just render nothing.
29602 cx.draw(
29603 gpui::point(px(0.), px(-10000.)),
29604 gpui::size(px(500.), px(3000.)),
29605 |_, _| editor.clone(),
29606 );
29607
29608 // If we get here without hanging, the test passes
29609}