1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
21 VisualTestContext, 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::rust_lang;
36use lsp::CompletionParams;
37use multi_buffer::{IndentGuide, PathKey};
38use parking_lot::Mutex;
39use pretty_assertions::{assert_eq, assert_ne};
40use project::{
41 FakeFs,
42 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
43 project_settings::LspSettings,
44};
45use serde_json::{self, json};
46use settings::{
47 AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
48 IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
49};
50use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
51use std::{
52 iter,
53 sync::atomic::{self, AtomicUsize},
54};
55use test::build_editor_with_project;
56use text::ToPoint as _;
57use unindent::Unindent;
58use util::{
59 assert_set_eq, path,
60 rel_path::rel_path,
61 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
62 uri,
63};
64use workspace::{
65 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
66 OpenOptions, ViewId,
67 invalid_item_view::InvalidItemView,
68 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
69 register_project_item,
70};
71
72fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
73 editor
74 .selections
75 .display_ranges(&editor.display_snapshot(cx))
76}
77
78#[gpui::test]
79fn test_edit_events(cx: &mut TestAppContext) {
80 init_test(cx, |_| {});
81
82 let buffer = cx.new(|cx| {
83 let mut buffer = language::Buffer::local("123456", cx);
84 buffer.set_group_interval(Duration::from_secs(1));
85 buffer
86 });
87
88 let events = Rc::new(RefCell::new(Vec::new()));
89 let editor1 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 let entity = cx.entity();
93 cx.subscribe_in(
94 &entity,
95 window,
96 move |_, _, event: &EditorEvent, _, _| match event {
97 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
98 EditorEvent::BufferEdited => {
99 events.borrow_mut().push(("editor1", "buffer edited"))
100 }
101 _ => {}
102 },
103 )
104 .detach();
105 Editor::for_buffer(buffer.clone(), None, window, cx)
106 }
107 });
108
109 let editor2 = cx.add_window({
110 let events = events.clone();
111 |window, cx| {
112 cx.subscribe_in(
113 &cx.entity(),
114 window,
115 move |_, _, event: &EditorEvent, _, _| match event {
116 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
117 EditorEvent::BufferEdited => {
118 events.borrow_mut().push(("editor2", "buffer edited"))
119 }
120 _ => {}
121 },
122 )
123 .detach();
124 Editor::for_buffer(buffer.clone(), None, window, cx)
125 }
126 });
127
128 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
129
130 // Mutating editor 1 will emit an `Edited` event only for that editor.
131 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
132 assert_eq!(
133 mem::take(&mut *events.borrow_mut()),
134 [
135 ("editor1", "edited"),
136 ("editor1", "buffer edited"),
137 ("editor2", "buffer edited"),
138 ]
139 );
140
141 // Mutating editor 2 will emit an `Edited` event only for that editor.
142 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
143 assert_eq!(
144 mem::take(&mut *events.borrow_mut()),
145 [
146 ("editor2", "edited"),
147 ("editor1", "buffer edited"),
148 ("editor2", "buffer edited"),
149 ]
150 );
151
152 // Undoing on editor 1 will emit an `Edited` event only for that editor.
153 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
154 assert_eq!(
155 mem::take(&mut *events.borrow_mut()),
156 [
157 ("editor1", "edited"),
158 ("editor1", "buffer edited"),
159 ("editor2", "buffer edited"),
160 ]
161 );
162
163 // Redoing on editor 1 will emit an `Edited` event only for that editor.
164 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
165 assert_eq!(
166 mem::take(&mut *events.borrow_mut()),
167 [
168 ("editor1", "edited"),
169 ("editor1", "buffer edited"),
170 ("editor2", "buffer edited"),
171 ]
172 );
173
174 // Undoing on editor 2 will emit an `Edited` event only for that editor.
175 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
176 assert_eq!(
177 mem::take(&mut *events.borrow_mut()),
178 [
179 ("editor2", "edited"),
180 ("editor1", "buffer edited"),
181 ("editor2", "buffer edited"),
182 ]
183 );
184
185 // Redoing on editor 2 will emit an `Edited` event only for that editor.
186 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
187 assert_eq!(
188 mem::take(&mut *events.borrow_mut()),
189 [
190 ("editor2", "edited"),
191 ("editor1", "buffer edited"),
192 ("editor2", "buffer edited"),
193 ]
194 );
195
196 // No event is emitted when the mutation is a no-op.
197 _ = editor2.update(cx, |editor, window, cx| {
198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
199 s.select_ranges([0..0])
200 });
201
202 editor.backspace(&Backspace, window, cx);
203 });
204 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
205}
206
207#[gpui::test]
208fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
209 init_test(cx, |_| {});
210
211 let mut now = Instant::now();
212 let group_interval = Duration::from_millis(1);
213 let buffer = cx.new(|cx| {
214 let mut buf = language::Buffer::local("123456", cx);
215 buf.set_group_interval(group_interval);
216 buf
217 });
218 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
219 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
220
221 _ = editor.update(cx, |editor, window, cx| {
222 editor.start_transaction_at(now, window, cx);
223 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
224 s.select_ranges([2..4])
225 });
226
227 editor.insert("cd", window, cx);
228 editor.end_transaction_at(now, cx);
229 assert_eq!(editor.text(cx), "12cd56");
230 assert_eq!(
231 editor.selections.ranges(&editor.display_snapshot(cx)),
232 vec![4..4]
233 );
234
235 editor.start_transaction_at(now, window, cx);
236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
237 s.select_ranges([4..5])
238 });
239 editor.insert("e", window, cx);
240 editor.end_transaction_at(now, cx);
241 assert_eq!(editor.text(cx), "12cde6");
242 assert_eq!(
243 editor.selections.ranges(&editor.display_snapshot(cx)),
244 vec![5..5]
245 );
246
247 now += group_interval + Duration::from_millis(1);
248 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
249 s.select_ranges([2..2])
250 });
251
252 // Simulate an edit in another editor
253 buffer.update(cx, |buffer, cx| {
254 buffer.start_transaction_at(now, cx);
255 buffer.edit([(0..1, "a")], None, cx);
256 buffer.edit([(1..1, "b")], None, cx);
257 buffer.end_transaction_at(now, cx);
258 });
259
260 assert_eq!(editor.text(cx), "ab2cde6");
261 assert_eq!(
262 editor.selections.ranges(&editor.display_snapshot(cx)),
263 vec![3..3]
264 );
265
266 // Last transaction happened past the group interval in a different editor.
267 // Undo it individually and don't restore selections.
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 assert_eq!(
271 editor.selections.ranges(&editor.display_snapshot(cx)),
272 vec![2..2]
273 );
274
275 // First two transactions happened within the group interval in this editor.
276 // Undo them together and restore selections.
277 editor.undo(&Undo, window, cx);
278 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
279 assert_eq!(editor.text(cx), "123456");
280 assert_eq!(
281 editor.selections.ranges(&editor.display_snapshot(cx)),
282 vec![0..0]
283 );
284
285 // Redo the first two transactions together.
286 editor.redo(&Redo, window, cx);
287 assert_eq!(editor.text(cx), "12cde6");
288 assert_eq!(
289 editor.selections.ranges(&editor.display_snapshot(cx)),
290 vec![5..5]
291 );
292
293 // Redo the last transaction on its own.
294 editor.redo(&Redo, window, cx);
295 assert_eq!(editor.text(cx), "ab2cde6");
296 assert_eq!(
297 editor.selections.ranges(&editor.display_snapshot(cx)),
298 vec![6..6]
299 );
300
301 // Test empty transactions.
302 editor.start_transaction_at(now, window, cx);
303 editor.end_transaction_at(now, cx);
304 editor.undo(&Undo, window, cx);
305 assert_eq!(editor.text(cx), "12cde6");
306 });
307}
308
309#[gpui::test]
310fn test_ime_composition(cx: &mut TestAppContext) {
311 init_test(cx, |_| {});
312
313 let buffer = cx.new(|cx| {
314 let mut buffer = language::Buffer::local("abcde", cx);
315 // Ensure automatic grouping doesn't occur.
316 buffer.set_group_interval(Duration::ZERO);
317 buffer
318 });
319
320 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
321 cx.add_window(|window, cx| {
322 let mut editor = build_editor(buffer.clone(), window, cx);
323
324 // Start a new IME composition.
325 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
326 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
327 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
328 assert_eq!(editor.text(cx), "äbcde");
329 assert_eq!(
330 editor.marked_text_ranges(cx),
331 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
332 );
333
334 // Finalize IME composition.
335 editor.replace_text_in_range(None, "ā", window, cx);
336 assert_eq!(editor.text(cx), "ābcde");
337 assert_eq!(editor.marked_text_ranges(cx), None);
338
339 // IME composition edits are grouped and are undone/redone at once.
340 editor.undo(&Default::default(), window, cx);
341 assert_eq!(editor.text(cx), "abcde");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343 editor.redo(&Default::default(), window, cx);
344 assert_eq!(editor.text(cx), "ābcde");
345 assert_eq!(editor.marked_text_ranges(cx), None);
346
347 // Start a new IME composition.
348 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
352 );
353
354 // Undoing during an IME composition cancels it.
355 editor.undo(&Default::default(), window, cx);
356 assert_eq!(editor.text(cx), "ābcde");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
360 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
361 assert_eq!(editor.text(cx), "ābcdè");
362 assert_eq!(
363 editor.marked_text_ranges(cx),
364 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
365 );
366
367 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
368 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
369 assert_eq!(editor.text(cx), "ābcdę");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 // Start a new IME composition with multiple cursors.
373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
374 s.select_ranges([
375 OffsetUtf16(1)..OffsetUtf16(1),
376 OffsetUtf16(3)..OffsetUtf16(3),
377 OffsetUtf16(5)..OffsetUtf16(5),
378 ])
379 });
380 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
381 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
382 assert_eq!(
383 editor.marked_text_ranges(cx),
384 Some(vec![
385 OffsetUtf16(0)..OffsetUtf16(3),
386 OffsetUtf16(4)..OffsetUtf16(7),
387 OffsetUtf16(8)..OffsetUtf16(11)
388 ])
389 );
390
391 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
392 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
393 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
394 assert_eq!(
395 editor.marked_text_ranges(cx),
396 Some(vec![
397 OffsetUtf16(1)..OffsetUtf16(2),
398 OffsetUtf16(5)..OffsetUtf16(6),
399 OffsetUtf16(9)..OffsetUtf16(10)
400 ])
401 );
402
403 // Finalize IME composition with multiple cursors.
404 editor.replace_text_in_range(Some(9..10), "2", window, cx);
405 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
406 assert_eq!(editor.marked_text_ranges(cx), None);
407
408 editor
409 });
410}
411
412#[gpui::test]
413fn test_selection_with_mouse(cx: &mut TestAppContext) {
414 init_test(cx, |_| {});
415
416 let editor = cx.add_window(|window, cx| {
417 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
418 build_editor(buffer, window, cx)
419 });
420
421 _ = editor.update(cx, |editor, window, cx| {
422 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
423 });
424 assert_eq!(
425 editor
426 .update(cx, |editor, _, cx| display_ranges(editor, cx))
427 .unwrap(),
428 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
429 );
430
431 _ = editor.update(cx, |editor, window, cx| {
432 editor.update_selection(
433 DisplayPoint::new(DisplayRow(3), 3),
434 0,
435 gpui::Point::<f32>::default(),
436 window,
437 cx,
438 );
439 });
440
441 assert_eq!(
442 editor
443 .update(cx, |editor, _, cx| display_ranges(editor, cx))
444 .unwrap(),
445 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(1), 1),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| display_ranges(editor, cx))
461 .unwrap(),
462 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
463 );
464
465 _ = editor.update(cx, |editor, window, cx| {
466 editor.end_selection(window, cx);
467 editor.update_selection(
468 DisplayPoint::new(DisplayRow(3), 3),
469 0,
470 gpui::Point::<f32>::default(),
471 window,
472 cx,
473 );
474 });
475
476 assert_eq!(
477 editor
478 .update(cx, |editor, _, cx| display_ranges(editor, cx))
479 .unwrap(),
480 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
481 );
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
485 editor.update_selection(
486 DisplayPoint::new(DisplayRow(0), 0),
487 0,
488 gpui::Point::<f32>::default(),
489 window,
490 cx,
491 );
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| display_ranges(editor, cx))
497 .unwrap(),
498 [
499 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
500 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
501 ]
502 );
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.end_selection(window, cx);
506 });
507
508 assert_eq!(
509 editor
510 .update(cx, |editor, _, cx| display_ranges(editor, cx))
511 .unwrap(),
512 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
513 );
514}
515
516#[gpui::test]
517fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
518 init_test(cx, |_| {});
519
520 let editor = cx.add_window(|window, cx| {
521 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
522 build_editor(buffer, window, cx)
523 });
524
525 _ = editor.update(cx, |editor, window, cx| {
526 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.end_selection(window, cx);
531 });
532
533 _ = editor.update(cx, |editor, window, cx| {
534 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
535 });
536
537 _ = editor.update(cx, |editor, window, cx| {
538 editor.end_selection(window, cx);
539 });
540
541 assert_eq!(
542 editor
543 .update(cx, |editor, _, cx| display_ranges(editor, cx))
544 .unwrap(),
545 [
546 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
547 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
548 ]
549 );
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.end_selection(window, cx);
557 });
558
559 assert_eq!(
560 editor
561 .update(cx, |editor, _, cx| display_ranges(editor, cx))
562 .unwrap(),
563 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
564 );
565}
566
567#[gpui::test]
568fn test_canceling_pending_selection(cx: &mut TestAppContext) {
569 init_test(cx, |_| {});
570
571 let editor = cx.add_window(|window, cx| {
572 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
573 build_editor(buffer, window, cx)
574 });
575
576 _ = editor.update(cx, |editor, window, cx| {
577 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
578 assert_eq!(
579 display_ranges(editor, cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
581 );
582 });
583
584 _ = editor.update(cx, |editor, window, cx| {
585 editor.update_selection(
586 DisplayPoint::new(DisplayRow(3), 3),
587 0,
588 gpui::Point::<f32>::default(),
589 window,
590 cx,
591 );
592 assert_eq!(
593 display_ranges(editor, cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
595 );
596 });
597
598 _ = editor.update(cx, |editor, window, cx| {
599 editor.cancel(&Cancel, window, cx);
600 editor.update_selection(
601 DisplayPoint::new(DisplayRow(1), 1),
602 0,
603 gpui::Point::<f32>::default(),
604 window,
605 cx,
606 );
607 assert_eq!(
608 display_ranges(editor, cx),
609 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let editor = cx.add_window(|window, cx| {
619 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
620 build_editor(buffer, window, cx)
621 });
622
623 _ = editor.update(cx, |editor, window, cx| {
624 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
625 assert_eq!(
626 display_ranges(editor, cx),
627 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
628 );
629
630 editor.move_down(&Default::default(), window, cx);
631 assert_eq!(
632 display_ranges(editor, cx),
633 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
634 );
635
636 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
637 assert_eq!(
638 display_ranges(editor, cx),
639 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
640 );
641
642 editor.move_up(&Default::default(), window, cx);
643 assert_eq!(
644 display_ranges(editor, cx),
645 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
646 );
647 });
648}
649
650#[gpui::test]
651fn test_extending_selection(cx: &mut TestAppContext) {
652 init_test(cx, |_| {});
653
654 let editor = cx.add_window(|window, cx| {
655 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
656 build_editor(buffer, window, cx)
657 });
658
659 _ = editor.update(cx, |editor, window, cx| {
660 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
661 editor.end_selection(window, cx);
662 assert_eq!(
663 display_ranges(editor, cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
665 );
666
667 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
668 editor.end_selection(window, cx);
669 assert_eq!(
670 display_ranges(editor, cx),
671 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
672 );
673
674 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
675 editor.end_selection(window, cx);
676 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
677 assert_eq!(
678 display_ranges(editor, cx),
679 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
680 );
681
682 editor.update_selection(
683 DisplayPoint::new(DisplayRow(0), 1),
684 0,
685 gpui::Point::<f32>::default(),
686 window,
687 cx,
688 );
689 editor.end_selection(window, cx);
690 assert_eq!(
691 display_ranges(editor, cx),
692 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
693 );
694
695 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
696 editor.end_selection(window, cx);
697 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
698 editor.end_selection(window, cx);
699 assert_eq!(
700 display_ranges(editor, cx),
701 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
702 );
703
704 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
705 assert_eq!(
706 display_ranges(editor, cx),
707 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
708 );
709
710 editor.update_selection(
711 DisplayPoint::new(DisplayRow(0), 6),
712 0,
713 gpui::Point::<f32>::default(),
714 window,
715 cx,
716 );
717 assert_eq!(
718 display_ranges(editor, cx),
719 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
720 );
721
722 editor.update_selection(
723 DisplayPoint::new(DisplayRow(0), 1),
724 0,
725 gpui::Point::<f32>::default(),
726 window,
727 cx,
728 );
729 editor.end_selection(window, cx);
730 assert_eq!(
731 display_ranges(editor, cx),
732 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
733 );
734 });
735}
736
737#[gpui::test]
738fn test_clone(cx: &mut TestAppContext) {
739 init_test(cx, |_| {});
740
741 let (text, selection_ranges) = marked_text_ranges(
742 indoc! {"
743 one
744 two
745 threeˇ
746 four
747 fiveˇ
748 "},
749 true,
750 );
751
752 let editor = cx.add_window(|window, cx| {
753 let buffer = MultiBuffer::build_simple(&text, cx);
754 build_editor(buffer, window, cx)
755 });
756
757 _ = editor.update(cx, |editor, window, cx| {
758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
759 s.select_ranges(selection_ranges.clone())
760 });
761 editor.fold_creases(
762 vec![
763 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
764 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
765 ],
766 true,
767 window,
768 cx,
769 );
770 });
771
772 let cloned_editor = editor
773 .update(cx, |editor, _, cx| {
774 cx.open_window(Default::default(), |window, cx| {
775 cx.new(|cx| editor.clone(window, cx))
776 })
777 })
778 .unwrap()
779 .unwrap();
780
781 let snapshot = editor
782 .update(cx, |e, window, cx| e.snapshot(window, cx))
783 .unwrap();
784 let cloned_snapshot = cloned_editor
785 .update(cx, |e, window, cx| e.snapshot(window, cx))
786 .unwrap();
787
788 assert_eq!(
789 cloned_editor
790 .update(cx, |e, _, cx| e.display_text(cx))
791 .unwrap(),
792 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
793 );
794 assert_eq!(
795 cloned_snapshot
796 .folds_in_range(0..text.len())
797 .collect::<Vec<_>>(),
798 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
799 );
800 assert_set_eq!(
801 cloned_editor
802 .update(cx, |editor, _, cx| editor
803 .selections
804 .ranges::<Point>(&editor.display_snapshot(cx)))
805 .unwrap(),
806 editor
807 .update(cx, |editor, _, cx| editor
808 .selections
809 .ranges(&editor.display_snapshot(cx)))
810 .unwrap()
811 );
812 assert_set_eq!(
813 cloned_editor
814 .update(cx, |e, _window, cx| e
815 .selections
816 .display_ranges(&e.display_snapshot(cx)))
817 .unwrap(),
818 editor
819 .update(cx, |e, _, cx| e
820 .selections
821 .display_ranges(&e.display_snapshot(cx)))
822 .unwrap()
823 );
824}
825
826#[gpui::test]
827async fn test_navigation_history(cx: &mut TestAppContext) {
828 init_test(cx, |_| {});
829
830 use workspace::item::Item;
831
832 let fs = FakeFs::new(cx.executor());
833 let project = Project::test(fs, [], cx).await;
834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
835 let pane = workspace
836 .update(cx, |workspace, _, _| workspace.active_pane().clone())
837 .unwrap();
838
839 _ = workspace.update(cx, |_v, window, cx| {
840 cx.new(|cx| {
841 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
842 let mut editor = build_editor(buffer, window, cx);
843 let handle = cx.entity();
844 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
845
846 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
847 editor.nav_history.as_mut().unwrap().pop_backward(cx)
848 }
849
850 // Move the cursor a small distance.
851 // Nothing is added to the navigation history.
852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
853 s.select_display_ranges([
854 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
855 ])
856 });
857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
858 s.select_display_ranges([
859 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
860 ])
861 });
862 assert!(pop_history(&mut editor, cx).is_none());
863
864 // Move the cursor a large distance.
865 // The history can jump back to the previous position.
866 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
867 s.select_display_ranges([
868 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
869 ])
870 });
871 let nav_entry = pop_history(&mut editor, cx).unwrap();
872 editor.navigate(nav_entry.data.unwrap(), window, cx);
873 assert_eq!(nav_entry.item.id(), cx.entity_id());
874 assert_eq!(
875 editor
876 .selections
877 .display_ranges(&editor.display_snapshot(cx)),
878 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
879 );
880 assert!(pop_history(&mut editor, cx).is_none());
881
882 // Move the cursor a small distance via the mouse.
883 // Nothing is added to the navigation history.
884 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
885 editor.end_selection(window, cx);
886 assert_eq!(
887 editor
888 .selections
889 .display_ranges(&editor.display_snapshot(cx)),
890 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
891 );
892 assert!(pop_history(&mut editor, cx).is_none());
893
894 // Move the cursor a large distance via the mouse.
895 // The history can jump back to the previous position.
896 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
897 editor.end_selection(window, cx);
898 assert_eq!(
899 editor
900 .selections
901 .display_ranges(&editor.display_snapshot(cx)),
902 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
903 );
904 let nav_entry = pop_history(&mut editor, cx).unwrap();
905 editor.navigate(nav_entry.data.unwrap(), window, cx);
906 assert_eq!(nav_entry.item.id(), cx.entity_id());
907 assert_eq!(
908 editor
909 .selections
910 .display_ranges(&editor.display_snapshot(cx)),
911 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
912 );
913 assert!(pop_history(&mut editor, cx).is_none());
914
915 // Set scroll position to check later
916 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
917 let original_scroll_position = editor.scroll_manager.anchor();
918
919 // Jump to the end of the document and adjust scroll
920 editor.move_to_end(&MoveToEnd, window, cx);
921 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
922 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
923
924 let nav_entry = pop_history(&mut editor, cx).unwrap();
925 editor.navigate(nav_entry.data.unwrap(), window, cx);
926 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
927
928 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
929 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
930 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
931 let invalid_point = Point::new(9999, 0);
932 editor.navigate(
933 Box::new(NavigationData {
934 cursor_anchor: invalid_anchor,
935 cursor_position: invalid_point,
936 scroll_anchor: ScrollAnchor {
937 anchor: invalid_anchor,
938 offset: Default::default(),
939 },
940 scroll_top_row: invalid_point.row,
941 }),
942 window,
943 cx,
944 );
945 assert_eq!(
946 editor
947 .selections
948 .display_ranges(&editor.display_snapshot(cx)),
949 &[editor.max_point(cx)..editor.max_point(cx)]
950 );
951 assert_eq!(
952 editor.scroll_position(cx),
953 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
954 );
955
956 editor
957 })
958 });
959}
960
961#[gpui::test]
962fn test_cancel(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
967 build_editor(buffer, window, cx)
968 });
969
970 _ = editor.update(cx, |editor, window, cx| {
971 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
972 editor.update_selection(
973 DisplayPoint::new(DisplayRow(1), 1),
974 0,
975 gpui::Point::<f32>::default(),
976 window,
977 cx,
978 );
979 editor.end_selection(window, cx);
980
981 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
982 editor.update_selection(
983 DisplayPoint::new(DisplayRow(0), 3),
984 0,
985 gpui::Point::<f32>::default(),
986 window,
987 cx,
988 );
989 editor.end_selection(window, cx);
990 assert_eq!(
991 display_ranges(editor, cx),
992 [
993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
994 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
995 ]
996 );
997 });
998
999 _ = editor.update(cx, |editor, window, cx| {
1000 editor.cancel(&Cancel, window, cx);
1001 assert_eq!(
1002 display_ranges(editor, cx),
1003 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1004 );
1005 });
1006
1007 _ = editor.update(cx, |editor, window, cx| {
1008 editor.cancel(&Cancel, window, cx);
1009 assert_eq!(
1010 display_ranges(editor, cx),
1011 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1012 );
1013 });
1014}
1015
1016#[gpui::test]
1017fn test_fold_action(cx: &mut TestAppContext) {
1018 init_test(cx, |_| {});
1019
1020 let editor = cx.add_window(|window, cx| {
1021 let buffer = MultiBuffer::build_simple(
1022 &"
1023 impl Foo {
1024 // Hello!
1025
1026 fn a() {
1027 1
1028 }
1029
1030 fn b() {
1031 2
1032 }
1033
1034 fn c() {
1035 3
1036 }
1037 }
1038 "
1039 .unindent(),
1040 cx,
1041 );
1042 build_editor(buffer, window, cx)
1043 });
1044
1045 _ = editor.update(cx, |editor, window, cx| {
1046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1047 s.select_display_ranges([
1048 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1049 ]);
1050 });
1051 editor.fold(&Fold, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 "
1055 impl Foo {
1056 // Hello!
1057
1058 fn a() {
1059 1
1060 }
1061
1062 fn b() {⋯
1063 }
1064
1065 fn c() {⋯
1066 }
1067 }
1068 "
1069 .unindent(),
1070 );
1071
1072 editor.fold(&Fold, window, cx);
1073 assert_eq!(
1074 editor.display_text(cx),
1075 "
1076 impl Foo {⋯
1077 }
1078 "
1079 .unindent(),
1080 );
1081
1082 editor.unfold_lines(&UnfoldLines, window, cx);
1083 assert_eq!(
1084 editor.display_text(cx),
1085 "
1086 impl Foo {
1087 // Hello!
1088
1089 fn a() {
1090 1
1091 }
1092
1093 fn b() {⋯
1094 }
1095
1096 fn c() {⋯
1097 }
1098 }
1099 "
1100 .unindent(),
1101 );
1102
1103 editor.unfold_lines(&UnfoldLines, window, cx);
1104 assert_eq!(
1105 editor.display_text(cx),
1106 editor.buffer.read(cx).read(cx).text()
1107 );
1108 });
1109}
1110
1111#[gpui::test]
1112fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1113 init_test(cx, |_| {});
1114
1115 let editor = cx.add_window(|window, cx| {
1116 let buffer = MultiBuffer::build_simple(
1117 &"
1118 class Foo:
1119 # Hello!
1120
1121 def a():
1122 print(1)
1123
1124 def b():
1125 print(2)
1126
1127 def c():
1128 print(3)
1129 "
1130 .unindent(),
1131 cx,
1132 );
1133 build_editor(buffer, window, cx)
1134 });
1135
1136 _ = editor.update(cx, |editor, window, cx| {
1137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1138 s.select_display_ranges([
1139 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1140 ]);
1141 });
1142 editor.fold(&Fold, window, cx);
1143 assert_eq!(
1144 editor.display_text(cx),
1145 "
1146 class Foo:
1147 # Hello!
1148
1149 def a():
1150 print(1)
1151
1152 def b():⋯
1153
1154 def c():⋯
1155 "
1156 .unindent(),
1157 );
1158
1159 editor.fold(&Fold, window, cx);
1160 assert_eq!(
1161 editor.display_text(cx),
1162 "
1163 class Foo:⋯
1164 "
1165 .unindent(),
1166 );
1167
1168 editor.unfold_lines(&UnfoldLines, window, cx);
1169 assert_eq!(
1170 editor.display_text(cx),
1171 "
1172 class Foo:
1173 # Hello!
1174
1175 def a():
1176 print(1)
1177
1178 def b():⋯
1179
1180 def c():⋯
1181 "
1182 .unindent(),
1183 );
1184
1185 editor.unfold_lines(&UnfoldLines, window, cx);
1186 assert_eq!(
1187 editor.display_text(cx),
1188 editor.buffer.read(cx).read(cx).text()
1189 );
1190 });
1191}
1192
1193#[gpui::test]
1194fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1195 init_test(cx, |_| {});
1196
1197 let editor = cx.add_window(|window, cx| {
1198 let buffer = MultiBuffer::build_simple(
1199 &"
1200 class Foo:
1201 # Hello!
1202
1203 def a():
1204 print(1)
1205
1206 def b():
1207 print(2)
1208
1209
1210 def c():
1211 print(3)
1212
1213
1214 "
1215 .unindent(),
1216 cx,
1217 );
1218 build_editor(buffer, window, cx)
1219 });
1220
1221 _ = editor.update(cx, |editor, window, cx| {
1222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1223 s.select_display_ranges([
1224 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1225 ]);
1226 });
1227 editor.fold(&Fold, window, cx);
1228 assert_eq!(
1229 editor.display_text(cx),
1230 "
1231 class Foo:
1232 # Hello!
1233
1234 def a():
1235 print(1)
1236
1237 def b():⋯
1238
1239
1240 def c():⋯
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 editor.fold(&Fold, window, cx);
1248 assert_eq!(
1249 editor.display_text(cx),
1250 "
1251 class Foo:⋯
1252
1253
1254 "
1255 .unindent(),
1256 );
1257
1258 editor.unfold_lines(&UnfoldLines, window, cx);
1259 assert_eq!(
1260 editor.display_text(cx),
1261 "
1262 class Foo:
1263 # Hello!
1264
1265 def a():
1266 print(1)
1267
1268 def b():⋯
1269
1270
1271 def c():⋯
1272
1273
1274 "
1275 .unindent(),
1276 );
1277
1278 editor.unfold_lines(&UnfoldLines, window, cx);
1279 assert_eq!(
1280 editor.display_text(cx),
1281 editor.buffer.read(cx).read(cx).text()
1282 );
1283 });
1284}
1285
1286#[gpui::test]
1287fn test_fold_at_level(cx: &mut TestAppContext) {
1288 init_test(cx, |_| {});
1289
1290 let editor = cx.add_window(|window, cx| {
1291 let buffer = MultiBuffer::build_simple(
1292 &"
1293 class Foo:
1294 # Hello!
1295
1296 def a():
1297 print(1)
1298
1299 def b():
1300 print(2)
1301
1302
1303 class Bar:
1304 # World!
1305
1306 def a():
1307 print(1)
1308
1309 def b():
1310 print(2)
1311
1312
1313 "
1314 .unindent(),
1315 cx,
1316 );
1317 build_editor(buffer, window, cx)
1318 });
1319
1320 _ = editor.update(cx, |editor, window, cx| {
1321 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1322 assert_eq!(
1323 editor.display_text(cx),
1324 "
1325 class Foo:
1326 # Hello!
1327
1328 def a():⋯
1329
1330 def b():⋯
1331
1332
1333 class Bar:
1334 # World!
1335
1336 def a():⋯
1337
1338 def b():⋯
1339
1340
1341 "
1342 .unindent(),
1343 );
1344
1345 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1346 assert_eq!(
1347 editor.display_text(cx),
1348 "
1349 class Foo:⋯
1350
1351
1352 class Bar:⋯
1353
1354
1355 "
1356 .unindent(),
1357 );
1358
1359 editor.unfold_all(&UnfoldAll, window, cx);
1360 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1361 assert_eq!(
1362 editor.display_text(cx),
1363 "
1364 class Foo:
1365 # Hello!
1366
1367 def a():
1368 print(1)
1369
1370 def b():
1371 print(2)
1372
1373
1374 class Bar:
1375 # World!
1376
1377 def a():
1378 print(1)
1379
1380 def b():
1381 print(2)
1382
1383
1384 "
1385 .unindent(),
1386 );
1387
1388 assert_eq!(
1389 editor.display_text(cx),
1390 editor.buffer.read(cx).read(cx).text()
1391 );
1392 let (_, positions) = marked_text_ranges(
1393 &"
1394 class Foo:
1395 # Hello!
1396
1397 def a():
1398 print(1)
1399
1400 def b():
1401 p«riˇ»nt(2)
1402
1403
1404 class Bar:
1405 # World!
1406
1407 def a():
1408 «ˇprint(1)
1409
1410 def b():
1411 print(2)»
1412
1413
1414 "
1415 .unindent(),
1416 true,
1417 );
1418
1419 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1420 s.select_ranges(positions)
1421 });
1422
1423 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1424 assert_eq!(
1425 editor.display_text(cx),
1426 "
1427 class Foo:
1428 # Hello!
1429
1430 def a():⋯
1431
1432 def b():
1433 print(2)
1434
1435
1436 class Bar:
1437 # World!
1438
1439 def a():
1440 print(1)
1441
1442 def b():
1443 print(2)
1444
1445
1446 "
1447 .unindent(),
1448 );
1449 });
1450}
1451
1452#[gpui::test]
1453fn test_move_cursor(cx: &mut TestAppContext) {
1454 init_test(cx, |_| {});
1455
1456 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1457 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1458
1459 buffer.update(cx, |buffer, cx| {
1460 buffer.edit(
1461 vec![
1462 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1463 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1464 ],
1465 None,
1466 cx,
1467 );
1468 });
1469 _ = editor.update(cx, |editor, window, cx| {
1470 assert_eq!(
1471 display_ranges(editor, cx),
1472 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1473 );
1474
1475 editor.move_down(&MoveDown, window, cx);
1476 assert_eq!(
1477 display_ranges(editor, cx),
1478 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1479 );
1480
1481 editor.move_right(&MoveRight, window, cx);
1482 assert_eq!(
1483 display_ranges(editor, cx),
1484 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1485 );
1486
1487 editor.move_left(&MoveLeft, window, cx);
1488 assert_eq!(
1489 display_ranges(editor, cx),
1490 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1491 );
1492
1493 editor.move_up(&MoveUp, window, cx);
1494 assert_eq!(
1495 display_ranges(editor, cx),
1496 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1497 );
1498
1499 editor.move_to_end(&MoveToEnd, window, cx);
1500 assert_eq!(
1501 display_ranges(editor, cx),
1502 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1503 );
1504
1505 editor.move_to_beginning(&MoveToBeginning, window, cx);
1506 assert_eq!(
1507 display_ranges(editor, cx),
1508 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1509 );
1510
1511 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1512 s.select_display_ranges([
1513 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1514 ]);
1515 });
1516 editor.select_to_beginning(&SelectToBeginning, window, cx);
1517 assert_eq!(
1518 display_ranges(editor, cx),
1519 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1520 );
1521
1522 editor.select_to_end(&SelectToEnd, window, cx);
1523 assert_eq!(
1524 display_ranges(editor, cx),
1525 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1526 );
1527 });
1528}
1529
1530#[gpui::test]
1531fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1532 init_test(cx, |_| {});
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538
1539 assert_eq!('🟥'.len_utf8(), 4);
1540 assert_eq!('α'.len_utf8(), 2);
1541
1542 _ = editor.update(cx, |editor, window, cx| {
1543 editor.fold_creases(
1544 vec![
1545 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1546 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1547 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1548 ],
1549 true,
1550 window,
1551 cx,
1552 );
1553 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1554
1555 editor.move_right(&MoveRight, window, cx);
1556 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1557 editor.move_right(&MoveRight, window, cx);
1558 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1559 editor.move_right(&MoveRight, window, cx);
1560 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1561
1562 editor.move_down(&MoveDown, window, cx);
1563 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1564 editor.move_left(&MoveLeft, window, cx);
1565 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1566 editor.move_left(&MoveLeft, window, cx);
1567 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1568 editor.move_left(&MoveLeft, window, cx);
1569 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1570
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1573 editor.move_right(&MoveRight, window, cx);
1574 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1575 editor.move_right(&MoveRight, window, cx);
1576 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1577 editor.move_right(&MoveRight, window, cx);
1578 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1579
1580 editor.move_up(&MoveUp, window, cx);
1581 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1582 editor.move_down(&MoveDown, window, cx);
1583 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1584 editor.move_up(&MoveUp, window, cx);
1585 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1586
1587 editor.move_up(&MoveUp, window, cx);
1588 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1589 editor.move_left(&MoveLeft, window, cx);
1590 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1591 editor.move_left(&MoveLeft, window, cx);
1592 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1593 });
1594}
1595
1596#[gpui::test]
1597fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1598 init_test(cx, |_| {});
1599
1600 let editor = cx.add_window(|window, cx| {
1601 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1602 build_editor(buffer, window, cx)
1603 });
1604 _ = editor.update(cx, |editor, window, cx| {
1605 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1606 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1607 });
1608
1609 // moving above start of document should move selection to start of document,
1610 // but the next move down should still be at the original goal_x
1611 editor.move_up(&MoveUp, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1613
1614 editor.move_down(&MoveDown, window, cx);
1615 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1616
1617 editor.move_down(&MoveDown, window, cx);
1618 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1619
1620 editor.move_down(&MoveDown, window, cx);
1621 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1622
1623 editor.move_down(&MoveDown, window, cx);
1624 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1625
1626 // moving past end of document should not change goal_x
1627 editor.move_down(&MoveDown, window, cx);
1628 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1629
1630 editor.move_down(&MoveDown, window, cx);
1631 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1632
1633 editor.move_up(&MoveUp, window, cx);
1634 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1635
1636 editor.move_up(&MoveUp, window, cx);
1637 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1638
1639 editor.move_up(&MoveUp, window, cx);
1640 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1641 });
1642}
1643
1644#[gpui::test]
1645fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1646 init_test(cx, |_| {});
1647 let move_to_beg = MoveToBeginningOfLine {
1648 stop_at_soft_wraps: true,
1649 stop_at_indent: true,
1650 };
1651
1652 let delete_to_beg = DeleteToBeginningOfLine {
1653 stop_at_indent: false,
1654 };
1655
1656 let move_to_end = MoveToEndOfLine {
1657 stop_at_soft_wraps: true,
1658 };
1659
1660 let editor = cx.add_window(|window, cx| {
1661 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1662 build_editor(buffer, window, cx)
1663 });
1664 _ = editor.update(cx, |editor, window, cx| {
1665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1666 s.select_display_ranges([
1667 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1669 ]);
1670 });
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1675 assert_eq!(
1676 display_ranges(editor, cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1679 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1686 assert_eq!(
1687 display_ranges(editor, cx),
1688 &[
1689 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1691 ]
1692 );
1693 });
1694
1695 _ = editor.update(cx, |editor, window, cx| {
1696 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1697 assert_eq!(
1698 display_ranges(editor, cx),
1699 &[
1700 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1701 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1702 ]
1703 );
1704 });
1705
1706 _ = editor.update(cx, |editor, window, cx| {
1707 editor.move_to_end_of_line(&move_to_end, window, cx);
1708 assert_eq!(
1709 display_ranges(editor, cx),
1710 &[
1711 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1712 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1713 ]
1714 );
1715 });
1716
1717 // Moving to the end of line again is a no-op.
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.move_to_end_of_line(&move_to_end, window, cx);
1720 assert_eq!(
1721 display_ranges(editor, cx),
1722 &[
1723 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1724 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1725 ]
1726 );
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.move_left(&MoveLeft, window, cx);
1731 editor.select_to_beginning_of_line(
1732 &SelectToBeginningOfLine {
1733 stop_at_soft_wraps: true,
1734 stop_at_indent: true,
1735 },
1736 window,
1737 cx,
1738 );
1739 assert_eq!(
1740 display_ranges(editor, cx),
1741 &[
1742 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1743 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1744 ]
1745 );
1746 });
1747
1748 _ = editor.update(cx, |editor, window, cx| {
1749 editor.select_to_beginning_of_line(
1750 &SelectToBeginningOfLine {
1751 stop_at_soft_wraps: true,
1752 stop_at_indent: true,
1753 },
1754 window,
1755 cx,
1756 );
1757 assert_eq!(
1758 display_ranges(editor, cx),
1759 &[
1760 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1761 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1762 ]
1763 );
1764 });
1765
1766 _ = editor.update(cx, |editor, window, cx| {
1767 editor.select_to_beginning_of_line(
1768 &SelectToBeginningOfLine {
1769 stop_at_soft_wraps: true,
1770 stop_at_indent: true,
1771 },
1772 window,
1773 cx,
1774 );
1775 assert_eq!(
1776 display_ranges(editor, cx),
1777 &[
1778 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1779 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1780 ]
1781 );
1782 });
1783
1784 _ = editor.update(cx, |editor, window, cx| {
1785 editor.select_to_end_of_line(
1786 &SelectToEndOfLine {
1787 stop_at_soft_wraps: true,
1788 },
1789 window,
1790 cx,
1791 );
1792 assert_eq!(
1793 display_ranges(editor, cx),
1794 &[
1795 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1796 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1797 ]
1798 );
1799 });
1800
1801 _ = editor.update(cx, |editor, window, cx| {
1802 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1803 assert_eq!(editor.display_text(cx), "ab\n de");
1804 assert_eq!(
1805 display_ranges(editor, cx),
1806 &[
1807 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1808 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1809 ]
1810 );
1811 });
1812
1813 _ = editor.update(cx, |editor, window, cx| {
1814 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1815 assert_eq!(editor.display_text(cx), "\n");
1816 assert_eq!(
1817 display_ranges(editor, cx),
1818 &[
1819 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1820 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1821 ]
1822 );
1823 });
1824}
1825
1826#[gpui::test]
1827fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1828 init_test(cx, |_| {});
1829 let move_to_beg = MoveToBeginningOfLine {
1830 stop_at_soft_wraps: false,
1831 stop_at_indent: false,
1832 };
1833
1834 let move_to_end = MoveToEndOfLine {
1835 stop_at_soft_wraps: false,
1836 };
1837
1838 let editor = cx.add_window(|window, cx| {
1839 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1840 build_editor(buffer, window, cx)
1841 });
1842
1843 _ = editor.update(cx, |editor, window, cx| {
1844 editor.set_wrap_width(Some(140.0.into()), cx);
1845
1846 // We expect the following lines after wrapping
1847 // ```
1848 // thequickbrownfox
1849 // jumpedoverthelazydo
1850 // gs
1851 // ```
1852 // The final `gs` was soft-wrapped onto a new line.
1853 assert_eq!(
1854 "thequickbrownfox\njumpedoverthelaz\nydogs",
1855 editor.display_text(cx),
1856 );
1857
1858 // First, let's assert behavior on the first line, that was not soft-wrapped.
1859 // Start the cursor at the `k` on the first line
1860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1861 s.select_display_ranges([
1862 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1863 ]);
1864 });
1865
1866 // Moving to the beginning of the line should put us at the beginning of the line.
1867 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1868 assert_eq!(
1869 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1870 display_ranges(editor, cx)
1871 );
1872
1873 // Moving to the end of the line should put us at the end of the line.
1874 editor.move_to_end_of_line(&move_to_end, window, cx);
1875 assert_eq!(
1876 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1877 display_ranges(editor, cx)
1878 );
1879
1880 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1881 // Start the cursor at the last line (`y` that was wrapped to a new line)
1882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1883 s.select_display_ranges([
1884 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1885 ]);
1886 });
1887
1888 // Moving to the beginning of the line should put us at the start of the second line of
1889 // display text, i.e., the `j`.
1890 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1891 assert_eq!(
1892 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1893 display_ranges(editor, cx)
1894 );
1895
1896 // Moving to the beginning of the line again should be a no-op.
1897 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1898 assert_eq!(
1899 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1900 display_ranges(editor, cx)
1901 );
1902
1903 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1904 // next display line.
1905 editor.move_to_end_of_line(&move_to_end, window, cx);
1906 assert_eq!(
1907 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1908 display_ranges(editor, cx)
1909 );
1910
1911 // Moving to the end of the line again should be a no-op.
1912 editor.move_to_end_of_line(&move_to_end, window, cx);
1913 assert_eq!(
1914 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1915 display_ranges(editor, cx)
1916 );
1917 });
1918}
1919
1920#[gpui::test]
1921fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1922 init_test(cx, |_| {});
1923
1924 let move_to_beg = MoveToBeginningOfLine {
1925 stop_at_soft_wraps: true,
1926 stop_at_indent: true,
1927 };
1928
1929 let select_to_beg = SelectToBeginningOfLine {
1930 stop_at_soft_wraps: true,
1931 stop_at_indent: true,
1932 };
1933
1934 let delete_to_beg = DeleteToBeginningOfLine {
1935 stop_at_indent: true,
1936 };
1937
1938 let move_to_end = MoveToEndOfLine {
1939 stop_at_soft_wraps: false,
1940 };
1941
1942 let editor = cx.add_window(|window, cx| {
1943 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1944 build_editor(buffer, window, cx)
1945 });
1946
1947 _ = editor.update(cx, |editor, window, cx| {
1948 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1949 s.select_display_ranges([
1950 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1951 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1952 ]);
1953 });
1954
1955 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1956 // and the second cursor at the first non-whitespace character in the line.
1957 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1958 assert_eq!(
1959 display_ranges(editor, cx),
1960 &[
1961 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1962 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1963 ]
1964 );
1965
1966 // Moving to the beginning of the line again should be a no-op for the first cursor,
1967 // and should move the second cursor to the beginning of the line.
1968 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1969 assert_eq!(
1970 display_ranges(editor, cx),
1971 &[
1972 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1973 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1974 ]
1975 );
1976
1977 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1978 // and should move the second cursor back to the first non-whitespace character in the line.
1979 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1980 assert_eq!(
1981 display_ranges(editor, cx),
1982 &[
1983 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1984 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1985 ]
1986 );
1987
1988 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1989 // and to the first non-whitespace character in the line for the second cursor.
1990 editor.move_to_end_of_line(&move_to_end, window, cx);
1991 editor.move_left(&MoveLeft, window, cx);
1992 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1993 assert_eq!(
1994 display_ranges(editor, cx),
1995 &[
1996 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1997 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1998 ]
1999 );
2000
2001 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2002 // and should select to the beginning of the line for the second cursor.
2003 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2004 assert_eq!(
2005 display_ranges(editor, cx),
2006 &[
2007 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2008 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2009 ]
2010 );
2011
2012 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2013 // and should delete to the first non-whitespace character in the line for the second cursor.
2014 editor.move_to_end_of_line(&move_to_end, window, cx);
2015 editor.move_left(&MoveLeft, window, cx);
2016 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2017 assert_eq!(editor.text(cx), "c\n f");
2018 });
2019}
2020
2021#[gpui::test]
2022fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2023 init_test(cx, |_| {});
2024
2025 let move_to_beg = MoveToBeginningOfLine {
2026 stop_at_soft_wraps: true,
2027 stop_at_indent: true,
2028 };
2029
2030 let editor = cx.add_window(|window, cx| {
2031 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2032 build_editor(buffer, window, cx)
2033 });
2034
2035 _ = editor.update(cx, |editor, window, cx| {
2036 // test cursor between line_start and indent_start
2037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2038 s.select_display_ranges([
2039 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2040 ]);
2041 });
2042
2043 // cursor should move to line_start
2044 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2045 assert_eq!(
2046 display_ranges(editor, cx),
2047 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2048 );
2049
2050 // cursor should move to indent_start
2051 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2052 assert_eq!(
2053 display_ranges(editor, cx),
2054 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2055 );
2056
2057 // cursor should move to back to line_start
2058 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2059 assert_eq!(
2060 display_ranges(editor, cx),
2061 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2062 );
2063 });
2064}
2065
2066#[gpui::test]
2067fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2068 init_test(cx, |_| {});
2069
2070 let editor = cx.add_window(|window, cx| {
2071 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2072 build_editor(buffer, window, cx)
2073 });
2074 _ = editor.update(cx, |editor, window, cx| {
2075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2076 s.select_display_ranges([
2077 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2078 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2079 ])
2080 });
2081 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2082 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2083
2084 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2085 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2086
2087 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2088 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2089
2090 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2091 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2092
2093 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2094 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2095
2096 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2097 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2098
2099 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2100 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2101
2102 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2103 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2104
2105 editor.move_right(&MoveRight, window, cx);
2106 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2107 assert_selection_ranges(
2108 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2109 editor,
2110 cx,
2111 );
2112
2113 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2114 assert_selection_ranges(
2115 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2116 editor,
2117 cx,
2118 );
2119
2120 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2121 assert_selection_ranges(
2122 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2123 editor,
2124 cx,
2125 );
2126 });
2127}
2128
2129#[gpui::test]
2130fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2131 init_test(cx, |_| {});
2132
2133 let editor = cx.add_window(|window, cx| {
2134 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2135 build_editor(buffer, window, cx)
2136 });
2137
2138 _ = editor.update(cx, |editor, window, cx| {
2139 editor.set_wrap_width(Some(140.0.into()), cx);
2140 assert_eq!(
2141 editor.display_text(cx),
2142 "use one::{\n two::three::\n four::five\n};"
2143 );
2144
2145 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2146 s.select_display_ranges([
2147 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2148 ]);
2149 });
2150
2151 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2152 assert_eq!(
2153 display_ranges(editor, cx),
2154 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2155 );
2156
2157 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2158 assert_eq!(
2159 display_ranges(editor, cx),
2160 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2161 );
2162
2163 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2164 assert_eq!(
2165 display_ranges(editor, cx),
2166 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2167 );
2168
2169 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2170 assert_eq!(
2171 display_ranges(editor, cx),
2172 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2173 );
2174
2175 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2176 assert_eq!(
2177 display_ranges(editor, cx),
2178 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2179 );
2180
2181 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2182 assert_eq!(
2183 display_ranges(editor, cx),
2184 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2185 );
2186 });
2187}
2188
2189#[gpui::test]
2190async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192 let mut cx = EditorTestContext::new(cx).await;
2193
2194 let line_height = cx.editor(|editor, window, _| {
2195 editor
2196 .style()
2197 .unwrap()
2198 .text
2199 .line_height_in_pixels(window.rem_size())
2200 });
2201 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2202
2203 cx.set_state(
2204 &r#"ˇone
2205 two
2206
2207 three
2208 fourˇ
2209 five
2210
2211 six"#
2212 .unindent(),
2213 );
2214
2215 cx.update_editor(|editor, window, cx| {
2216 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2217 });
2218 cx.assert_editor_state(
2219 &r#"one
2220 two
2221 ˇ
2222 three
2223 four
2224 five
2225 ˇ
2226 six"#
2227 .unindent(),
2228 );
2229
2230 cx.update_editor(|editor, window, cx| {
2231 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2232 });
2233 cx.assert_editor_state(
2234 &r#"one
2235 two
2236
2237 three
2238 four
2239 five
2240 ˇ
2241 sixˇ"#
2242 .unindent(),
2243 );
2244
2245 cx.update_editor(|editor, window, cx| {
2246 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2247 });
2248 cx.assert_editor_state(
2249 &r#"one
2250 two
2251
2252 three
2253 four
2254 five
2255
2256 sixˇ"#
2257 .unindent(),
2258 );
2259
2260 cx.update_editor(|editor, window, cx| {
2261 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2262 });
2263 cx.assert_editor_state(
2264 &r#"one
2265 two
2266
2267 three
2268 four
2269 five
2270 ˇ
2271 six"#
2272 .unindent(),
2273 );
2274
2275 cx.update_editor(|editor, window, cx| {
2276 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2277 });
2278 cx.assert_editor_state(
2279 &r#"one
2280 two
2281 ˇ
2282 three
2283 four
2284 five
2285
2286 six"#
2287 .unindent(),
2288 );
2289
2290 cx.update_editor(|editor, window, cx| {
2291 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2292 });
2293 cx.assert_editor_state(
2294 &r#"ˇone
2295 two
2296
2297 three
2298 four
2299 five
2300
2301 six"#
2302 .unindent(),
2303 );
2304}
2305
2306#[gpui::test]
2307async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2308 init_test(cx, |_| {});
2309 let mut cx = EditorTestContext::new(cx).await;
2310 let line_height = cx.editor(|editor, window, _| {
2311 editor
2312 .style()
2313 .unwrap()
2314 .text
2315 .line_height_in_pixels(window.rem_size())
2316 });
2317 let window = cx.window;
2318 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2319
2320 cx.set_state(
2321 r#"ˇone
2322 two
2323 three
2324 four
2325 five
2326 six
2327 seven
2328 eight
2329 nine
2330 ten
2331 "#,
2332 );
2333
2334 cx.update_editor(|editor, window, cx| {
2335 assert_eq!(
2336 editor.snapshot(window, cx).scroll_position(),
2337 gpui::Point::new(0., 0.)
2338 );
2339 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2340 assert_eq!(
2341 editor.snapshot(window, cx).scroll_position(),
2342 gpui::Point::new(0., 3.)
2343 );
2344 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2345 assert_eq!(
2346 editor.snapshot(window, cx).scroll_position(),
2347 gpui::Point::new(0., 6.)
2348 );
2349 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2350 assert_eq!(
2351 editor.snapshot(window, cx).scroll_position(),
2352 gpui::Point::new(0., 3.)
2353 );
2354
2355 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2356 assert_eq!(
2357 editor.snapshot(window, cx).scroll_position(),
2358 gpui::Point::new(0., 1.)
2359 );
2360 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2361 assert_eq!(
2362 editor.snapshot(window, cx).scroll_position(),
2363 gpui::Point::new(0., 3.)
2364 );
2365 });
2366}
2367
2368#[gpui::test]
2369async fn test_autoscroll(cx: &mut TestAppContext) {
2370 init_test(cx, |_| {});
2371 let mut cx = EditorTestContext::new(cx).await;
2372
2373 let line_height = cx.update_editor(|editor, window, cx| {
2374 editor.set_vertical_scroll_margin(2, cx);
2375 editor
2376 .style()
2377 .unwrap()
2378 .text
2379 .line_height_in_pixels(window.rem_size())
2380 });
2381 let window = cx.window;
2382 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2383
2384 cx.set_state(
2385 r#"ˇone
2386 two
2387 three
2388 four
2389 five
2390 six
2391 seven
2392 eight
2393 nine
2394 ten
2395 "#,
2396 );
2397 cx.update_editor(|editor, window, cx| {
2398 assert_eq!(
2399 editor.snapshot(window, cx).scroll_position(),
2400 gpui::Point::new(0., 0.0)
2401 );
2402 });
2403
2404 // Add a cursor below the visible area. Since both cursors cannot fit
2405 // on screen, the editor autoscrolls to reveal the newest cursor, and
2406 // allows the vertical scroll margin below that cursor.
2407 cx.update_editor(|editor, window, cx| {
2408 editor.change_selections(Default::default(), window, cx, |selections| {
2409 selections.select_ranges([
2410 Point::new(0, 0)..Point::new(0, 0),
2411 Point::new(6, 0)..Point::new(6, 0),
2412 ]);
2413 })
2414 });
2415 cx.update_editor(|editor, window, cx| {
2416 assert_eq!(
2417 editor.snapshot(window, cx).scroll_position(),
2418 gpui::Point::new(0., 3.0)
2419 );
2420 });
2421
2422 // Move down. The editor cursor scrolls down to track the newest cursor.
2423 cx.update_editor(|editor, window, cx| {
2424 editor.move_down(&Default::default(), window, cx);
2425 });
2426 cx.update_editor(|editor, window, cx| {
2427 assert_eq!(
2428 editor.snapshot(window, cx).scroll_position(),
2429 gpui::Point::new(0., 4.0)
2430 );
2431 });
2432
2433 // Add a cursor above the visible area. Since both cursors fit on screen,
2434 // the editor scrolls to show both.
2435 cx.update_editor(|editor, window, cx| {
2436 editor.change_selections(Default::default(), window, cx, |selections| {
2437 selections.select_ranges([
2438 Point::new(1, 0)..Point::new(1, 0),
2439 Point::new(6, 0)..Point::new(6, 0),
2440 ]);
2441 })
2442 });
2443 cx.update_editor(|editor, window, cx| {
2444 assert_eq!(
2445 editor.snapshot(window, cx).scroll_position(),
2446 gpui::Point::new(0., 1.0)
2447 );
2448 });
2449}
2450
2451#[gpui::test]
2452async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2453 init_test(cx, |_| {});
2454 let mut cx = EditorTestContext::new(cx).await;
2455
2456 let line_height = cx.editor(|editor, window, _cx| {
2457 editor
2458 .style()
2459 .unwrap()
2460 .text
2461 .line_height_in_pixels(window.rem_size())
2462 });
2463 let window = cx.window;
2464 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2465 cx.set_state(
2466 &r#"
2467 ˇone
2468 two
2469 threeˇ
2470 four
2471 five
2472 six
2473 seven
2474 eight
2475 nine
2476 ten
2477 "#
2478 .unindent(),
2479 );
2480
2481 cx.update_editor(|editor, window, cx| {
2482 editor.move_page_down(&MovePageDown::default(), window, cx)
2483 });
2484 cx.assert_editor_state(
2485 &r#"
2486 one
2487 two
2488 three
2489 ˇfour
2490 five
2491 sixˇ
2492 seven
2493 eight
2494 nine
2495 ten
2496 "#
2497 .unindent(),
2498 );
2499
2500 cx.update_editor(|editor, window, cx| {
2501 editor.move_page_down(&MovePageDown::default(), window, cx)
2502 });
2503 cx.assert_editor_state(
2504 &r#"
2505 one
2506 two
2507 three
2508 four
2509 five
2510 six
2511 ˇseven
2512 eight
2513 nineˇ
2514 ten
2515 "#
2516 .unindent(),
2517 );
2518
2519 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2520 cx.assert_editor_state(
2521 &r#"
2522 one
2523 two
2524 three
2525 ˇfour
2526 five
2527 sixˇ
2528 seven
2529 eight
2530 nine
2531 ten
2532 "#
2533 .unindent(),
2534 );
2535
2536 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2537 cx.assert_editor_state(
2538 &r#"
2539 ˇone
2540 two
2541 threeˇ
2542 four
2543 five
2544 six
2545 seven
2546 eight
2547 nine
2548 ten
2549 "#
2550 .unindent(),
2551 );
2552
2553 // Test select collapsing
2554 cx.update_editor(|editor, window, cx| {
2555 editor.move_page_down(&MovePageDown::default(), window, cx);
2556 editor.move_page_down(&MovePageDown::default(), window, cx);
2557 editor.move_page_down(&MovePageDown::default(), window, cx);
2558 });
2559 cx.assert_editor_state(
2560 &r#"
2561 one
2562 two
2563 three
2564 four
2565 five
2566 six
2567 seven
2568 eight
2569 nine
2570 ˇten
2571 ˇ"#
2572 .unindent(),
2573 );
2574}
2575
2576#[gpui::test]
2577async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2578 init_test(cx, |_| {});
2579 let mut cx = EditorTestContext::new(cx).await;
2580 cx.set_state("one «two threeˇ» four");
2581 cx.update_editor(|editor, window, cx| {
2582 editor.delete_to_beginning_of_line(
2583 &DeleteToBeginningOfLine {
2584 stop_at_indent: false,
2585 },
2586 window,
2587 cx,
2588 );
2589 assert_eq!(editor.text(cx), " four");
2590 });
2591}
2592
2593#[gpui::test]
2594async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2595 init_test(cx, |_| {});
2596
2597 let mut cx = EditorTestContext::new(cx).await;
2598
2599 // For an empty selection, the preceding word fragment is deleted.
2600 // For non-empty selections, only selected characters are deleted.
2601 cx.set_state("onˇe two t«hreˇ»e four");
2602 cx.update_editor(|editor, window, cx| {
2603 editor.delete_to_previous_word_start(
2604 &DeleteToPreviousWordStart {
2605 ignore_newlines: false,
2606 ignore_brackets: false,
2607 },
2608 window,
2609 cx,
2610 );
2611 });
2612 cx.assert_editor_state("ˇe two tˇe four");
2613
2614 cx.set_state("e tˇwo te «fˇ»our");
2615 cx.update_editor(|editor, window, cx| {
2616 editor.delete_to_next_word_end(
2617 &DeleteToNextWordEnd {
2618 ignore_newlines: false,
2619 ignore_brackets: false,
2620 },
2621 window,
2622 cx,
2623 );
2624 });
2625 cx.assert_editor_state("e tˇ te ˇour");
2626}
2627
2628#[gpui::test]
2629async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2630 init_test(cx, |_| {});
2631
2632 let mut cx = EditorTestContext::new(cx).await;
2633
2634 cx.set_state("here is some text ˇwith a space");
2635 cx.update_editor(|editor, window, cx| {
2636 editor.delete_to_previous_word_start(
2637 &DeleteToPreviousWordStart {
2638 ignore_newlines: false,
2639 ignore_brackets: true,
2640 },
2641 window,
2642 cx,
2643 );
2644 });
2645 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2646 cx.assert_editor_state("here is some textˇwith a space");
2647
2648 cx.set_state("here is some text ˇwith a space");
2649 cx.update_editor(|editor, window, cx| {
2650 editor.delete_to_previous_word_start(
2651 &DeleteToPreviousWordStart {
2652 ignore_newlines: false,
2653 ignore_brackets: false,
2654 },
2655 window,
2656 cx,
2657 );
2658 });
2659 cx.assert_editor_state("here is some textˇwith a space");
2660
2661 cx.set_state("here is some textˇ with a space");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_next_word_end(
2664 &DeleteToNextWordEnd {
2665 ignore_newlines: false,
2666 ignore_brackets: true,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 // Same happens in the other direction.
2673 cx.assert_editor_state("here is some textˇwith a space");
2674
2675 cx.set_state("here is some textˇ with a space");
2676 cx.update_editor(|editor, window, cx| {
2677 editor.delete_to_next_word_end(
2678 &DeleteToNextWordEnd {
2679 ignore_newlines: false,
2680 ignore_brackets: false,
2681 },
2682 window,
2683 cx,
2684 );
2685 });
2686 cx.assert_editor_state("here is some textˇwith a space");
2687
2688 cx.set_state("here is some textˇ with a space");
2689 cx.update_editor(|editor, window, cx| {
2690 editor.delete_to_next_word_end(
2691 &DeleteToNextWordEnd {
2692 ignore_newlines: true,
2693 ignore_brackets: false,
2694 },
2695 window,
2696 cx,
2697 );
2698 });
2699 cx.assert_editor_state("here is some textˇwith a space");
2700 cx.update_editor(|editor, window, cx| {
2701 editor.delete_to_previous_word_start(
2702 &DeleteToPreviousWordStart {
2703 ignore_newlines: true,
2704 ignore_brackets: false,
2705 },
2706 window,
2707 cx,
2708 );
2709 });
2710 cx.assert_editor_state("here is some ˇwith a space");
2711 cx.update_editor(|editor, window, cx| {
2712 editor.delete_to_previous_word_start(
2713 &DeleteToPreviousWordStart {
2714 ignore_newlines: true,
2715 ignore_brackets: false,
2716 },
2717 window,
2718 cx,
2719 );
2720 });
2721 // Single whitespaces are removed with the word behind them.
2722 cx.assert_editor_state("here is ˇwith a space");
2723 cx.update_editor(|editor, window, cx| {
2724 editor.delete_to_previous_word_start(
2725 &DeleteToPreviousWordStart {
2726 ignore_newlines: true,
2727 ignore_brackets: false,
2728 },
2729 window,
2730 cx,
2731 );
2732 });
2733 cx.assert_editor_state("here ˇwith a space");
2734 cx.update_editor(|editor, window, cx| {
2735 editor.delete_to_previous_word_start(
2736 &DeleteToPreviousWordStart {
2737 ignore_newlines: true,
2738 ignore_brackets: false,
2739 },
2740 window,
2741 cx,
2742 );
2743 });
2744 cx.assert_editor_state("ˇwith a space");
2745 cx.update_editor(|editor, window, cx| {
2746 editor.delete_to_previous_word_start(
2747 &DeleteToPreviousWordStart {
2748 ignore_newlines: true,
2749 ignore_brackets: false,
2750 },
2751 window,
2752 cx,
2753 );
2754 });
2755 cx.assert_editor_state("ˇwith a space");
2756 cx.update_editor(|editor, window, cx| {
2757 editor.delete_to_next_word_end(
2758 &DeleteToNextWordEnd {
2759 ignore_newlines: true,
2760 ignore_brackets: false,
2761 },
2762 window,
2763 cx,
2764 );
2765 });
2766 // Same happens in the other direction.
2767 cx.assert_editor_state("ˇ a space");
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_next_word_end(
2770 &DeleteToNextWordEnd {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state("ˇ space");
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_next_word_end(
2781 &DeleteToNextWordEnd {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state("ˇ");
2790 cx.update_editor(|editor, window, cx| {
2791 editor.delete_to_next_word_end(
2792 &DeleteToNextWordEnd {
2793 ignore_newlines: true,
2794 ignore_brackets: false,
2795 },
2796 window,
2797 cx,
2798 );
2799 });
2800 cx.assert_editor_state("ˇ");
2801 cx.update_editor(|editor, window, cx| {
2802 editor.delete_to_previous_word_start(
2803 &DeleteToPreviousWordStart {
2804 ignore_newlines: true,
2805 ignore_brackets: false,
2806 },
2807 window,
2808 cx,
2809 );
2810 });
2811 cx.assert_editor_state("ˇ");
2812}
2813
2814#[gpui::test]
2815async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2816 init_test(cx, |_| {});
2817
2818 let language = Arc::new(
2819 Language::new(
2820 LanguageConfig {
2821 brackets: BracketPairConfig {
2822 pairs: vec![
2823 BracketPair {
2824 start: "\"".to_string(),
2825 end: "\"".to_string(),
2826 close: true,
2827 surround: true,
2828 newline: false,
2829 },
2830 BracketPair {
2831 start: "(".to_string(),
2832 end: ")".to_string(),
2833 close: true,
2834 surround: true,
2835 newline: true,
2836 },
2837 ],
2838 ..BracketPairConfig::default()
2839 },
2840 ..LanguageConfig::default()
2841 },
2842 Some(tree_sitter_rust::LANGUAGE.into()),
2843 )
2844 .with_brackets_query(
2845 r#"
2846 ("(" @open ")" @close)
2847 ("\"" @open "\"" @close)
2848 "#,
2849 )
2850 .unwrap(),
2851 );
2852
2853 let mut cx = EditorTestContext::new(cx).await;
2854 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2855
2856 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2857 cx.update_editor(|editor, window, cx| {
2858 editor.delete_to_previous_word_start(
2859 &DeleteToPreviousWordStart {
2860 ignore_newlines: true,
2861 ignore_brackets: false,
2862 },
2863 window,
2864 cx,
2865 );
2866 });
2867 // Deletion stops before brackets if asked to not ignore them.
2868 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2869 cx.update_editor(|editor, window, cx| {
2870 editor.delete_to_previous_word_start(
2871 &DeleteToPreviousWordStart {
2872 ignore_newlines: true,
2873 ignore_brackets: false,
2874 },
2875 window,
2876 cx,
2877 );
2878 });
2879 // Deletion has to remove a single bracket and then stop again.
2880 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2881
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 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2893
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 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2905
2906 cx.update_editor(|editor, window, cx| {
2907 editor.delete_to_previous_word_start(
2908 &DeleteToPreviousWordStart {
2909 ignore_newlines: true,
2910 ignore_brackets: false,
2911 },
2912 window,
2913 cx,
2914 );
2915 });
2916 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2917
2918 cx.update_editor(|editor, window, cx| {
2919 editor.delete_to_next_word_end(
2920 &DeleteToNextWordEnd {
2921 ignore_newlines: true,
2922 ignore_brackets: false,
2923 },
2924 window,
2925 cx,
2926 );
2927 });
2928 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2929 cx.assert_editor_state(r#"ˇ");"#);
2930
2931 cx.update_editor(|editor, window, cx| {
2932 editor.delete_to_next_word_end(
2933 &DeleteToNextWordEnd {
2934 ignore_newlines: true,
2935 ignore_brackets: false,
2936 },
2937 window,
2938 cx,
2939 );
2940 });
2941 cx.assert_editor_state(r#"ˇ"#);
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 cx.assert_editor_state(r#"ˇ"#);
2954
2955 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_previous_word_start(
2958 &DeleteToPreviousWordStart {
2959 ignore_newlines: true,
2960 ignore_brackets: true,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2967}
2968
2969#[gpui::test]
2970fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2971 init_test(cx, |_| {});
2972
2973 let editor = cx.add_window(|window, cx| {
2974 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2975 build_editor(buffer, window, cx)
2976 });
2977 let del_to_prev_word_start = DeleteToPreviousWordStart {
2978 ignore_newlines: false,
2979 ignore_brackets: false,
2980 };
2981 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2982 ignore_newlines: true,
2983 ignore_brackets: false,
2984 };
2985
2986 _ = editor.update(cx, |editor, window, cx| {
2987 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2988 s.select_display_ranges([
2989 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2990 ])
2991 });
2992 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2993 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2994 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2995 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2996 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2997 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2998 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2999 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3000 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3001 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3002 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3003 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3004 });
3005}
3006
3007#[gpui::test]
3008fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3009 init_test(cx, |_| {});
3010
3011 let editor = cx.add_window(|window, cx| {
3012 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3013 build_editor(buffer, window, cx)
3014 });
3015 let del_to_next_word_end = DeleteToNextWordEnd {
3016 ignore_newlines: false,
3017 ignore_brackets: false,
3018 };
3019 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3020 ignore_newlines: true,
3021 ignore_brackets: false,
3022 };
3023
3024 _ = editor.update(cx, |editor, window, cx| {
3025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3026 s.select_display_ranges([
3027 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3028 ])
3029 });
3030 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3031 assert_eq!(
3032 editor.buffer.read(cx).read(cx).text(),
3033 "one\n two\nthree\n four"
3034 );
3035 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3036 assert_eq!(
3037 editor.buffer.read(cx).read(cx).text(),
3038 "\n two\nthree\n four"
3039 );
3040 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3041 assert_eq!(
3042 editor.buffer.read(cx).read(cx).text(),
3043 "two\nthree\n four"
3044 );
3045 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3046 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3047 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3048 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3049 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3050 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3051 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3052 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3053 });
3054}
3055
3056#[gpui::test]
3057fn test_newline(cx: &mut TestAppContext) {
3058 init_test(cx, |_| {});
3059
3060 let editor = cx.add_window(|window, cx| {
3061 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3062 build_editor(buffer, window, cx)
3063 });
3064
3065 _ = editor.update(cx, |editor, window, cx| {
3066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3067 s.select_display_ranges([
3068 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3069 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3070 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3071 ])
3072 });
3073
3074 editor.newline(&Newline, window, cx);
3075 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3076 });
3077}
3078
3079#[gpui::test]
3080async fn test_newline_yaml(cx: &mut TestAppContext) {
3081 init_test(cx, |_| {});
3082
3083 let mut cx = EditorTestContext::new(cx).await;
3084 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3085 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3086
3087 // Object (between 2 fields)
3088 cx.set_state(indoc! {"
3089 test:ˇ
3090 hello: bye"});
3091 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 test:
3094 ˇ
3095 hello: bye"});
3096
3097 // Object (first and single line)
3098 cx.set_state(indoc! {"
3099 test:ˇ"});
3100 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3101 cx.assert_editor_state(indoc! {"
3102 test:
3103 ˇ"});
3104
3105 // Array with objects (after first element)
3106 cx.set_state(indoc! {"
3107 test:
3108 - foo: barˇ"});
3109 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 test:
3112 - foo: bar
3113 ˇ"});
3114
3115 // Array with objects and comment
3116 cx.set_state(indoc! {"
3117 test:
3118 - foo: bar
3119 - bar: # testˇ"});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 test:
3123 - foo: bar
3124 - bar: # test
3125 ˇ"});
3126
3127 // Array with objects (after second element)
3128 cx.set_state(indoc! {"
3129 test:
3130 - foo: bar
3131 - bar: fooˇ"});
3132 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3133 cx.assert_editor_state(indoc! {"
3134 test:
3135 - foo: bar
3136 - bar: foo
3137 ˇ"});
3138
3139 // Array with strings (after first element)
3140 cx.set_state(indoc! {"
3141 test:
3142 - fooˇ"});
3143 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3144 cx.assert_editor_state(indoc! {"
3145 test:
3146 - foo
3147 ˇ"});
3148}
3149
3150#[gpui::test]
3151fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3152 init_test(cx, |_| {});
3153
3154 let editor = cx.add_window(|window, cx| {
3155 let buffer = MultiBuffer::build_simple(
3156 "
3157 a
3158 b(
3159 X
3160 )
3161 c(
3162 X
3163 )
3164 "
3165 .unindent()
3166 .as_str(),
3167 cx,
3168 );
3169 let mut editor = build_editor(buffer, window, cx);
3170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3171 s.select_ranges([
3172 Point::new(2, 4)..Point::new(2, 5),
3173 Point::new(5, 4)..Point::new(5, 5),
3174 ])
3175 });
3176 editor
3177 });
3178
3179 _ = editor.update(cx, |editor, window, cx| {
3180 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3181 editor.buffer.update(cx, |buffer, cx| {
3182 buffer.edit(
3183 [
3184 (Point::new(1, 2)..Point::new(3, 0), ""),
3185 (Point::new(4, 2)..Point::new(6, 0), ""),
3186 ],
3187 None,
3188 cx,
3189 );
3190 assert_eq!(
3191 buffer.read(cx).text(),
3192 "
3193 a
3194 b()
3195 c()
3196 "
3197 .unindent()
3198 );
3199 });
3200 assert_eq!(
3201 editor.selections.ranges(&editor.display_snapshot(cx)),
3202 &[
3203 Point::new(1, 2)..Point::new(1, 2),
3204 Point::new(2, 2)..Point::new(2, 2),
3205 ],
3206 );
3207
3208 editor.newline(&Newline, window, cx);
3209 assert_eq!(
3210 editor.text(cx),
3211 "
3212 a
3213 b(
3214 )
3215 c(
3216 )
3217 "
3218 .unindent()
3219 );
3220
3221 // The selections are moved after the inserted newlines
3222 assert_eq!(
3223 editor.selections.ranges(&editor.display_snapshot(cx)),
3224 &[
3225 Point::new(2, 0)..Point::new(2, 0),
3226 Point::new(4, 0)..Point::new(4, 0),
3227 ],
3228 );
3229 });
3230}
3231
3232#[gpui::test]
3233async fn test_newline_above(cx: &mut TestAppContext) {
3234 init_test(cx, |settings| {
3235 settings.defaults.tab_size = NonZeroU32::new(4)
3236 });
3237
3238 let language = Arc::new(
3239 Language::new(
3240 LanguageConfig::default(),
3241 Some(tree_sitter_rust::LANGUAGE.into()),
3242 )
3243 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3244 .unwrap(),
3245 );
3246
3247 let mut cx = EditorTestContext::new(cx).await;
3248 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3249 cx.set_state(indoc! {"
3250 const a: ˇA = (
3251 (ˇ
3252 «const_functionˇ»(ˇ),
3253 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3254 )ˇ
3255 ˇ);ˇ
3256 "});
3257
3258 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3259 cx.assert_editor_state(indoc! {"
3260 ˇ
3261 const a: A = (
3262 ˇ
3263 (
3264 ˇ
3265 ˇ
3266 const_function(),
3267 ˇ
3268 ˇ
3269 ˇ
3270 ˇ
3271 something_else,
3272 ˇ
3273 )
3274 ˇ
3275 ˇ
3276 );
3277 "});
3278}
3279
3280#[gpui::test]
3281async fn test_newline_below(cx: &mut TestAppContext) {
3282 init_test(cx, |settings| {
3283 settings.defaults.tab_size = NonZeroU32::new(4)
3284 });
3285
3286 let language = Arc::new(
3287 Language::new(
3288 LanguageConfig::default(),
3289 Some(tree_sitter_rust::LANGUAGE.into()),
3290 )
3291 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3292 .unwrap(),
3293 );
3294
3295 let mut cx = EditorTestContext::new(cx).await;
3296 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3297 cx.set_state(indoc! {"
3298 const a: ˇA = (
3299 (ˇ
3300 «const_functionˇ»(ˇ),
3301 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3302 )ˇ
3303 ˇ);ˇ
3304 "});
3305
3306 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3307 cx.assert_editor_state(indoc! {"
3308 const a: A = (
3309 ˇ
3310 (
3311 ˇ
3312 const_function(),
3313 ˇ
3314 ˇ
3315 something_else,
3316 ˇ
3317 ˇ
3318 ˇ
3319 ˇ
3320 )
3321 ˇ
3322 );
3323 ˇ
3324 ˇ
3325 "});
3326}
3327
3328#[gpui::test]
3329async fn test_newline_comments(cx: &mut TestAppContext) {
3330 init_test(cx, |settings| {
3331 settings.defaults.tab_size = NonZeroU32::new(4)
3332 });
3333
3334 let language = Arc::new(Language::new(
3335 LanguageConfig {
3336 line_comments: vec!["// ".into()],
3337 ..LanguageConfig::default()
3338 },
3339 None,
3340 ));
3341 {
3342 let mut cx = EditorTestContext::new(cx).await;
3343 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3344 cx.set_state(indoc! {"
3345 // Fooˇ
3346 "});
3347
3348 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 // Foo
3351 // ˇ
3352 "});
3353 // Ensure that we add comment prefix when existing line contains space
3354 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3355 cx.assert_editor_state(
3356 indoc! {"
3357 // Foo
3358 //s
3359 // ˇ
3360 "}
3361 .replace("s", " ") // s is used as space placeholder to prevent format on save
3362 .as_str(),
3363 );
3364 // Ensure that we add comment prefix when existing line does not contain space
3365 cx.set_state(indoc! {"
3366 // Foo
3367 //ˇ
3368 "});
3369 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3370 cx.assert_editor_state(indoc! {"
3371 // Foo
3372 //
3373 // ˇ
3374 "});
3375 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3376 cx.set_state(indoc! {"
3377 ˇ// Foo
3378 "});
3379 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381
3382 ˇ// Foo
3383 "});
3384 }
3385 // Ensure that comment continuations can be disabled.
3386 update_test_language_settings(cx, |settings| {
3387 settings.defaults.extend_comment_on_newline = Some(false);
3388 });
3389 let mut cx = EditorTestContext::new(cx).await;
3390 cx.set_state(indoc! {"
3391 // Fooˇ
3392 "});
3393 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3394 cx.assert_editor_state(indoc! {"
3395 // Foo
3396 ˇ
3397 "});
3398}
3399
3400#[gpui::test]
3401async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3402 init_test(cx, |settings| {
3403 settings.defaults.tab_size = NonZeroU32::new(4)
3404 });
3405
3406 let language = Arc::new(Language::new(
3407 LanguageConfig {
3408 line_comments: vec!["// ".into(), "/// ".into()],
3409 ..LanguageConfig::default()
3410 },
3411 None,
3412 ));
3413 {
3414 let mut cx = EditorTestContext::new(cx).await;
3415 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3416 cx.set_state(indoc! {"
3417 //ˇ
3418 "});
3419 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3420 cx.assert_editor_state(indoc! {"
3421 //
3422 // ˇ
3423 "});
3424
3425 cx.set_state(indoc! {"
3426 ///ˇ
3427 "});
3428 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3429 cx.assert_editor_state(indoc! {"
3430 ///
3431 /// ˇ
3432 "});
3433 }
3434}
3435
3436#[gpui::test]
3437async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3438 init_test(cx, |settings| {
3439 settings.defaults.tab_size = NonZeroU32::new(4)
3440 });
3441
3442 let language = Arc::new(
3443 Language::new(
3444 LanguageConfig {
3445 documentation_comment: Some(language::BlockCommentConfig {
3446 start: "/**".into(),
3447 end: "*/".into(),
3448 prefix: "* ".into(),
3449 tab_size: 1,
3450 }),
3451
3452 ..LanguageConfig::default()
3453 },
3454 Some(tree_sitter_rust::LANGUAGE.into()),
3455 )
3456 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3457 .unwrap(),
3458 );
3459
3460 {
3461 let mut cx = EditorTestContext::new(cx).await;
3462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3463 cx.set_state(indoc! {"
3464 /**ˇ
3465 "});
3466
3467 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3468 cx.assert_editor_state(indoc! {"
3469 /**
3470 * ˇ
3471 "});
3472 // Ensure that if cursor is before the comment start,
3473 // we do not actually insert a comment prefix.
3474 cx.set_state(indoc! {"
3475 ˇ/**
3476 "});
3477 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479
3480 ˇ/**
3481 "});
3482 // Ensure that if cursor is between it doesn't add comment prefix.
3483 cx.set_state(indoc! {"
3484 /*ˇ*
3485 "});
3486 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3487 cx.assert_editor_state(indoc! {"
3488 /*
3489 ˇ*
3490 "});
3491 // Ensure that if suffix exists on same line after cursor it adds new line.
3492 cx.set_state(indoc! {"
3493 /**ˇ*/
3494 "});
3495 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 /**
3498 * ˇ
3499 */
3500 "});
3501 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3502 cx.set_state(indoc! {"
3503 /**ˇ */
3504 "});
3505 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3506 cx.assert_editor_state(indoc! {"
3507 /**
3508 * ˇ
3509 */
3510 "});
3511 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3512 cx.set_state(indoc! {"
3513 /** ˇ*/
3514 "});
3515 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3516 cx.assert_editor_state(
3517 indoc! {"
3518 /**s
3519 * ˇ
3520 */
3521 "}
3522 .replace("s", " ") // s is used as space placeholder to prevent format on save
3523 .as_str(),
3524 );
3525 // Ensure that delimiter space is preserved when newline on already
3526 // spaced delimiter.
3527 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3528 cx.assert_editor_state(
3529 indoc! {"
3530 /**s
3531 *s
3532 * ˇ
3533 */
3534 "}
3535 .replace("s", " ") // s is used as space placeholder to prevent format on save
3536 .as_str(),
3537 );
3538 // Ensure that delimiter space is preserved when space is not
3539 // on existing delimiter.
3540 cx.set_state(indoc! {"
3541 /**
3542 *ˇ
3543 */
3544 "});
3545 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 /**
3548 *
3549 * ˇ
3550 */
3551 "});
3552 // Ensure that if suffix exists on same line after cursor it
3553 // doesn't add extra new line if prefix is not on same line.
3554 cx.set_state(indoc! {"
3555 /**
3556 ˇ*/
3557 "});
3558 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3559 cx.assert_editor_state(indoc! {"
3560 /**
3561
3562 ˇ*/
3563 "});
3564 // Ensure that it detects suffix after existing prefix.
3565 cx.set_state(indoc! {"
3566 /**ˇ/
3567 "});
3568 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3569 cx.assert_editor_state(indoc! {"
3570 /**
3571 ˇ/
3572 "});
3573 // Ensure that if suffix exists on same line before
3574 // cursor it does not add comment prefix.
3575 cx.set_state(indoc! {"
3576 /** */ˇ
3577 "});
3578 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3579 cx.assert_editor_state(indoc! {"
3580 /** */
3581 ˇ
3582 "});
3583 // Ensure that if suffix exists on same line before
3584 // cursor it does not add comment prefix.
3585 cx.set_state(indoc! {"
3586 /**
3587 *
3588 */ˇ
3589 "});
3590 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3591 cx.assert_editor_state(indoc! {"
3592 /**
3593 *
3594 */
3595 ˇ
3596 "});
3597
3598 // Ensure that inline comment followed by code
3599 // doesn't add comment prefix on newline
3600 cx.set_state(indoc! {"
3601 /** */ textˇ
3602 "});
3603 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3604 cx.assert_editor_state(indoc! {"
3605 /** */ text
3606 ˇ
3607 "});
3608
3609 // Ensure that text after comment end tag
3610 // doesn't add comment prefix on newline
3611 cx.set_state(indoc! {"
3612 /**
3613 *
3614 */ˇtext
3615 "});
3616 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3617 cx.assert_editor_state(indoc! {"
3618 /**
3619 *
3620 */
3621 ˇtext
3622 "});
3623
3624 // Ensure if not comment block it doesn't
3625 // add comment prefix on newline
3626 cx.set_state(indoc! {"
3627 * textˇ
3628 "});
3629 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3630 cx.assert_editor_state(indoc! {"
3631 * text
3632 ˇ
3633 "});
3634 }
3635 // Ensure that comment continuations can be disabled.
3636 update_test_language_settings(cx, |settings| {
3637 settings.defaults.extend_comment_on_newline = Some(false);
3638 });
3639 let mut cx = EditorTestContext::new(cx).await;
3640 cx.set_state(indoc! {"
3641 /**ˇ
3642 "});
3643 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3644 cx.assert_editor_state(indoc! {"
3645 /**
3646 ˇ
3647 "});
3648}
3649
3650#[gpui::test]
3651async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3652 init_test(cx, |settings| {
3653 settings.defaults.tab_size = NonZeroU32::new(4)
3654 });
3655
3656 let lua_language = Arc::new(Language::new(
3657 LanguageConfig {
3658 line_comments: vec!["--".into()],
3659 block_comment: Some(language::BlockCommentConfig {
3660 start: "--[[".into(),
3661 prefix: "".into(),
3662 end: "]]".into(),
3663 tab_size: 0,
3664 }),
3665 ..LanguageConfig::default()
3666 },
3667 None,
3668 ));
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3672
3673 // Line with line comment should extend
3674 cx.set_state(indoc! {"
3675 --ˇ
3676 "});
3677 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3678 cx.assert_editor_state(indoc! {"
3679 --
3680 --ˇ
3681 "});
3682
3683 // Line with block comment that matches line comment should not extend
3684 cx.set_state(indoc! {"
3685 --[[ˇ
3686 "});
3687 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 --[[
3690 ˇ
3691 "});
3692}
3693
3694#[gpui::test]
3695fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3696 init_test(cx, |_| {});
3697
3698 let editor = cx.add_window(|window, cx| {
3699 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3700 let mut editor = build_editor(buffer, window, cx);
3701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3702 s.select_ranges([3..4, 11..12, 19..20])
3703 });
3704 editor
3705 });
3706
3707 _ = editor.update(cx, |editor, window, cx| {
3708 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3709 editor.buffer.update(cx, |buffer, cx| {
3710 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3711 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3712 });
3713 assert_eq!(
3714 editor.selections.ranges(&editor.display_snapshot(cx)),
3715 &[2..2, 7..7, 12..12],
3716 );
3717
3718 editor.insert("Z", window, cx);
3719 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3720
3721 // The selections are moved after the inserted characters
3722 assert_eq!(
3723 editor.selections.ranges(&editor.display_snapshot(cx)),
3724 &[3..3, 9..9, 15..15],
3725 );
3726 });
3727}
3728
3729#[gpui::test]
3730async fn test_tab(cx: &mut TestAppContext) {
3731 init_test(cx, |settings| {
3732 settings.defaults.tab_size = NonZeroU32::new(3)
3733 });
3734
3735 let mut cx = EditorTestContext::new(cx).await;
3736 cx.set_state(indoc! {"
3737 ˇabˇc
3738 ˇ🏀ˇ🏀ˇefg
3739 dˇ
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 ˇab ˇc
3744 ˇ🏀 ˇ🏀 ˇefg
3745 d ˇ
3746 "});
3747
3748 cx.set_state(indoc! {"
3749 a
3750 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3751 "});
3752 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3753 cx.assert_editor_state(indoc! {"
3754 a
3755 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3756 "});
3757}
3758
3759#[gpui::test]
3760async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3761 init_test(cx, |_| {});
3762
3763 let mut cx = EditorTestContext::new(cx).await;
3764 let language = Arc::new(
3765 Language::new(
3766 LanguageConfig::default(),
3767 Some(tree_sitter_rust::LANGUAGE.into()),
3768 )
3769 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3770 .unwrap(),
3771 );
3772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3773
3774 // test when all cursors are not at suggested indent
3775 // then simply move to their suggested indent location
3776 cx.set_state(indoc! {"
3777 const a: B = (
3778 c(
3779 ˇ
3780 ˇ )
3781 );
3782 "});
3783 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3784 cx.assert_editor_state(indoc! {"
3785 const a: B = (
3786 c(
3787 ˇ
3788 ˇ)
3789 );
3790 "});
3791
3792 // test cursor already at suggested indent not moving when
3793 // other cursors are yet to reach their suggested indents
3794 cx.set_state(indoc! {"
3795 ˇ
3796 const a: B = (
3797 c(
3798 d(
3799 ˇ
3800 )
3801 ˇ
3802 ˇ )
3803 );
3804 "});
3805 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 ˇ
3808 const a: B = (
3809 c(
3810 d(
3811 ˇ
3812 )
3813 ˇ
3814 ˇ)
3815 );
3816 "});
3817 // test when all cursors are at suggested indent then tab is inserted
3818 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 ˇ
3821 const a: B = (
3822 c(
3823 d(
3824 ˇ
3825 )
3826 ˇ
3827 ˇ)
3828 );
3829 "});
3830
3831 // test when current indent is less than suggested indent,
3832 // we adjust line to match suggested indent and move cursor to it
3833 //
3834 // when no other cursor is at word boundary, all of them should move
3835 cx.set_state(indoc! {"
3836 const a: B = (
3837 c(
3838 d(
3839 ˇ
3840 ˇ )
3841 ˇ )
3842 );
3843 "});
3844 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3845 cx.assert_editor_state(indoc! {"
3846 const a: B = (
3847 c(
3848 d(
3849 ˇ
3850 ˇ)
3851 ˇ)
3852 );
3853 "});
3854
3855 // test when current indent is less than suggested indent,
3856 // we adjust line to match suggested indent and move cursor to it
3857 //
3858 // when some other cursor is at word boundary, it should not move
3859 cx.set_state(indoc! {"
3860 const a: B = (
3861 c(
3862 d(
3863 ˇ
3864 ˇ )
3865 ˇ)
3866 );
3867 "});
3868 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3869 cx.assert_editor_state(indoc! {"
3870 const a: B = (
3871 c(
3872 d(
3873 ˇ
3874 ˇ)
3875 ˇ)
3876 );
3877 "});
3878
3879 // test when current indent is more than suggested indent,
3880 // we just move cursor to current indent instead of suggested indent
3881 //
3882 // when no other cursor is at word boundary, all of them should move
3883 cx.set_state(indoc! {"
3884 const a: B = (
3885 c(
3886 d(
3887 ˇ
3888 ˇ )
3889 ˇ )
3890 );
3891 "});
3892 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3893 cx.assert_editor_state(indoc! {"
3894 const a: B = (
3895 c(
3896 d(
3897 ˇ
3898 ˇ)
3899 ˇ)
3900 );
3901 "});
3902 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3903 cx.assert_editor_state(indoc! {"
3904 const a: B = (
3905 c(
3906 d(
3907 ˇ
3908 ˇ)
3909 ˇ)
3910 );
3911 "});
3912
3913 // test when current indent is more than suggested indent,
3914 // we just move cursor to current indent instead of suggested indent
3915 //
3916 // when some other cursor is at word boundary, it doesn't move
3917 cx.set_state(indoc! {"
3918 const a: B = (
3919 c(
3920 d(
3921 ˇ
3922 ˇ )
3923 ˇ)
3924 );
3925 "});
3926 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3927 cx.assert_editor_state(indoc! {"
3928 const a: B = (
3929 c(
3930 d(
3931 ˇ
3932 ˇ)
3933 ˇ)
3934 );
3935 "});
3936
3937 // handle auto-indent when there are multiple cursors on the same line
3938 cx.set_state(indoc! {"
3939 const a: B = (
3940 c(
3941 ˇ ˇ
3942 ˇ )
3943 );
3944 "});
3945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3946 cx.assert_editor_state(indoc! {"
3947 const a: B = (
3948 c(
3949 ˇ
3950 ˇ)
3951 );
3952 "});
3953}
3954
3955#[gpui::test]
3956async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3957 init_test(cx, |settings| {
3958 settings.defaults.tab_size = NonZeroU32::new(3)
3959 });
3960
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state(indoc! {"
3963 ˇ
3964 \t ˇ
3965 \t ˇ
3966 \t ˇ
3967 \t \t\t \t \t\t \t\t \t \t ˇ
3968 "});
3969
3970 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3971 cx.assert_editor_state(indoc! {"
3972 ˇ
3973 \t ˇ
3974 \t ˇ
3975 \t ˇ
3976 \t \t\t \t \t\t \t\t \t \t ˇ
3977 "});
3978}
3979
3980#[gpui::test]
3981async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3982 init_test(cx, |settings| {
3983 settings.defaults.tab_size = NonZeroU32::new(4)
3984 });
3985
3986 let language = Arc::new(
3987 Language::new(
3988 LanguageConfig::default(),
3989 Some(tree_sitter_rust::LANGUAGE.into()),
3990 )
3991 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3992 .unwrap(),
3993 );
3994
3995 let mut cx = EditorTestContext::new(cx).await;
3996 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3997 cx.set_state(indoc! {"
3998 fn a() {
3999 if b {
4000 \t ˇc
4001 }
4002 }
4003 "});
4004
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 fn a() {
4008 if b {
4009 ˇc
4010 }
4011 }
4012 "});
4013}
4014
4015#[gpui::test]
4016async fn test_indent_outdent(cx: &mut TestAppContext) {
4017 init_test(cx, |settings| {
4018 settings.defaults.tab_size = NonZeroU32::new(4);
4019 });
4020
4021 let mut cx = EditorTestContext::new(cx).await;
4022
4023 cx.set_state(indoc! {"
4024 «oneˇ» «twoˇ»
4025 three
4026 four
4027 "});
4028 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4029 cx.assert_editor_state(indoc! {"
4030 «oneˇ» «twoˇ»
4031 three
4032 four
4033 "});
4034
4035 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4036 cx.assert_editor_state(indoc! {"
4037 «oneˇ» «twoˇ»
4038 three
4039 four
4040 "});
4041
4042 // select across line ending
4043 cx.set_state(indoc! {"
4044 one two
4045 t«hree
4046 ˇ» four
4047 "});
4048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 t«hree
4052 ˇ» four
4053 "});
4054
4055 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4056 cx.assert_editor_state(indoc! {"
4057 one two
4058 t«hree
4059 ˇ» four
4060 "});
4061
4062 // Ensure that indenting/outdenting works when the cursor is at column 0.
4063 cx.set_state(indoc! {"
4064 one two
4065 ˇthree
4066 four
4067 "});
4068 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4069 cx.assert_editor_state(indoc! {"
4070 one two
4071 ˇthree
4072 four
4073 "});
4074
4075 cx.set_state(indoc! {"
4076 one two
4077 ˇ three
4078 four
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
4088#[gpui::test]
4089async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4090 // This is a regression test for issue #33761
4091 init_test(cx, |_| {});
4092
4093 let mut cx = EditorTestContext::new(cx).await;
4094 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4095 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4096
4097 cx.set_state(
4098 r#"ˇ# ingress:
4099ˇ# api:
4100ˇ# enabled: false
4101ˇ# pathType: Prefix
4102ˇ# console:
4103ˇ# enabled: false
4104ˇ# pathType: Prefix
4105"#,
4106 );
4107
4108 // Press tab to indent all lines
4109 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4110
4111 cx.assert_editor_state(
4112 r#" ˇ# ingress:
4113 ˇ# api:
4114 ˇ# enabled: false
4115 ˇ# pathType: Prefix
4116 ˇ# console:
4117 ˇ# enabled: false
4118 ˇ# pathType: Prefix
4119"#,
4120 );
4121}
4122
4123#[gpui::test]
4124async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4125 // This is a test to make sure our fix for issue #33761 didn't break anything
4126 init_test(cx, |_| {});
4127
4128 let mut cx = EditorTestContext::new(cx).await;
4129 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4130 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4131
4132 cx.set_state(
4133 r#"ˇingress:
4134ˇ api:
4135ˇ enabled: false
4136ˇ pathType: Prefix
4137"#,
4138 );
4139
4140 // Press tab to indent all lines
4141 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4142
4143 cx.assert_editor_state(
4144 r#"ˇingress:
4145 ˇapi:
4146 ˇenabled: false
4147 ˇpathType: Prefix
4148"#,
4149 );
4150}
4151
4152#[gpui::test]
4153async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4154 init_test(cx, |settings| {
4155 settings.defaults.hard_tabs = Some(true);
4156 });
4157
4158 let mut cx = EditorTestContext::new(cx).await;
4159
4160 // select two ranges on one line
4161 cx.set_state(indoc! {"
4162 «oneˇ» «twoˇ»
4163 three
4164 four
4165 "});
4166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4167 cx.assert_editor_state(indoc! {"
4168 \t«oneˇ» «twoˇ»
4169 three
4170 four
4171 "});
4172 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 \t\t«oneˇ» «twoˇ»
4175 three
4176 four
4177 "});
4178 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4179 cx.assert_editor_state(indoc! {"
4180 \t«oneˇ» «twoˇ»
4181 three
4182 four
4183 "});
4184 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4185 cx.assert_editor_state(indoc! {"
4186 «oneˇ» «twoˇ»
4187 three
4188 four
4189 "});
4190
4191 // select across a line ending
4192 cx.set_state(indoc! {"
4193 one two
4194 t«hree
4195 ˇ»four
4196 "});
4197 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 one two
4200 \tt«hree
4201 ˇ»four
4202 "});
4203 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4204 cx.assert_editor_state(indoc! {"
4205 one two
4206 \t\tt«hree
4207 ˇ»four
4208 "});
4209 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4210 cx.assert_editor_state(indoc! {"
4211 one two
4212 \tt«hree
4213 ˇ»four
4214 "});
4215 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4216 cx.assert_editor_state(indoc! {"
4217 one two
4218 t«hree
4219 ˇ»four
4220 "});
4221
4222 // Ensure that indenting/outdenting works when the cursor is at column 0.
4223 cx.set_state(indoc! {"
4224 one two
4225 ˇthree
4226 four
4227 "});
4228 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4229 cx.assert_editor_state(indoc! {"
4230 one two
4231 ˇthree
4232 four
4233 "});
4234 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4235 cx.assert_editor_state(indoc! {"
4236 one two
4237 \tˇthree
4238 four
4239 "});
4240 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4241 cx.assert_editor_state(indoc! {"
4242 one two
4243 ˇthree
4244 four
4245 "});
4246}
4247
4248#[gpui::test]
4249fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4250 init_test(cx, |settings| {
4251 settings.languages.0.extend([
4252 (
4253 "TOML".into(),
4254 LanguageSettingsContent {
4255 tab_size: NonZeroU32::new(2),
4256 ..Default::default()
4257 },
4258 ),
4259 (
4260 "Rust".into(),
4261 LanguageSettingsContent {
4262 tab_size: NonZeroU32::new(4),
4263 ..Default::default()
4264 },
4265 ),
4266 ]);
4267 });
4268
4269 let toml_language = Arc::new(Language::new(
4270 LanguageConfig {
4271 name: "TOML".into(),
4272 ..Default::default()
4273 },
4274 None,
4275 ));
4276 let rust_language = Arc::new(Language::new(
4277 LanguageConfig {
4278 name: "Rust".into(),
4279 ..Default::default()
4280 },
4281 None,
4282 ));
4283
4284 let toml_buffer =
4285 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4286 let rust_buffer =
4287 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4288 let multibuffer = cx.new(|cx| {
4289 let mut multibuffer = MultiBuffer::new(ReadWrite);
4290 multibuffer.push_excerpts(
4291 toml_buffer.clone(),
4292 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4293 cx,
4294 );
4295 multibuffer.push_excerpts(
4296 rust_buffer.clone(),
4297 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4298 cx,
4299 );
4300 multibuffer
4301 });
4302
4303 cx.add_window(|window, cx| {
4304 let mut editor = build_editor(multibuffer, window, cx);
4305
4306 assert_eq!(
4307 editor.text(cx),
4308 indoc! {"
4309 a = 1
4310 b = 2
4311
4312 const c: usize = 3;
4313 "}
4314 );
4315
4316 select_ranges(
4317 &mut editor,
4318 indoc! {"
4319 «aˇ» = 1
4320 b = 2
4321
4322 «const c:ˇ» usize = 3;
4323 "},
4324 window,
4325 cx,
4326 );
4327
4328 editor.tab(&Tab, window, cx);
4329 assert_text_with_selections(
4330 &mut editor,
4331 indoc! {"
4332 «aˇ» = 1
4333 b = 2
4334
4335 «const c:ˇ» usize = 3;
4336 "},
4337 cx,
4338 );
4339 editor.backtab(&Backtab, window, cx);
4340 assert_text_with_selections(
4341 &mut editor,
4342 indoc! {"
4343 «aˇ» = 1
4344 b = 2
4345
4346 «const c:ˇ» usize = 3;
4347 "},
4348 cx,
4349 );
4350
4351 editor
4352 });
4353}
4354
4355#[gpui::test]
4356async fn test_backspace(cx: &mut TestAppContext) {
4357 init_test(cx, |_| {});
4358
4359 let mut cx = EditorTestContext::new(cx).await;
4360
4361 // Basic backspace
4362 cx.set_state(indoc! {"
4363 onˇe two three
4364 fou«rˇ» five six
4365 seven «ˇeight nine
4366 »ten
4367 "});
4368 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4369 cx.assert_editor_state(indoc! {"
4370 oˇe two three
4371 fouˇ five six
4372 seven ˇten
4373 "});
4374
4375 // Test backspace inside and around indents
4376 cx.set_state(indoc! {"
4377 zero
4378 ˇone
4379 ˇtwo
4380 ˇ ˇ ˇ three
4381 ˇ ˇ four
4382 "});
4383 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4384 cx.assert_editor_state(indoc! {"
4385 zero
4386 ˇone
4387 ˇtwo
4388 ˇ threeˇ four
4389 "});
4390}
4391
4392#[gpui::test]
4393async fn test_delete(cx: &mut TestAppContext) {
4394 init_test(cx, |_| {});
4395
4396 let mut cx = EditorTestContext::new(cx).await;
4397 cx.set_state(indoc! {"
4398 onˇe two three
4399 fou«rˇ» five six
4400 seven «ˇeight nine
4401 »ten
4402 "});
4403 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4404 cx.assert_editor_state(indoc! {"
4405 onˇ two three
4406 fouˇ five six
4407 seven ˇten
4408 "});
4409}
4410
4411#[gpui::test]
4412fn test_delete_line(cx: &mut TestAppContext) {
4413 init_test(cx, |_| {});
4414
4415 let editor = cx.add_window(|window, cx| {
4416 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4417 build_editor(buffer, window, cx)
4418 });
4419 _ = editor.update(cx, |editor, window, cx| {
4420 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4421 s.select_display_ranges([
4422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4423 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4424 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4425 ])
4426 });
4427 editor.delete_line(&DeleteLine, window, cx);
4428 assert_eq!(editor.display_text(cx), "ghi");
4429 assert_eq!(
4430 display_ranges(editor, cx),
4431 vec![
4432 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4433 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4434 ]
4435 );
4436 });
4437
4438 let editor = cx.add_window(|window, cx| {
4439 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4440 build_editor(buffer, window, cx)
4441 });
4442 _ = editor.update(cx, |editor, window, cx| {
4443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4444 s.select_display_ranges([
4445 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4446 ])
4447 });
4448 editor.delete_line(&DeleteLine, window, cx);
4449 assert_eq!(editor.display_text(cx), "ghi\n");
4450 assert_eq!(
4451 display_ranges(editor, cx),
4452 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4453 );
4454 });
4455
4456 let editor = cx.add_window(|window, cx| {
4457 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4458 build_editor(buffer, window, cx)
4459 });
4460 _ = editor.update(cx, |editor, window, cx| {
4461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4462 s.select_display_ranges([
4463 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4464 ])
4465 });
4466 editor.delete_line(&DeleteLine, window, cx);
4467 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4468 assert_eq!(
4469 display_ranges(editor, cx),
4470 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4471 );
4472 });
4473}
4474
4475#[gpui::test]
4476fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4477 init_test(cx, |_| {});
4478
4479 cx.add_window(|window, cx| {
4480 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4481 let mut editor = build_editor(buffer.clone(), window, cx);
4482 let buffer = buffer.read(cx).as_singleton().unwrap();
4483
4484 assert_eq!(
4485 editor
4486 .selections
4487 .ranges::<Point>(&editor.display_snapshot(cx)),
4488 &[Point::new(0, 0)..Point::new(0, 0)]
4489 );
4490
4491 // When on single line, replace newline at end by space
4492 editor.join_lines(&JoinLines, window, cx);
4493 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4494 assert_eq!(
4495 editor
4496 .selections
4497 .ranges::<Point>(&editor.display_snapshot(cx)),
4498 &[Point::new(0, 3)..Point::new(0, 3)]
4499 );
4500
4501 // When multiple lines are selected, remove newlines that are spanned by the selection
4502 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4503 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4504 });
4505 editor.join_lines(&JoinLines, window, cx);
4506 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4507 assert_eq!(
4508 editor
4509 .selections
4510 .ranges::<Point>(&editor.display_snapshot(cx)),
4511 &[Point::new(0, 11)..Point::new(0, 11)]
4512 );
4513
4514 // Undo should be transactional
4515 editor.undo(&Undo, window, cx);
4516 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4517 assert_eq!(
4518 editor
4519 .selections
4520 .ranges::<Point>(&editor.display_snapshot(cx)),
4521 &[Point::new(0, 5)..Point::new(2, 2)]
4522 );
4523
4524 // When joining an empty line don't insert a space
4525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4526 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4527 });
4528 editor.join_lines(&JoinLines, window, cx);
4529 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4530 assert_eq!(
4531 editor
4532 .selections
4533 .ranges::<Point>(&editor.display_snapshot(cx)),
4534 [Point::new(2, 3)..Point::new(2, 3)]
4535 );
4536
4537 // We can remove trailing newlines
4538 editor.join_lines(&JoinLines, window, cx);
4539 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4540 assert_eq!(
4541 editor
4542 .selections
4543 .ranges::<Point>(&editor.display_snapshot(cx)),
4544 [Point::new(2, 3)..Point::new(2, 3)]
4545 );
4546
4547 // We don't blow up on the last line
4548 editor.join_lines(&JoinLines, window, cx);
4549 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4550 assert_eq!(
4551 editor
4552 .selections
4553 .ranges::<Point>(&editor.display_snapshot(cx)),
4554 [Point::new(2, 3)..Point::new(2, 3)]
4555 );
4556
4557 // reset to test indentation
4558 editor.buffer.update(cx, |buffer, cx| {
4559 buffer.edit(
4560 [
4561 (Point::new(1, 0)..Point::new(1, 2), " "),
4562 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4563 ],
4564 None,
4565 cx,
4566 )
4567 });
4568
4569 // We remove any leading spaces
4570 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4571 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4572 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4573 });
4574 editor.join_lines(&JoinLines, window, cx);
4575 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4576
4577 // We don't insert a space for a line containing only spaces
4578 editor.join_lines(&JoinLines, window, cx);
4579 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4580
4581 // We ignore any leading tabs
4582 editor.join_lines(&JoinLines, window, cx);
4583 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4584
4585 editor
4586 });
4587}
4588
4589#[gpui::test]
4590fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4591 init_test(cx, |_| {});
4592
4593 cx.add_window(|window, cx| {
4594 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4595 let mut editor = build_editor(buffer.clone(), window, cx);
4596 let buffer = buffer.read(cx).as_singleton().unwrap();
4597
4598 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4599 s.select_ranges([
4600 Point::new(0, 2)..Point::new(1, 1),
4601 Point::new(1, 2)..Point::new(1, 2),
4602 Point::new(3, 1)..Point::new(3, 2),
4603 ])
4604 });
4605
4606 editor.join_lines(&JoinLines, window, cx);
4607 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4608
4609 assert_eq!(
4610 editor
4611 .selections
4612 .ranges::<Point>(&editor.display_snapshot(cx)),
4613 [
4614 Point::new(0, 7)..Point::new(0, 7),
4615 Point::new(1, 3)..Point::new(1, 3)
4616 ]
4617 );
4618 editor
4619 });
4620}
4621
4622#[gpui::test]
4623async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4624 init_test(cx, |_| {});
4625
4626 let mut cx = EditorTestContext::new(cx).await;
4627
4628 let diff_base = r#"
4629 Line 0
4630 Line 1
4631 Line 2
4632 Line 3
4633 "#
4634 .unindent();
4635
4636 cx.set_state(
4637 &r#"
4638 ˇLine 0
4639 Line 1
4640 Line 2
4641 Line 3
4642 "#
4643 .unindent(),
4644 );
4645
4646 cx.set_head_text(&diff_base);
4647 executor.run_until_parked();
4648
4649 // Join lines
4650 cx.update_editor(|editor, window, cx| {
4651 editor.join_lines(&JoinLines, window, cx);
4652 });
4653 executor.run_until_parked();
4654
4655 cx.assert_editor_state(
4656 &r#"
4657 Line 0ˇ Line 1
4658 Line 2
4659 Line 3
4660 "#
4661 .unindent(),
4662 );
4663 // Join again
4664 cx.update_editor(|editor, window, cx| {
4665 editor.join_lines(&JoinLines, window, cx);
4666 });
4667 executor.run_until_parked();
4668
4669 cx.assert_editor_state(
4670 &r#"
4671 Line 0 Line 1ˇ Line 2
4672 Line 3
4673 "#
4674 .unindent(),
4675 );
4676}
4677
4678#[gpui::test]
4679async fn test_custom_newlines_cause_no_false_positive_diffs(
4680 executor: BackgroundExecutor,
4681 cx: &mut TestAppContext,
4682) {
4683 init_test(cx, |_| {});
4684 let mut cx = EditorTestContext::new(cx).await;
4685 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4686 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4687 executor.run_until_parked();
4688
4689 cx.update_editor(|editor, window, cx| {
4690 let snapshot = editor.snapshot(window, cx);
4691 assert_eq!(
4692 snapshot
4693 .buffer_snapshot()
4694 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4695 .collect::<Vec<_>>(),
4696 Vec::new(),
4697 "Should not have any diffs for files with custom newlines"
4698 );
4699 });
4700}
4701
4702#[gpui::test]
4703async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4704 init_test(cx, |_| {});
4705
4706 let mut cx = EditorTestContext::new(cx).await;
4707
4708 // Test sort_lines_case_insensitive()
4709 cx.set_state(indoc! {"
4710 «z
4711 y
4712 x
4713 Z
4714 Y
4715 Xˇ»
4716 "});
4717 cx.update_editor(|e, window, cx| {
4718 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4719 });
4720 cx.assert_editor_state(indoc! {"
4721 «x
4722 X
4723 y
4724 Y
4725 z
4726 Zˇ»
4727 "});
4728
4729 // Test sort_lines_by_length()
4730 //
4731 // Demonstrates:
4732 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4733 // - sort is stable
4734 cx.set_state(indoc! {"
4735 «123
4736 æ
4737 12
4738 ∞
4739 1
4740 æˇ»
4741 "});
4742 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4743 cx.assert_editor_state(indoc! {"
4744 «æ
4745 ∞
4746 1
4747 æ
4748 12
4749 123ˇ»
4750 "});
4751
4752 // Test reverse_lines()
4753 cx.set_state(indoc! {"
4754 «5
4755 4
4756 3
4757 2
4758 1ˇ»
4759 "});
4760 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4761 cx.assert_editor_state(indoc! {"
4762 «1
4763 2
4764 3
4765 4
4766 5ˇ»
4767 "});
4768
4769 // Skip testing shuffle_line()
4770
4771 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4772 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4773
4774 // Don't manipulate when cursor is on single line, but expand the selection
4775 cx.set_state(indoc! {"
4776 ddˇdd
4777 ccc
4778 bb
4779 a
4780 "});
4781 cx.update_editor(|e, window, cx| {
4782 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4783 });
4784 cx.assert_editor_state(indoc! {"
4785 «ddddˇ»
4786 ccc
4787 bb
4788 a
4789 "});
4790
4791 // Basic manipulate case
4792 // Start selection moves to column 0
4793 // End of selection shrinks to fit shorter line
4794 cx.set_state(indoc! {"
4795 dd«d
4796 ccc
4797 bb
4798 aaaaaˇ»
4799 "});
4800 cx.update_editor(|e, window, cx| {
4801 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4802 });
4803 cx.assert_editor_state(indoc! {"
4804 «aaaaa
4805 bb
4806 ccc
4807 dddˇ»
4808 "});
4809
4810 // Manipulate case with newlines
4811 cx.set_state(indoc! {"
4812 dd«d
4813 ccc
4814
4815 bb
4816 aaaaa
4817
4818 ˇ»
4819 "});
4820 cx.update_editor(|e, window, cx| {
4821 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4822 });
4823 cx.assert_editor_state(indoc! {"
4824 «
4825
4826 aaaaa
4827 bb
4828 ccc
4829 dddˇ»
4830
4831 "});
4832
4833 // Adding new line
4834 cx.set_state(indoc! {"
4835 aa«a
4836 bbˇ»b
4837 "});
4838 cx.update_editor(|e, window, cx| {
4839 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4840 });
4841 cx.assert_editor_state(indoc! {"
4842 «aaa
4843 bbb
4844 added_lineˇ»
4845 "});
4846
4847 // Removing line
4848 cx.set_state(indoc! {"
4849 aa«a
4850 bbbˇ»
4851 "});
4852 cx.update_editor(|e, window, cx| {
4853 e.manipulate_immutable_lines(window, cx, |lines| {
4854 lines.pop();
4855 })
4856 });
4857 cx.assert_editor_state(indoc! {"
4858 «aaaˇ»
4859 "});
4860
4861 // Removing all lines
4862 cx.set_state(indoc! {"
4863 aa«a
4864 bbbˇ»
4865 "});
4866 cx.update_editor(|e, window, cx| {
4867 e.manipulate_immutable_lines(window, cx, |lines| {
4868 lines.drain(..);
4869 })
4870 });
4871 cx.assert_editor_state(indoc! {"
4872 ˇ
4873 "});
4874}
4875
4876#[gpui::test]
4877async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879
4880 let mut cx = EditorTestContext::new(cx).await;
4881
4882 // Consider continuous selection as single selection
4883 cx.set_state(indoc! {"
4884 Aaa«aa
4885 cˇ»c«c
4886 bb
4887 aaaˇ»aa
4888 "});
4889 cx.update_editor(|e, window, cx| {
4890 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4891 });
4892 cx.assert_editor_state(indoc! {"
4893 «Aaaaa
4894 ccc
4895 bb
4896 aaaaaˇ»
4897 "});
4898
4899 cx.set_state(indoc! {"
4900 Aaa«aa
4901 cˇ»c«c
4902 bb
4903 aaaˇ»aa
4904 "});
4905 cx.update_editor(|e, window, cx| {
4906 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4907 });
4908 cx.assert_editor_state(indoc! {"
4909 «Aaaaa
4910 ccc
4911 bbˇ»
4912 "});
4913
4914 // Consider non continuous selection as distinct dedup operations
4915 cx.set_state(indoc! {"
4916 «aaaaa
4917 bb
4918 aaaaa
4919 aaaaaˇ»
4920
4921 aaa«aaˇ»
4922 "});
4923 cx.update_editor(|e, window, cx| {
4924 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4925 });
4926 cx.assert_editor_state(indoc! {"
4927 «aaaaa
4928 bbˇ»
4929
4930 «aaaaaˇ»
4931 "});
4932}
4933
4934#[gpui::test]
4935async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4936 init_test(cx, |_| {});
4937
4938 let mut cx = EditorTestContext::new(cx).await;
4939
4940 cx.set_state(indoc! {"
4941 «Aaa
4942 aAa
4943 Aaaˇ»
4944 "});
4945 cx.update_editor(|e, window, cx| {
4946 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4947 });
4948 cx.assert_editor_state(indoc! {"
4949 «Aaa
4950 aAaˇ»
4951 "});
4952
4953 cx.set_state(indoc! {"
4954 «Aaa
4955 aAa
4956 aaAˇ»
4957 "});
4958 cx.update_editor(|e, window, cx| {
4959 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4960 });
4961 cx.assert_editor_state(indoc! {"
4962 «Aaaˇ»
4963 "});
4964}
4965
4966#[gpui::test]
4967async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4968 init_test(cx, |_| {});
4969
4970 let mut cx = EditorTestContext::new(cx).await;
4971
4972 let js_language = Arc::new(Language::new(
4973 LanguageConfig {
4974 name: "JavaScript".into(),
4975 wrap_characters: Some(language::WrapCharactersConfig {
4976 start_prefix: "<".into(),
4977 start_suffix: ">".into(),
4978 end_prefix: "</".into(),
4979 end_suffix: ">".into(),
4980 }),
4981 ..LanguageConfig::default()
4982 },
4983 None,
4984 ));
4985
4986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4987
4988 cx.set_state(indoc! {"
4989 «testˇ»
4990 "});
4991 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4992 cx.assert_editor_state(indoc! {"
4993 <«ˇ»>test</«ˇ»>
4994 "});
4995
4996 cx.set_state(indoc! {"
4997 «test
4998 testˇ»
4999 "});
5000 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5001 cx.assert_editor_state(indoc! {"
5002 <«ˇ»>test
5003 test</«ˇ»>
5004 "});
5005
5006 cx.set_state(indoc! {"
5007 teˇst
5008 "});
5009 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5010 cx.assert_editor_state(indoc! {"
5011 te<«ˇ»></«ˇ»>st
5012 "});
5013}
5014
5015#[gpui::test]
5016async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5017 init_test(cx, |_| {});
5018
5019 let mut cx = EditorTestContext::new(cx).await;
5020
5021 let js_language = Arc::new(Language::new(
5022 LanguageConfig {
5023 name: "JavaScript".into(),
5024 wrap_characters: Some(language::WrapCharactersConfig {
5025 start_prefix: "<".into(),
5026 start_suffix: ">".into(),
5027 end_prefix: "</".into(),
5028 end_suffix: ">".into(),
5029 }),
5030 ..LanguageConfig::default()
5031 },
5032 None,
5033 ));
5034
5035 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5036
5037 cx.set_state(indoc! {"
5038 «testˇ»
5039 «testˇ» «testˇ»
5040 «testˇ»
5041 "});
5042 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5043 cx.assert_editor_state(indoc! {"
5044 <«ˇ»>test</«ˇ»>
5045 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5046 <«ˇ»>test</«ˇ»>
5047 "});
5048
5049 cx.set_state(indoc! {"
5050 «test
5051 testˇ»
5052 «test
5053 testˇ»
5054 "});
5055 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5056 cx.assert_editor_state(indoc! {"
5057 <«ˇ»>test
5058 test</«ˇ»>
5059 <«ˇ»>test
5060 test</«ˇ»>
5061 "});
5062}
5063
5064#[gpui::test]
5065async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5066 init_test(cx, |_| {});
5067
5068 let mut cx = EditorTestContext::new(cx).await;
5069
5070 let plaintext_language = Arc::new(Language::new(
5071 LanguageConfig {
5072 name: "Plain Text".into(),
5073 ..LanguageConfig::default()
5074 },
5075 None,
5076 ));
5077
5078 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5079
5080 cx.set_state(indoc! {"
5081 «testˇ»
5082 "});
5083 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5084 cx.assert_editor_state(indoc! {"
5085 «testˇ»
5086 "});
5087}
5088
5089#[gpui::test]
5090async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5091 init_test(cx, |_| {});
5092
5093 let mut cx = EditorTestContext::new(cx).await;
5094
5095 // Manipulate with multiple selections on a single line
5096 cx.set_state(indoc! {"
5097 dd«dd
5098 cˇ»c«c
5099 bb
5100 aaaˇ»aa
5101 "});
5102 cx.update_editor(|e, window, cx| {
5103 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5104 });
5105 cx.assert_editor_state(indoc! {"
5106 «aaaaa
5107 bb
5108 ccc
5109 ddddˇ»
5110 "});
5111
5112 // Manipulate with multiple disjoin selections
5113 cx.set_state(indoc! {"
5114 5«
5115 4
5116 3
5117 2
5118 1ˇ»
5119
5120 dd«dd
5121 ccc
5122 bb
5123 aaaˇ»aa
5124 "});
5125 cx.update_editor(|e, window, cx| {
5126 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5127 });
5128 cx.assert_editor_state(indoc! {"
5129 «1
5130 2
5131 3
5132 4
5133 5ˇ»
5134
5135 «aaaaa
5136 bb
5137 ccc
5138 ddddˇ»
5139 "});
5140
5141 // Adding lines on each selection
5142 cx.set_state(indoc! {"
5143 2«
5144 1ˇ»
5145
5146 bb«bb
5147 aaaˇ»aa
5148 "});
5149 cx.update_editor(|e, window, cx| {
5150 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5151 });
5152 cx.assert_editor_state(indoc! {"
5153 «2
5154 1
5155 added lineˇ»
5156
5157 «bbbb
5158 aaaaa
5159 added lineˇ»
5160 "});
5161
5162 // Removing lines on each selection
5163 cx.set_state(indoc! {"
5164 2«
5165 1ˇ»
5166
5167 bb«bb
5168 aaaˇ»aa
5169 "});
5170 cx.update_editor(|e, window, cx| {
5171 e.manipulate_immutable_lines(window, cx, |lines| {
5172 lines.pop();
5173 })
5174 });
5175 cx.assert_editor_state(indoc! {"
5176 «2ˇ»
5177
5178 «bbbbˇ»
5179 "});
5180}
5181
5182#[gpui::test]
5183async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5184 init_test(cx, |settings| {
5185 settings.defaults.tab_size = NonZeroU32::new(3)
5186 });
5187
5188 let mut cx = EditorTestContext::new(cx).await;
5189
5190 // MULTI SELECTION
5191 // Ln.1 "«" tests empty lines
5192 // Ln.9 tests just leading whitespace
5193 cx.set_state(indoc! {"
5194 «
5195 abc // No indentationˇ»
5196 «\tabc // 1 tabˇ»
5197 \t\tabc « ˇ» // 2 tabs
5198 \t ab«c // Tab followed by space
5199 \tabc // Space followed by tab (3 spaces should be the result)
5200 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5201 abˇ»ˇc ˇ ˇ // Already space indented«
5202 \t
5203 \tabc\tdef // Only the leading tab is manipulatedˇ»
5204 "});
5205 cx.update_editor(|e, window, cx| {
5206 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5207 });
5208 cx.assert_editor_state(
5209 indoc! {"
5210 «
5211 abc // No indentation
5212 abc // 1 tab
5213 abc // 2 tabs
5214 abc // Tab followed by space
5215 abc // Space followed by tab (3 spaces should be the result)
5216 abc // Mixed indentation (tab conversion depends on the column)
5217 abc // Already space indented
5218 ·
5219 abc\tdef // Only the leading tab is manipulatedˇ»
5220 "}
5221 .replace("·", "")
5222 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5223 );
5224
5225 // Test on just a few lines, the others should remain unchanged
5226 // Only lines (3, 5, 10, 11) should change
5227 cx.set_state(
5228 indoc! {"
5229 ·
5230 abc // No indentation
5231 \tabcˇ // 1 tab
5232 \t\tabc // 2 tabs
5233 \t abcˇ // Tab followed by space
5234 \tabc // Space followed by tab (3 spaces should be the result)
5235 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5236 abc // Already space indented
5237 «\t
5238 \tabc\tdef // Only the leading tab is manipulatedˇ»
5239 "}
5240 .replace("·", "")
5241 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5242 );
5243 cx.update_editor(|e, window, cx| {
5244 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5245 });
5246 cx.assert_editor_state(
5247 indoc! {"
5248 ·
5249 abc // No indentation
5250 « abc // 1 tabˇ»
5251 \t\tabc // 2 tabs
5252 « abc // Tab followed by spaceˇ»
5253 \tabc // Space followed by tab (3 spaces should be the result)
5254 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5255 abc // Already space indented
5256 « ·
5257 abc\tdef // Only the leading tab is manipulatedˇ»
5258 "}
5259 .replace("·", "")
5260 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5261 );
5262
5263 // SINGLE SELECTION
5264 // Ln.1 "«" tests empty lines
5265 // Ln.9 tests just leading whitespace
5266 cx.set_state(indoc! {"
5267 «
5268 abc // No indentation
5269 \tabc // 1 tab
5270 \t\tabc // 2 tabs
5271 \t abc // Tab followed by space
5272 \tabc // Space followed by tab (3 spaces should be the result)
5273 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5274 abc // Already space indented
5275 \t
5276 \tabc\tdef // Only the leading tab is manipulatedˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| {
5279 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5280 });
5281 cx.assert_editor_state(
5282 indoc! {"
5283 «
5284 abc // No indentation
5285 abc // 1 tab
5286 abc // 2 tabs
5287 abc // Tab followed by space
5288 abc // Space followed by tab (3 spaces should be the result)
5289 abc // Mixed indentation (tab conversion depends on the column)
5290 abc // Already space indented
5291 ·
5292 abc\tdef // Only the leading tab is manipulatedˇ»
5293 "}
5294 .replace("·", "")
5295 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5296 );
5297}
5298
5299#[gpui::test]
5300async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5301 init_test(cx, |settings| {
5302 settings.defaults.tab_size = NonZeroU32::new(3)
5303 });
5304
5305 let mut cx = EditorTestContext::new(cx).await;
5306
5307 // MULTI SELECTION
5308 // Ln.1 "«" tests empty lines
5309 // Ln.11 tests just leading whitespace
5310 cx.set_state(indoc! {"
5311 «
5312 abˇ»ˇc // No indentation
5313 abc ˇ ˇ // 1 space (< 3 so dont convert)
5314 abc « // 2 spaces (< 3 so dont convert)
5315 abc // 3 spaces (convert)
5316 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5317 «\tˇ»\t«\tˇ»abc // Already tab indented
5318 «\t abc // Tab followed by space
5319 \tabc // Space followed by tab (should be consumed due to tab)
5320 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5321 \tˇ» «\t
5322 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5323 "});
5324 cx.update_editor(|e, window, cx| {
5325 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5326 });
5327 cx.assert_editor_state(indoc! {"
5328 «
5329 abc // No indentation
5330 abc // 1 space (< 3 so dont convert)
5331 abc // 2 spaces (< 3 so dont convert)
5332 \tabc // 3 spaces (convert)
5333 \t abc // 5 spaces (1 tab + 2 spaces)
5334 \t\t\tabc // Already tab indented
5335 \t abc // Tab followed by space
5336 \tabc // Space followed by tab (should be consumed due to tab)
5337 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5338 \t\t\t
5339 \tabc \t // Only the leading spaces should be convertedˇ»
5340 "});
5341
5342 // Test on just a few lines, the other should remain unchanged
5343 // Only lines (4, 8, 11, 12) should change
5344 cx.set_state(
5345 indoc! {"
5346 ·
5347 abc // No indentation
5348 abc // 1 space (< 3 so dont convert)
5349 abc // 2 spaces (< 3 so dont convert)
5350 « abc // 3 spaces (convert)ˇ»
5351 abc // 5 spaces (1 tab + 2 spaces)
5352 \t\t\tabc // Already tab indented
5353 \t abc // Tab followed by space
5354 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5355 \t\t \tabc // Mixed indentation
5356 \t \t \t \tabc // Mixed indentation
5357 \t \tˇ
5358 « abc \t // Only the leading spaces should be convertedˇ»
5359 "}
5360 .replace("·", "")
5361 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5362 );
5363 cx.update_editor(|e, window, cx| {
5364 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5365 });
5366 cx.assert_editor_state(
5367 indoc! {"
5368 ·
5369 abc // No indentation
5370 abc // 1 space (< 3 so dont convert)
5371 abc // 2 spaces (< 3 so dont convert)
5372 «\tabc // 3 spaces (convert)ˇ»
5373 abc // 5 spaces (1 tab + 2 spaces)
5374 \t\t\tabc // Already tab indented
5375 \t abc // Tab followed by space
5376 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5377 \t\t \tabc // Mixed indentation
5378 \t \t \t \tabc // Mixed indentation
5379 «\t\t\t
5380 \tabc \t // Only the leading spaces should be convertedˇ»
5381 "}
5382 .replace("·", "")
5383 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5384 );
5385
5386 // SINGLE SELECTION
5387 // Ln.1 "«" tests empty lines
5388 // Ln.11 tests just leading whitespace
5389 cx.set_state(indoc! {"
5390 «
5391 abc // No indentation
5392 abc // 1 space (< 3 so dont convert)
5393 abc // 2 spaces (< 3 so dont convert)
5394 abc // 3 spaces (convert)
5395 abc // 5 spaces (1 tab + 2 spaces)
5396 \t\t\tabc // Already tab indented
5397 \t abc // Tab followed by space
5398 \tabc // Space followed by tab (should be consumed due to tab)
5399 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5400 \t \t
5401 abc \t // Only the leading spaces should be convertedˇ»
5402 "});
5403 cx.update_editor(|e, window, cx| {
5404 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5405 });
5406 cx.assert_editor_state(indoc! {"
5407 «
5408 abc // No indentation
5409 abc // 1 space (< 3 so dont convert)
5410 abc // 2 spaces (< 3 so dont convert)
5411 \tabc // 3 spaces (convert)
5412 \t abc // 5 spaces (1 tab + 2 spaces)
5413 \t\t\tabc // Already tab indented
5414 \t abc // Tab followed by space
5415 \tabc // Space followed by tab (should be consumed due to tab)
5416 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5417 \t\t\t
5418 \tabc \t // Only the leading spaces should be convertedˇ»
5419 "});
5420}
5421
5422#[gpui::test]
5423async fn test_toggle_case(cx: &mut TestAppContext) {
5424 init_test(cx, |_| {});
5425
5426 let mut cx = EditorTestContext::new(cx).await;
5427
5428 // If all lower case -> upper case
5429 cx.set_state(indoc! {"
5430 «hello worldˇ»
5431 "});
5432 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5433 cx.assert_editor_state(indoc! {"
5434 «HELLO WORLDˇ»
5435 "});
5436
5437 // If all upper case -> lower case
5438 cx.set_state(indoc! {"
5439 «HELLO WORLDˇ»
5440 "});
5441 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5442 cx.assert_editor_state(indoc! {"
5443 «hello worldˇ»
5444 "});
5445
5446 // If any upper case characters are identified -> lower case
5447 // This matches JetBrains IDEs
5448 cx.set_state(indoc! {"
5449 «hEllo worldˇ»
5450 "});
5451 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5452 cx.assert_editor_state(indoc! {"
5453 «hello worldˇ»
5454 "});
5455}
5456
5457#[gpui::test]
5458async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5459 init_test(cx, |_| {});
5460
5461 let mut cx = EditorTestContext::new(cx).await;
5462
5463 cx.set_state(indoc! {"
5464 «implement-windows-supportˇ»
5465 "});
5466 cx.update_editor(|e, window, cx| {
5467 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5468 });
5469 cx.assert_editor_state(indoc! {"
5470 «Implement windows supportˇ»
5471 "});
5472}
5473
5474#[gpui::test]
5475async fn test_manipulate_text(cx: &mut TestAppContext) {
5476 init_test(cx, |_| {});
5477
5478 let mut cx = EditorTestContext::new(cx).await;
5479
5480 // Test convert_to_upper_case()
5481 cx.set_state(indoc! {"
5482 «hello worldˇ»
5483 "});
5484 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5485 cx.assert_editor_state(indoc! {"
5486 «HELLO WORLDˇ»
5487 "});
5488
5489 // Test convert_to_lower_case()
5490 cx.set_state(indoc! {"
5491 «HELLO WORLDˇ»
5492 "});
5493 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5494 cx.assert_editor_state(indoc! {"
5495 «hello worldˇ»
5496 "});
5497
5498 // Test multiple line, single selection case
5499 cx.set_state(indoc! {"
5500 «The quick brown
5501 fox jumps over
5502 the lazy dogˇ»
5503 "});
5504 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5505 cx.assert_editor_state(indoc! {"
5506 «The Quick Brown
5507 Fox Jumps Over
5508 The Lazy Dogˇ»
5509 "});
5510
5511 // Test multiple line, single selection case
5512 cx.set_state(indoc! {"
5513 «The quick brown
5514 fox jumps over
5515 the lazy dogˇ»
5516 "});
5517 cx.update_editor(|e, window, cx| {
5518 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5519 });
5520 cx.assert_editor_state(indoc! {"
5521 «TheQuickBrown
5522 FoxJumpsOver
5523 TheLazyDogˇ»
5524 "});
5525
5526 // From here on out, test more complex cases of manipulate_text()
5527
5528 // Test no selection case - should affect words cursors are in
5529 // Cursor at beginning, middle, and end of word
5530 cx.set_state(indoc! {"
5531 ˇhello big beauˇtiful worldˇ
5532 "});
5533 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5536 "});
5537
5538 // Test multiple selections on a single line and across multiple lines
5539 cx.set_state(indoc! {"
5540 «Theˇ» quick «brown
5541 foxˇ» jumps «overˇ»
5542 the «lazyˇ» dog
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «THEˇ» quick «BROWN
5547 FOXˇ» jumps «OVERˇ»
5548 the «LAZYˇ» dog
5549 "});
5550
5551 // Test case where text length grows
5552 cx.set_state(indoc! {"
5553 «tschüߡ»
5554 "});
5555 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5556 cx.assert_editor_state(indoc! {"
5557 «TSCHÜSSˇ»
5558 "});
5559
5560 // Test to make sure we don't crash when text shrinks
5561 cx.set_state(indoc! {"
5562 aaa_bbbˇ
5563 "});
5564 cx.update_editor(|e, window, cx| {
5565 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5566 });
5567 cx.assert_editor_state(indoc! {"
5568 «aaaBbbˇ»
5569 "});
5570
5571 // Test to make sure we all aware of the fact that each word can grow and shrink
5572 // Final selections should be aware of this fact
5573 cx.set_state(indoc! {"
5574 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5575 "});
5576 cx.update_editor(|e, window, cx| {
5577 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5578 });
5579 cx.assert_editor_state(indoc! {"
5580 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5581 "});
5582
5583 cx.set_state(indoc! {"
5584 «hElLo, WoRld!ˇ»
5585 "});
5586 cx.update_editor(|e, window, cx| {
5587 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5588 });
5589 cx.assert_editor_state(indoc! {"
5590 «HeLlO, wOrLD!ˇ»
5591 "});
5592
5593 // Test selections with `line_mode() = true`.
5594 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5595 cx.set_state(indoc! {"
5596 «The quick brown
5597 fox jumps over
5598 tˇ»he lazy dog
5599 "});
5600 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5601 cx.assert_editor_state(indoc! {"
5602 «THE QUICK BROWN
5603 FOX JUMPS OVER
5604 THE LAZY DOGˇ»
5605 "});
5606}
5607
5608#[gpui::test]
5609fn test_duplicate_line(cx: &mut TestAppContext) {
5610 init_test(cx, |_| {});
5611
5612 let editor = cx.add_window(|window, cx| {
5613 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5614 build_editor(buffer, window, cx)
5615 });
5616 _ = editor.update(cx, |editor, window, cx| {
5617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5618 s.select_display_ranges([
5619 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5620 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5621 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5622 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5623 ])
5624 });
5625 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5626 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5627 assert_eq!(
5628 display_ranges(editor, cx),
5629 vec![
5630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5631 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5632 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5633 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5634 ]
5635 );
5636 });
5637
5638 let editor = cx.add_window(|window, cx| {
5639 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5640 build_editor(buffer, window, cx)
5641 });
5642 _ = editor.update(cx, |editor, window, cx| {
5643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5644 s.select_display_ranges([
5645 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5646 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5647 ])
5648 });
5649 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5650 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5651 assert_eq!(
5652 display_ranges(editor, cx),
5653 vec![
5654 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5655 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5656 ]
5657 );
5658 });
5659
5660 // With `duplicate_line_up` the selections move to the duplicated lines,
5661 // which are inserted above the original lines
5662 let editor = cx.add_window(|window, cx| {
5663 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5664 build_editor(buffer, window, cx)
5665 });
5666 _ = editor.update(cx, |editor, window, cx| {
5667 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5668 s.select_display_ranges([
5669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5671 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5672 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5673 ])
5674 });
5675 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5676 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5677 assert_eq!(
5678 display_ranges(editor, cx),
5679 vec![
5680 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5682 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5683 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5684 ]
5685 );
5686 });
5687
5688 let editor = cx.add_window(|window, cx| {
5689 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5690 build_editor(buffer, window, cx)
5691 });
5692 _ = editor.update(cx, |editor, window, cx| {
5693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5694 s.select_display_ranges([
5695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5696 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5697 ])
5698 });
5699 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5700 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5701 assert_eq!(
5702 display_ranges(editor, cx),
5703 vec![
5704 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5705 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5706 ]
5707 );
5708 });
5709
5710 let editor = cx.add_window(|window, cx| {
5711 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5712 build_editor(buffer, window, cx)
5713 });
5714 _ = editor.update(cx, |editor, window, cx| {
5715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5716 s.select_display_ranges([
5717 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5718 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5719 ])
5720 });
5721 editor.duplicate_selection(&DuplicateSelection, window, cx);
5722 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5723 assert_eq!(
5724 display_ranges(editor, cx),
5725 vec![
5726 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5727 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5728 ]
5729 );
5730 });
5731}
5732
5733#[gpui::test]
5734fn test_move_line_up_down(cx: &mut TestAppContext) {
5735 init_test(cx, |_| {});
5736
5737 let editor = cx.add_window(|window, cx| {
5738 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5739 build_editor(buffer, window, cx)
5740 });
5741 _ = editor.update(cx, |editor, window, cx| {
5742 editor.fold_creases(
5743 vec![
5744 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5745 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5746 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5747 ],
5748 true,
5749 window,
5750 cx,
5751 );
5752 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5753 s.select_display_ranges([
5754 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5755 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5756 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5757 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5758 ])
5759 });
5760 assert_eq!(
5761 editor.display_text(cx),
5762 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5763 );
5764
5765 editor.move_line_up(&MoveLineUp, window, cx);
5766 assert_eq!(
5767 editor.display_text(cx),
5768 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5769 );
5770 assert_eq!(
5771 display_ranges(editor, cx),
5772 vec![
5773 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5774 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5775 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5776 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5777 ]
5778 );
5779 });
5780
5781 _ = editor.update(cx, |editor, window, cx| {
5782 editor.move_line_down(&MoveLineDown, window, cx);
5783 assert_eq!(
5784 editor.display_text(cx),
5785 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5786 );
5787 assert_eq!(
5788 display_ranges(editor, cx),
5789 vec![
5790 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5791 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5792 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5793 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5794 ]
5795 );
5796 });
5797
5798 _ = editor.update(cx, |editor, window, cx| {
5799 editor.move_line_down(&MoveLineDown, window, cx);
5800 assert_eq!(
5801 editor.display_text(cx),
5802 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5803 );
5804 assert_eq!(
5805 display_ranges(editor, cx),
5806 vec![
5807 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5808 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5809 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5810 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5811 ]
5812 );
5813 });
5814
5815 _ = editor.update(cx, |editor, window, cx| {
5816 editor.move_line_up(&MoveLineUp, window, cx);
5817 assert_eq!(
5818 editor.display_text(cx),
5819 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5820 );
5821 assert_eq!(
5822 display_ranges(editor, cx),
5823 vec![
5824 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5825 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5826 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5827 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5828 ]
5829 );
5830 });
5831}
5832
5833#[gpui::test]
5834fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5835 init_test(cx, |_| {});
5836 let editor = cx.add_window(|window, cx| {
5837 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5838 build_editor(buffer, window, cx)
5839 });
5840 _ = editor.update(cx, |editor, window, cx| {
5841 editor.fold_creases(
5842 vec![Crease::simple(
5843 Point::new(6, 4)..Point::new(7, 4),
5844 FoldPlaceholder::test(),
5845 )],
5846 true,
5847 window,
5848 cx,
5849 );
5850 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5851 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5852 });
5853 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5854 editor.move_line_up(&MoveLineUp, window, cx);
5855 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5856 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5857 });
5858}
5859
5860#[gpui::test]
5861fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5862 init_test(cx, |_| {});
5863
5864 let editor = cx.add_window(|window, cx| {
5865 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5866 build_editor(buffer, window, cx)
5867 });
5868 _ = editor.update(cx, |editor, window, cx| {
5869 let snapshot = editor.buffer.read(cx).snapshot(cx);
5870 editor.insert_blocks(
5871 [BlockProperties {
5872 style: BlockStyle::Fixed,
5873 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5874 height: Some(1),
5875 render: Arc::new(|_| div().into_any()),
5876 priority: 0,
5877 }],
5878 Some(Autoscroll::fit()),
5879 cx,
5880 );
5881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5882 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5883 });
5884 editor.move_line_down(&MoveLineDown, window, cx);
5885 });
5886}
5887
5888#[gpui::test]
5889async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5890 init_test(cx, |_| {});
5891
5892 let mut cx = EditorTestContext::new(cx).await;
5893 cx.set_state(
5894 &"
5895 ˇzero
5896 one
5897 two
5898 three
5899 four
5900 five
5901 "
5902 .unindent(),
5903 );
5904
5905 // Create a four-line block that replaces three lines of text.
5906 cx.update_editor(|editor, window, cx| {
5907 let snapshot = editor.snapshot(window, cx);
5908 let snapshot = &snapshot.buffer_snapshot();
5909 let placement = BlockPlacement::Replace(
5910 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5911 );
5912 editor.insert_blocks(
5913 [BlockProperties {
5914 placement,
5915 height: Some(4),
5916 style: BlockStyle::Sticky,
5917 render: Arc::new(|_| gpui::div().into_any_element()),
5918 priority: 0,
5919 }],
5920 None,
5921 cx,
5922 );
5923 });
5924
5925 // Move down so that the cursor touches the block.
5926 cx.update_editor(|editor, window, cx| {
5927 editor.move_down(&Default::default(), window, cx);
5928 });
5929 cx.assert_editor_state(
5930 &"
5931 zero
5932 «one
5933 two
5934 threeˇ»
5935 four
5936 five
5937 "
5938 .unindent(),
5939 );
5940
5941 // Move down past the block.
5942 cx.update_editor(|editor, window, cx| {
5943 editor.move_down(&Default::default(), window, cx);
5944 });
5945 cx.assert_editor_state(
5946 &"
5947 zero
5948 one
5949 two
5950 three
5951 ˇfour
5952 five
5953 "
5954 .unindent(),
5955 );
5956}
5957
5958#[gpui::test]
5959fn test_transpose(cx: &mut TestAppContext) {
5960 init_test(cx, |_| {});
5961
5962 _ = cx.add_window(|window, cx| {
5963 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5964 editor.set_style(EditorStyle::default(), window, cx);
5965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5966 s.select_ranges([1..1])
5967 });
5968 editor.transpose(&Default::default(), window, cx);
5969 assert_eq!(editor.text(cx), "bac");
5970 assert_eq!(
5971 editor.selections.ranges(&editor.display_snapshot(cx)),
5972 [2..2]
5973 );
5974
5975 editor.transpose(&Default::default(), window, cx);
5976 assert_eq!(editor.text(cx), "bca");
5977 assert_eq!(
5978 editor.selections.ranges(&editor.display_snapshot(cx)),
5979 [3..3]
5980 );
5981
5982 editor.transpose(&Default::default(), window, cx);
5983 assert_eq!(editor.text(cx), "bac");
5984 assert_eq!(
5985 editor.selections.ranges(&editor.display_snapshot(cx)),
5986 [3..3]
5987 );
5988
5989 editor
5990 });
5991
5992 _ = cx.add_window(|window, cx| {
5993 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5994 editor.set_style(EditorStyle::default(), window, cx);
5995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5996 s.select_ranges([3..3])
5997 });
5998 editor.transpose(&Default::default(), window, cx);
5999 assert_eq!(editor.text(cx), "acb\nde");
6000 assert_eq!(
6001 editor.selections.ranges(&editor.display_snapshot(cx)),
6002 [3..3]
6003 );
6004
6005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6006 s.select_ranges([4..4])
6007 });
6008 editor.transpose(&Default::default(), window, cx);
6009 assert_eq!(editor.text(cx), "acbd\ne");
6010 assert_eq!(
6011 editor.selections.ranges(&editor.display_snapshot(cx)),
6012 [5..5]
6013 );
6014
6015 editor.transpose(&Default::default(), window, cx);
6016 assert_eq!(editor.text(cx), "acbde\n");
6017 assert_eq!(
6018 editor.selections.ranges(&editor.display_snapshot(cx)),
6019 [6..6]
6020 );
6021
6022 editor.transpose(&Default::default(), window, cx);
6023 assert_eq!(editor.text(cx), "acbd\ne");
6024 assert_eq!(
6025 editor.selections.ranges(&editor.display_snapshot(cx)),
6026 [6..6]
6027 );
6028
6029 editor
6030 });
6031
6032 _ = cx.add_window(|window, cx| {
6033 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6034 editor.set_style(EditorStyle::default(), window, cx);
6035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6036 s.select_ranges([1..1, 2..2, 4..4])
6037 });
6038 editor.transpose(&Default::default(), window, cx);
6039 assert_eq!(editor.text(cx), "bacd\ne");
6040 assert_eq!(
6041 editor.selections.ranges(&editor.display_snapshot(cx)),
6042 [2..2, 3..3, 5..5]
6043 );
6044
6045 editor.transpose(&Default::default(), window, cx);
6046 assert_eq!(editor.text(cx), "bcade\n");
6047 assert_eq!(
6048 editor.selections.ranges(&editor.display_snapshot(cx)),
6049 [3..3, 4..4, 6..6]
6050 );
6051
6052 editor.transpose(&Default::default(), window, cx);
6053 assert_eq!(editor.text(cx), "bcda\ne");
6054 assert_eq!(
6055 editor.selections.ranges(&editor.display_snapshot(cx)),
6056 [4..4, 6..6]
6057 );
6058
6059 editor.transpose(&Default::default(), window, cx);
6060 assert_eq!(editor.text(cx), "bcade\n");
6061 assert_eq!(
6062 editor.selections.ranges(&editor.display_snapshot(cx)),
6063 [4..4, 6..6]
6064 );
6065
6066 editor.transpose(&Default::default(), window, cx);
6067 assert_eq!(editor.text(cx), "bcaed\n");
6068 assert_eq!(
6069 editor.selections.ranges(&editor.display_snapshot(cx)),
6070 [5..5, 6..6]
6071 );
6072
6073 editor
6074 });
6075
6076 _ = cx.add_window(|window, cx| {
6077 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6078 editor.set_style(EditorStyle::default(), window, cx);
6079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6080 s.select_ranges([4..4])
6081 });
6082 editor.transpose(&Default::default(), window, cx);
6083 assert_eq!(editor.text(cx), "🏀🍐✋");
6084 assert_eq!(
6085 editor.selections.ranges(&editor.display_snapshot(cx)),
6086 [8..8]
6087 );
6088
6089 editor.transpose(&Default::default(), window, cx);
6090 assert_eq!(editor.text(cx), "🏀✋🍐");
6091 assert_eq!(
6092 editor.selections.ranges(&editor.display_snapshot(cx)),
6093 [11..11]
6094 );
6095
6096 editor.transpose(&Default::default(), window, cx);
6097 assert_eq!(editor.text(cx), "🏀🍐✋");
6098 assert_eq!(
6099 editor.selections.ranges(&editor.display_snapshot(cx)),
6100 [11..11]
6101 );
6102
6103 editor
6104 });
6105}
6106
6107#[gpui::test]
6108async fn test_rewrap(cx: &mut TestAppContext) {
6109 init_test(cx, |settings| {
6110 settings.languages.0.extend([
6111 (
6112 "Markdown".into(),
6113 LanguageSettingsContent {
6114 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6115 preferred_line_length: Some(40),
6116 ..Default::default()
6117 },
6118 ),
6119 (
6120 "Plain Text".into(),
6121 LanguageSettingsContent {
6122 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6123 preferred_line_length: Some(40),
6124 ..Default::default()
6125 },
6126 ),
6127 (
6128 "C++".into(),
6129 LanguageSettingsContent {
6130 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6131 preferred_line_length: Some(40),
6132 ..Default::default()
6133 },
6134 ),
6135 (
6136 "Python".into(),
6137 LanguageSettingsContent {
6138 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6139 preferred_line_length: Some(40),
6140 ..Default::default()
6141 },
6142 ),
6143 (
6144 "Rust".into(),
6145 LanguageSettingsContent {
6146 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6147 preferred_line_length: Some(40),
6148 ..Default::default()
6149 },
6150 ),
6151 ])
6152 });
6153
6154 let mut cx = EditorTestContext::new(cx).await;
6155
6156 let cpp_language = Arc::new(Language::new(
6157 LanguageConfig {
6158 name: "C++".into(),
6159 line_comments: vec!["// ".into()],
6160 ..LanguageConfig::default()
6161 },
6162 None,
6163 ));
6164 let python_language = Arc::new(Language::new(
6165 LanguageConfig {
6166 name: "Python".into(),
6167 line_comments: vec!["# ".into()],
6168 ..LanguageConfig::default()
6169 },
6170 None,
6171 ));
6172 let markdown_language = Arc::new(Language::new(
6173 LanguageConfig {
6174 name: "Markdown".into(),
6175 rewrap_prefixes: vec![
6176 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6177 regex::Regex::new("[-*+]\\s+").unwrap(),
6178 ],
6179 ..LanguageConfig::default()
6180 },
6181 None,
6182 ));
6183 let rust_language = Arc::new(
6184 Language::new(
6185 LanguageConfig {
6186 name: "Rust".into(),
6187 line_comments: vec!["// ".into(), "/// ".into()],
6188 ..LanguageConfig::default()
6189 },
6190 Some(tree_sitter_rust::LANGUAGE.into()),
6191 )
6192 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6193 .unwrap(),
6194 );
6195
6196 let plaintext_language = Arc::new(Language::new(
6197 LanguageConfig {
6198 name: "Plain Text".into(),
6199 ..LanguageConfig::default()
6200 },
6201 None,
6202 ));
6203
6204 // Test basic rewrapping of a long line with a cursor
6205 assert_rewrap(
6206 indoc! {"
6207 // ˇThis is a long comment that needs to be wrapped.
6208 "},
6209 indoc! {"
6210 // ˇThis is a long comment that needs to
6211 // be wrapped.
6212 "},
6213 cpp_language.clone(),
6214 &mut cx,
6215 );
6216
6217 // Test rewrapping a full selection
6218 assert_rewrap(
6219 indoc! {"
6220 «// This selected long comment needs to be wrapped.ˇ»"
6221 },
6222 indoc! {"
6223 «// This selected long comment needs to
6224 // be wrapped.ˇ»"
6225 },
6226 cpp_language.clone(),
6227 &mut cx,
6228 );
6229
6230 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6231 assert_rewrap(
6232 indoc! {"
6233 // ˇThis is the first line.
6234 // Thisˇ is the second line.
6235 // This is the thirdˇ line, all part of one paragraph.
6236 "},
6237 indoc! {"
6238 // ˇThis is the first line. Thisˇ is the
6239 // second line. This is the thirdˇ line,
6240 // all part of one paragraph.
6241 "},
6242 cpp_language.clone(),
6243 &mut cx,
6244 );
6245
6246 // Test multiple cursors in different paragraphs trigger separate rewraps
6247 assert_rewrap(
6248 indoc! {"
6249 // ˇThis is the first paragraph, first line.
6250 // ˇThis is the first paragraph, second line.
6251
6252 // ˇThis is the second paragraph, first line.
6253 // ˇThis is the second paragraph, second line.
6254 "},
6255 indoc! {"
6256 // ˇThis is the first paragraph, first
6257 // line. ˇThis is the first paragraph,
6258 // second line.
6259
6260 // ˇThis is the second paragraph, first
6261 // line. ˇThis is the second paragraph,
6262 // second line.
6263 "},
6264 cpp_language.clone(),
6265 &mut cx,
6266 );
6267
6268 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6269 assert_rewrap(
6270 indoc! {"
6271 «// A regular long long comment to be wrapped.
6272 /// A documentation long comment to be wrapped.ˇ»
6273 "},
6274 indoc! {"
6275 «// A regular long long comment to be
6276 // wrapped.
6277 /// A documentation long comment to be
6278 /// wrapped.ˇ»
6279 "},
6280 rust_language.clone(),
6281 &mut cx,
6282 );
6283
6284 // Test that change in indentation level trigger seperate rewraps
6285 assert_rewrap(
6286 indoc! {"
6287 fn foo() {
6288 «// This is a long comment at the base indent.
6289 // This is a long comment at the next indent.ˇ»
6290 }
6291 "},
6292 indoc! {"
6293 fn foo() {
6294 «// This is a long comment at the
6295 // base indent.
6296 // This is a long comment at the
6297 // next indent.ˇ»
6298 }
6299 "},
6300 rust_language.clone(),
6301 &mut cx,
6302 );
6303
6304 // Test that different comment prefix characters (e.g., '#') are handled correctly
6305 assert_rewrap(
6306 indoc! {"
6307 # ˇThis is a long comment using a pound sign.
6308 "},
6309 indoc! {"
6310 # ˇThis is a long comment using a pound
6311 # sign.
6312 "},
6313 python_language,
6314 &mut cx,
6315 );
6316
6317 // Test rewrapping only affects comments, not code even when selected
6318 assert_rewrap(
6319 indoc! {"
6320 «/// This doc comment is long and should be wrapped.
6321 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6322 "},
6323 indoc! {"
6324 «/// This doc comment is long and should
6325 /// be wrapped.
6326 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6327 "},
6328 rust_language.clone(),
6329 &mut cx,
6330 );
6331
6332 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6333 assert_rewrap(
6334 indoc! {"
6335 # Header
6336
6337 A long long long line of markdown text to wrap.ˇ
6338 "},
6339 indoc! {"
6340 # Header
6341
6342 A long long long line of markdown text
6343 to wrap.ˇ
6344 "},
6345 markdown_language.clone(),
6346 &mut cx,
6347 );
6348
6349 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6350 assert_rewrap(
6351 indoc! {"
6352 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6353 2. This is a numbered list item that is very long and needs to be wrapped properly.
6354 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6355 "},
6356 indoc! {"
6357 «1. This is a numbered list item that is
6358 very long and needs to be wrapped
6359 properly.
6360 2. This is a numbered list item that is
6361 very long and needs to be wrapped
6362 properly.
6363 - This is an unordered list item that is
6364 also very long and should not merge
6365 with the numbered item.ˇ»
6366 "},
6367 markdown_language.clone(),
6368 &mut cx,
6369 );
6370
6371 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6372 assert_rewrap(
6373 indoc! {"
6374 «1. This is a numbered list item that is
6375 very long and needs to be wrapped
6376 properly.
6377 2. This is a numbered list item that is
6378 very long and needs to be wrapped
6379 properly.
6380 - This is an unordered list item that is
6381 also very long and should not merge with
6382 the numbered item.ˇ»
6383 "},
6384 indoc! {"
6385 «1. This is a numbered list item that is
6386 very long and needs to be wrapped
6387 properly.
6388 2. This is a numbered list item that is
6389 very long and needs to be wrapped
6390 properly.
6391 - This is an unordered list item that is
6392 also very long and should not merge
6393 with the numbered item.ˇ»
6394 "},
6395 markdown_language.clone(),
6396 &mut cx,
6397 );
6398
6399 // Test that rewrapping maintain indents even when they already exists.
6400 assert_rewrap(
6401 indoc! {"
6402 «1. This is a numbered list
6403 item that is very long and needs to be wrapped properly.
6404 2. This is a numbered list
6405 item that is very long and needs to be wrapped properly.
6406 - This is an unordered list item that is also very long and
6407 should not merge with the numbered item.ˇ»
6408 "},
6409 indoc! {"
6410 «1. This is a numbered list item that is
6411 very long and needs to be wrapped
6412 properly.
6413 2. This is a numbered list item that is
6414 very long and needs to be wrapped
6415 properly.
6416 - This is an unordered list item that is
6417 also very long and should not merge
6418 with the numbered item.ˇ»
6419 "},
6420 markdown_language,
6421 &mut cx,
6422 );
6423
6424 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6425 assert_rewrap(
6426 indoc! {"
6427 ˇThis is a very long line of plain text that will be wrapped.
6428 "},
6429 indoc! {"
6430 ˇThis is a very long line of plain text
6431 that will be wrapped.
6432 "},
6433 plaintext_language.clone(),
6434 &mut cx,
6435 );
6436
6437 // Test that non-commented code acts as a paragraph boundary within a selection
6438 assert_rewrap(
6439 indoc! {"
6440 «// This is the first long comment block to be wrapped.
6441 fn my_func(a: u32);
6442 // This is the second long comment block to be wrapped.ˇ»
6443 "},
6444 indoc! {"
6445 «// This is the first long comment block
6446 // to be wrapped.
6447 fn my_func(a: u32);
6448 // This is the second long comment block
6449 // to be wrapped.ˇ»
6450 "},
6451 rust_language,
6452 &mut cx,
6453 );
6454
6455 // Test rewrapping multiple selections, including ones with blank lines or tabs
6456 assert_rewrap(
6457 indoc! {"
6458 «ˇThis is a very long line that will be wrapped.
6459
6460 This is another paragraph in the same selection.»
6461
6462 «\tThis is a very long indented line that will be wrapped.ˇ»
6463 "},
6464 indoc! {"
6465 «ˇThis is a very long line that will be
6466 wrapped.
6467
6468 This is another paragraph in the same
6469 selection.»
6470
6471 «\tThis is a very long indented line
6472 \tthat will be wrapped.ˇ»
6473 "},
6474 plaintext_language,
6475 &mut cx,
6476 );
6477
6478 // Test that an empty comment line acts as a paragraph boundary
6479 assert_rewrap(
6480 indoc! {"
6481 // ˇThis is a long comment that will be wrapped.
6482 //
6483 // And this is another long comment that will also be wrapped.ˇ
6484 "},
6485 indoc! {"
6486 // ˇThis is a long comment that will be
6487 // wrapped.
6488 //
6489 // And this is another long comment that
6490 // will also be wrapped.ˇ
6491 "},
6492 cpp_language,
6493 &mut cx,
6494 );
6495
6496 #[track_caller]
6497 fn assert_rewrap(
6498 unwrapped_text: &str,
6499 wrapped_text: &str,
6500 language: Arc<Language>,
6501 cx: &mut EditorTestContext,
6502 ) {
6503 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6504 cx.set_state(unwrapped_text);
6505 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6506 cx.assert_editor_state(wrapped_text);
6507 }
6508}
6509
6510#[gpui::test]
6511async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6512 init_test(cx, |settings| {
6513 settings.languages.0.extend([(
6514 "Rust".into(),
6515 LanguageSettingsContent {
6516 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6517 preferred_line_length: Some(40),
6518 ..Default::default()
6519 },
6520 )])
6521 });
6522
6523 let mut cx = EditorTestContext::new(cx).await;
6524
6525 let rust_lang = Arc::new(
6526 Language::new(
6527 LanguageConfig {
6528 name: "Rust".into(),
6529 line_comments: vec!["// ".into()],
6530 block_comment: Some(BlockCommentConfig {
6531 start: "/*".into(),
6532 end: "*/".into(),
6533 prefix: "* ".into(),
6534 tab_size: 1,
6535 }),
6536 documentation_comment: Some(BlockCommentConfig {
6537 start: "/**".into(),
6538 end: "*/".into(),
6539 prefix: "* ".into(),
6540 tab_size: 1,
6541 }),
6542
6543 ..LanguageConfig::default()
6544 },
6545 Some(tree_sitter_rust::LANGUAGE.into()),
6546 )
6547 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6548 .unwrap(),
6549 );
6550
6551 // regular block comment
6552 assert_rewrap(
6553 indoc! {"
6554 /*
6555 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6556 */
6557 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6558 "},
6559 indoc! {"
6560 /*
6561 *ˇ Lorem ipsum dolor sit amet,
6562 * consectetur adipiscing elit.
6563 */
6564 /*
6565 *ˇ Lorem ipsum dolor sit amet,
6566 * consectetur adipiscing elit.
6567 */
6568 "},
6569 rust_lang.clone(),
6570 &mut cx,
6571 );
6572
6573 // indent is respected
6574 assert_rewrap(
6575 indoc! {"
6576 {}
6577 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6578 "},
6579 indoc! {"
6580 {}
6581 /*
6582 *ˇ Lorem ipsum dolor sit amet,
6583 * consectetur adipiscing elit.
6584 */
6585 "},
6586 rust_lang.clone(),
6587 &mut cx,
6588 );
6589
6590 // short block comments with inline delimiters
6591 assert_rewrap(
6592 indoc! {"
6593 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6594 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6595 */
6596 /*
6597 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6598 "},
6599 indoc! {"
6600 /*
6601 *ˇ Lorem ipsum dolor sit amet,
6602 * consectetur adipiscing elit.
6603 */
6604 /*
6605 *ˇ Lorem ipsum dolor sit amet,
6606 * consectetur adipiscing elit.
6607 */
6608 /*
6609 *ˇ Lorem ipsum dolor sit amet,
6610 * consectetur adipiscing elit.
6611 */
6612 "},
6613 rust_lang.clone(),
6614 &mut cx,
6615 );
6616
6617 // multiline block comment with inline start/end delimiters
6618 assert_rewrap(
6619 indoc! {"
6620 /*ˇ Lorem ipsum dolor sit amet,
6621 * consectetur adipiscing elit. */
6622 "},
6623 indoc! {"
6624 /*
6625 *ˇ Lorem ipsum dolor sit amet,
6626 * consectetur adipiscing elit.
6627 */
6628 "},
6629 rust_lang.clone(),
6630 &mut cx,
6631 );
6632
6633 // block comment rewrap still respects paragraph bounds
6634 assert_rewrap(
6635 indoc! {"
6636 /*
6637 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6638 *
6639 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6640 */
6641 "},
6642 indoc! {"
6643 /*
6644 *ˇ Lorem ipsum dolor sit amet,
6645 * consectetur adipiscing elit.
6646 *
6647 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6648 */
6649 "},
6650 rust_lang.clone(),
6651 &mut cx,
6652 );
6653
6654 // documentation comments
6655 assert_rewrap(
6656 indoc! {"
6657 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6658 /**
6659 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6660 */
6661 "},
6662 indoc! {"
6663 /**
6664 *ˇ Lorem ipsum dolor sit amet,
6665 * consectetur adipiscing elit.
6666 */
6667 /**
6668 *ˇ Lorem ipsum dolor sit amet,
6669 * consectetur adipiscing elit.
6670 */
6671 "},
6672 rust_lang.clone(),
6673 &mut cx,
6674 );
6675
6676 // different, adjacent comments
6677 assert_rewrap(
6678 indoc! {"
6679 /**
6680 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6681 */
6682 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6683 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6684 "},
6685 indoc! {"
6686 /**
6687 *ˇ Lorem ipsum dolor sit amet,
6688 * consectetur adipiscing elit.
6689 */
6690 /*
6691 *ˇ Lorem ipsum dolor sit amet,
6692 * consectetur adipiscing elit.
6693 */
6694 //ˇ Lorem ipsum dolor sit amet,
6695 // consectetur adipiscing elit.
6696 "},
6697 rust_lang.clone(),
6698 &mut cx,
6699 );
6700
6701 // selection w/ single short block comment
6702 assert_rewrap(
6703 indoc! {"
6704 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6705 "},
6706 indoc! {"
6707 «/*
6708 * Lorem ipsum dolor sit amet,
6709 * consectetur adipiscing elit.
6710 */ˇ»
6711 "},
6712 rust_lang.clone(),
6713 &mut cx,
6714 );
6715
6716 // rewrapping a single comment w/ abutting comments
6717 assert_rewrap(
6718 indoc! {"
6719 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6720 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6721 "},
6722 indoc! {"
6723 /*
6724 * ˇLorem ipsum dolor sit amet,
6725 * consectetur adipiscing elit.
6726 */
6727 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6728 "},
6729 rust_lang.clone(),
6730 &mut cx,
6731 );
6732
6733 // selection w/ non-abutting short block comments
6734 assert_rewrap(
6735 indoc! {"
6736 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6737
6738 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6739 "},
6740 indoc! {"
6741 «/*
6742 * Lorem ipsum dolor sit amet,
6743 * consectetur adipiscing elit.
6744 */
6745
6746 /*
6747 * Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit.
6749 */ˇ»
6750 "},
6751 rust_lang.clone(),
6752 &mut cx,
6753 );
6754
6755 // selection of multiline block comments
6756 assert_rewrap(
6757 indoc! {"
6758 «/* Lorem ipsum dolor sit amet,
6759 * consectetur adipiscing elit. */ˇ»
6760 "},
6761 indoc! {"
6762 «/*
6763 * Lorem ipsum dolor sit amet,
6764 * consectetur adipiscing elit.
6765 */ˇ»
6766 "},
6767 rust_lang.clone(),
6768 &mut cx,
6769 );
6770
6771 // partial selection of multiline block comments
6772 assert_rewrap(
6773 indoc! {"
6774 «/* Lorem ipsum dolor sit amet,ˇ»
6775 * consectetur adipiscing elit. */
6776 /* Lorem ipsum dolor sit amet,
6777 «* consectetur adipiscing elit. */ˇ»
6778 "},
6779 indoc! {"
6780 «/*
6781 * Lorem ipsum dolor sit amet,ˇ»
6782 * consectetur adipiscing elit. */
6783 /* Lorem ipsum dolor sit amet,
6784 «* consectetur adipiscing elit.
6785 */ˇ»
6786 "},
6787 rust_lang.clone(),
6788 &mut cx,
6789 );
6790
6791 // selection w/ abutting short block comments
6792 // TODO: should not be combined; should rewrap as 2 comments
6793 assert_rewrap(
6794 indoc! {"
6795 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6796 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6797 "},
6798 // desired behavior:
6799 // indoc! {"
6800 // «/*
6801 // * Lorem ipsum dolor sit amet,
6802 // * consectetur adipiscing elit.
6803 // */
6804 // /*
6805 // * Lorem ipsum dolor sit amet,
6806 // * consectetur adipiscing elit.
6807 // */ˇ»
6808 // "},
6809 // actual behaviour:
6810 indoc! {"
6811 «/*
6812 * Lorem ipsum dolor sit amet,
6813 * consectetur adipiscing elit. Lorem
6814 * ipsum dolor sit amet, consectetur
6815 * adipiscing elit.
6816 */ˇ»
6817 "},
6818 rust_lang.clone(),
6819 &mut cx,
6820 );
6821
6822 // TODO: same as above, but with delimiters on separate line
6823 // assert_rewrap(
6824 // indoc! {"
6825 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6826 // */
6827 // /*
6828 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6829 // "},
6830 // // desired:
6831 // // indoc! {"
6832 // // «/*
6833 // // * Lorem ipsum dolor sit amet,
6834 // // * consectetur adipiscing elit.
6835 // // */
6836 // // /*
6837 // // * Lorem ipsum dolor sit amet,
6838 // // * consectetur adipiscing elit.
6839 // // */ˇ»
6840 // // "},
6841 // // actual: (but with trailing w/s on the empty lines)
6842 // indoc! {"
6843 // «/*
6844 // * Lorem ipsum dolor sit amet,
6845 // * consectetur adipiscing elit.
6846 // *
6847 // */
6848 // /*
6849 // *
6850 // * Lorem ipsum dolor sit amet,
6851 // * consectetur adipiscing elit.
6852 // */ˇ»
6853 // "},
6854 // rust_lang.clone(),
6855 // &mut cx,
6856 // );
6857
6858 // TODO these are unhandled edge cases; not correct, just documenting known issues
6859 assert_rewrap(
6860 indoc! {"
6861 /*
6862 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6863 */
6864 /*
6865 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6866 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6867 "},
6868 // desired:
6869 // indoc! {"
6870 // /*
6871 // *ˇ Lorem ipsum dolor sit amet,
6872 // * consectetur adipiscing elit.
6873 // */
6874 // /*
6875 // *ˇ Lorem ipsum dolor sit amet,
6876 // * consectetur adipiscing elit.
6877 // */
6878 // /*
6879 // *ˇ Lorem ipsum dolor sit amet
6880 // */ /* consectetur adipiscing elit. */
6881 // "},
6882 // actual:
6883 indoc! {"
6884 /*
6885 //ˇ Lorem ipsum dolor sit amet,
6886 // consectetur adipiscing elit.
6887 */
6888 /*
6889 * //ˇ Lorem ipsum dolor sit amet,
6890 * consectetur adipiscing elit.
6891 */
6892 /*
6893 *ˇ Lorem ipsum dolor sit amet */ /*
6894 * consectetur adipiscing elit.
6895 */
6896 "},
6897 rust_lang,
6898 &mut cx,
6899 );
6900
6901 #[track_caller]
6902 fn assert_rewrap(
6903 unwrapped_text: &str,
6904 wrapped_text: &str,
6905 language: Arc<Language>,
6906 cx: &mut EditorTestContext,
6907 ) {
6908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6909 cx.set_state(unwrapped_text);
6910 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6911 cx.assert_editor_state(wrapped_text);
6912 }
6913}
6914
6915#[gpui::test]
6916async fn test_hard_wrap(cx: &mut TestAppContext) {
6917 init_test(cx, |_| {});
6918 let mut cx = EditorTestContext::new(cx).await;
6919
6920 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6921 cx.update_editor(|editor, _, cx| {
6922 editor.set_hard_wrap(Some(14), cx);
6923 });
6924
6925 cx.set_state(indoc!(
6926 "
6927 one two three ˇ
6928 "
6929 ));
6930 cx.simulate_input("four");
6931 cx.run_until_parked();
6932
6933 cx.assert_editor_state(indoc!(
6934 "
6935 one two three
6936 fourˇ
6937 "
6938 ));
6939
6940 cx.update_editor(|editor, window, cx| {
6941 editor.newline(&Default::default(), window, cx);
6942 });
6943 cx.run_until_parked();
6944 cx.assert_editor_state(indoc!(
6945 "
6946 one two three
6947 four
6948 ˇ
6949 "
6950 ));
6951
6952 cx.simulate_input("five");
6953 cx.run_until_parked();
6954 cx.assert_editor_state(indoc!(
6955 "
6956 one two three
6957 four
6958 fiveˇ
6959 "
6960 ));
6961
6962 cx.update_editor(|editor, window, cx| {
6963 editor.newline(&Default::default(), window, cx);
6964 });
6965 cx.run_until_parked();
6966 cx.simulate_input("# ");
6967 cx.run_until_parked();
6968 cx.assert_editor_state(indoc!(
6969 "
6970 one two three
6971 four
6972 five
6973 # ˇ
6974 "
6975 ));
6976
6977 cx.update_editor(|editor, window, cx| {
6978 editor.newline(&Default::default(), window, cx);
6979 });
6980 cx.run_until_parked();
6981 cx.assert_editor_state(indoc!(
6982 "
6983 one two three
6984 four
6985 five
6986 #\x20
6987 #ˇ
6988 "
6989 ));
6990
6991 cx.simulate_input(" 6");
6992 cx.run_until_parked();
6993 cx.assert_editor_state(indoc!(
6994 "
6995 one two three
6996 four
6997 five
6998 #
6999 # 6ˇ
7000 "
7001 ));
7002}
7003
7004#[gpui::test]
7005async fn test_cut_line_ends(cx: &mut TestAppContext) {
7006 init_test(cx, |_| {});
7007
7008 let mut cx = EditorTestContext::new(cx).await;
7009
7010 cx.set_state(indoc! {"The quick brownˇ"});
7011 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7012 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7013
7014 cx.set_state(indoc! {"The emacs foxˇ"});
7015 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7016 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7017
7018 cx.set_state(indoc! {"
7019 The quick« brownˇ»
7020 fox jumps overˇ
7021 the lazy dog"});
7022 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7023 cx.assert_editor_state(indoc! {"
7024 The quickˇ
7025 ˇthe lazy dog"});
7026
7027 cx.set_state(indoc! {"
7028 The quick« brownˇ»
7029 fox jumps overˇ
7030 the lazy dog"});
7031 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7032 cx.assert_editor_state(indoc! {"
7033 The quickˇ
7034 fox jumps overˇthe lazy dog"});
7035
7036 cx.set_state(indoc! {"
7037 The quick« brownˇ»
7038 fox jumps overˇ
7039 the lazy dog"});
7040 cx.update_editor(|e, window, cx| {
7041 e.cut_to_end_of_line(
7042 &CutToEndOfLine {
7043 stop_at_newlines: true,
7044 },
7045 window,
7046 cx,
7047 )
7048 });
7049 cx.assert_editor_state(indoc! {"
7050 The quickˇ
7051 fox jumps overˇ
7052 the lazy dog"});
7053
7054 cx.set_state(indoc! {"
7055 The quick« brownˇ»
7056 fox jumps overˇ
7057 the lazy dog"});
7058 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7059 cx.assert_editor_state(indoc! {"
7060 The quickˇ
7061 fox jumps overˇthe lazy dog"});
7062}
7063
7064#[gpui::test]
7065async fn test_clipboard(cx: &mut TestAppContext) {
7066 init_test(cx, |_| {});
7067
7068 let mut cx = EditorTestContext::new(cx).await;
7069
7070 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7071 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7072 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7073
7074 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7075 cx.set_state("two ˇfour ˇsix ˇ");
7076 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7077 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7078
7079 // Paste again but with only two cursors. Since the number of cursors doesn't
7080 // match the number of slices in the clipboard, the entire clipboard text
7081 // is pasted at each cursor.
7082 cx.set_state("ˇtwo one✅ four three six five ˇ");
7083 cx.update_editor(|e, window, cx| {
7084 e.handle_input("( ", window, cx);
7085 e.paste(&Paste, window, cx);
7086 e.handle_input(") ", window, cx);
7087 });
7088 cx.assert_editor_state(
7089 &([
7090 "( one✅ ",
7091 "three ",
7092 "five ) ˇtwo one✅ four three six five ( one✅ ",
7093 "three ",
7094 "five ) ˇ",
7095 ]
7096 .join("\n")),
7097 );
7098
7099 // Cut with three selections, one of which is full-line.
7100 cx.set_state(indoc! {"
7101 1«2ˇ»3
7102 4ˇ567
7103 «8ˇ»9"});
7104 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7105 cx.assert_editor_state(indoc! {"
7106 1ˇ3
7107 ˇ9"});
7108
7109 // Paste with three selections, noticing how the copied selection that was full-line
7110 // gets inserted before the second cursor.
7111 cx.set_state(indoc! {"
7112 1ˇ3
7113 9ˇ
7114 «oˇ»ne"});
7115 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7116 cx.assert_editor_state(indoc! {"
7117 12ˇ3
7118 4567
7119 9ˇ
7120 8ˇne"});
7121
7122 // Copy with a single cursor only, which writes the whole line into the clipboard.
7123 cx.set_state(indoc! {"
7124 The quick brown
7125 fox juˇmps over
7126 the lazy dog"});
7127 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7128 assert_eq!(
7129 cx.read_from_clipboard()
7130 .and_then(|item| item.text().as_deref().map(str::to_string)),
7131 Some("fox jumps over\n".to_string())
7132 );
7133
7134 // Paste with three selections, noticing how the copied full-line selection is inserted
7135 // before the empty selections but replaces the selection that is non-empty.
7136 cx.set_state(indoc! {"
7137 Tˇhe quick brown
7138 «foˇ»x jumps over
7139 tˇhe lazy dog"});
7140 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7141 cx.assert_editor_state(indoc! {"
7142 fox jumps over
7143 Tˇhe quick brown
7144 fox jumps over
7145 ˇx jumps over
7146 fox jumps over
7147 tˇhe lazy dog"});
7148}
7149
7150#[gpui::test]
7151async fn test_copy_trim(cx: &mut TestAppContext) {
7152 init_test(cx, |_| {});
7153
7154 let mut cx = EditorTestContext::new(cx).await;
7155 cx.set_state(
7156 r#" «for selection in selections.iter() {
7157 let mut start = selection.start;
7158 let mut end = selection.end;
7159 let is_entire_line = selection.is_empty();
7160 if is_entire_line {
7161 start = Point::new(start.row, 0);ˇ»
7162 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7163 }
7164 "#,
7165 );
7166 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7167 assert_eq!(
7168 cx.read_from_clipboard()
7169 .and_then(|item| item.text().as_deref().map(str::to_string)),
7170 Some(
7171 "for selection in selections.iter() {
7172 let mut start = selection.start;
7173 let mut end = selection.end;
7174 let is_entire_line = selection.is_empty();
7175 if is_entire_line {
7176 start = Point::new(start.row, 0);"
7177 .to_string()
7178 ),
7179 "Regular copying preserves all indentation selected",
7180 );
7181 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7182 assert_eq!(
7183 cx.read_from_clipboard()
7184 .and_then(|item| item.text().as_deref().map(str::to_string)),
7185 Some(
7186 "for selection in selections.iter() {
7187let mut start = selection.start;
7188let mut end = selection.end;
7189let is_entire_line = selection.is_empty();
7190if is_entire_line {
7191 start = Point::new(start.row, 0);"
7192 .to_string()
7193 ),
7194 "Copying with stripping should strip all leading whitespaces"
7195 );
7196
7197 cx.set_state(
7198 r#" « for selection in selections.iter() {
7199 let mut start = selection.start;
7200 let mut end = selection.end;
7201 let is_entire_line = selection.is_empty();
7202 if is_entire_line {
7203 start = Point::new(start.row, 0);ˇ»
7204 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7205 }
7206 "#,
7207 );
7208 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7209 assert_eq!(
7210 cx.read_from_clipboard()
7211 .and_then(|item| item.text().as_deref().map(str::to_string)),
7212 Some(
7213 " for selection in selections.iter() {
7214 let mut start = selection.start;
7215 let mut end = selection.end;
7216 let is_entire_line = selection.is_empty();
7217 if is_entire_line {
7218 start = Point::new(start.row, 0);"
7219 .to_string()
7220 ),
7221 "Regular copying preserves all indentation selected",
7222 );
7223 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7224 assert_eq!(
7225 cx.read_from_clipboard()
7226 .and_then(|item| item.text().as_deref().map(str::to_string)),
7227 Some(
7228 "for selection in selections.iter() {
7229let mut start = selection.start;
7230let mut end = selection.end;
7231let is_entire_line = selection.is_empty();
7232if is_entire_line {
7233 start = Point::new(start.row, 0);"
7234 .to_string()
7235 ),
7236 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7237 );
7238
7239 cx.set_state(
7240 r#" «ˇ for selection in selections.iter() {
7241 let mut start = selection.start;
7242 let mut end = selection.end;
7243 let is_entire_line = selection.is_empty();
7244 if is_entire_line {
7245 start = Point::new(start.row, 0);»
7246 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7247 }
7248 "#,
7249 );
7250 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7251 assert_eq!(
7252 cx.read_from_clipboard()
7253 .and_then(|item| item.text().as_deref().map(str::to_string)),
7254 Some(
7255 " for selection in selections.iter() {
7256 let mut start = selection.start;
7257 let mut end = selection.end;
7258 let is_entire_line = selection.is_empty();
7259 if is_entire_line {
7260 start = Point::new(start.row, 0);"
7261 .to_string()
7262 ),
7263 "Regular copying for reverse selection works the same",
7264 );
7265 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7266 assert_eq!(
7267 cx.read_from_clipboard()
7268 .and_then(|item| item.text().as_deref().map(str::to_string)),
7269 Some(
7270 "for selection in selections.iter() {
7271let mut start = selection.start;
7272let mut end = selection.end;
7273let is_entire_line = selection.is_empty();
7274if is_entire_line {
7275 start = Point::new(start.row, 0);"
7276 .to_string()
7277 ),
7278 "Copying with stripping for reverse selection works the same"
7279 );
7280
7281 cx.set_state(
7282 r#" for selection «in selections.iter() {
7283 let mut start = selection.start;
7284 let mut end = selection.end;
7285 let is_entire_line = selection.is_empty();
7286 if is_entire_line {
7287 start = Point::new(start.row, 0);ˇ»
7288 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7289 }
7290 "#,
7291 );
7292 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7293 assert_eq!(
7294 cx.read_from_clipboard()
7295 .and_then(|item| item.text().as_deref().map(str::to_string)),
7296 Some(
7297 "in selections.iter() {
7298 let mut start = selection.start;
7299 let mut end = selection.end;
7300 let is_entire_line = selection.is_empty();
7301 if is_entire_line {
7302 start = Point::new(start.row, 0);"
7303 .to_string()
7304 ),
7305 "When selecting past the indent, the copying works as usual",
7306 );
7307 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7308 assert_eq!(
7309 cx.read_from_clipboard()
7310 .and_then(|item| item.text().as_deref().map(str::to_string)),
7311 Some(
7312 "in selections.iter() {
7313 let mut start = selection.start;
7314 let mut end = selection.end;
7315 let is_entire_line = selection.is_empty();
7316 if is_entire_line {
7317 start = Point::new(start.row, 0);"
7318 .to_string()
7319 ),
7320 "When selecting past the indent, nothing is trimmed"
7321 );
7322
7323 cx.set_state(
7324 r#" «for selection in selections.iter() {
7325 let mut start = selection.start;
7326
7327 let mut end = selection.end;
7328 let is_entire_line = selection.is_empty();
7329 if is_entire_line {
7330 start = Point::new(start.row, 0);
7331ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7332 }
7333 "#,
7334 );
7335 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7336 assert_eq!(
7337 cx.read_from_clipboard()
7338 .and_then(|item| item.text().as_deref().map(str::to_string)),
7339 Some(
7340 "for selection in selections.iter() {
7341let mut start = selection.start;
7342
7343let mut end = selection.end;
7344let is_entire_line = selection.is_empty();
7345if is_entire_line {
7346 start = Point::new(start.row, 0);
7347"
7348 .to_string()
7349 ),
7350 "Copying with stripping should ignore empty lines"
7351 );
7352}
7353
7354#[gpui::test]
7355async fn test_paste_multiline(cx: &mut TestAppContext) {
7356 init_test(cx, |_| {});
7357
7358 let mut cx = EditorTestContext::new(cx).await;
7359 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7360
7361 // Cut an indented block, without the leading whitespace.
7362 cx.set_state(indoc! {"
7363 const a: B = (
7364 c(),
7365 «d(
7366 e,
7367 f
7368 )ˇ»
7369 );
7370 "});
7371 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7372 cx.assert_editor_state(indoc! {"
7373 const a: B = (
7374 c(),
7375 ˇ
7376 );
7377 "});
7378
7379 // Paste it at the same position.
7380 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7381 cx.assert_editor_state(indoc! {"
7382 const a: B = (
7383 c(),
7384 d(
7385 e,
7386 f
7387 )ˇ
7388 );
7389 "});
7390
7391 // Paste it at a line with a lower indent level.
7392 cx.set_state(indoc! {"
7393 ˇ
7394 const a: B = (
7395 c(),
7396 );
7397 "});
7398 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7399 cx.assert_editor_state(indoc! {"
7400 d(
7401 e,
7402 f
7403 )ˇ
7404 const a: B = (
7405 c(),
7406 );
7407 "});
7408
7409 // Cut an indented block, with the leading whitespace.
7410 cx.set_state(indoc! {"
7411 const a: B = (
7412 c(),
7413 « d(
7414 e,
7415 f
7416 )
7417 ˇ»);
7418 "});
7419 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7420 cx.assert_editor_state(indoc! {"
7421 const a: B = (
7422 c(),
7423 ˇ);
7424 "});
7425
7426 // Paste it at the same position.
7427 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7428 cx.assert_editor_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 d(
7432 e,
7433 f
7434 )
7435 ˇ);
7436 "});
7437
7438 // Paste it at a line with a higher indent level.
7439 cx.set_state(indoc! {"
7440 const a: B = (
7441 c(),
7442 d(
7443 e,
7444 fˇ
7445 )
7446 );
7447 "});
7448 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7449 cx.assert_editor_state(indoc! {"
7450 const a: B = (
7451 c(),
7452 d(
7453 e,
7454 f d(
7455 e,
7456 f
7457 )
7458 ˇ
7459 )
7460 );
7461 "});
7462
7463 // Copy an indented block, starting mid-line
7464 cx.set_state(indoc! {"
7465 const a: B = (
7466 c(),
7467 somethin«g(
7468 e,
7469 f
7470 )ˇ»
7471 );
7472 "});
7473 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7474
7475 // Paste it on a line with a lower indent level
7476 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7477 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7478 cx.assert_editor_state(indoc! {"
7479 const a: B = (
7480 c(),
7481 something(
7482 e,
7483 f
7484 )
7485 );
7486 g(
7487 e,
7488 f
7489 )ˇ"});
7490}
7491
7492#[gpui::test]
7493async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7494 init_test(cx, |_| {});
7495
7496 cx.write_to_clipboard(ClipboardItem::new_string(
7497 " d(\n e\n );\n".into(),
7498 ));
7499
7500 let mut cx = EditorTestContext::new(cx).await;
7501 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7502
7503 cx.set_state(indoc! {"
7504 fn a() {
7505 b();
7506 if c() {
7507 ˇ
7508 }
7509 }
7510 "});
7511
7512 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7513 cx.assert_editor_state(indoc! {"
7514 fn a() {
7515 b();
7516 if c() {
7517 d(
7518 e
7519 );
7520 ˇ
7521 }
7522 }
7523 "});
7524
7525 cx.set_state(indoc! {"
7526 fn a() {
7527 b();
7528 ˇ
7529 }
7530 "});
7531
7532 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7533 cx.assert_editor_state(indoc! {"
7534 fn a() {
7535 b();
7536 d(
7537 e
7538 );
7539 ˇ
7540 }
7541 "});
7542}
7543
7544#[gpui::test]
7545fn test_select_all(cx: &mut TestAppContext) {
7546 init_test(cx, |_| {});
7547
7548 let editor = cx.add_window(|window, cx| {
7549 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7550 build_editor(buffer, window, cx)
7551 });
7552 _ = editor.update(cx, |editor, window, cx| {
7553 editor.select_all(&SelectAll, window, cx);
7554 assert_eq!(
7555 display_ranges(editor, cx),
7556 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7557 );
7558 });
7559}
7560
7561#[gpui::test]
7562fn test_select_line(cx: &mut TestAppContext) {
7563 init_test(cx, |_| {});
7564
7565 let editor = cx.add_window(|window, cx| {
7566 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7567 build_editor(buffer, window, cx)
7568 });
7569 _ = editor.update(cx, |editor, window, cx| {
7570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7571 s.select_display_ranges([
7572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7573 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7574 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7575 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7576 ])
7577 });
7578 editor.select_line(&SelectLine, window, cx);
7579 assert_eq!(
7580 display_ranges(editor, cx),
7581 vec![
7582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7584 ]
7585 );
7586 });
7587
7588 _ = editor.update(cx, |editor, window, cx| {
7589 editor.select_line(&SelectLine, window, cx);
7590 assert_eq!(
7591 display_ranges(editor, cx),
7592 vec![
7593 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7594 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7595 ]
7596 );
7597 });
7598
7599 _ = editor.update(cx, |editor, window, cx| {
7600 editor.select_line(&SelectLine, window, cx);
7601 assert_eq!(
7602 display_ranges(editor, cx),
7603 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7604 );
7605 });
7606}
7607
7608#[gpui::test]
7609async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7610 init_test(cx, |_| {});
7611 let mut cx = EditorTestContext::new(cx).await;
7612
7613 #[track_caller]
7614 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7615 cx.set_state(initial_state);
7616 cx.update_editor(|e, window, cx| {
7617 e.split_selection_into_lines(&Default::default(), window, cx)
7618 });
7619 cx.assert_editor_state(expected_state);
7620 }
7621
7622 // Selection starts and ends at the middle of lines, left-to-right
7623 test(
7624 &mut cx,
7625 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7626 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7627 );
7628 // Same thing, right-to-left
7629 test(
7630 &mut cx,
7631 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7632 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7633 );
7634
7635 // Whole buffer, left-to-right, last line *doesn't* end with newline
7636 test(
7637 &mut cx,
7638 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7639 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7640 );
7641 // Same thing, right-to-left
7642 test(
7643 &mut cx,
7644 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7645 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7646 );
7647
7648 // Whole buffer, left-to-right, last line ends with newline
7649 test(
7650 &mut cx,
7651 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7652 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7653 );
7654 // Same thing, right-to-left
7655 test(
7656 &mut cx,
7657 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7658 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7659 );
7660
7661 // Starts at the end of a line, ends at the start of another
7662 test(
7663 &mut cx,
7664 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7665 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7666 );
7667}
7668
7669#[gpui::test]
7670async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7671 init_test(cx, |_| {});
7672
7673 let editor = cx.add_window(|window, cx| {
7674 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7675 build_editor(buffer, window, cx)
7676 });
7677
7678 // setup
7679 _ = editor.update(cx, |editor, window, cx| {
7680 editor.fold_creases(
7681 vec![
7682 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7683 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7684 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7685 ],
7686 true,
7687 window,
7688 cx,
7689 );
7690 assert_eq!(
7691 editor.display_text(cx),
7692 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7693 );
7694 });
7695
7696 _ = editor.update(cx, |editor, window, cx| {
7697 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7698 s.select_display_ranges([
7699 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7700 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7701 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7702 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7703 ])
7704 });
7705 editor.split_selection_into_lines(&Default::default(), window, cx);
7706 assert_eq!(
7707 editor.display_text(cx),
7708 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7709 );
7710 });
7711 EditorTestContext::for_editor(editor, cx)
7712 .await
7713 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7714
7715 _ = editor.update(cx, |editor, window, cx| {
7716 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7717 s.select_display_ranges([
7718 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7719 ])
7720 });
7721 editor.split_selection_into_lines(&Default::default(), window, cx);
7722 assert_eq!(
7723 editor.display_text(cx),
7724 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7725 );
7726 assert_eq!(
7727 display_ranges(editor, cx),
7728 [
7729 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7730 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7731 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7732 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7733 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7734 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7735 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7736 ]
7737 );
7738 });
7739 EditorTestContext::for_editor(editor, cx)
7740 .await
7741 .assert_editor_state(
7742 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7743 );
7744}
7745
7746#[gpui::test]
7747async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7748 init_test(cx, |_| {});
7749
7750 let mut cx = EditorTestContext::new(cx).await;
7751
7752 cx.set_state(indoc!(
7753 r#"abc
7754 defˇghi
7755
7756 jk
7757 nlmo
7758 "#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.add_selection_above(&Default::default(), window, cx);
7763 });
7764
7765 cx.assert_editor_state(indoc!(
7766 r#"abcˇ
7767 defˇghi
7768
7769 jk
7770 nlmo
7771 "#
7772 ));
7773
7774 cx.update_editor(|editor, window, cx| {
7775 editor.add_selection_above(&Default::default(), window, cx);
7776 });
7777
7778 cx.assert_editor_state(indoc!(
7779 r#"abcˇ
7780 defˇghi
7781
7782 jk
7783 nlmo
7784 "#
7785 ));
7786
7787 cx.update_editor(|editor, window, cx| {
7788 editor.add_selection_below(&Default::default(), window, cx);
7789 });
7790
7791 cx.assert_editor_state(indoc!(
7792 r#"abc
7793 defˇghi
7794
7795 jk
7796 nlmo
7797 "#
7798 ));
7799
7800 cx.update_editor(|editor, window, cx| {
7801 editor.undo_selection(&Default::default(), window, cx);
7802 });
7803
7804 cx.assert_editor_state(indoc!(
7805 r#"abcˇ
7806 defˇghi
7807
7808 jk
7809 nlmo
7810 "#
7811 ));
7812
7813 cx.update_editor(|editor, window, cx| {
7814 editor.redo_selection(&Default::default(), window, cx);
7815 });
7816
7817 cx.assert_editor_state(indoc!(
7818 r#"abc
7819 defˇghi
7820
7821 jk
7822 nlmo
7823 "#
7824 ));
7825
7826 cx.update_editor(|editor, window, cx| {
7827 editor.add_selection_below(&Default::default(), window, cx);
7828 });
7829
7830 cx.assert_editor_state(indoc!(
7831 r#"abc
7832 defˇghi
7833 ˇ
7834 jk
7835 nlmo
7836 "#
7837 ));
7838
7839 cx.update_editor(|editor, window, cx| {
7840 editor.add_selection_below(&Default::default(), window, cx);
7841 });
7842
7843 cx.assert_editor_state(indoc!(
7844 r#"abc
7845 defˇghi
7846 ˇ
7847 jkˇ
7848 nlmo
7849 "#
7850 ));
7851
7852 cx.update_editor(|editor, window, cx| {
7853 editor.add_selection_below(&Default::default(), window, cx);
7854 });
7855
7856 cx.assert_editor_state(indoc!(
7857 r#"abc
7858 defˇghi
7859 ˇ
7860 jkˇ
7861 nlmˇo
7862 "#
7863 ));
7864
7865 cx.update_editor(|editor, window, cx| {
7866 editor.add_selection_below(&Default::default(), window, cx);
7867 });
7868
7869 cx.assert_editor_state(indoc!(
7870 r#"abc
7871 defˇghi
7872 ˇ
7873 jkˇ
7874 nlmˇo
7875 ˇ"#
7876 ));
7877
7878 // change selections
7879 cx.set_state(indoc!(
7880 r#"abc
7881 def«ˇg»hi
7882
7883 jk
7884 nlmo
7885 "#
7886 ));
7887
7888 cx.update_editor(|editor, window, cx| {
7889 editor.add_selection_below(&Default::default(), window, cx);
7890 });
7891
7892 cx.assert_editor_state(indoc!(
7893 r#"abc
7894 def«ˇg»hi
7895
7896 jk
7897 nlm«ˇo»
7898 "#
7899 ));
7900
7901 cx.update_editor(|editor, window, cx| {
7902 editor.add_selection_below(&Default::default(), window, cx);
7903 });
7904
7905 cx.assert_editor_state(indoc!(
7906 r#"abc
7907 def«ˇg»hi
7908
7909 jk
7910 nlm«ˇo»
7911 "#
7912 ));
7913
7914 cx.update_editor(|editor, window, cx| {
7915 editor.add_selection_above(&Default::default(), window, cx);
7916 });
7917
7918 cx.assert_editor_state(indoc!(
7919 r#"abc
7920 def«ˇg»hi
7921
7922 jk
7923 nlmo
7924 "#
7925 ));
7926
7927 cx.update_editor(|editor, window, cx| {
7928 editor.add_selection_above(&Default::default(), window, cx);
7929 });
7930
7931 cx.assert_editor_state(indoc!(
7932 r#"abc
7933 def«ˇg»hi
7934
7935 jk
7936 nlmo
7937 "#
7938 ));
7939
7940 // Change selections again
7941 cx.set_state(indoc!(
7942 r#"a«bc
7943 defgˇ»hi
7944
7945 jk
7946 nlmo
7947 "#
7948 ));
7949
7950 cx.update_editor(|editor, window, cx| {
7951 editor.add_selection_below(&Default::default(), window, cx);
7952 });
7953
7954 cx.assert_editor_state(indoc!(
7955 r#"a«bcˇ»
7956 d«efgˇ»hi
7957
7958 j«kˇ»
7959 nlmo
7960 "#
7961 ));
7962
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_below(&Default::default(), window, cx);
7965 });
7966 cx.assert_editor_state(indoc!(
7967 r#"a«bcˇ»
7968 d«efgˇ»hi
7969
7970 j«kˇ»
7971 n«lmoˇ»
7972 "#
7973 ));
7974 cx.update_editor(|editor, window, cx| {
7975 editor.add_selection_above(&Default::default(), window, cx);
7976 });
7977
7978 cx.assert_editor_state(indoc!(
7979 r#"a«bcˇ»
7980 d«efgˇ»hi
7981
7982 j«kˇ»
7983 nlmo
7984 "#
7985 ));
7986
7987 // Change selections again
7988 cx.set_state(indoc!(
7989 r#"abc
7990 d«ˇefghi
7991
7992 jk
7993 nlm»o
7994 "#
7995 ));
7996
7997 cx.update_editor(|editor, window, cx| {
7998 editor.add_selection_above(&Default::default(), window, cx);
7999 });
8000
8001 cx.assert_editor_state(indoc!(
8002 r#"a«ˇbc»
8003 d«ˇef»ghi
8004
8005 j«ˇk»
8006 n«ˇlm»o
8007 "#
8008 ));
8009
8010 cx.update_editor(|editor, window, cx| {
8011 editor.add_selection_below(&Default::default(), window, cx);
8012 });
8013
8014 cx.assert_editor_state(indoc!(
8015 r#"abc
8016 d«ˇef»ghi
8017
8018 j«ˇk»
8019 n«ˇlm»o
8020 "#
8021 ));
8022}
8023
8024#[gpui::test]
8025async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8026 init_test(cx, |_| {});
8027 let mut cx = EditorTestContext::new(cx).await;
8028
8029 cx.set_state(indoc!(
8030 r#"line onˇe
8031 liˇne two
8032 line three
8033 line four"#
8034 ));
8035
8036 cx.update_editor(|editor, window, cx| {
8037 editor.add_selection_below(&Default::default(), window, cx);
8038 });
8039
8040 // test multiple cursors expand in the same direction
8041 cx.assert_editor_state(indoc!(
8042 r#"line onˇe
8043 liˇne twˇo
8044 liˇne three
8045 line four"#
8046 ));
8047
8048 cx.update_editor(|editor, window, cx| {
8049 editor.add_selection_below(&Default::default(), window, cx);
8050 });
8051
8052 cx.update_editor(|editor, window, cx| {
8053 editor.add_selection_below(&Default::default(), window, cx);
8054 });
8055
8056 // test multiple cursors expand below overflow
8057 cx.assert_editor_state(indoc!(
8058 r#"line onˇe
8059 liˇne twˇo
8060 liˇne thˇree
8061 liˇne foˇur"#
8062 ));
8063
8064 cx.update_editor(|editor, window, cx| {
8065 editor.add_selection_above(&Default::default(), window, cx);
8066 });
8067
8068 // test multiple cursors retrieves back correctly
8069 cx.assert_editor_state(indoc!(
8070 r#"line onˇe
8071 liˇne twˇo
8072 liˇne thˇree
8073 line four"#
8074 ));
8075
8076 cx.update_editor(|editor, window, cx| {
8077 editor.add_selection_above(&Default::default(), window, cx);
8078 });
8079
8080 cx.update_editor(|editor, window, cx| {
8081 editor.add_selection_above(&Default::default(), window, cx);
8082 });
8083
8084 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8085 cx.assert_editor_state(indoc!(
8086 r#"liˇne onˇe
8087 liˇne two
8088 line three
8089 line four"#
8090 ));
8091
8092 cx.update_editor(|editor, window, cx| {
8093 editor.undo_selection(&Default::default(), window, cx);
8094 });
8095
8096 // test undo
8097 cx.assert_editor_state(indoc!(
8098 r#"line onˇe
8099 liˇne twˇo
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.redo_selection(&Default::default(), window, cx);
8106 });
8107
8108 // test redo
8109 cx.assert_editor_state(indoc!(
8110 r#"liˇne onˇe
8111 liˇne two
8112 line three
8113 line four"#
8114 ));
8115
8116 cx.set_state(indoc!(
8117 r#"abcd
8118 ef«ghˇ»
8119 ijkl
8120 «mˇ»nop"#
8121 ));
8122
8123 cx.update_editor(|editor, window, cx| {
8124 editor.add_selection_above(&Default::default(), window, cx);
8125 });
8126
8127 // test multiple selections expand in the same direction
8128 cx.assert_editor_state(indoc!(
8129 r#"ab«cdˇ»
8130 ef«ghˇ»
8131 «iˇ»jkl
8132 «mˇ»nop"#
8133 ));
8134
8135 cx.update_editor(|editor, window, cx| {
8136 editor.add_selection_above(&Default::default(), window, cx);
8137 });
8138
8139 // test multiple selection upward overflow
8140 cx.assert_editor_state(indoc!(
8141 r#"ab«cdˇ»
8142 «eˇ»f«ghˇ»
8143 «iˇ»jkl
8144 «mˇ»nop"#
8145 ));
8146
8147 cx.update_editor(|editor, window, cx| {
8148 editor.add_selection_below(&Default::default(), window, cx);
8149 });
8150
8151 // test multiple selection retrieves back correctly
8152 cx.assert_editor_state(indoc!(
8153 r#"abcd
8154 ef«ghˇ»
8155 «iˇ»jkl
8156 «mˇ»nop"#
8157 ));
8158
8159 cx.update_editor(|editor, window, cx| {
8160 editor.add_selection_below(&Default::default(), window, cx);
8161 });
8162
8163 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8164 cx.assert_editor_state(indoc!(
8165 r#"abcd
8166 ef«ghˇ»
8167 ij«klˇ»
8168 «mˇ»nop"#
8169 ));
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.undo_selection(&Default::default(), window, cx);
8173 });
8174
8175 // test undo
8176 cx.assert_editor_state(indoc!(
8177 r#"abcd
8178 ef«ghˇ»
8179 «iˇ»jkl
8180 «mˇ»nop"#
8181 ));
8182
8183 cx.update_editor(|editor, window, cx| {
8184 editor.redo_selection(&Default::default(), window, cx);
8185 });
8186
8187 // test redo
8188 cx.assert_editor_state(indoc!(
8189 r#"abcd
8190 ef«ghˇ»
8191 ij«klˇ»
8192 «mˇ»nop"#
8193 ));
8194}
8195
8196#[gpui::test]
8197async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8198 init_test(cx, |_| {});
8199 let mut cx = EditorTestContext::new(cx).await;
8200
8201 cx.set_state(indoc!(
8202 r#"line onˇe
8203 liˇne two
8204 line three
8205 line four"#
8206 ));
8207
8208 cx.update_editor(|editor, window, cx| {
8209 editor.add_selection_below(&Default::default(), window, cx);
8210 editor.add_selection_below(&Default::default(), window, cx);
8211 editor.add_selection_below(&Default::default(), window, cx);
8212 });
8213
8214 // initial state with two multi cursor groups
8215 cx.assert_editor_state(indoc!(
8216 r#"line onˇe
8217 liˇne twˇo
8218 liˇne thˇree
8219 liˇne foˇur"#
8220 ));
8221
8222 // add single cursor in middle - simulate opt click
8223 cx.update_editor(|editor, window, cx| {
8224 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8225 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8226 editor.end_selection(window, cx);
8227 });
8228
8229 cx.assert_editor_state(indoc!(
8230 r#"line onˇe
8231 liˇne twˇo
8232 liˇneˇ thˇree
8233 liˇne foˇur"#
8234 ));
8235
8236 cx.update_editor(|editor, window, cx| {
8237 editor.add_selection_above(&Default::default(), window, cx);
8238 });
8239
8240 // test new added selection expands above and existing selection shrinks
8241 cx.assert_editor_state(indoc!(
8242 r#"line onˇe
8243 liˇneˇ twˇo
8244 liˇneˇ thˇree
8245 line four"#
8246 ));
8247
8248 cx.update_editor(|editor, window, cx| {
8249 editor.add_selection_above(&Default::default(), window, cx);
8250 });
8251
8252 // test new added selection expands above and existing selection shrinks
8253 cx.assert_editor_state(indoc!(
8254 r#"lineˇ onˇe
8255 liˇneˇ twˇo
8256 lineˇ three
8257 line four"#
8258 ));
8259
8260 // intial state with two selection groups
8261 cx.set_state(indoc!(
8262 r#"abcd
8263 ef«ghˇ»
8264 ijkl
8265 «mˇ»nop"#
8266 ));
8267
8268 cx.update_editor(|editor, window, cx| {
8269 editor.add_selection_above(&Default::default(), window, cx);
8270 editor.add_selection_above(&Default::default(), window, cx);
8271 });
8272
8273 cx.assert_editor_state(indoc!(
8274 r#"ab«cdˇ»
8275 «eˇ»f«ghˇ»
8276 «iˇ»jkl
8277 «mˇ»nop"#
8278 ));
8279
8280 // add single selection in middle - simulate opt drag
8281 cx.update_editor(|editor, window, cx| {
8282 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8283 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8284 editor.update_selection(
8285 DisplayPoint::new(DisplayRow(2), 4),
8286 0,
8287 gpui::Point::<f32>::default(),
8288 window,
8289 cx,
8290 );
8291 editor.end_selection(window, cx);
8292 });
8293
8294 cx.assert_editor_state(indoc!(
8295 r#"ab«cdˇ»
8296 «eˇ»f«ghˇ»
8297 «iˇ»jk«lˇ»
8298 «mˇ»nop"#
8299 ));
8300
8301 cx.update_editor(|editor, window, cx| {
8302 editor.add_selection_below(&Default::default(), window, cx);
8303 });
8304
8305 // test new added selection expands below, others shrinks from above
8306 cx.assert_editor_state(indoc!(
8307 r#"abcd
8308 ef«ghˇ»
8309 «iˇ»jk«lˇ»
8310 «mˇ»no«pˇ»"#
8311 ));
8312}
8313
8314#[gpui::test]
8315async fn test_select_next(cx: &mut TestAppContext) {
8316 init_test(cx, |_| {});
8317 let mut cx = EditorTestContext::new(cx).await;
8318
8319 // Enable case sensitive search.
8320 update_test_editor_settings(&mut cx, |settings| {
8321 let mut search_settings = SearchSettingsContent::default();
8322 search_settings.case_sensitive = Some(true);
8323 settings.search = Some(search_settings);
8324 });
8325
8326 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8327
8328 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8329 .unwrap();
8330 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8331
8332 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8333 .unwrap();
8334 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8335
8336 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8337 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8338
8339 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8340 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8341
8342 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8343 .unwrap();
8344 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8345
8346 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8347 .unwrap();
8348 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8349
8350 // Test selection direction should be preserved
8351 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8352
8353 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8354 .unwrap();
8355 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8356
8357 // Test case sensitivity
8358 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8359 cx.update_editor(|e, window, cx| {
8360 e.select_next(&SelectNext::default(), window, cx).unwrap();
8361 });
8362 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8363
8364 // Disable case sensitive search.
8365 update_test_editor_settings(&mut cx, |settings| {
8366 let mut search_settings = SearchSettingsContent::default();
8367 search_settings.case_sensitive = Some(false);
8368 settings.search = Some(search_settings);
8369 });
8370
8371 cx.set_state("«ˇfoo»\nFOO\nFoo");
8372 cx.update_editor(|e, window, cx| {
8373 e.select_next(&SelectNext::default(), window, cx).unwrap();
8374 e.select_next(&SelectNext::default(), window, cx).unwrap();
8375 });
8376 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8377}
8378
8379#[gpui::test]
8380async fn test_select_all_matches(cx: &mut TestAppContext) {
8381 init_test(cx, |_| {});
8382 let mut cx = EditorTestContext::new(cx).await;
8383
8384 // Enable case sensitive search.
8385 update_test_editor_settings(&mut cx, |settings| {
8386 let mut search_settings = SearchSettingsContent::default();
8387 search_settings.case_sensitive = Some(true);
8388 settings.search = Some(search_settings);
8389 });
8390
8391 // Test caret-only selections
8392 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8393 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8394 .unwrap();
8395 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8396
8397 // Test left-to-right selections
8398 cx.set_state("abc\n«abcˇ»\nabc");
8399 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8400 .unwrap();
8401 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8402
8403 // Test right-to-left selections
8404 cx.set_state("abc\n«ˇabc»\nabc");
8405 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8406 .unwrap();
8407 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8408
8409 // Test selecting whitespace with caret selection
8410 cx.set_state("abc\nˇ abc\nabc");
8411 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8412 .unwrap();
8413 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8414
8415 // Test selecting whitespace with left-to-right selection
8416 cx.set_state("abc\n«ˇ »abc\nabc");
8417 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8418 .unwrap();
8419 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8420
8421 // Test no matches with right-to-left selection
8422 cx.set_state("abc\n« ˇ»abc\nabc");
8423 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8424 .unwrap();
8425 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8426
8427 // Test with a single word and clip_at_line_ends=true (#29823)
8428 cx.set_state("aˇbc");
8429 cx.update_editor(|e, window, cx| {
8430 e.set_clip_at_line_ends(true, cx);
8431 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8432 e.set_clip_at_line_ends(false, cx);
8433 });
8434 cx.assert_editor_state("«abcˇ»");
8435
8436 // Test case sensitivity
8437 cx.set_state("fˇoo\nFOO\nFoo");
8438 cx.update_editor(|e, window, cx| {
8439 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8440 });
8441 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8442
8443 // Disable case sensitive search.
8444 update_test_editor_settings(&mut cx, |settings| {
8445 let mut search_settings = SearchSettingsContent::default();
8446 search_settings.case_sensitive = Some(false);
8447 settings.search = Some(search_settings);
8448 });
8449
8450 cx.set_state("fˇoo\nFOO\nFoo");
8451 cx.update_editor(|e, window, cx| {
8452 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8453 });
8454 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8455}
8456
8457#[gpui::test]
8458async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8459 init_test(cx, |_| {});
8460
8461 let mut cx = EditorTestContext::new(cx).await;
8462
8463 let large_body_1 = "\nd".repeat(200);
8464 let large_body_2 = "\ne".repeat(200);
8465
8466 cx.set_state(&format!(
8467 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8468 ));
8469 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8470 let scroll_position = editor.scroll_position(cx);
8471 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8472 scroll_position
8473 });
8474
8475 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8476 .unwrap();
8477 cx.assert_editor_state(&format!(
8478 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8479 ));
8480 let scroll_position_after_selection =
8481 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8482 assert_eq!(
8483 initial_scroll_position, scroll_position_after_selection,
8484 "Scroll position should not change after selecting all matches"
8485 );
8486}
8487
8488#[gpui::test]
8489async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8490 init_test(cx, |_| {});
8491
8492 let mut cx = EditorLspTestContext::new_rust(
8493 lsp::ServerCapabilities {
8494 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8495 ..Default::default()
8496 },
8497 cx,
8498 )
8499 .await;
8500
8501 cx.set_state(indoc! {"
8502 line 1
8503 line 2
8504 linˇe 3
8505 line 4
8506 line 5
8507 "});
8508
8509 // Make an edit
8510 cx.update_editor(|editor, window, cx| {
8511 editor.handle_input("X", window, cx);
8512 });
8513
8514 // Move cursor to a different position
8515 cx.update_editor(|editor, window, cx| {
8516 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8517 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8518 });
8519 });
8520
8521 cx.assert_editor_state(indoc! {"
8522 line 1
8523 line 2
8524 linXe 3
8525 line 4
8526 liˇne 5
8527 "});
8528
8529 cx.lsp
8530 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8531 Ok(Some(vec![lsp::TextEdit::new(
8532 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8533 "PREFIX ".to_string(),
8534 )]))
8535 });
8536
8537 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8538 .unwrap()
8539 .await
8540 .unwrap();
8541
8542 cx.assert_editor_state(indoc! {"
8543 PREFIX line 1
8544 line 2
8545 linXe 3
8546 line 4
8547 liˇne 5
8548 "});
8549
8550 // Undo formatting
8551 cx.update_editor(|editor, window, cx| {
8552 editor.undo(&Default::default(), window, cx);
8553 });
8554
8555 // Verify cursor moved back to position after edit
8556 cx.assert_editor_state(indoc! {"
8557 line 1
8558 line 2
8559 linXˇe 3
8560 line 4
8561 line 5
8562 "});
8563}
8564
8565#[gpui::test]
8566async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8567 init_test(cx, |_| {});
8568
8569 let mut cx = EditorTestContext::new(cx).await;
8570
8571 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8572 cx.update_editor(|editor, window, cx| {
8573 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8574 });
8575
8576 cx.set_state(indoc! {"
8577 line 1
8578 line 2
8579 linˇe 3
8580 line 4
8581 line 5
8582 line 6
8583 line 7
8584 line 8
8585 line 9
8586 line 10
8587 "});
8588
8589 let snapshot = cx.buffer_snapshot();
8590 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8591
8592 cx.update(|_, cx| {
8593 provider.update(cx, |provider, _| {
8594 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8595 id: None,
8596 edits: vec![(edit_position..edit_position, "X".into())],
8597 edit_preview: None,
8598 }))
8599 })
8600 });
8601
8602 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8603 cx.update_editor(|editor, window, cx| {
8604 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8605 });
8606
8607 cx.assert_editor_state(indoc! {"
8608 line 1
8609 line 2
8610 lineXˇ 3
8611 line 4
8612 line 5
8613 line 6
8614 line 7
8615 line 8
8616 line 9
8617 line 10
8618 "});
8619
8620 cx.update_editor(|editor, window, cx| {
8621 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8622 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8623 });
8624 });
8625
8626 cx.assert_editor_state(indoc! {"
8627 line 1
8628 line 2
8629 lineX 3
8630 line 4
8631 line 5
8632 line 6
8633 line 7
8634 line 8
8635 line 9
8636 liˇne 10
8637 "});
8638
8639 cx.update_editor(|editor, window, cx| {
8640 editor.undo(&Default::default(), window, cx);
8641 });
8642
8643 cx.assert_editor_state(indoc! {"
8644 line 1
8645 line 2
8646 lineˇ 3
8647 line 4
8648 line 5
8649 line 6
8650 line 7
8651 line 8
8652 line 9
8653 line 10
8654 "});
8655}
8656
8657#[gpui::test]
8658async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8659 init_test(cx, |_| {});
8660
8661 let mut cx = EditorTestContext::new(cx).await;
8662 cx.set_state(
8663 r#"let foo = 2;
8664lˇet foo = 2;
8665let fooˇ = 2;
8666let foo = 2;
8667let foo = ˇ2;"#,
8668 );
8669
8670 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8671 .unwrap();
8672 cx.assert_editor_state(
8673 r#"let foo = 2;
8674«letˇ» foo = 2;
8675let «fooˇ» = 2;
8676let foo = 2;
8677let foo = «2ˇ»;"#,
8678 );
8679
8680 // noop for multiple selections with different contents
8681 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8682 .unwrap();
8683 cx.assert_editor_state(
8684 r#"let foo = 2;
8685«letˇ» foo = 2;
8686let «fooˇ» = 2;
8687let foo = 2;
8688let foo = «2ˇ»;"#,
8689 );
8690
8691 // Test last selection direction should be preserved
8692 cx.set_state(
8693 r#"let foo = 2;
8694let foo = 2;
8695let «fooˇ» = 2;
8696let «ˇfoo» = 2;
8697let foo = 2;"#,
8698 );
8699
8700 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8701 .unwrap();
8702 cx.assert_editor_state(
8703 r#"let foo = 2;
8704let foo = 2;
8705let «fooˇ» = 2;
8706let «ˇfoo» = 2;
8707let «ˇfoo» = 2;"#,
8708 );
8709}
8710
8711#[gpui::test]
8712async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8713 init_test(cx, |_| {});
8714
8715 let mut cx =
8716 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8717
8718 cx.assert_editor_state(indoc! {"
8719 ˇbbb
8720 ccc
8721
8722 bbb
8723 ccc
8724 "});
8725 cx.dispatch_action(SelectPrevious::default());
8726 cx.assert_editor_state(indoc! {"
8727 «bbbˇ»
8728 ccc
8729
8730 bbb
8731 ccc
8732 "});
8733 cx.dispatch_action(SelectPrevious::default());
8734 cx.assert_editor_state(indoc! {"
8735 «bbbˇ»
8736 ccc
8737
8738 «bbbˇ»
8739 ccc
8740 "});
8741}
8742
8743#[gpui::test]
8744async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8745 init_test(cx, |_| {});
8746
8747 let mut cx = EditorTestContext::new(cx).await;
8748 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8749
8750 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8751 .unwrap();
8752 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8753
8754 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8755 .unwrap();
8756 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8757
8758 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8759 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8760
8761 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8762 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8763
8764 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8765 .unwrap();
8766 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8767
8768 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8769 .unwrap();
8770 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8771}
8772
8773#[gpui::test]
8774async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8775 init_test(cx, |_| {});
8776
8777 let mut cx = EditorTestContext::new(cx).await;
8778 cx.set_state("aˇ");
8779
8780 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8781 .unwrap();
8782 cx.assert_editor_state("«aˇ»");
8783 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8784 .unwrap();
8785 cx.assert_editor_state("«aˇ»");
8786}
8787
8788#[gpui::test]
8789async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8790 init_test(cx, |_| {});
8791
8792 let mut cx = EditorTestContext::new(cx).await;
8793 cx.set_state(
8794 r#"let foo = 2;
8795lˇet foo = 2;
8796let fooˇ = 2;
8797let foo = 2;
8798let foo = ˇ2;"#,
8799 );
8800
8801 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8802 .unwrap();
8803 cx.assert_editor_state(
8804 r#"let foo = 2;
8805«letˇ» foo = 2;
8806let «fooˇ» = 2;
8807let foo = 2;
8808let foo = «2ˇ»;"#,
8809 );
8810
8811 // noop for multiple selections with different contents
8812 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8813 .unwrap();
8814 cx.assert_editor_state(
8815 r#"let foo = 2;
8816«letˇ» foo = 2;
8817let «fooˇ» = 2;
8818let foo = 2;
8819let foo = «2ˇ»;"#,
8820 );
8821}
8822
8823#[gpui::test]
8824async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8825 init_test(cx, |_| {});
8826 let mut cx = EditorTestContext::new(cx).await;
8827
8828 // Enable case sensitive search.
8829 update_test_editor_settings(&mut cx, |settings| {
8830 let mut search_settings = SearchSettingsContent::default();
8831 search_settings.case_sensitive = Some(true);
8832 settings.search = Some(search_settings);
8833 });
8834
8835 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8836
8837 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8838 .unwrap();
8839 // selection direction is preserved
8840 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8841
8842 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8843 .unwrap();
8844 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8845
8846 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8847 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8848
8849 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8850 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8851
8852 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8853 .unwrap();
8854 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8855
8856 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8857 .unwrap();
8858 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8859
8860 // Test case sensitivity
8861 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
8862 cx.update_editor(|e, window, cx| {
8863 e.select_previous(&SelectPrevious::default(), window, cx)
8864 .unwrap();
8865 e.select_previous(&SelectPrevious::default(), window, cx)
8866 .unwrap();
8867 });
8868 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8869
8870 // Disable case sensitive search.
8871 update_test_editor_settings(&mut cx, |settings| {
8872 let mut search_settings = SearchSettingsContent::default();
8873 search_settings.case_sensitive = Some(false);
8874 settings.search = Some(search_settings);
8875 });
8876
8877 cx.set_state("foo\nFOO\n«ˇFoo»");
8878 cx.update_editor(|e, window, cx| {
8879 e.select_previous(&SelectPrevious::default(), window, cx)
8880 .unwrap();
8881 e.select_previous(&SelectPrevious::default(), window, cx)
8882 .unwrap();
8883 });
8884 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8885}
8886
8887#[gpui::test]
8888async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8889 init_test(cx, |_| {});
8890
8891 let language = Arc::new(Language::new(
8892 LanguageConfig::default(),
8893 Some(tree_sitter_rust::LANGUAGE.into()),
8894 ));
8895
8896 let text = r#"
8897 use mod1::mod2::{mod3, mod4};
8898
8899 fn fn_1(param1: bool, param2: &str) {
8900 let var1 = "text";
8901 }
8902 "#
8903 .unindent();
8904
8905 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8906 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8907 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8908
8909 editor
8910 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8911 .await;
8912
8913 editor.update_in(cx, |editor, window, cx| {
8914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8915 s.select_display_ranges([
8916 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8917 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8918 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8919 ]);
8920 });
8921 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8922 });
8923 editor.update(cx, |editor, cx| {
8924 assert_text_with_selections(
8925 editor,
8926 indoc! {r#"
8927 use mod1::mod2::{mod3, «mod4ˇ»};
8928
8929 fn fn_1«ˇ(param1: bool, param2: &str)» {
8930 let var1 = "«ˇtext»";
8931 }
8932 "#},
8933 cx,
8934 );
8935 });
8936
8937 editor.update_in(cx, |editor, window, cx| {
8938 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8939 });
8940 editor.update(cx, |editor, cx| {
8941 assert_text_with_selections(
8942 editor,
8943 indoc! {r#"
8944 use mod1::mod2::«{mod3, mod4}ˇ»;
8945
8946 «ˇfn fn_1(param1: bool, param2: &str) {
8947 let var1 = "text";
8948 }»
8949 "#},
8950 cx,
8951 );
8952 });
8953
8954 editor.update_in(cx, |editor, window, cx| {
8955 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8956 });
8957 assert_eq!(
8958 editor.update(cx, |editor, cx| editor
8959 .selections
8960 .display_ranges(&editor.display_snapshot(cx))),
8961 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8962 );
8963
8964 // Trying to expand the selected syntax node one more time has no effect.
8965 editor.update_in(cx, |editor, window, cx| {
8966 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8967 });
8968 assert_eq!(
8969 editor.update(cx, |editor, cx| editor
8970 .selections
8971 .display_ranges(&editor.display_snapshot(cx))),
8972 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8973 );
8974
8975 editor.update_in(cx, |editor, window, cx| {
8976 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8977 });
8978 editor.update(cx, |editor, cx| {
8979 assert_text_with_selections(
8980 editor,
8981 indoc! {r#"
8982 use mod1::mod2::«{mod3, mod4}ˇ»;
8983
8984 «ˇfn fn_1(param1: bool, param2: &str) {
8985 let var1 = "text";
8986 }»
8987 "#},
8988 cx,
8989 );
8990 });
8991
8992 editor.update_in(cx, |editor, window, cx| {
8993 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8994 });
8995 editor.update(cx, |editor, cx| {
8996 assert_text_with_selections(
8997 editor,
8998 indoc! {r#"
8999 use mod1::mod2::{mod3, «mod4ˇ»};
9000
9001 fn fn_1«ˇ(param1: bool, param2: &str)» {
9002 let var1 = "«ˇtext»";
9003 }
9004 "#},
9005 cx,
9006 );
9007 });
9008
9009 editor.update_in(cx, |editor, window, cx| {
9010 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9011 });
9012 editor.update(cx, |editor, cx| {
9013 assert_text_with_selections(
9014 editor,
9015 indoc! {r#"
9016 use mod1::mod2::{mod3, moˇd4};
9017
9018 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9019 let var1 = "teˇxt";
9020 }
9021 "#},
9022 cx,
9023 );
9024 });
9025
9026 // Trying to shrink the selected syntax node one more time has no effect.
9027 editor.update_in(cx, |editor, window, cx| {
9028 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9029 });
9030 editor.update_in(cx, |editor, _, cx| {
9031 assert_text_with_selections(
9032 editor,
9033 indoc! {r#"
9034 use mod1::mod2::{mod3, moˇd4};
9035
9036 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9037 let var1 = "teˇxt";
9038 }
9039 "#},
9040 cx,
9041 );
9042 });
9043
9044 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9045 // a fold.
9046 editor.update_in(cx, |editor, window, cx| {
9047 editor.fold_creases(
9048 vec![
9049 Crease::simple(
9050 Point::new(0, 21)..Point::new(0, 24),
9051 FoldPlaceholder::test(),
9052 ),
9053 Crease::simple(
9054 Point::new(3, 20)..Point::new(3, 22),
9055 FoldPlaceholder::test(),
9056 ),
9057 ],
9058 true,
9059 window,
9060 cx,
9061 );
9062 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9063 });
9064 editor.update(cx, |editor, cx| {
9065 assert_text_with_selections(
9066 editor,
9067 indoc! {r#"
9068 use mod1::mod2::«{mod3, mod4}ˇ»;
9069
9070 fn fn_1«ˇ(param1: bool, param2: &str)» {
9071 let var1 = "«ˇtext»";
9072 }
9073 "#},
9074 cx,
9075 );
9076 });
9077}
9078
9079#[gpui::test]
9080async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9081 init_test(cx, |_| {});
9082
9083 let language = Arc::new(Language::new(
9084 LanguageConfig::default(),
9085 Some(tree_sitter_rust::LANGUAGE.into()),
9086 ));
9087
9088 let text = "let a = 2;";
9089
9090 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9091 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9092 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9093
9094 editor
9095 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9096 .await;
9097
9098 // Test case 1: Cursor at end of word
9099 editor.update_in(cx, |editor, window, cx| {
9100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9101 s.select_display_ranges([
9102 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9103 ]);
9104 });
9105 });
9106 editor.update(cx, |editor, cx| {
9107 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9108 });
9109 editor.update_in(cx, |editor, window, cx| {
9110 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9111 });
9112 editor.update(cx, |editor, cx| {
9113 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9114 });
9115 editor.update_in(cx, |editor, window, cx| {
9116 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9117 });
9118 editor.update(cx, |editor, cx| {
9119 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9120 });
9121
9122 // Test case 2: Cursor at end of statement
9123 editor.update_in(cx, |editor, window, cx| {
9124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9125 s.select_display_ranges([
9126 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9127 ]);
9128 });
9129 });
9130 editor.update(cx, |editor, cx| {
9131 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9132 });
9133 editor.update_in(cx, |editor, window, cx| {
9134 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9135 });
9136 editor.update(cx, |editor, cx| {
9137 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9138 });
9139}
9140
9141#[gpui::test]
9142async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9143 init_test(cx, |_| {});
9144
9145 let language = Arc::new(Language::new(
9146 LanguageConfig {
9147 name: "JavaScript".into(),
9148 ..Default::default()
9149 },
9150 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9151 ));
9152
9153 let text = r#"
9154 let a = {
9155 key: "value",
9156 };
9157 "#
9158 .unindent();
9159
9160 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9161 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9162 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9163
9164 editor
9165 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9166 .await;
9167
9168 // Test case 1: Cursor after '{'
9169 editor.update_in(cx, |editor, window, cx| {
9170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9171 s.select_display_ranges([
9172 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9173 ]);
9174 });
9175 });
9176 editor.update(cx, |editor, cx| {
9177 assert_text_with_selections(
9178 editor,
9179 indoc! {r#"
9180 let a = {ˇ
9181 key: "value",
9182 };
9183 "#},
9184 cx,
9185 );
9186 });
9187 editor.update_in(cx, |editor, window, cx| {
9188 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9189 });
9190 editor.update(cx, |editor, cx| {
9191 assert_text_with_selections(
9192 editor,
9193 indoc! {r#"
9194 let a = «ˇ{
9195 key: "value",
9196 }»;
9197 "#},
9198 cx,
9199 );
9200 });
9201
9202 // Test case 2: Cursor after ':'
9203 editor.update_in(cx, |editor, window, cx| {
9204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9205 s.select_display_ranges([
9206 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9207 ]);
9208 });
9209 });
9210 editor.update(cx, |editor, cx| {
9211 assert_text_with_selections(
9212 editor,
9213 indoc! {r#"
9214 let a = {
9215 key:ˇ "value",
9216 };
9217 "#},
9218 cx,
9219 );
9220 });
9221 editor.update_in(cx, |editor, window, cx| {
9222 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9223 });
9224 editor.update(cx, |editor, cx| {
9225 assert_text_with_selections(
9226 editor,
9227 indoc! {r#"
9228 let a = {
9229 «ˇkey: "value"»,
9230 };
9231 "#},
9232 cx,
9233 );
9234 });
9235 editor.update_in(cx, |editor, window, cx| {
9236 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9237 });
9238 editor.update(cx, |editor, cx| {
9239 assert_text_with_selections(
9240 editor,
9241 indoc! {r#"
9242 let a = «ˇ{
9243 key: "value",
9244 }»;
9245 "#},
9246 cx,
9247 );
9248 });
9249
9250 // Test case 3: Cursor after ','
9251 editor.update_in(cx, |editor, window, cx| {
9252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9253 s.select_display_ranges([
9254 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9255 ]);
9256 });
9257 });
9258 editor.update(cx, |editor, cx| {
9259 assert_text_with_selections(
9260 editor,
9261 indoc! {r#"
9262 let a = {
9263 key: "value",ˇ
9264 };
9265 "#},
9266 cx,
9267 );
9268 });
9269 editor.update_in(cx, |editor, window, cx| {
9270 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9271 });
9272 editor.update(cx, |editor, cx| {
9273 assert_text_with_selections(
9274 editor,
9275 indoc! {r#"
9276 let a = «ˇ{
9277 key: "value",
9278 }»;
9279 "#},
9280 cx,
9281 );
9282 });
9283
9284 // Test case 4: Cursor after ';'
9285 editor.update_in(cx, |editor, window, cx| {
9286 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9287 s.select_display_ranges([
9288 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9289 ]);
9290 });
9291 });
9292 editor.update(cx, |editor, cx| {
9293 assert_text_with_selections(
9294 editor,
9295 indoc! {r#"
9296 let a = {
9297 key: "value",
9298 };ˇ
9299 "#},
9300 cx,
9301 );
9302 });
9303 editor.update_in(cx, |editor, window, cx| {
9304 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9305 });
9306 editor.update(cx, |editor, cx| {
9307 assert_text_with_selections(
9308 editor,
9309 indoc! {r#"
9310 «ˇlet a = {
9311 key: "value",
9312 };
9313 »"#},
9314 cx,
9315 );
9316 });
9317}
9318
9319#[gpui::test]
9320async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9321 init_test(cx, |_| {});
9322
9323 let language = Arc::new(Language::new(
9324 LanguageConfig::default(),
9325 Some(tree_sitter_rust::LANGUAGE.into()),
9326 ));
9327
9328 let text = r#"
9329 use mod1::mod2::{mod3, mod4};
9330
9331 fn fn_1(param1: bool, param2: &str) {
9332 let var1 = "hello world";
9333 }
9334 "#
9335 .unindent();
9336
9337 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9339 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9340
9341 editor
9342 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9343 .await;
9344
9345 // Test 1: Cursor on a letter of a string word
9346 editor.update_in(cx, |editor, window, cx| {
9347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9348 s.select_display_ranges([
9349 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9350 ]);
9351 });
9352 });
9353 editor.update_in(cx, |editor, window, cx| {
9354 assert_text_with_selections(
9355 editor,
9356 indoc! {r#"
9357 use mod1::mod2::{mod3, mod4};
9358
9359 fn fn_1(param1: bool, param2: &str) {
9360 let var1 = "hˇello world";
9361 }
9362 "#},
9363 cx,
9364 );
9365 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9366 assert_text_with_selections(
9367 editor,
9368 indoc! {r#"
9369 use mod1::mod2::{mod3, mod4};
9370
9371 fn fn_1(param1: bool, param2: &str) {
9372 let var1 = "«ˇhello» world";
9373 }
9374 "#},
9375 cx,
9376 );
9377 });
9378
9379 // Test 2: Partial selection within a word
9380 editor.update_in(cx, |editor, window, cx| {
9381 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9382 s.select_display_ranges([
9383 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9384 ]);
9385 });
9386 });
9387 editor.update_in(cx, |editor, window, cx| {
9388 assert_text_with_selections(
9389 editor,
9390 indoc! {r#"
9391 use mod1::mod2::{mod3, mod4};
9392
9393 fn fn_1(param1: bool, param2: &str) {
9394 let var1 = "h«elˇ»lo world";
9395 }
9396 "#},
9397 cx,
9398 );
9399 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9400 assert_text_with_selections(
9401 editor,
9402 indoc! {r#"
9403 use mod1::mod2::{mod3, mod4};
9404
9405 fn fn_1(param1: bool, param2: &str) {
9406 let var1 = "«ˇhello» world";
9407 }
9408 "#},
9409 cx,
9410 );
9411 });
9412
9413 // Test 3: Complete word already selected
9414 editor.update_in(cx, |editor, window, cx| {
9415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9416 s.select_display_ranges([
9417 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9418 ]);
9419 });
9420 });
9421 editor.update_in(cx, |editor, window, cx| {
9422 assert_text_with_selections(
9423 editor,
9424 indoc! {r#"
9425 use mod1::mod2::{mod3, mod4};
9426
9427 fn fn_1(param1: bool, param2: &str) {
9428 let var1 = "«helloˇ» world";
9429 }
9430 "#},
9431 cx,
9432 );
9433 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9434 assert_text_with_selections(
9435 editor,
9436 indoc! {r#"
9437 use mod1::mod2::{mod3, mod4};
9438
9439 fn fn_1(param1: bool, param2: &str) {
9440 let var1 = "«hello worldˇ»";
9441 }
9442 "#},
9443 cx,
9444 );
9445 });
9446
9447 // Test 4: Selection spanning across words
9448 editor.update_in(cx, |editor, window, cx| {
9449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9450 s.select_display_ranges([
9451 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9452 ]);
9453 });
9454 });
9455 editor.update_in(cx, |editor, window, cx| {
9456 assert_text_with_selections(
9457 editor,
9458 indoc! {r#"
9459 use mod1::mod2::{mod3, mod4};
9460
9461 fn fn_1(param1: bool, param2: &str) {
9462 let var1 = "hel«lo woˇ»rld";
9463 }
9464 "#},
9465 cx,
9466 );
9467 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9468 assert_text_with_selections(
9469 editor,
9470 indoc! {r#"
9471 use mod1::mod2::{mod3, mod4};
9472
9473 fn fn_1(param1: bool, param2: &str) {
9474 let var1 = "«ˇhello world»";
9475 }
9476 "#},
9477 cx,
9478 );
9479 });
9480
9481 // Test 5: Expansion beyond string
9482 editor.update_in(cx, |editor, window, cx| {
9483 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9485 assert_text_with_selections(
9486 editor,
9487 indoc! {r#"
9488 use mod1::mod2::{mod3, mod4};
9489
9490 fn fn_1(param1: bool, param2: &str) {
9491 «ˇlet var1 = "hello world";»
9492 }
9493 "#},
9494 cx,
9495 );
9496 });
9497}
9498
9499#[gpui::test]
9500async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9501 init_test(cx, |_| {});
9502
9503 let mut cx = EditorTestContext::new(cx).await;
9504
9505 let language = Arc::new(Language::new(
9506 LanguageConfig::default(),
9507 Some(tree_sitter_rust::LANGUAGE.into()),
9508 ));
9509
9510 cx.update_buffer(|buffer, cx| {
9511 buffer.set_language(Some(language), cx);
9512 });
9513
9514 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9515 cx.update_editor(|editor, window, cx| {
9516 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9517 });
9518
9519 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9520
9521 cx.set_state(indoc! { r#"fn a() {
9522 // what
9523 // a
9524 // ˇlong
9525 // method
9526 // I
9527 // sure
9528 // hope
9529 // it
9530 // works
9531 }"# });
9532
9533 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9534 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9535 cx.update(|_, cx| {
9536 multi_buffer.update(cx, |multi_buffer, cx| {
9537 multi_buffer.set_excerpts_for_path(
9538 PathKey::for_buffer(&buffer, cx),
9539 buffer,
9540 [Point::new(1, 0)..Point::new(1, 0)],
9541 3,
9542 cx,
9543 );
9544 });
9545 });
9546
9547 let editor2 = cx.new_window_entity(|window, cx| {
9548 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9549 });
9550
9551 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9552 cx.update_editor(|editor, window, cx| {
9553 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9554 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9555 })
9556 });
9557
9558 cx.assert_editor_state(indoc! { "
9559 fn a() {
9560 // what
9561 // a
9562 ˇ // long
9563 // method"});
9564
9565 cx.update_editor(|editor, window, cx| {
9566 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9567 });
9568
9569 // Although we could potentially make the action work when the syntax node
9570 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9571 // did. Maybe we could also expand the excerpt to contain the range?
9572 cx.assert_editor_state(indoc! { "
9573 fn a() {
9574 // what
9575 // a
9576 ˇ // long
9577 // method"});
9578}
9579
9580#[gpui::test]
9581async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9582 init_test(cx, |_| {});
9583
9584 let base_text = r#"
9585 impl A {
9586 // this is an uncommitted comment
9587
9588 fn b() {
9589 c();
9590 }
9591
9592 // this is another uncommitted comment
9593
9594 fn d() {
9595 // e
9596 // f
9597 }
9598 }
9599
9600 fn g() {
9601 // h
9602 }
9603 "#
9604 .unindent();
9605
9606 let text = r#"
9607 ˇimpl A {
9608
9609 fn b() {
9610 c();
9611 }
9612
9613 fn d() {
9614 // e
9615 // f
9616 }
9617 }
9618
9619 fn g() {
9620 // h
9621 }
9622 "#
9623 .unindent();
9624
9625 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9626 cx.set_state(&text);
9627 cx.set_head_text(&base_text);
9628 cx.update_editor(|editor, window, cx| {
9629 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9630 });
9631
9632 cx.assert_state_with_diff(
9633 "
9634 ˇimpl A {
9635 - // this is an uncommitted comment
9636
9637 fn b() {
9638 c();
9639 }
9640
9641 - // this is another uncommitted comment
9642 -
9643 fn d() {
9644 // e
9645 // f
9646 }
9647 }
9648
9649 fn g() {
9650 // h
9651 }
9652 "
9653 .unindent(),
9654 );
9655
9656 let expected_display_text = "
9657 impl A {
9658 // this is an uncommitted comment
9659
9660 fn b() {
9661 ⋯
9662 }
9663
9664 // this is another uncommitted comment
9665
9666 fn d() {
9667 ⋯
9668 }
9669 }
9670
9671 fn g() {
9672 ⋯
9673 }
9674 "
9675 .unindent();
9676
9677 cx.update_editor(|editor, window, cx| {
9678 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9679 assert_eq!(editor.display_text(cx), expected_display_text);
9680 });
9681}
9682
9683#[gpui::test]
9684async fn test_autoindent(cx: &mut TestAppContext) {
9685 init_test(cx, |_| {});
9686
9687 let language = Arc::new(
9688 Language::new(
9689 LanguageConfig {
9690 brackets: BracketPairConfig {
9691 pairs: vec![
9692 BracketPair {
9693 start: "{".to_string(),
9694 end: "}".to_string(),
9695 close: false,
9696 surround: false,
9697 newline: true,
9698 },
9699 BracketPair {
9700 start: "(".to_string(),
9701 end: ")".to_string(),
9702 close: false,
9703 surround: false,
9704 newline: true,
9705 },
9706 ],
9707 ..Default::default()
9708 },
9709 ..Default::default()
9710 },
9711 Some(tree_sitter_rust::LANGUAGE.into()),
9712 )
9713 .with_indents_query(
9714 r#"
9715 (_ "(" ")" @end) @indent
9716 (_ "{" "}" @end) @indent
9717 "#,
9718 )
9719 .unwrap(),
9720 );
9721
9722 let text = "fn a() {}";
9723
9724 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9725 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9726 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9727 editor
9728 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9729 .await;
9730
9731 editor.update_in(cx, |editor, window, cx| {
9732 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9733 s.select_ranges([5..5, 8..8, 9..9])
9734 });
9735 editor.newline(&Newline, window, cx);
9736 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9737 assert_eq!(
9738 editor.selections.ranges(&editor.display_snapshot(cx)),
9739 &[
9740 Point::new(1, 4)..Point::new(1, 4),
9741 Point::new(3, 4)..Point::new(3, 4),
9742 Point::new(5, 0)..Point::new(5, 0)
9743 ]
9744 );
9745 });
9746}
9747
9748#[gpui::test]
9749async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9750 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9751
9752 let language = Arc::new(
9753 Language::new(
9754 LanguageConfig {
9755 brackets: BracketPairConfig {
9756 pairs: vec![
9757 BracketPair {
9758 start: "{".to_string(),
9759 end: "}".to_string(),
9760 close: false,
9761 surround: false,
9762 newline: true,
9763 },
9764 BracketPair {
9765 start: "(".to_string(),
9766 end: ")".to_string(),
9767 close: false,
9768 surround: false,
9769 newline: true,
9770 },
9771 ],
9772 ..Default::default()
9773 },
9774 ..Default::default()
9775 },
9776 Some(tree_sitter_rust::LANGUAGE.into()),
9777 )
9778 .with_indents_query(
9779 r#"
9780 (_ "(" ")" @end) @indent
9781 (_ "{" "}" @end) @indent
9782 "#,
9783 )
9784 .unwrap(),
9785 );
9786
9787 let text = "fn a() {}";
9788
9789 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9790 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9791 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9792 editor
9793 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9794 .await;
9795
9796 editor.update_in(cx, |editor, window, cx| {
9797 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9798 s.select_ranges([5..5, 8..8, 9..9])
9799 });
9800 editor.newline(&Newline, window, cx);
9801 assert_eq!(
9802 editor.text(cx),
9803 indoc!(
9804 "
9805 fn a(
9806
9807 ) {
9808
9809 }
9810 "
9811 )
9812 );
9813 assert_eq!(
9814 editor.selections.ranges(&editor.display_snapshot(cx)),
9815 &[
9816 Point::new(1, 0)..Point::new(1, 0),
9817 Point::new(3, 0)..Point::new(3, 0),
9818 Point::new(5, 0)..Point::new(5, 0)
9819 ]
9820 );
9821 });
9822}
9823
9824#[gpui::test]
9825async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9826 init_test(cx, |settings| {
9827 settings.defaults.auto_indent = Some(true);
9828 settings.languages.0.insert(
9829 "python".into(),
9830 LanguageSettingsContent {
9831 auto_indent: Some(false),
9832 ..Default::default()
9833 },
9834 );
9835 });
9836
9837 let mut cx = EditorTestContext::new(cx).await;
9838
9839 let injected_language = Arc::new(
9840 Language::new(
9841 LanguageConfig {
9842 brackets: BracketPairConfig {
9843 pairs: vec![
9844 BracketPair {
9845 start: "{".to_string(),
9846 end: "}".to_string(),
9847 close: false,
9848 surround: false,
9849 newline: true,
9850 },
9851 BracketPair {
9852 start: "(".to_string(),
9853 end: ")".to_string(),
9854 close: true,
9855 surround: false,
9856 newline: true,
9857 },
9858 ],
9859 ..Default::default()
9860 },
9861 name: "python".into(),
9862 ..Default::default()
9863 },
9864 Some(tree_sitter_python::LANGUAGE.into()),
9865 )
9866 .with_indents_query(
9867 r#"
9868 (_ "(" ")" @end) @indent
9869 (_ "{" "}" @end) @indent
9870 "#,
9871 )
9872 .unwrap(),
9873 );
9874
9875 let language = Arc::new(
9876 Language::new(
9877 LanguageConfig {
9878 brackets: BracketPairConfig {
9879 pairs: vec![
9880 BracketPair {
9881 start: "{".to_string(),
9882 end: "}".to_string(),
9883 close: false,
9884 surround: false,
9885 newline: true,
9886 },
9887 BracketPair {
9888 start: "(".to_string(),
9889 end: ")".to_string(),
9890 close: true,
9891 surround: false,
9892 newline: true,
9893 },
9894 ],
9895 ..Default::default()
9896 },
9897 name: LanguageName::new("rust"),
9898 ..Default::default()
9899 },
9900 Some(tree_sitter_rust::LANGUAGE.into()),
9901 )
9902 .with_indents_query(
9903 r#"
9904 (_ "(" ")" @end) @indent
9905 (_ "{" "}" @end) @indent
9906 "#,
9907 )
9908 .unwrap()
9909 .with_injection_query(
9910 r#"
9911 (macro_invocation
9912 macro: (identifier) @_macro_name
9913 (token_tree) @injection.content
9914 (#set! injection.language "python"))
9915 "#,
9916 )
9917 .unwrap(),
9918 );
9919
9920 cx.language_registry().add(injected_language);
9921 cx.language_registry().add(language.clone());
9922
9923 cx.update_buffer(|buffer, cx| {
9924 buffer.set_language(Some(language), cx);
9925 });
9926
9927 cx.set_state(r#"struct A {ˇ}"#);
9928
9929 cx.update_editor(|editor, window, cx| {
9930 editor.newline(&Default::default(), window, cx);
9931 });
9932
9933 cx.assert_editor_state(indoc!(
9934 "struct A {
9935 ˇ
9936 }"
9937 ));
9938
9939 cx.set_state(r#"select_biased!(ˇ)"#);
9940
9941 cx.update_editor(|editor, window, cx| {
9942 editor.newline(&Default::default(), window, cx);
9943 editor.handle_input("def ", window, cx);
9944 editor.handle_input("(", window, cx);
9945 editor.newline(&Default::default(), window, cx);
9946 editor.handle_input("a", window, cx);
9947 });
9948
9949 cx.assert_editor_state(indoc!(
9950 "select_biased!(
9951 def (
9952 aˇ
9953 )
9954 )"
9955 ));
9956}
9957
9958#[gpui::test]
9959async fn test_autoindent_selections(cx: &mut TestAppContext) {
9960 init_test(cx, |_| {});
9961
9962 {
9963 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9964 cx.set_state(indoc! {"
9965 impl A {
9966
9967 fn b() {}
9968
9969 «fn c() {
9970
9971 }ˇ»
9972 }
9973 "});
9974
9975 cx.update_editor(|editor, window, cx| {
9976 editor.autoindent(&Default::default(), window, cx);
9977 });
9978
9979 cx.assert_editor_state(indoc! {"
9980 impl A {
9981
9982 fn b() {}
9983
9984 «fn c() {
9985
9986 }ˇ»
9987 }
9988 "});
9989 }
9990
9991 {
9992 let mut cx = EditorTestContext::new_multibuffer(
9993 cx,
9994 [indoc! { "
9995 impl A {
9996 «
9997 // a
9998 fn b(){}
9999 »
10000 «
10001 }
10002 fn c(){}
10003 »
10004 "}],
10005 );
10006
10007 let buffer = cx.update_editor(|editor, _, cx| {
10008 let buffer = editor.buffer().update(cx, |buffer, _| {
10009 buffer.all_buffers().iter().next().unwrap().clone()
10010 });
10011 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10012 buffer
10013 });
10014
10015 cx.run_until_parked();
10016 cx.update_editor(|editor, window, cx| {
10017 editor.select_all(&Default::default(), window, cx);
10018 editor.autoindent(&Default::default(), window, cx)
10019 });
10020 cx.run_until_parked();
10021
10022 cx.update(|_, cx| {
10023 assert_eq!(
10024 buffer.read(cx).text(),
10025 indoc! { "
10026 impl A {
10027
10028 // a
10029 fn b(){}
10030
10031
10032 }
10033 fn c(){}
10034
10035 " }
10036 )
10037 });
10038 }
10039}
10040
10041#[gpui::test]
10042async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10043 init_test(cx, |_| {});
10044
10045 let mut cx = EditorTestContext::new(cx).await;
10046
10047 let language = Arc::new(Language::new(
10048 LanguageConfig {
10049 brackets: BracketPairConfig {
10050 pairs: vec![
10051 BracketPair {
10052 start: "{".to_string(),
10053 end: "}".to_string(),
10054 close: true,
10055 surround: true,
10056 newline: true,
10057 },
10058 BracketPair {
10059 start: "(".to_string(),
10060 end: ")".to_string(),
10061 close: true,
10062 surround: true,
10063 newline: true,
10064 },
10065 BracketPair {
10066 start: "/*".to_string(),
10067 end: " */".to_string(),
10068 close: true,
10069 surround: true,
10070 newline: true,
10071 },
10072 BracketPair {
10073 start: "[".to_string(),
10074 end: "]".to_string(),
10075 close: false,
10076 surround: false,
10077 newline: true,
10078 },
10079 BracketPair {
10080 start: "\"".to_string(),
10081 end: "\"".to_string(),
10082 close: true,
10083 surround: true,
10084 newline: false,
10085 },
10086 BracketPair {
10087 start: "<".to_string(),
10088 end: ">".to_string(),
10089 close: false,
10090 surround: true,
10091 newline: true,
10092 },
10093 ],
10094 ..Default::default()
10095 },
10096 autoclose_before: "})]".to_string(),
10097 ..Default::default()
10098 },
10099 Some(tree_sitter_rust::LANGUAGE.into()),
10100 ));
10101
10102 cx.language_registry().add(language.clone());
10103 cx.update_buffer(|buffer, cx| {
10104 buffer.set_language(Some(language), cx);
10105 });
10106
10107 cx.set_state(
10108 &r#"
10109 🏀ˇ
10110 εˇ
10111 ❤️ˇ
10112 "#
10113 .unindent(),
10114 );
10115
10116 // autoclose multiple nested brackets at multiple cursors
10117 cx.update_editor(|editor, window, cx| {
10118 editor.handle_input("{", window, cx);
10119 editor.handle_input("{", window, cx);
10120 editor.handle_input("{", window, cx);
10121 });
10122 cx.assert_editor_state(
10123 &"
10124 🏀{{{ˇ}}}
10125 ε{{{ˇ}}}
10126 ❤️{{{ˇ}}}
10127 "
10128 .unindent(),
10129 );
10130
10131 // insert a different closing bracket
10132 cx.update_editor(|editor, window, cx| {
10133 editor.handle_input(")", window, cx);
10134 });
10135 cx.assert_editor_state(
10136 &"
10137 🏀{{{)ˇ}}}
10138 ε{{{)ˇ}}}
10139 ❤️{{{)ˇ}}}
10140 "
10141 .unindent(),
10142 );
10143
10144 // skip over the auto-closed brackets when typing a closing bracket
10145 cx.update_editor(|editor, window, cx| {
10146 editor.move_right(&MoveRight, window, cx);
10147 editor.handle_input("}", window, cx);
10148 editor.handle_input("}", window, cx);
10149 editor.handle_input("}", window, cx);
10150 });
10151 cx.assert_editor_state(
10152 &"
10153 🏀{{{)}}}}ˇ
10154 ε{{{)}}}}ˇ
10155 ❤️{{{)}}}}ˇ
10156 "
10157 .unindent(),
10158 );
10159
10160 // autoclose multi-character pairs
10161 cx.set_state(
10162 &"
10163 ˇ
10164 ˇ
10165 "
10166 .unindent(),
10167 );
10168 cx.update_editor(|editor, window, cx| {
10169 editor.handle_input("/", window, cx);
10170 editor.handle_input("*", window, cx);
10171 });
10172 cx.assert_editor_state(
10173 &"
10174 /*ˇ */
10175 /*ˇ */
10176 "
10177 .unindent(),
10178 );
10179
10180 // one cursor autocloses a multi-character pair, one cursor
10181 // does not autoclose.
10182 cx.set_state(
10183 &"
10184 /ˇ
10185 ˇ
10186 "
10187 .unindent(),
10188 );
10189 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10190 cx.assert_editor_state(
10191 &"
10192 /*ˇ */
10193 *ˇ
10194 "
10195 .unindent(),
10196 );
10197
10198 // Don't autoclose if the next character isn't whitespace and isn't
10199 // listed in the language's "autoclose_before" section.
10200 cx.set_state("ˇa b");
10201 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10202 cx.assert_editor_state("{ˇa b");
10203
10204 // Don't autoclose if `close` is false for the bracket pair
10205 cx.set_state("ˇ");
10206 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10207 cx.assert_editor_state("[ˇ");
10208
10209 // Surround with brackets if text is selected
10210 cx.set_state("«aˇ» b");
10211 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10212 cx.assert_editor_state("{«aˇ»} b");
10213
10214 // Autoclose when not immediately after a word character
10215 cx.set_state("a ˇ");
10216 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10217 cx.assert_editor_state("a \"ˇ\"");
10218
10219 // Autoclose pair where the start and end characters are the same
10220 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10221 cx.assert_editor_state("a \"\"ˇ");
10222
10223 // Don't autoclose when immediately after a word character
10224 cx.set_state("aˇ");
10225 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10226 cx.assert_editor_state("a\"ˇ");
10227
10228 // Do autoclose when after a non-word character
10229 cx.set_state("{ˇ");
10230 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10231 cx.assert_editor_state("{\"ˇ\"");
10232
10233 // Non identical pairs autoclose regardless of preceding character
10234 cx.set_state("aˇ");
10235 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10236 cx.assert_editor_state("a{ˇ}");
10237
10238 // Don't autoclose pair if autoclose is disabled
10239 cx.set_state("ˇ");
10240 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10241 cx.assert_editor_state("<ˇ");
10242
10243 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10244 cx.set_state("«aˇ» b");
10245 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10246 cx.assert_editor_state("<«aˇ»> b");
10247}
10248
10249#[gpui::test]
10250async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10251 init_test(cx, |settings| {
10252 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10253 });
10254
10255 let mut cx = EditorTestContext::new(cx).await;
10256
10257 let language = Arc::new(Language::new(
10258 LanguageConfig {
10259 brackets: BracketPairConfig {
10260 pairs: vec![
10261 BracketPair {
10262 start: "{".to_string(),
10263 end: "}".to_string(),
10264 close: true,
10265 surround: true,
10266 newline: true,
10267 },
10268 BracketPair {
10269 start: "(".to_string(),
10270 end: ")".to_string(),
10271 close: true,
10272 surround: true,
10273 newline: true,
10274 },
10275 BracketPair {
10276 start: "[".to_string(),
10277 end: "]".to_string(),
10278 close: false,
10279 surround: false,
10280 newline: true,
10281 },
10282 ],
10283 ..Default::default()
10284 },
10285 autoclose_before: "})]".to_string(),
10286 ..Default::default()
10287 },
10288 Some(tree_sitter_rust::LANGUAGE.into()),
10289 ));
10290
10291 cx.language_registry().add(language.clone());
10292 cx.update_buffer(|buffer, cx| {
10293 buffer.set_language(Some(language), cx);
10294 });
10295
10296 cx.set_state(
10297 &"
10298 ˇ
10299 ˇ
10300 ˇ
10301 "
10302 .unindent(),
10303 );
10304
10305 // ensure only matching closing brackets are skipped over
10306 cx.update_editor(|editor, window, cx| {
10307 editor.handle_input("}", window, cx);
10308 editor.move_left(&MoveLeft, window, cx);
10309 editor.handle_input(")", window, cx);
10310 editor.move_left(&MoveLeft, window, cx);
10311 });
10312 cx.assert_editor_state(
10313 &"
10314 ˇ)}
10315 ˇ)}
10316 ˇ)}
10317 "
10318 .unindent(),
10319 );
10320
10321 // skip-over closing brackets at multiple cursors
10322 cx.update_editor(|editor, window, cx| {
10323 editor.handle_input(")", window, cx);
10324 editor.handle_input("}", window, cx);
10325 });
10326 cx.assert_editor_state(
10327 &"
10328 )}ˇ
10329 )}ˇ
10330 )}ˇ
10331 "
10332 .unindent(),
10333 );
10334
10335 // ignore non-close brackets
10336 cx.update_editor(|editor, window, cx| {
10337 editor.handle_input("]", window, cx);
10338 editor.move_left(&MoveLeft, window, cx);
10339 editor.handle_input("]", window, cx);
10340 });
10341 cx.assert_editor_state(
10342 &"
10343 )}]ˇ]
10344 )}]ˇ]
10345 )}]ˇ]
10346 "
10347 .unindent(),
10348 );
10349}
10350
10351#[gpui::test]
10352async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10353 init_test(cx, |_| {});
10354
10355 let mut cx = EditorTestContext::new(cx).await;
10356
10357 let html_language = Arc::new(
10358 Language::new(
10359 LanguageConfig {
10360 name: "HTML".into(),
10361 brackets: BracketPairConfig {
10362 pairs: vec![
10363 BracketPair {
10364 start: "<".into(),
10365 end: ">".into(),
10366 close: true,
10367 ..Default::default()
10368 },
10369 BracketPair {
10370 start: "{".into(),
10371 end: "}".into(),
10372 close: true,
10373 ..Default::default()
10374 },
10375 BracketPair {
10376 start: "(".into(),
10377 end: ")".into(),
10378 close: true,
10379 ..Default::default()
10380 },
10381 ],
10382 ..Default::default()
10383 },
10384 autoclose_before: "})]>".into(),
10385 ..Default::default()
10386 },
10387 Some(tree_sitter_html::LANGUAGE.into()),
10388 )
10389 .with_injection_query(
10390 r#"
10391 (script_element
10392 (raw_text) @injection.content
10393 (#set! injection.language "javascript"))
10394 "#,
10395 )
10396 .unwrap(),
10397 );
10398
10399 let javascript_language = Arc::new(Language::new(
10400 LanguageConfig {
10401 name: "JavaScript".into(),
10402 brackets: BracketPairConfig {
10403 pairs: vec![
10404 BracketPair {
10405 start: "/*".into(),
10406 end: " */".into(),
10407 close: true,
10408 ..Default::default()
10409 },
10410 BracketPair {
10411 start: "{".into(),
10412 end: "}".into(),
10413 close: true,
10414 ..Default::default()
10415 },
10416 BracketPair {
10417 start: "(".into(),
10418 end: ")".into(),
10419 close: true,
10420 ..Default::default()
10421 },
10422 ],
10423 ..Default::default()
10424 },
10425 autoclose_before: "})]>".into(),
10426 ..Default::default()
10427 },
10428 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10429 ));
10430
10431 cx.language_registry().add(html_language.clone());
10432 cx.language_registry().add(javascript_language);
10433 cx.executor().run_until_parked();
10434
10435 cx.update_buffer(|buffer, cx| {
10436 buffer.set_language(Some(html_language), cx);
10437 });
10438
10439 cx.set_state(
10440 &r#"
10441 <body>ˇ
10442 <script>
10443 var x = 1;ˇ
10444 </script>
10445 </body>ˇ
10446 "#
10447 .unindent(),
10448 );
10449
10450 // Precondition: different languages are active at different locations.
10451 cx.update_editor(|editor, window, cx| {
10452 let snapshot = editor.snapshot(window, cx);
10453 let cursors = editor
10454 .selections
10455 .ranges::<usize>(&editor.display_snapshot(cx));
10456 let languages = cursors
10457 .iter()
10458 .map(|c| snapshot.language_at(c.start).unwrap().name())
10459 .collect::<Vec<_>>();
10460 assert_eq!(
10461 languages,
10462 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10463 );
10464 });
10465
10466 // Angle brackets autoclose in HTML, but not JavaScript.
10467 cx.update_editor(|editor, window, cx| {
10468 editor.handle_input("<", window, cx);
10469 editor.handle_input("a", window, cx);
10470 });
10471 cx.assert_editor_state(
10472 &r#"
10473 <body><aˇ>
10474 <script>
10475 var x = 1;<aˇ
10476 </script>
10477 </body><aˇ>
10478 "#
10479 .unindent(),
10480 );
10481
10482 // Curly braces and parens autoclose in both HTML and JavaScript.
10483 cx.update_editor(|editor, window, cx| {
10484 editor.handle_input(" b=", window, cx);
10485 editor.handle_input("{", window, cx);
10486 editor.handle_input("c", window, cx);
10487 editor.handle_input("(", window, cx);
10488 });
10489 cx.assert_editor_state(
10490 &r#"
10491 <body><a b={c(ˇ)}>
10492 <script>
10493 var x = 1;<a b={c(ˇ)}
10494 </script>
10495 </body><a b={c(ˇ)}>
10496 "#
10497 .unindent(),
10498 );
10499
10500 // Brackets that were already autoclosed are skipped.
10501 cx.update_editor(|editor, window, cx| {
10502 editor.handle_input(")", window, cx);
10503 editor.handle_input("d", window, cx);
10504 editor.handle_input("}", window, cx);
10505 });
10506 cx.assert_editor_state(
10507 &r#"
10508 <body><a b={c()d}ˇ>
10509 <script>
10510 var x = 1;<a b={c()d}ˇ
10511 </script>
10512 </body><a b={c()d}ˇ>
10513 "#
10514 .unindent(),
10515 );
10516 cx.update_editor(|editor, window, cx| {
10517 editor.handle_input(">", window, cx);
10518 });
10519 cx.assert_editor_state(
10520 &r#"
10521 <body><a b={c()d}>ˇ
10522 <script>
10523 var x = 1;<a b={c()d}>ˇ
10524 </script>
10525 </body><a b={c()d}>ˇ
10526 "#
10527 .unindent(),
10528 );
10529
10530 // Reset
10531 cx.set_state(
10532 &r#"
10533 <body>ˇ
10534 <script>
10535 var x = 1;ˇ
10536 </script>
10537 </body>ˇ
10538 "#
10539 .unindent(),
10540 );
10541
10542 cx.update_editor(|editor, window, cx| {
10543 editor.handle_input("<", window, cx);
10544 });
10545 cx.assert_editor_state(
10546 &r#"
10547 <body><ˇ>
10548 <script>
10549 var x = 1;<ˇ
10550 </script>
10551 </body><ˇ>
10552 "#
10553 .unindent(),
10554 );
10555
10556 // When backspacing, the closing angle brackets are removed.
10557 cx.update_editor(|editor, window, cx| {
10558 editor.backspace(&Backspace, window, cx);
10559 });
10560 cx.assert_editor_state(
10561 &r#"
10562 <body>ˇ
10563 <script>
10564 var x = 1;ˇ
10565 </script>
10566 </body>ˇ
10567 "#
10568 .unindent(),
10569 );
10570
10571 // Block comments autoclose in JavaScript, but not HTML.
10572 cx.update_editor(|editor, window, cx| {
10573 editor.handle_input("/", window, cx);
10574 editor.handle_input("*", window, cx);
10575 });
10576 cx.assert_editor_state(
10577 &r#"
10578 <body>/*ˇ
10579 <script>
10580 var x = 1;/*ˇ */
10581 </script>
10582 </body>/*ˇ
10583 "#
10584 .unindent(),
10585 );
10586}
10587
10588#[gpui::test]
10589async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10590 init_test(cx, |_| {});
10591
10592 let mut cx = EditorTestContext::new(cx).await;
10593
10594 let rust_language = Arc::new(
10595 Language::new(
10596 LanguageConfig {
10597 name: "Rust".into(),
10598 brackets: serde_json::from_value(json!([
10599 { "start": "{", "end": "}", "close": true, "newline": true },
10600 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10601 ]))
10602 .unwrap(),
10603 autoclose_before: "})]>".into(),
10604 ..Default::default()
10605 },
10606 Some(tree_sitter_rust::LANGUAGE.into()),
10607 )
10608 .with_override_query("(string_literal) @string")
10609 .unwrap(),
10610 );
10611
10612 cx.language_registry().add(rust_language.clone());
10613 cx.update_buffer(|buffer, cx| {
10614 buffer.set_language(Some(rust_language), cx);
10615 });
10616
10617 cx.set_state(
10618 &r#"
10619 let x = ˇ
10620 "#
10621 .unindent(),
10622 );
10623
10624 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10625 cx.update_editor(|editor, window, cx| {
10626 editor.handle_input("\"", window, cx);
10627 });
10628 cx.assert_editor_state(
10629 &r#"
10630 let x = "ˇ"
10631 "#
10632 .unindent(),
10633 );
10634
10635 // Inserting another quotation mark. The cursor moves across the existing
10636 // automatically-inserted quotation mark.
10637 cx.update_editor(|editor, window, cx| {
10638 editor.handle_input("\"", window, cx);
10639 });
10640 cx.assert_editor_state(
10641 &r#"
10642 let x = ""ˇ
10643 "#
10644 .unindent(),
10645 );
10646
10647 // Reset
10648 cx.set_state(
10649 &r#"
10650 let x = ˇ
10651 "#
10652 .unindent(),
10653 );
10654
10655 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10656 cx.update_editor(|editor, window, cx| {
10657 editor.handle_input("\"", window, cx);
10658 editor.handle_input(" ", window, cx);
10659 editor.move_left(&Default::default(), window, cx);
10660 editor.handle_input("\\", window, cx);
10661 editor.handle_input("\"", window, cx);
10662 });
10663 cx.assert_editor_state(
10664 &r#"
10665 let x = "\"ˇ "
10666 "#
10667 .unindent(),
10668 );
10669
10670 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10671 // mark. Nothing is inserted.
10672 cx.update_editor(|editor, window, cx| {
10673 editor.move_right(&Default::default(), window, cx);
10674 editor.handle_input("\"", window, cx);
10675 });
10676 cx.assert_editor_state(
10677 &r#"
10678 let x = "\" "ˇ
10679 "#
10680 .unindent(),
10681 );
10682}
10683
10684#[gpui::test]
10685async fn test_surround_with_pair(cx: &mut TestAppContext) {
10686 init_test(cx, |_| {});
10687
10688 let language = Arc::new(Language::new(
10689 LanguageConfig {
10690 brackets: BracketPairConfig {
10691 pairs: vec![
10692 BracketPair {
10693 start: "{".to_string(),
10694 end: "}".to_string(),
10695 close: true,
10696 surround: true,
10697 newline: true,
10698 },
10699 BracketPair {
10700 start: "/* ".to_string(),
10701 end: "*/".to_string(),
10702 close: true,
10703 surround: true,
10704 ..Default::default()
10705 },
10706 ],
10707 ..Default::default()
10708 },
10709 ..Default::default()
10710 },
10711 Some(tree_sitter_rust::LANGUAGE.into()),
10712 ));
10713
10714 let text = r#"
10715 a
10716 b
10717 c
10718 "#
10719 .unindent();
10720
10721 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10722 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10723 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10724 editor
10725 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10726 .await;
10727
10728 editor.update_in(cx, |editor, window, cx| {
10729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10730 s.select_display_ranges([
10731 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10732 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10733 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10734 ])
10735 });
10736
10737 editor.handle_input("{", window, cx);
10738 editor.handle_input("{", window, cx);
10739 editor.handle_input("{", window, cx);
10740 assert_eq!(
10741 editor.text(cx),
10742 "
10743 {{{a}}}
10744 {{{b}}}
10745 {{{c}}}
10746 "
10747 .unindent()
10748 );
10749 assert_eq!(
10750 display_ranges(editor, cx),
10751 [
10752 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10753 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10754 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10755 ]
10756 );
10757
10758 editor.undo(&Undo, window, cx);
10759 editor.undo(&Undo, window, cx);
10760 editor.undo(&Undo, window, cx);
10761 assert_eq!(
10762 editor.text(cx),
10763 "
10764 a
10765 b
10766 c
10767 "
10768 .unindent()
10769 );
10770 assert_eq!(
10771 display_ranges(editor, cx),
10772 [
10773 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10774 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10775 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10776 ]
10777 );
10778
10779 // Ensure inserting the first character of a multi-byte bracket pair
10780 // doesn't surround the selections with the bracket.
10781 editor.handle_input("/", window, cx);
10782 assert_eq!(
10783 editor.text(cx),
10784 "
10785 /
10786 /
10787 /
10788 "
10789 .unindent()
10790 );
10791 assert_eq!(
10792 display_ranges(editor, cx),
10793 [
10794 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10795 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10796 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10797 ]
10798 );
10799
10800 editor.undo(&Undo, window, cx);
10801 assert_eq!(
10802 editor.text(cx),
10803 "
10804 a
10805 b
10806 c
10807 "
10808 .unindent()
10809 );
10810 assert_eq!(
10811 display_ranges(editor, cx),
10812 [
10813 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10814 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10815 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10816 ]
10817 );
10818
10819 // Ensure inserting the last character of a multi-byte bracket pair
10820 // doesn't surround the selections with the bracket.
10821 editor.handle_input("*", window, cx);
10822 assert_eq!(
10823 editor.text(cx),
10824 "
10825 *
10826 *
10827 *
10828 "
10829 .unindent()
10830 );
10831 assert_eq!(
10832 display_ranges(editor, cx),
10833 [
10834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10835 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10836 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10837 ]
10838 );
10839 });
10840}
10841
10842#[gpui::test]
10843async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10844 init_test(cx, |_| {});
10845
10846 let language = Arc::new(Language::new(
10847 LanguageConfig {
10848 brackets: BracketPairConfig {
10849 pairs: vec![BracketPair {
10850 start: "{".to_string(),
10851 end: "}".to_string(),
10852 close: true,
10853 surround: true,
10854 newline: true,
10855 }],
10856 ..Default::default()
10857 },
10858 autoclose_before: "}".to_string(),
10859 ..Default::default()
10860 },
10861 Some(tree_sitter_rust::LANGUAGE.into()),
10862 ));
10863
10864 let text = r#"
10865 a
10866 b
10867 c
10868 "#
10869 .unindent();
10870
10871 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10872 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10873 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10874 editor
10875 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10876 .await;
10877
10878 editor.update_in(cx, |editor, window, cx| {
10879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10880 s.select_ranges([
10881 Point::new(0, 1)..Point::new(0, 1),
10882 Point::new(1, 1)..Point::new(1, 1),
10883 Point::new(2, 1)..Point::new(2, 1),
10884 ])
10885 });
10886
10887 editor.handle_input("{", window, cx);
10888 editor.handle_input("{", window, cx);
10889 editor.handle_input("_", window, cx);
10890 assert_eq!(
10891 editor.text(cx),
10892 "
10893 a{{_}}
10894 b{{_}}
10895 c{{_}}
10896 "
10897 .unindent()
10898 );
10899 assert_eq!(
10900 editor
10901 .selections
10902 .ranges::<Point>(&editor.display_snapshot(cx)),
10903 [
10904 Point::new(0, 4)..Point::new(0, 4),
10905 Point::new(1, 4)..Point::new(1, 4),
10906 Point::new(2, 4)..Point::new(2, 4)
10907 ]
10908 );
10909
10910 editor.backspace(&Default::default(), window, cx);
10911 editor.backspace(&Default::default(), window, cx);
10912 assert_eq!(
10913 editor.text(cx),
10914 "
10915 a{}
10916 b{}
10917 c{}
10918 "
10919 .unindent()
10920 );
10921 assert_eq!(
10922 editor
10923 .selections
10924 .ranges::<Point>(&editor.display_snapshot(cx)),
10925 [
10926 Point::new(0, 2)..Point::new(0, 2),
10927 Point::new(1, 2)..Point::new(1, 2),
10928 Point::new(2, 2)..Point::new(2, 2)
10929 ]
10930 );
10931
10932 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10933 assert_eq!(
10934 editor.text(cx),
10935 "
10936 a
10937 b
10938 c
10939 "
10940 .unindent()
10941 );
10942 assert_eq!(
10943 editor
10944 .selections
10945 .ranges::<Point>(&editor.display_snapshot(cx)),
10946 [
10947 Point::new(0, 1)..Point::new(0, 1),
10948 Point::new(1, 1)..Point::new(1, 1),
10949 Point::new(2, 1)..Point::new(2, 1)
10950 ]
10951 );
10952 });
10953}
10954
10955#[gpui::test]
10956async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10957 init_test(cx, |settings| {
10958 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10959 });
10960
10961 let mut cx = EditorTestContext::new(cx).await;
10962
10963 let language = Arc::new(Language::new(
10964 LanguageConfig {
10965 brackets: BracketPairConfig {
10966 pairs: vec![
10967 BracketPair {
10968 start: "{".to_string(),
10969 end: "}".to_string(),
10970 close: true,
10971 surround: true,
10972 newline: true,
10973 },
10974 BracketPair {
10975 start: "(".to_string(),
10976 end: ")".to_string(),
10977 close: true,
10978 surround: true,
10979 newline: true,
10980 },
10981 BracketPair {
10982 start: "[".to_string(),
10983 end: "]".to_string(),
10984 close: false,
10985 surround: true,
10986 newline: true,
10987 },
10988 ],
10989 ..Default::default()
10990 },
10991 autoclose_before: "})]".to_string(),
10992 ..Default::default()
10993 },
10994 Some(tree_sitter_rust::LANGUAGE.into()),
10995 ));
10996
10997 cx.language_registry().add(language.clone());
10998 cx.update_buffer(|buffer, cx| {
10999 buffer.set_language(Some(language), cx);
11000 });
11001
11002 cx.set_state(
11003 &"
11004 {(ˇ)}
11005 [[ˇ]]
11006 {(ˇ)}
11007 "
11008 .unindent(),
11009 );
11010
11011 cx.update_editor(|editor, window, cx| {
11012 editor.backspace(&Default::default(), window, cx);
11013 editor.backspace(&Default::default(), window, cx);
11014 });
11015
11016 cx.assert_editor_state(
11017 &"
11018 ˇ
11019 ˇ]]
11020 ˇ
11021 "
11022 .unindent(),
11023 );
11024
11025 cx.update_editor(|editor, window, cx| {
11026 editor.handle_input("{", window, cx);
11027 editor.handle_input("{", window, cx);
11028 editor.move_right(&MoveRight, window, cx);
11029 editor.move_right(&MoveRight, window, cx);
11030 editor.move_left(&MoveLeft, window, cx);
11031 editor.move_left(&MoveLeft, window, cx);
11032 editor.backspace(&Default::default(), window, cx);
11033 });
11034
11035 cx.assert_editor_state(
11036 &"
11037 {ˇ}
11038 {ˇ}]]
11039 {ˇ}
11040 "
11041 .unindent(),
11042 );
11043
11044 cx.update_editor(|editor, window, cx| {
11045 editor.backspace(&Default::default(), window, cx);
11046 });
11047
11048 cx.assert_editor_state(
11049 &"
11050 ˇ
11051 ˇ]]
11052 ˇ
11053 "
11054 .unindent(),
11055 );
11056}
11057
11058#[gpui::test]
11059async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11060 init_test(cx, |_| {});
11061
11062 let language = Arc::new(Language::new(
11063 LanguageConfig::default(),
11064 Some(tree_sitter_rust::LANGUAGE.into()),
11065 ));
11066
11067 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11069 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11070 editor
11071 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11072 .await;
11073
11074 editor.update_in(cx, |editor, window, cx| {
11075 editor.set_auto_replace_emoji_shortcode(true);
11076
11077 editor.handle_input("Hello ", window, cx);
11078 editor.handle_input(":wave", window, cx);
11079 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11080
11081 editor.handle_input(":", window, cx);
11082 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11083
11084 editor.handle_input(" :smile", window, cx);
11085 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11086
11087 editor.handle_input(":", window, cx);
11088 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11089
11090 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11091 editor.handle_input(":wave", window, cx);
11092 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11093
11094 editor.handle_input(":", window, cx);
11095 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11096
11097 editor.handle_input(":1", window, cx);
11098 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11099
11100 editor.handle_input(":", window, cx);
11101 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11102
11103 // Ensure shortcode does not get replaced when it is part of a word
11104 editor.handle_input(" Test:wave", window, cx);
11105 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11106
11107 editor.handle_input(":", window, cx);
11108 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11109
11110 editor.set_auto_replace_emoji_shortcode(false);
11111
11112 // Ensure shortcode does not get replaced when auto replace is off
11113 editor.handle_input(" :wave", window, cx);
11114 assert_eq!(
11115 editor.text(cx),
11116 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11117 );
11118
11119 editor.handle_input(":", window, cx);
11120 assert_eq!(
11121 editor.text(cx),
11122 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11123 );
11124 });
11125}
11126
11127#[gpui::test]
11128async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11129 init_test(cx, |_| {});
11130
11131 let (text, insertion_ranges) = marked_text_ranges(
11132 indoc! {"
11133 ˇ
11134 "},
11135 false,
11136 );
11137
11138 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11139 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11140
11141 _ = editor.update_in(cx, |editor, window, cx| {
11142 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11143
11144 editor
11145 .insert_snippet(&insertion_ranges, snippet, window, cx)
11146 .unwrap();
11147
11148 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11149 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11150 assert_eq!(editor.text(cx), expected_text);
11151 assert_eq!(
11152 editor
11153 .selections
11154 .ranges::<usize>(&editor.display_snapshot(cx)),
11155 selection_ranges
11156 );
11157 }
11158
11159 assert(
11160 editor,
11161 cx,
11162 indoc! {"
11163 type «» =•
11164 "},
11165 );
11166
11167 assert!(editor.context_menu_visible(), "There should be a matches");
11168 });
11169}
11170
11171#[gpui::test]
11172async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11173 init_test(cx, |_| {});
11174
11175 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11176 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11177 assert_eq!(editor.text(cx), expected_text);
11178 assert_eq!(
11179 editor
11180 .selections
11181 .ranges::<usize>(&editor.display_snapshot(cx)),
11182 selection_ranges
11183 );
11184 }
11185
11186 let (text, insertion_ranges) = marked_text_ranges(
11187 indoc! {"
11188 ˇ
11189 "},
11190 false,
11191 );
11192
11193 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11194 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11195
11196 _ = editor.update_in(cx, |editor, window, cx| {
11197 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11198
11199 editor
11200 .insert_snippet(&insertion_ranges, snippet, window, cx)
11201 .unwrap();
11202
11203 assert_state(
11204 editor,
11205 cx,
11206 indoc! {"
11207 type «» = ;•
11208 "},
11209 );
11210
11211 assert!(
11212 editor.context_menu_visible(),
11213 "Context menu should be visible for placeholder choices"
11214 );
11215
11216 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11217
11218 assert_state(
11219 editor,
11220 cx,
11221 indoc! {"
11222 type = «»;•
11223 "},
11224 );
11225
11226 assert!(
11227 !editor.context_menu_visible(),
11228 "Context menu should be hidden after moving to next tabstop"
11229 );
11230
11231 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11232
11233 assert_state(
11234 editor,
11235 cx,
11236 indoc! {"
11237 type = ; ˇ
11238 "},
11239 );
11240
11241 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11242
11243 assert_state(
11244 editor,
11245 cx,
11246 indoc! {"
11247 type = ; ˇ
11248 "},
11249 );
11250 });
11251
11252 _ = editor.update_in(cx, |editor, window, cx| {
11253 editor.select_all(&SelectAll, window, cx);
11254 editor.backspace(&Backspace, window, cx);
11255
11256 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11257 let insertion_ranges = editor
11258 .selections
11259 .all(&editor.display_snapshot(cx))
11260 .iter()
11261 .map(|s| s.range())
11262 .collect::<Vec<_>>();
11263
11264 editor
11265 .insert_snippet(&insertion_ranges, snippet, window, cx)
11266 .unwrap();
11267
11268 assert_state(editor, cx, "fn «» = value;•");
11269
11270 assert!(
11271 editor.context_menu_visible(),
11272 "Context menu should be visible for placeholder choices"
11273 );
11274
11275 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11276
11277 assert_state(editor, cx, "fn = «valueˇ»;•");
11278
11279 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11280
11281 assert_state(editor, cx, "fn «» = value;•");
11282
11283 assert!(
11284 editor.context_menu_visible(),
11285 "Context menu should be visible again after returning to first tabstop"
11286 );
11287
11288 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11289
11290 assert_state(editor, cx, "fn «» = value;•");
11291 });
11292}
11293
11294#[gpui::test]
11295async fn test_snippets(cx: &mut TestAppContext) {
11296 init_test(cx, |_| {});
11297
11298 let mut cx = EditorTestContext::new(cx).await;
11299
11300 cx.set_state(indoc! {"
11301 a.ˇ b
11302 a.ˇ b
11303 a.ˇ b
11304 "});
11305
11306 cx.update_editor(|editor, window, cx| {
11307 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11308 let insertion_ranges = editor
11309 .selections
11310 .all(&editor.display_snapshot(cx))
11311 .iter()
11312 .map(|s| s.range())
11313 .collect::<Vec<_>>();
11314 editor
11315 .insert_snippet(&insertion_ranges, snippet, window, cx)
11316 .unwrap();
11317 });
11318
11319 cx.assert_editor_state(indoc! {"
11320 a.f(«oneˇ», two, «threeˇ») b
11321 a.f(«oneˇ», two, «threeˇ») b
11322 a.f(«oneˇ», two, «threeˇ») b
11323 "});
11324
11325 // Can't move earlier than the first tab stop
11326 cx.update_editor(|editor, window, cx| {
11327 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11328 });
11329 cx.assert_editor_state(indoc! {"
11330 a.f(«oneˇ», two, «threeˇ») b
11331 a.f(«oneˇ», two, «threeˇ») b
11332 a.f(«oneˇ», two, «threeˇ») b
11333 "});
11334
11335 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11336 cx.assert_editor_state(indoc! {"
11337 a.f(one, «twoˇ», three) b
11338 a.f(one, «twoˇ», three) b
11339 a.f(one, «twoˇ», three) b
11340 "});
11341
11342 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11343 cx.assert_editor_state(indoc! {"
11344 a.f(«oneˇ», two, «threeˇ») b
11345 a.f(«oneˇ», two, «threeˇ») b
11346 a.f(«oneˇ», two, «threeˇ») b
11347 "});
11348
11349 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11350 cx.assert_editor_state(indoc! {"
11351 a.f(one, «twoˇ», three) b
11352 a.f(one, «twoˇ», three) b
11353 a.f(one, «twoˇ», three) b
11354 "});
11355 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11356 cx.assert_editor_state(indoc! {"
11357 a.f(one, two, three)ˇ b
11358 a.f(one, two, three)ˇ b
11359 a.f(one, two, three)ˇ b
11360 "});
11361
11362 // As soon as the last tab stop is reached, snippet state is gone
11363 cx.update_editor(|editor, window, cx| {
11364 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11365 });
11366 cx.assert_editor_state(indoc! {"
11367 a.f(one, two, three)ˇ b
11368 a.f(one, two, three)ˇ b
11369 a.f(one, two, three)ˇ b
11370 "});
11371}
11372
11373#[gpui::test]
11374async fn test_snippet_indentation(cx: &mut TestAppContext) {
11375 init_test(cx, |_| {});
11376
11377 let mut cx = EditorTestContext::new(cx).await;
11378
11379 cx.update_editor(|editor, window, cx| {
11380 let snippet = Snippet::parse(indoc! {"
11381 /*
11382 * Multiline comment with leading indentation
11383 *
11384 * $1
11385 */
11386 $0"})
11387 .unwrap();
11388 let insertion_ranges = editor
11389 .selections
11390 .all(&editor.display_snapshot(cx))
11391 .iter()
11392 .map(|s| s.range())
11393 .collect::<Vec<_>>();
11394 editor
11395 .insert_snippet(&insertion_ranges, snippet, window, cx)
11396 .unwrap();
11397 });
11398
11399 cx.assert_editor_state(indoc! {"
11400 /*
11401 * Multiline comment with leading indentation
11402 *
11403 * ˇ
11404 */
11405 "});
11406
11407 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11408 cx.assert_editor_state(indoc! {"
11409 /*
11410 * Multiline comment with leading indentation
11411 *
11412 *•
11413 */
11414 ˇ"});
11415}
11416
11417#[gpui::test]
11418async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11419 init_test(cx, |_| {});
11420
11421 let mut cx = EditorTestContext::new(cx).await;
11422 cx.update_editor(|editor, _, cx| {
11423 editor.project().unwrap().update(cx, |project, cx| {
11424 project.snippets().update(cx, |snippets, _cx| {
11425 let snippet = project::snippet_provider::Snippet {
11426 prefix: vec!["multi word".to_string()],
11427 body: "this is many words".to_string(),
11428 description: Some("description".to_string()),
11429 name: "multi-word snippet test".to_string(),
11430 };
11431 snippets.add_snippet_for_test(
11432 None,
11433 PathBuf::from("test_snippets.json"),
11434 vec![Arc::new(snippet)],
11435 );
11436 });
11437 })
11438 });
11439
11440 for (input_to_simulate, should_match_snippet) in [
11441 ("m", true),
11442 ("m ", true),
11443 ("m w", true),
11444 ("aa m w", true),
11445 ("aa m g", false),
11446 ] {
11447 cx.set_state("ˇ");
11448 cx.simulate_input(input_to_simulate); // fails correctly
11449
11450 cx.update_editor(|editor, _, _| {
11451 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11452 else {
11453 assert!(!should_match_snippet); // no completions! don't even show the menu
11454 return;
11455 };
11456 assert!(context_menu.visible());
11457 let completions = context_menu.completions.borrow();
11458
11459 assert_eq!(!completions.is_empty(), should_match_snippet);
11460 });
11461 }
11462}
11463
11464#[gpui::test]
11465async fn test_document_format_during_save(cx: &mut TestAppContext) {
11466 init_test(cx, |_| {});
11467
11468 let fs = FakeFs::new(cx.executor());
11469 fs.insert_file(path!("/file.rs"), Default::default()).await;
11470
11471 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11472
11473 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11474 language_registry.add(rust_lang());
11475 let mut fake_servers = language_registry.register_fake_lsp(
11476 "Rust",
11477 FakeLspAdapter {
11478 capabilities: lsp::ServerCapabilities {
11479 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11480 ..Default::default()
11481 },
11482 ..Default::default()
11483 },
11484 );
11485
11486 let buffer = project
11487 .update(cx, |project, cx| {
11488 project.open_local_buffer(path!("/file.rs"), cx)
11489 })
11490 .await
11491 .unwrap();
11492
11493 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11494 let (editor, cx) = cx.add_window_view(|window, cx| {
11495 build_editor_with_project(project.clone(), buffer, window, cx)
11496 });
11497 editor.update_in(cx, |editor, window, cx| {
11498 editor.set_text("one\ntwo\nthree\n", window, cx)
11499 });
11500 assert!(cx.read(|cx| editor.is_dirty(cx)));
11501
11502 cx.executor().start_waiting();
11503 let fake_server = fake_servers.next().await.unwrap();
11504
11505 {
11506 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11507 move |params, _| async move {
11508 assert_eq!(
11509 params.text_document.uri,
11510 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11511 );
11512 assert_eq!(params.options.tab_size, 4);
11513 Ok(Some(vec![lsp::TextEdit::new(
11514 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11515 ", ".to_string(),
11516 )]))
11517 },
11518 );
11519 let save = editor
11520 .update_in(cx, |editor, window, cx| {
11521 editor.save(
11522 SaveOptions {
11523 format: true,
11524 autosave: false,
11525 },
11526 project.clone(),
11527 window,
11528 cx,
11529 )
11530 })
11531 .unwrap();
11532 cx.executor().start_waiting();
11533 save.await;
11534
11535 assert_eq!(
11536 editor.update(cx, |editor, cx| editor.text(cx)),
11537 "one, two\nthree\n"
11538 );
11539 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11540 }
11541
11542 {
11543 editor.update_in(cx, |editor, window, cx| {
11544 editor.set_text("one\ntwo\nthree\n", window, cx)
11545 });
11546 assert!(cx.read(|cx| editor.is_dirty(cx)));
11547
11548 // Ensure we can still save even if formatting hangs.
11549 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11550 move |params, _| async move {
11551 assert_eq!(
11552 params.text_document.uri,
11553 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11554 );
11555 futures::future::pending::<()>().await;
11556 unreachable!()
11557 },
11558 );
11559 let save = editor
11560 .update_in(cx, |editor, window, cx| {
11561 editor.save(
11562 SaveOptions {
11563 format: true,
11564 autosave: false,
11565 },
11566 project.clone(),
11567 window,
11568 cx,
11569 )
11570 })
11571 .unwrap();
11572 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11573 cx.executor().start_waiting();
11574 save.await;
11575 assert_eq!(
11576 editor.update(cx, |editor, cx| editor.text(cx)),
11577 "one\ntwo\nthree\n"
11578 );
11579 }
11580
11581 // Set rust language override and assert overridden tabsize is sent to language server
11582 update_test_language_settings(cx, |settings| {
11583 settings.languages.0.insert(
11584 "Rust".into(),
11585 LanguageSettingsContent {
11586 tab_size: NonZeroU32::new(8),
11587 ..Default::default()
11588 },
11589 );
11590 });
11591
11592 {
11593 editor.update_in(cx, |editor, window, cx| {
11594 editor.set_text("somehting_new\n", window, cx)
11595 });
11596 assert!(cx.read(|cx| editor.is_dirty(cx)));
11597 let _formatting_request_signal = fake_server
11598 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11599 assert_eq!(
11600 params.text_document.uri,
11601 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11602 );
11603 assert_eq!(params.options.tab_size, 8);
11604 Ok(Some(vec![]))
11605 });
11606 let save = editor
11607 .update_in(cx, |editor, window, cx| {
11608 editor.save(
11609 SaveOptions {
11610 format: true,
11611 autosave: false,
11612 },
11613 project.clone(),
11614 window,
11615 cx,
11616 )
11617 })
11618 .unwrap();
11619 cx.executor().start_waiting();
11620 save.await;
11621 }
11622}
11623
11624#[gpui::test]
11625async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11626 init_test(cx, |settings| {
11627 settings.defaults.ensure_final_newline_on_save = Some(false);
11628 });
11629
11630 let fs = FakeFs::new(cx.executor());
11631 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11632
11633 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11634
11635 let buffer = project
11636 .update(cx, |project, cx| {
11637 project.open_local_buffer(path!("/file.txt"), cx)
11638 })
11639 .await
11640 .unwrap();
11641
11642 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11643 let (editor, cx) = cx.add_window_view(|window, cx| {
11644 build_editor_with_project(project.clone(), buffer, window, cx)
11645 });
11646 editor.update_in(cx, |editor, window, cx| {
11647 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11648 s.select_ranges([0..0])
11649 });
11650 });
11651 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11652
11653 editor.update_in(cx, |editor, window, cx| {
11654 editor.handle_input("\n", window, cx)
11655 });
11656 cx.run_until_parked();
11657 save(&editor, &project, cx).await;
11658 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11659
11660 editor.update_in(cx, |editor, window, cx| {
11661 editor.undo(&Default::default(), window, cx);
11662 });
11663 save(&editor, &project, cx).await;
11664 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11665
11666 editor.update_in(cx, |editor, window, cx| {
11667 editor.redo(&Default::default(), window, cx);
11668 });
11669 cx.run_until_parked();
11670 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11671
11672 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11673 let save = editor
11674 .update_in(cx, |editor, window, cx| {
11675 editor.save(
11676 SaveOptions {
11677 format: true,
11678 autosave: false,
11679 },
11680 project.clone(),
11681 window,
11682 cx,
11683 )
11684 })
11685 .unwrap();
11686 cx.executor().start_waiting();
11687 save.await;
11688 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11689 }
11690}
11691
11692#[gpui::test]
11693async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11694 init_test(cx, |_| {});
11695
11696 let cols = 4;
11697 let rows = 10;
11698 let sample_text_1 = sample_text(rows, cols, 'a');
11699 assert_eq!(
11700 sample_text_1,
11701 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11702 );
11703 let sample_text_2 = sample_text(rows, cols, 'l');
11704 assert_eq!(
11705 sample_text_2,
11706 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11707 );
11708 let sample_text_3 = sample_text(rows, cols, 'v');
11709 assert_eq!(
11710 sample_text_3,
11711 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11712 );
11713
11714 let fs = FakeFs::new(cx.executor());
11715 fs.insert_tree(
11716 path!("/a"),
11717 json!({
11718 "main.rs": sample_text_1,
11719 "other.rs": sample_text_2,
11720 "lib.rs": sample_text_3,
11721 }),
11722 )
11723 .await;
11724
11725 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11726 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11727 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11728
11729 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11730 language_registry.add(rust_lang());
11731 let mut fake_servers = language_registry.register_fake_lsp(
11732 "Rust",
11733 FakeLspAdapter {
11734 capabilities: lsp::ServerCapabilities {
11735 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11736 ..Default::default()
11737 },
11738 ..Default::default()
11739 },
11740 );
11741
11742 let worktree = project.update(cx, |project, cx| {
11743 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11744 assert_eq!(worktrees.len(), 1);
11745 worktrees.pop().unwrap()
11746 });
11747 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11748
11749 let buffer_1 = project
11750 .update(cx, |project, cx| {
11751 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11752 })
11753 .await
11754 .unwrap();
11755 let buffer_2 = project
11756 .update(cx, |project, cx| {
11757 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11758 })
11759 .await
11760 .unwrap();
11761 let buffer_3 = project
11762 .update(cx, |project, cx| {
11763 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11764 })
11765 .await
11766 .unwrap();
11767
11768 let multi_buffer = cx.new(|cx| {
11769 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11770 multi_buffer.push_excerpts(
11771 buffer_1.clone(),
11772 [
11773 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11774 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11775 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11776 ],
11777 cx,
11778 );
11779 multi_buffer.push_excerpts(
11780 buffer_2.clone(),
11781 [
11782 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11783 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11784 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11785 ],
11786 cx,
11787 );
11788 multi_buffer.push_excerpts(
11789 buffer_3.clone(),
11790 [
11791 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11792 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11793 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11794 ],
11795 cx,
11796 );
11797 multi_buffer
11798 });
11799 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11800 Editor::new(
11801 EditorMode::full(),
11802 multi_buffer,
11803 Some(project.clone()),
11804 window,
11805 cx,
11806 )
11807 });
11808
11809 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11810 editor.change_selections(
11811 SelectionEffects::scroll(Autoscroll::Next),
11812 window,
11813 cx,
11814 |s| s.select_ranges(Some(1..2)),
11815 );
11816 editor.insert("|one|two|three|", window, cx);
11817 });
11818 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11819 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11820 editor.change_selections(
11821 SelectionEffects::scroll(Autoscroll::Next),
11822 window,
11823 cx,
11824 |s| s.select_ranges(Some(60..70)),
11825 );
11826 editor.insert("|four|five|six|", window, cx);
11827 });
11828 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11829
11830 // First two buffers should be edited, but not the third one.
11831 assert_eq!(
11832 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11833 "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}",
11834 );
11835 buffer_1.update(cx, |buffer, _| {
11836 assert!(buffer.is_dirty());
11837 assert_eq!(
11838 buffer.text(),
11839 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11840 )
11841 });
11842 buffer_2.update(cx, |buffer, _| {
11843 assert!(buffer.is_dirty());
11844 assert_eq!(
11845 buffer.text(),
11846 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11847 )
11848 });
11849 buffer_3.update(cx, |buffer, _| {
11850 assert!(!buffer.is_dirty());
11851 assert_eq!(buffer.text(), sample_text_3,)
11852 });
11853 cx.executor().run_until_parked();
11854
11855 cx.executor().start_waiting();
11856 let save = multi_buffer_editor
11857 .update_in(cx, |editor, window, cx| {
11858 editor.save(
11859 SaveOptions {
11860 format: true,
11861 autosave: false,
11862 },
11863 project.clone(),
11864 window,
11865 cx,
11866 )
11867 })
11868 .unwrap();
11869
11870 let fake_server = fake_servers.next().await.unwrap();
11871 fake_server
11872 .server
11873 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11874 Ok(Some(vec![lsp::TextEdit::new(
11875 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11876 format!("[{} formatted]", params.text_document.uri),
11877 )]))
11878 })
11879 .detach();
11880 save.await;
11881
11882 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11883 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11884 assert_eq!(
11885 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11886 uri!(
11887 "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}"
11888 ),
11889 );
11890 buffer_1.update(cx, |buffer, _| {
11891 assert!(!buffer.is_dirty());
11892 assert_eq!(
11893 buffer.text(),
11894 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11895 )
11896 });
11897 buffer_2.update(cx, |buffer, _| {
11898 assert!(!buffer.is_dirty());
11899 assert_eq!(
11900 buffer.text(),
11901 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11902 )
11903 });
11904 buffer_3.update(cx, |buffer, _| {
11905 assert!(!buffer.is_dirty());
11906 assert_eq!(buffer.text(), sample_text_3,)
11907 });
11908}
11909
11910#[gpui::test]
11911async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11912 init_test(cx, |_| {});
11913
11914 let fs = FakeFs::new(cx.executor());
11915 fs.insert_tree(
11916 path!("/dir"),
11917 json!({
11918 "file1.rs": "fn main() { println!(\"hello\"); }",
11919 "file2.rs": "fn test() { println!(\"test\"); }",
11920 "file3.rs": "fn other() { println!(\"other\"); }\n",
11921 }),
11922 )
11923 .await;
11924
11925 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11926 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11927 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11928
11929 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11930 language_registry.add(rust_lang());
11931
11932 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11933 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11934
11935 // Open three buffers
11936 let buffer_1 = project
11937 .update(cx, |project, cx| {
11938 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11939 })
11940 .await
11941 .unwrap();
11942 let buffer_2 = project
11943 .update(cx, |project, cx| {
11944 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11945 })
11946 .await
11947 .unwrap();
11948 let buffer_3 = project
11949 .update(cx, |project, cx| {
11950 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11951 })
11952 .await
11953 .unwrap();
11954
11955 // Create a multi-buffer with all three buffers
11956 let multi_buffer = cx.new(|cx| {
11957 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11958 multi_buffer.push_excerpts(
11959 buffer_1.clone(),
11960 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11961 cx,
11962 );
11963 multi_buffer.push_excerpts(
11964 buffer_2.clone(),
11965 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11966 cx,
11967 );
11968 multi_buffer.push_excerpts(
11969 buffer_3.clone(),
11970 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11971 cx,
11972 );
11973 multi_buffer
11974 });
11975
11976 let editor = cx.new_window_entity(|window, cx| {
11977 Editor::new(
11978 EditorMode::full(),
11979 multi_buffer,
11980 Some(project.clone()),
11981 window,
11982 cx,
11983 )
11984 });
11985
11986 // Edit only the first buffer
11987 editor.update_in(cx, |editor, window, cx| {
11988 editor.change_selections(
11989 SelectionEffects::scroll(Autoscroll::Next),
11990 window,
11991 cx,
11992 |s| s.select_ranges(Some(10..10)),
11993 );
11994 editor.insert("// edited", window, cx);
11995 });
11996
11997 // Verify that only buffer 1 is dirty
11998 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11999 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12000 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12001
12002 // Get write counts after file creation (files were created with initial content)
12003 // We expect each file to have been written once during creation
12004 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12005 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12006 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12007
12008 // Perform autosave
12009 let save_task = editor.update_in(cx, |editor, window, cx| {
12010 editor.save(
12011 SaveOptions {
12012 format: true,
12013 autosave: true,
12014 },
12015 project.clone(),
12016 window,
12017 cx,
12018 )
12019 });
12020 save_task.await.unwrap();
12021
12022 // Only the dirty buffer should have been saved
12023 assert_eq!(
12024 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12025 1,
12026 "Buffer 1 was dirty, so it should have been written once during autosave"
12027 );
12028 assert_eq!(
12029 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12030 0,
12031 "Buffer 2 was clean, so it should not have been written during autosave"
12032 );
12033 assert_eq!(
12034 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12035 0,
12036 "Buffer 3 was clean, so it should not have been written during autosave"
12037 );
12038
12039 // Verify buffer states after autosave
12040 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12041 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12042 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12043
12044 // Now perform a manual save (format = true)
12045 let save_task = editor.update_in(cx, |editor, window, cx| {
12046 editor.save(
12047 SaveOptions {
12048 format: true,
12049 autosave: false,
12050 },
12051 project.clone(),
12052 window,
12053 cx,
12054 )
12055 });
12056 save_task.await.unwrap();
12057
12058 // During manual save, clean buffers don't get written to disk
12059 // They just get did_save called for language server notifications
12060 assert_eq!(
12061 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12062 1,
12063 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12064 );
12065 assert_eq!(
12066 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12067 0,
12068 "Buffer 2 should not have been written at all"
12069 );
12070 assert_eq!(
12071 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12072 0,
12073 "Buffer 3 should not have been written at all"
12074 );
12075}
12076
12077async fn setup_range_format_test(
12078 cx: &mut TestAppContext,
12079) -> (
12080 Entity<Project>,
12081 Entity<Editor>,
12082 &mut gpui::VisualTestContext,
12083 lsp::FakeLanguageServer,
12084) {
12085 init_test(cx, |_| {});
12086
12087 let fs = FakeFs::new(cx.executor());
12088 fs.insert_file(path!("/file.rs"), Default::default()).await;
12089
12090 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12091
12092 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12093 language_registry.add(rust_lang());
12094 let mut fake_servers = language_registry.register_fake_lsp(
12095 "Rust",
12096 FakeLspAdapter {
12097 capabilities: lsp::ServerCapabilities {
12098 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12099 ..lsp::ServerCapabilities::default()
12100 },
12101 ..FakeLspAdapter::default()
12102 },
12103 );
12104
12105 let buffer = project
12106 .update(cx, |project, cx| {
12107 project.open_local_buffer(path!("/file.rs"), cx)
12108 })
12109 .await
12110 .unwrap();
12111
12112 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12113 let (editor, cx) = cx.add_window_view(|window, cx| {
12114 build_editor_with_project(project.clone(), buffer, window, cx)
12115 });
12116
12117 cx.executor().start_waiting();
12118 let fake_server = fake_servers.next().await.unwrap();
12119
12120 (project, editor, cx, fake_server)
12121}
12122
12123#[gpui::test]
12124async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12125 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12126
12127 editor.update_in(cx, |editor, window, cx| {
12128 editor.set_text("one\ntwo\nthree\n", window, cx)
12129 });
12130 assert!(cx.read(|cx| editor.is_dirty(cx)));
12131
12132 let save = editor
12133 .update_in(cx, |editor, window, cx| {
12134 editor.save(
12135 SaveOptions {
12136 format: true,
12137 autosave: false,
12138 },
12139 project.clone(),
12140 window,
12141 cx,
12142 )
12143 })
12144 .unwrap();
12145 fake_server
12146 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12147 assert_eq!(
12148 params.text_document.uri,
12149 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12150 );
12151 assert_eq!(params.options.tab_size, 4);
12152 Ok(Some(vec![lsp::TextEdit::new(
12153 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12154 ", ".to_string(),
12155 )]))
12156 })
12157 .next()
12158 .await;
12159 cx.executor().start_waiting();
12160 save.await;
12161 assert_eq!(
12162 editor.update(cx, |editor, cx| editor.text(cx)),
12163 "one, two\nthree\n"
12164 );
12165 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12166}
12167
12168#[gpui::test]
12169async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12170 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12171
12172 editor.update_in(cx, |editor, window, cx| {
12173 editor.set_text("one\ntwo\nthree\n", window, cx)
12174 });
12175 assert!(cx.read(|cx| editor.is_dirty(cx)));
12176
12177 // Test that save still works when formatting hangs
12178 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12179 move |params, _| async move {
12180 assert_eq!(
12181 params.text_document.uri,
12182 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12183 );
12184 futures::future::pending::<()>().await;
12185 unreachable!()
12186 },
12187 );
12188 let save = editor
12189 .update_in(cx, |editor, window, cx| {
12190 editor.save(
12191 SaveOptions {
12192 format: true,
12193 autosave: false,
12194 },
12195 project.clone(),
12196 window,
12197 cx,
12198 )
12199 })
12200 .unwrap();
12201 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12202 cx.executor().start_waiting();
12203 save.await;
12204 assert_eq!(
12205 editor.update(cx, |editor, cx| editor.text(cx)),
12206 "one\ntwo\nthree\n"
12207 );
12208 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12209}
12210
12211#[gpui::test]
12212async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12213 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12214
12215 // Buffer starts clean, no formatting should be requested
12216 let save = editor
12217 .update_in(cx, |editor, window, cx| {
12218 editor.save(
12219 SaveOptions {
12220 format: false,
12221 autosave: false,
12222 },
12223 project.clone(),
12224 window,
12225 cx,
12226 )
12227 })
12228 .unwrap();
12229 let _pending_format_request = fake_server
12230 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12231 panic!("Should not be invoked");
12232 })
12233 .next();
12234 cx.executor().start_waiting();
12235 save.await;
12236 cx.run_until_parked();
12237}
12238
12239#[gpui::test]
12240async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12241 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12242
12243 // Set Rust language override and assert overridden tabsize is sent to language server
12244 update_test_language_settings(cx, |settings| {
12245 settings.languages.0.insert(
12246 "Rust".into(),
12247 LanguageSettingsContent {
12248 tab_size: NonZeroU32::new(8),
12249 ..Default::default()
12250 },
12251 );
12252 });
12253
12254 editor.update_in(cx, |editor, window, cx| {
12255 editor.set_text("something_new\n", window, cx)
12256 });
12257 assert!(cx.read(|cx| editor.is_dirty(cx)));
12258 let save = editor
12259 .update_in(cx, |editor, window, cx| {
12260 editor.save(
12261 SaveOptions {
12262 format: true,
12263 autosave: false,
12264 },
12265 project.clone(),
12266 window,
12267 cx,
12268 )
12269 })
12270 .unwrap();
12271 fake_server
12272 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12273 assert_eq!(
12274 params.text_document.uri,
12275 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12276 );
12277 assert_eq!(params.options.tab_size, 8);
12278 Ok(Some(Vec::new()))
12279 })
12280 .next()
12281 .await;
12282 save.await;
12283}
12284
12285#[gpui::test]
12286async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12287 init_test(cx, |settings| {
12288 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12289 settings::LanguageServerFormatterSpecifier::Current,
12290 )))
12291 });
12292
12293 let fs = FakeFs::new(cx.executor());
12294 fs.insert_file(path!("/file.rs"), Default::default()).await;
12295
12296 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12297
12298 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12299 language_registry.add(Arc::new(Language::new(
12300 LanguageConfig {
12301 name: "Rust".into(),
12302 matcher: LanguageMatcher {
12303 path_suffixes: vec!["rs".to_string()],
12304 ..Default::default()
12305 },
12306 ..LanguageConfig::default()
12307 },
12308 Some(tree_sitter_rust::LANGUAGE.into()),
12309 )));
12310 update_test_language_settings(cx, |settings| {
12311 // Enable Prettier formatting for the same buffer, and ensure
12312 // LSP is called instead of Prettier.
12313 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12314 });
12315 let mut fake_servers = language_registry.register_fake_lsp(
12316 "Rust",
12317 FakeLspAdapter {
12318 capabilities: lsp::ServerCapabilities {
12319 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12320 ..Default::default()
12321 },
12322 ..Default::default()
12323 },
12324 );
12325
12326 let buffer = project
12327 .update(cx, |project, cx| {
12328 project.open_local_buffer(path!("/file.rs"), cx)
12329 })
12330 .await
12331 .unwrap();
12332
12333 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12334 let (editor, cx) = cx.add_window_view(|window, cx| {
12335 build_editor_with_project(project.clone(), buffer, window, cx)
12336 });
12337 editor.update_in(cx, |editor, window, cx| {
12338 editor.set_text("one\ntwo\nthree\n", window, cx)
12339 });
12340
12341 cx.executor().start_waiting();
12342 let fake_server = fake_servers.next().await.unwrap();
12343
12344 let format = editor
12345 .update_in(cx, |editor, window, cx| {
12346 editor.perform_format(
12347 project.clone(),
12348 FormatTrigger::Manual,
12349 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12350 window,
12351 cx,
12352 )
12353 })
12354 .unwrap();
12355 fake_server
12356 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12357 assert_eq!(
12358 params.text_document.uri,
12359 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12360 );
12361 assert_eq!(params.options.tab_size, 4);
12362 Ok(Some(vec![lsp::TextEdit::new(
12363 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12364 ", ".to_string(),
12365 )]))
12366 })
12367 .next()
12368 .await;
12369 cx.executor().start_waiting();
12370 format.await;
12371 assert_eq!(
12372 editor.update(cx, |editor, cx| editor.text(cx)),
12373 "one, two\nthree\n"
12374 );
12375
12376 editor.update_in(cx, |editor, window, cx| {
12377 editor.set_text("one\ntwo\nthree\n", window, cx)
12378 });
12379 // Ensure we don't lock if formatting hangs.
12380 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12381 move |params, _| async move {
12382 assert_eq!(
12383 params.text_document.uri,
12384 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12385 );
12386 futures::future::pending::<()>().await;
12387 unreachable!()
12388 },
12389 );
12390 let format = editor
12391 .update_in(cx, |editor, window, cx| {
12392 editor.perform_format(
12393 project,
12394 FormatTrigger::Manual,
12395 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12396 window,
12397 cx,
12398 )
12399 })
12400 .unwrap();
12401 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12402 cx.executor().start_waiting();
12403 format.await;
12404 assert_eq!(
12405 editor.update(cx, |editor, cx| editor.text(cx)),
12406 "one\ntwo\nthree\n"
12407 );
12408}
12409
12410#[gpui::test]
12411async fn test_multiple_formatters(cx: &mut TestAppContext) {
12412 init_test(cx, |settings| {
12413 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12414 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12415 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12416 Formatter::CodeAction("code-action-1".into()),
12417 Formatter::CodeAction("code-action-2".into()),
12418 ]))
12419 });
12420
12421 let fs = FakeFs::new(cx.executor());
12422 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12423 .await;
12424
12425 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12426 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12427 language_registry.add(rust_lang());
12428
12429 let mut fake_servers = language_registry.register_fake_lsp(
12430 "Rust",
12431 FakeLspAdapter {
12432 capabilities: lsp::ServerCapabilities {
12433 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12434 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12435 commands: vec!["the-command-for-code-action-1".into()],
12436 ..Default::default()
12437 }),
12438 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12439 ..Default::default()
12440 },
12441 ..Default::default()
12442 },
12443 );
12444
12445 let buffer = project
12446 .update(cx, |project, cx| {
12447 project.open_local_buffer(path!("/file.rs"), cx)
12448 })
12449 .await
12450 .unwrap();
12451
12452 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12453 let (editor, cx) = cx.add_window_view(|window, cx| {
12454 build_editor_with_project(project.clone(), buffer, window, cx)
12455 });
12456
12457 cx.executor().start_waiting();
12458
12459 let fake_server = fake_servers.next().await.unwrap();
12460 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12461 move |_params, _| async move {
12462 Ok(Some(vec![lsp::TextEdit::new(
12463 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12464 "applied-formatting\n".to_string(),
12465 )]))
12466 },
12467 );
12468 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12469 move |params, _| async move {
12470 let requested_code_actions = params.context.only.expect("Expected code action request");
12471 assert_eq!(requested_code_actions.len(), 1);
12472
12473 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12474 let code_action = match requested_code_actions[0].as_str() {
12475 "code-action-1" => lsp::CodeAction {
12476 kind: Some("code-action-1".into()),
12477 edit: Some(lsp::WorkspaceEdit::new(
12478 [(
12479 uri,
12480 vec![lsp::TextEdit::new(
12481 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12482 "applied-code-action-1-edit\n".to_string(),
12483 )],
12484 )]
12485 .into_iter()
12486 .collect(),
12487 )),
12488 command: Some(lsp::Command {
12489 command: "the-command-for-code-action-1".into(),
12490 ..Default::default()
12491 }),
12492 ..Default::default()
12493 },
12494 "code-action-2" => lsp::CodeAction {
12495 kind: Some("code-action-2".into()),
12496 edit: Some(lsp::WorkspaceEdit::new(
12497 [(
12498 uri,
12499 vec![lsp::TextEdit::new(
12500 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12501 "applied-code-action-2-edit\n".to_string(),
12502 )],
12503 )]
12504 .into_iter()
12505 .collect(),
12506 )),
12507 ..Default::default()
12508 },
12509 req => panic!("Unexpected code action request: {:?}", req),
12510 };
12511 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12512 code_action,
12513 )]))
12514 },
12515 );
12516
12517 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12518 move |params, _| async move { Ok(params) }
12519 });
12520
12521 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12522 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12523 let fake = fake_server.clone();
12524 let lock = command_lock.clone();
12525 move |params, _| {
12526 assert_eq!(params.command, "the-command-for-code-action-1");
12527 let fake = fake.clone();
12528 let lock = lock.clone();
12529 async move {
12530 lock.lock().await;
12531 fake.server
12532 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12533 label: None,
12534 edit: lsp::WorkspaceEdit {
12535 changes: Some(
12536 [(
12537 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12538 vec![lsp::TextEdit {
12539 range: lsp::Range::new(
12540 lsp::Position::new(0, 0),
12541 lsp::Position::new(0, 0),
12542 ),
12543 new_text: "applied-code-action-1-command\n".into(),
12544 }],
12545 )]
12546 .into_iter()
12547 .collect(),
12548 ),
12549 ..Default::default()
12550 },
12551 })
12552 .await
12553 .into_response()
12554 .unwrap();
12555 Ok(Some(json!(null)))
12556 }
12557 }
12558 });
12559
12560 cx.executor().start_waiting();
12561 editor
12562 .update_in(cx, |editor, window, cx| {
12563 editor.perform_format(
12564 project.clone(),
12565 FormatTrigger::Manual,
12566 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12567 window,
12568 cx,
12569 )
12570 })
12571 .unwrap()
12572 .await;
12573 editor.update(cx, |editor, cx| {
12574 assert_eq!(
12575 editor.text(cx),
12576 r#"
12577 applied-code-action-2-edit
12578 applied-code-action-1-command
12579 applied-code-action-1-edit
12580 applied-formatting
12581 one
12582 two
12583 three
12584 "#
12585 .unindent()
12586 );
12587 });
12588
12589 editor.update_in(cx, |editor, window, cx| {
12590 editor.undo(&Default::default(), window, cx);
12591 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12592 });
12593
12594 // Perform a manual edit while waiting for an LSP command
12595 // that's being run as part of a formatting code action.
12596 let lock_guard = command_lock.lock().await;
12597 let format = editor
12598 .update_in(cx, |editor, window, cx| {
12599 editor.perform_format(
12600 project.clone(),
12601 FormatTrigger::Manual,
12602 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12603 window,
12604 cx,
12605 )
12606 })
12607 .unwrap();
12608 cx.run_until_parked();
12609 editor.update(cx, |editor, cx| {
12610 assert_eq!(
12611 editor.text(cx),
12612 r#"
12613 applied-code-action-1-edit
12614 applied-formatting
12615 one
12616 two
12617 three
12618 "#
12619 .unindent()
12620 );
12621
12622 editor.buffer.update(cx, |buffer, cx| {
12623 let ix = buffer.len(cx);
12624 buffer.edit([(ix..ix, "edited\n")], None, cx);
12625 });
12626 });
12627
12628 // Allow the LSP command to proceed. Because the buffer was edited,
12629 // the second code action will not be run.
12630 drop(lock_guard);
12631 format.await;
12632 editor.update_in(cx, |editor, window, cx| {
12633 assert_eq!(
12634 editor.text(cx),
12635 r#"
12636 applied-code-action-1-command
12637 applied-code-action-1-edit
12638 applied-formatting
12639 one
12640 two
12641 three
12642 edited
12643 "#
12644 .unindent()
12645 );
12646
12647 // The manual edit is undone first, because it is the last thing the user did
12648 // (even though the command completed afterwards).
12649 editor.undo(&Default::default(), window, cx);
12650 assert_eq!(
12651 editor.text(cx),
12652 r#"
12653 applied-code-action-1-command
12654 applied-code-action-1-edit
12655 applied-formatting
12656 one
12657 two
12658 three
12659 "#
12660 .unindent()
12661 );
12662
12663 // All the formatting (including the command, which completed after the manual edit)
12664 // is undone together.
12665 editor.undo(&Default::default(), window, cx);
12666 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12667 });
12668}
12669
12670#[gpui::test]
12671async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12672 init_test(cx, |settings| {
12673 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12674 settings::LanguageServerFormatterSpecifier::Current,
12675 )]))
12676 });
12677
12678 let fs = FakeFs::new(cx.executor());
12679 fs.insert_file(path!("/file.ts"), Default::default()).await;
12680
12681 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12682
12683 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12684 language_registry.add(Arc::new(Language::new(
12685 LanguageConfig {
12686 name: "TypeScript".into(),
12687 matcher: LanguageMatcher {
12688 path_suffixes: vec!["ts".to_string()],
12689 ..Default::default()
12690 },
12691 ..LanguageConfig::default()
12692 },
12693 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12694 )));
12695 update_test_language_settings(cx, |settings| {
12696 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12697 });
12698 let mut fake_servers = language_registry.register_fake_lsp(
12699 "TypeScript",
12700 FakeLspAdapter {
12701 capabilities: lsp::ServerCapabilities {
12702 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12703 ..Default::default()
12704 },
12705 ..Default::default()
12706 },
12707 );
12708
12709 let buffer = project
12710 .update(cx, |project, cx| {
12711 project.open_local_buffer(path!("/file.ts"), cx)
12712 })
12713 .await
12714 .unwrap();
12715
12716 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12717 let (editor, cx) = cx.add_window_view(|window, cx| {
12718 build_editor_with_project(project.clone(), buffer, window, cx)
12719 });
12720 editor.update_in(cx, |editor, window, cx| {
12721 editor.set_text(
12722 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12723 window,
12724 cx,
12725 )
12726 });
12727
12728 cx.executor().start_waiting();
12729 let fake_server = fake_servers.next().await.unwrap();
12730
12731 let format = editor
12732 .update_in(cx, |editor, window, cx| {
12733 editor.perform_code_action_kind(
12734 project.clone(),
12735 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12736 window,
12737 cx,
12738 )
12739 })
12740 .unwrap();
12741 fake_server
12742 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12743 assert_eq!(
12744 params.text_document.uri,
12745 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12746 );
12747 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12748 lsp::CodeAction {
12749 title: "Organize Imports".to_string(),
12750 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12751 edit: Some(lsp::WorkspaceEdit {
12752 changes: Some(
12753 [(
12754 params.text_document.uri.clone(),
12755 vec![lsp::TextEdit::new(
12756 lsp::Range::new(
12757 lsp::Position::new(1, 0),
12758 lsp::Position::new(2, 0),
12759 ),
12760 "".to_string(),
12761 )],
12762 )]
12763 .into_iter()
12764 .collect(),
12765 ),
12766 ..Default::default()
12767 }),
12768 ..Default::default()
12769 },
12770 )]))
12771 })
12772 .next()
12773 .await;
12774 cx.executor().start_waiting();
12775 format.await;
12776 assert_eq!(
12777 editor.update(cx, |editor, cx| editor.text(cx)),
12778 "import { a } from 'module';\n\nconst x = a;\n"
12779 );
12780
12781 editor.update_in(cx, |editor, window, cx| {
12782 editor.set_text(
12783 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12784 window,
12785 cx,
12786 )
12787 });
12788 // Ensure we don't lock if code action hangs.
12789 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12790 move |params, _| async move {
12791 assert_eq!(
12792 params.text_document.uri,
12793 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12794 );
12795 futures::future::pending::<()>().await;
12796 unreachable!()
12797 },
12798 );
12799 let format = editor
12800 .update_in(cx, |editor, window, cx| {
12801 editor.perform_code_action_kind(
12802 project,
12803 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12804 window,
12805 cx,
12806 )
12807 })
12808 .unwrap();
12809 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12810 cx.executor().start_waiting();
12811 format.await;
12812 assert_eq!(
12813 editor.update(cx, |editor, cx| editor.text(cx)),
12814 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12815 );
12816}
12817
12818#[gpui::test]
12819async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12820 init_test(cx, |_| {});
12821
12822 let mut cx = EditorLspTestContext::new_rust(
12823 lsp::ServerCapabilities {
12824 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12825 ..Default::default()
12826 },
12827 cx,
12828 )
12829 .await;
12830
12831 cx.set_state(indoc! {"
12832 one.twoˇ
12833 "});
12834
12835 // The format request takes a long time. When it completes, it inserts
12836 // a newline and an indent before the `.`
12837 cx.lsp
12838 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12839 let executor = cx.background_executor().clone();
12840 async move {
12841 executor.timer(Duration::from_millis(100)).await;
12842 Ok(Some(vec![lsp::TextEdit {
12843 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12844 new_text: "\n ".into(),
12845 }]))
12846 }
12847 });
12848
12849 // Submit a format request.
12850 let format_1 = cx
12851 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12852 .unwrap();
12853 cx.executor().run_until_parked();
12854
12855 // Submit a second format request.
12856 let format_2 = cx
12857 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12858 .unwrap();
12859 cx.executor().run_until_parked();
12860
12861 // Wait for both format requests to complete
12862 cx.executor().advance_clock(Duration::from_millis(200));
12863 cx.executor().start_waiting();
12864 format_1.await.unwrap();
12865 cx.executor().start_waiting();
12866 format_2.await.unwrap();
12867
12868 // The formatting edits only happens once.
12869 cx.assert_editor_state(indoc! {"
12870 one
12871 .twoˇ
12872 "});
12873}
12874
12875#[gpui::test]
12876async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12877 init_test(cx, |settings| {
12878 settings.defaults.formatter = Some(FormatterList::default())
12879 });
12880
12881 let mut cx = EditorLspTestContext::new_rust(
12882 lsp::ServerCapabilities {
12883 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12884 ..Default::default()
12885 },
12886 cx,
12887 )
12888 .await;
12889
12890 // Record which buffer changes have been sent to the language server
12891 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12892 cx.lsp
12893 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12894 let buffer_changes = buffer_changes.clone();
12895 move |params, _| {
12896 buffer_changes.lock().extend(
12897 params
12898 .content_changes
12899 .into_iter()
12900 .map(|e| (e.range.unwrap(), e.text)),
12901 );
12902 }
12903 });
12904 // Handle formatting requests to the language server.
12905 cx.lsp
12906 .set_request_handler::<lsp::request::Formatting, _, _>({
12907 let buffer_changes = buffer_changes.clone();
12908 move |_, _| {
12909 let buffer_changes = buffer_changes.clone();
12910 // Insert blank lines between each line of the buffer.
12911 async move {
12912 // When formatting is requested, trailing whitespace has already been stripped,
12913 // and the trailing newline has already been added.
12914 assert_eq!(
12915 &buffer_changes.lock()[1..],
12916 &[
12917 (
12918 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12919 "".into()
12920 ),
12921 (
12922 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12923 "".into()
12924 ),
12925 (
12926 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12927 "\n".into()
12928 ),
12929 ]
12930 );
12931
12932 Ok(Some(vec![
12933 lsp::TextEdit {
12934 range: lsp::Range::new(
12935 lsp::Position::new(1, 0),
12936 lsp::Position::new(1, 0),
12937 ),
12938 new_text: "\n".into(),
12939 },
12940 lsp::TextEdit {
12941 range: lsp::Range::new(
12942 lsp::Position::new(2, 0),
12943 lsp::Position::new(2, 0),
12944 ),
12945 new_text: "\n".into(),
12946 },
12947 ]))
12948 }
12949 }
12950 });
12951
12952 // Set up a buffer white some trailing whitespace and no trailing newline.
12953 cx.set_state(
12954 &[
12955 "one ", //
12956 "twoˇ", //
12957 "three ", //
12958 "four", //
12959 ]
12960 .join("\n"),
12961 );
12962 cx.run_until_parked();
12963
12964 // Submit a format request.
12965 let format = cx
12966 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12967 .unwrap();
12968
12969 cx.run_until_parked();
12970 // After formatting the buffer, the trailing whitespace is stripped,
12971 // a newline is appended, and the edits provided by the language server
12972 // have been applied.
12973 format.await.unwrap();
12974
12975 cx.assert_editor_state(
12976 &[
12977 "one", //
12978 "", //
12979 "twoˇ", //
12980 "", //
12981 "three", //
12982 "four", //
12983 "", //
12984 ]
12985 .join("\n"),
12986 );
12987
12988 // Undoing the formatting undoes the trailing whitespace removal, the
12989 // trailing newline, and the LSP edits.
12990 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12991 cx.assert_editor_state(
12992 &[
12993 "one ", //
12994 "twoˇ", //
12995 "three ", //
12996 "four", //
12997 ]
12998 .join("\n"),
12999 );
13000}
13001
13002#[gpui::test]
13003async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13004 cx: &mut TestAppContext,
13005) {
13006 init_test(cx, |_| {});
13007
13008 cx.update(|cx| {
13009 cx.update_global::<SettingsStore, _>(|settings, cx| {
13010 settings.update_user_settings(cx, |settings| {
13011 settings.editor.auto_signature_help = Some(true);
13012 });
13013 });
13014 });
13015
13016 let mut cx = EditorLspTestContext::new_rust(
13017 lsp::ServerCapabilities {
13018 signature_help_provider: Some(lsp::SignatureHelpOptions {
13019 ..Default::default()
13020 }),
13021 ..Default::default()
13022 },
13023 cx,
13024 )
13025 .await;
13026
13027 let language = Language::new(
13028 LanguageConfig {
13029 name: "Rust".into(),
13030 brackets: BracketPairConfig {
13031 pairs: vec![
13032 BracketPair {
13033 start: "{".to_string(),
13034 end: "}".to_string(),
13035 close: true,
13036 surround: true,
13037 newline: true,
13038 },
13039 BracketPair {
13040 start: "(".to_string(),
13041 end: ")".to_string(),
13042 close: true,
13043 surround: true,
13044 newline: true,
13045 },
13046 BracketPair {
13047 start: "/*".to_string(),
13048 end: " */".to_string(),
13049 close: true,
13050 surround: true,
13051 newline: true,
13052 },
13053 BracketPair {
13054 start: "[".to_string(),
13055 end: "]".to_string(),
13056 close: false,
13057 surround: false,
13058 newline: true,
13059 },
13060 BracketPair {
13061 start: "\"".to_string(),
13062 end: "\"".to_string(),
13063 close: true,
13064 surround: true,
13065 newline: false,
13066 },
13067 BracketPair {
13068 start: "<".to_string(),
13069 end: ">".to_string(),
13070 close: false,
13071 surround: true,
13072 newline: true,
13073 },
13074 ],
13075 ..Default::default()
13076 },
13077 autoclose_before: "})]".to_string(),
13078 ..Default::default()
13079 },
13080 Some(tree_sitter_rust::LANGUAGE.into()),
13081 );
13082 let language = Arc::new(language);
13083
13084 cx.language_registry().add(language.clone());
13085 cx.update_buffer(|buffer, cx| {
13086 buffer.set_language(Some(language), cx);
13087 });
13088
13089 cx.set_state(
13090 &r#"
13091 fn main() {
13092 sampleˇ
13093 }
13094 "#
13095 .unindent(),
13096 );
13097
13098 cx.update_editor(|editor, window, cx| {
13099 editor.handle_input("(", window, cx);
13100 });
13101 cx.assert_editor_state(
13102 &"
13103 fn main() {
13104 sample(ˇ)
13105 }
13106 "
13107 .unindent(),
13108 );
13109
13110 let mocked_response = lsp::SignatureHelp {
13111 signatures: vec![lsp::SignatureInformation {
13112 label: "fn sample(param1: u8, param2: u8)".to_string(),
13113 documentation: None,
13114 parameters: Some(vec![
13115 lsp::ParameterInformation {
13116 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13117 documentation: None,
13118 },
13119 lsp::ParameterInformation {
13120 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13121 documentation: None,
13122 },
13123 ]),
13124 active_parameter: None,
13125 }],
13126 active_signature: Some(0),
13127 active_parameter: Some(0),
13128 };
13129 handle_signature_help_request(&mut cx, mocked_response).await;
13130
13131 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13132 .await;
13133
13134 cx.editor(|editor, _, _| {
13135 let signature_help_state = editor.signature_help_state.popover().cloned();
13136 let signature = signature_help_state.unwrap();
13137 assert_eq!(
13138 signature.signatures[signature.current_signature].label,
13139 "fn sample(param1: u8, param2: u8)"
13140 );
13141 });
13142}
13143
13144#[gpui::test]
13145async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13146 init_test(cx, |_| {});
13147
13148 cx.update(|cx| {
13149 cx.update_global::<SettingsStore, _>(|settings, cx| {
13150 settings.update_user_settings(cx, |settings| {
13151 settings.editor.auto_signature_help = Some(false);
13152 settings.editor.show_signature_help_after_edits = Some(false);
13153 });
13154 });
13155 });
13156
13157 let mut cx = EditorLspTestContext::new_rust(
13158 lsp::ServerCapabilities {
13159 signature_help_provider: Some(lsp::SignatureHelpOptions {
13160 ..Default::default()
13161 }),
13162 ..Default::default()
13163 },
13164 cx,
13165 )
13166 .await;
13167
13168 let language = Language::new(
13169 LanguageConfig {
13170 name: "Rust".into(),
13171 brackets: BracketPairConfig {
13172 pairs: vec![
13173 BracketPair {
13174 start: "{".to_string(),
13175 end: "}".to_string(),
13176 close: true,
13177 surround: true,
13178 newline: true,
13179 },
13180 BracketPair {
13181 start: "(".to_string(),
13182 end: ")".to_string(),
13183 close: true,
13184 surround: true,
13185 newline: true,
13186 },
13187 BracketPair {
13188 start: "/*".to_string(),
13189 end: " */".to_string(),
13190 close: true,
13191 surround: true,
13192 newline: true,
13193 },
13194 BracketPair {
13195 start: "[".to_string(),
13196 end: "]".to_string(),
13197 close: false,
13198 surround: false,
13199 newline: true,
13200 },
13201 BracketPair {
13202 start: "\"".to_string(),
13203 end: "\"".to_string(),
13204 close: true,
13205 surround: true,
13206 newline: false,
13207 },
13208 BracketPair {
13209 start: "<".to_string(),
13210 end: ">".to_string(),
13211 close: false,
13212 surround: true,
13213 newline: true,
13214 },
13215 ],
13216 ..Default::default()
13217 },
13218 autoclose_before: "})]".to_string(),
13219 ..Default::default()
13220 },
13221 Some(tree_sitter_rust::LANGUAGE.into()),
13222 );
13223 let language = Arc::new(language);
13224
13225 cx.language_registry().add(language.clone());
13226 cx.update_buffer(|buffer, cx| {
13227 buffer.set_language(Some(language), cx);
13228 });
13229
13230 // Ensure that signature_help is not called when no signature help is enabled.
13231 cx.set_state(
13232 &r#"
13233 fn main() {
13234 sampleˇ
13235 }
13236 "#
13237 .unindent(),
13238 );
13239 cx.update_editor(|editor, window, cx| {
13240 editor.handle_input("(", window, cx);
13241 });
13242 cx.assert_editor_state(
13243 &"
13244 fn main() {
13245 sample(ˇ)
13246 }
13247 "
13248 .unindent(),
13249 );
13250 cx.editor(|editor, _, _| {
13251 assert!(editor.signature_help_state.task().is_none());
13252 });
13253
13254 let mocked_response = lsp::SignatureHelp {
13255 signatures: vec![lsp::SignatureInformation {
13256 label: "fn sample(param1: u8, param2: u8)".to_string(),
13257 documentation: None,
13258 parameters: Some(vec![
13259 lsp::ParameterInformation {
13260 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13261 documentation: None,
13262 },
13263 lsp::ParameterInformation {
13264 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13265 documentation: None,
13266 },
13267 ]),
13268 active_parameter: None,
13269 }],
13270 active_signature: Some(0),
13271 active_parameter: Some(0),
13272 };
13273
13274 // Ensure that signature_help is called when enabled afte edits
13275 cx.update(|_, cx| {
13276 cx.update_global::<SettingsStore, _>(|settings, cx| {
13277 settings.update_user_settings(cx, |settings| {
13278 settings.editor.auto_signature_help = Some(false);
13279 settings.editor.show_signature_help_after_edits = Some(true);
13280 });
13281 });
13282 });
13283 cx.set_state(
13284 &r#"
13285 fn main() {
13286 sampleˇ
13287 }
13288 "#
13289 .unindent(),
13290 );
13291 cx.update_editor(|editor, window, cx| {
13292 editor.handle_input("(", window, cx);
13293 });
13294 cx.assert_editor_state(
13295 &"
13296 fn main() {
13297 sample(ˇ)
13298 }
13299 "
13300 .unindent(),
13301 );
13302 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13303 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13304 .await;
13305 cx.update_editor(|editor, _, _| {
13306 let signature_help_state = editor.signature_help_state.popover().cloned();
13307 assert!(signature_help_state.is_some());
13308 let signature = signature_help_state.unwrap();
13309 assert_eq!(
13310 signature.signatures[signature.current_signature].label,
13311 "fn sample(param1: u8, param2: u8)"
13312 );
13313 editor.signature_help_state = SignatureHelpState::default();
13314 });
13315
13316 // Ensure that signature_help is called when auto signature help override is enabled
13317 cx.update(|_, cx| {
13318 cx.update_global::<SettingsStore, _>(|settings, cx| {
13319 settings.update_user_settings(cx, |settings| {
13320 settings.editor.auto_signature_help = Some(true);
13321 settings.editor.show_signature_help_after_edits = Some(false);
13322 });
13323 });
13324 });
13325 cx.set_state(
13326 &r#"
13327 fn main() {
13328 sampleˇ
13329 }
13330 "#
13331 .unindent(),
13332 );
13333 cx.update_editor(|editor, window, cx| {
13334 editor.handle_input("(", window, cx);
13335 });
13336 cx.assert_editor_state(
13337 &"
13338 fn main() {
13339 sample(ˇ)
13340 }
13341 "
13342 .unindent(),
13343 );
13344 handle_signature_help_request(&mut cx, mocked_response).await;
13345 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13346 .await;
13347 cx.editor(|editor, _, _| {
13348 let signature_help_state = editor.signature_help_state.popover().cloned();
13349 assert!(signature_help_state.is_some());
13350 let signature = signature_help_state.unwrap();
13351 assert_eq!(
13352 signature.signatures[signature.current_signature].label,
13353 "fn sample(param1: u8, param2: u8)"
13354 );
13355 });
13356}
13357
13358#[gpui::test]
13359async fn test_signature_help(cx: &mut TestAppContext) {
13360 init_test(cx, |_| {});
13361 cx.update(|cx| {
13362 cx.update_global::<SettingsStore, _>(|settings, cx| {
13363 settings.update_user_settings(cx, |settings| {
13364 settings.editor.auto_signature_help = Some(true);
13365 });
13366 });
13367 });
13368
13369 let mut cx = EditorLspTestContext::new_rust(
13370 lsp::ServerCapabilities {
13371 signature_help_provider: Some(lsp::SignatureHelpOptions {
13372 ..Default::default()
13373 }),
13374 ..Default::default()
13375 },
13376 cx,
13377 )
13378 .await;
13379
13380 // A test that directly calls `show_signature_help`
13381 cx.update_editor(|editor, window, cx| {
13382 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13383 });
13384
13385 let mocked_response = lsp::SignatureHelp {
13386 signatures: vec![lsp::SignatureInformation {
13387 label: "fn sample(param1: u8, param2: u8)".to_string(),
13388 documentation: None,
13389 parameters: Some(vec![
13390 lsp::ParameterInformation {
13391 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13392 documentation: None,
13393 },
13394 lsp::ParameterInformation {
13395 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13396 documentation: None,
13397 },
13398 ]),
13399 active_parameter: None,
13400 }],
13401 active_signature: Some(0),
13402 active_parameter: Some(0),
13403 };
13404 handle_signature_help_request(&mut cx, mocked_response).await;
13405
13406 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13407 .await;
13408
13409 cx.editor(|editor, _, _| {
13410 let signature_help_state = editor.signature_help_state.popover().cloned();
13411 assert!(signature_help_state.is_some());
13412 let signature = signature_help_state.unwrap();
13413 assert_eq!(
13414 signature.signatures[signature.current_signature].label,
13415 "fn sample(param1: u8, param2: u8)"
13416 );
13417 });
13418
13419 // When exiting outside from inside the brackets, `signature_help` is closed.
13420 cx.set_state(indoc! {"
13421 fn main() {
13422 sample(ˇ);
13423 }
13424
13425 fn sample(param1: u8, param2: u8) {}
13426 "});
13427
13428 cx.update_editor(|editor, window, cx| {
13429 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13430 s.select_ranges([0..0])
13431 });
13432 });
13433
13434 let mocked_response = lsp::SignatureHelp {
13435 signatures: Vec::new(),
13436 active_signature: None,
13437 active_parameter: None,
13438 };
13439 handle_signature_help_request(&mut cx, mocked_response).await;
13440
13441 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13442 .await;
13443
13444 cx.editor(|editor, _, _| {
13445 assert!(!editor.signature_help_state.is_shown());
13446 });
13447
13448 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13449 cx.set_state(indoc! {"
13450 fn main() {
13451 sample(ˇ);
13452 }
13453
13454 fn sample(param1: u8, param2: u8) {}
13455 "});
13456
13457 let mocked_response = lsp::SignatureHelp {
13458 signatures: vec![lsp::SignatureInformation {
13459 label: "fn sample(param1: u8, param2: u8)".to_string(),
13460 documentation: None,
13461 parameters: Some(vec![
13462 lsp::ParameterInformation {
13463 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13464 documentation: None,
13465 },
13466 lsp::ParameterInformation {
13467 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13468 documentation: None,
13469 },
13470 ]),
13471 active_parameter: None,
13472 }],
13473 active_signature: Some(0),
13474 active_parameter: Some(0),
13475 };
13476 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13477 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13478 .await;
13479 cx.editor(|editor, _, _| {
13480 assert!(editor.signature_help_state.is_shown());
13481 });
13482
13483 // Restore the popover with more parameter input
13484 cx.set_state(indoc! {"
13485 fn main() {
13486 sample(param1, param2ˇ);
13487 }
13488
13489 fn sample(param1: u8, param2: u8) {}
13490 "});
13491
13492 let mocked_response = lsp::SignatureHelp {
13493 signatures: vec![lsp::SignatureInformation {
13494 label: "fn sample(param1: u8, param2: u8)".to_string(),
13495 documentation: None,
13496 parameters: Some(vec![
13497 lsp::ParameterInformation {
13498 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13499 documentation: None,
13500 },
13501 lsp::ParameterInformation {
13502 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13503 documentation: None,
13504 },
13505 ]),
13506 active_parameter: None,
13507 }],
13508 active_signature: Some(0),
13509 active_parameter: Some(1),
13510 };
13511 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13512 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13513 .await;
13514
13515 // When selecting a range, the popover is gone.
13516 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13517 cx.update_editor(|editor, window, cx| {
13518 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13519 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13520 })
13521 });
13522 cx.assert_editor_state(indoc! {"
13523 fn main() {
13524 sample(param1, «ˇparam2»);
13525 }
13526
13527 fn sample(param1: u8, param2: u8) {}
13528 "});
13529 cx.editor(|editor, _, _| {
13530 assert!(!editor.signature_help_state.is_shown());
13531 });
13532
13533 // When unselecting again, the popover is back if within the brackets.
13534 cx.update_editor(|editor, window, cx| {
13535 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13536 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13537 })
13538 });
13539 cx.assert_editor_state(indoc! {"
13540 fn main() {
13541 sample(param1, ˇparam2);
13542 }
13543
13544 fn sample(param1: u8, param2: u8) {}
13545 "});
13546 handle_signature_help_request(&mut cx, mocked_response).await;
13547 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13548 .await;
13549 cx.editor(|editor, _, _| {
13550 assert!(editor.signature_help_state.is_shown());
13551 });
13552
13553 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13554 cx.update_editor(|editor, window, cx| {
13555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13556 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13557 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13558 })
13559 });
13560 cx.assert_editor_state(indoc! {"
13561 fn main() {
13562 sample(param1, ˇparam2);
13563 }
13564
13565 fn sample(param1: u8, param2: u8) {}
13566 "});
13567
13568 let mocked_response = lsp::SignatureHelp {
13569 signatures: vec![lsp::SignatureInformation {
13570 label: "fn sample(param1: u8, param2: u8)".to_string(),
13571 documentation: None,
13572 parameters: Some(vec![
13573 lsp::ParameterInformation {
13574 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13575 documentation: None,
13576 },
13577 lsp::ParameterInformation {
13578 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13579 documentation: None,
13580 },
13581 ]),
13582 active_parameter: None,
13583 }],
13584 active_signature: Some(0),
13585 active_parameter: Some(1),
13586 };
13587 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13588 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13589 .await;
13590 cx.update_editor(|editor, _, cx| {
13591 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13592 });
13593 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13594 .await;
13595 cx.update_editor(|editor, window, cx| {
13596 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13597 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13598 })
13599 });
13600 cx.assert_editor_state(indoc! {"
13601 fn main() {
13602 sample(param1, «ˇparam2»);
13603 }
13604
13605 fn sample(param1: u8, param2: u8) {}
13606 "});
13607 cx.update_editor(|editor, window, cx| {
13608 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13609 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13610 })
13611 });
13612 cx.assert_editor_state(indoc! {"
13613 fn main() {
13614 sample(param1, ˇparam2);
13615 }
13616
13617 fn sample(param1: u8, param2: u8) {}
13618 "});
13619 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13620 .await;
13621}
13622
13623#[gpui::test]
13624async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13625 init_test(cx, |_| {});
13626
13627 let mut cx = EditorLspTestContext::new_rust(
13628 lsp::ServerCapabilities {
13629 signature_help_provider: Some(lsp::SignatureHelpOptions {
13630 ..Default::default()
13631 }),
13632 ..Default::default()
13633 },
13634 cx,
13635 )
13636 .await;
13637
13638 cx.set_state(indoc! {"
13639 fn main() {
13640 overloadedˇ
13641 }
13642 "});
13643
13644 cx.update_editor(|editor, window, cx| {
13645 editor.handle_input("(", window, cx);
13646 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13647 });
13648
13649 // Mock response with 3 signatures
13650 let mocked_response = lsp::SignatureHelp {
13651 signatures: vec![
13652 lsp::SignatureInformation {
13653 label: "fn overloaded(x: i32)".to_string(),
13654 documentation: None,
13655 parameters: Some(vec![lsp::ParameterInformation {
13656 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13657 documentation: None,
13658 }]),
13659 active_parameter: None,
13660 },
13661 lsp::SignatureInformation {
13662 label: "fn overloaded(x: i32, y: i32)".to_string(),
13663 documentation: None,
13664 parameters: Some(vec![
13665 lsp::ParameterInformation {
13666 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13667 documentation: None,
13668 },
13669 lsp::ParameterInformation {
13670 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13671 documentation: None,
13672 },
13673 ]),
13674 active_parameter: None,
13675 },
13676 lsp::SignatureInformation {
13677 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13678 documentation: None,
13679 parameters: Some(vec![
13680 lsp::ParameterInformation {
13681 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13682 documentation: None,
13683 },
13684 lsp::ParameterInformation {
13685 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13686 documentation: None,
13687 },
13688 lsp::ParameterInformation {
13689 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13690 documentation: None,
13691 },
13692 ]),
13693 active_parameter: None,
13694 },
13695 ],
13696 active_signature: Some(1),
13697 active_parameter: Some(0),
13698 };
13699 handle_signature_help_request(&mut cx, mocked_response).await;
13700
13701 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13702 .await;
13703
13704 // Verify we have multiple signatures and the right one is selected
13705 cx.editor(|editor, _, _| {
13706 let popover = editor.signature_help_state.popover().cloned().unwrap();
13707 assert_eq!(popover.signatures.len(), 3);
13708 // active_signature was 1, so that should be the current
13709 assert_eq!(popover.current_signature, 1);
13710 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13711 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13712 assert_eq!(
13713 popover.signatures[2].label,
13714 "fn overloaded(x: i32, y: i32, z: i32)"
13715 );
13716 });
13717
13718 // Test navigation functionality
13719 cx.update_editor(|editor, window, cx| {
13720 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13721 });
13722
13723 cx.editor(|editor, _, _| {
13724 let popover = editor.signature_help_state.popover().cloned().unwrap();
13725 assert_eq!(popover.current_signature, 2);
13726 });
13727
13728 // Test wrap around
13729 cx.update_editor(|editor, window, cx| {
13730 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13731 });
13732
13733 cx.editor(|editor, _, _| {
13734 let popover = editor.signature_help_state.popover().cloned().unwrap();
13735 assert_eq!(popover.current_signature, 0);
13736 });
13737
13738 // Test previous navigation
13739 cx.update_editor(|editor, window, cx| {
13740 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13741 });
13742
13743 cx.editor(|editor, _, _| {
13744 let popover = editor.signature_help_state.popover().cloned().unwrap();
13745 assert_eq!(popover.current_signature, 2);
13746 });
13747}
13748
13749#[gpui::test]
13750async fn test_completion_mode(cx: &mut TestAppContext) {
13751 init_test(cx, |_| {});
13752 let mut cx = EditorLspTestContext::new_rust(
13753 lsp::ServerCapabilities {
13754 completion_provider: Some(lsp::CompletionOptions {
13755 resolve_provider: Some(true),
13756 ..Default::default()
13757 }),
13758 ..Default::default()
13759 },
13760 cx,
13761 )
13762 .await;
13763
13764 struct Run {
13765 run_description: &'static str,
13766 initial_state: String,
13767 buffer_marked_text: String,
13768 completion_label: &'static str,
13769 completion_text: &'static str,
13770 expected_with_insert_mode: String,
13771 expected_with_replace_mode: String,
13772 expected_with_replace_subsequence_mode: String,
13773 expected_with_replace_suffix_mode: String,
13774 }
13775
13776 let runs = [
13777 Run {
13778 run_description: "Start of word matches completion text",
13779 initial_state: "before ediˇ after".into(),
13780 buffer_marked_text: "before <edi|> after".into(),
13781 completion_label: "editor",
13782 completion_text: "editor",
13783 expected_with_insert_mode: "before editorˇ after".into(),
13784 expected_with_replace_mode: "before editorˇ after".into(),
13785 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13786 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13787 },
13788 Run {
13789 run_description: "Accept same text at the middle of the word",
13790 initial_state: "before ediˇtor after".into(),
13791 buffer_marked_text: "before <edi|tor> after".into(),
13792 completion_label: "editor",
13793 completion_text: "editor",
13794 expected_with_insert_mode: "before editorˇtor after".into(),
13795 expected_with_replace_mode: "before editorˇ after".into(),
13796 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13797 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13798 },
13799 Run {
13800 run_description: "End of word matches completion text -- cursor at end",
13801 initial_state: "before torˇ after".into(),
13802 buffer_marked_text: "before <tor|> after".into(),
13803 completion_label: "editor",
13804 completion_text: "editor",
13805 expected_with_insert_mode: "before editorˇ after".into(),
13806 expected_with_replace_mode: "before editorˇ after".into(),
13807 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13808 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13809 },
13810 Run {
13811 run_description: "End of word matches completion text -- cursor at start",
13812 initial_state: "before ˇtor after".into(),
13813 buffer_marked_text: "before <|tor> after".into(),
13814 completion_label: "editor",
13815 completion_text: "editor",
13816 expected_with_insert_mode: "before editorˇtor after".into(),
13817 expected_with_replace_mode: "before editorˇ after".into(),
13818 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13819 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13820 },
13821 Run {
13822 run_description: "Prepend text containing whitespace",
13823 initial_state: "pˇfield: bool".into(),
13824 buffer_marked_text: "<p|field>: bool".into(),
13825 completion_label: "pub ",
13826 completion_text: "pub ",
13827 expected_with_insert_mode: "pub ˇfield: bool".into(),
13828 expected_with_replace_mode: "pub ˇ: bool".into(),
13829 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13830 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13831 },
13832 Run {
13833 run_description: "Add element to start of list",
13834 initial_state: "[element_ˇelement_2]".into(),
13835 buffer_marked_text: "[<element_|element_2>]".into(),
13836 completion_label: "element_1",
13837 completion_text: "element_1",
13838 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13839 expected_with_replace_mode: "[element_1ˇ]".into(),
13840 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13841 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13842 },
13843 Run {
13844 run_description: "Add element to start of list -- first and second elements are equal",
13845 initial_state: "[elˇelement]".into(),
13846 buffer_marked_text: "[<el|element>]".into(),
13847 completion_label: "element",
13848 completion_text: "element",
13849 expected_with_insert_mode: "[elementˇelement]".into(),
13850 expected_with_replace_mode: "[elementˇ]".into(),
13851 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13852 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13853 },
13854 Run {
13855 run_description: "Ends with matching suffix",
13856 initial_state: "SubˇError".into(),
13857 buffer_marked_text: "<Sub|Error>".into(),
13858 completion_label: "SubscriptionError",
13859 completion_text: "SubscriptionError",
13860 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13861 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13862 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13863 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13864 },
13865 Run {
13866 run_description: "Suffix is a subsequence -- contiguous",
13867 initial_state: "SubˇErr".into(),
13868 buffer_marked_text: "<Sub|Err>".into(),
13869 completion_label: "SubscriptionError",
13870 completion_text: "SubscriptionError",
13871 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13872 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13873 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13874 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13875 },
13876 Run {
13877 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13878 initial_state: "Suˇscrirr".into(),
13879 buffer_marked_text: "<Su|scrirr>".into(),
13880 completion_label: "SubscriptionError",
13881 completion_text: "SubscriptionError",
13882 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13883 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13884 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13885 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13886 },
13887 Run {
13888 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13889 initial_state: "foo(indˇix)".into(),
13890 buffer_marked_text: "foo(<ind|ix>)".into(),
13891 completion_label: "node_index",
13892 completion_text: "node_index",
13893 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13894 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13895 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13896 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13897 },
13898 Run {
13899 run_description: "Replace range ends before cursor - should extend to cursor",
13900 initial_state: "before editˇo after".into(),
13901 buffer_marked_text: "before <{ed}>it|o after".into(),
13902 completion_label: "editor",
13903 completion_text: "editor",
13904 expected_with_insert_mode: "before editorˇo after".into(),
13905 expected_with_replace_mode: "before editorˇo after".into(),
13906 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13907 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13908 },
13909 Run {
13910 run_description: "Uses label for suffix matching",
13911 initial_state: "before ediˇtor after".into(),
13912 buffer_marked_text: "before <edi|tor> after".into(),
13913 completion_label: "editor",
13914 completion_text: "editor()",
13915 expected_with_insert_mode: "before editor()ˇtor after".into(),
13916 expected_with_replace_mode: "before editor()ˇ after".into(),
13917 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13918 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13919 },
13920 Run {
13921 run_description: "Case insensitive subsequence and suffix matching",
13922 initial_state: "before EDiˇtoR after".into(),
13923 buffer_marked_text: "before <EDi|toR> after".into(),
13924 completion_label: "editor",
13925 completion_text: "editor",
13926 expected_with_insert_mode: "before editorˇtoR after".into(),
13927 expected_with_replace_mode: "before editorˇ after".into(),
13928 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13929 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13930 },
13931 ];
13932
13933 for run in runs {
13934 let run_variations = [
13935 (LspInsertMode::Insert, run.expected_with_insert_mode),
13936 (LspInsertMode::Replace, run.expected_with_replace_mode),
13937 (
13938 LspInsertMode::ReplaceSubsequence,
13939 run.expected_with_replace_subsequence_mode,
13940 ),
13941 (
13942 LspInsertMode::ReplaceSuffix,
13943 run.expected_with_replace_suffix_mode,
13944 ),
13945 ];
13946
13947 for (lsp_insert_mode, expected_text) in run_variations {
13948 eprintln!(
13949 "run = {:?}, mode = {lsp_insert_mode:.?}",
13950 run.run_description,
13951 );
13952
13953 update_test_language_settings(&mut cx, |settings| {
13954 settings.defaults.completions = Some(CompletionSettingsContent {
13955 lsp_insert_mode: Some(lsp_insert_mode),
13956 words: Some(WordsCompletionMode::Disabled),
13957 words_min_length: Some(0),
13958 ..Default::default()
13959 });
13960 });
13961
13962 cx.set_state(&run.initial_state);
13963 cx.update_editor(|editor, window, cx| {
13964 editor.show_completions(&ShowCompletions, window, cx);
13965 });
13966
13967 let counter = Arc::new(AtomicUsize::new(0));
13968 handle_completion_request_with_insert_and_replace(
13969 &mut cx,
13970 &run.buffer_marked_text,
13971 vec![(run.completion_label, run.completion_text)],
13972 counter.clone(),
13973 )
13974 .await;
13975 cx.condition(|editor, _| editor.context_menu_visible())
13976 .await;
13977 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13978
13979 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13980 editor
13981 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13982 .unwrap()
13983 });
13984 cx.assert_editor_state(&expected_text);
13985 handle_resolve_completion_request(&mut cx, None).await;
13986 apply_additional_edits.await.unwrap();
13987 }
13988 }
13989}
13990
13991#[gpui::test]
13992async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13993 init_test(cx, |_| {});
13994 let mut cx = EditorLspTestContext::new_rust(
13995 lsp::ServerCapabilities {
13996 completion_provider: Some(lsp::CompletionOptions {
13997 resolve_provider: Some(true),
13998 ..Default::default()
13999 }),
14000 ..Default::default()
14001 },
14002 cx,
14003 )
14004 .await;
14005
14006 let initial_state = "SubˇError";
14007 let buffer_marked_text = "<Sub|Error>";
14008 let completion_text = "SubscriptionError";
14009 let expected_with_insert_mode = "SubscriptionErrorˇError";
14010 let expected_with_replace_mode = "SubscriptionErrorˇ";
14011
14012 update_test_language_settings(&mut cx, |settings| {
14013 settings.defaults.completions = Some(CompletionSettingsContent {
14014 words: Some(WordsCompletionMode::Disabled),
14015 words_min_length: Some(0),
14016 // set the opposite here to ensure that the action is overriding the default behavior
14017 lsp_insert_mode: Some(LspInsertMode::Insert),
14018 ..Default::default()
14019 });
14020 });
14021
14022 cx.set_state(initial_state);
14023 cx.update_editor(|editor, window, cx| {
14024 editor.show_completions(&ShowCompletions, window, cx);
14025 });
14026
14027 let counter = Arc::new(AtomicUsize::new(0));
14028 handle_completion_request_with_insert_and_replace(
14029 &mut cx,
14030 buffer_marked_text,
14031 vec![(completion_text, completion_text)],
14032 counter.clone(),
14033 )
14034 .await;
14035 cx.condition(|editor, _| editor.context_menu_visible())
14036 .await;
14037 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14038
14039 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14040 editor
14041 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14042 .unwrap()
14043 });
14044 cx.assert_editor_state(expected_with_replace_mode);
14045 handle_resolve_completion_request(&mut cx, None).await;
14046 apply_additional_edits.await.unwrap();
14047
14048 update_test_language_settings(&mut cx, |settings| {
14049 settings.defaults.completions = Some(CompletionSettingsContent {
14050 words: Some(WordsCompletionMode::Disabled),
14051 words_min_length: Some(0),
14052 // set the opposite here to ensure that the action is overriding the default behavior
14053 lsp_insert_mode: Some(LspInsertMode::Replace),
14054 ..Default::default()
14055 });
14056 });
14057
14058 cx.set_state(initial_state);
14059 cx.update_editor(|editor, window, cx| {
14060 editor.show_completions(&ShowCompletions, window, cx);
14061 });
14062 handle_completion_request_with_insert_and_replace(
14063 &mut cx,
14064 buffer_marked_text,
14065 vec![(completion_text, completion_text)],
14066 counter.clone(),
14067 )
14068 .await;
14069 cx.condition(|editor, _| editor.context_menu_visible())
14070 .await;
14071 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14072
14073 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14074 editor
14075 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14076 .unwrap()
14077 });
14078 cx.assert_editor_state(expected_with_insert_mode);
14079 handle_resolve_completion_request(&mut cx, None).await;
14080 apply_additional_edits.await.unwrap();
14081}
14082
14083#[gpui::test]
14084async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14085 init_test(cx, |_| {});
14086 let mut cx = EditorLspTestContext::new_rust(
14087 lsp::ServerCapabilities {
14088 completion_provider: Some(lsp::CompletionOptions {
14089 resolve_provider: Some(true),
14090 ..Default::default()
14091 }),
14092 ..Default::default()
14093 },
14094 cx,
14095 )
14096 .await;
14097
14098 // scenario: surrounding text matches completion text
14099 let completion_text = "to_offset";
14100 let initial_state = indoc! {"
14101 1. buf.to_offˇsuffix
14102 2. buf.to_offˇsuf
14103 3. buf.to_offˇfix
14104 4. buf.to_offˇ
14105 5. into_offˇensive
14106 6. ˇsuffix
14107 7. let ˇ //
14108 8. aaˇzz
14109 9. buf.to_off«zzzzzˇ»suffix
14110 10. buf.«ˇzzzzz»suffix
14111 11. to_off«ˇzzzzz»
14112
14113 buf.to_offˇsuffix // newest cursor
14114 "};
14115 let completion_marked_buffer = indoc! {"
14116 1. buf.to_offsuffix
14117 2. buf.to_offsuf
14118 3. buf.to_offfix
14119 4. buf.to_off
14120 5. into_offensive
14121 6. suffix
14122 7. let //
14123 8. aazz
14124 9. buf.to_offzzzzzsuffix
14125 10. buf.zzzzzsuffix
14126 11. to_offzzzzz
14127
14128 buf.<to_off|suffix> // newest cursor
14129 "};
14130 let expected = indoc! {"
14131 1. buf.to_offsetˇ
14132 2. buf.to_offsetˇsuf
14133 3. buf.to_offsetˇfix
14134 4. buf.to_offsetˇ
14135 5. into_offsetˇensive
14136 6. to_offsetˇsuffix
14137 7. let to_offsetˇ //
14138 8. aato_offsetˇzz
14139 9. buf.to_offsetˇ
14140 10. buf.to_offsetˇsuffix
14141 11. to_offsetˇ
14142
14143 buf.to_offsetˇ // newest cursor
14144 "};
14145 cx.set_state(initial_state);
14146 cx.update_editor(|editor, window, cx| {
14147 editor.show_completions(&ShowCompletions, window, cx);
14148 });
14149 handle_completion_request_with_insert_and_replace(
14150 &mut cx,
14151 completion_marked_buffer,
14152 vec![(completion_text, completion_text)],
14153 Arc::new(AtomicUsize::new(0)),
14154 )
14155 .await;
14156 cx.condition(|editor, _| editor.context_menu_visible())
14157 .await;
14158 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14159 editor
14160 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14161 .unwrap()
14162 });
14163 cx.assert_editor_state(expected);
14164 handle_resolve_completion_request(&mut cx, None).await;
14165 apply_additional_edits.await.unwrap();
14166
14167 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14168 let completion_text = "foo_and_bar";
14169 let initial_state = indoc! {"
14170 1. ooanbˇ
14171 2. zooanbˇ
14172 3. ooanbˇz
14173 4. zooanbˇz
14174 5. ooanˇ
14175 6. oanbˇ
14176
14177 ooanbˇ
14178 "};
14179 let completion_marked_buffer = indoc! {"
14180 1. ooanb
14181 2. zooanb
14182 3. ooanbz
14183 4. zooanbz
14184 5. ooan
14185 6. oanb
14186
14187 <ooanb|>
14188 "};
14189 let expected = indoc! {"
14190 1. foo_and_barˇ
14191 2. zfoo_and_barˇ
14192 3. foo_and_barˇz
14193 4. zfoo_and_barˇz
14194 5. ooanfoo_and_barˇ
14195 6. oanbfoo_and_barˇ
14196
14197 foo_and_barˇ
14198 "};
14199 cx.set_state(initial_state);
14200 cx.update_editor(|editor, window, cx| {
14201 editor.show_completions(&ShowCompletions, window, cx);
14202 });
14203 handle_completion_request_with_insert_and_replace(
14204 &mut cx,
14205 completion_marked_buffer,
14206 vec![(completion_text, completion_text)],
14207 Arc::new(AtomicUsize::new(0)),
14208 )
14209 .await;
14210 cx.condition(|editor, _| editor.context_menu_visible())
14211 .await;
14212 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14213 editor
14214 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14215 .unwrap()
14216 });
14217 cx.assert_editor_state(expected);
14218 handle_resolve_completion_request(&mut cx, None).await;
14219 apply_additional_edits.await.unwrap();
14220
14221 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14222 // (expects the same as if it was inserted at the end)
14223 let completion_text = "foo_and_bar";
14224 let initial_state = indoc! {"
14225 1. ooˇanb
14226 2. zooˇanb
14227 3. ooˇanbz
14228 4. zooˇanbz
14229
14230 ooˇanb
14231 "};
14232 let completion_marked_buffer = indoc! {"
14233 1. ooanb
14234 2. zooanb
14235 3. ooanbz
14236 4. zooanbz
14237
14238 <oo|anb>
14239 "};
14240 let expected = indoc! {"
14241 1. foo_and_barˇ
14242 2. zfoo_and_barˇ
14243 3. foo_and_barˇz
14244 4. zfoo_and_barˇz
14245
14246 foo_and_barˇ
14247 "};
14248 cx.set_state(initial_state);
14249 cx.update_editor(|editor, window, cx| {
14250 editor.show_completions(&ShowCompletions, window, cx);
14251 });
14252 handle_completion_request_with_insert_and_replace(
14253 &mut cx,
14254 completion_marked_buffer,
14255 vec![(completion_text, completion_text)],
14256 Arc::new(AtomicUsize::new(0)),
14257 )
14258 .await;
14259 cx.condition(|editor, _| editor.context_menu_visible())
14260 .await;
14261 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14262 editor
14263 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14264 .unwrap()
14265 });
14266 cx.assert_editor_state(expected);
14267 handle_resolve_completion_request(&mut cx, None).await;
14268 apply_additional_edits.await.unwrap();
14269}
14270
14271// This used to crash
14272#[gpui::test]
14273async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14274 init_test(cx, |_| {});
14275
14276 let buffer_text = indoc! {"
14277 fn main() {
14278 10.satu;
14279
14280 //
14281 // separate cursors so they open in different excerpts (manually reproducible)
14282 //
14283
14284 10.satu20;
14285 }
14286 "};
14287 let multibuffer_text_with_selections = indoc! {"
14288 fn main() {
14289 10.satuˇ;
14290
14291 //
14292
14293 //
14294
14295 10.satuˇ20;
14296 }
14297 "};
14298 let expected_multibuffer = indoc! {"
14299 fn main() {
14300 10.saturating_sub()ˇ;
14301
14302 //
14303
14304 //
14305
14306 10.saturating_sub()ˇ;
14307 }
14308 "};
14309
14310 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14311 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14312
14313 let fs = FakeFs::new(cx.executor());
14314 fs.insert_tree(
14315 path!("/a"),
14316 json!({
14317 "main.rs": buffer_text,
14318 }),
14319 )
14320 .await;
14321
14322 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14323 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14324 language_registry.add(rust_lang());
14325 let mut fake_servers = language_registry.register_fake_lsp(
14326 "Rust",
14327 FakeLspAdapter {
14328 capabilities: lsp::ServerCapabilities {
14329 completion_provider: Some(lsp::CompletionOptions {
14330 resolve_provider: None,
14331 ..lsp::CompletionOptions::default()
14332 }),
14333 ..lsp::ServerCapabilities::default()
14334 },
14335 ..FakeLspAdapter::default()
14336 },
14337 );
14338 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14339 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14340 let buffer = project
14341 .update(cx, |project, cx| {
14342 project.open_local_buffer(path!("/a/main.rs"), cx)
14343 })
14344 .await
14345 .unwrap();
14346
14347 let multi_buffer = cx.new(|cx| {
14348 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14349 multi_buffer.push_excerpts(
14350 buffer.clone(),
14351 [ExcerptRange::new(0..first_excerpt_end)],
14352 cx,
14353 );
14354 multi_buffer.push_excerpts(
14355 buffer.clone(),
14356 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14357 cx,
14358 );
14359 multi_buffer
14360 });
14361
14362 let editor = workspace
14363 .update(cx, |_, window, cx| {
14364 cx.new(|cx| {
14365 Editor::new(
14366 EditorMode::Full {
14367 scale_ui_elements_with_buffer_font_size: false,
14368 show_active_line_background: false,
14369 sizing_behavior: SizingBehavior::Default,
14370 },
14371 multi_buffer.clone(),
14372 Some(project.clone()),
14373 window,
14374 cx,
14375 )
14376 })
14377 })
14378 .unwrap();
14379
14380 let pane = workspace
14381 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14382 .unwrap();
14383 pane.update_in(cx, |pane, window, cx| {
14384 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14385 });
14386
14387 let fake_server = fake_servers.next().await.unwrap();
14388
14389 editor.update_in(cx, |editor, window, cx| {
14390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14391 s.select_ranges([
14392 Point::new(1, 11)..Point::new(1, 11),
14393 Point::new(7, 11)..Point::new(7, 11),
14394 ])
14395 });
14396
14397 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14398 });
14399
14400 editor.update_in(cx, |editor, window, cx| {
14401 editor.show_completions(&ShowCompletions, window, cx);
14402 });
14403
14404 fake_server
14405 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14406 let completion_item = lsp::CompletionItem {
14407 label: "saturating_sub()".into(),
14408 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14409 lsp::InsertReplaceEdit {
14410 new_text: "saturating_sub()".to_owned(),
14411 insert: lsp::Range::new(
14412 lsp::Position::new(7, 7),
14413 lsp::Position::new(7, 11),
14414 ),
14415 replace: lsp::Range::new(
14416 lsp::Position::new(7, 7),
14417 lsp::Position::new(7, 13),
14418 ),
14419 },
14420 )),
14421 ..lsp::CompletionItem::default()
14422 };
14423
14424 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14425 })
14426 .next()
14427 .await
14428 .unwrap();
14429
14430 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14431 .await;
14432
14433 editor
14434 .update_in(cx, |editor, window, cx| {
14435 editor
14436 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14437 .unwrap()
14438 })
14439 .await
14440 .unwrap();
14441
14442 editor.update(cx, |editor, cx| {
14443 assert_text_with_selections(editor, expected_multibuffer, cx);
14444 })
14445}
14446
14447#[gpui::test]
14448async fn test_completion(cx: &mut TestAppContext) {
14449 init_test(cx, |_| {});
14450
14451 let mut cx = EditorLspTestContext::new_rust(
14452 lsp::ServerCapabilities {
14453 completion_provider: Some(lsp::CompletionOptions {
14454 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14455 resolve_provider: Some(true),
14456 ..Default::default()
14457 }),
14458 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14459 ..Default::default()
14460 },
14461 cx,
14462 )
14463 .await;
14464 let counter = Arc::new(AtomicUsize::new(0));
14465
14466 cx.set_state(indoc! {"
14467 oneˇ
14468 two
14469 three
14470 "});
14471 cx.simulate_keystroke(".");
14472 handle_completion_request(
14473 indoc! {"
14474 one.|<>
14475 two
14476 three
14477 "},
14478 vec!["first_completion", "second_completion"],
14479 true,
14480 counter.clone(),
14481 &mut cx,
14482 )
14483 .await;
14484 cx.condition(|editor, _| editor.context_menu_visible())
14485 .await;
14486 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14487
14488 let _handler = handle_signature_help_request(
14489 &mut cx,
14490 lsp::SignatureHelp {
14491 signatures: vec![lsp::SignatureInformation {
14492 label: "test signature".to_string(),
14493 documentation: None,
14494 parameters: Some(vec![lsp::ParameterInformation {
14495 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14496 documentation: None,
14497 }]),
14498 active_parameter: None,
14499 }],
14500 active_signature: None,
14501 active_parameter: None,
14502 },
14503 );
14504 cx.update_editor(|editor, window, cx| {
14505 assert!(
14506 !editor.signature_help_state.is_shown(),
14507 "No signature help was called for"
14508 );
14509 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14510 });
14511 cx.run_until_parked();
14512 cx.update_editor(|editor, _, _| {
14513 assert!(
14514 !editor.signature_help_state.is_shown(),
14515 "No signature help should be shown when completions menu is open"
14516 );
14517 });
14518
14519 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14520 editor.context_menu_next(&Default::default(), window, cx);
14521 editor
14522 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14523 .unwrap()
14524 });
14525 cx.assert_editor_state(indoc! {"
14526 one.second_completionˇ
14527 two
14528 three
14529 "});
14530
14531 handle_resolve_completion_request(
14532 &mut cx,
14533 Some(vec![
14534 (
14535 //This overlaps with the primary completion edit which is
14536 //misbehavior from the LSP spec, test that we filter it out
14537 indoc! {"
14538 one.second_ˇcompletion
14539 two
14540 threeˇ
14541 "},
14542 "overlapping additional edit",
14543 ),
14544 (
14545 indoc! {"
14546 one.second_completion
14547 two
14548 threeˇ
14549 "},
14550 "\nadditional edit",
14551 ),
14552 ]),
14553 )
14554 .await;
14555 apply_additional_edits.await.unwrap();
14556 cx.assert_editor_state(indoc! {"
14557 one.second_completionˇ
14558 two
14559 three
14560 additional edit
14561 "});
14562
14563 cx.set_state(indoc! {"
14564 one.second_completion
14565 twoˇ
14566 threeˇ
14567 additional edit
14568 "});
14569 cx.simulate_keystroke(" ");
14570 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14571 cx.simulate_keystroke("s");
14572 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14573
14574 cx.assert_editor_state(indoc! {"
14575 one.second_completion
14576 two sˇ
14577 three sˇ
14578 additional edit
14579 "});
14580 handle_completion_request(
14581 indoc! {"
14582 one.second_completion
14583 two s
14584 three <s|>
14585 additional edit
14586 "},
14587 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14588 true,
14589 counter.clone(),
14590 &mut cx,
14591 )
14592 .await;
14593 cx.condition(|editor, _| editor.context_menu_visible())
14594 .await;
14595 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14596
14597 cx.simulate_keystroke("i");
14598
14599 handle_completion_request(
14600 indoc! {"
14601 one.second_completion
14602 two si
14603 three <si|>
14604 additional edit
14605 "},
14606 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14607 true,
14608 counter.clone(),
14609 &mut cx,
14610 )
14611 .await;
14612 cx.condition(|editor, _| editor.context_menu_visible())
14613 .await;
14614 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14615
14616 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14617 editor
14618 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14619 .unwrap()
14620 });
14621 cx.assert_editor_state(indoc! {"
14622 one.second_completion
14623 two sixth_completionˇ
14624 three sixth_completionˇ
14625 additional edit
14626 "});
14627
14628 apply_additional_edits.await.unwrap();
14629
14630 update_test_language_settings(&mut cx, |settings| {
14631 settings.defaults.show_completions_on_input = Some(false);
14632 });
14633 cx.set_state("editorˇ");
14634 cx.simulate_keystroke(".");
14635 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14636 cx.simulate_keystrokes("c l o");
14637 cx.assert_editor_state("editor.cloˇ");
14638 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14639 cx.update_editor(|editor, window, cx| {
14640 editor.show_completions(&ShowCompletions, window, cx);
14641 });
14642 handle_completion_request(
14643 "editor.<clo|>",
14644 vec!["close", "clobber"],
14645 true,
14646 counter.clone(),
14647 &mut cx,
14648 )
14649 .await;
14650 cx.condition(|editor, _| editor.context_menu_visible())
14651 .await;
14652 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14653
14654 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14655 editor
14656 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14657 .unwrap()
14658 });
14659 cx.assert_editor_state("editor.clobberˇ");
14660 handle_resolve_completion_request(&mut cx, None).await;
14661 apply_additional_edits.await.unwrap();
14662}
14663
14664#[gpui::test]
14665async fn test_completion_reuse(cx: &mut TestAppContext) {
14666 init_test(cx, |_| {});
14667
14668 let mut cx = EditorLspTestContext::new_rust(
14669 lsp::ServerCapabilities {
14670 completion_provider: Some(lsp::CompletionOptions {
14671 trigger_characters: Some(vec![".".to_string()]),
14672 ..Default::default()
14673 }),
14674 ..Default::default()
14675 },
14676 cx,
14677 )
14678 .await;
14679
14680 let counter = Arc::new(AtomicUsize::new(0));
14681 cx.set_state("objˇ");
14682 cx.simulate_keystroke(".");
14683
14684 // Initial completion request returns complete results
14685 let is_incomplete = false;
14686 handle_completion_request(
14687 "obj.|<>",
14688 vec!["a", "ab", "abc"],
14689 is_incomplete,
14690 counter.clone(),
14691 &mut cx,
14692 )
14693 .await;
14694 cx.run_until_parked();
14695 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14696 cx.assert_editor_state("obj.ˇ");
14697 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14698
14699 // Type "a" - filters existing completions
14700 cx.simulate_keystroke("a");
14701 cx.run_until_parked();
14702 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14703 cx.assert_editor_state("obj.aˇ");
14704 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14705
14706 // Type "b" - filters existing completions
14707 cx.simulate_keystroke("b");
14708 cx.run_until_parked();
14709 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14710 cx.assert_editor_state("obj.abˇ");
14711 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14712
14713 // Type "c" - filters existing completions
14714 cx.simulate_keystroke("c");
14715 cx.run_until_parked();
14716 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14717 cx.assert_editor_state("obj.abcˇ");
14718 check_displayed_completions(vec!["abc"], &mut cx);
14719
14720 // Backspace to delete "c" - filters existing completions
14721 cx.update_editor(|editor, window, cx| {
14722 editor.backspace(&Backspace, window, cx);
14723 });
14724 cx.run_until_parked();
14725 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14726 cx.assert_editor_state("obj.abˇ");
14727 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14728
14729 // Moving cursor to the left dismisses menu.
14730 cx.update_editor(|editor, window, cx| {
14731 editor.move_left(&MoveLeft, window, cx);
14732 });
14733 cx.run_until_parked();
14734 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14735 cx.assert_editor_state("obj.aˇb");
14736 cx.update_editor(|editor, _, _| {
14737 assert_eq!(editor.context_menu_visible(), false);
14738 });
14739
14740 // Type "b" - new request
14741 cx.simulate_keystroke("b");
14742 let is_incomplete = false;
14743 handle_completion_request(
14744 "obj.<ab|>a",
14745 vec!["ab", "abc"],
14746 is_incomplete,
14747 counter.clone(),
14748 &mut cx,
14749 )
14750 .await;
14751 cx.run_until_parked();
14752 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14753 cx.assert_editor_state("obj.abˇb");
14754 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14755
14756 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14757 cx.update_editor(|editor, window, cx| {
14758 editor.backspace(&Backspace, window, cx);
14759 });
14760 let is_incomplete = false;
14761 handle_completion_request(
14762 "obj.<a|>b",
14763 vec!["a", "ab", "abc"],
14764 is_incomplete,
14765 counter.clone(),
14766 &mut cx,
14767 )
14768 .await;
14769 cx.run_until_parked();
14770 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14771 cx.assert_editor_state("obj.aˇb");
14772 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14773
14774 // Backspace to delete "a" - dismisses menu.
14775 cx.update_editor(|editor, window, cx| {
14776 editor.backspace(&Backspace, window, cx);
14777 });
14778 cx.run_until_parked();
14779 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14780 cx.assert_editor_state("obj.ˇb");
14781 cx.update_editor(|editor, _, _| {
14782 assert_eq!(editor.context_menu_visible(), false);
14783 });
14784}
14785
14786#[gpui::test]
14787async fn test_word_completion(cx: &mut TestAppContext) {
14788 let lsp_fetch_timeout_ms = 10;
14789 init_test(cx, |language_settings| {
14790 language_settings.defaults.completions = Some(CompletionSettingsContent {
14791 words_min_length: Some(0),
14792 lsp_fetch_timeout_ms: Some(10),
14793 lsp_insert_mode: Some(LspInsertMode::Insert),
14794 ..Default::default()
14795 });
14796 });
14797
14798 let mut cx = EditorLspTestContext::new_rust(
14799 lsp::ServerCapabilities {
14800 completion_provider: Some(lsp::CompletionOptions {
14801 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14802 ..lsp::CompletionOptions::default()
14803 }),
14804 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14805 ..lsp::ServerCapabilities::default()
14806 },
14807 cx,
14808 )
14809 .await;
14810
14811 let throttle_completions = Arc::new(AtomicBool::new(false));
14812
14813 let lsp_throttle_completions = throttle_completions.clone();
14814 let _completion_requests_handler =
14815 cx.lsp
14816 .server
14817 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14818 let lsp_throttle_completions = lsp_throttle_completions.clone();
14819 let cx = cx.clone();
14820 async move {
14821 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14822 cx.background_executor()
14823 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14824 .await;
14825 }
14826 Ok(Some(lsp::CompletionResponse::Array(vec![
14827 lsp::CompletionItem {
14828 label: "first".into(),
14829 ..lsp::CompletionItem::default()
14830 },
14831 lsp::CompletionItem {
14832 label: "last".into(),
14833 ..lsp::CompletionItem::default()
14834 },
14835 ])))
14836 }
14837 });
14838
14839 cx.set_state(indoc! {"
14840 oneˇ
14841 two
14842 three
14843 "});
14844 cx.simulate_keystroke(".");
14845 cx.executor().run_until_parked();
14846 cx.condition(|editor, _| editor.context_menu_visible())
14847 .await;
14848 cx.update_editor(|editor, window, cx| {
14849 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14850 {
14851 assert_eq!(
14852 completion_menu_entries(menu),
14853 &["first", "last"],
14854 "When LSP server is fast to reply, no fallback word completions are used"
14855 );
14856 } else {
14857 panic!("expected completion menu to be open");
14858 }
14859 editor.cancel(&Cancel, window, cx);
14860 });
14861 cx.executor().run_until_parked();
14862 cx.condition(|editor, _| !editor.context_menu_visible())
14863 .await;
14864
14865 throttle_completions.store(true, atomic::Ordering::Release);
14866 cx.simulate_keystroke(".");
14867 cx.executor()
14868 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14869 cx.executor().run_until_parked();
14870 cx.condition(|editor, _| editor.context_menu_visible())
14871 .await;
14872 cx.update_editor(|editor, _, _| {
14873 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14874 {
14875 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14876 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14877 } else {
14878 panic!("expected completion menu to be open");
14879 }
14880 });
14881}
14882
14883#[gpui::test]
14884async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14885 init_test(cx, |language_settings| {
14886 language_settings.defaults.completions = Some(CompletionSettingsContent {
14887 words: Some(WordsCompletionMode::Enabled),
14888 words_min_length: Some(0),
14889 lsp_insert_mode: Some(LspInsertMode::Insert),
14890 ..Default::default()
14891 });
14892 });
14893
14894 let mut cx = EditorLspTestContext::new_rust(
14895 lsp::ServerCapabilities {
14896 completion_provider: Some(lsp::CompletionOptions {
14897 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14898 ..lsp::CompletionOptions::default()
14899 }),
14900 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14901 ..lsp::ServerCapabilities::default()
14902 },
14903 cx,
14904 )
14905 .await;
14906
14907 let _completion_requests_handler =
14908 cx.lsp
14909 .server
14910 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14911 Ok(Some(lsp::CompletionResponse::Array(vec![
14912 lsp::CompletionItem {
14913 label: "first".into(),
14914 ..lsp::CompletionItem::default()
14915 },
14916 lsp::CompletionItem {
14917 label: "last".into(),
14918 ..lsp::CompletionItem::default()
14919 },
14920 ])))
14921 });
14922
14923 cx.set_state(indoc! {"ˇ
14924 first
14925 last
14926 second
14927 "});
14928 cx.simulate_keystroke(".");
14929 cx.executor().run_until_parked();
14930 cx.condition(|editor, _| editor.context_menu_visible())
14931 .await;
14932 cx.update_editor(|editor, _, _| {
14933 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14934 {
14935 assert_eq!(
14936 completion_menu_entries(menu),
14937 &["first", "last", "second"],
14938 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14939 );
14940 } else {
14941 panic!("expected completion menu to be open");
14942 }
14943 });
14944}
14945
14946#[gpui::test]
14947async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14948 init_test(cx, |language_settings| {
14949 language_settings.defaults.completions = Some(CompletionSettingsContent {
14950 words: Some(WordsCompletionMode::Disabled),
14951 words_min_length: Some(0),
14952 lsp_insert_mode: Some(LspInsertMode::Insert),
14953 ..Default::default()
14954 });
14955 });
14956
14957 let mut cx = EditorLspTestContext::new_rust(
14958 lsp::ServerCapabilities {
14959 completion_provider: Some(lsp::CompletionOptions {
14960 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14961 ..lsp::CompletionOptions::default()
14962 }),
14963 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14964 ..lsp::ServerCapabilities::default()
14965 },
14966 cx,
14967 )
14968 .await;
14969
14970 let _completion_requests_handler =
14971 cx.lsp
14972 .server
14973 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14974 panic!("LSP completions should not be queried when dealing with word completions")
14975 });
14976
14977 cx.set_state(indoc! {"ˇ
14978 first
14979 last
14980 second
14981 "});
14982 cx.update_editor(|editor, window, cx| {
14983 editor.show_word_completions(&ShowWordCompletions, window, cx);
14984 });
14985 cx.executor().run_until_parked();
14986 cx.condition(|editor, _| editor.context_menu_visible())
14987 .await;
14988 cx.update_editor(|editor, _, _| {
14989 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14990 {
14991 assert_eq!(
14992 completion_menu_entries(menu),
14993 &["first", "last", "second"],
14994 "`ShowWordCompletions` action should show word completions"
14995 );
14996 } else {
14997 panic!("expected completion menu to be open");
14998 }
14999 });
15000
15001 cx.simulate_keystroke("l");
15002 cx.executor().run_until_parked();
15003 cx.condition(|editor, _| editor.context_menu_visible())
15004 .await;
15005 cx.update_editor(|editor, _, _| {
15006 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15007 {
15008 assert_eq!(
15009 completion_menu_entries(menu),
15010 &["last"],
15011 "After showing word completions, further editing should filter them and not query the LSP"
15012 );
15013 } else {
15014 panic!("expected completion menu to be open");
15015 }
15016 });
15017}
15018
15019#[gpui::test]
15020async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15021 init_test(cx, |language_settings| {
15022 language_settings.defaults.completions = Some(CompletionSettingsContent {
15023 words_min_length: Some(0),
15024 lsp: Some(false),
15025 lsp_insert_mode: Some(LspInsertMode::Insert),
15026 ..Default::default()
15027 });
15028 });
15029
15030 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15031
15032 cx.set_state(indoc! {"ˇ
15033 0_usize
15034 let
15035 33
15036 4.5f32
15037 "});
15038 cx.update_editor(|editor, window, cx| {
15039 editor.show_completions(&ShowCompletions, window, cx);
15040 });
15041 cx.executor().run_until_parked();
15042 cx.condition(|editor, _| editor.context_menu_visible())
15043 .await;
15044 cx.update_editor(|editor, window, cx| {
15045 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15046 {
15047 assert_eq!(
15048 completion_menu_entries(menu),
15049 &["let"],
15050 "With no digits in the completion query, no digits should be in the word completions"
15051 );
15052 } else {
15053 panic!("expected completion menu to be open");
15054 }
15055 editor.cancel(&Cancel, window, cx);
15056 });
15057
15058 cx.set_state(indoc! {"3ˇ
15059 0_usize
15060 let
15061 3
15062 33.35f32
15063 "});
15064 cx.update_editor(|editor, window, cx| {
15065 editor.show_completions(&ShowCompletions, window, cx);
15066 });
15067 cx.executor().run_until_parked();
15068 cx.condition(|editor, _| editor.context_menu_visible())
15069 .await;
15070 cx.update_editor(|editor, _, _| {
15071 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15072 {
15073 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15074 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15075 } else {
15076 panic!("expected completion menu to be open");
15077 }
15078 });
15079}
15080
15081#[gpui::test]
15082async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15083 init_test(cx, |language_settings| {
15084 language_settings.defaults.completions = Some(CompletionSettingsContent {
15085 words: Some(WordsCompletionMode::Enabled),
15086 words_min_length: Some(3),
15087 lsp_insert_mode: Some(LspInsertMode::Insert),
15088 ..Default::default()
15089 });
15090 });
15091
15092 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15093 cx.set_state(indoc! {"ˇ
15094 wow
15095 wowen
15096 wowser
15097 "});
15098 cx.simulate_keystroke("w");
15099 cx.executor().run_until_parked();
15100 cx.update_editor(|editor, _, _| {
15101 if editor.context_menu.borrow_mut().is_some() {
15102 panic!(
15103 "expected completion menu to be hidden, as words completion threshold is not met"
15104 );
15105 }
15106 });
15107
15108 cx.update_editor(|editor, window, cx| {
15109 editor.show_word_completions(&ShowWordCompletions, window, cx);
15110 });
15111 cx.executor().run_until_parked();
15112 cx.update_editor(|editor, window, cx| {
15113 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15114 {
15115 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");
15116 } else {
15117 panic!("expected completion menu to be open after the word completions are called with an action");
15118 }
15119
15120 editor.cancel(&Cancel, window, cx);
15121 });
15122 cx.update_editor(|editor, _, _| {
15123 if editor.context_menu.borrow_mut().is_some() {
15124 panic!("expected completion menu to be hidden after canceling");
15125 }
15126 });
15127
15128 cx.simulate_keystroke("o");
15129 cx.executor().run_until_parked();
15130 cx.update_editor(|editor, _, _| {
15131 if editor.context_menu.borrow_mut().is_some() {
15132 panic!(
15133 "expected completion menu to be hidden, as words completion threshold is not met still"
15134 );
15135 }
15136 });
15137
15138 cx.simulate_keystroke("w");
15139 cx.executor().run_until_parked();
15140 cx.update_editor(|editor, _, _| {
15141 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15142 {
15143 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15144 } else {
15145 panic!("expected completion menu to be open after the word completions threshold is met");
15146 }
15147 });
15148}
15149
15150#[gpui::test]
15151async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15152 init_test(cx, |language_settings| {
15153 language_settings.defaults.completions = Some(CompletionSettingsContent {
15154 words: Some(WordsCompletionMode::Enabled),
15155 words_min_length: Some(0),
15156 lsp_insert_mode: Some(LspInsertMode::Insert),
15157 ..Default::default()
15158 });
15159 });
15160
15161 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15162 cx.update_editor(|editor, _, _| {
15163 editor.disable_word_completions();
15164 });
15165 cx.set_state(indoc! {"ˇ
15166 wow
15167 wowen
15168 wowser
15169 "});
15170 cx.simulate_keystroke("w");
15171 cx.executor().run_until_parked();
15172 cx.update_editor(|editor, _, _| {
15173 if editor.context_menu.borrow_mut().is_some() {
15174 panic!(
15175 "expected completion menu to be hidden, as words completion are disabled for this editor"
15176 );
15177 }
15178 });
15179
15180 cx.update_editor(|editor, window, cx| {
15181 editor.show_word_completions(&ShowWordCompletions, window, cx);
15182 });
15183 cx.executor().run_until_parked();
15184 cx.update_editor(|editor, _, _| {
15185 if editor.context_menu.borrow_mut().is_some() {
15186 panic!(
15187 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15188 );
15189 }
15190 });
15191}
15192
15193#[gpui::test]
15194async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15195 init_test(cx, |language_settings| {
15196 language_settings.defaults.completions = Some(CompletionSettingsContent {
15197 words: Some(WordsCompletionMode::Disabled),
15198 words_min_length: Some(0),
15199 lsp_insert_mode: Some(LspInsertMode::Insert),
15200 ..Default::default()
15201 });
15202 });
15203
15204 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15205 cx.update_editor(|editor, _, _| {
15206 editor.set_completion_provider(None);
15207 });
15208 cx.set_state(indoc! {"ˇ
15209 wow
15210 wowen
15211 wowser
15212 "});
15213 cx.simulate_keystroke("w");
15214 cx.executor().run_until_parked();
15215 cx.update_editor(|editor, _, _| {
15216 if editor.context_menu.borrow_mut().is_some() {
15217 panic!("expected completion menu to be hidden, as disabled in settings");
15218 }
15219 });
15220}
15221
15222fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15223 let position = || lsp::Position {
15224 line: params.text_document_position.position.line,
15225 character: params.text_document_position.position.character,
15226 };
15227 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15228 range: lsp::Range {
15229 start: position(),
15230 end: position(),
15231 },
15232 new_text: text.to_string(),
15233 }))
15234}
15235
15236#[gpui::test]
15237async fn test_multiline_completion(cx: &mut TestAppContext) {
15238 init_test(cx, |_| {});
15239
15240 let fs = FakeFs::new(cx.executor());
15241 fs.insert_tree(
15242 path!("/a"),
15243 json!({
15244 "main.ts": "a",
15245 }),
15246 )
15247 .await;
15248
15249 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15250 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15251 let typescript_language = Arc::new(Language::new(
15252 LanguageConfig {
15253 name: "TypeScript".into(),
15254 matcher: LanguageMatcher {
15255 path_suffixes: vec!["ts".to_string()],
15256 ..LanguageMatcher::default()
15257 },
15258 line_comments: vec!["// ".into()],
15259 ..LanguageConfig::default()
15260 },
15261 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15262 ));
15263 language_registry.add(typescript_language.clone());
15264 let mut fake_servers = language_registry.register_fake_lsp(
15265 "TypeScript",
15266 FakeLspAdapter {
15267 capabilities: lsp::ServerCapabilities {
15268 completion_provider: Some(lsp::CompletionOptions {
15269 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15270 ..lsp::CompletionOptions::default()
15271 }),
15272 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15273 ..lsp::ServerCapabilities::default()
15274 },
15275 // Emulate vtsls label generation
15276 label_for_completion: Some(Box::new(|item, _| {
15277 let text = if let Some(description) = item
15278 .label_details
15279 .as_ref()
15280 .and_then(|label_details| label_details.description.as_ref())
15281 {
15282 format!("{} {}", item.label, description)
15283 } else if let Some(detail) = &item.detail {
15284 format!("{} {}", item.label, detail)
15285 } else {
15286 item.label.clone()
15287 };
15288 Some(language::CodeLabel::plain(text, None))
15289 })),
15290 ..FakeLspAdapter::default()
15291 },
15292 );
15293 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15294 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15295 let worktree_id = workspace
15296 .update(cx, |workspace, _window, cx| {
15297 workspace.project().update(cx, |project, cx| {
15298 project.worktrees(cx).next().unwrap().read(cx).id()
15299 })
15300 })
15301 .unwrap();
15302 let _buffer = project
15303 .update(cx, |project, cx| {
15304 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15305 })
15306 .await
15307 .unwrap();
15308 let editor = workspace
15309 .update(cx, |workspace, window, cx| {
15310 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15311 })
15312 .unwrap()
15313 .await
15314 .unwrap()
15315 .downcast::<Editor>()
15316 .unwrap();
15317 let fake_server = fake_servers.next().await.unwrap();
15318
15319 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15320 let multiline_label_2 = "a\nb\nc\n";
15321 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15322 let multiline_description = "d\ne\nf\n";
15323 let multiline_detail_2 = "g\nh\ni\n";
15324
15325 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15326 move |params, _| async move {
15327 Ok(Some(lsp::CompletionResponse::Array(vec![
15328 lsp::CompletionItem {
15329 label: multiline_label.to_string(),
15330 text_edit: gen_text_edit(¶ms, "new_text_1"),
15331 ..lsp::CompletionItem::default()
15332 },
15333 lsp::CompletionItem {
15334 label: "single line label 1".to_string(),
15335 detail: Some(multiline_detail.to_string()),
15336 text_edit: gen_text_edit(¶ms, "new_text_2"),
15337 ..lsp::CompletionItem::default()
15338 },
15339 lsp::CompletionItem {
15340 label: "single line label 2".to_string(),
15341 label_details: Some(lsp::CompletionItemLabelDetails {
15342 description: Some(multiline_description.to_string()),
15343 detail: None,
15344 }),
15345 text_edit: gen_text_edit(¶ms, "new_text_2"),
15346 ..lsp::CompletionItem::default()
15347 },
15348 lsp::CompletionItem {
15349 label: multiline_label_2.to_string(),
15350 detail: Some(multiline_detail_2.to_string()),
15351 text_edit: gen_text_edit(¶ms, "new_text_3"),
15352 ..lsp::CompletionItem::default()
15353 },
15354 lsp::CompletionItem {
15355 label: "Label with many spaces and \t but without newlines".to_string(),
15356 detail: Some(
15357 "Details with many spaces and \t but without newlines".to_string(),
15358 ),
15359 text_edit: gen_text_edit(¶ms, "new_text_4"),
15360 ..lsp::CompletionItem::default()
15361 },
15362 ])))
15363 },
15364 );
15365
15366 editor.update_in(cx, |editor, window, cx| {
15367 cx.focus_self(window);
15368 editor.move_to_end(&MoveToEnd, window, cx);
15369 editor.handle_input(".", window, cx);
15370 });
15371 cx.run_until_parked();
15372 completion_handle.next().await.unwrap();
15373
15374 editor.update(cx, |editor, _| {
15375 assert!(editor.context_menu_visible());
15376 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15377 {
15378 let completion_labels = menu
15379 .completions
15380 .borrow()
15381 .iter()
15382 .map(|c| c.label.text.clone())
15383 .collect::<Vec<_>>();
15384 assert_eq!(
15385 completion_labels,
15386 &[
15387 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15388 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15389 "single line label 2 d e f ",
15390 "a b c g h i ",
15391 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15392 ],
15393 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15394 );
15395
15396 for completion in menu
15397 .completions
15398 .borrow()
15399 .iter() {
15400 assert_eq!(
15401 completion.label.filter_range,
15402 0..completion.label.text.len(),
15403 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15404 );
15405 }
15406 } else {
15407 panic!("expected completion menu to be open");
15408 }
15409 });
15410}
15411
15412#[gpui::test]
15413async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15414 init_test(cx, |_| {});
15415 let mut cx = EditorLspTestContext::new_rust(
15416 lsp::ServerCapabilities {
15417 completion_provider: Some(lsp::CompletionOptions {
15418 trigger_characters: Some(vec![".".to_string()]),
15419 ..Default::default()
15420 }),
15421 ..Default::default()
15422 },
15423 cx,
15424 )
15425 .await;
15426 cx.lsp
15427 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15428 Ok(Some(lsp::CompletionResponse::Array(vec![
15429 lsp::CompletionItem {
15430 label: "first".into(),
15431 ..Default::default()
15432 },
15433 lsp::CompletionItem {
15434 label: "last".into(),
15435 ..Default::default()
15436 },
15437 ])))
15438 });
15439 cx.set_state("variableˇ");
15440 cx.simulate_keystroke(".");
15441 cx.executor().run_until_parked();
15442
15443 cx.update_editor(|editor, _, _| {
15444 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15445 {
15446 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15447 } else {
15448 panic!("expected completion menu to be open");
15449 }
15450 });
15451
15452 cx.update_editor(|editor, window, cx| {
15453 editor.move_page_down(&MovePageDown::default(), window, cx);
15454 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15455 {
15456 assert!(
15457 menu.selected_item == 1,
15458 "expected PageDown to select the last item from the context menu"
15459 );
15460 } else {
15461 panic!("expected completion menu to stay open after PageDown");
15462 }
15463 });
15464
15465 cx.update_editor(|editor, window, cx| {
15466 editor.move_page_up(&MovePageUp::default(), window, cx);
15467 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15468 {
15469 assert!(
15470 menu.selected_item == 0,
15471 "expected PageUp to select the first item from the context menu"
15472 );
15473 } else {
15474 panic!("expected completion menu to stay open after PageUp");
15475 }
15476 });
15477}
15478
15479#[gpui::test]
15480async fn test_as_is_completions(cx: &mut TestAppContext) {
15481 init_test(cx, |_| {});
15482 let mut cx = EditorLspTestContext::new_rust(
15483 lsp::ServerCapabilities {
15484 completion_provider: Some(lsp::CompletionOptions {
15485 ..Default::default()
15486 }),
15487 ..Default::default()
15488 },
15489 cx,
15490 )
15491 .await;
15492 cx.lsp
15493 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15494 Ok(Some(lsp::CompletionResponse::Array(vec![
15495 lsp::CompletionItem {
15496 label: "unsafe".into(),
15497 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15498 range: lsp::Range {
15499 start: lsp::Position {
15500 line: 1,
15501 character: 2,
15502 },
15503 end: lsp::Position {
15504 line: 1,
15505 character: 3,
15506 },
15507 },
15508 new_text: "unsafe".to_string(),
15509 })),
15510 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15511 ..Default::default()
15512 },
15513 ])))
15514 });
15515 cx.set_state("fn a() {}\n nˇ");
15516 cx.executor().run_until_parked();
15517 cx.update_editor(|editor, window, cx| {
15518 editor.trigger_completion_on_input("n", true, window, cx)
15519 });
15520 cx.executor().run_until_parked();
15521
15522 cx.update_editor(|editor, window, cx| {
15523 editor.confirm_completion(&Default::default(), window, cx)
15524 });
15525 cx.executor().run_until_parked();
15526 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15527}
15528
15529#[gpui::test]
15530async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15531 init_test(cx, |_| {});
15532 let language =
15533 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15534 let mut cx = EditorLspTestContext::new(
15535 language,
15536 lsp::ServerCapabilities {
15537 completion_provider: Some(lsp::CompletionOptions {
15538 ..lsp::CompletionOptions::default()
15539 }),
15540 ..lsp::ServerCapabilities::default()
15541 },
15542 cx,
15543 )
15544 .await;
15545
15546 cx.set_state(
15547 "#ifndef BAR_H
15548#define BAR_H
15549
15550#include <stdbool.h>
15551
15552int fn_branch(bool do_branch1, bool do_branch2);
15553
15554#endif // BAR_H
15555ˇ",
15556 );
15557 cx.executor().run_until_parked();
15558 cx.update_editor(|editor, window, cx| {
15559 editor.handle_input("#", window, cx);
15560 });
15561 cx.executor().run_until_parked();
15562 cx.update_editor(|editor, window, cx| {
15563 editor.handle_input("i", window, cx);
15564 });
15565 cx.executor().run_until_parked();
15566 cx.update_editor(|editor, window, cx| {
15567 editor.handle_input("n", window, cx);
15568 });
15569 cx.executor().run_until_parked();
15570 cx.assert_editor_state(
15571 "#ifndef BAR_H
15572#define BAR_H
15573
15574#include <stdbool.h>
15575
15576int fn_branch(bool do_branch1, bool do_branch2);
15577
15578#endif // BAR_H
15579#inˇ",
15580 );
15581
15582 cx.lsp
15583 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15584 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15585 is_incomplete: false,
15586 item_defaults: None,
15587 items: vec![lsp::CompletionItem {
15588 kind: Some(lsp::CompletionItemKind::SNIPPET),
15589 label_details: Some(lsp::CompletionItemLabelDetails {
15590 detail: Some("header".to_string()),
15591 description: None,
15592 }),
15593 label: " include".to_string(),
15594 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15595 range: lsp::Range {
15596 start: lsp::Position {
15597 line: 8,
15598 character: 1,
15599 },
15600 end: lsp::Position {
15601 line: 8,
15602 character: 1,
15603 },
15604 },
15605 new_text: "include \"$0\"".to_string(),
15606 })),
15607 sort_text: Some("40b67681include".to_string()),
15608 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15609 filter_text: Some("include".to_string()),
15610 insert_text: Some("include \"$0\"".to_string()),
15611 ..lsp::CompletionItem::default()
15612 }],
15613 })))
15614 });
15615 cx.update_editor(|editor, window, cx| {
15616 editor.show_completions(&ShowCompletions, window, cx);
15617 });
15618 cx.executor().run_until_parked();
15619 cx.update_editor(|editor, window, cx| {
15620 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15621 });
15622 cx.executor().run_until_parked();
15623 cx.assert_editor_state(
15624 "#ifndef BAR_H
15625#define BAR_H
15626
15627#include <stdbool.h>
15628
15629int fn_branch(bool do_branch1, bool do_branch2);
15630
15631#endif // BAR_H
15632#include \"ˇ\"",
15633 );
15634
15635 cx.lsp
15636 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15637 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15638 is_incomplete: true,
15639 item_defaults: None,
15640 items: vec![lsp::CompletionItem {
15641 kind: Some(lsp::CompletionItemKind::FILE),
15642 label: "AGL/".to_string(),
15643 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15644 range: lsp::Range {
15645 start: lsp::Position {
15646 line: 8,
15647 character: 10,
15648 },
15649 end: lsp::Position {
15650 line: 8,
15651 character: 11,
15652 },
15653 },
15654 new_text: "AGL/".to_string(),
15655 })),
15656 sort_text: Some("40b67681AGL/".to_string()),
15657 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15658 filter_text: Some("AGL/".to_string()),
15659 insert_text: Some("AGL/".to_string()),
15660 ..lsp::CompletionItem::default()
15661 }],
15662 })))
15663 });
15664 cx.update_editor(|editor, window, cx| {
15665 editor.show_completions(&ShowCompletions, window, cx);
15666 });
15667 cx.executor().run_until_parked();
15668 cx.update_editor(|editor, window, cx| {
15669 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15670 });
15671 cx.executor().run_until_parked();
15672 cx.assert_editor_state(
15673 r##"#ifndef BAR_H
15674#define BAR_H
15675
15676#include <stdbool.h>
15677
15678int fn_branch(bool do_branch1, bool do_branch2);
15679
15680#endif // BAR_H
15681#include "AGL/ˇ"##,
15682 );
15683
15684 cx.update_editor(|editor, window, cx| {
15685 editor.handle_input("\"", window, cx);
15686 });
15687 cx.executor().run_until_parked();
15688 cx.assert_editor_state(
15689 r##"#ifndef BAR_H
15690#define BAR_H
15691
15692#include <stdbool.h>
15693
15694int fn_branch(bool do_branch1, bool do_branch2);
15695
15696#endif // BAR_H
15697#include "AGL/"ˇ"##,
15698 );
15699}
15700
15701#[gpui::test]
15702async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15703 init_test(cx, |_| {});
15704
15705 let mut cx = EditorLspTestContext::new_rust(
15706 lsp::ServerCapabilities {
15707 completion_provider: Some(lsp::CompletionOptions {
15708 trigger_characters: Some(vec![".".to_string()]),
15709 resolve_provider: Some(true),
15710 ..Default::default()
15711 }),
15712 ..Default::default()
15713 },
15714 cx,
15715 )
15716 .await;
15717
15718 cx.set_state("fn main() { let a = 2ˇ; }");
15719 cx.simulate_keystroke(".");
15720 let completion_item = lsp::CompletionItem {
15721 label: "Some".into(),
15722 kind: Some(lsp::CompletionItemKind::SNIPPET),
15723 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15724 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15725 kind: lsp::MarkupKind::Markdown,
15726 value: "```rust\nSome(2)\n```".to_string(),
15727 })),
15728 deprecated: Some(false),
15729 sort_text: Some("Some".to_string()),
15730 filter_text: Some("Some".to_string()),
15731 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15732 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15733 range: lsp::Range {
15734 start: lsp::Position {
15735 line: 0,
15736 character: 22,
15737 },
15738 end: lsp::Position {
15739 line: 0,
15740 character: 22,
15741 },
15742 },
15743 new_text: "Some(2)".to_string(),
15744 })),
15745 additional_text_edits: Some(vec![lsp::TextEdit {
15746 range: lsp::Range {
15747 start: lsp::Position {
15748 line: 0,
15749 character: 20,
15750 },
15751 end: lsp::Position {
15752 line: 0,
15753 character: 22,
15754 },
15755 },
15756 new_text: "".to_string(),
15757 }]),
15758 ..Default::default()
15759 };
15760
15761 let closure_completion_item = completion_item.clone();
15762 let counter = Arc::new(AtomicUsize::new(0));
15763 let counter_clone = counter.clone();
15764 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15765 let task_completion_item = closure_completion_item.clone();
15766 counter_clone.fetch_add(1, atomic::Ordering::Release);
15767 async move {
15768 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15769 is_incomplete: true,
15770 item_defaults: None,
15771 items: vec![task_completion_item],
15772 })))
15773 }
15774 });
15775
15776 cx.condition(|editor, _| editor.context_menu_visible())
15777 .await;
15778 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15779 assert!(request.next().await.is_some());
15780 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15781
15782 cx.simulate_keystrokes("S o m");
15783 cx.condition(|editor, _| editor.context_menu_visible())
15784 .await;
15785 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15786 assert!(request.next().await.is_some());
15787 assert!(request.next().await.is_some());
15788 assert!(request.next().await.is_some());
15789 request.close();
15790 assert!(request.next().await.is_none());
15791 assert_eq!(
15792 counter.load(atomic::Ordering::Acquire),
15793 4,
15794 "With the completions menu open, only one LSP request should happen per input"
15795 );
15796}
15797
15798#[gpui::test]
15799async fn test_toggle_comment(cx: &mut TestAppContext) {
15800 init_test(cx, |_| {});
15801 let mut cx = EditorTestContext::new(cx).await;
15802 let language = Arc::new(Language::new(
15803 LanguageConfig {
15804 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15805 ..Default::default()
15806 },
15807 Some(tree_sitter_rust::LANGUAGE.into()),
15808 ));
15809 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15810
15811 // If multiple selections intersect a line, the line is only toggled once.
15812 cx.set_state(indoc! {"
15813 fn a() {
15814 «//b();
15815 ˇ»// «c();
15816 //ˇ» d();
15817 }
15818 "});
15819
15820 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15821
15822 cx.assert_editor_state(indoc! {"
15823 fn a() {
15824 «b();
15825 c();
15826 ˇ» d();
15827 }
15828 "});
15829
15830 // The comment prefix is inserted at the same column for every line in a
15831 // selection.
15832 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15833
15834 cx.assert_editor_state(indoc! {"
15835 fn a() {
15836 // «b();
15837 // c();
15838 ˇ»// d();
15839 }
15840 "});
15841
15842 // If a selection ends at the beginning of a line, that line is not toggled.
15843 cx.set_selections_state(indoc! {"
15844 fn a() {
15845 // b();
15846 «// c();
15847 ˇ» // d();
15848 }
15849 "});
15850
15851 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15852
15853 cx.assert_editor_state(indoc! {"
15854 fn a() {
15855 // b();
15856 «c();
15857 ˇ» // d();
15858 }
15859 "});
15860
15861 // If a selection span a single line and is empty, the line is toggled.
15862 cx.set_state(indoc! {"
15863 fn a() {
15864 a();
15865 b();
15866 ˇ
15867 }
15868 "});
15869
15870 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15871
15872 cx.assert_editor_state(indoc! {"
15873 fn a() {
15874 a();
15875 b();
15876 //•ˇ
15877 }
15878 "});
15879
15880 // If a selection span multiple lines, empty lines are not toggled.
15881 cx.set_state(indoc! {"
15882 fn a() {
15883 «a();
15884
15885 c();ˇ»
15886 }
15887 "});
15888
15889 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15890
15891 cx.assert_editor_state(indoc! {"
15892 fn a() {
15893 // «a();
15894
15895 // c();ˇ»
15896 }
15897 "});
15898
15899 // If a selection includes multiple comment prefixes, all lines are uncommented.
15900 cx.set_state(indoc! {"
15901 fn a() {
15902 «// a();
15903 /// b();
15904 //! c();ˇ»
15905 }
15906 "});
15907
15908 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15909
15910 cx.assert_editor_state(indoc! {"
15911 fn a() {
15912 «a();
15913 b();
15914 c();ˇ»
15915 }
15916 "});
15917}
15918
15919#[gpui::test]
15920async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15921 init_test(cx, |_| {});
15922 let mut cx = EditorTestContext::new(cx).await;
15923 let language = Arc::new(Language::new(
15924 LanguageConfig {
15925 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15926 ..Default::default()
15927 },
15928 Some(tree_sitter_rust::LANGUAGE.into()),
15929 ));
15930 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15931
15932 let toggle_comments = &ToggleComments {
15933 advance_downwards: false,
15934 ignore_indent: true,
15935 };
15936
15937 // If multiple selections intersect a line, the line is only toggled once.
15938 cx.set_state(indoc! {"
15939 fn a() {
15940 // «b();
15941 // c();
15942 // ˇ» d();
15943 }
15944 "});
15945
15946 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15947
15948 cx.assert_editor_state(indoc! {"
15949 fn a() {
15950 «b();
15951 c();
15952 ˇ» d();
15953 }
15954 "});
15955
15956 // The comment prefix is inserted at the beginning of each line
15957 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15958
15959 cx.assert_editor_state(indoc! {"
15960 fn a() {
15961 // «b();
15962 // c();
15963 // ˇ» d();
15964 }
15965 "});
15966
15967 // If a selection ends at the beginning of a line, that line is not toggled.
15968 cx.set_selections_state(indoc! {"
15969 fn a() {
15970 // b();
15971 // «c();
15972 ˇ»// d();
15973 }
15974 "});
15975
15976 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15977
15978 cx.assert_editor_state(indoc! {"
15979 fn a() {
15980 // b();
15981 «c();
15982 ˇ»// d();
15983 }
15984 "});
15985
15986 // If a selection span a single line and is empty, the line is toggled.
15987 cx.set_state(indoc! {"
15988 fn a() {
15989 a();
15990 b();
15991 ˇ
15992 }
15993 "});
15994
15995 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15996
15997 cx.assert_editor_state(indoc! {"
15998 fn a() {
15999 a();
16000 b();
16001 //ˇ
16002 }
16003 "});
16004
16005 // If a selection span multiple lines, empty lines are not toggled.
16006 cx.set_state(indoc! {"
16007 fn a() {
16008 «a();
16009
16010 c();ˇ»
16011 }
16012 "});
16013
16014 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16015
16016 cx.assert_editor_state(indoc! {"
16017 fn a() {
16018 // «a();
16019
16020 // c();ˇ»
16021 }
16022 "});
16023
16024 // If a selection includes multiple comment prefixes, all lines are uncommented.
16025 cx.set_state(indoc! {"
16026 fn a() {
16027 // «a();
16028 /// b();
16029 //! c();ˇ»
16030 }
16031 "});
16032
16033 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16034
16035 cx.assert_editor_state(indoc! {"
16036 fn a() {
16037 «a();
16038 b();
16039 c();ˇ»
16040 }
16041 "});
16042}
16043
16044#[gpui::test]
16045async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16046 init_test(cx, |_| {});
16047
16048 let language = Arc::new(Language::new(
16049 LanguageConfig {
16050 line_comments: vec!["// ".into()],
16051 ..Default::default()
16052 },
16053 Some(tree_sitter_rust::LANGUAGE.into()),
16054 ));
16055
16056 let mut cx = EditorTestContext::new(cx).await;
16057
16058 cx.language_registry().add(language.clone());
16059 cx.update_buffer(|buffer, cx| {
16060 buffer.set_language(Some(language), cx);
16061 });
16062
16063 let toggle_comments = &ToggleComments {
16064 advance_downwards: true,
16065 ignore_indent: false,
16066 };
16067
16068 // Single cursor on one line -> advance
16069 // Cursor moves horizontally 3 characters as well on non-blank line
16070 cx.set_state(indoc!(
16071 "fn a() {
16072 ˇdog();
16073 cat();
16074 }"
16075 ));
16076 cx.update_editor(|editor, window, cx| {
16077 editor.toggle_comments(toggle_comments, window, cx);
16078 });
16079 cx.assert_editor_state(indoc!(
16080 "fn a() {
16081 // dog();
16082 catˇ();
16083 }"
16084 ));
16085
16086 // Single selection on one line -> don't advance
16087 cx.set_state(indoc!(
16088 "fn a() {
16089 «dog()ˇ»;
16090 cat();
16091 }"
16092 ));
16093 cx.update_editor(|editor, window, cx| {
16094 editor.toggle_comments(toggle_comments, window, cx);
16095 });
16096 cx.assert_editor_state(indoc!(
16097 "fn a() {
16098 // «dog()ˇ»;
16099 cat();
16100 }"
16101 ));
16102
16103 // Multiple cursors on one line -> advance
16104 cx.set_state(indoc!(
16105 "fn a() {
16106 ˇdˇog();
16107 cat();
16108 }"
16109 ));
16110 cx.update_editor(|editor, window, cx| {
16111 editor.toggle_comments(toggle_comments, window, cx);
16112 });
16113 cx.assert_editor_state(indoc!(
16114 "fn a() {
16115 // dog();
16116 catˇ(ˇ);
16117 }"
16118 ));
16119
16120 // Multiple cursors on one line, with selection -> don't advance
16121 cx.set_state(indoc!(
16122 "fn a() {
16123 ˇdˇog«()ˇ»;
16124 cat();
16125 }"
16126 ));
16127 cx.update_editor(|editor, window, cx| {
16128 editor.toggle_comments(toggle_comments, window, cx);
16129 });
16130 cx.assert_editor_state(indoc!(
16131 "fn a() {
16132 // ˇdˇog«()ˇ»;
16133 cat();
16134 }"
16135 ));
16136
16137 // Single cursor on one line -> advance
16138 // Cursor moves to column 0 on blank line
16139 cx.set_state(indoc!(
16140 "fn a() {
16141 ˇdog();
16142
16143 cat();
16144 }"
16145 ));
16146 cx.update_editor(|editor, window, cx| {
16147 editor.toggle_comments(toggle_comments, window, cx);
16148 });
16149 cx.assert_editor_state(indoc!(
16150 "fn a() {
16151 // dog();
16152 ˇ
16153 cat();
16154 }"
16155 ));
16156
16157 // Single cursor on one line -> advance
16158 // Cursor starts and ends at column 0
16159 cx.set_state(indoc!(
16160 "fn a() {
16161 ˇ dog();
16162 cat();
16163 }"
16164 ));
16165 cx.update_editor(|editor, window, cx| {
16166 editor.toggle_comments(toggle_comments, window, cx);
16167 });
16168 cx.assert_editor_state(indoc!(
16169 "fn a() {
16170 // dog();
16171 ˇ cat();
16172 }"
16173 ));
16174}
16175
16176#[gpui::test]
16177async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16178 init_test(cx, |_| {});
16179
16180 let mut cx = EditorTestContext::new(cx).await;
16181
16182 let html_language = Arc::new(
16183 Language::new(
16184 LanguageConfig {
16185 name: "HTML".into(),
16186 block_comment: Some(BlockCommentConfig {
16187 start: "<!-- ".into(),
16188 prefix: "".into(),
16189 end: " -->".into(),
16190 tab_size: 0,
16191 }),
16192 ..Default::default()
16193 },
16194 Some(tree_sitter_html::LANGUAGE.into()),
16195 )
16196 .with_injection_query(
16197 r#"
16198 (script_element
16199 (raw_text) @injection.content
16200 (#set! injection.language "javascript"))
16201 "#,
16202 )
16203 .unwrap(),
16204 );
16205
16206 let javascript_language = Arc::new(Language::new(
16207 LanguageConfig {
16208 name: "JavaScript".into(),
16209 line_comments: vec!["// ".into()],
16210 ..Default::default()
16211 },
16212 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16213 ));
16214
16215 cx.language_registry().add(html_language.clone());
16216 cx.language_registry().add(javascript_language);
16217 cx.update_buffer(|buffer, cx| {
16218 buffer.set_language(Some(html_language), cx);
16219 });
16220
16221 // Toggle comments for empty selections
16222 cx.set_state(
16223 &r#"
16224 <p>A</p>ˇ
16225 <p>B</p>ˇ
16226 <p>C</p>ˇ
16227 "#
16228 .unindent(),
16229 );
16230 cx.update_editor(|editor, window, cx| {
16231 editor.toggle_comments(&ToggleComments::default(), window, cx)
16232 });
16233 cx.assert_editor_state(
16234 &r#"
16235 <!-- <p>A</p>ˇ -->
16236 <!-- <p>B</p>ˇ -->
16237 <!-- <p>C</p>ˇ -->
16238 "#
16239 .unindent(),
16240 );
16241 cx.update_editor(|editor, window, cx| {
16242 editor.toggle_comments(&ToggleComments::default(), window, cx)
16243 });
16244 cx.assert_editor_state(
16245 &r#"
16246 <p>A</p>ˇ
16247 <p>B</p>ˇ
16248 <p>C</p>ˇ
16249 "#
16250 .unindent(),
16251 );
16252
16253 // Toggle comments for mixture of empty and non-empty selections, where
16254 // multiple selections occupy a given line.
16255 cx.set_state(
16256 &r#"
16257 <p>A«</p>
16258 <p>ˇ»B</p>ˇ
16259 <p>C«</p>
16260 <p>ˇ»D</p>ˇ
16261 "#
16262 .unindent(),
16263 );
16264
16265 cx.update_editor(|editor, window, cx| {
16266 editor.toggle_comments(&ToggleComments::default(), window, cx)
16267 });
16268 cx.assert_editor_state(
16269 &r#"
16270 <!-- <p>A«</p>
16271 <p>ˇ»B</p>ˇ -->
16272 <!-- <p>C«</p>
16273 <p>ˇ»D</p>ˇ -->
16274 "#
16275 .unindent(),
16276 );
16277 cx.update_editor(|editor, window, cx| {
16278 editor.toggle_comments(&ToggleComments::default(), window, cx)
16279 });
16280 cx.assert_editor_state(
16281 &r#"
16282 <p>A«</p>
16283 <p>ˇ»B</p>ˇ
16284 <p>C«</p>
16285 <p>ˇ»D</p>ˇ
16286 "#
16287 .unindent(),
16288 );
16289
16290 // Toggle comments when different languages are active for different
16291 // selections.
16292 cx.set_state(
16293 &r#"
16294 ˇ<script>
16295 ˇvar x = new Y();
16296 ˇ</script>
16297 "#
16298 .unindent(),
16299 );
16300 cx.executor().run_until_parked();
16301 cx.update_editor(|editor, window, cx| {
16302 editor.toggle_comments(&ToggleComments::default(), window, cx)
16303 });
16304 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16305 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16306 cx.assert_editor_state(
16307 &r#"
16308 <!-- ˇ<script> -->
16309 // ˇvar x = new Y();
16310 <!-- ˇ</script> -->
16311 "#
16312 .unindent(),
16313 );
16314}
16315
16316#[gpui::test]
16317fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16318 init_test(cx, |_| {});
16319
16320 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16321 let multibuffer = cx.new(|cx| {
16322 let mut multibuffer = MultiBuffer::new(ReadWrite);
16323 multibuffer.push_excerpts(
16324 buffer.clone(),
16325 [
16326 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16327 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16328 ],
16329 cx,
16330 );
16331 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16332 multibuffer
16333 });
16334
16335 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16336 editor.update_in(cx, |editor, window, cx| {
16337 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16338 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16339 s.select_ranges([
16340 Point::new(0, 0)..Point::new(0, 0),
16341 Point::new(1, 0)..Point::new(1, 0),
16342 ])
16343 });
16344
16345 editor.handle_input("X", window, cx);
16346 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16347 assert_eq!(
16348 editor.selections.ranges(&editor.display_snapshot(cx)),
16349 [
16350 Point::new(0, 1)..Point::new(0, 1),
16351 Point::new(1, 1)..Point::new(1, 1),
16352 ]
16353 );
16354
16355 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16356 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16357 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16358 });
16359 editor.backspace(&Default::default(), window, cx);
16360 assert_eq!(editor.text(cx), "Xa\nbbb");
16361 assert_eq!(
16362 editor.selections.ranges(&editor.display_snapshot(cx)),
16363 [Point::new(1, 0)..Point::new(1, 0)]
16364 );
16365
16366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16367 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16368 });
16369 editor.backspace(&Default::default(), window, cx);
16370 assert_eq!(editor.text(cx), "X\nbb");
16371 assert_eq!(
16372 editor.selections.ranges(&editor.display_snapshot(cx)),
16373 [Point::new(0, 1)..Point::new(0, 1)]
16374 );
16375 });
16376}
16377
16378#[gpui::test]
16379fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16380 init_test(cx, |_| {});
16381
16382 let markers = vec![('[', ']').into(), ('(', ')').into()];
16383 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16384 indoc! {"
16385 [aaaa
16386 (bbbb]
16387 cccc)",
16388 },
16389 markers.clone(),
16390 );
16391 let excerpt_ranges = markers.into_iter().map(|marker| {
16392 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16393 ExcerptRange::new(context)
16394 });
16395 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16396 let multibuffer = cx.new(|cx| {
16397 let mut multibuffer = MultiBuffer::new(ReadWrite);
16398 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16399 multibuffer
16400 });
16401
16402 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16403 editor.update_in(cx, |editor, window, cx| {
16404 let (expected_text, selection_ranges) = marked_text_ranges(
16405 indoc! {"
16406 aaaa
16407 bˇbbb
16408 bˇbbˇb
16409 cccc"
16410 },
16411 true,
16412 );
16413 assert_eq!(editor.text(cx), expected_text);
16414 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16415 s.select_ranges(selection_ranges)
16416 });
16417
16418 editor.handle_input("X", window, cx);
16419
16420 let (expected_text, expected_selections) = marked_text_ranges(
16421 indoc! {"
16422 aaaa
16423 bXˇbbXb
16424 bXˇbbXˇb
16425 cccc"
16426 },
16427 false,
16428 );
16429 assert_eq!(editor.text(cx), expected_text);
16430 assert_eq!(
16431 editor.selections.ranges(&editor.display_snapshot(cx)),
16432 expected_selections
16433 );
16434
16435 editor.newline(&Newline, window, cx);
16436 let (expected_text, expected_selections) = marked_text_ranges(
16437 indoc! {"
16438 aaaa
16439 bX
16440 ˇbbX
16441 b
16442 bX
16443 ˇbbX
16444 ˇb
16445 cccc"
16446 },
16447 false,
16448 );
16449 assert_eq!(editor.text(cx), expected_text);
16450 assert_eq!(
16451 editor.selections.ranges(&editor.display_snapshot(cx)),
16452 expected_selections
16453 );
16454 });
16455}
16456
16457#[gpui::test]
16458fn test_refresh_selections(cx: &mut TestAppContext) {
16459 init_test(cx, |_| {});
16460
16461 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16462 let mut excerpt1_id = None;
16463 let multibuffer = cx.new(|cx| {
16464 let mut multibuffer = MultiBuffer::new(ReadWrite);
16465 excerpt1_id = multibuffer
16466 .push_excerpts(
16467 buffer.clone(),
16468 [
16469 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16470 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16471 ],
16472 cx,
16473 )
16474 .into_iter()
16475 .next();
16476 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16477 multibuffer
16478 });
16479
16480 let editor = cx.add_window(|window, cx| {
16481 let mut editor = build_editor(multibuffer.clone(), window, cx);
16482 let snapshot = editor.snapshot(window, cx);
16483 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16484 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16485 });
16486 editor.begin_selection(
16487 Point::new(2, 1).to_display_point(&snapshot),
16488 true,
16489 1,
16490 window,
16491 cx,
16492 );
16493 assert_eq!(
16494 editor.selections.ranges(&editor.display_snapshot(cx)),
16495 [
16496 Point::new(1, 3)..Point::new(1, 3),
16497 Point::new(2, 1)..Point::new(2, 1),
16498 ]
16499 );
16500 editor
16501 });
16502
16503 // Refreshing selections is a no-op when excerpts haven't changed.
16504 _ = editor.update(cx, |editor, window, cx| {
16505 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16506 assert_eq!(
16507 editor.selections.ranges(&editor.display_snapshot(cx)),
16508 [
16509 Point::new(1, 3)..Point::new(1, 3),
16510 Point::new(2, 1)..Point::new(2, 1),
16511 ]
16512 );
16513 });
16514
16515 multibuffer.update(cx, |multibuffer, cx| {
16516 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16517 });
16518 _ = editor.update(cx, |editor, window, cx| {
16519 // Removing an excerpt causes the first selection to become degenerate.
16520 assert_eq!(
16521 editor.selections.ranges(&editor.display_snapshot(cx)),
16522 [
16523 Point::new(0, 0)..Point::new(0, 0),
16524 Point::new(0, 1)..Point::new(0, 1)
16525 ]
16526 );
16527
16528 // Refreshing selections will relocate the first selection to the original buffer
16529 // location.
16530 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16531 assert_eq!(
16532 editor.selections.ranges(&editor.display_snapshot(cx)),
16533 [
16534 Point::new(0, 1)..Point::new(0, 1),
16535 Point::new(0, 3)..Point::new(0, 3)
16536 ]
16537 );
16538 assert!(editor.selections.pending_anchor().is_some());
16539 });
16540}
16541
16542#[gpui::test]
16543fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16544 init_test(cx, |_| {});
16545
16546 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16547 let mut excerpt1_id = None;
16548 let multibuffer = cx.new(|cx| {
16549 let mut multibuffer = MultiBuffer::new(ReadWrite);
16550 excerpt1_id = multibuffer
16551 .push_excerpts(
16552 buffer.clone(),
16553 [
16554 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16555 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16556 ],
16557 cx,
16558 )
16559 .into_iter()
16560 .next();
16561 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16562 multibuffer
16563 });
16564
16565 let editor = cx.add_window(|window, cx| {
16566 let mut editor = build_editor(multibuffer.clone(), window, cx);
16567 let snapshot = editor.snapshot(window, cx);
16568 editor.begin_selection(
16569 Point::new(1, 3).to_display_point(&snapshot),
16570 false,
16571 1,
16572 window,
16573 cx,
16574 );
16575 assert_eq!(
16576 editor.selections.ranges(&editor.display_snapshot(cx)),
16577 [Point::new(1, 3)..Point::new(1, 3)]
16578 );
16579 editor
16580 });
16581
16582 multibuffer.update(cx, |multibuffer, cx| {
16583 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16584 });
16585 _ = editor.update(cx, |editor, window, cx| {
16586 assert_eq!(
16587 editor.selections.ranges(&editor.display_snapshot(cx)),
16588 [Point::new(0, 0)..Point::new(0, 0)]
16589 );
16590
16591 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16592 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16593 assert_eq!(
16594 editor.selections.ranges(&editor.display_snapshot(cx)),
16595 [Point::new(0, 3)..Point::new(0, 3)]
16596 );
16597 assert!(editor.selections.pending_anchor().is_some());
16598 });
16599}
16600
16601#[gpui::test]
16602async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16603 init_test(cx, |_| {});
16604
16605 let language = Arc::new(
16606 Language::new(
16607 LanguageConfig {
16608 brackets: BracketPairConfig {
16609 pairs: vec![
16610 BracketPair {
16611 start: "{".to_string(),
16612 end: "}".to_string(),
16613 close: true,
16614 surround: true,
16615 newline: true,
16616 },
16617 BracketPair {
16618 start: "/* ".to_string(),
16619 end: " */".to_string(),
16620 close: true,
16621 surround: true,
16622 newline: true,
16623 },
16624 ],
16625 ..Default::default()
16626 },
16627 ..Default::default()
16628 },
16629 Some(tree_sitter_rust::LANGUAGE.into()),
16630 )
16631 .with_indents_query("")
16632 .unwrap(),
16633 );
16634
16635 let text = concat!(
16636 "{ }\n", //
16637 " x\n", //
16638 " /* */\n", //
16639 "x\n", //
16640 "{{} }\n", //
16641 );
16642
16643 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16644 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16645 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16646 editor
16647 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16648 .await;
16649
16650 editor.update_in(cx, |editor, window, cx| {
16651 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16652 s.select_display_ranges([
16653 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16654 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16655 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16656 ])
16657 });
16658 editor.newline(&Newline, window, cx);
16659
16660 assert_eq!(
16661 editor.buffer().read(cx).read(cx).text(),
16662 concat!(
16663 "{ \n", // Suppress rustfmt
16664 "\n", //
16665 "}\n", //
16666 " x\n", //
16667 " /* \n", //
16668 " \n", //
16669 " */\n", //
16670 "x\n", //
16671 "{{} \n", //
16672 "}\n", //
16673 )
16674 );
16675 });
16676}
16677
16678#[gpui::test]
16679fn test_highlighted_ranges(cx: &mut TestAppContext) {
16680 init_test(cx, |_| {});
16681
16682 let editor = cx.add_window(|window, cx| {
16683 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16684 build_editor(buffer, window, cx)
16685 });
16686
16687 _ = editor.update(cx, |editor, window, cx| {
16688 struct Type1;
16689 struct Type2;
16690
16691 let buffer = editor.buffer.read(cx).snapshot(cx);
16692
16693 let anchor_range =
16694 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16695
16696 editor.highlight_background::<Type1>(
16697 &[
16698 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16699 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16700 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16701 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16702 ],
16703 |_| Hsla::red(),
16704 cx,
16705 );
16706 editor.highlight_background::<Type2>(
16707 &[
16708 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16709 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16710 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16711 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16712 ],
16713 |_| Hsla::green(),
16714 cx,
16715 );
16716
16717 let snapshot = editor.snapshot(window, cx);
16718 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16719 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16720 &snapshot,
16721 cx.theme(),
16722 );
16723 assert_eq!(
16724 highlighted_ranges,
16725 &[
16726 (
16727 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16728 Hsla::green(),
16729 ),
16730 (
16731 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16732 Hsla::red(),
16733 ),
16734 (
16735 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16736 Hsla::green(),
16737 ),
16738 (
16739 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16740 Hsla::red(),
16741 ),
16742 ]
16743 );
16744 assert_eq!(
16745 editor.sorted_background_highlights_in_range(
16746 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16747 &snapshot,
16748 cx.theme(),
16749 ),
16750 &[(
16751 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16752 Hsla::red(),
16753 )]
16754 );
16755 });
16756}
16757
16758#[gpui::test]
16759async fn test_following(cx: &mut TestAppContext) {
16760 init_test(cx, |_| {});
16761
16762 let fs = FakeFs::new(cx.executor());
16763 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16764
16765 let buffer = project.update(cx, |project, cx| {
16766 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16767 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16768 });
16769 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16770 let follower = cx.update(|cx| {
16771 cx.open_window(
16772 WindowOptions {
16773 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16774 gpui::Point::new(px(0.), px(0.)),
16775 gpui::Point::new(px(10.), px(80.)),
16776 ))),
16777 ..Default::default()
16778 },
16779 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16780 )
16781 .unwrap()
16782 });
16783
16784 let is_still_following = Rc::new(RefCell::new(true));
16785 let follower_edit_event_count = Rc::new(RefCell::new(0));
16786 let pending_update = Rc::new(RefCell::new(None));
16787 let leader_entity = leader.root(cx).unwrap();
16788 let follower_entity = follower.root(cx).unwrap();
16789 _ = follower.update(cx, {
16790 let update = pending_update.clone();
16791 let is_still_following = is_still_following.clone();
16792 let follower_edit_event_count = follower_edit_event_count.clone();
16793 |_, window, cx| {
16794 cx.subscribe_in(
16795 &leader_entity,
16796 window,
16797 move |_, leader, event, window, cx| {
16798 leader.read(cx).add_event_to_update_proto(
16799 event,
16800 &mut update.borrow_mut(),
16801 window,
16802 cx,
16803 );
16804 },
16805 )
16806 .detach();
16807
16808 cx.subscribe_in(
16809 &follower_entity,
16810 window,
16811 move |_, _, event: &EditorEvent, _window, _cx| {
16812 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16813 *is_still_following.borrow_mut() = false;
16814 }
16815
16816 if let EditorEvent::BufferEdited = event {
16817 *follower_edit_event_count.borrow_mut() += 1;
16818 }
16819 },
16820 )
16821 .detach();
16822 }
16823 });
16824
16825 // Update the selections only
16826 _ = leader.update(cx, |leader, window, cx| {
16827 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16828 s.select_ranges([1..1])
16829 });
16830 });
16831 follower
16832 .update(cx, |follower, window, cx| {
16833 follower.apply_update_proto(
16834 &project,
16835 pending_update.borrow_mut().take().unwrap(),
16836 window,
16837 cx,
16838 )
16839 })
16840 .unwrap()
16841 .await
16842 .unwrap();
16843 _ = follower.update(cx, |follower, _, cx| {
16844 assert_eq!(
16845 follower.selections.ranges(&follower.display_snapshot(cx)),
16846 vec![1..1]
16847 );
16848 });
16849 assert!(*is_still_following.borrow());
16850 assert_eq!(*follower_edit_event_count.borrow(), 0);
16851
16852 // Update the scroll position only
16853 _ = leader.update(cx, |leader, window, cx| {
16854 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16855 });
16856 follower
16857 .update(cx, |follower, window, cx| {
16858 follower.apply_update_proto(
16859 &project,
16860 pending_update.borrow_mut().take().unwrap(),
16861 window,
16862 cx,
16863 )
16864 })
16865 .unwrap()
16866 .await
16867 .unwrap();
16868 assert_eq!(
16869 follower
16870 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16871 .unwrap(),
16872 gpui::Point::new(1.5, 3.5)
16873 );
16874 assert!(*is_still_following.borrow());
16875 assert_eq!(*follower_edit_event_count.borrow(), 0);
16876
16877 // Update the selections and scroll position. The follower's scroll position is updated
16878 // via autoscroll, not via the leader's exact scroll position.
16879 _ = leader.update(cx, |leader, window, cx| {
16880 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16881 s.select_ranges([0..0])
16882 });
16883 leader.request_autoscroll(Autoscroll::newest(), cx);
16884 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16885 });
16886 follower
16887 .update(cx, |follower, window, cx| {
16888 follower.apply_update_proto(
16889 &project,
16890 pending_update.borrow_mut().take().unwrap(),
16891 window,
16892 cx,
16893 )
16894 })
16895 .unwrap()
16896 .await
16897 .unwrap();
16898 _ = follower.update(cx, |follower, _, cx| {
16899 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16900 assert_eq!(
16901 follower.selections.ranges(&follower.display_snapshot(cx)),
16902 vec![0..0]
16903 );
16904 });
16905 assert!(*is_still_following.borrow());
16906
16907 // Creating a pending selection that precedes another selection
16908 _ = leader.update(cx, |leader, window, cx| {
16909 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16910 s.select_ranges([1..1])
16911 });
16912 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16913 });
16914 follower
16915 .update(cx, |follower, window, cx| {
16916 follower.apply_update_proto(
16917 &project,
16918 pending_update.borrow_mut().take().unwrap(),
16919 window,
16920 cx,
16921 )
16922 })
16923 .unwrap()
16924 .await
16925 .unwrap();
16926 _ = follower.update(cx, |follower, _, cx| {
16927 assert_eq!(
16928 follower.selections.ranges(&follower.display_snapshot(cx)),
16929 vec![0..0, 1..1]
16930 );
16931 });
16932 assert!(*is_still_following.borrow());
16933
16934 // Extend the pending selection so that it surrounds another selection
16935 _ = leader.update(cx, |leader, window, cx| {
16936 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16937 });
16938 follower
16939 .update(cx, |follower, window, cx| {
16940 follower.apply_update_proto(
16941 &project,
16942 pending_update.borrow_mut().take().unwrap(),
16943 window,
16944 cx,
16945 )
16946 })
16947 .unwrap()
16948 .await
16949 .unwrap();
16950 _ = follower.update(cx, |follower, _, cx| {
16951 assert_eq!(
16952 follower.selections.ranges(&follower.display_snapshot(cx)),
16953 vec![0..2]
16954 );
16955 });
16956
16957 // Scrolling locally breaks the follow
16958 _ = follower.update(cx, |follower, window, cx| {
16959 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16960 follower.set_scroll_anchor(
16961 ScrollAnchor {
16962 anchor: top_anchor,
16963 offset: gpui::Point::new(0.0, 0.5),
16964 },
16965 window,
16966 cx,
16967 );
16968 });
16969 assert!(!(*is_still_following.borrow()));
16970}
16971
16972#[gpui::test]
16973async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16974 init_test(cx, |_| {});
16975
16976 let fs = FakeFs::new(cx.executor());
16977 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16978 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16979 let pane = workspace
16980 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16981 .unwrap();
16982
16983 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16984
16985 let leader = pane.update_in(cx, |_, window, cx| {
16986 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16987 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16988 });
16989
16990 // Start following the editor when it has no excerpts.
16991 let mut state_message =
16992 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16993 let workspace_entity = workspace.root(cx).unwrap();
16994 let follower_1 = cx
16995 .update_window(*workspace.deref(), |_, window, cx| {
16996 Editor::from_state_proto(
16997 workspace_entity,
16998 ViewId {
16999 creator: CollaboratorId::PeerId(PeerId::default()),
17000 id: 0,
17001 },
17002 &mut state_message,
17003 window,
17004 cx,
17005 )
17006 })
17007 .unwrap()
17008 .unwrap()
17009 .await
17010 .unwrap();
17011
17012 let update_message = Rc::new(RefCell::new(None));
17013 follower_1.update_in(cx, {
17014 let update = update_message.clone();
17015 |_, window, cx| {
17016 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17017 leader.read(cx).add_event_to_update_proto(
17018 event,
17019 &mut update.borrow_mut(),
17020 window,
17021 cx,
17022 );
17023 })
17024 .detach();
17025 }
17026 });
17027
17028 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17029 (
17030 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17031 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17032 )
17033 });
17034
17035 // Insert some excerpts.
17036 leader.update(cx, |leader, cx| {
17037 leader.buffer.update(cx, |multibuffer, cx| {
17038 multibuffer.set_excerpts_for_path(
17039 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17040 buffer_1.clone(),
17041 vec![
17042 Point::row_range(0..3),
17043 Point::row_range(1..6),
17044 Point::row_range(12..15),
17045 ],
17046 0,
17047 cx,
17048 );
17049 multibuffer.set_excerpts_for_path(
17050 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17051 buffer_2.clone(),
17052 vec![Point::row_range(0..6), Point::row_range(8..12)],
17053 0,
17054 cx,
17055 );
17056 });
17057 });
17058
17059 // Apply the update of adding the excerpts.
17060 follower_1
17061 .update_in(cx, |follower, window, cx| {
17062 follower.apply_update_proto(
17063 &project,
17064 update_message.borrow().clone().unwrap(),
17065 window,
17066 cx,
17067 )
17068 })
17069 .await
17070 .unwrap();
17071 assert_eq!(
17072 follower_1.update(cx, |editor, cx| editor.text(cx)),
17073 leader.update(cx, |editor, cx| editor.text(cx))
17074 );
17075 update_message.borrow_mut().take();
17076
17077 // Start following separately after it already has excerpts.
17078 let mut state_message =
17079 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17080 let workspace_entity = workspace.root(cx).unwrap();
17081 let follower_2 = cx
17082 .update_window(*workspace.deref(), |_, window, cx| {
17083 Editor::from_state_proto(
17084 workspace_entity,
17085 ViewId {
17086 creator: CollaboratorId::PeerId(PeerId::default()),
17087 id: 0,
17088 },
17089 &mut state_message,
17090 window,
17091 cx,
17092 )
17093 })
17094 .unwrap()
17095 .unwrap()
17096 .await
17097 .unwrap();
17098 assert_eq!(
17099 follower_2.update(cx, |editor, cx| editor.text(cx)),
17100 leader.update(cx, |editor, cx| editor.text(cx))
17101 );
17102
17103 // Remove some excerpts.
17104 leader.update(cx, |leader, cx| {
17105 leader.buffer.update(cx, |multibuffer, cx| {
17106 let excerpt_ids = multibuffer.excerpt_ids();
17107 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17108 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17109 });
17110 });
17111
17112 // Apply the update of removing the excerpts.
17113 follower_1
17114 .update_in(cx, |follower, window, cx| {
17115 follower.apply_update_proto(
17116 &project,
17117 update_message.borrow().clone().unwrap(),
17118 window,
17119 cx,
17120 )
17121 })
17122 .await
17123 .unwrap();
17124 follower_2
17125 .update_in(cx, |follower, window, cx| {
17126 follower.apply_update_proto(
17127 &project,
17128 update_message.borrow().clone().unwrap(),
17129 window,
17130 cx,
17131 )
17132 })
17133 .await
17134 .unwrap();
17135 update_message.borrow_mut().take();
17136 assert_eq!(
17137 follower_1.update(cx, |editor, cx| editor.text(cx)),
17138 leader.update(cx, |editor, cx| editor.text(cx))
17139 );
17140}
17141
17142#[gpui::test]
17143async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17144 init_test(cx, |_| {});
17145
17146 let mut cx = EditorTestContext::new(cx).await;
17147 let lsp_store =
17148 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17149
17150 cx.set_state(indoc! {"
17151 ˇfn func(abc def: i32) -> u32 {
17152 }
17153 "});
17154
17155 cx.update(|_, cx| {
17156 lsp_store.update(cx, |lsp_store, cx| {
17157 lsp_store
17158 .update_diagnostics(
17159 LanguageServerId(0),
17160 lsp::PublishDiagnosticsParams {
17161 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17162 version: None,
17163 diagnostics: vec![
17164 lsp::Diagnostic {
17165 range: lsp::Range::new(
17166 lsp::Position::new(0, 11),
17167 lsp::Position::new(0, 12),
17168 ),
17169 severity: Some(lsp::DiagnosticSeverity::ERROR),
17170 ..Default::default()
17171 },
17172 lsp::Diagnostic {
17173 range: lsp::Range::new(
17174 lsp::Position::new(0, 12),
17175 lsp::Position::new(0, 15),
17176 ),
17177 severity: Some(lsp::DiagnosticSeverity::ERROR),
17178 ..Default::default()
17179 },
17180 lsp::Diagnostic {
17181 range: lsp::Range::new(
17182 lsp::Position::new(0, 25),
17183 lsp::Position::new(0, 28),
17184 ),
17185 severity: Some(lsp::DiagnosticSeverity::ERROR),
17186 ..Default::default()
17187 },
17188 ],
17189 },
17190 None,
17191 DiagnosticSourceKind::Pushed,
17192 &[],
17193 cx,
17194 )
17195 .unwrap()
17196 });
17197 });
17198
17199 executor.run_until_parked();
17200
17201 cx.update_editor(|editor, window, cx| {
17202 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17203 });
17204
17205 cx.assert_editor_state(indoc! {"
17206 fn func(abc def: i32) -> ˇu32 {
17207 }
17208 "});
17209
17210 cx.update_editor(|editor, window, cx| {
17211 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17212 });
17213
17214 cx.assert_editor_state(indoc! {"
17215 fn func(abc ˇdef: i32) -> u32 {
17216 }
17217 "});
17218
17219 cx.update_editor(|editor, window, cx| {
17220 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17221 });
17222
17223 cx.assert_editor_state(indoc! {"
17224 fn func(abcˇ def: i32) -> u32 {
17225 }
17226 "});
17227
17228 cx.update_editor(|editor, window, cx| {
17229 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17230 });
17231
17232 cx.assert_editor_state(indoc! {"
17233 fn func(abc def: i32) -> ˇu32 {
17234 }
17235 "});
17236}
17237
17238#[gpui::test]
17239async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17240 init_test(cx, |_| {});
17241
17242 let mut cx = EditorTestContext::new(cx).await;
17243
17244 let diff_base = r#"
17245 use some::mod;
17246
17247 const A: u32 = 42;
17248
17249 fn main() {
17250 println!("hello");
17251
17252 println!("world");
17253 }
17254 "#
17255 .unindent();
17256
17257 // Edits are modified, removed, modified, added
17258 cx.set_state(
17259 &r#"
17260 use some::modified;
17261
17262 ˇ
17263 fn main() {
17264 println!("hello there");
17265
17266 println!("around the");
17267 println!("world");
17268 }
17269 "#
17270 .unindent(),
17271 );
17272
17273 cx.set_head_text(&diff_base);
17274 executor.run_until_parked();
17275
17276 cx.update_editor(|editor, window, cx| {
17277 //Wrap around the bottom of the buffer
17278 for _ in 0..3 {
17279 editor.go_to_next_hunk(&GoToHunk, window, cx);
17280 }
17281 });
17282
17283 cx.assert_editor_state(
17284 &r#"
17285 ˇuse some::modified;
17286
17287
17288 fn main() {
17289 println!("hello there");
17290
17291 println!("around the");
17292 println!("world");
17293 }
17294 "#
17295 .unindent(),
17296 );
17297
17298 cx.update_editor(|editor, window, cx| {
17299 //Wrap around the top of the buffer
17300 for _ in 0..2 {
17301 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17302 }
17303 });
17304
17305 cx.assert_editor_state(
17306 &r#"
17307 use some::modified;
17308
17309
17310 fn main() {
17311 ˇ println!("hello there");
17312
17313 println!("around the");
17314 println!("world");
17315 }
17316 "#
17317 .unindent(),
17318 );
17319
17320 cx.update_editor(|editor, window, cx| {
17321 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17322 });
17323
17324 cx.assert_editor_state(
17325 &r#"
17326 use some::modified;
17327
17328 ˇ
17329 fn main() {
17330 println!("hello there");
17331
17332 println!("around the");
17333 println!("world");
17334 }
17335 "#
17336 .unindent(),
17337 );
17338
17339 cx.update_editor(|editor, window, cx| {
17340 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17341 });
17342
17343 cx.assert_editor_state(
17344 &r#"
17345 ˇuse some::modified;
17346
17347
17348 fn main() {
17349 println!("hello there");
17350
17351 println!("around the");
17352 println!("world");
17353 }
17354 "#
17355 .unindent(),
17356 );
17357
17358 cx.update_editor(|editor, window, cx| {
17359 for _ in 0..2 {
17360 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17361 }
17362 });
17363
17364 cx.assert_editor_state(
17365 &r#"
17366 use some::modified;
17367
17368
17369 fn main() {
17370 ˇ println!("hello there");
17371
17372 println!("around the");
17373 println!("world");
17374 }
17375 "#
17376 .unindent(),
17377 );
17378
17379 cx.update_editor(|editor, window, cx| {
17380 editor.fold(&Fold, window, cx);
17381 });
17382
17383 cx.update_editor(|editor, window, cx| {
17384 editor.go_to_next_hunk(&GoToHunk, window, cx);
17385 });
17386
17387 cx.assert_editor_state(
17388 &r#"
17389 ˇuse some::modified;
17390
17391
17392 fn main() {
17393 println!("hello there");
17394
17395 println!("around the");
17396 println!("world");
17397 }
17398 "#
17399 .unindent(),
17400 );
17401}
17402
17403#[test]
17404fn test_split_words() {
17405 fn split(text: &str) -> Vec<&str> {
17406 split_words(text).collect()
17407 }
17408
17409 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17410 assert_eq!(split("hello_world"), &["hello_", "world"]);
17411 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17412 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17413 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17414 assert_eq!(split("helloworld"), &["helloworld"]);
17415
17416 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17417}
17418
17419#[test]
17420fn test_split_words_for_snippet_prefix() {
17421 fn split(text: &str) -> Vec<&str> {
17422 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17423 }
17424
17425 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17426 assert_eq!(split("hello_world"), &["hello_world"]);
17427 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17428 assert_eq!(split("Hello_World"), &["Hello_World"]);
17429 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17430 assert_eq!(split("helloworld"), &["helloworld"]);
17431 assert_eq!(
17432 split("this@is!@#$^many . symbols"),
17433 &[
17434 "symbols",
17435 " symbols",
17436 ". symbols",
17437 " . symbols",
17438 " . symbols",
17439 " . symbols",
17440 "many . symbols",
17441 "^many . symbols",
17442 "$^many . symbols",
17443 "#$^many . symbols",
17444 "@#$^many . symbols",
17445 "!@#$^many . symbols",
17446 "is!@#$^many . symbols",
17447 "@is!@#$^many . symbols",
17448 "this@is!@#$^many . symbols",
17449 ],
17450 );
17451 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17452}
17453
17454#[gpui::test]
17455async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17456 init_test(cx, |_| {});
17457
17458 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17459 let mut assert = |before, after| {
17460 let _state_context = cx.set_state(before);
17461 cx.run_until_parked();
17462 cx.update_editor(|editor, window, cx| {
17463 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17464 });
17465 cx.run_until_parked();
17466 cx.assert_editor_state(after);
17467 };
17468
17469 // Outside bracket jumps to outside of matching bracket
17470 assert("console.logˇ(var);", "console.log(var)ˇ;");
17471 assert("console.log(var)ˇ;", "console.logˇ(var);");
17472
17473 // Inside bracket jumps to inside of matching bracket
17474 assert("console.log(ˇvar);", "console.log(varˇ);");
17475 assert("console.log(varˇ);", "console.log(ˇvar);");
17476
17477 // When outside a bracket and inside, favor jumping to the inside bracket
17478 assert(
17479 "console.log('foo', [1, 2, 3]ˇ);",
17480 "console.log(ˇ'foo', [1, 2, 3]);",
17481 );
17482 assert(
17483 "console.log(ˇ'foo', [1, 2, 3]);",
17484 "console.log('foo', [1, 2, 3]ˇ);",
17485 );
17486
17487 // Bias forward if two options are equally likely
17488 assert(
17489 "let result = curried_fun()ˇ();",
17490 "let result = curried_fun()()ˇ;",
17491 );
17492
17493 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17494 assert(
17495 indoc! {"
17496 function test() {
17497 console.log('test')ˇ
17498 }"},
17499 indoc! {"
17500 function test() {
17501 console.logˇ('test')
17502 }"},
17503 );
17504}
17505
17506#[gpui::test]
17507async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17508 init_test(cx, |_| {});
17509
17510 let fs = FakeFs::new(cx.executor());
17511 fs.insert_tree(
17512 path!("/a"),
17513 json!({
17514 "main.rs": "fn main() { let a = 5; }",
17515 "other.rs": "// Test file",
17516 }),
17517 )
17518 .await;
17519 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17520
17521 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17522 language_registry.add(Arc::new(Language::new(
17523 LanguageConfig {
17524 name: "Rust".into(),
17525 matcher: LanguageMatcher {
17526 path_suffixes: vec!["rs".to_string()],
17527 ..Default::default()
17528 },
17529 brackets: BracketPairConfig {
17530 pairs: vec![BracketPair {
17531 start: "{".to_string(),
17532 end: "}".to_string(),
17533 close: true,
17534 surround: true,
17535 newline: true,
17536 }],
17537 disabled_scopes_by_bracket_ix: Vec::new(),
17538 },
17539 ..Default::default()
17540 },
17541 Some(tree_sitter_rust::LANGUAGE.into()),
17542 )));
17543 let mut fake_servers = language_registry.register_fake_lsp(
17544 "Rust",
17545 FakeLspAdapter {
17546 capabilities: lsp::ServerCapabilities {
17547 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17548 first_trigger_character: "{".to_string(),
17549 more_trigger_character: None,
17550 }),
17551 ..Default::default()
17552 },
17553 ..Default::default()
17554 },
17555 );
17556
17557 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17558
17559 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17560
17561 let worktree_id = workspace
17562 .update(cx, |workspace, _, cx| {
17563 workspace.project().update(cx, |project, cx| {
17564 project.worktrees(cx).next().unwrap().read(cx).id()
17565 })
17566 })
17567 .unwrap();
17568
17569 let buffer = project
17570 .update(cx, |project, cx| {
17571 project.open_local_buffer(path!("/a/main.rs"), cx)
17572 })
17573 .await
17574 .unwrap();
17575 let editor_handle = workspace
17576 .update(cx, |workspace, window, cx| {
17577 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17578 })
17579 .unwrap()
17580 .await
17581 .unwrap()
17582 .downcast::<Editor>()
17583 .unwrap();
17584
17585 cx.executor().start_waiting();
17586 let fake_server = fake_servers.next().await.unwrap();
17587
17588 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17589 |params, _| async move {
17590 assert_eq!(
17591 params.text_document_position.text_document.uri,
17592 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17593 );
17594 assert_eq!(
17595 params.text_document_position.position,
17596 lsp::Position::new(0, 21),
17597 );
17598
17599 Ok(Some(vec![lsp::TextEdit {
17600 new_text: "]".to_string(),
17601 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17602 }]))
17603 },
17604 );
17605
17606 editor_handle.update_in(cx, |editor, window, cx| {
17607 window.focus(&editor.focus_handle(cx));
17608 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17609 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17610 });
17611 editor.handle_input("{", window, cx);
17612 });
17613
17614 cx.executor().run_until_parked();
17615
17616 buffer.update(cx, |buffer, _| {
17617 assert_eq!(
17618 buffer.text(),
17619 "fn main() { let a = {5}; }",
17620 "No extra braces from on type formatting should appear in the buffer"
17621 )
17622 });
17623}
17624
17625#[gpui::test(iterations = 20, seeds(31))]
17626async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17627 init_test(cx, |_| {});
17628
17629 let mut cx = EditorLspTestContext::new_rust(
17630 lsp::ServerCapabilities {
17631 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17632 first_trigger_character: ".".to_string(),
17633 more_trigger_character: None,
17634 }),
17635 ..Default::default()
17636 },
17637 cx,
17638 )
17639 .await;
17640
17641 cx.update_buffer(|buffer, _| {
17642 // This causes autoindent to be async.
17643 buffer.set_sync_parse_timeout(Duration::ZERO)
17644 });
17645
17646 cx.set_state("fn c() {\n d()ˇ\n}\n");
17647 cx.simulate_keystroke("\n");
17648 cx.run_until_parked();
17649
17650 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17651 let mut request =
17652 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17653 let buffer_cloned = buffer_cloned.clone();
17654 async move {
17655 buffer_cloned.update(&mut cx, |buffer, _| {
17656 assert_eq!(
17657 buffer.text(),
17658 "fn c() {\n d()\n .\n}\n",
17659 "OnTypeFormatting should triggered after autoindent applied"
17660 )
17661 })?;
17662
17663 Ok(Some(vec![]))
17664 }
17665 });
17666
17667 cx.simulate_keystroke(".");
17668 cx.run_until_parked();
17669
17670 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17671 assert!(request.next().await.is_some());
17672 request.close();
17673 assert!(request.next().await.is_none());
17674}
17675
17676#[gpui::test]
17677async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17678 init_test(cx, |_| {});
17679
17680 let fs = FakeFs::new(cx.executor());
17681 fs.insert_tree(
17682 path!("/a"),
17683 json!({
17684 "main.rs": "fn main() { let a = 5; }",
17685 "other.rs": "// Test file",
17686 }),
17687 )
17688 .await;
17689
17690 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17691
17692 let server_restarts = Arc::new(AtomicUsize::new(0));
17693 let closure_restarts = Arc::clone(&server_restarts);
17694 let language_server_name = "test language server";
17695 let language_name: LanguageName = "Rust".into();
17696
17697 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17698 language_registry.add(Arc::new(Language::new(
17699 LanguageConfig {
17700 name: language_name.clone(),
17701 matcher: LanguageMatcher {
17702 path_suffixes: vec!["rs".to_string()],
17703 ..Default::default()
17704 },
17705 ..Default::default()
17706 },
17707 Some(tree_sitter_rust::LANGUAGE.into()),
17708 )));
17709 let mut fake_servers = language_registry.register_fake_lsp(
17710 "Rust",
17711 FakeLspAdapter {
17712 name: language_server_name,
17713 initialization_options: Some(json!({
17714 "testOptionValue": true
17715 })),
17716 initializer: Some(Box::new(move |fake_server| {
17717 let task_restarts = Arc::clone(&closure_restarts);
17718 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17719 task_restarts.fetch_add(1, atomic::Ordering::Release);
17720 futures::future::ready(Ok(()))
17721 });
17722 })),
17723 ..Default::default()
17724 },
17725 );
17726
17727 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17728 let _buffer = project
17729 .update(cx, |project, cx| {
17730 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17731 })
17732 .await
17733 .unwrap();
17734 let _fake_server = fake_servers.next().await.unwrap();
17735 update_test_language_settings(cx, |language_settings| {
17736 language_settings.languages.0.insert(
17737 language_name.clone().0,
17738 LanguageSettingsContent {
17739 tab_size: NonZeroU32::new(8),
17740 ..Default::default()
17741 },
17742 );
17743 });
17744 cx.executor().run_until_parked();
17745 assert_eq!(
17746 server_restarts.load(atomic::Ordering::Acquire),
17747 0,
17748 "Should not restart LSP server on an unrelated change"
17749 );
17750
17751 update_test_project_settings(cx, |project_settings| {
17752 project_settings.lsp.insert(
17753 "Some other server name".into(),
17754 LspSettings {
17755 binary: None,
17756 settings: None,
17757 initialization_options: Some(json!({
17758 "some other init value": false
17759 })),
17760 enable_lsp_tasks: false,
17761 fetch: None,
17762 },
17763 );
17764 });
17765 cx.executor().run_until_parked();
17766 assert_eq!(
17767 server_restarts.load(atomic::Ordering::Acquire),
17768 0,
17769 "Should not restart LSP server on an unrelated LSP settings change"
17770 );
17771
17772 update_test_project_settings(cx, |project_settings| {
17773 project_settings.lsp.insert(
17774 language_server_name.into(),
17775 LspSettings {
17776 binary: None,
17777 settings: None,
17778 initialization_options: Some(json!({
17779 "anotherInitValue": false
17780 })),
17781 enable_lsp_tasks: false,
17782 fetch: None,
17783 },
17784 );
17785 });
17786 cx.executor().run_until_parked();
17787 assert_eq!(
17788 server_restarts.load(atomic::Ordering::Acquire),
17789 1,
17790 "Should restart LSP server on a related LSP settings change"
17791 );
17792
17793 update_test_project_settings(cx, |project_settings| {
17794 project_settings.lsp.insert(
17795 language_server_name.into(),
17796 LspSettings {
17797 binary: None,
17798 settings: None,
17799 initialization_options: Some(json!({
17800 "anotherInitValue": false
17801 })),
17802 enable_lsp_tasks: false,
17803 fetch: None,
17804 },
17805 );
17806 });
17807 cx.executor().run_until_parked();
17808 assert_eq!(
17809 server_restarts.load(atomic::Ordering::Acquire),
17810 1,
17811 "Should not restart LSP server on a related LSP settings change that is the same"
17812 );
17813
17814 update_test_project_settings(cx, |project_settings| {
17815 project_settings.lsp.insert(
17816 language_server_name.into(),
17817 LspSettings {
17818 binary: None,
17819 settings: None,
17820 initialization_options: None,
17821 enable_lsp_tasks: false,
17822 fetch: None,
17823 },
17824 );
17825 });
17826 cx.executor().run_until_parked();
17827 assert_eq!(
17828 server_restarts.load(atomic::Ordering::Acquire),
17829 2,
17830 "Should restart LSP server on another related LSP settings change"
17831 );
17832}
17833
17834#[gpui::test]
17835async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17836 init_test(cx, |_| {});
17837
17838 let mut cx = EditorLspTestContext::new_rust(
17839 lsp::ServerCapabilities {
17840 completion_provider: Some(lsp::CompletionOptions {
17841 trigger_characters: Some(vec![".".to_string()]),
17842 resolve_provider: Some(true),
17843 ..Default::default()
17844 }),
17845 ..Default::default()
17846 },
17847 cx,
17848 )
17849 .await;
17850
17851 cx.set_state("fn main() { let a = 2ˇ; }");
17852 cx.simulate_keystroke(".");
17853 let completion_item = lsp::CompletionItem {
17854 label: "some".into(),
17855 kind: Some(lsp::CompletionItemKind::SNIPPET),
17856 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17857 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17858 kind: lsp::MarkupKind::Markdown,
17859 value: "```rust\nSome(2)\n```".to_string(),
17860 })),
17861 deprecated: Some(false),
17862 sort_text: Some("fffffff2".to_string()),
17863 filter_text: Some("some".to_string()),
17864 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17865 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17866 range: lsp::Range {
17867 start: lsp::Position {
17868 line: 0,
17869 character: 22,
17870 },
17871 end: lsp::Position {
17872 line: 0,
17873 character: 22,
17874 },
17875 },
17876 new_text: "Some(2)".to_string(),
17877 })),
17878 additional_text_edits: Some(vec![lsp::TextEdit {
17879 range: lsp::Range {
17880 start: lsp::Position {
17881 line: 0,
17882 character: 20,
17883 },
17884 end: lsp::Position {
17885 line: 0,
17886 character: 22,
17887 },
17888 },
17889 new_text: "".to_string(),
17890 }]),
17891 ..Default::default()
17892 };
17893
17894 let closure_completion_item = completion_item.clone();
17895 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17896 let task_completion_item = closure_completion_item.clone();
17897 async move {
17898 Ok(Some(lsp::CompletionResponse::Array(vec![
17899 task_completion_item,
17900 ])))
17901 }
17902 });
17903
17904 request.next().await;
17905
17906 cx.condition(|editor, _| editor.context_menu_visible())
17907 .await;
17908 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17909 editor
17910 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17911 .unwrap()
17912 });
17913 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17914
17915 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17916 let task_completion_item = completion_item.clone();
17917 async move { Ok(task_completion_item) }
17918 })
17919 .next()
17920 .await
17921 .unwrap();
17922 apply_additional_edits.await.unwrap();
17923 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17924}
17925
17926#[gpui::test]
17927async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17928 init_test(cx, |_| {});
17929
17930 let mut cx = EditorLspTestContext::new_rust(
17931 lsp::ServerCapabilities {
17932 completion_provider: Some(lsp::CompletionOptions {
17933 trigger_characters: Some(vec![".".to_string()]),
17934 resolve_provider: Some(true),
17935 ..Default::default()
17936 }),
17937 ..Default::default()
17938 },
17939 cx,
17940 )
17941 .await;
17942
17943 cx.set_state("fn main() { let a = 2ˇ; }");
17944 cx.simulate_keystroke(".");
17945
17946 let item1 = lsp::CompletionItem {
17947 label: "method id()".to_string(),
17948 filter_text: Some("id".to_string()),
17949 detail: None,
17950 documentation: None,
17951 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17952 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17953 new_text: ".id".to_string(),
17954 })),
17955 ..lsp::CompletionItem::default()
17956 };
17957
17958 let item2 = lsp::CompletionItem {
17959 label: "other".to_string(),
17960 filter_text: Some("other".to_string()),
17961 detail: None,
17962 documentation: None,
17963 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17964 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17965 new_text: ".other".to_string(),
17966 })),
17967 ..lsp::CompletionItem::default()
17968 };
17969
17970 let item1 = item1.clone();
17971 cx.set_request_handler::<lsp::request::Completion, _, _>({
17972 let item1 = item1.clone();
17973 move |_, _, _| {
17974 let item1 = item1.clone();
17975 let item2 = item2.clone();
17976 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17977 }
17978 })
17979 .next()
17980 .await;
17981
17982 cx.condition(|editor, _| editor.context_menu_visible())
17983 .await;
17984 cx.update_editor(|editor, _, _| {
17985 let context_menu = editor.context_menu.borrow_mut();
17986 let context_menu = context_menu
17987 .as_ref()
17988 .expect("Should have the context menu deployed");
17989 match context_menu {
17990 CodeContextMenu::Completions(completions_menu) => {
17991 let completions = completions_menu.completions.borrow_mut();
17992 assert_eq!(
17993 completions
17994 .iter()
17995 .map(|completion| &completion.label.text)
17996 .collect::<Vec<_>>(),
17997 vec!["method id()", "other"]
17998 )
17999 }
18000 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18001 }
18002 });
18003
18004 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18005 let item1 = item1.clone();
18006 move |_, item_to_resolve, _| {
18007 let item1 = item1.clone();
18008 async move {
18009 if item1 == item_to_resolve {
18010 Ok(lsp::CompletionItem {
18011 label: "method id()".to_string(),
18012 filter_text: Some("id".to_string()),
18013 detail: Some("Now resolved!".to_string()),
18014 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18015 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18016 range: lsp::Range::new(
18017 lsp::Position::new(0, 22),
18018 lsp::Position::new(0, 22),
18019 ),
18020 new_text: ".id".to_string(),
18021 })),
18022 ..lsp::CompletionItem::default()
18023 })
18024 } else {
18025 Ok(item_to_resolve)
18026 }
18027 }
18028 }
18029 })
18030 .next()
18031 .await
18032 .unwrap();
18033 cx.run_until_parked();
18034
18035 cx.update_editor(|editor, window, cx| {
18036 editor.context_menu_next(&Default::default(), window, cx);
18037 });
18038
18039 cx.update_editor(|editor, _, _| {
18040 let context_menu = editor.context_menu.borrow_mut();
18041 let context_menu = context_menu
18042 .as_ref()
18043 .expect("Should have the context menu deployed");
18044 match context_menu {
18045 CodeContextMenu::Completions(completions_menu) => {
18046 let completions = completions_menu.completions.borrow_mut();
18047 assert_eq!(
18048 completions
18049 .iter()
18050 .map(|completion| &completion.label.text)
18051 .collect::<Vec<_>>(),
18052 vec!["method id() Now resolved!", "other"],
18053 "Should update first completion label, but not second as the filter text did not match."
18054 );
18055 }
18056 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18057 }
18058 });
18059}
18060
18061#[gpui::test]
18062async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18063 init_test(cx, |_| {});
18064 let mut cx = EditorLspTestContext::new_rust(
18065 lsp::ServerCapabilities {
18066 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18067 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18068 completion_provider: Some(lsp::CompletionOptions {
18069 resolve_provider: Some(true),
18070 ..Default::default()
18071 }),
18072 ..Default::default()
18073 },
18074 cx,
18075 )
18076 .await;
18077 cx.set_state(indoc! {"
18078 struct TestStruct {
18079 field: i32
18080 }
18081
18082 fn mainˇ() {
18083 let unused_var = 42;
18084 let test_struct = TestStruct { field: 42 };
18085 }
18086 "});
18087 let symbol_range = cx.lsp_range(indoc! {"
18088 struct TestStruct {
18089 field: i32
18090 }
18091
18092 «fn main»() {
18093 let unused_var = 42;
18094 let test_struct = TestStruct { field: 42 };
18095 }
18096 "});
18097 let mut hover_requests =
18098 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18099 Ok(Some(lsp::Hover {
18100 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18101 kind: lsp::MarkupKind::Markdown,
18102 value: "Function documentation".to_string(),
18103 }),
18104 range: Some(symbol_range),
18105 }))
18106 });
18107
18108 // Case 1: Test that code action menu hide hover popover
18109 cx.dispatch_action(Hover);
18110 hover_requests.next().await;
18111 cx.condition(|editor, _| editor.hover_state.visible()).await;
18112 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18113 move |_, _, _| async move {
18114 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18115 lsp::CodeAction {
18116 title: "Remove unused variable".to_string(),
18117 kind: Some(CodeActionKind::QUICKFIX),
18118 edit: Some(lsp::WorkspaceEdit {
18119 changes: Some(
18120 [(
18121 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18122 vec![lsp::TextEdit {
18123 range: lsp::Range::new(
18124 lsp::Position::new(5, 4),
18125 lsp::Position::new(5, 27),
18126 ),
18127 new_text: "".to_string(),
18128 }],
18129 )]
18130 .into_iter()
18131 .collect(),
18132 ),
18133 ..Default::default()
18134 }),
18135 ..Default::default()
18136 },
18137 )]))
18138 },
18139 );
18140 cx.update_editor(|editor, window, cx| {
18141 editor.toggle_code_actions(
18142 &ToggleCodeActions {
18143 deployed_from: None,
18144 quick_launch: false,
18145 },
18146 window,
18147 cx,
18148 );
18149 });
18150 code_action_requests.next().await;
18151 cx.run_until_parked();
18152 cx.condition(|editor, _| editor.context_menu_visible())
18153 .await;
18154 cx.update_editor(|editor, _, _| {
18155 assert!(
18156 !editor.hover_state.visible(),
18157 "Hover popover should be hidden when code action menu is shown"
18158 );
18159 // Hide code actions
18160 editor.context_menu.take();
18161 });
18162
18163 // Case 2: Test that code completions hide hover popover
18164 cx.dispatch_action(Hover);
18165 hover_requests.next().await;
18166 cx.condition(|editor, _| editor.hover_state.visible()).await;
18167 let counter = Arc::new(AtomicUsize::new(0));
18168 let mut completion_requests =
18169 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18170 let counter = counter.clone();
18171 async move {
18172 counter.fetch_add(1, atomic::Ordering::Release);
18173 Ok(Some(lsp::CompletionResponse::Array(vec![
18174 lsp::CompletionItem {
18175 label: "main".into(),
18176 kind: Some(lsp::CompletionItemKind::FUNCTION),
18177 detail: Some("() -> ()".to_string()),
18178 ..Default::default()
18179 },
18180 lsp::CompletionItem {
18181 label: "TestStruct".into(),
18182 kind: Some(lsp::CompletionItemKind::STRUCT),
18183 detail: Some("struct TestStruct".to_string()),
18184 ..Default::default()
18185 },
18186 ])))
18187 }
18188 });
18189 cx.update_editor(|editor, window, cx| {
18190 editor.show_completions(&ShowCompletions, window, cx);
18191 });
18192 completion_requests.next().await;
18193 cx.condition(|editor, _| editor.context_menu_visible())
18194 .await;
18195 cx.update_editor(|editor, _, _| {
18196 assert!(
18197 !editor.hover_state.visible(),
18198 "Hover popover should be hidden when completion menu is shown"
18199 );
18200 });
18201}
18202
18203#[gpui::test]
18204async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18205 init_test(cx, |_| {});
18206
18207 let mut cx = EditorLspTestContext::new_rust(
18208 lsp::ServerCapabilities {
18209 completion_provider: Some(lsp::CompletionOptions {
18210 trigger_characters: Some(vec![".".to_string()]),
18211 resolve_provider: Some(true),
18212 ..Default::default()
18213 }),
18214 ..Default::default()
18215 },
18216 cx,
18217 )
18218 .await;
18219
18220 cx.set_state("fn main() { let a = 2ˇ; }");
18221 cx.simulate_keystroke(".");
18222
18223 let unresolved_item_1 = lsp::CompletionItem {
18224 label: "id".to_string(),
18225 filter_text: Some("id".to_string()),
18226 detail: None,
18227 documentation: None,
18228 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18229 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18230 new_text: ".id".to_string(),
18231 })),
18232 ..lsp::CompletionItem::default()
18233 };
18234 let resolved_item_1 = lsp::CompletionItem {
18235 additional_text_edits: Some(vec![lsp::TextEdit {
18236 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18237 new_text: "!!".to_string(),
18238 }]),
18239 ..unresolved_item_1.clone()
18240 };
18241 let unresolved_item_2 = lsp::CompletionItem {
18242 label: "other".to_string(),
18243 filter_text: Some("other".to_string()),
18244 detail: None,
18245 documentation: None,
18246 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18247 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18248 new_text: ".other".to_string(),
18249 })),
18250 ..lsp::CompletionItem::default()
18251 };
18252 let resolved_item_2 = lsp::CompletionItem {
18253 additional_text_edits: Some(vec![lsp::TextEdit {
18254 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18255 new_text: "??".to_string(),
18256 }]),
18257 ..unresolved_item_2.clone()
18258 };
18259
18260 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18261 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18262 cx.lsp
18263 .server
18264 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18265 let unresolved_item_1 = unresolved_item_1.clone();
18266 let resolved_item_1 = resolved_item_1.clone();
18267 let unresolved_item_2 = unresolved_item_2.clone();
18268 let resolved_item_2 = resolved_item_2.clone();
18269 let resolve_requests_1 = resolve_requests_1.clone();
18270 let resolve_requests_2 = resolve_requests_2.clone();
18271 move |unresolved_request, _| {
18272 let unresolved_item_1 = unresolved_item_1.clone();
18273 let resolved_item_1 = resolved_item_1.clone();
18274 let unresolved_item_2 = unresolved_item_2.clone();
18275 let resolved_item_2 = resolved_item_2.clone();
18276 let resolve_requests_1 = resolve_requests_1.clone();
18277 let resolve_requests_2 = resolve_requests_2.clone();
18278 async move {
18279 if unresolved_request == unresolved_item_1 {
18280 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18281 Ok(resolved_item_1.clone())
18282 } else if unresolved_request == unresolved_item_2 {
18283 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18284 Ok(resolved_item_2.clone())
18285 } else {
18286 panic!("Unexpected completion item {unresolved_request:?}")
18287 }
18288 }
18289 }
18290 })
18291 .detach();
18292
18293 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18294 let unresolved_item_1 = unresolved_item_1.clone();
18295 let unresolved_item_2 = unresolved_item_2.clone();
18296 async move {
18297 Ok(Some(lsp::CompletionResponse::Array(vec![
18298 unresolved_item_1,
18299 unresolved_item_2,
18300 ])))
18301 }
18302 })
18303 .next()
18304 .await;
18305
18306 cx.condition(|editor, _| editor.context_menu_visible())
18307 .await;
18308 cx.update_editor(|editor, _, _| {
18309 let context_menu = editor.context_menu.borrow_mut();
18310 let context_menu = context_menu
18311 .as_ref()
18312 .expect("Should have the context menu deployed");
18313 match context_menu {
18314 CodeContextMenu::Completions(completions_menu) => {
18315 let completions = completions_menu.completions.borrow_mut();
18316 assert_eq!(
18317 completions
18318 .iter()
18319 .map(|completion| &completion.label.text)
18320 .collect::<Vec<_>>(),
18321 vec!["id", "other"]
18322 )
18323 }
18324 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18325 }
18326 });
18327 cx.run_until_parked();
18328
18329 cx.update_editor(|editor, window, cx| {
18330 editor.context_menu_next(&ContextMenuNext, window, cx);
18331 });
18332 cx.run_until_parked();
18333 cx.update_editor(|editor, window, cx| {
18334 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18335 });
18336 cx.run_until_parked();
18337 cx.update_editor(|editor, window, cx| {
18338 editor.context_menu_next(&ContextMenuNext, window, cx);
18339 });
18340 cx.run_until_parked();
18341 cx.update_editor(|editor, window, cx| {
18342 editor
18343 .compose_completion(&ComposeCompletion::default(), window, cx)
18344 .expect("No task returned")
18345 })
18346 .await
18347 .expect("Completion failed");
18348 cx.run_until_parked();
18349
18350 cx.update_editor(|editor, _, cx| {
18351 assert_eq!(
18352 resolve_requests_1.load(atomic::Ordering::Acquire),
18353 1,
18354 "Should always resolve once despite multiple selections"
18355 );
18356 assert_eq!(
18357 resolve_requests_2.load(atomic::Ordering::Acquire),
18358 1,
18359 "Should always resolve once after multiple selections and applying the completion"
18360 );
18361 assert_eq!(
18362 editor.text(cx),
18363 "fn main() { let a = ??.other; }",
18364 "Should use resolved data when applying the completion"
18365 );
18366 });
18367}
18368
18369#[gpui::test]
18370async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18371 init_test(cx, |_| {});
18372
18373 let item_0 = lsp::CompletionItem {
18374 label: "abs".into(),
18375 insert_text: Some("abs".into()),
18376 data: Some(json!({ "very": "special"})),
18377 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18378 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18379 lsp::InsertReplaceEdit {
18380 new_text: "abs".to_string(),
18381 insert: lsp::Range::default(),
18382 replace: lsp::Range::default(),
18383 },
18384 )),
18385 ..lsp::CompletionItem::default()
18386 };
18387 let items = iter::once(item_0.clone())
18388 .chain((11..51).map(|i| lsp::CompletionItem {
18389 label: format!("item_{}", i),
18390 insert_text: Some(format!("item_{}", i)),
18391 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18392 ..lsp::CompletionItem::default()
18393 }))
18394 .collect::<Vec<_>>();
18395
18396 let default_commit_characters = vec!["?".to_string()];
18397 let default_data = json!({ "default": "data"});
18398 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18399 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18400 let default_edit_range = lsp::Range {
18401 start: lsp::Position {
18402 line: 0,
18403 character: 5,
18404 },
18405 end: lsp::Position {
18406 line: 0,
18407 character: 5,
18408 },
18409 };
18410
18411 let mut cx = EditorLspTestContext::new_rust(
18412 lsp::ServerCapabilities {
18413 completion_provider: Some(lsp::CompletionOptions {
18414 trigger_characters: Some(vec![".".to_string()]),
18415 resolve_provider: Some(true),
18416 ..Default::default()
18417 }),
18418 ..Default::default()
18419 },
18420 cx,
18421 )
18422 .await;
18423
18424 cx.set_state("fn main() { let a = 2ˇ; }");
18425 cx.simulate_keystroke(".");
18426
18427 let completion_data = default_data.clone();
18428 let completion_characters = default_commit_characters.clone();
18429 let completion_items = items.clone();
18430 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18431 let default_data = completion_data.clone();
18432 let default_commit_characters = completion_characters.clone();
18433 let items = completion_items.clone();
18434 async move {
18435 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18436 items,
18437 item_defaults: Some(lsp::CompletionListItemDefaults {
18438 data: Some(default_data.clone()),
18439 commit_characters: Some(default_commit_characters.clone()),
18440 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18441 default_edit_range,
18442 )),
18443 insert_text_format: Some(default_insert_text_format),
18444 insert_text_mode: Some(default_insert_text_mode),
18445 }),
18446 ..lsp::CompletionList::default()
18447 })))
18448 }
18449 })
18450 .next()
18451 .await;
18452
18453 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18454 cx.lsp
18455 .server
18456 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18457 let closure_resolved_items = resolved_items.clone();
18458 move |item_to_resolve, _| {
18459 let closure_resolved_items = closure_resolved_items.clone();
18460 async move {
18461 closure_resolved_items.lock().push(item_to_resolve.clone());
18462 Ok(item_to_resolve)
18463 }
18464 }
18465 })
18466 .detach();
18467
18468 cx.condition(|editor, _| editor.context_menu_visible())
18469 .await;
18470 cx.run_until_parked();
18471 cx.update_editor(|editor, _, _| {
18472 let menu = editor.context_menu.borrow_mut();
18473 match menu.as_ref().expect("should have the completions menu") {
18474 CodeContextMenu::Completions(completions_menu) => {
18475 assert_eq!(
18476 completions_menu
18477 .entries
18478 .borrow()
18479 .iter()
18480 .map(|mat| mat.string.clone())
18481 .collect::<Vec<String>>(),
18482 items
18483 .iter()
18484 .map(|completion| completion.label.clone())
18485 .collect::<Vec<String>>()
18486 );
18487 }
18488 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18489 }
18490 });
18491 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18492 // with 4 from the end.
18493 assert_eq!(
18494 *resolved_items.lock(),
18495 [&items[0..16], &items[items.len() - 4..items.len()]]
18496 .concat()
18497 .iter()
18498 .cloned()
18499 .map(|mut item| {
18500 if item.data.is_none() {
18501 item.data = Some(default_data.clone());
18502 }
18503 item
18504 })
18505 .collect::<Vec<lsp::CompletionItem>>(),
18506 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18507 );
18508 resolved_items.lock().clear();
18509
18510 cx.update_editor(|editor, window, cx| {
18511 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18512 });
18513 cx.run_until_parked();
18514 // Completions that have already been resolved are skipped.
18515 assert_eq!(
18516 *resolved_items.lock(),
18517 items[items.len() - 17..items.len() - 4]
18518 .iter()
18519 .cloned()
18520 .map(|mut item| {
18521 if item.data.is_none() {
18522 item.data = Some(default_data.clone());
18523 }
18524 item
18525 })
18526 .collect::<Vec<lsp::CompletionItem>>()
18527 );
18528 resolved_items.lock().clear();
18529}
18530
18531#[gpui::test]
18532async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18533 init_test(cx, |_| {});
18534
18535 let mut cx = EditorLspTestContext::new(
18536 Language::new(
18537 LanguageConfig {
18538 matcher: LanguageMatcher {
18539 path_suffixes: vec!["jsx".into()],
18540 ..Default::default()
18541 },
18542 overrides: [(
18543 "element".into(),
18544 LanguageConfigOverride {
18545 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18546 ..Default::default()
18547 },
18548 )]
18549 .into_iter()
18550 .collect(),
18551 ..Default::default()
18552 },
18553 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18554 )
18555 .with_override_query("(jsx_self_closing_element) @element")
18556 .unwrap(),
18557 lsp::ServerCapabilities {
18558 completion_provider: Some(lsp::CompletionOptions {
18559 trigger_characters: Some(vec![":".to_string()]),
18560 ..Default::default()
18561 }),
18562 ..Default::default()
18563 },
18564 cx,
18565 )
18566 .await;
18567
18568 cx.lsp
18569 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18570 Ok(Some(lsp::CompletionResponse::Array(vec![
18571 lsp::CompletionItem {
18572 label: "bg-blue".into(),
18573 ..Default::default()
18574 },
18575 lsp::CompletionItem {
18576 label: "bg-red".into(),
18577 ..Default::default()
18578 },
18579 lsp::CompletionItem {
18580 label: "bg-yellow".into(),
18581 ..Default::default()
18582 },
18583 ])))
18584 });
18585
18586 cx.set_state(r#"<p class="bgˇ" />"#);
18587
18588 // Trigger completion when typing a dash, because the dash is an extra
18589 // word character in the 'element' scope, which contains the cursor.
18590 cx.simulate_keystroke("-");
18591 cx.executor().run_until_parked();
18592 cx.update_editor(|editor, _, _| {
18593 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18594 {
18595 assert_eq!(
18596 completion_menu_entries(menu),
18597 &["bg-blue", "bg-red", "bg-yellow"]
18598 );
18599 } else {
18600 panic!("expected completion menu to be open");
18601 }
18602 });
18603
18604 cx.simulate_keystroke("l");
18605 cx.executor().run_until_parked();
18606 cx.update_editor(|editor, _, _| {
18607 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18608 {
18609 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18610 } else {
18611 panic!("expected completion menu to be open");
18612 }
18613 });
18614
18615 // When filtering completions, consider the character after the '-' to
18616 // be the start of a subword.
18617 cx.set_state(r#"<p class="yelˇ" />"#);
18618 cx.simulate_keystroke("l");
18619 cx.executor().run_until_parked();
18620 cx.update_editor(|editor, _, _| {
18621 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18622 {
18623 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18624 } else {
18625 panic!("expected completion menu to be open");
18626 }
18627 });
18628}
18629
18630fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18631 let entries = menu.entries.borrow();
18632 entries.iter().map(|mat| mat.string.clone()).collect()
18633}
18634
18635#[gpui::test]
18636async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18637 init_test(cx, |settings| {
18638 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18639 });
18640
18641 let fs = FakeFs::new(cx.executor());
18642 fs.insert_file(path!("/file.ts"), Default::default()).await;
18643
18644 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18645 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18646
18647 language_registry.add(Arc::new(Language::new(
18648 LanguageConfig {
18649 name: "TypeScript".into(),
18650 matcher: LanguageMatcher {
18651 path_suffixes: vec!["ts".to_string()],
18652 ..Default::default()
18653 },
18654 ..Default::default()
18655 },
18656 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18657 )));
18658 update_test_language_settings(cx, |settings| {
18659 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18660 });
18661
18662 let test_plugin = "test_plugin";
18663 let _ = language_registry.register_fake_lsp(
18664 "TypeScript",
18665 FakeLspAdapter {
18666 prettier_plugins: vec![test_plugin],
18667 ..Default::default()
18668 },
18669 );
18670
18671 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18672 let buffer = project
18673 .update(cx, |project, cx| {
18674 project.open_local_buffer(path!("/file.ts"), cx)
18675 })
18676 .await
18677 .unwrap();
18678
18679 let buffer_text = "one\ntwo\nthree\n";
18680 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18681 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18682 editor.update_in(cx, |editor, window, cx| {
18683 editor.set_text(buffer_text, window, cx)
18684 });
18685
18686 editor
18687 .update_in(cx, |editor, window, cx| {
18688 editor.perform_format(
18689 project.clone(),
18690 FormatTrigger::Manual,
18691 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18692 window,
18693 cx,
18694 )
18695 })
18696 .unwrap()
18697 .await;
18698 assert_eq!(
18699 editor.update(cx, |editor, cx| editor.text(cx)),
18700 buffer_text.to_string() + prettier_format_suffix,
18701 "Test prettier formatting was not applied to the original buffer text",
18702 );
18703
18704 update_test_language_settings(cx, |settings| {
18705 settings.defaults.formatter = Some(FormatterList::default())
18706 });
18707 let format = editor.update_in(cx, |editor, window, cx| {
18708 editor.perform_format(
18709 project.clone(),
18710 FormatTrigger::Manual,
18711 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18712 window,
18713 cx,
18714 )
18715 });
18716 format.await.unwrap();
18717 assert_eq!(
18718 editor.update(cx, |editor, cx| editor.text(cx)),
18719 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18720 "Autoformatting (via test prettier) was not applied to the original buffer text",
18721 );
18722}
18723
18724#[gpui::test]
18725async fn test_addition_reverts(cx: &mut TestAppContext) {
18726 init_test(cx, |_| {});
18727 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18728 let base_text = indoc! {r#"
18729 struct Row;
18730 struct Row1;
18731 struct Row2;
18732
18733 struct Row4;
18734 struct Row5;
18735 struct Row6;
18736
18737 struct Row8;
18738 struct Row9;
18739 struct Row10;"#};
18740
18741 // When addition hunks are not adjacent to carets, no hunk revert is performed
18742 assert_hunk_revert(
18743 indoc! {r#"struct Row;
18744 struct Row1;
18745 struct Row1.1;
18746 struct Row1.2;
18747 struct Row2;ˇ
18748
18749 struct Row4;
18750 struct Row5;
18751 struct Row6;
18752
18753 struct Row8;
18754 ˇstruct Row9;
18755 struct Row9.1;
18756 struct Row9.2;
18757 struct Row9.3;
18758 struct Row10;"#},
18759 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18760 indoc! {r#"struct Row;
18761 struct Row1;
18762 struct Row1.1;
18763 struct Row1.2;
18764 struct Row2;ˇ
18765
18766 struct Row4;
18767 struct Row5;
18768 struct Row6;
18769
18770 struct Row8;
18771 ˇstruct Row9;
18772 struct Row9.1;
18773 struct Row9.2;
18774 struct Row9.3;
18775 struct Row10;"#},
18776 base_text,
18777 &mut cx,
18778 );
18779 // Same for selections
18780 assert_hunk_revert(
18781 indoc! {r#"struct Row;
18782 struct Row1;
18783 struct Row2;
18784 struct Row2.1;
18785 struct Row2.2;
18786 «ˇ
18787 struct Row4;
18788 struct» Row5;
18789 «struct Row6;
18790 ˇ»
18791 struct Row9.1;
18792 struct Row9.2;
18793 struct Row9.3;
18794 struct Row8;
18795 struct Row9;
18796 struct Row10;"#},
18797 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18798 indoc! {r#"struct Row;
18799 struct Row1;
18800 struct Row2;
18801 struct Row2.1;
18802 struct Row2.2;
18803 «ˇ
18804 struct Row4;
18805 struct» Row5;
18806 «struct Row6;
18807 ˇ»
18808 struct Row9.1;
18809 struct Row9.2;
18810 struct Row9.3;
18811 struct Row8;
18812 struct Row9;
18813 struct Row10;"#},
18814 base_text,
18815 &mut cx,
18816 );
18817
18818 // When carets and selections intersect the addition hunks, those are reverted.
18819 // Adjacent carets got merged.
18820 assert_hunk_revert(
18821 indoc! {r#"struct Row;
18822 ˇ// something on the top
18823 struct Row1;
18824 struct Row2;
18825 struct Roˇw3.1;
18826 struct Row2.2;
18827 struct Row2.3;ˇ
18828
18829 struct Row4;
18830 struct ˇRow5.1;
18831 struct Row5.2;
18832 struct «Rowˇ»5.3;
18833 struct Row5;
18834 struct Row6;
18835 ˇ
18836 struct Row9.1;
18837 struct «Rowˇ»9.2;
18838 struct «ˇRow»9.3;
18839 struct Row8;
18840 struct Row9;
18841 «ˇ// something on bottom»
18842 struct Row10;"#},
18843 vec![
18844 DiffHunkStatusKind::Added,
18845 DiffHunkStatusKind::Added,
18846 DiffHunkStatusKind::Added,
18847 DiffHunkStatusKind::Added,
18848 DiffHunkStatusKind::Added,
18849 ],
18850 indoc! {r#"struct Row;
18851 ˇstruct Row1;
18852 struct Row2;
18853 ˇ
18854 struct Row4;
18855 ˇstruct Row5;
18856 struct Row6;
18857 ˇ
18858 ˇstruct Row8;
18859 struct Row9;
18860 ˇstruct Row10;"#},
18861 base_text,
18862 &mut cx,
18863 );
18864}
18865
18866#[gpui::test]
18867async fn test_modification_reverts(cx: &mut TestAppContext) {
18868 init_test(cx, |_| {});
18869 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18870 let base_text = indoc! {r#"
18871 struct Row;
18872 struct Row1;
18873 struct Row2;
18874
18875 struct Row4;
18876 struct Row5;
18877 struct Row6;
18878
18879 struct Row8;
18880 struct Row9;
18881 struct Row10;"#};
18882
18883 // Modification hunks behave the same as the addition ones.
18884 assert_hunk_revert(
18885 indoc! {r#"struct Row;
18886 struct Row1;
18887 struct Row33;
18888 ˇ
18889 struct Row4;
18890 struct Row5;
18891 struct Row6;
18892 ˇ
18893 struct Row99;
18894 struct Row9;
18895 struct Row10;"#},
18896 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18897 indoc! {r#"struct Row;
18898 struct Row1;
18899 struct Row33;
18900 ˇ
18901 struct Row4;
18902 struct Row5;
18903 struct Row6;
18904 ˇ
18905 struct Row99;
18906 struct Row9;
18907 struct Row10;"#},
18908 base_text,
18909 &mut cx,
18910 );
18911 assert_hunk_revert(
18912 indoc! {r#"struct Row;
18913 struct Row1;
18914 struct Row33;
18915 «ˇ
18916 struct Row4;
18917 struct» Row5;
18918 «struct Row6;
18919 ˇ»
18920 struct Row99;
18921 struct Row9;
18922 struct Row10;"#},
18923 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18924 indoc! {r#"struct Row;
18925 struct Row1;
18926 struct Row33;
18927 «ˇ
18928 struct Row4;
18929 struct» Row5;
18930 «struct Row6;
18931 ˇ»
18932 struct Row99;
18933 struct Row9;
18934 struct Row10;"#},
18935 base_text,
18936 &mut cx,
18937 );
18938
18939 assert_hunk_revert(
18940 indoc! {r#"ˇstruct Row1.1;
18941 struct Row1;
18942 «ˇstr»uct Row22;
18943
18944 struct ˇRow44;
18945 struct Row5;
18946 struct «Rˇ»ow66;ˇ
18947
18948 «struˇ»ct Row88;
18949 struct Row9;
18950 struct Row1011;ˇ"#},
18951 vec![
18952 DiffHunkStatusKind::Modified,
18953 DiffHunkStatusKind::Modified,
18954 DiffHunkStatusKind::Modified,
18955 DiffHunkStatusKind::Modified,
18956 DiffHunkStatusKind::Modified,
18957 DiffHunkStatusKind::Modified,
18958 ],
18959 indoc! {r#"struct Row;
18960 ˇstruct Row1;
18961 struct Row2;
18962 ˇ
18963 struct Row4;
18964 ˇstruct Row5;
18965 struct Row6;
18966 ˇ
18967 struct Row8;
18968 ˇstruct Row9;
18969 struct Row10;ˇ"#},
18970 base_text,
18971 &mut cx,
18972 );
18973}
18974
18975#[gpui::test]
18976async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18977 init_test(cx, |_| {});
18978 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18979 let base_text = indoc! {r#"
18980 one
18981
18982 two
18983 three
18984 "#};
18985
18986 cx.set_head_text(base_text);
18987 cx.set_state("\nˇ\n");
18988 cx.executor().run_until_parked();
18989 cx.update_editor(|editor, _window, cx| {
18990 editor.expand_selected_diff_hunks(cx);
18991 });
18992 cx.executor().run_until_parked();
18993 cx.update_editor(|editor, window, cx| {
18994 editor.backspace(&Default::default(), window, cx);
18995 });
18996 cx.run_until_parked();
18997 cx.assert_state_with_diff(
18998 indoc! {r#"
18999
19000 - two
19001 - threeˇ
19002 +
19003 "#}
19004 .to_string(),
19005 );
19006}
19007
19008#[gpui::test]
19009async fn test_deletion_reverts(cx: &mut TestAppContext) {
19010 init_test(cx, |_| {});
19011 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19012 let base_text = indoc! {r#"struct Row;
19013struct Row1;
19014struct Row2;
19015
19016struct Row4;
19017struct Row5;
19018struct Row6;
19019
19020struct Row8;
19021struct Row9;
19022struct Row10;"#};
19023
19024 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19025 assert_hunk_revert(
19026 indoc! {r#"struct Row;
19027 struct Row2;
19028
19029 ˇstruct Row4;
19030 struct Row5;
19031 struct Row6;
19032 ˇ
19033 struct Row8;
19034 struct Row10;"#},
19035 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19036 indoc! {r#"struct Row;
19037 struct Row2;
19038
19039 ˇstruct Row4;
19040 struct Row5;
19041 struct Row6;
19042 ˇ
19043 struct Row8;
19044 struct Row10;"#},
19045 base_text,
19046 &mut cx,
19047 );
19048 assert_hunk_revert(
19049 indoc! {r#"struct Row;
19050 struct Row2;
19051
19052 «ˇstruct Row4;
19053 struct» Row5;
19054 «struct Row6;
19055 ˇ»
19056 struct Row8;
19057 struct Row10;"#},
19058 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19059 indoc! {r#"struct Row;
19060 struct Row2;
19061
19062 «ˇstruct Row4;
19063 struct» Row5;
19064 «struct Row6;
19065 ˇ»
19066 struct Row8;
19067 struct Row10;"#},
19068 base_text,
19069 &mut cx,
19070 );
19071
19072 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19073 assert_hunk_revert(
19074 indoc! {r#"struct Row;
19075 ˇstruct Row2;
19076
19077 struct Row4;
19078 struct Row5;
19079 struct Row6;
19080
19081 struct Row8;ˇ
19082 struct Row10;"#},
19083 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19084 indoc! {r#"struct Row;
19085 struct Row1;
19086 ˇstruct Row2;
19087
19088 struct Row4;
19089 struct Row5;
19090 struct Row6;
19091
19092 struct Row8;ˇ
19093 struct Row9;
19094 struct Row10;"#},
19095 base_text,
19096 &mut cx,
19097 );
19098 assert_hunk_revert(
19099 indoc! {r#"struct Row;
19100 struct Row2«ˇ;
19101 struct Row4;
19102 struct» Row5;
19103 «struct Row6;
19104
19105 struct Row8;ˇ»
19106 struct Row10;"#},
19107 vec![
19108 DiffHunkStatusKind::Deleted,
19109 DiffHunkStatusKind::Deleted,
19110 DiffHunkStatusKind::Deleted,
19111 ],
19112 indoc! {r#"struct Row;
19113 struct Row1;
19114 struct Row2«ˇ;
19115
19116 struct Row4;
19117 struct» Row5;
19118 «struct Row6;
19119
19120 struct Row8;ˇ»
19121 struct Row9;
19122 struct Row10;"#},
19123 base_text,
19124 &mut cx,
19125 );
19126}
19127
19128#[gpui::test]
19129async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19130 init_test(cx, |_| {});
19131
19132 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19133 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19134 let base_text_3 =
19135 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19136
19137 let text_1 = edit_first_char_of_every_line(base_text_1);
19138 let text_2 = edit_first_char_of_every_line(base_text_2);
19139 let text_3 = edit_first_char_of_every_line(base_text_3);
19140
19141 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19142 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19143 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19144
19145 let multibuffer = cx.new(|cx| {
19146 let mut multibuffer = MultiBuffer::new(ReadWrite);
19147 multibuffer.push_excerpts(
19148 buffer_1.clone(),
19149 [
19150 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19151 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19152 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19153 ],
19154 cx,
19155 );
19156 multibuffer.push_excerpts(
19157 buffer_2.clone(),
19158 [
19159 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19160 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19161 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19162 ],
19163 cx,
19164 );
19165 multibuffer.push_excerpts(
19166 buffer_3.clone(),
19167 [
19168 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19169 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19170 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19171 ],
19172 cx,
19173 );
19174 multibuffer
19175 });
19176
19177 let fs = FakeFs::new(cx.executor());
19178 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19179 let (editor, cx) = cx
19180 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19181 editor.update_in(cx, |editor, _window, cx| {
19182 for (buffer, diff_base) in [
19183 (buffer_1.clone(), base_text_1),
19184 (buffer_2.clone(), base_text_2),
19185 (buffer_3.clone(), base_text_3),
19186 ] {
19187 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19188 editor
19189 .buffer
19190 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19191 }
19192 });
19193 cx.executor().run_until_parked();
19194
19195 editor.update_in(cx, |editor, window, cx| {
19196 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}");
19197 editor.select_all(&SelectAll, window, cx);
19198 editor.git_restore(&Default::default(), window, cx);
19199 });
19200 cx.executor().run_until_parked();
19201
19202 // When all ranges are selected, all buffer hunks are reverted.
19203 editor.update(cx, |editor, cx| {
19204 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");
19205 });
19206 buffer_1.update(cx, |buffer, _| {
19207 assert_eq!(buffer.text(), base_text_1);
19208 });
19209 buffer_2.update(cx, |buffer, _| {
19210 assert_eq!(buffer.text(), base_text_2);
19211 });
19212 buffer_3.update(cx, |buffer, _| {
19213 assert_eq!(buffer.text(), base_text_3);
19214 });
19215
19216 editor.update_in(cx, |editor, window, cx| {
19217 editor.undo(&Default::default(), window, cx);
19218 });
19219
19220 editor.update_in(cx, |editor, window, cx| {
19221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19222 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19223 });
19224 editor.git_restore(&Default::default(), window, cx);
19225 });
19226
19227 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19228 // but not affect buffer_2 and its related excerpts.
19229 editor.update(cx, |editor, cx| {
19230 assert_eq!(
19231 editor.text(cx),
19232 "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}"
19233 );
19234 });
19235 buffer_1.update(cx, |buffer, _| {
19236 assert_eq!(buffer.text(), base_text_1);
19237 });
19238 buffer_2.update(cx, |buffer, _| {
19239 assert_eq!(
19240 buffer.text(),
19241 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19242 );
19243 });
19244 buffer_3.update(cx, |buffer, _| {
19245 assert_eq!(
19246 buffer.text(),
19247 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19248 );
19249 });
19250
19251 fn edit_first_char_of_every_line(text: &str) -> String {
19252 text.split('\n')
19253 .map(|line| format!("X{}", &line[1..]))
19254 .collect::<Vec<_>>()
19255 .join("\n")
19256 }
19257}
19258
19259#[gpui::test]
19260async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19261 init_test(cx, |_| {});
19262
19263 let cols = 4;
19264 let rows = 10;
19265 let sample_text_1 = sample_text(rows, cols, 'a');
19266 assert_eq!(
19267 sample_text_1,
19268 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19269 );
19270 let sample_text_2 = sample_text(rows, cols, 'l');
19271 assert_eq!(
19272 sample_text_2,
19273 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19274 );
19275 let sample_text_3 = sample_text(rows, cols, 'v');
19276 assert_eq!(
19277 sample_text_3,
19278 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19279 );
19280
19281 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19282 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19283 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19284
19285 let multi_buffer = cx.new(|cx| {
19286 let mut multibuffer = MultiBuffer::new(ReadWrite);
19287 multibuffer.push_excerpts(
19288 buffer_1.clone(),
19289 [
19290 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19291 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19292 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19293 ],
19294 cx,
19295 );
19296 multibuffer.push_excerpts(
19297 buffer_2.clone(),
19298 [
19299 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19300 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19301 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19302 ],
19303 cx,
19304 );
19305 multibuffer.push_excerpts(
19306 buffer_3.clone(),
19307 [
19308 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19309 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19310 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19311 ],
19312 cx,
19313 );
19314 multibuffer
19315 });
19316
19317 let fs = FakeFs::new(cx.executor());
19318 fs.insert_tree(
19319 "/a",
19320 json!({
19321 "main.rs": sample_text_1,
19322 "other.rs": sample_text_2,
19323 "lib.rs": sample_text_3,
19324 }),
19325 )
19326 .await;
19327 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19328 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19329 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19330 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19331 Editor::new(
19332 EditorMode::full(),
19333 multi_buffer,
19334 Some(project.clone()),
19335 window,
19336 cx,
19337 )
19338 });
19339 let multibuffer_item_id = workspace
19340 .update(cx, |workspace, window, cx| {
19341 assert!(
19342 workspace.active_item(cx).is_none(),
19343 "active item should be None before the first item is added"
19344 );
19345 workspace.add_item_to_active_pane(
19346 Box::new(multi_buffer_editor.clone()),
19347 None,
19348 true,
19349 window,
19350 cx,
19351 );
19352 let active_item = workspace
19353 .active_item(cx)
19354 .expect("should have an active item after adding the multi buffer");
19355 assert_eq!(
19356 active_item.buffer_kind(cx),
19357 ItemBufferKind::Multibuffer,
19358 "A multi buffer was expected to active after adding"
19359 );
19360 active_item.item_id()
19361 })
19362 .unwrap();
19363 cx.executor().run_until_parked();
19364
19365 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19366 editor.change_selections(
19367 SelectionEffects::scroll(Autoscroll::Next),
19368 window,
19369 cx,
19370 |s| s.select_ranges(Some(1..2)),
19371 );
19372 editor.open_excerpts(&OpenExcerpts, window, cx);
19373 });
19374 cx.executor().run_until_parked();
19375 let first_item_id = workspace
19376 .update(cx, |workspace, window, cx| {
19377 let active_item = workspace
19378 .active_item(cx)
19379 .expect("should have an active item after navigating into the 1st buffer");
19380 let first_item_id = active_item.item_id();
19381 assert_ne!(
19382 first_item_id, multibuffer_item_id,
19383 "Should navigate into the 1st buffer and activate it"
19384 );
19385 assert_eq!(
19386 active_item.buffer_kind(cx),
19387 ItemBufferKind::Singleton,
19388 "New active item should be a singleton buffer"
19389 );
19390 assert_eq!(
19391 active_item
19392 .act_as::<Editor>(cx)
19393 .expect("should have navigated into an editor for the 1st buffer")
19394 .read(cx)
19395 .text(cx),
19396 sample_text_1
19397 );
19398
19399 workspace
19400 .go_back(workspace.active_pane().downgrade(), window, cx)
19401 .detach_and_log_err(cx);
19402
19403 first_item_id
19404 })
19405 .unwrap();
19406 cx.executor().run_until_parked();
19407 workspace
19408 .update(cx, |workspace, _, cx| {
19409 let active_item = workspace
19410 .active_item(cx)
19411 .expect("should have an active item after navigating back");
19412 assert_eq!(
19413 active_item.item_id(),
19414 multibuffer_item_id,
19415 "Should navigate back to the multi buffer"
19416 );
19417 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19418 })
19419 .unwrap();
19420
19421 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19422 editor.change_selections(
19423 SelectionEffects::scroll(Autoscroll::Next),
19424 window,
19425 cx,
19426 |s| s.select_ranges(Some(39..40)),
19427 );
19428 editor.open_excerpts(&OpenExcerpts, window, cx);
19429 });
19430 cx.executor().run_until_parked();
19431 let second_item_id = workspace
19432 .update(cx, |workspace, window, cx| {
19433 let active_item = workspace
19434 .active_item(cx)
19435 .expect("should have an active item after navigating into the 2nd buffer");
19436 let second_item_id = active_item.item_id();
19437 assert_ne!(
19438 second_item_id, multibuffer_item_id,
19439 "Should navigate away from the multibuffer"
19440 );
19441 assert_ne!(
19442 second_item_id, first_item_id,
19443 "Should navigate into the 2nd buffer and activate it"
19444 );
19445 assert_eq!(
19446 active_item.buffer_kind(cx),
19447 ItemBufferKind::Singleton,
19448 "New active item should be a singleton buffer"
19449 );
19450 assert_eq!(
19451 active_item
19452 .act_as::<Editor>(cx)
19453 .expect("should have navigated into an editor")
19454 .read(cx)
19455 .text(cx),
19456 sample_text_2
19457 );
19458
19459 workspace
19460 .go_back(workspace.active_pane().downgrade(), window, cx)
19461 .detach_and_log_err(cx);
19462
19463 second_item_id
19464 })
19465 .unwrap();
19466 cx.executor().run_until_parked();
19467 workspace
19468 .update(cx, |workspace, _, cx| {
19469 let active_item = workspace
19470 .active_item(cx)
19471 .expect("should have an active item after navigating back from the 2nd buffer");
19472 assert_eq!(
19473 active_item.item_id(),
19474 multibuffer_item_id,
19475 "Should navigate back from the 2nd buffer to the multi buffer"
19476 );
19477 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19478 })
19479 .unwrap();
19480
19481 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19482 editor.change_selections(
19483 SelectionEffects::scroll(Autoscroll::Next),
19484 window,
19485 cx,
19486 |s| s.select_ranges(Some(70..70)),
19487 );
19488 editor.open_excerpts(&OpenExcerpts, window, cx);
19489 });
19490 cx.executor().run_until_parked();
19491 workspace
19492 .update(cx, |workspace, window, cx| {
19493 let active_item = workspace
19494 .active_item(cx)
19495 .expect("should have an active item after navigating into the 3rd buffer");
19496 let third_item_id = active_item.item_id();
19497 assert_ne!(
19498 third_item_id, multibuffer_item_id,
19499 "Should navigate into the 3rd buffer and activate it"
19500 );
19501 assert_ne!(third_item_id, first_item_id);
19502 assert_ne!(third_item_id, second_item_id);
19503 assert_eq!(
19504 active_item.buffer_kind(cx),
19505 ItemBufferKind::Singleton,
19506 "New active item should be a singleton buffer"
19507 );
19508 assert_eq!(
19509 active_item
19510 .act_as::<Editor>(cx)
19511 .expect("should have navigated into an editor")
19512 .read(cx)
19513 .text(cx),
19514 sample_text_3
19515 );
19516
19517 workspace
19518 .go_back(workspace.active_pane().downgrade(), window, cx)
19519 .detach_and_log_err(cx);
19520 })
19521 .unwrap();
19522 cx.executor().run_until_parked();
19523 workspace
19524 .update(cx, |workspace, _, cx| {
19525 let active_item = workspace
19526 .active_item(cx)
19527 .expect("should have an active item after navigating back from the 3rd buffer");
19528 assert_eq!(
19529 active_item.item_id(),
19530 multibuffer_item_id,
19531 "Should navigate back from the 3rd buffer to the multi buffer"
19532 );
19533 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19534 })
19535 .unwrap();
19536}
19537
19538#[gpui::test]
19539async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19540 init_test(cx, |_| {});
19541
19542 let mut cx = EditorTestContext::new(cx).await;
19543
19544 let diff_base = r#"
19545 use some::mod;
19546
19547 const A: u32 = 42;
19548
19549 fn main() {
19550 println!("hello");
19551
19552 println!("world");
19553 }
19554 "#
19555 .unindent();
19556
19557 cx.set_state(
19558 &r#"
19559 use some::modified;
19560
19561 ˇ
19562 fn main() {
19563 println!("hello there");
19564
19565 println!("around the");
19566 println!("world");
19567 }
19568 "#
19569 .unindent(),
19570 );
19571
19572 cx.set_head_text(&diff_base);
19573 executor.run_until_parked();
19574
19575 cx.update_editor(|editor, window, cx| {
19576 editor.go_to_next_hunk(&GoToHunk, window, cx);
19577 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19578 });
19579 executor.run_until_parked();
19580 cx.assert_state_with_diff(
19581 r#"
19582 use some::modified;
19583
19584
19585 fn main() {
19586 - println!("hello");
19587 + ˇ println!("hello there");
19588
19589 println!("around the");
19590 println!("world");
19591 }
19592 "#
19593 .unindent(),
19594 );
19595
19596 cx.update_editor(|editor, window, cx| {
19597 for _ in 0..2 {
19598 editor.go_to_next_hunk(&GoToHunk, window, cx);
19599 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19600 }
19601 });
19602 executor.run_until_parked();
19603 cx.assert_state_with_diff(
19604 r#"
19605 - use some::mod;
19606 + ˇuse some::modified;
19607
19608
19609 fn main() {
19610 - println!("hello");
19611 + println!("hello there");
19612
19613 + println!("around the");
19614 println!("world");
19615 }
19616 "#
19617 .unindent(),
19618 );
19619
19620 cx.update_editor(|editor, window, cx| {
19621 editor.go_to_next_hunk(&GoToHunk, window, cx);
19622 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19623 });
19624 executor.run_until_parked();
19625 cx.assert_state_with_diff(
19626 r#"
19627 - use some::mod;
19628 + use some::modified;
19629
19630 - const A: u32 = 42;
19631 ˇ
19632 fn main() {
19633 - println!("hello");
19634 + println!("hello there");
19635
19636 + println!("around the");
19637 println!("world");
19638 }
19639 "#
19640 .unindent(),
19641 );
19642
19643 cx.update_editor(|editor, window, cx| {
19644 editor.cancel(&Cancel, window, cx);
19645 });
19646
19647 cx.assert_state_with_diff(
19648 r#"
19649 use some::modified;
19650
19651 ˇ
19652 fn main() {
19653 println!("hello there");
19654
19655 println!("around the");
19656 println!("world");
19657 }
19658 "#
19659 .unindent(),
19660 );
19661}
19662
19663#[gpui::test]
19664async fn test_diff_base_change_with_expanded_diff_hunks(
19665 executor: BackgroundExecutor,
19666 cx: &mut TestAppContext,
19667) {
19668 init_test(cx, |_| {});
19669
19670 let mut cx = EditorTestContext::new(cx).await;
19671
19672 let diff_base = r#"
19673 use some::mod1;
19674 use some::mod2;
19675
19676 const A: u32 = 42;
19677 const B: u32 = 42;
19678 const C: u32 = 42;
19679
19680 fn main() {
19681 println!("hello");
19682
19683 println!("world");
19684 }
19685 "#
19686 .unindent();
19687
19688 cx.set_state(
19689 &r#"
19690 use some::mod2;
19691
19692 const A: u32 = 42;
19693 const C: u32 = 42;
19694
19695 fn main(ˇ) {
19696 //println!("hello");
19697
19698 println!("world");
19699 //
19700 //
19701 }
19702 "#
19703 .unindent(),
19704 );
19705
19706 cx.set_head_text(&diff_base);
19707 executor.run_until_parked();
19708
19709 cx.update_editor(|editor, window, cx| {
19710 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19711 });
19712 executor.run_until_parked();
19713 cx.assert_state_with_diff(
19714 r#"
19715 - use some::mod1;
19716 use some::mod2;
19717
19718 const A: u32 = 42;
19719 - const B: u32 = 42;
19720 const C: u32 = 42;
19721
19722 fn main(ˇ) {
19723 - println!("hello");
19724 + //println!("hello");
19725
19726 println!("world");
19727 + //
19728 + //
19729 }
19730 "#
19731 .unindent(),
19732 );
19733
19734 cx.set_head_text("new diff base!");
19735 executor.run_until_parked();
19736 cx.assert_state_with_diff(
19737 r#"
19738 - new diff base!
19739 + use some::mod2;
19740 +
19741 + const A: u32 = 42;
19742 + const C: u32 = 42;
19743 +
19744 + fn main(ˇ) {
19745 + //println!("hello");
19746 +
19747 + println!("world");
19748 + //
19749 + //
19750 + }
19751 "#
19752 .unindent(),
19753 );
19754}
19755
19756#[gpui::test]
19757async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19758 init_test(cx, |_| {});
19759
19760 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19761 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19762 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19763 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19764 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19765 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19766
19767 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19768 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19769 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19770
19771 let multi_buffer = cx.new(|cx| {
19772 let mut multibuffer = MultiBuffer::new(ReadWrite);
19773 multibuffer.push_excerpts(
19774 buffer_1.clone(),
19775 [
19776 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19777 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19778 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19779 ],
19780 cx,
19781 );
19782 multibuffer.push_excerpts(
19783 buffer_2.clone(),
19784 [
19785 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19786 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19787 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19788 ],
19789 cx,
19790 );
19791 multibuffer.push_excerpts(
19792 buffer_3.clone(),
19793 [
19794 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19795 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19796 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19797 ],
19798 cx,
19799 );
19800 multibuffer
19801 });
19802
19803 let editor =
19804 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19805 editor
19806 .update(cx, |editor, _window, cx| {
19807 for (buffer, diff_base) in [
19808 (buffer_1.clone(), file_1_old),
19809 (buffer_2.clone(), file_2_old),
19810 (buffer_3.clone(), file_3_old),
19811 ] {
19812 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19813 editor
19814 .buffer
19815 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19816 }
19817 })
19818 .unwrap();
19819
19820 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19821 cx.run_until_parked();
19822
19823 cx.assert_editor_state(
19824 &"
19825 ˇaaa
19826 ccc
19827 ddd
19828
19829 ggg
19830 hhh
19831
19832
19833 lll
19834 mmm
19835 NNN
19836
19837 qqq
19838 rrr
19839
19840 uuu
19841 111
19842 222
19843 333
19844
19845 666
19846 777
19847
19848 000
19849 !!!"
19850 .unindent(),
19851 );
19852
19853 cx.update_editor(|editor, window, cx| {
19854 editor.select_all(&SelectAll, window, cx);
19855 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19856 });
19857 cx.executor().run_until_parked();
19858
19859 cx.assert_state_with_diff(
19860 "
19861 «aaa
19862 - bbb
19863 ccc
19864 ddd
19865
19866 ggg
19867 hhh
19868
19869
19870 lll
19871 mmm
19872 - nnn
19873 + NNN
19874
19875 qqq
19876 rrr
19877
19878 uuu
19879 111
19880 222
19881 333
19882
19883 + 666
19884 777
19885
19886 000
19887 !!!ˇ»"
19888 .unindent(),
19889 );
19890}
19891
19892#[gpui::test]
19893async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19894 init_test(cx, |_| {});
19895
19896 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19897 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19898
19899 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19900 let multi_buffer = cx.new(|cx| {
19901 let mut multibuffer = MultiBuffer::new(ReadWrite);
19902 multibuffer.push_excerpts(
19903 buffer.clone(),
19904 [
19905 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19906 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19907 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19908 ],
19909 cx,
19910 );
19911 multibuffer
19912 });
19913
19914 let editor =
19915 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19916 editor
19917 .update(cx, |editor, _window, cx| {
19918 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19919 editor
19920 .buffer
19921 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19922 })
19923 .unwrap();
19924
19925 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19926 cx.run_until_parked();
19927
19928 cx.update_editor(|editor, window, cx| {
19929 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19930 });
19931 cx.executor().run_until_parked();
19932
19933 // When the start of a hunk coincides with the start of its excerpt,
19934 // the hunk is expanded. When the start of a hunk is earlier than
19935 // the start of its excerpt, the hunk is not expanded.
19936 cx.assert_state_with_diff(
19937 "
19938 ˇaaa
19939 - bbb
19940 + BBB
19941
19942 - ddd
19943 - eee
19944 + DDD
19945 + EEE
19946 fff
19947
19948 iii
19949 "
19950 .unindent(),
19951 );
19952}
19953
19954#[gpui::test]
19955async fn test_edits_around_expanded_insertion_hunks(
19956 executor: BackgroundExecutor,
19957 cx: &mut TestAppContext,
19958) {
19959 init_test(cx, |_| {});
19960
19961 let mut cx = EditorTestContext::new(cx).await;
19962
19963 let diff_base = r#"
19964 use some::mod1;
19965 use some::mod2;
19966
19967 const A: u32 = 42;
19968
19969 fn main() {
19970 println!("hello");
19971
19972 println!("world");
19973 }
19974 "#
19975 .unindent();
19976 executor.run_until_parked();
19977 cx.set_state(
19978 &r#"
19979 use some::mod1;
19980 use some::mod2;
19981
19982 const A: u32 = 42;
19983 const B: u32 = 42;
19984 const C: u32 = 42;
19985 ˇ
19986
19987 fn main() {
19988 println!("hello");
19989
19990 println!("world");
19991 }
19992 "#
19993 .unindent(),
19994 );
19995
19996 cx.set_head_text(&diff_base);
19997 executor.run_until_parked();
19998
19999 cx.update_editor(|editor, window, cx| {
20000 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20001 });
20002 executor.run_until_parked();
20003
20004 cx.assert_state_with_diff(
20005 r#"
20006 use some::mod1;
20007 use some::mod2;
20008
20009 const A: u32 = 42;
20010 + const B: u32 = 42;
20011 + const C: u32 = 42;
20012 + ˇ
20013
20014 fn main() {
20015 println!("hello");
20016
20017 println!("world");
20018 }
20019 "#
20020 .unindent(),
20021 );
20022
20023 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20024 executor.run_until_parked();
20025
20026 cx.assert_state_with_diff(
20027 r#"
20028 use some::mod1;
20029 use some::mod2;
20030
20031 const A: u32 = 42;
20032 + const B: u32 = 42;
20033 + const C: u32 = 42;
20034 + const D: u32 = 42;
20035 + ˇ
20036
20037 fn main() {
20038 println!("hello");
20039
20040 println!("world");
20041 }
20042 "#
20043 .unindent(),
20044 );
20045
20046 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20047 executor.run_until_parked();
20048
20049 cx.assert_state_with_diff(
20050 r#"
20051 use some::mod1;
20052 use some::mod2;
20053
20054 const A: u32 = 42;
20055 + const B: u32 = 42;
20056 + const C: u32 = 42;
20057 + const D: u32 = 42;
20058 + const E: u32 = 42;
20059 + ˇ
20060
20061 fn main() {
20062 println!("hello");
20063
20064 println!("world");
20065 }
20066 "#
20067 .unindent(),
20068 );
20069
20070 cx.update_editor(|editor, window, cx| {
20071 editor.delete_line(&DeleteLine, window, cx);
20072 });
20073 executor.run_until_parked();
20074
20075 cx.assert_state_with_diff(
20076 r#"
20077 use some::mod1;
20078 use some::mod2;
20079
20080 const A: u32 = 42;
20081 + const B: u32 = 42;
20082 + const C: u32 = 42;
20083 + const D: u32 = 42;
20084 + const E: u32 = 42;
20085 ˇ
20086 fn main() {
20087 println!("hello");
20088
20089 println!("world");
20090 }
20091 "#
20092 .unindent(),
20093 );
20094
20095 cx.update_editor(|editor, window, cx| {
20096 editor.move_up(&MoveUp, window, cx);
20097 editor.delete_line(&DeleteLine, window, cx);
20098 editor.move_up(&MoveUp, window, cx);
20099 editor.delete_line(&DeleteLine, window, cx);
20100 editor.move_up(&MoveUp, window, cx);
20101 editor.delete_line(&DeleteLine, window, cx);
20102 });
20103 executor.run_until_parked();
20104 cx.assert_state_with_diff(
20105 r#"
20106 use some::mod1;
20107 use some::mod2;
20108
20109 const A: u32 = 42;
20110 + const B: u32 = 42;
20111 ˇ
20112 fn main() {
20113 println!("hello");
20114
20115 println!("world");
20116 }
20117 "#
20118 .unindent(),
20119 );
20120
20121 cx.update_editor(|editor, window, cx| {
20122 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20123 editor.delete_line(&DeleteLine, window, cx);
20124 });
20125 executor.run_until_parked();
20126 cx.assert_state_with_diff(
20127 r#"
20128 ˇ
20129 fn main() {
20130 println!("hello");
20131
20132 println!("world");
20133 }
20134 "#
20135 .unindent(),
20136 );
20137}
20138
20139#[gpui::test]
20140async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20141 init_test(cx, |_| {});
20142
20143 let mut cx = EditorTestContext::new(cx).await;
20144 cx.set_head_text(indoc! { "
20145 one
20146 two
20147 three
20148 four
20149 five
20150 "
20151 });
20152 cx.set_state(indoc! { "
20153 one
20154 ˇthree
20155 five
20156 "});
20157 cx.run_until_parked();
20158 cx.update_editor(|editor, window, cx| {
20159 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20160 });
20161 cx.assert_state_with_diff(
20162 indoc! { "
20163 one
20164 - two
20165 ˇthree
20166 - four
20167 five
20168 "}
20169 .to_string(),
20170 );
20171 cx.update_editor(|editor, window, cx| {
20172 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20173 });
20174
20175 cx.assert_state_with_diff(
20176 indoc! { "
20177 one
20178 ˇthree
20179 five
20180 "}
20181 .to_string(),
20182 );
20183
20184 cx.set_state(indoc! { "
20185 one
20186 ˇTWO
20187 three
20188 four
20189 five
20190 "});
20191 cx.run_until_parked();
20192 cx.update_editor(|editor, window, cx| {
20193 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20194 });
20195
20196 cx.assert_state_with_diff(
20197 indoc! { "
20198 one
20199 - two
20200 + ˇTWO
20201 three
20202 four
20203 five
20204 "}
20205 .to_string(),
20206 );
20207 cx.update_editor(|editor, window, cx| {
20208 editor.move_up(&Default::default(), window, cx);
20209 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20210 });
20211 cx.assert_state_with_diff(
20212 indoc! { "
20213 one
20214 ˇTWO
20215 three
20216 four
20217 five
20218 "}
20219 .to_string(),
20220 );
20221}
20222
20223#[gpui::test]
20224async fn test_edits_around_expanded_deletion_hunks(
20225 executor: BackgroundExecutor,
20226 cx: &mut TestAppContext,
20227) {
20228 init_test(cx, |_| {});
20229
20230 let mut cx = EditorTestContext::new(cx).await;
20231
20232 let diff_base = r#"
20233 use some::mod1;
20234 use some::mod2;
20235
20236 const A: u32 = 42;
20237 const B: u32 = 42;
20238 const C: u32 = 42;
20239
20240
20241 fn main() {
20242 println!("hello");
20243
20244 println!("world");
20245 }
20246 "#
20247 .unindent();
20248 executor.run_until_parked();
20249 cx.set_state(
20250 &r#"
20251 use some::mod1;
20252 use some::mod2;
20253
20254 ˇconst B: u32 = 42;
20255 const C: u32 = 42;
20256
20257
20258 fn main() {
20259 println!("hello");
20260
20261 println!("world");
20262 }
20263 "#
20264 .unindent(),
20265 );
20266
20267 cx.set_head_text(&diff_base);
20268 executor.run_until_parked();
20269
20270 cx.update_editor(|editor, window, cx| {
20271 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20272 });
20273 executor.run_until_parked();
20274
20275 cx.assert_state_with_diff(
20276 r#"
20277 use some::mod1;
20278 use some::mod2;
20279
20280 - const A: u32 = 42;
20281 ˇconst B: u32 = 42;
20282 const C: u32 = 42;
20283
20284
20285 fn main() {
20286 println!("hello");
20287
20288 println!("world");
20289 }
20290 "#
20291 .unindent(),
20292 );
20293
20294 cx.update_editor(|editor, window, cx| {
20295 editor.delete_line(&DeleteLine, window, cx);
20296 });
20297 executor.run_until_parked();
20298 cx.assert_state_with_diff(
20299 r#"
20300 use some::mod1;
20301 use some::mod2;
20302
20303 - const A: u32 = 42;
20304 - const B: u32 = 42;
20305 ˇconst C: u32 = 42;
20306
20307
20308 fn main() {
20309 println!("hello");
20310
20311 println!("world");
20312 }
20313 "#
20314 .unindent(),
20315 );
20316
20317 cx.update_editor(|editor, window, cx| {
20318 editor.delete_line(&DeleteLine, window, cx);
20319 });
20320 executor.run_until_parked();
20321 cx.assert_state_with_diff(
20322 r#"
20323 use some::mod1;
20324 use some::mod2;
20325
20326 - const A: u32 = 42;
20327 - const B: u32 = 42;
20328 - const C: u32 = 42;
20329 ˇ
20330
20331 fn main() {
20332 println!("hello");
20333
20334 println!("world");
20335 }
20336 "#
20337 .unindent(),
20338 );
20339
20340 cx.update_editor(|editor, window, cx| {
20341 editor.handle_input("replacement", window, cx);
20342 });
20343 executor.run_until_parked();
20344 cx.assert_state_with_diff(
20345 r#"
20346 use some::mod1;
20347 use some::mod2;
20348
20349 - const A: u32 = 42;
20350 - const B: u32 = 42;
20351 - const C: u32 = 42;
20352 -
20353 + replacementˇ
20354
20355 fn main() {
20356 println!("hello");
20357
20358 println!("world");
20359 }
20360 "#
20361 .unindent(),
20362 );
20363}
20364
20365#[gpui::test]
20366async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20367 init_test(cx, |_| {});
20368
20369 let mut cx = EditorTestContext::new(cx).await;
20370
20371 let base_text = r#"
20372 one
20373 two
20374 three
20375 four
20376 five
20377 "#
20378 .unindent();
20379 executor.run_until_parked();
20380 cx.set_state(
20381 &r#"
20382 one
20383 two
20384 fˇour
20385 five
20386 "#
20387 .unindent(),
20388 );
20389
20390 cx.set_head_text(&base_text);
20391 executor.run_until_parked();
20392
20393 cx.update_editor(|editor, window, cx| {
20394 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20395 });
20396 executor.run_until_parked();
20397
20398 cx.assert_state_with_diff(
20399 r#"
20400 one
20401 two
20402 - three
20403 fˇour
20404 five
20405 "#
20406 .unindent(),
20407 );
20408
20409 cx.update_editor(|editor, window, cx| {
20410 editor.backspace(&Backspace, window, cx);
20411 editor.backspace(&Backspace, window, cx);
20412 });
20413 executor.run_until_parked();
20414 cx.assert_state_with_diff(
20415 r#"
20416 one
20417 two
20418 - threeˇ
20419 - four
20420 + our
20421 five
20422 "#
20423 .unindent(),
20424 );
20425}
20426
20427#[gpui::test]
20428async fn test_edit_after_expanded_modification_hunk(
20429 executor: BackgroundExecutor,
20430 cx: &mut TestAppContext,
20431) {
20432 init_test(cx, |_| {});
20433
20434 let mut cx = EditorTestContext::new(cx).await;
20435
20436 let diff_base = r#"
20437 use some::mod1;
20438 use some::mod2;
20439
20440 const A: u32 = 42;
20441 const B: u32 = 42;
20442 const C: u32 = 42;
20443 const D: u32 = 42;
20444
20445
20446 fn main() {
20447 println!("hello");
20448
20449 println!("world");
20450 }"#
20451 .unindent();
20452
20453 cx.set_state(
20454 &r#"
20455 use some::mod1;
20456 use some::mod2;
20457
20458 const A: u32 = 42;
20459 const B: u32 = 42;
20460 const C: u32 = 43ˇ
20461 const D: u32 = 42;
20462
20463
20464 fn main() {
20465 println!("hello");
20466
20467 println!("world");
20468 }"#
20469 .unindent(),
20470 );
20471
20472 cx.set_head_text(&diff_base);
20473 executor.run_until_parked();
20474 cx.update_editor(|editor, window, cx| {
20475 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20476 });
20477 executor.run_until_parked();
20478
20479 cx.assert_state_with_diff(
20480 r#"
20481 use some::mod1;
20482 use some::mod2;
20483
20484 const A: u32 = 42;
20485 const B: u32 = 42;
20486 - const C: u32 = 42;
20487 + const C: u32 = 43ˇ
20488 const D: u32 = 42;
20489
20490
20491 fn main() {
20492 println!("hello");
20493
20494 println!("world");
20495 }"#
20496 .unindent(),
20497 );
20498
20499 cx.update_editor(|editor, window, cx| {
20500 editor.handle_input("\nnew_line\n", window, cx);
20501 });
20502 executor.run_until_parked();
20503
20504 cx.assert_state_with_diff(
20505 r#"
20506 use some::mod1;
20507 use some::mod2;
20508
20509 const A: u32 = 42;
20510 const B: u32 = 42;
20511 - const C: u32 = 42;
20512 + const C: u32 = 43
20513 + new_line
20514 + ˇ
20515 const D: u32 = 42;
20516
20517
20518 fn main() {
20519 println!("hello");
20520
20521 println!("world");
20522 }"#
20523 .unindent(),
20524 );
20525}
20526
20527#[gpui::test]
20528async fn test_stage_and_unstage_added_file_hunk(
20529 executor: BackgroundExecutor,
20530 cx: &mut TestAppContext,
20531) {
20532 init_test(cx, |_| {});
20533
20534 let mut cx = EditorTestContext::new(cx).await;
20535 cx.update_editor(|editor, _, cx| {
20536 editor.set_expand_all_diff_hunks(cx);
20537 });
20538
20539 let working_copy = r#"
20540 ˇfn main() {
20541 println!("hello, world!");
20542 }
20543 "#
20544 .unindent();
20545
20546 cx.set_state(&working_copy);
20547 executor.run_until_parked();
20548
20549 cx.assert_state_with_diff(
20550 r#"
20551 + ˇfn main() {
20552 + println!("hello, world!");
20553 + }
20554 "#
20555 .unindent(),
20556 );
20557 cx.assert_index_text(None);
20558
20559 cx.update_editor(|editor, window, cx| {
20560 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20561 });
20562 executor.run_until_parked();
20563 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20564 cx.assert_state_with_diff(
20565 r#"
20566 + ˇfn main() {
20567 + println!("hello, world!");
20568 + }
20569 "#
20570 .unindent(),
20571 );
20572
20573 cx.update_editor(|editor, window, cx| {
20574 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20575 });
20576 executor.run_until_parked();
20577 cx.assert_index_text(None);
20578}
20579
20580async fn setup_indent_guides_editor(
20581 text: &str,
20582 cx: &mut TestAppContext,
20583) -> (BufferId, EditorTestContext) {
20584 init_test(cx, |_| {});
20585
20586 let mut cx = EditorTestContext::new(cx).await;
20587
20588 let buffer_id = cx.update_editor(|editor, window, cx| {
20589 editor.set_text(text, window, cx);
20590 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20591
20592 buffer_ids[0]
20593 });
20594
20595 (buffer_id, cx)
20596}
20597
20598fn assert_indent_guides(
20599 range: Range<u32>,
20600 expected: Vec<IndentGuide>,
20601 active_indices: Option<Vec<usize>>,
20602 cx: &mut EditorTestContext,
20603) {
20604 let indent_guides = cx.update_editor(|editor, window, cx| {
20605 let snapshot = editor.snapshot(window, cx).display_snapshot;
20606 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20607 editor,
20608 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20609 true,
20610 &snapshot,
20611 cx,
20612 );
20613
20614 indent_guides.sort_by(|a, b| {
20615 a.depth.cmp(&b.depth).then(
20616 a.start_row
20617 .cmp(&b.start_row)
20618 .then(a.end_row.cmp(&b.end_row)),
20619 )
20620 });
20621 indent_guides
20622 });
20623
20624 if let Some(expected) = active_indices {
20625 let active_indices = cx.update_editor(|editor, window, cx| {
20626 let snapshot = editor.snapshot(window, cx).display_snapshot;
20627 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20628 });
20629
20630 assert_eq!(
20631 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20632 expected,
20633 "Active indent guide indices do not match"
20634 );
20635 }
20636
20637 assert_eq!(indent_guides, expected, "Indent guides do not match");
20638}
20639
20640fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20641 IndentGuide {
20642 buffer_id,
20643 start_row: MultiBufferRow(start_row),
20644 end_row: MultiBufferRow(end_row),
20645 depth,
20646 tab_size: 4,
20647 settings: IndentGuideSettings {
20648 enabled: true,
20649 line_width: 1,
20650 active_line_width: 1,
20651 coloring: IndentGuideColoring::default(),
20652 background_coloring: IndentGuideBackgroundColoring::default(),
20653 },
20654 }
20655}
20656
20657#[gpui::test]
20658async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20659 let (buffer_id, mut cx) = setup_indent_guides_editor(
20660 &"
20661 fn main() {
20662 let a = 1;
20663 }"
20664 .unindent(),
20665 cx,
20666 )
20667 .await;
20668
20669 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20670}
20671
20672#[gpui::test]
20673async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20674 let (buffer_id, mut cx) = setup_indent_guides_editor(
20675 &"
20676 fn main() {
20677 let a = 1;
20678 let b = 2;
20679 }"
20680 .unindent(),
20681 cx,
20682 )
20683 .await;
20684
20685 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20686}
20687
20688#[gpui::test]
20689async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20690 let (buffer_id, mut cx) = setup_indent_guides_editor(
20691 &"
20692 fn main() {
20693 let a = 1;
20694 if a == 3 {
20695 let b = 2;
20696 } else {
20697 let c = 3;
20698 }
20699 }"
20700 .unindent(),
20701 cx,
20702 )
20703 .await;
20704
20705 assert_indent_guides(
20706 0..8,
20707 vec![
20708 indent_guide(buffer_id, 1, 6, 0),
20709 indent_guide(buffer_id, 3, 3, 1),
20710 indent_guide(buffer_id, 5, 5, 1),
20711 ],
20712 None,
20713 &mut cx,
20714 );
20715}
20716
20717#[gpui::test]
20718async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20719 let (buffer_id, mut cx) = setup_indent_guides_editor(
20720 &"
20721 fn main() {
20722 let a = 1;
20723 let b = 2;
20724 let c = 3;
20725 }"
20726 .unindent(),
20727 cx,
20728 )
20729 .await;
20730
20731 assert_indent_guides(
20732 0..5,
20733 vec![
20734 indent_guide(buffer_id, 1, 3, 0),
20735 indent_guide(buffer_id, 2, 2, 1),
20736 ],
20737 None,
20738 &mut cx,
20739 );
20740}
20741
20742#[gpui::test]
20743async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20744 let (buffer_id, mut cx) = setup_indent_guides_editor(
20745 &"
20746 fn main() {
20747 let a = 1;
20748
20749 let c = 3;
20750 }"
20751 .unindent(),
20752 cx,
20753 )
20754 .await;
20755
20756 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20757}
20758
20759#[gpui::test]
20760async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20761 let (buffer_id, mut cx) = setup_indent_guides_editor(
20762 &"
20763 fn main() {
20764 let a = 1;
20765
20766 let c = 3;
20767
20768 if a == 3 {
20769 let b = 2;
20770 } else {
20771 let c = 3;
20772 }
20773 }"
20774 .unindent(),
20775 cx,
20776 )
20777 .await;
20778
20779 assert_indent_guides(
20780 0..11,
20781 vec![
20782 indent_guide(buffer_id, 1, 9, 0),
20783 indent_guide(buffer_id, 6, 6, 1),
20784 indent_guide(buffer_id, 8, 8, 1),
20785 ],
20786 None,
20787 &mut cx,
20788 );
20789}
20790
20791#[gpui::test]
20792async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20793 let (buffer_id, mut cx) = setup_indent_guides_editor(
20794 &"
20795 fn main() {
20796 let a = 1;
20797
20798 let c = 3;
20799
20800 if a == 3 {
20801 let b = 2;
20802 } else {
20803 let c = 3;
20804 }
20805 }"
20806 .unindent(),
20807 cx,
20808 )
20809 .await;
20810
20811 assert_indent_guides(
20812 1..11,
20813 vec![
20814 indent_guide(buffer_id, 1, 9, 0),
20815 indent_guide(buffer_id, 6, 6, 1),
20816 indent_guide(buffer_id, 8, 8, 1),
20817 ],
20818 None,
20819 &mut cx,
20820 );
20821}
20822
20823#[gpui::test]
20824async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20825 let (buffer_id, mut cx) = setup_indent_guides_editor(
20826 &"
20827 fn main() {
20828 let a = 1;
20829
20830 let c = 3;
20831
20832 if a == 3 {
20833 let b = 2;
20834 } else {
20835 let c = 3;
20836 }
20837 }"
20838 .unindent(),
20839 cx,
20840 )
20841 .await;
20842
20843 assert_indent_guides(
20844 1..10,
20845 vec![
20846 indent_guide(buffer_id, 1, 9, 0),
20847 indent_guide(buffer_id, 6, 6, 1),
20848 indent_guide(buffer_id, 8, 8, 1),
20849 ],
20850 None,
20851 &mut cx,
20852 );
20853}
20854
20855#[gpui::test]
20856async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20857 let (buffer_id, mut cx) = setup_indent_guides_editor(
20858 &"
20859 fn main() {
20860 if a {
20861 b(
20862 c,
20863 d,
20864 )
20865 } else {
20866 e(
20867 f
20868 )
20869 }
20870 }"
20871 .unindent(),
20872 cx,
20873 )
20874 .await;
20875
20876 assert_indent_guides(
20877 0..11,
20878 vec![
20879 indent_guide(buffer_id, 1, 10, 0),
20880 indent_guide(buffer_id, 2, 5, 1),
20881 indent_guide(buffer_id, 7, 9, 1),
20882 indent_guide(buffer_id, 3, 4, 2),
20883 indent_guide(buffer_id, 8, 8, 2),
20884 ],
20885 None,
20886 &mut cx,
20887 );
20888
20889 cx.update_editor(|editor, window, cx| {
20890 editor.fold_at(MultiBufferRow(2), window, cx);
20891 assert_eq!(
20892 editor.display_text(cx),
20893 "
20894 fn main() {
20895 if a {
20896 b(⋯
20897 )
20898 } else {
20899 e(
20900 f
20901 )
20902 }
20903 }"
20904 .unindent()
20905 );
20906 });
20907
20908 assert_indent_guides(
20909 0..11,
20910 vec![
20911 indent_guide(buffer_id, 1, 10, 0),
20912 indent_guide(buffer_id, 2, 5, 1),
20913 indent_guide(buffer_id, 7, 9, 1),
20914 indent_guide(buffer_id, 8, 8, 2),
20915 ],
20916 None,
20917 &mut cx,
20918 );
20919}
20920
20921#[gpui::test]
20922async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20923 let (buffer_id, mut cx) = setup_indent_guides_editor(
20924 &"
20925 block1
20926 block2
20927 block3
20928 block4
20929 block2
20930 block1
20931 block1"
20932 .unindent(),
20933 cx,
20934 )
20935 .await;
20936
20937 assert_indent_guides(
20938 1..10,
20939 vec![
20940 indent_guide(buffer_id, 1, 4, 0),
20941 indent_guide(buffer_id, 2, 3, 1),
20942 indent_guide(buffer_id, 3, 3, 2),
20943 ],
20944 None,
20945 &mut cx,
20946 );
20947}
20948
20949#[gpui::test]
20950async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20951 let (buffer_id, mut cx) = setup_indent_guides_editor(
20952 &"
20953 block1
20954 block2
20955 block3
20956
20957 block1
20958 block1"
20959 .unindent(),
20960 cx,
20961 )
20962 .await;
20963
20964 assert_indent_guides(
20965 0..6,
20966 vec![
20967 indent_guide(buffer_id, 1, 2, 0),
20968 indent_guide(buffer_id, 2, 2, 1),
20969 ],
20970 None,
20971 &mut cx,
20972 );
20973}
20974
20975#[gpui::test]
20976async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20977 let (buffer_id, mut cx) = setup_indent_guides_editor(
20978 &"
20979 function component() {
20980 \treturn (
20981 \t\t\t
20982 \t\t<div>
20983 \t\t\t<abc></abc>
20984 \t\t</div>
20985 \t)
20986 }"
20987 .unindent(),
20988 cx,
20989 )
20990 .await;
20991
20992 assert_indent_guides(
20993 0..8,
20994 vec![
20995 indent_guide(buffer_id, 1, 6, 0),
20996 indent_guide(buffer_id, 2, 5, 1),
20997 indent_guide(buffer_id, 4, 4, 2),
20998 ],
20999 None,
21000 &mut cx,
21001 );
21002}
21003
21004#[gpui::test]
21005async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21006 let (buffer_id, mut cx) = setup_indent_guides_editor(
21007 &"
21008 function component() {
21009 \treturn (
21010 \t
21011 \t\t<div>
21012 \t\t\t<abc></abc>
21013 \t\t</div>
21014 \t)
21015 }"
21016 .unindent(),
21017 cx,
21018 )
21019 .await;
21020
21021 assert_indent_guides(
21022 0..8,
21023 vec![
21024 indent_guide(buffer_id, 1, 6, 0),
21025 indent_guide(buffer_id, 2, 5, 1),
21026 indent_guide(buffer_id, 4, 4, 2),
21027 ],
21028 None,
21029 &mut cx,
21030 );
21031}
21032
21033#[gpui::test]
21034async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21035 let (buffer_id, mut cx) = setup_indent_guides_editor(
21036 &"
21037 block1
21038
21039
21040
21041 block2
21042 "
21043 .unindent(),
21044 cx,
21045 )
21046 .await;
21047
21048 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21049}
21050
21051#[gpui::test]
21052async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21053 let (buffer_id, mut cx) = setup_indent_guides_editor(
21054 &"
21055 def a:
21056 \tb = 3
21057 \tif True:
21058 \t\tc = 4
21059 \t\td = 5
21060 \tprint(b)
21061 "
21062 .unindent(),
21063 cx,
21064 )
21065 .await;
21066
21067 assert_indent_guides(
21068 0..6,
21069 vec![
21070 indent_guide(buffer_id, 1, 5, 0),
21071 indent_guide(buffer_id, 3, 4, 1),
21072 ],
21073 None,
21074 &mut cx,
21075 );
21076}
21077
21078#[gpui::test]
21079async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21080 let (buffer_id, mut cx) = setup_indent_guides_editor(
21081 &"
21082 fn main() {
21083 let a = 1;
21084 }"
21085 .unindent(),
21086 cx,
21087 )
21088 .await;
21089
21090 cx.update_editor(|editor, window, cx| {
21091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21092 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21093 });
21094 });
21095
21096 assert_indent_guides(
21097 0..3,
21098 vec![indent_guide(buffer_id, 1, 1, 0)],
21099 Some(vec![0]),
21100 &mut cx,
21101 );
21102}
21103
21104#[gpui::test]
21105async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21106 let (buffer_id, mut cx) = setup_indent_guides_editor(
21107 &"
21108 fn main() {
21109 if 1 == 2 {
21110 let a = 1;
21111 }
21112 }"
21113 .unindent(),
21114 cx,
21115 )
21116 .await;
21117
21118 cx.update_editor(|editor, window, cx| {
21119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21120 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21121 });
21122 });
21123
21124 assert_indent_guides(
21125 0..4,
21126 vec![
21127 indent_guide(buffer_id, 1, 3, 0),
21128 indent_guide(buffer_id, 2, 2, 1),
21129 ],
21130 Some(vec![1]),
21131 &mut cx,
21132 );
21133
21134 cx.update_editor(|editor, window, cx| {
21135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21136 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21137 });
21138 });
21139
21140 assert_indent_guides(
21141 0..4,
21142 vec![
21143 indent_guide(buffer_id, 1, 3, 0),
21144 indent_guide(buffer_id, 2, 2, 1),
21145 ],
21146 Some(vec![1]),
21147 &mut cx,
21148 );
21149
21150 cx.update_editor(|editor, window, cx| {
21151 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21152 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21153 });
21154 });
21155
21156 assert_indent_guides(
21157 0..4,
21158 vec![
21159 indent_guide(buffer_id, 1, 3, 0),
21160 indent_guide(buffer_id, 2, 2, 1),
21161 ],
21162 Some(vec![0]),
21163 &mut cx,
21164 );
21165}
21166
21167#[gpui::test]
21168async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21169 let (buffer_id, mut cx) = setup_indent_guides_editor(
21170 &"
21171 fn main() {
21172 let a = 1;
21173
21174 let b = 2;
21175 }"
21176 .unindent(),
21177 cx,
21178 )
21179 .await;
21180
21181 cx.update_editor(|editor, window, cx| {
21182 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21183 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21184 });
21185 });
21186
21187 assert_indent_guides(
21188 0..5,
21189 vec![indent_guide(buffer_id, 1, 3, 0)],
21190 Some(vec![0]),
21191 &mut cx,
21192 );
21193}
21194
21195#[gpui::test]
21196async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21197 let (buffer_id, mut cx) = setup_indent_guides_editor(
21198 &"
21199 def m:
21200 a = 1
21201 pass"
21202 .unindent(),
21203 cx,
21204 )
21205 .await;
21206
21207 cx.update_editor(|editor, window, cx| {
21208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21209 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21210 });
21211 });
21212
21213 assert_indent_guides(
21214 0..3,
21215 vec![indent_guide(buffer_id, 1, 2, 0)],
21216 Some(vec![0]),
21217 &mut cx,
21218 );
21219}
21220
21221#[gpui::test]
21222async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21223 init_test(cx, |_| {});
21224 let mut cx = EditorTestContext::new(cx).await;
21225 let text = indoc! {
21226 "
21227 impl A {
21228 fn b() {
21229 0;
21230 3;
21231 5;
21232 6;
21233 7;
21234 }
21235 }
21236 "
21237 };
21238 let base_text = indoc! {
21239 "
21240 impl A {
21241 fn b() {
21242 0;
21243 1;
21244 2;
21245 3;
21246 4;
21247 }
21248 fn c() {
21249 5;
21250 6;
21251 7;
21252 }
21253 }
21254 "
21255 };
21256
21257 cx.update_editor(|editor, window, cx| {
21258 editor.set_text(text, window, cx);
21259
21260 editor.buffer().update(cx, |multibuffer, cx| {
21261 let buffer = multibuffer.as_singleton().unwrap();
21262 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21263
21264 multibuffer.set_all_diff_hunks_expanded(cx);
21265 multibuffer.add_diff(diff, cx);
21266
21267 buffer.read(cx).remote_id()
21268 })
21269 });
21270 cx.run_until_parked();
21271
21272 cx.assert_state_with_diff(
21273 indoc! { "
21274 impl A {
21275 fn b() {
21276 0;
21277 - 1;
21278 - 2;
21279 3;
21280 - 4;
21281 - }
21282 - fn c() {
21283 5;
21284 6;
21285 7;
21286 }
21287 }
21288 ˇ"
21289 }
21290 .to_string(),
21291 );
21292
21293 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21294 editor
21295 .snapshot(window, cx)
21296 .buffer_snapshot()
21297 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21298 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21299 .collect::<Vec<_>>()
21300 });
21301 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21302 assert_eq!(
21303 actual_guides,
21304 vec![
21305 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21306 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21307 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21308 ]
21309 );
21310}
21311
21312#[gpui::test]
21313async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21314 init_test(cx, |_| {});
21315 let mut cx = EditorTestContext::new(cx).await;
21316
21317 let diff_base = r#"
21318 a
21319 b
21320 c
21321 "#
21322 .unindent();
21323
21324 cx.set_state(
21325 &r#"
21326 ˇA
21327 b
21328 C
21329 "#
21330 .unindent(),
21331 );
21332 cx.set_head_text(&diff_base);
21333 cx.update_editor(|editor, window, cx| {
21334 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21335 });
21336 executor.run_until_parked();
21337
21338 let both_hunks_expanded = r#"
21339 - a
21340 + ˇA
21341 b
21342 - c
21343 + C
21344 "#
21345 .unindent();
21346
21347 cx.assert_state_with_diff(both_hunks_expanded.clone());
21348
21349 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21350 let snapshot = editor.snapshot(window, cx);
21351 let hunks = editor
21352 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21353 .collect::<Vec<_>>();
21354 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21355 let buffer_id = hunks[0].buffer_id;
21356 hunks
21357 .into_iter()
21358 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21359 .collect::<Vec<_>>()
21360 });
21361 assert_eq!(hunk_ranges.len(), 2);
21362
21363 cx.update_editor(|editor, _, cx| {
21364 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21365 });
21366 executor.run_until_parked();
21367
21368 let second_hunk_expanded = r#"
21369 ˇA
21370 b
21371 - c
21372 + C
21373 "#
21374 .unindent();
21375
21376 cx.assert_state_with_diff(second_hunk_expanded);
21377
21378 cx.update_editor(|editor, _, cx| {
21379 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21380 });
21381 executor.run_until_parked();
21382
21383 cx.assert_state_with_diff(both_hunks_expanded.clone());
21384
21385 cx.update_editor(|editor, _, cx| {
21386 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21387 });
21388 executor.run_until_parked();
21389
21390 let first_hunk_expanded = r#"
21391 - a
21392 + ˇA
21393 b
21394 C
21395 "#
21396 .unindent();
21397
21398 cx.assert_state_with_diff(first_hunk_expanded);
21399
21400 cx.update_editor(|editor, _, cx| {
21401 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21402 });
21403 executor.run_until_parked();
21404
21405 cx.assert_state_with_diff(both_hunks_expanded);
21406
21407 cx.set_state(
21408 &r#"
21409 ˇA
21410 b
21411 "#
21412 .unindent(),
21413 );
21414 cx.run_until_parked();
21415
21416 // TODO this cursor position seems bad
21417 cx.assert_state_with_diff(
21418 r#"
21419 - ˇa
21420 + A
21421 b
21422 "#
21423 .unindent(),
21424 );
21425
21426 cx.update_editor(|editor, window, cx| {
21427 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21428 });
21429
21430 cx.assert_state_with_diff(
21431 r#"
21432 - ˇa
21433 + A
21434 b
21435 - c
21436 "#
21437 .unindent(),
21438 );
21439
21440 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21441 let snapshot = editor.snapshot(window, cx);
21442 let hunks = editor
21443 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21444 .collect::<Vec<_>>();
21445 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21446 let buffer_id = hunks[0].buffer_id;
21447 hunks
21448 .into_iter()
21449 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21450 .collect::<Vec<_>>()
21451 });
21452 assert_eq!(hunk_ranges.len(), 2);
21453
21454 cx.update_editor(|editor, _, cx| {
21455 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21456 });
21457 executor.run_until_parked();
21458
21459 cx.assert_state_with_diff(
21460 r#"
21461 - ˇa
21462 + A
21463 b
21464 "#
21465 .unindent(),
21466 );
21467}
21468
21469#[gpui::test]
21470async fn test_toggle_deletion_hunk_at_start_of_file(
21471 executor: BackgroundExecutor,
21472 cx: &mut TestAppContext,
21473) {
21474 init_test(cx, |_| {});
21475 let mut cx = EditorTestContext::new(cx).await;
21476
21477 let diff_base = r#"
21478 a
21479 b
21480 c
21481 "#
21482 .unindent();
21483
21484 cx.set_state(
21485 &r#"
21486 ˇb
21487 c
21488 "#
21489 .unindent(),
21490 );
21491 cx.set_head_text(&diff_base);
21492 cx.update_editor(|editor, window, cx| {
21493 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21494 });
21495 executor.run_until_parked();
21496
21497 let hunk_expanded = r#"
21498 - a
21499 ˇb
21500 c
21501 "#
21502 .unindent();
21503
21504 cx.assert_state_with_diff(hunk_expanded.clone());
21505
21506 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21507 let snapshot = editor.snapshot(window, cx);
21508 let hunks = editor
21509 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21510 .collect::<Vec<_>>();
21511 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21512 let buffer_id = hunks[0].buffer_id;
21513 hunks
21514 .into_iter()
21515 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21516 .collect::<Vec<_>>()
21517 });
21518 assert_eq!(hunk_ranges.len(), 1);
21519
21520 cx.update_editor(|editor, _, cx| {
21521 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21522 });
21523 executor.run_until_parked();
21524
21525 let hunk_collapsed = r#"
21526 ˇb
21527 c
21528 "#
21529 .unindent();
21530
21531 cx.assert_state_with_diff(hunk_collapsed);
21532
21533 cx.update_editor(|editor, _, cx| {
21534 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21535 });
21536 executor.run_until_parked();
21537
21538 cx.assert_state_with_diff(hunk_expanded);
21539}
21540
21541#[gpui::test]
21542async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21543 init_test(cx, |_| {});
21544
21545 let fs = FakeFs::new(cx.executor());
21546 fs.insert_tree(
21547 path!("/test"),
21548 json!({
21549 ".git": {},
21550 "file-1": "ONE\n",
21551 "file-2": "TWO\n",
21552 "file-3": "THREE\n",
21553 }),
21554 )
21555 .await;
21556
21557 fs.set_head_for_repo(
21558 path!("/test/.git").as_ref(),
21559 &[
21560 ("file-1", "one\n".into()),
21561 ("file-2", "two\n".into()),
21562 ("file-3", "three\n".into()),
21563 ],
21564 "deadbeef",
21565 );
21566
21567 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21568 let mut buffers = vec![];
21569 for i in 1..=3 {
21570 let buffer = project
21571 .update(cx, |project, cx| {
21572 let path = format!(path!("/test/file-{}"), i);
21573 project.open_local_buffer(path, cx)
21574 })
21575 .await
21576 .unwrap();
21577 buffers.push(buffer);
21578 }
21579
21580 let multibuffer = cx.new(|cx| {
21581 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21582 multibuffer.set_all_diff_hunks_expanded(cx);
21583 for buffer in &buffers {
21584 let snapshot = buffer.read(cx).snapshot();
21585 multibuffer.set_excerpts_for_path(
21586 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21587 buffer.clone(),
21588 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21589 2,
21590 cx,
21591 );
21592 }
21593 multibuffer
21594 });
21595
21596 let editor = cx.add_window(|window, cx| {
21597 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21598 });
21599 cx.run_until_parked();
21600
21601 let snapshot = editor
21602 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21603 .unwrap();
21604 let hunks = snapshot
21605 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21606 .map(|hunk| match hunk {
21607 DisplayDiffHunk::Unfolded {
21608 display_row_range, ..
21609 } => display_row_range,
21610 DisplayDiffHunk::Folded { .. } => unreachable!(),
21611 })
21612 .collect::<Vec<_>>();
21613 assert_eq!(
21614 hunks,
21615 [
21616 DisplayRow(2)..DisplayRow(4),
21617 DisplayRow(7)..DisplayRow(9),
21618 DisplayRow(12)..DisplayRow(14),
21619 ]
21620 );
21621}
21622
21623#[gpui::test]
21624async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21625 init_test(cx, |_| {});
21626
21627 let mut cx = EditorTestContext::new(cx).await;
21628 cx.set_head_text(indoc! { "
21629 one
21630 two
21631 three
21632 four
21633 five
21634 "
21635 });
21636 cx.set_index_text(indoc! { "
21637 one
21638 two
21639 three
21640 four
21641 five
21642 "
21643 });
21644 cx.set_state(indoc! {"
21645 one
21646 TWO
21647 ˇTHREE
21648 FOUR
21649 five
21650 "});
21651 cx.run_until_parked();
21652 cx.update_editor(|editor, window, cx| {
21653 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21654 });
21655 cx.run_until_parked();
21656 cx.assert_index_text(Some(indoc! {"
21657 one
21658 TWO
21659 THREE
21660 FOUR
21661 five
21662 "}));
21663 cx.set_state(indoc! { "
21664 one
21665 TWO
21666 ˇTHREE-HUNDRED
21667 FOUR
21668 five
21669 "});
21670 cx.run_until_parked();
21671 cx.update_editor(|editor, window, cx| {
21672 let snapshot = editor.snapshot(window, cx);
21673 let hunks = editor
21674 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21675 .collect::<Vec<_>>();
21676 assert_eq!(hunks.len(), 1);
21677 assert_eq!(
21678 hunks[0].status(),
21679 DiffHunkStatus {
21680 kind: DiffHunkStatusKind::Modified,
21681 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21682 }
21683 );
21684
21685 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21686 });
21687 cx.run_until_parked();
21688 cx.assert_index_text(Some(indoc! {"
21689 one
21690 TWO
21691 THREE-HUNDRED
21692 FOUR
21693 five
21694 "}));
21695}
21696
21697#[gpui::test]
21698fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21699 init_test(cx, |_| {});
21700
21701 let editor = cx.add_window(|window, cx| {
21702 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21703 build_editor(buffer, window, cx)
21704 });
21705
21706 let render_args = Arc::new(Mutex::new(None));
21707 let snapshot = editor
21708 .update(cx, |editor, window, cx| {
21709 let snapshot = editor.buffer().read(cx).snapshot(cx);
21710 let range =
21711 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21712
21713 struct RenderArgs {
21714 row: MultiBufferRow,
21715 folded: bool,
21716 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21717 }
21718
21719 let crease = Crease::inline(
21720 range,
21721 FoldPlaceholder::test(),
21722 {
21723 let toggle_callback = render_args.clone();
21724 move |row, folded, callback, _window, _cx| {
21725 *toggle_callback.lock() = Some(RenderArgs {
21726 row,
21727 folded,
21728 callback,
21729 });
21730 div()
21731 }
21732 },
21733 |_row, _folded, _window, _cx| div(),
21734 );
21735
21736 editor.insert_creases(Some(crease), cx);
21737 let snapshot = editor.snapshot(window, cx);
21738 let _div =
21739 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21740 snapshot
21741 })
21742 .unwrap();
21743
21744 let render_args = render_args.lock().take().unwrap();
21745 assert_eq!(render_args.row, MultiBufferRow(1));
21746 assert!(!render_args.folded);
21747 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21748
21749 cx.update_window(*editor, |_, window, cx| {
21750 (render_args.callback)(true, window, cx)
21751 })
21752 .unwrap();
21753 let snapshot = editor
21754 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21755 .unwrap();
21756 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21757
21758 cx.update_window(*editor, |_, window, cx| {
21759 (render_args.callback)(false, window, cx)
21760 })
21761 .unwrap();
21762 let snapshot = editor
21763 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21764 .unwrap();
21765 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21766}
21767
21768#[gpui::test]
21769async fn test_input_text(cx: &mut TestAppContext) {
21770 init_test(cx, |_| {});
21771 let mut cx = EditorTestContext::new(cx).await;
21772
21773 cx.set_state(
21774 &r#"ˇone
21775 two
21776
21777 three
21778 fourˇ
21779 five
21780
21781 siˇx"#
21782 .unindent(),
21783 );
21784
21785 cx.dispatch_action(HandleInput(String::new()));
21786 cx.assert_editor_state(
21787 &r#"ˇone
21788 two
21789
21790 three
21791 fourˇ
21792 five
21793
21794 siˇx"#
21795 .unindent(),
21796 );
21797
21798 cx.dispatch_action(HandleInput("AAAA".to_string()));
21799 cx.assert_editor_state(
21800 &r#"AAAAˇone
21801 two
21802
21803 three
21804 fourAAAAˇ
21805 five
21806
21807 siAAAAˇx"#
21808 .unindent(),
21809 );
21810}
21811
21812#[gpui::test]
21813async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21814 init_test(cx, |_| {});
21815
21816 let mut cx = EditorTestContext::new(cx).await;
21817 cx.set_state(
21818 r#"let foo = 1;
21819let foo = 2;
21820let foo = 3;
21821let fooˇ = 4;
21822let foo = 5;
21823let foo = 6;
21824let foo = 7;
21825let foo = 8;
21826let foo = 9;
21827let foo = 10;
21828let foo = 11;
21829let foo = 12;
21830let foo = 13;
21831let foo = 14;
21832let foo = 15;"#,
21833 );
21834
21835 cx.update_editor(|e, window, cx| {
21836 assert_eq!(
21837 e.next_scroll_position,
21838 NextScrollCursorCenterTopBottom::Center,
21839 "Default next scroll direction is center",
21840 );
21841
21842 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21843 assert_eq!(
21844 e.next_scroll_position,
21845 NextScrollCursorCenterTopBottom::Top,
21846 "After center, next scroll direction should be top",
21847 );
21848
21849 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21850 assert_eq!(
21851 e.next_scroll_position,
21852 NextScrollCursorCenterTopBottom::Bottom,
21853 "After top, next scroll direction should be bottom",
21854 );
21855
21856 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21857 assert_eq!(
21858 e.next_scroll_position,
21859 NextScrollCursorCenterTopBottom::Center,
21860 "After bottom, scrolling should start over",
21861 );
21862
21863 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21864 assert_eq!(
21865 e.next_scroll_position,
21866 NextScrollCursorCenterTopBottom::Top,
21867 "Scrolling continues if retriggered fast enough"
21868 );
21869 });
21870
21871 cx.executor()
21872 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21873 cx.executor().run_until_parked();
21874 cx.update_editor(|e, _, _| {
21875 assert_eq!(
21876 e.next_scroll_position,
21877 NextScrollCursorCenterTopBottom::Center,
21878 "If scrolling is not triggered fast enough, it should reset"
21879 );
21880 });
21881}
21882
21883#[gpui::test]
21884async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21885 init_test(cx, |_| {});
21886 let mut cx = EditorLspTestContext::new_rust(
21887 lsp::ServerCapabilities {
21888 definition_provider: Some(lsp::OneOf::Left(true)),
21889 references_provider: Some(lsp::OneOf::Left(true)),
21890 ..lsp::ServerCapabilities::default()
21891 },
21892 cx,
21893 )
21894 .await;
21895
21896 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21897 let go_to_definition = cx
21898 .lsp
21899 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21900 move |params, _| async move {
21901 if empty_go_to_definition {
21902 Ok(None)
21903 } else {
21904 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21905 uri: params.text_document_position_params.text_document.uri,
21906 range: lsp::Range::new(
21907 lsp::Position::new(4, 3),
21908 lsp::Position::new(4, 6),
21909 ),
21910 })))
21911 }
21912 },
21913 );
21914 let references = cx
21915 .lsp
21916 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21917 Ok(Some(vec![lsp::Location {
21918 uri: params.text_document_position.text_document.uri,
21919 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21920 }]))
21921 });
21922 (go_to_definition, references)
21923 };
21924
21925 cx.set_state(
21926 &r#"fn one() {
21927 let mut a = ˇtwo();
21928 }
21929
21930 fn two() {}"#
21931 .unindent(),
21932 );
21933 set_up_lsp_handlers(false, &mut cx);
21934 let navigated = cx
21935 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21936 .await
21937 .expect("Failed to navigate to definition");
21938 assert_eq!(
21939 navigated,
21940 Navigated::Yes,
21941 "Should have navigated to definition from the GetDefinition response"
21942 );
21943 cx.assert_editor_state(
21944 &r#"fn one() {
21945 let mut a = two();
21946 }
21947
21948 fn «twoˇ»() {}"#
21949 .unindent(),
21950 );
21951
21952 let editors = cx.update_workspace(|workspace, _, cx| {
21953 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21954 });
21955 cx.update_editor(|_, _, test_editor_cx| {
21956 assert_eq!(
21957 editors.len(),
21958 1,
21959 "Initially, only one, test, editor should be open in the workspace"
21960 );
21961 assert_eq!(
21962 test_editor_cx.entity(),
21963 editors.last().expect("Asserted len is 1").clone()
21964 );
21965 });
21966
21967 set_up_lsp_handlers(true, &mut cx);
21968 let navigated = cx
21969 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21970 .await
21971 .expect("Failed to navigate to lookup references");
21972 assert_eq!(
21973 navigated,
21974 Navigated::Yes,
21975 "Should have navigated to references as a fallback after empty GoToDefinition response"
21976 );
21977 // We should not change the selections in the existing file,
21978 // if opening another milti buffer with the references
21979 cx.assert_editor_state(
21980 &r#"fn one() {
21981 let mut a = two();
21982 }
21983
21984 fn «twoˇ»() {}"#
21985 .unindent(),
21986 );
21987 let editors = cx.update_workspace(|workspace, _, cx| {
21988 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21989 });
21990 cx.update_editor(|_, _, test_editor_cx| {
21991 assert_eq!(
21992 editors.len(),
21993 2,
21994 "After falling back to references search, we open a new editor with the results"
21995 );
21996 let references_fallback_text = editors
21997 .into_iter()
21998 .find(|new_editor| *new_editor != test_editor_cx.entity())
21999 .expect("Should have one non-test editor now")
22000 .read(test_editor_cx)
22001 .text(test_editor_cx);
22002 assert_eq!(
22003 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22004 "Should use the range from the references response and not the GoToDefinition one"
22005 );
22006 });
22007}
22008
22009#[gpui::test]
22010async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22011 init_test(cx, |_| {});
22012 cx.update(|cx| {
22013 let mut editor_settings = EditorSettings::get_global(cx).clone();
22014 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22015 EditorSettings::override_global(editor_settings, cx);
22016 });
22017 let mut cx = EditorLspTestContext::new_rust(
22018 lsp::ServerCapabilities {
22019 definition_provider: Some(lsp::OneOf::Left(true)),
22020 references_provider: Some(lsp::OneOf::Left(true)),
22021 ..lsp::ServerCapabilities::default()
22022 },
22023 cx,
22024 )
22025 .await;
22026 let original_state = r#"fn one() {
22027 let mut a = ˇtwo();
22028 }
22029
22030 fn two() {}"#
22031 .unindent();
22032 cx.set_state(&original_state);
22033
22034 let mut go_to_definition = cx
22035 .lsp
22036 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22037 move |_, _| async move { Ok(None) },
22038 );
22039 let _references = cx
22040 .lsp
22041 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22042 panic!("Should not call for references with no go to definition fallback")
22043 });
22044
22045 let navigated = cx
22046 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22047 .await
22048 .expect("Failed to navigate to lookup references");
22049 go_to_definition
22050 .next()
22051 .await
22052 .expect("Should have called the go_to_definition handler");
22053
22054 assert_eq!(
22055 navigated,
22056 Navigated::No,
22057 "Should have navigated to references as a fallback after empty GoToDefinition response"
22058 );
22059 cx.assert_editor_state(&original_state);
22060 let editors = cx.update_workspace(|workspace, _, cx| {
22061 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22062 });
22063 cx.update_editor(|_, _, _| {
22064 assert_eq!(
22065 editors.len(),
22066 1,
22067 "After unsuccessful fallback, no other editor should have been opened"
22068 );
22069 });
22070}
22071
22072#[gpui::test]
22073async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22074 init_test(cx, |_| {});
22075 let mut cx = EditorLspTestContext::new_rust(
22076 lsp::ServerCapabilities {
22077 references_provider: Some(lsp::OneOf::Left(true)),
22078 ..lsp::ServerCapabilities::default()
22079 },
22080 cx,
22081 )
22082 .await;
22083
22084 cx.set_state(
22085 &r#"
22086 fn one() {
22087 let mut a = two();
22088 }
22089
22090 fn ˇtwo() {}"#
22091 .unindent(),
22092 );
22093 cx.lsp
22094 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22095 Ok(Some(vec![
22096 lsp::Location {
22097 uri: params.text_document_position.text_document.uri.clone(),
22098 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22099 },
22100 lsp::Location {
22101 uri: params.text_document_position.text_document.uri,
22102 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22103 },
22104 ]))
22105 });
22106 let navigated = cx
22107 .update_editor(|editor, window, cx| {
22108 editor.find_all_references(&FindAllReferences, window, cx)
22109 })
22110 .unwrap()
22111 .await
22112 .expect("Failed to navigate to references");
22113 assert_eq!(
22114 navigated,
22115 Navigated::Yes,
22116 "Should have navigated to references from the FindAllReferences response"
22117 );
22118 cx.assert_editor_state(
22119 &r#"fn one() {
22120 let mut a = two();
22121 }
22122
22123 fn ˇtwo() {}"#
22124 .unindent(),
22125 );
22126
22127 let editors = cx.update_workspace(|workspace, _, cx| {
22128 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22129 });
22130 cx.update_editor(|_, _, _| {
22131 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22132 });
22133
22134 cx.set_state(
22135 &r#"fn one() {
22136 let mut a = ˇtwo();
22137 }
22138
22139 fn two() {}"#
22140 .unindent(),
22141 );
22142 let navigated = cx
22143 .update_editor(|editor, window, cx| {
22144 editor.find_all_references(&FindAllReferences, window, cx)
22145 })
22146 .unwrap()
22147 .await
22148 .expect("Failed to navigate to references");
22149 assert_eq!(
22150 navigated,
22151 Navigated::Yes,
22152 "Should have navigated to references from the FindAllReferences response"
22153 );
22154 cx.assert_editor_state(
22155 &r#"fn one() {
22156 let mut a = ˇtwo();
22157 }
22158
22159 fn two() {}"#
22160 .unindent(),
22161 );
22162 let editors = cx.update_workspace(|workspace, _, cx| {
22163 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22164 });
22165 cx.update_editor(|_, _, _| {
22166 assert_eq!(
22167 editors.len(),
22168 2,
22169 "should have re-used the previous multibuffer"
22170 );
22171 });
22172
22173 cx.set_state(
22174 &r#"fn one() {
22175 let mut a = ˇtwo();
22176 }
22177 fn three() {}
22178 fn two() {}"#
22179 .unindent(),
22180 );
22181 cx.lsp
22182 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22183 Ok(Some(vec![
22184 lsp::Location {
22185 uri: params.text_document_position.text_document.uri.clone(),
22186 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22187 },
22188 lsp::Location {
22189 uri: params.text_document_position.text_document.uri,
22190 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22191 },
22192 ]))
22193 });
22194 let navigated = cx
22195 .update_editor(|editor, window, cx| {
22196 editor.find_all_references(&FindAllReferences, window, cx)
22197 })
22198 .unwrap()
22199 .await
22200 .expect("Failed to navigate to references");
22201 assert_eq!(
22202 navigated,
22203 Navigated::Yes,
22204 "Should have navigated to references from the FindAllReferences response"
22205 );
22206 cx.assert_editor_state(
22207 &r#"fn one() {
22208 let mut a = ˇtwo();
22209 }
22210 fn three() {}
22211 fn two() {}"#
22212 .unindent(),
22213 );
22214 let editors = cx.update_workspace(|workspace, _, cx| {
22215 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22216 });
22217 cx.update_editor(|_, _, _| {
22218 assert_eq!(
22219 editors.len(),
22220 3,
22221 "should have used a new multibuffer as offsets changed"
22222 );
22223 });
22224}
22225#[gpui::test]
22226async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22227 init_test(cx, |_| {});
22228
22229 let language = Arc::new(Language::new(
22230 LanguageConfig::default(),
22231 Some(tree_sitter_rust::LANGUAGE.into()),
22232 ));
22233
22234 let text = r#"
22235 #[cfg(test)]
22236 mod tests() {
22237 #[test]
22238 fn runnable_1() {
22239 let a = 1;
22240 }
22241
22242 #[test]
22243 fn runnable_2() {
22244 let a = 1;
22245 let b = 2;
22246 }
22247 }
22248 "#
22249 .unindent();
22250
22251 let fs = FakeFs::new(cx.executor());
22252 fs.insert_file("/file.rs", Default::default()).await;
22253
22254 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22255 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22256 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22257 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22258 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22259
22260 let editor = cx.new_window_entity(|window, cx| {
22261 Editor::new(
22262 EditorMode::full(),
22263 multi_buffer,
22264 Some(project.clone()),
22265 window,
22266 cx,
22267 )
22268 });
22269
22270 editor.update_in(cx, |editor, window, cx| {
22271 let snapshot = editor.buffer().read(cx).snapshot(cx);
22272 editor.tasks.insert(
22273 (buffer.read(cx).remote_id(), 3),
22274 RunnableTasks {
22275 templates: vec![],
22276 offset: snapshot.anchor_before(43),
22277 column: 0,
22278 extra_variables: HashMap::default(),
22279 context_range: BufferOffset(43)..BufferOffset(85),
22280 },
22281 );
22282 editor.tasks.insert(
22283 (buffer.read(cx).remote_id(), 8),
22284 RunnableTasks {
22285 templates: vec![],
22286 offset: snapshot.anchor_before(86),
22287 column: 0,
22288 extra_variables: HashMap::default(),
22289 context_range: BufferOffset(86)..BufferOffset(191),
22290 },
22291 );
22292
22293 // Test finding task when cursor is inside function body
22294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22295 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22296 });
22297 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22298 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22299
22300 // Test finding task when cursor is on function name
22301 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22302 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22303 });
22304 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22305 assert_eq!(row, 8, "Should find task when cursor is on function name");
22306 });
22307}
22308
22309#[gpui::test]
22310async fn test_folding_buffers(cx: &mut TestAppContext) {
22311 init_test(cx, |_| {});
22312
22313 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22314 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22315 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22316
22317 let fs = FakeFs::new(cx.executor());
22318 fs.insert_tree(
22319 path!("/a"),
22320 json!({
22321 "first.rs": sample_text_1,
22322 "second.rs": sample_text_2,
22323 "third.rs": sample_text_3,
22324 }),
22325 )
22326 .await;
22327 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22328 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22329 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22330 let worktree = project.update(cx, |project, cx| {
22331 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22332 assert_eq!(worktrees.len(), 1);
22333 worktrees.pop().unwrap()
22334 });
22335 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22336
22337 let buffer_1 = project
22338 .update(cx, |project, cx| {
22339 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22340 })
22341 .await
22342 .unwrap();
22343 let buffer_2 = project
22344 .update(cx, |project, cx| {
22345 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22346 })
22347 .await
22348 .unwrap();
22349 let buffer_3 = project
22350 .update(cx, |project, cx| {
22351 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22352 })
22353 .await
22354 .unwrap();
22355
22356 let multi_buffer = cx.new(|cx| {
22357 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22358 multi_buffer.push_excerpts(
22359 buffer_1.clone(),
22360 [
22361 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22362 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22363 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22364 ],
22365 cx,
22366 );
22367 multi_buffer.push_excerpts(
22368 buffer_2.clone(),
22369 [
22370 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22371 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22372 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22373 ],
22374 cx,
22375 );
22376 multi_buffer.push_excerpts(
22377 buffer_3.clone(),
22378 [
22379 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22380 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22381 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22382 ],
22383 cx,
22384 );
22385 multi_buffer
22386 });
22387 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22388 Editor::new(
22389 EditorMode::full(),
22390 multi_buffer.clone(),
22391 Some(project.clone()),
22392 window,
22393 cx,
22394 )
22395 });
22396
22397 assert_eq!(
22398 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22399 "\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",
22400 );
22401
22402 multi_buffer_editor.update(cx, |editor, cx| {
22403 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22404 });
22405 assert_eq!(
22406 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22407 "\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",
22408 "After folding the first buffer, its text should not be displayed"
22409 );
22410
22411 multi_buffer_editor.update(cx, |editor, cx| {
22412 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22413 });
22414 assert_eq!(
22415 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22416 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22417 "After folding the second buffer, its text should not be displayed"
22418 );
22419
22420 multi_buffer_editor.update(cx, |editor, cx| {
22421 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22422 });
22423 assert_eq!(
22424 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22425 "\n\n\n\n\n",
22426 "After folding the third buffer, its text should not be displayed"
22427 );
22428
22429 // Emulate selection inside the fold logic, that should work
22430 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22431 editor
22432 .snapshot(window, cx)
22433 .next_line_boundary(Point::new(0, 4));
22434 });
22435
22436 multi_buffer_editor.update(cx, |editor, cx| {
22437 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22438 });
22439 assert_eq!(
22440 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22441 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22442 "After unfolding the second buffer, its text should be displayed"
22443 );
22444
22445 // Typing inside of buffer 1 causes that buffer to be unfolded.
22446 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22447 assert_eq!(
22448 multi_buffer
22449 .read(cx)
22450 .snapshot(cx)
22451 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22452 .collect::<String>(),
22453 "bbbb"
22454 );
22455 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22456 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22457 });
22458 editor.handle_input("B", window, cx);
22459 });
22460
22461 assert_eq!(
22462 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22463 "\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",
22464 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22465 );
22466
22467 multi_buffer_editor.update(cx, |editor, cx| {
22468 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22469 });
22470 assert_eq!(
22471 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22472 "\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",
22473 "After unfolding the all buffers, all original text should be displayed"
22474 );
22475}
22476
22477#[gpui::test]
22478async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22479 init_test(cx, |_| {});
22480
22481 let sample_text_1 = "1111\n2222\n3333".to_string();
22482 let sample_text_2 = "4444\n5555\n6666".to_string();
22483 let sample_text_3 = "7777\n8888\n9999".to_string();
22484
22485 let fs = FakeFs::new(cx.executor());
22486 fs.insert_tree(
22487 path!("/a"),
22488 json!({
22489 "first.rs": sample_text_1,
22490 "second.rs": sample_text_2,
22491 "third.rs": sample_text_3,
22492 }),
22493 )
22494 .await;
22495 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22496 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22497 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22498 let worktree = project.update(cx, |project, cx| {
22499 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22500 assert_eq!(worktrees.len(), 1);
22501 worktrees.pop().unwrap()
22502 });
22503 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22504
22505 let buffer_1 = project
22506 .update(cx, |project, cx| {
22507 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22508 })
22509 .await
22510 .unwrap();
22511 let buffer_2 = project
22512 .update(cx, |project, cx| {
22513 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22514 })
22515 .await
22516 .unwrap();
22517 let buffer_3 = project
22518 .update(cx, |project, cx| {
22519 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22520 })
22521 .await
22522 .unwrap();
22523
22524 let multi_buffer = cx.new(|cx| {
22525 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22526 multi_buffer.push_excerpts(
22527 buffer_1.clone(),
22528 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22529 cx,
22530 );
22531 multi_buffer.push_excerpts(
22532 buffer_2.clone(),
22533 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22534 cx,
22535 );
22536 multi_buffer.push_excerpts(
22537 buffer_3.clone(),
22538 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22539 cx,
22540 );
22541 multi_buffer
22542 });
22543
22544 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22545 Editor::new(
22546 EditorMode::full(),
22547 multi_buffer,
22548 Some(project.clone()),
22549 window,
22550 cx,
22551 )
22552 });
22553
22554 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22555 assert_eq!(
22556 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22557 full_text,
22558 );
22559
22560 multi_buffer_editor.update(cx, |editor, cx| {
22561 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22562 });
22563 assert_eq!(
22564 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22565 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22566 "After folding the first buffer, its text should not be displayed"
22567 );
22568
22569 multi_buffer_editor.update(cx, |editor, cx| {
22570 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22571 });
22572
22573 assert_eq!(
22574 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22575 "\n\n\n\n\n\n7777\n8888\n9999",
22576 "After folding the second buffer, its text should not be displayed"
22577 );
22578
22579 multi_buffer_editor.update(cx, |editor, cx| {
22580 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22581 });
22582 assert_eq!(
22583 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22584 "\n\n\n\n\n",
22585 "After folding the third buffer, its text should not be displayed"
22586 );
22587
22588 multi_buffer_editor.update(cx, |editor, cx| {
22589 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22590 });
22591 assert_eq!(
22592 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22593 "\n\n\n\n4444\n5555\n6666\n\n",
22594 "After unfolding the second buffer, its text should be displayed"
22595 );
22596
22597 multi_buffer_editor.update(cx, |editor, cx| {
22598 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22599 });
22600 assert_eq!(
22601 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22602 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22603 "After unfolding the first buffer, its text should be displayed"
22604 );
22605
22606 multi_buffer_editor.update(cx, |editor, cx| {
22607 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22608 });
22609 assert_eq!(
22610 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22611 full_text,
22612 "After unfolding all buffers, all original text should be displayed"
22613 );
22614}
22615
22616#[gpui::test]
22617async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22618 init_test(cx, |_| {});
22619
22620 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22621
22622 let fs = FakeFs::new(cx.executor());
22623 fs.insert_tree(
22624 path!("/a"),
22625 json!({
22626 "main.rs": sample_text,
22627 }),
22628 )
22629 .await;
22630 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22631 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22632 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22633 let worktree = project.update(cx, |project, cx| {
22634 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22635 assert_eq!(worktrees.len(), 1);
22636 worktrees.pop().unwrap()
22637 });
22638 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22639
22640 let buffer_1 = project
22641 .update(cx, |project, cx| {
22642 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22643 })
22644 .await
22645 .unwrap();
22646
22647 let multi_buffer = cx.new(|cx| {
22648 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22649 multi_buffer.push_excerpts(
22650 buffer_1.clone(),
22651 [ExcerptRange::new(
22652 Point::new(0, 0)
22653 ..Point::new(
22654 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22655 0,
22656 ),
22657 )],
22658 cx,
22659 );
22660 multi_buffer
22661 });
22662 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22663 Editor::new(
22664 EditorMode::full(),
22665 multi_buffer,
22666 Some(project.clone()),
22667 window,
22668 cx,
22669 )
22670 });
22671
22672 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22673 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22674 enum TestHighlight {}
22675 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22676 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22677 editor.highlight_text::<TestHighlight>(
22678 vec![highlight_range.clone()],
22679 HighlightStyle::color(Hsla::green()),
22680 cx,
22681 );
22682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22683 s.select_ranges(Some(highlight_range))
22684 });
22685 });
22686
22687 let full_text = format!("\n\n{sample_text}");
22688 assert_eq!(
22689 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22690 full_text,
22691 );
22692}
22693
22694#[gpui::test]
22695async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22696 init_test(cx, |_| {});
22697 cx.update(|cx| {
22698 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22699 "keymaps/default-linux.json",
22700 cx,
22701 )
22702 .unwrap();
22703 cx.bind_keys(default_key_bindings);
22704 });
22705
22706 let (editor, cx) = cx.add_window_view(|window, cx| {
22707 let multi_buffer = MultiBuffer::build_multi(
22708 [
22709 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22710 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22711 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22712 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22713 ],
22714 cx,
22715 );
22716 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22717
22718 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22719 // fold all but the second buffer, so that we test navigating between two
22720 // adjacent folded buffers, as well as folded buffers at the start and
22721 // end the multibuffer
22722 editor.fold_buffer(buffer_ids[0], cx);
22723 editor.fold_buffer(buffer_ids[2], cx);
22724 editor.fold_buffer(buffer_ids[3], cx);
22725
22726 editor
22727 });
22728 cx.simulate_resize(size(px(1000.), px(1000.)));
22729
22730 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22731 cx.assert_excerpts_with_selections(indoc! {"
22732 [EXCERPT]
22733 ˇ[FOLDED]
22734 [EXCERPT]
22735 a1
22736 b1
22737 [EXCERPT]
22738 [FOLDED]
22739 [EXCERPT]
22740 [FOLDED]
22741 "
22742 });
22743 cx.simulate_keystroke("down");
22744 cx.assert_excerpts_with_selections(indoc! {"
22745 [EXCERPT]
22746 [FOLDED]
22747 [EXCERPT]
22748 ˇa1
22749 b1
22750 [EXCERPT]
22751 [FOLDED]
22752 [EXCERPT]
22753 [FOLDED]
22754 "
22755 });
22756 cx.simulate_keystroke("down");
22757 cx.assert_excerpts_with_selections(indoc! {"
22758 [EXCERPT]
22759 [FOLDED]
22760 [EXCERPT]
22761 a1
22762 ˇb1
22763 [EXCERPT]
22764 [FOLDED]
22765 [EXCERPT]
22766 [FOLDED]
22767 "
22768 });
22769 cx.simulate_keystroke("down");
22770 cx.assert_excerpts_with_selections(indoc! {"
22771 [EXCERPT]
22772 [FOLDED]
22773 [EXCERPT]
22774 a1
22775 b1
22776 ˇ[EXCERPT]
22777 [FOLDED]
22778 [EXCERPT]
22779 [FOLDED]
22780 "
22781 });
22782 cx.simulate_keystroke("down");
22783 cx.assert_excerpts_with_selections(indoc! {"
22784 [EXCERPT]
22785 [FOLDED]
22786 [EXCERPT]
22787 a1
22788 b1
22789 [EXCERPT]
22790 ˇ[FOLDED]
22791 [EXCERPT]
22792 [FOLDED]
22793 "
22794 });
22795 for _ in 0..5 {
22796 cx.simulate_keystroke("down");
22797 cx.assert_excerpts_with_selections(indoc! {"
22798 [EXCERPT]
22799 [FOLDED]
22800 [EXCERPT]
22801 a1
22802 b1
22803 [EXCERPT]
22804 [FOLDED]
22805 [EXCERPT]
22806 ˇ[FOLDED]
22807 "
22808 });
22809 }
22810
22811 cx.simulate_keystroke("up");
22812 cx.assert_excerpts_with_selections(indoc! {"
22813 [EXCERPT]
22814 [FOLDED]
22815 [EXCERPT]
22816 a1
22817 b1
22818 [EXCERPT]
22819 ˇ[FOLDED]
22820 [EXCERPT]
22821 [FOLDED]
22822 "
22823 });
22824 cx.simulate_keystroke("up");
22825 cx.assert_excerpts_with_selections(indoc! {"
22826 [EXCERPT]
22827 [FOLDED]
22828 [EXCERPT]
22829 a1
22830 b1
22831 ˇ[EXCERPT]
22832 [FOLDED]
22833 [EXCERPT]
22834 [FOLDED]
22835 "
22836 });
22837 cx.simulate_keystroke("up");
22838 cx.assert_excerpts_with_selections(indoc! {"
22839 [EXCERPT]
22840 [FOLDED]
22841 [EXCERPT]
22842 a1
22843 ˇb1
22844 [EXCERPT]
22845 [FOLDED]
22846 [EXCERPT]
22847 [FOLDED]
22848 "
22849 });
22850 cx.simulate_keystroke("up");
22851 cx.assert_excerpts_with_selections(indoc! {"
22852 [EXCERPT]
22853 [FOLDED]
22854 [EXCERPT]
22855 ˇa1
22856 b1
22857 [EXCERPT]
22858 [FOLDED]
22859 [EXCERPT]
22860 [FOLDED]
22861 "
22862 });
22863 for _ in 0..5 {
22864 cx.simulate_keystroke("up");
22865 cx.assert_excerpts_with_selections(indoc! {"
22866 [EXCERPT]
22867 ˇ[FOLDED]
22868 [EXCERPT]
22869 a1
22870 b1
22871 [EXCERPT]
22872 [FOLDED]
22873 [EXCERPT]
22874 [FOLDED]
22875 "
22876 });
22877 }
22878}
22879
22880#[gpui::test]
22881async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22882 init_test(cx, |_| {});
22883
22884 // Simple insertion
22885 assert_highlighted_edits(
22886 "Hello, world!",
22887 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22888 true,
22889 cx,
22890 |highlighted_edits, cx| {
22891 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22892 assert_eq!(highlighted_edits.highlights.len(), 1);
22893 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22894 assert_eq!(
22895 highlighted_edits.highlights[0].1.background_color,
22896 Some(cx.theme().status().created_background)
22897 );
22898 },
22899 )
22900 .await;
22901
22902 // Replacement
22903 assert_highlighted_edits(
22904 "This is a test.",
22905 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22906 false,
22907 cx,
22908 |highlighted_edits, cx| {
22909 assert_eq!(highlighted_edits.text, "That is a test.");
22910 assert_eq!(highlighted_edits.highlights.len(), 1);
22911 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22912 assert_eq!(
22913 highlighted_edits.highlights[0].1.background_color,
22914 Some(cx.theme().status().created_background)
22915 );
22916 },
22917 )
22918 .await;
22919
22920 // Multiple edits
22921 assert_highlighted_edits(
22922 "Hello, world!",
22923 vec![
22924 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22925 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22926 ],
22927 false,
22928 cx,
22929 |highlighted_edits, cx| {
22930 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22931 assert_eq!(highlighted_edits.highlights.len(), 2);
22932 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22933 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22934 assert_eq!(
22935 highlighted_edits.highlights[0].1.background_color,
22936 Some(cx.theme().status().created_background)
22937 );
22938 assert_eq!(
22939 highlighted_edits.highlights[1].1.background_color,
22940 Some(cx.theme().status().created_background)
22941 );
22942 },
22943 )
22944 .await;
22945
22946 // Multiple lines with edits
22947 assert_highlighted_edits(
22948 "First line\nSecond line\nThird line\nFourth line",
22949 vec![
22950 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22951 (
22952 Point::new(2, 0)..Point::new(2, 10),
22953 "New third line".to_string(),
22954 ),
22955 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22956 ],
22957 false,
22958 cx,
22959 |highlighted_edits, cx| {
22960 assert_eq!(
22961 highlighted_edits.text,
22962 "Second modified\nNew third line\nFourth updated line"
22963 );
22964 assert_eq!(highlighted_edits.highlights.len(), 3);
22965 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22966 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22967 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22968 for highlight in &highlighted_edits.highlights {
22969 assert_eq!(
22970 highlight.1.background_color,
22971 Some(cx.theme().status().created_background)
22972 );
22973 }
22974 },
22975 )
22976 .await;
22977}
22978
22979#[gpui::test]
22980async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22981 init_test(cx, |_| {});
22982
22983 // Deletion
22984 assert_highlighted_edits(
22985 "Hello, world!",
22986 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22987 true,
22988 cx,
22989 |highlighted_edits, cx| {
22990 assert_eq!(highlighted_edits.text, "Hello, world!");
22991 assert_eq!(highlighted_edits.highlights.len(), 1);
22992 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22993 assert_eq!(
22994 highlighted_edits.highlights[0].1.background_color,
22995 Some(cx.theme().status().deleted_background)
22996 );
22997 },
22998 )
22999 .await;
23000
23001 // Insertion
23002 assert_highlighted_edits(
23003 "Hello, world!",
23004 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23005 true,
23006 cx,
23007 |highlighted_edits, cx| {
23008 assert_eq!(highlighted_edits.highlights.len(), 1);
23009 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23010 assert_eq!(
23011 highlighted_edits.highlights[0].1.background_color,
23012 Some(cx.theme().status().created_background)
23013 );
23014 },
23015 )
23016 .await;
23017}
23018
23019async fn assert_highlighted_edits(
23020 text: &str,
23021 edits: Vec<(Range<Point>, String)>,
23022 include_deletions: bool,
23023 cx: &mut TestAppContext,
23024 assertion_fn: impl Fn(HighlightedText, &App),
23025) {
23026 let window = cx.add_window(|window, cx| {
23027 let buffer = MultiBuffer::build_simple(text, cx);
23028 Editor::new(EditorMode::full(), buffer, None, window, cx)
23029 });
23030 let cx = &mut VisualTestContext::from_window(*window, cx);
23031
23032 let (buffer, snapshot) = window
23033 .update(cx, |editor, _window, cx| {
23034 (
23035 editor.buffer().clone(),
23036 editor.buffer().read(cx).snapshot(cx),
23037 )
23038 })
23039 .unwrap();
23040
23041 let edits = edits
23042 .into_iter()
23043 .map(|(range, edit)| {
23044 (
23045 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23046 edit,
23047 )
23048 })
23049 .collect::<Vec<_>>();
23050
23051 let text_anchor_edits = edits
23052 .clone()
23053 .into_iter()
23054 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23055 .collect::<Vec<_>>();
23056
23057 let edit_preview = window
23058 .update(cx, |_, _window, cx| {
23059 buffer
23060 .read(cx)
23061 .as_singleton()
23062 .unwrap()
23063 .read(cx)
23064 .preview_edits(text_anchor_edits.into(), cx)
23065 })
23066 .unwrap()
23067 .await;
23068
23069 cx.update(|_window, cx| {
23070 let highlighted_edits = edit_prediction_edit_text(
23071 snapshot.as_singleton().unwrap().2,
23072 &edits,
23073 &edit_preview,
23074 include_deletions,
23075 cx,
23076 );
23077 assertion_fn(highlighted_edits, cx)
23078 });
23079}
23080
23081#[track_caller]
23082fn assert_breakpoint(
23083 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23084 path: &Arc<Path>,
23085 expected: Vec<(u32, Breakpoint)>,
23086) {
23087 if expected.is_empty() {
23088 assert!(!breakpoints.contains_key(path), "{}", path.display());
23089 } else {
23090 let mut breakpoint = breakpoints
23091 .get(path)
23092 .unwrap()
23093 .iter()
23094 .map(|breakpoint| {
23095 (
23096 breakpoint.row,
23097 Breakpoint {
23098 message: breakpoint.message.clone(),
23099 state: breakpoint.state,
23100 condition: breakpoint.condition.clone(),
23101 hit_condition: breakpoint.hit_condition.clone(),
23102 },
23103 )
23104 })
23105 .collect::<Vec<_>>();
23106
23107 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23108
23109 assert_eq!(expected, breakpoint);
23110 }
23111}
23112
23113fn add_log_breakpoint_at_cursor(
23114 editor: &mut Editor,
23115 log_message: &str,
23116 window: &mut Window,
23117 cx: &mut Context<Editor>,
23118) {
23119 let (anchor, bp) = editor
23120 .breakpoints_at_cursors(window, cx)
23121 .first()
23122 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23123 .unwrap_or_else(|| {
23124 let snapshot = editor.snapshot(window, cx);
23125 let cursor_position: Point =
23126 editor.selections.newest(&snapshot.display_snapshot).head();
23127
23128 let breakpoint_position = snapshot
23129 .buffer_snapshot()
23130 .anchor_before(Point::new(cursor_position.row, 0));
23131
23132 (breakpoint_position, Breakpoint::new_log(log_message))
23133 });
23134
23135 editor.edit_breakpoint_at_anchor(
23136 anchor,
23137 bp,
23138 BreakpointEditAction::EditLogMessage(log_message.into()),
23139 cx,
23140 );
23141}
23142
23143#[gpui::test]
23144async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23145 init_test(cx, |_| {});
23146
23147 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23148 let fs = FakeFs::new(cx.executor());
23149 fs.insert_tree(
23150 path!("/a"),
23151 json!({
23152 "main.rs": sample_text,
23153 }),
23154 )
23155 .await;
23156 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23157 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23158 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23159
23160 let fs = FakeFs::new(cx.executor());
23161 fs.insert_tree(
23162 path!("/a"),
23163 json!({
23164 "main.rs": sample_text,
23165 }),
23166 )
23167 .await;
23168 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23169 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23170 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23171 let worktree_id = workspace
23172 .update(cx, |workspace, _window, cx| {
23173 workspace.project().update(cx, |project, cx| {
23174 project.worktrees(cx).next().unwrap().read(cx).id()
23175 })
23176 })
23177 .unwrap();
23178
23179 let buffer = project
23180 .update(cx, |project, cx| {
23181 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23182 })
23183 .await
23184 .unwrap();
23185
23186 let (editor, cx) = cx.add_window_view(|window, cx| {
23187 Editor::new(
23188 EditorMode::full(),
23189 MultiBuffer::build_from_buffer(buffer, cx),
23190 Some(project.clone()),
23191 window,
23192 cx,
23193 )
23194 });
23195
23196 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23197 let abs_path = project.read_with(cx, |project, cx| {
23198 project
23199 .absolute_path(&project_path, cx)
23200 .map(Arc::from)
23201 .unwrap()
23202 });
23203
23204 // assert we can add breakpoint on the first line
23205 editor.update_in(cx, |editor, window, cx| {
23206 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23207 editor.move_to_end(&MoveToEnd, window, cx);
23208 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23209 });
23210
23211 let breakpoints = editor.update(cx, |editor, cx| {
23212 editor
23213 .breakpoint_store()
23214 .as_ref()
23215 .unwrap()
23216 .read(cx)
23217 .all_source_breakpoints(cx)
23218 });
23219
23220 assert_eq!(1, breakpoints.len());
23221 assert_breakpoint(
23222 &breakpoints,
23223 &abs_path,
23224 vec![
23225 (0, Breakpoint::new_standard()),
23226 (3, Breakpoint::new_standard()),
23227 ],
23228 );
23229
23230 editor.update_in(cx, |editor, window, cx| {
23231 editor.move_to_beginning(&MoveToBeginning, window, cx);
23232 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23233 });
23234
23235 let breakpoints = editor.update(cx, |editor, cx| {
23236 editor
23237 .breakpoint_store()
23238 .as_ref()
23239 .unwrap()
23240 .read(cx)
23241 .all_source_breakpoints(cx)
23242 });
23243
23244 assert_eq!(1, breakpoints.len());
23245 assert_breakpoint(
23246 &breakpoints,
23247 &abs_path,
23248 vec![(3, Breakpoint::new_standard())],
23249 );
23250
23251 editor.update_in(cx, |editor, window, cx| {
23252 editor.move_to_end(&MoveToEnd, window, cx);
23253 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23254 });
23255
23256 let breakpoints = editor.update(cx, |editor, cx| {
23257 editor
23258 .breakpoint_store()
23259 .as_ref()
23260 .unwrap()
23261 .read(cx)
23262 .all_source_breakpoints(cx)
23263 });
23264
23265 assert_eq!(0, breakpoints.len());
23266 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23267}
23268
23269#[gpui::test]
23270async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23271 init_test(cx, |_| {});
23272
23273 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23274
23275 let fs = FakeFs::new(cx.executor());
23276 fs.insert_tree(
23277 path!("/a"),
23278 json!({
23279 "main.rs": sample_text,
23280 }),
23281 )
23282 .await;
23283 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23284 let (workspace, cx) =
23285 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23286
23287 let worktree_id = workspace.update(cx, |workspace, cx| {
23288 workspace.project().update(cx, |project, cx| {
23289 project.worktrees(cx).next().unwrap().read(cx).id()
23290 })
23291 });
23292
23293 let buffer = project
23294 .update(cx, |project, cx| {
23295 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23296 })
23297 .await
23298 .unwrap();
23299
23300 let (editor, cx) = cx.add_window_view(|window, cx| {
23301 Editor::new(
23302 EditorMode::full(),
23303 MultiBuffer::build_from_buffer(buffer, cx),
23304 Some(project.clone()),
23305 window,
23306 cx,
23307 )
23308 });
23309
23310 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23311 let abs_path = project.read_with(cx, |project, cx| {
23312 project
23313 .absolute_path(&project_path, cx)
23314 .map(Arc::from)
23315 .unwrap()
23316 });
23317
23318 editor.update_in(cx, |editor, window, cx| {
23319 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23320 });
23321
23322 let breakpoints = editor.update(cx, |editor, cx| {
23323 editor
23324 .breakpoint_store()
23325 .as_ref()
23326 .unwrap()
23327 .read(cx)
23328 .all_source_breakpoints(cx)
23329 });
23330
23331 assert_breakpoint(
23332 &breakpoints,
23333 &abs_path,
23334 vec![(0, Breakpoint::new_log("hello world"))],
23335 );
23336
23337 // Removing a log message from a log breakpoint should remove it
23338 editor.update_in(cx, |editor, window, cx| {
23339 add_log_breakpoint_at_cursor(editor, "", window, cx);
23340 });
23341
23342 let breakpoints = editor.update(cx, |editor, cx| {
23343 editor
23344 .breakpoint_store()
23345 .as_ref()
23346 .unwrap()
23347 .read(cx)
23348 .all_source_breakpoints(cx)
23349 });
23350
23351 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23352
23353 editor.update_in(cx, |editor, window, cx| {
23354 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23355 editor.move_to_end(&MoveToEnd, window, cx);
23356 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23357 // Not adding a log message to a standard breakpoint shouldn't remove it
23358 add_log_breakpoint_at_cursor(editor, "", window, cx);
23359 });
23360
23361 let breakpoints = editor.update(cx, |editor, cx| {
23362 editor
23363 .breakpoint_store()
23364 .as_ref()
23365 .unwrap()
23366 .read(cx)
23367 .all_source_breakpoints(cx)
23368 });
23369
23370 assert_breakpoint(
23371 &breakpoints,
23372 &abs_path,
23373 vec![
23374 (0, Breakpoint::new_standard()),
23375 (3, Breakpoint::new_standard()),
23376 ],
23377 );
23378
23379 editor.update_in(cx, |editor, window, cx| {
23380 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23381 });
23382
23383 let breakpoints = editor.update(cx, |editor, cx| {
23384 editor
23385 .breakpoint_store()
23386 .as_ref()
23387 .unwrap()
23388 .read(cx)
23389 .all_source_breakpoints(cx)
23390 });
23391
23392 assert_breakpoint(
23393 &breakpoints,
23394 &abs_path,
23395 vec![
23396 (0, Breakpoint::new_standard()),
23397 (3, Breakpoint::new_log("hello world")),
23398 ],
23399 );
23400
23401 editor.update_in(cx, |editor, window, cx| {
23402 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23403 });
23404
23405 let breakpoints = editor.update(cx, |editor, cx| {
23406 editor
23407 .breakpoint_store()
23408 .as_ref()
23409 .unwrap()
23410 .read(cx)
23411 .all_source_breakpoints(cx)
23412 });
23413
23414 assert_breakpoint(
23415 &breakpoints,
23416 &abs_path,
23417 vec![
23418 (0, Breakpoint::new_standard()),
23419 (3, Breakpoint::new_log("hello Earth!!")),
23420 ],
23421 );
23422}
23423
23424/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23425/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23426/// or when breakpoints were placed out of order. This tests for a regression too
23427#[gpui::test]
23428async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23429 init_test(cx, |_| {});
23430
23431 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23432 let fs = FakeFs::new(cx.executor());
23433 fs.insert_tree(
23434 path!("/a"),
23435 json!({
23436 "main.rs": sample_text,
23437 }),
23438 )
23439 .await;
23440 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23441 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23442 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23443
23444 let fs = FakeFs::new(cx.executor());
23445 fs.insert_tree(
23446 path!("/a"),
23447 json!({
23448 "main.rs": sample_text,
23449 }),
23450 )
23451 .await;
23452 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23453 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23454 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23455 let worktree_id = workspace
23456 .update(cx, |workspace, _window, cx| {
23457 workspace.project().update(cx, |project, cx| {
23458 project.worktrees(cx).next().unwrap().read(cx).id()
23459 })
23460 })
23461 .unwrap();
23462
23463 let buffer = project
23464 .update(cx, |project, cx| {
23465 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23466 })
23467 .await
23468 .unwrap();
23469
23470 let (editor, cx) = cx.add_window_view(|window, cx| {
23471 Editor::new(
23472 EditorMode::full(),
23473 MultiBuffer::build_from_buffer(buffer, cx),
23474 Some(project.clone()),
23475 window,
23476 cx,
23477 )
23478 });
23479
23480 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23481 let abs_path = project.read_with(cx, |project, cx| {
23482 project
23483 .absolute_path(&project_path, cx)
23484 .map(Arc::from)
23485 .unwrap()
23486 });
23487
23488 // assert we can add breakpoint on the first line
23489 editor.update_in(cx, |editor, window, cx| {
23490 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23491 editor.move_to_end(&MoveToEnd, window, cx);
23492 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23493 editor.move_up(&MoveUp, window, cx);
23494 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23495 });
23496
23497 let breakpoints = editor.update(cx, |editor, cx| {
23498 editor
23499 .breakpoint_store()
23500 .as_ref()
23501 .unwrap()
23502 .read(cx)
23503 .all_source_breakpoints(cx)
23504 });
23505
23506 assert_eq!(1, breakpoints.len());
23507 assert_breakpoint(
23508 &breakpoints,
23509 &abs_path,
23510 vec![
23511 (0, Breakpoint::new_standard()),
23512 (2, Breakpoint::new_standard()),
23513 (3, Breakpoint::new_standard()),
23514 ],
23515 );
23516
23517 editor.update_in(cx, |editor, window, cx| {
23518 editor.move_to_beginning(&MoveToBeginning, window, cx);
23519 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23520 editor.move_to_end(&MoveToEnd, window, cx);
23521 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23522 // Disabling a breakpoint that doesn't exist should do nothing
23523 editor.move_up(&MoveUp, window, cx);
23524 editor.move_up(&MoveUp, window, cx);
23525 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23526 });
23527
23528 let breakpoints = editor.update(cx, |editor, cx| {
23529 editor
23530 .breakpoint_store()
23531 .as_ref()
23532 .unwrap()
23533 .read(cx)
23534 .all_source_breakpoints(cx)
23535 });
23536
23537 let disable_breakpoint = {
23538 let mut bp = Breakpoint::new_standard();
23539 bp.state = BreakpointState::Disabled;
23540 bp
23541 };
23542
23543 assert_eq!(1, breakpoints.len());
23544 assert_breakpoint(
23545 &breakpoints,
23546 &abs_path,
23547 vec![
23548 (0, disable_breakpoint.clone()),
23549 (2, Breakpoint::new_standard()),
23550 (3, disable_breakpoint.clone()),
23551 ],
23552 );
23553
23554 editor.update_in(cx, |editor, window, cx| {
23555 editor.move_to_beginning(&MoveToBeginning, window, cx);
23556 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23557 editor.move_to_end(&MoveToEnd, window, cx);
23558 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23559 editor.move_up(&MoveUp, window, cx);
23560 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23561 });
23562
23563 let breakpoints = editor.update(cx, |editor, cx| {
23564 editor
23565 .breakpoint_store()
23566 .as_ref()
23567 .unwrap()
23568 .read(cx)
23569 .all_source_breakpoints(cx)
23570 });
23571
23572 assert_eq!(1, breakpoints.len());
23573 assert_breakpoint(
23574 &breakpoints,
23575 &abs_path,
23576 vec![
23577 (0, Breakpoint::new_standard()),
23578 (2, disable_breakpoint),
23579 (3, Breakpoint::new_standard()),
23580 ],
23581 );
23582}
23583
23584#[gpui::test]
23585async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23586 init_test(cx, |_| {});
23587 let capabilities = lsp::ServerCapabilities {
23588 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23589 prepare_provider: Some(true),
23590 work_done_progress_options: Default::default(),
23591 })),
23592 ..Default::default()
23593 };
23594 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23595
23596 cx.set_state(indoc! {"
23597 struct Fˇoo {}
23598 "});
23599
23600 cx.update_editor(|editor, _, cx| {
23601 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23602 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23603 editor.highlight_background::<DocumentHighlightRead>(
23604 &[highlight_range],
23605 |theme| theme.colors().editor_document_highlight_read_background,
23606 cx,
23607 );
23608 });
23609
23610 let mut prepare_rename_handler = cx
23611 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23612 move |_, _, _| async move {
23613 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23614 start: lsp::Position {
23615 line: 0,
23616 character: 7,
23617 },
23618 end: lsp::Position {
23619 line: 0,
23620 character: 10,
23621 },
23622 })))
23623 },
23624 );
23625 let prepare_rename_task = cx
23626 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23627 .expect("Prepare rename was not started");
23628 prepare_rename_handler.next().await.unwrap();
23629 prepare_rename_task.await.expect("Prepare rename failed");
23630
23631 let mut rename_handler =
23632 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23633 let edit = lsp::TextEdit {
23634 range: lsp::Range {
23635 start: lsp::Position {
23636 line: 0,
23637 character: 7,
23638 },
23639 end: lsp::Position {
23640 line: 0,
23641 character: 10,
23642 },
23643 },
23644 new_text: "FooRenamed".to_string(),
23645 };
23646 Ok(Some(lsp::WorkspaceEdit::new(
23647 // Specify the same edit twice
23648 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23649 )))
23650 });
23651 let rename_task = cx
23652 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23653 .expect("Confirm rename was not started");
23654 rename_handler.next().await.unwrap();
23655 rename_task.await.expect("Confirm rename failed");
23656 cx.run_until_parked();
23657
23658 // Despite two edits, only one is actually applied as those are identical
23659 cx.assert_editor_state(indoc! {"
23660 struct FooRenamedˇ {}
23661 "});
23662}
23663
23664#[gpui::test]
23665async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23666 init_test(cx, |_| {});
23667 // These capabilities indicate that the server does not support prepare rename.
23668 let capabilities = lsp::ServerCapabilities {
23669 rename_provider: Some(lsp::OneOf::Left(true)),
23670 ..Default::default()
23671 };
23672 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23673
23674 cx.set_state(indoc! {"
23675 struct Fˇoo {}
23676 "});
23677
23678 cx.update_editor(|editor, _window, cx| {
23679 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23680 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23681 editor.highlight_background::<DocumentHighlightRead>(
23682 &[highlight_range],
23683 |theme| theme.colors().editor_document_highlight_read_background,
23684 cx,
23685 );
23686 });
23687
23688 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23689 .expect("Prepare rename was not started")
23690 .await
23691 .expect("Prepare rename failed");
23692
23693 let mut rename_handler =
23694 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23695 let edit = lsp::TextEdit {
23696 range: lsp::Range {
23697 start: lsp::Position {
23698 line: 0,
23699 character: 7,
23700 },
23701 end: lsp::Position {
23702 line: 0,
23703 character: 10,
23704 },
23705 },
23706 new_text: "FooRenamed".to_string(),
23707 };
23708 Ok(Some(lsp::WorkspaceEdit::new(
23709 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23710 )))
23711 });
23712 let rename_task = cx
23713 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23714 .expect("Confirm rename was not started");
23715 rename_handler.next().await.unwrap();
23716 rename_task.await.expect("Confirm rename failed");
23717 cx.run_until_parked();
23718
23719 // Correct range is renamed, as `surrounding_word` is used to find it.
23720 cx.assert_editor_state(indoc! {"
23721 struct FooRenamedˇ {}
23722 "});
23723}
23724
23725#[gpui::test]
23726async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23727 init_test(cx, |_| {});
23728 let mut cx = EditorTestContext::new(cx).await;
23729
23730 let language = Arc::new(
23731 Language::new(
23732 LanguageConfig::default(),
23733 Some(tree_sitter_html::LANGUAGE.into()),
23734 )
23735 .with_brackets_query(
23736 r#"
23737 ("<" @open "/>" @close)
23738 ("</" @open ">" @close)
23739 ("<" @open ">" @close)
23740 ("\"" @open "\"" @close)
23741 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23742 "#,
23743 )
23744 .unwrap(),
23745 );
23746 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23747
23748 cx.set_state(indoc! {"
23749 <span>ˇ</span>
23750 "});
23751 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23752 cx.assert_editor_state(indoc! {"
23753 <span>
23754 ˇ
23755 </span>
23756 "});
23757
23758 cx.set_state(indoc! {"
23759 <span><span></span>ˇ</span>
23760 "});
23761 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23762 cx.assert_editor_state(indoc! {"
23763 <span><span></span>
23764 ˇ</span>
23765 "});
23766
23767 cx.set_state(indoc! {"
23768 <span>ˇ
23769 </span>
23770 "});
23771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23772 cx.assert_editor_state(indoc! {"
23773 <span>
23774 ˇ
23775 </span>
23776 "});
23777}
23778
23779#[gpui::test(iterations = 10)]
23780async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23781 init_test(cx, |_| {});
23782
23783 let fs = FakeFs::new(cx.executor());
23784 fs.insert_tree(
23785 path!("/dir"),
23786 json!({
23787 "a.ts": "a",
23788 }),
23789 )
23790 .await;
23791
23792 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23793 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23794 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23795
23796 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23797 language_registry.add(Arc::new(Language::new(
23798 LanguageConfig {
23799 name: "TypeScript".into(),
23800 matcher: LanguageMatcher {
23801 path_suffixes: vec!["ts".to_string()],
23802 ..Default::default()
23803 },
23804 ..Default::default()
23805 },
23806 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23807 )));
23808 let mut fake_language_servers = language_registry.register_fake_lsp(
23809 "TypeScript",
23810 FakeLspAdapter {
23811 capabilities: lsp::ServerCapabilities {
23812 code_lens_provider: Some(lsp::CodeLensOptions {
23813 resolve_provider: Some(true),
23814 }),
23815 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23816 commands: vec!["_the/command".to_string()],
23817 ..lsp::ExecuteCommandOptions::default()
23818 }),
23819 ..lsp::ServerCapabilities::default()
23820 },
23821 ..FakeLspAdapter::default()
23822 },
23823 );
23824
23825 let editor = workspace
23826 .update(cx, |workspace, window, cx| {
23827 workspace.open_abs_path(
23828 PathBuf::from(path!("/dir/a.ts")),
23829 OpenOptions::default(),
23830 window,
23831 cx,
23832 )
23833 })
23834 .unwrap()
23835 .await
23836 .unwrap()
23837 .downcast::<Editor>()
23838 .unwrap();
23839 cx.executor().run_until_parked();
23840
23841 let fake_server = fake_language_servers.next().await.unwrap();
23842
23843 let buffer = editor.update(cx, |editor, cx| {
23844 editor
23845 .buffer()
23846 .read(cx)
23847 .as_singleton()
23848 .expect("have opened a single file by path")
23849 });
23850
23851 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23852 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23853 drop(buffer_snapshot);
23854 let actions = cx
23855 .update_window(*workspace, |_, window, cx| {
23856 project.code_actions(&buffer, anchor..anchor, window, cx)
23857 })
23858 .unwrap();
23859
23860 fake_server
23861 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23862 Ok(Some(vec![
23863 lsp::CodeLens {
23864 range: lsp::Range::default(),
23865 command: Some(lsp::Command {
23866 title: "Code lens command".to_owned(),
23867 command: "_the/command".to_owned(),
23868 arguments: None,
23869 }),
23870 data: None,
23871 },
23872 lsp::CodeLens {
23873 range: lsp::Range::default(),
23874 command: Some(lsp::Command {
23875 title: "Command not in capabilities".to_owned(),
23876 command: "not in capabilities".to_owned(),
23877 arguments: None,
23878 }),
23879 data: None,
23880 },
23881 lsp::CodeLens {
23882 range: lsp::Range {
23883 start: lsp::Position {
23884 line: 1,
23885 character: 1,
23886 },
23887 end: lsp::Position {
23888 line: 1,
23889 character: 1,
23890 },
23891 },
23892 command: Some(lsp::Command {
23893 title: "Command not in range".to_owned(),
23894 command: "_the/command".to_owned(),
23895 arguments: None,
23896 }),
23897 data: None,
23898 },
23899 ]))
23900 })
23901 .next()
23902 .await;
23903
23904 let actions = actions.await.unwrap();
23905 assert_eq!(
23906 actions.len(),
23907 1,
23908 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23909 );
23910 let action = actions[0].clone();
23911 let apply = project.update(cx, |project, cx| {
23912 project.apply_code_action(buffer.clone(), action, true, cx)
23913 });
23914
23915 // Resolving the code action does not populate its edits. In absence of
23916 // edits, we must execute the given command.
23917 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23918 |mut lens, _| async move {
23919 let lens_command = lens.command.as_mut().expect("should have a command");
23920 assert_eq!(lens_command.title, "Code lens command");
23921 lens_command.arguments = Some(vec![json!("the-argument")]);
23922 Ok(lens)
23923 },
23924 );
23925
23926 // While executing the command, the language server sends the editor
23927 // a `workspaceEdit` request.
23928 fake_server
23929 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23930 let fake = fake_server.clone();
23931 move |params, _| {
23932 assert_eq!(params.command, "_the/command");
23933 let fake = fake.clone();
23934 async move {
23935 fake.server
23936 .request::<lsp::request::ApplyWorkspaceEdit>(
23937 lsp::ApplyWorkspaceEditParams {
23938 label: None,
23939 edit: lsp::WorkspaceEdit {
23940 changes: Some(
23941 [(
23942 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23943 vec![lsp::TextEdit {
23944 range: lsp::Range::new(
23945 lsp::Position::new(0, 0),
23946 lsp::Position::new(0, 0),
23947 ),
23948 new_text: "X".into(),
23949 }],
23950 )]
23951 .into_iter()
23952 .collect(),
23953 ),
23954 ..lsp::WorkspaceEdit::default()
23955 },
23956 },
23957 )
23958 .await
23959 .into_response()
23960 .unwrap();
23961 Ok(Some(json!(null)))
23962 }
23963 }
23964 })
23965 .next()
23966 .await;
23967
23968 // Applying the code lens command returns a project transaction containing the edits
23969 // sent by the language server in its `workspaceEdit` request.
23970 let transaction = apply.await.unwrap();
23971 assert!(transaction.0.contains_key(&buffer));
23972 buffer.update(cx, |buffer, cx| {
23973 assert_eq!(buffer.text(), "Xa");
23974 buffer.undo(cx);
23975 assert_eq!(buffer.text(), "a");
23976 });
23977
23978 let actions_after_edits = cx
23979 .update_window(*workspace, |_, window, cx| {
23980 project.code_actions(&buffer, anchor..anchor, window, cx)
23981 })
23982 .unwrap()
23983 .await
23984 .unwrap();
23985 assert_eq!(
23986 actions, actions_after_edits,
23987 "For the same selection, same code lens actions should be returned"
23988 );
23989
23990 let _responses =
23991 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23992 panic!("No more code lens requests are expected");
23993 });
23994 editor.update_in(cx, |editor, window, cx| {
23995 editor.select_all(&SelectAll, window, cx);
23996 });
23997 cx.executor().run_until_parked();
23998 let new_actions = cx
23999 .update_window(*workspace, |_, window, cx| {
24000 project.code_actions(&buffer, anchor..anchor, window, cx)
24001 })
24002 .unwrap()
24003 .await
24004 .unwrap();
24005 assert_eq!(
24006 actions, new_actions,
24007 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24008 );
24009}
24010
24011#[gpui::test]
24012async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24013 init_test(cx, |_| {});
24014
24015 let fs = FakeFs::new(cx.executor());
24016 let main_text = r#"fn main() {
24017println!("1");
24018println!("2");
24019println!("3");
24020println!("4");
24021println!("5");
24022}"#;
24023 let lib_text = "mod foo {}";
24024 fs.insert_tree(
24025 path!("/a"),
24026 json!({
24027 "lib.rs": lib_text,
24028 "main.rs": main_text,
24029 }),
24030 )
24031 .await;
24032
24033 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24034 let (workspace, cx) =
24035 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24036 let worktree_id = workspace.update(cx, |workspace, cx| {
24037 workspace.project().update(cx, |project, cx| {
24038 project.worktrees(cx).next().unwrap().read(cx).id()
24039 })
24040 });
24041
24042 let expected_ranges = vec![
24043 Point::new(0, 0)..Point::new(0, 0),
24044 Point::new(1, 0)..Point::new(1, 1),
24045 Point::new(2, 0)..Point::new(2, 2),
24046 Point::new(3, 0)..Point::new(3, 3),
24047 ];
24048
24049 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24050 let editor_1 = workspace
24051 .update_in(cx, |workspace, window, cx| {
24052 workspace.open_path(
24053 (worktree_id, rel_path("main.rs")),
24054 Some(pane_1.downgrade()),
24055 true,
24056 window,
24057 cx,
24058 )
24059 })
24060 .unwrap()
24061 .await
24062 .downcast::<Editor>()
24063 .unwrap();
24064 pane_1.update(cx, |pane, cx| {
24065 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24066 open_editor.update(cx, |editor, cx| {
24067 assert_eq!(
24068 editor.display_text(cx),
24069 main_text,
24070 "Original main.rs text on initial open",
24071 );
24072 assert_eq!(
24073 editor
24074 .selections
24075 .all::<Point>(&editor.display_snapshot(cx))
24076 .into_iter()
24077 .map(|s| s.range())
24078 .collect::<Vec<_>>(),
24079 vec![Point::zero()..Point::zero()],
24080 "Default selections on initial open",
24081 );
24082 })
24083 });
24084 editor_1.update_in(cx, |editor, window, cx| {
24085 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24086 s.select_ranges(expected_ranges.clone());
24087 });
24088 });
24089
24090 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24091 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24092 });
24093 let editor_2 = workspace
24094 .update_in(cx, |workspace, window, cx| {
24095 workspace.open_path(
24096 (worktree_id, rel_path("main.rs")),
24097 Some(pane_2.downgrade()),
24098 true,
24099 window,
24100 cx,
24101 )
24102 })
24103 .unwrap()
24104 .await
24105 .downcast::<Editor>()
24106 .unwrap();
24107 pane_2.update(cx, |pane, cx| {
24108 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24109 open_editor.update(cx, |editor, cx| {
24110 assert_eq!(
24111 editor.display_text(cx),
24112 main_text,
24113 "Original main.rs text on initial open in another panel",
24114 );
24115 assert_eq!(
24116 editor
24117 .selections
24118 .all::<Point>(&editor.display_snapshot(cx))
24119 .into_iter()
24120 .map(|s| s.range())
24121 .collect::<Vec<_>>(),
24122 vec![Point::zero()..Point::zero()],
24123 "Default selections on initial open in another panel",
24124 );
24125 })
24126 });
24127
24128 editor_2.update_in(cx, |editor, window, cx| {
24129 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24130 });
24131
24132 let _other_editor_1 = workspace
24133 .update_in(cx, |workspace, window, cx| {
24134 workspace.open_path(
24135 (worktree_id, rel_path("lib.rs")),
24136 Some(pane_1.downgrade()),
24137 true,
24138 window,
24139 cx,
24140 )
24141 })
24142 .unwrap()
24143 .await
24144 .downcast::<Editor>()
24145 .unwrap();
24146 pane_1
24147 .update_in(cx, |pane, window, cx| {
24148 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24149 })
24150 .await
24151 .unwrap();
24152 drop(editor_1);
24153 pane_1.update(cx, |pane, cx| {
24154 pane.active_item()
24155 .unwrap()
24156 .downcast::<Editor>()
24157 .unwrap()
24158 .update(cx, |editor, cx| {
24159 assert_eq!(
24160 editor.display_text(cx),
24161 lib_text,
24162 "Other file should be open and active",
24163 );
24164 });
24165 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24166 });
24167
24168 let _other_editor_2 = workspace
24169 .update_in(cx, |workspace, window, cx| {
24170 workspace.open_path(
24171 (worktree_id, rel_path("lib.rs")),
24172 Some(pane_2.downgrade()),
24173 true,
24174 window,
24175 cx,
24176 )
24177 })
24178 .unwrap()
24179 .await
24180 .downcast::<Editor>()
24181 .unwrap();
24182 pane_2
24183 .update_in(cx, |pane, window, cx| {
24184 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24185 })
24186 .await
24187 .unwrap();
24188 drop(editor_2);
24189 pane_2.update(cx, |pane, cx| {
24190 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24191 open_editor.update(cx, |editor, cx| {
24192 assert_eq!(
24193 editor.display_text(cx),
24194 lib_text,
24195 "Other file should be open and active in another panel too",
24196 );
24197 });
24198 assert_eq!(
24199 pane.items().count(),
24200 1,
24201 "No other editors should be open in another pane",
24202 );
24203 });
24204
24205 let _editor_1_reopened = workspace
24206 .update_in(cx, |workspace, window, cx| {
24207 workspace.open_path(
24208 (worktree_id, rel_path("main.rs")),
24209 Some(pane_1.downgrade()),
24210 true,
24211 window,
24212 cx,
24213 )
24214 })
24215 .unwrap()
24216 .await
24217 .downcast::<Editor>()
24218 .unwrap();
24219 let _editor_2_reopened = workspace
24220 .update_in(cx, |workspace, window, cx| {
24221 workspace.open_path(
24222 (worktree_id, rel_path("main.rs")),
24223 Some(pane_2.downgrade()),
24224 true,
24225 window,
24226 cx,
24227 )
24228 })
24229 .unwrap()
24230 .await
24231 .downcast::<Editor>()
24232 .unwrap();
24233 pane_1.update(cx, |pane, cx| {
24234 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24235 open_editor.update(cx, |editor, cx| {
24236 assert_eq!(
24237 editor.display_text(cx),
24238 main_text,
24239 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24240 );
24241 assert_eq!(
24242 editor
24243 .selections
24244 .all::<Point>(&editor.display_snapshot(cx))
24245 .into_iter()
24246 .map(|s| s.range())
24247 .collect::<Vec<_>>(),
24248 expected_ranges,
24249 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24250 );
24251 })
24252 });
24253 pane_2.update(cx, |pane, cx| {
24254 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24255 open_editor.update(cx, |editor, cx| {
24256 assert_eq!(
24257 editor.display_text(cx),
24258 r#"fn main() {
24259⋯rintln!("1");
24260⋯intln!("2");
24261⋯ntln!("3");
24262println!("4");
24263println!("5");
24264}"#,
24265 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24266 );
24267 assert_eq!(
24268 editor
24269 .selections
24270 .all::<Point>(&editor.display_snapshot(cx))
24271 .into_iter()
24272 .map(|s| s.range())
24273 .collect::<Vec<_>>(),
24274 vec![Point::zero()..Point::zero()],
24275 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24276 );
24277 })
24278 });
24279}
24280
24281#[gpui::test]
24282async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24283 init_test(cx, |_| {});
24284
24285 let fs = FakeFs::new(cx.executor());
24286 let main_text = r#"fn main() {
24287println!("1");
24288println!("2");
24289println!("3");
24290println!("4");
24291println!("5");
24292}"#;
24293 let lib_text = "mod foo {}";
24294 fs.insert_tree(
24295 path!("/a"),
24296 json!({
24297 "lib.rs": lib_text,
24298 "main.rs": main_text,
24299 }),
24300 )
24301 .await;
24302
24303 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24304 let (workspace, cx) =
24305 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24306 let worktree_id = workspace.update(cx, |workspace, cx| {
24307 workspace.project().update(cx, |project, cx| {
24308 project.worktrees(cx).next().unwrap().read(cx).id()
24309 })
24310 });
24311
24312 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24313 let editor = workspace
24314 .update_in(cx, |workspace, window, cx| {
24315 workspace.open_path(
24316 (worktree_id, rel_path("main.rs")),
24317 Some(pane.downgrade()),
24318 true,
24319 window,
24320 cx,
24321 )
24322 })
24323 .unwrap()
24324 .await
24325 .downcast::<Editor>()
24326 .unwrap();
24327 pane.update(cx, |pane, cx| {
24328 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24329 open_editor.update(cx, |editor, cx| {
24330 assert_eq!(
24331 editor.display_text(cx),
24332 main_text,
24333 "Original main.rs text on initial open",
24334 );
24335 })
24336 });
24337 editor.update_in(cx, |editor, window, cx| {
24338 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24339 });
24340
24341 cx.update_global(|store: &mut SettingsStore, cx| {
24342 store.update_user_settings(cx, |s| {
24343 s.workspace.restore_on_file_reopen = Some(false);
24344 });
24345 });
24346 editor.update_in(cx, |editor, window, cx| {
24347 editor.fold_ranges(
24348 vec![
24349 Point::new(1, 0)..Point::new(1, 1),
24350 Point::new(2, 0)..Point::new(2, 2),
24351 Point::new(3, 0)..Point::new(3, 3),
24352 ],
24353 false,
24354 window,
24355 cx,
24356 );
24357 });
24358 pane.update_in(cx, |pane, window, cx| {
24359 pane.close_all_items(&CloseAllItems::default(), window, cx)
24360 })
24361 .await
24362 .unwrap();
24363 pane.update(cx, |pane, _| {
24364 assert!(pane.active_item().is_none());
24365 });
24366 cx.update_global(|store: &mut SettingsStore, cx| {
24367 store.update_user_settings(cx, |s| {
24368 s.workspace.restore_on_file_reopen = Some(true);
24369 });
24370 });
24371
24372 let _editor_reopened = workspace
24373 .update_in(cx, |workspace, window, cx| {
24374 workspace.open_path(
24375 (worktree_id, rel_path("main.rs")),
24376 Some(pane.downgrade()),
24377 true,
24378 window,
24379 cx,
24380 )
24381 })
24382 .unwrap()
24383 .await
24384 .downcast::<Editor>()
24385 .unwrap();
24386 pane.update(cx, |pane, cx| {
24387 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24388 open_editor.update(cx, |editor, cx| {
24389 assert_eq!(
24390 editor.display_text(cx),
24391 main_text,
24392 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24393 );
24394 })
24395 });
24396}
24397
24398#[gpui::test]
24399async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24400 struct EmptyModalView {
24401 focus_handle: gpui::FocusHandle,
24402 }
24403 impl EventEmitter<DismissEvent> for EmptyModalView {}
24404 impl Render for EmptyModalView {
24405 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24406 div()
24407 }
24408 }
24409 impl Focusable for EmptyModalView {
24410 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24411 self.focus_handle.clone()
24412 }
24413 }
24414 impl workspace::ModalView for EmptyModalView {}
24415 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24416 EmptyModalView {
24417 focus_handle: cx.focus_handle(),
24418 }
24419 }
24420
24421 init_test(cx, |_| {});
24422
24423 let fs = FakeFs::new(cx.executor());
24424 let project = Project::test(fs, [], cx).await;
24425 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24426 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24427 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24428 let editor = cx.new_window_entity(|window, cx| {
24429 Editor::new(
24430 EditorMode::full(),
24431 buffer,
24432 Some(project.clone()),
24433 window,
24434 cx,
24435 )
24436 });
24437 workspace
24438 .update(cx, |workspace, window, cx| {
24439 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24440 })
24441 .unwrap();
24442 editor.update_in(cx, |editor, window, cx| {
24443 editor.open_context_menu(&OpenContextMenu, window, cx);
24444 assert!(editor.mouse_context_menu.is_some());
24445 });
24446 workspace
24447 .update(cx, |workspace, window, cx| {
24448 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24449 })
24450 .unwrap();
24451 cx.read(|cx| {
24452 assert!(editor.read(cx).mouse_context_menu.is_none());
24453 });
24454}
24455
24456fn set_linked_edit_ranges(
24457 opening: (Point, Point),
24458 closing: (Point, Point),
24459 editor: &mut Editor,
24460 cx: &mut Context<Editor>,
24461) {
24462 let Some((buffer, _)) = editor
24463 .buffer
24464 .read(cx)
24465 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24466 else {
24467 panic!("Failed to get buffer for selection position");
24468 };
24469 let buffer = buffer.read(cx);
24470 let buffer_id = buffer.remote_id();
24471 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24472 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24473 let mut linked_ranges = HashMap::default();
24474 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24475 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24476}
24477
24478#[gpui::test]
24479async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24480 init_test(cx, |_| {});
24481
24482 let fs = FakeFs::new(cx.executor());
24483 fs.insert_file(path!("/file.html"), Default::default())
24484 .await;
24485
24486 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24487
24488 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24489 let html_language = Arc::new(Language::new(
24490 LanguageConfig {
24491 name: "HTML".into(),
24492 matcher: LanguageMatcher {
24493 path_suffixes: vec!["html".to_string()],
24494 ..LanguageMatcher::default()
24495 },
24496 brackets: BracketPairConfig {
24497 pairs: vec![BracketPair {
24498 start: "<".into(),
24499 end: ">".into(),
24500 close: true,
24501 ..Default::default()
24502 }],
24503 ..Default::default()
24504 },
24505 ..Default::default()
24506 },
24507 Some(tree_sitter_html::LANGUAGE.into()),
24508 ));
24509 language_registry.add(html_language);
24510 let mut fake_servers = language_registry.register_fake_lsp(
24511 "HTML",
24512 FakeLspAdapter {
24513 capabilities: lsp::ServerCapabilities {
24514 completion_provider: Some(lsp::CompletionOptions {
24515 resolve_provider: Some(true),
24516 ..Default::default()
24517 }),
24518 ..Default::default()
24519 },
24520 ..Default::default()
24521 },
24522 );
24523
24524 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24525 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24526
24527 let worktree_id = workspace
24528 .update(cx, |workspace, _window, cx| {
24529 workspace.project().update(cx, |project, cx| {
24530 project.worktrees(cx).next().unwrap().read(cx).id()
24531 })
24532 })
24533 .unwrap();
24534 project
24535 .update(cx, |project, cx| {
24536 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24537 })
24538 .await
24539 .unwrap();
24540 let editor = workspace
24541 .update(cx, |workspace, window, cx| {
24542 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24543 })
24544 .unwrap()
24545 .await
24546 .unwrap()
24547 .downcast::<Editor>()
24548 .unwrap();
24549
24550 let fake_server = fake_servers.next().await.unwrap();
24551 editor.update_in(cx, |editor, window, cx| {
24552 editor.set_text("<ad></ad>", window, cx);
24553 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24554 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24555 });
24556 set_linked_edit_ranges(
24557 (Point::new(0, 1), Point::new(0, 3)),
24558 (Point::new(0, 6), Point::new(0, 8)),
24559 editor,
24560 cx,
24561 );
24562 });
24563 let mut completion_handle =
24564 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24565 Ok(Some(lsp::CompletionResponse::Array(vec![
24566 lsp::CompletionItem {
24567 label: "head".to_string(),
24568 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24569 lsp::InsertReplaceEdit {
24570 new_text: "head".to_string(),
24571 insert: lsp::Range::new(
24572 lsp::Position::new(0, 1),
24573 lsp::Position::new(0, 3),
24574 ),
24575 replace: lsp::Range::new(
24576 lsp::Position::new(0, 1),
24577 lsp::Position::new(0, 3),
24578 ),
24579 },
24580 )),
24581 ..Default::default()
24582 },
24583 ])))
24584 });
24585 editor.update_in(cx, |editor, window, cx| {
24586 editor.show_completions(&ShowCompletions, window, cx);
24587 });
24588 cx.run_until_parked();
24589 completion_handle.next().await.unwrap();
24590 editor.update(cx, |editor, _| {
24591 assert!(
24592 editor.context_menu_visible(),
24593 "Completion menu should be visible"
24594 );
24595 });
24596 editor.update_in(cx, |editor, window, cx| {
24597 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24598 });
24599 cx.executor().run_until_parked();
24600 editor.update(cx, |editor, cx| {
24601 assert_eq!(editor.text(cx), "<head></head>");
24602 });
24603}
24604
24605#[gpui::test]
24606async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24607 init_test(cx, |_| {});
24608
24609 let mut cx = EditorTestContext::new(cx).await;
24610 let language = Arc::new(Language::new(
24611 LanguageConfig {
24612 name: "TSX".into(),
24613 matcher: LanguageMatcher {
24614 path_suffixes: vec!["tsx".to_string()],
24615 ..LanguageMatcher::default()
24616 },
24617 brackets: BracketPairConfig {
24618 pairs: vec![BracketPair {
24619 start: "<".into(),
24620 end: ">".into(),
24621 close: true,
24622 ..Default::default()
24623 }],
24624 ..Default::default()
24625 },
24626 linked_edit_characters: HashSet::from_iter(['.']),
24627 ..Default::default()
24628 },
24629 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24630 ));
24631 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24632
24633 // Test typing > does not extend linked pair
24634 cx.set_state("<divˇ<div></div>");
24635 cx.update_editor(|editor, _, cx| {
24636 set_linked_edit_ranges(
24637 (Point::new(0, 1), Point::new(0, 4)),
24638 (Point::new(0, 11), Point::new(0, 14)),
24639 editor,
24640 cx,
24641 );
24642 });
24643 cx.update_editor(|editor, window, cx| {
24644 editor.handle_input(">", window, cx);
24645 });
24646 cx.assert_editor_state("<div>ˇ<div></div>");
24647
24648 // Test typing . do extend linked pair
24649 cx.set_state("<Animatedˇ></Animated>");
24650 cx.update_editor(|editor, _, cx| {
24651 set_linked_edit_ranges(
24652 (Point::new(0, 1), Point::new(0, 9)),
24653 (Point::new(0, 12), Point::new(0, 20)),
24654 editor,
24655 cx,
24656 );
24657 });
24658 cx.update_editor(|editor, window, cx| {
24659 editor.handle_input(".", window, cx);
24660 });
24661 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24662 cx.update_editor(|editor, _, cx| {
24663 set_linked_edit_ranges(
24664 (Point::new(0, 1), Point::new(0, 10)),
24665 (Point::new(0, 13), Point::new(0, 21)),
24666 editor,
24667 cx,
24668 );
24669 });
24670 cx.update_editor(|editor, window, cx| {
24671 editor.handle_input("V", window, cx);
24672 });
24673 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24674}
24675
24676#[gpui::test]
24677async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24678 init_test(cx, |_| {});
24679
24680 let fs = FakeFs::new(cx.executor());
24681 fs.insert_tree(
24682 path!("/root"),
24683 json!({
24684 "a": {
24685 "main.rs": "fn main() {}",
24686 },
24687 "foo": {
24688 "bar": {
24689 "external_file.rs": "pub mod external {}",
24690 }
24691 }
24692 }),
24693 )
24694 .await;
24695
24696 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24697 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24698 language_registry.add(rust_lang());
24699 let _fake_servers = language_registry.register_fake_lsp(
24700 "Rust",
24701 FakeLspAdapter {
24702 ..FakeLspAdapter::default()
24703 },
24704 );
24705 let (workspace, cx) =
24706 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24707 let worktree_id = workspace.update(cx, |workspace, cx| {
24708 workspace.project().update(cx, |project, cx| {
24709 project.worktrees(cx).next().unwrap().read(cx).id()
24710 })
24711 });
24712
24713 let assert_language_servers_count =
24714 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24715 project.update(cx, |project, cx| {
24716 let current = project
24717 .lsp_store()
24718 .read(cx)
24719 .as_local()
24720 .unwrap()
24721 .language_servers
24722 .len();
24723 assert_eq!(expected, current, "{context}");
24724 });
24725 };
24726
24727 assert_language_servers_count(
24728 0,
24729 "No servers should be running before any file is open",
24730 cx,
24731 );
24732 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24733 let main_editor = workspace
24734 .update_in(cx, |workspace, window, cx| {
24735 workspace.open_path(
24736 (worktree_id, rel_path("main.rs")),
24737 Some(pane.downgrade()),
24738 true,
24739 window,
24740 cx,
24741 )
24742 })
24743 .unwrap()
24744 .await
24745 .downcast::<Editor>()
24746 .unwrap();
24747 pane.update(cx, |pane, cx| {
24748 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24749 open_editor.update(cx, |editor, cx| {
24750 assert_eq!(
24751 editor.display_text(cx),
24752 "fn main() {}",
24753 "Original main.rs text on initial open",
24754 );
24755 });
24756 assert_eq!(open_editor, main_editor);
24757 });
24758 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24759
24760 let external_editor = workspace
24761 .update_in(cx, |workspace, window, cx| {
24762 workspace.open_abs_path(
24763 PathBuf::from("/root/foo/bar/external_file.rs"),
24764 OpenOptions::default(),
24765 window,
24766 cx,
24767 )
24768 })
24769 .await
24770 .expect("opening external file")
24771 .downcast::<Editor>()
24772 .expect("downcasted external file's open element to editor");
24773 pane.update(cx, |pane, cx| {
24774 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24775 open_editor.update(cx, |editor, cx| {
24776 assert_eq!(
24777 editor.display_text(cx),
24778 "pub mod external {}",
24779 "External file is open now",
24780 );
24781 });
24782 assert_eq!(open_editor, external_editor);
24783 });
24784 assert_language_servers_count(
24785 1,
24786 "Second, external, *.rs file should join the existing server",
24787 cx,
24788 );
24789
24790 pane.update_in(cx, |pane, window, cx| {
24791 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24792 })
24793 .await
24794 .unwrap();
24795 pane.update_in(cx, |pane, window, cx| {
24796 pane.navigate_backward(&Default::default(), window, cx);
24797 });
24798 cx.run_until_parked();
24799 pane.update(cx, |pane, cx| {
24800 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24801 open_editor.update(cx, |editor, cx| {
24802 assert_eq!(
24803 editor.display_text(cx),
24804 "pub mod external {}",
24805 "External file is open now",
24806 );
24807 });
24808 });
24809 assert_language_servers_count(
24810 1,
24811 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24812 cx,
24813 );
24814
24815 cx.update(|_, cx| {
24816 workspace::reload(cx);
24817 });
24818 assert_language_servers_count(
24819 1,
24820 "After reloading the worktree with local and external files opened, only one project should be started",
24821 cx,
24822 );
24823}
24824
24825#[gpui::test]
24826async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24827 init_test(cx, |_| {});
24828
24829 let mut cx = EditorTestContext::new(cx).await;
24830 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24831 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24832
24833 // test cursor move to start of each line on tab
24834 // for `if`, `elif`, `else`, `while`, `with` and `for`
24835 cx.set_state(indoc! {"
24836 def main():
24837 ˇ for item in items:
24838 ˇ while item.active:
24839 ˇ if item.value > 10:
24840 ˇ continue
24841 ˇ elif item.value < 0:
24842 ˇ break
24843 ˇ else:
24844 ˇ with item.context() as ctx:
24845 ˇ yield count
24846 ˇ else:
24847 ˇ log('while else')
24848 ˇ else:
24849 ˇ log('for else')
24850 "});
24851 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24852 cx.assert_editor_state(indoc! {"
24853 def main():
24854 ˇfor item in items:
24855 ˇwhile item.active:
24856 ˇif item.value > 10:
24857 ˇcontinue
24858 ˇelif item.value < 0:
24859 ˇbreak
24860 ˇelse:
24861 ˇwith item.context() as ctx:
24862 ˇyield count
24863 ˇelse:
24864 ˇlog('while else')
24865 ˇelse:
24866 ˇlog('for else')
24867 "});
24868 // test relative indent is preserved when tab
24869 // for `if`, `elif`, `else`, `while`, `with` and `for`
24870 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24871 cx.assert_editor_state(indoc! {"
24872 def main():
24873 ˇfor item in items:
24874 ˇwhile item.active:
24875 ˇif item.value > 10:
24876 ˇcontinue
24877 ˇelif item.value < 0:
24878 ˇbreak
24879 ˇelse:
24880 ˇwith item.context() as ctx:
24881 ˇyield count
24882 ˇelse:
24883 ˇlog('while else')
24884 ˇelse:
24885 ˇlog('for else')
24886 "});
24887
24888 // test cursor move to start of each line on tab
24889 // for `try`, `except`, `else`, `finally`, `match` and `def`
24890 cx.set_state(indoc! {"
24891 def main():
24892 ˇ try:
24893 ˇ fetch()
24894 ˇ except ValueError:
24895 ˇ handle_error()
24896 ˇ else:
24897 ˇ match value:
24898 ˇ case _:
24899 ˇ finally:
24900 ˇ def status():
24901 ˇ return 0
24902 "});
24903 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24904 cx.assert_editor_state(indoc! {"
24905 def main():
24906 ˇtry:
24907 ˇfetch()
24908 ˇexcept ValueError:
24909 ˇhandle_error()
24910 ˇelse:
24911 ˇmatch value:
24912 ˇcase _:
24913 ˇfinally:
24914 ˇdef status():
24915 ˇreturn 0
24916 "});
24917 // test relative indent is preserved when tab
24918 // for `try`, `except`, `else`, `finally`, `match` and `def`
24919 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24920 cx.assert_editor_state(indoc! {"
24921 def main():
24922 ˇtry:
24923 ˇfetch()
24924 ˇexcept ValueError:
24925 ˇhandle_error()
24926 ˇelse:
24927 ˇmatch value:
24928 ˇcase _:
24929 ˇfinally:
24930 ˇdef status():
24931 ˇreturn 0
24932 "});
24933}
24934
24935#[gpui::test]
24936async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24937 init_test(cx, |_| {});
24938
24939 let mut cx = EditorTestContext::new(cx).await;
24940 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24941 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24942
24943 // test `else` auto outdents when typed inside `if` block
24944 cx.set_state(indoc! {"
24945 def main():
24946 if i == 2:
24947 return
24948 ˇ
24949 "});
24950 cx.update_editor(|editor, window, cx| {
24951 editor.handle_input("else:", window, cx);
24952 });
24953 cx.assert_editor_state(indoc! {"
24954 def main():
24955 if i == 2:
24956 return
24957 else:ˇ
24958 "});
24959
24960 // test `except` auto outdents when typed inside `try` block
24961 cx.set_state(indoc! {"
24962 def main():
24963 try:
24964 i = 2
24965 ˇ
24966 "});
24967 cx.update_editor(|editor, window, cx| {
24968 editor.handle_input("except:", window, cx);
24969 });
24970 cx.assert_editor_state(indoc! {"
24971 def main():
24972 try:
24973 i = 2
24974 except:ˇ
24975 "});
24976
24977 // test `else` auto outdents when typed inside `except` block
24978 cx.set_state(indoc! {"
24979 def main():
24980 try:
24981 i = 2
24982 except:
24983 j = 2
24984 ˇ
24985 "});
24986 cx.update_editor(|editor, window, cx| {
24987 editor.handle_input("else:", window, cx);
24988 });
24989 cx.assert_editor_state(indoc! {"
24990 def main():
24991 try:
24992 i = 2
24993 except:
24994 j = 2
24995 else:ˇ
24996 "});
24997
24998 // test `finally` auto outdents when typed inside `else` block
24999 cx.set_state(indoc! {"
25000 def main():
25001 try:
25002 i = 2
25003 except:
25004 j = 2
25005 else:
25006 k = 2
25007 ˇ
25008 "});
25009 cx.update_editor(|editor, window, cx| {
25010 editor.handle_input("finally:", window, cx);
25011 });
25012 cx.assert_editor_state(indoc! {"
25013 def main():
25014 try:
25015 i = 2
25016 except:
25017 j = 2
25018 else:
25019 k = 2
25020 finally:ˇ
25021 "});
25022
25023 // test `else` does not outdents when typed inside `except` block right after for block
25024 cx.set_state(indoc! {"
25025 def main():
25026 try:
25027 i = 2
25028 except:
25029 for i in range(n):
25030 pass
25031 ˇ
25032 "});
25033 cx.update_editor(|editor, window, cx| {
25034 editor.handle_input("else:", window, cx);
25035 });
25036 cx.assert_editor_state(indoc! {"
25037 def main():
25038 try:
25039 i = 2
25040 except:
25041 for i in range(n):
25042 pass
25043 else:ˇ
25044 "});
25045
25046 // test `finally` auto outdents when typed inside `else` block right after for block
25047 cx.set_state(indoc! {"
25048 def main():
25049 try:
25050 i = 2
25051 except:
25052 j = 2
25053 else:
25054 for i in range(n):
25055 pass
25056 ˇ
25057 "});
25058 cx.update_editor(|editor, window, cx| {
25059 editor.handle_input("finally:", window, cx);
25060 });
25061 cx.assert_editor_state(indoc! {"
25062 def main():
25063 try:
25064 i = 2
25065 except:
25066 j = 2
25067 else:
25068 for i in range(n):
25069 pass
25070 finally:ˇ
25071 "});
25072
25073 // test `except` outdents to inner "try" block
25074 cx.set_state(indoc! {"
25075 def main():
25076 try:
25077 i = 2
25078 if i == 2:
25079 try:
25080 i = 3
25081 ˇ
25082 "});
25083 cx.update_editor(|editor, window, cx| {
25084 editor.handle_input("except:", window, cx);
25085 });
25086 cx.assert_editor_state(indoc! {"
25087 def main():
25088 try:
25089 i = 2
25090 if i == 2:
25091 try:
25092 i = 3
25093 except:ˇ
25094 "});
25095
25096 // test `except` outdents to outer "try" block
25097 cx.set_state(indoc! {"
25098 def main():
25099 try:
25100 i = 2
25101 if i == 2:
25102 try:
25103 i = 3
25104 ˇ
25105 "});
25106 cx.update_editor(|editor, window, cx| {
25107 editor.handle_input("except:", window, cx);
25108 });
25109 cx.assert_editor_state(indoc! {"
25110 def main():
25111 try:
25112 i = 2
25113 if i == 2:
25114 try:
25115 i = 3
25116 except:ˇ
25117 "});
25118
25119 // test `else` stays at correct indent when typed after `for` block
25120 cx.set_state(indoc! {"
25121 def main():
25122 for i in range(10):
25123 if i == 3:
25124 break
25125 ˇ
25126 "});
25127 cx.update_editor(|editor, window, cx| {
25128 editor.handle_input("else:", window, cx);
25129 });
25130 cx.assert_editor_state(indoc! {"
25131 def main():
25132 for i in range(10):
25133 if i == 3:
25134 break
25135 else:ˇ
25136 "});
25137
25138 // test does not outdent on typing after line with square brackets
25139 cx.set_state(indoc! {"
25140 def f() -> list[str]:
25141 ˇ
25142 "});
25143 cx.update_editor(|editor, window, cx| {
25144 editor.handle_input("a", window, cx);
25145 });
25146 cx.assert_editor_state(indoc! {"
25147 def f() -> list[str]:
25148 aˇ
25149 "});
25150
25151 // test does not outdent on typing : after case keyword
25152 cx.set_state(indoc! {"
25153 match 1:
25154 caseˇ
25155 "});
25156 cx.update_editor(|editor, window, cx| {
25157 editor.handle_input(":", window, cx);
25158 });
25159 cx.assert_editor_state(indoc! {"
25160 match 1:
25161 case:ˇ
25162 "});
25163}
25164
25165#[gpui::test]
25166async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
25167 init_test(cx, |_| {});
25168 update_test_language_settings(cx, |settings| {
25169 settings.defaults.extend_comment_on_newline = Some(false);
25170 });
25171 let mut cx = EditorTestContext::new(cx).await;
25172 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25173 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25174
25175 // test correct indent after newline on comment
25176 cx.set_state(indoc! {"
25177 # COMMENT:ˇ
25178 "});
25179 cx.update_editor(|editor, window, cx| {
25180 editor.newline(&Newline, window, cx);
25181 });
25182 cx.assert_editor_state(indoc! {"
25183 # COMMENT:
25184 ˇ
25185 "});
25186
25187 // test correct indent after newline in brackets
25188 cx.set_state(indoc! {"
25189 {ˇ}
25190 "});
25191 cx.update_editor(|editor, window, cx| {
25192 editor.newline(&Newline, window, cx);
25193 });
25194 cx.run_until_parked();
25195 cx.assert_editor_state(indoc! {"
25196 {
25197 ˇ
25198 }
25199 "});
25200
25201 cx.set_state(indoc! {"
25202 (ˇ)
25203 "});
25204 cx.update_editor(|editor, window, cx| {
25205 editor.newline(&Newline, window, cx);
25206 });
25207 cx.run_until_parked();
25208 cx.assert_editor_state(indoc! {"
25209 (
25210 ˇ
25211 )
25212 "});
25213
25214 // do not indent after empty lists or dictionaries
25215 cx.set_state(indoc! {"
25216 a = []ˇ
25217 "});
25218 cx.update_editor(|editor, window, cx| {
25219 editor.newline(&Newline, window, cx);
25220 });
25221 cx.run_until_parked();
25222 cx.assert_editor_state(indoc! {"
25223 a = []
25224 ˇ
25225 "});
25226}
25227
25228#[gpui::test]
25229async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25230 init_test(cx, |_| {});
25231
25232 let mut cx = EditorTestContext::new(cx).await;
25233 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25235
25236 // test cursor move to start of each line on tab
25237 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25238 cx.set_state(indoc! {"
25239 function main() {
25240 ˇ for item in $items; do
25241 ˇ while [ -n \"$item\" ]; do
25242 ˇ if [ \"$value\" -gt 10 ]; then
25243 ˇ continue
25244 ˇ elif [ \"$value\" -lt 0 ]; then
25245 ˇ break
25246 ˇ else
25247 ˇ echo \"$item\"
25248 ˇ fi
25249 ˇ done
25250 ˇ done
25251 ˇ}
25252 "});
25253 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25254 cx.assert_editor_state(indoc! {"
25255 function main() {
25256 ˇfor item in $items; do
25257 ˇwhile [ -n \"$item\" ]; do
25258 ˇif [ \"$value\" -gt 10 ]; then
25259 ˇcontinue
25260 ˇelif [ \"$value\" -lt 0 ]; then
25261 ˇbreak
25262 ˇelse
25263 ˇecho \"$item\"
25264 ˇfi
25265 ˇdone
25266 ˇdone
25267 ˇ}
25268 "});
25269 // test relative indent is preserved when tab
25270 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25271 cx.assert_editor_state(indoc! {"
25272 function main() {
25273 ˇfor item in $items; do
25274 ˇwhile [ -n \"$item\" ]; do
25275 ˇif [ \"$value\" -gt 10 ]; then
25276 ˇcontinue
25277 ˇelif [ \"$value\" -lt 0 ]; then
25278 ˇbreak
25279 ˇelse
25280 ˇecho \"$item\"
25281 ˇfi
25282 ˇdone
25283 ˇdone
25284 ˇ}
25285 "});
25286
25287 // test cursor move to start of each line on tab
25288 // for `case` statement with patterns
25289 cx.set_state(indoc! {"
25290 function handle() {
25291 ˇ case \"$1\" in
25292 ˇ start)
25293 ˇ echo \"a\"
25294 ˇ ;;
25295 ˇ stop)
25296 ˇ echo \"b\"
25297 ˇ ;;
25298 ˇ *)
25299 ˇ echo \"c\"
25300 ˇ ;;
25301 ˇ esac
25302 ˇ}
25303 "});
25304 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25305 cx.assert_editor_state(indoc! {"
25306 function handle() {
25307 ˇcase \"$1\" in
25308 ˇstart)
25309 ˇecho \"a\"
25310 ˇ;;
25311 ˇstop)
25312 ˇecho \"b\"
25313 ˇ;;
25314 ˇ*)
25315 ˇecho \"c\"
25316 ˇ;;
25317 ˇesac
25318 ˇ}
25319 "});
25320}
25321
25322#[gpui::test]
25323async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25324 init_test(cx, |_| {});
25325
25326 let mut cx = EditorTestContext::new(cx).await;
25327 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25328 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25329
25330 // test indents on comment insert
25331 cx.set_state(indoc! {"
25332 function main() {
25333 ˇ for item in $items; do
25334 ˇ while [ -n \"$item\" ]; do
25335 ˇ if [ \"$value\" -gt 10 ]; then
25336 ˇ continue
25337 ˇ elif [ \"$value\" -lt 0 ]; then
25338 ˇ break
25339 ˇ else
25340 ˇ echo \"$item\"
25341 ˇ fi
25342 ˇ done
25343 ˇ done
25344 ˇ}
25345 "});
25346 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25347 cx.assert_editor_state(indoc! {"
25348 function main() {
25349 #ˇ for item in $items; do
25350 #ˇ while [ -n \"$item\" ]; do
25351 #ˇ if [ \"$value\" -gt 10 ]; then
25352 #ˇ continue
25353 #ˇ elif [ \"$value\" -lt 0 ]; then
25354 #ˇ break
25355 #ˇ else
25356 #ˇ echo \"$item\"
25357 #ˇ fi
25358 #ˇ done
25359 #ˇ done
25360 #ˇ}
25361 "});
25362}
25363
25364#[gpui::test]
25365async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25366 init_test(cx, |_| {});
25367
25368 let mut cx = EditorTestContext::new(cx).await;
25369 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25371
25372 // test `else` auto outdents when typed inside `if` block
25373 cx.set_state(indoc! {"
25374 if [ \"$1\" = \"test\" ]; then
25375 echo \"foo bar\"
25376 ˇ
25377 "});
25378 cx.update_editor(|editor, window, cx| {
25379 editor.handle_input("else", window, cx);
25380 });
25381 cx.assert_editor_state(indoc! {"
25382 if [ \"$1\" = \"test\" ]; then
25383 echo \"foo bar\"
25384 elseˇ
25385 "});
25386
25387 // test `elif` auto outdents when typed inside `if` block
25388 cx.set_state(indoc! {"
25389 if [ \"$1\" = \"test\" ]; then
25390 echo \"foo bar\"
25391 ˇ
25392 "});
25393 cx.update_editor(|editor, window, cx| {
25394 editor.handle_input("elif", window, cx);
25395 });
25396 cx.assert_editor_state(indoc! {"
25397 if [ \"$1\" = \"test\" ]; then
25398 echo \"foo bar\"
25399 elifˇ
25400 "});
25401
25402 // test `fi` auto outdents when typed inside `else` block
25403 cx.set_state(indoc! {"
25404 if [ \"$1\" = \"test\" ]; then
25405 echo \"foo bar\"
25406 else
25407 echo \"bar baz\"
25408 ˇ
25409 "});
25410 cx.update_editor(|editor, window, cx| {
25411 editor.handle_input("fi", window, cx);
25412 });
25413 cx.assert_editor_state(indoc! {"
25414 if [ \"$1\" = \"test\" ]; then
25415 echo \"foo bar\"
25416 else
25417 echo \"bar baz\"
25418 fiˇ
25419 "});
25420
25421 // test `done` auto outdents when typed inside `while` block
25422 cx.set_state(indoc! {"
25423 while read line; do
25424 echo \"$line\"
25425 ˇ
25426 "});
25427 cx.update_editor(|editor, window, cx| {
25428 editor.handle_input("done", window, cx);
25429 });
25430 cx.assert_editor_state(indoc! {"
25431 while read line; do
25432 echo \"$line\"
25433 doneˇ
25434 "});
25435
25436 // test `done` auto outdents when typed inside `for` block
25437 cx.set_state(indoc! {"
25438 for file in *.txt; do
25439 cat \"$file\"
25440 ˇ
25441 "});
25442 cx.update_editor(|editor, window, cx| {
25443 editor.handle_input("done", window, cx);
25444 });
25445 cx.assert_editor_state(indoc! {"
25446 for file in *.txt; do
25447 cat \"$file\"
25448 doneˇ
25449 "});
25450
25451 // test `esac` auto outdents when typed inside `case` block
25452 cx.set_state(indoc! {"
25453 case \"$1\" in
25454 start)
25455 echo \"foo bar\"
25456 ;;
25457 stop)
25458 echo \"bar baz\"
25459 ;;
25460 ˇ
25461 "});
25462 cx.update_editor(|editor, window, cx| {
25463 editor.handle_input("esac", window, cx);
25464 });
25465 cx.assert_editor_state(indoc! {"
25466 case \"$1\" in
25467 start)
25468 echo \"foo bar\"
25469 ;;
25470 stop)
25471 echo \"bar baz\"
25472 ;;
25473 esacˇ
25474 "});
25475
25476 // test `*)` auto outdents when typed inside `case` block
25477 cx.set_state(indoc! {"
25478 case \"$1\" in
25479 start)
25480 echo \"foo bar\"
25481 ;;
25482 ˇ
25483 "});
25484 cx.update_editor(|editor, window, cx| {
25485 editor.handle_input("*)", window, cx);
25486 });
25487 cx.assert_editor_state(indoc! {"
25488 case \"$1\" in
25489 start)
25490 echo \"foo bar\"
25491 ;;
25492 *)ˇ
25493 "});
25494
25495 // test `fi` outdents to correct level with nested if blocks
25496 cx.set_state(indoc! {"
25497 if [ \"$1\" = \"test\" ]; then
25498 echo \"outer if\"
25499 if [ \"$2\" = \"debug\" ]; then
25500 echo \"inner if\"
25501 ˇ
25502 "});
25503 cx.update_editor(|editor, window, cx| {
25504 editor.handle_input("fi", window, cx);
25505 });
25506 cx.assert_editor_state(indoc! {"
25507 if [ \"$1\" = \"test\" ]; then
25508 echo \"outer if\"
25509 if [ \"$2\" = \"debug\" ]; then
25510 echo \"inner if\"
25511 fiˇ
25512 "});
25513}
25514
25515#[gpui::test]
25516async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25517 init_test(cx, |_| {});
25518 update_test_language_settings(cx, |settings| {
25519 settings.defaults.extend_comment_on_newline = Some(false);
25520 });
25521 let mut cx = EditorTestContext::new(cx).await;
25522 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25523 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25524
25525 // test correct indent after newline on comment
25526 cx.set_state(indoc! {"
25527 # COMMENT:ˇ
25528 "});
25529 cx.update_editor(|editor, window, cx| {
25530 editor.newline(&Newline, window, cx);
25531 });
25532 cx.assert_editor_state(indoc! {"
25533 # COMMENT:
25534 ˇ
25535 "});
25536
25537 // test correct indent after newline after `then`
25538 cx.set_state(indoc! {"
25539
25540 if [ \"$1\" = \"test\" ]; thenˇ
25541 "});
25542 cx.update_editor(|editor, window, cx| {
25543 editor.newline(&Newline, window, cx);
25544 });
25545 cx.run_until_parked();
25546 cx.assert_editor_state(indoc! {"
25547
25548 if [ \"$1\" = \"test\" ]; then
25549 ˇ
25550 "});
25551
25552 // test correct indent after newline after `else`
25553 cx.set_state(indoc! {"
25554 if [ \"$1\" = \"test\" ]; then
25555 elseˇ
25556 "});
25557 cx.update_editor(|editor, window, cx| {
25558 editor.newline(&Newline, window, cx);
25559 });
25560 cx.run_until_parked();
25561 cx.assert_editor_state(indoc! {"
25562 if [ \"$1\" = \"test\" ]; then
25563 else
25564 ˇ
25565 "});
25566
25567 // test correct indent after newline after `elif`
25568 cx.set_state(indoc! {"
25569 if [ \"$1\" = \"test\" ]; then
25570 elifˇ
25571 "});
25572 cx.update_editor(|editor, window, cx| {
25573 editor.newline(&Newline, window, cx);
25574 });
25575 cx.run_until_parked();
25576 cx.assert_editor_state(indoc! {"
25577 if [ \"$1\" = \"test\" ]; then
25578 elif
25579 ˇ
25580 "});
25581
25582 // test correct indent after newline after `do`
25583 cx.set_state(indoc! {"
25584 for file in *.txt; doˇ
25585 "});
25586 cx.update_editor(|editor, window, cx| {
25587 editor.newline(&Newline, window, cx);
25588 });
25589 cx.run_until_parked();
25590 cx.assert_editor_state(indoc! {"
25591 for file in *.txt; do
25592 ˇ
25593 "});
25594
25595 // test correct indent after newline after case pattern
25596 cx.set_state(indoc! {"
25597 case \"$1\" in
25598 start)ˇ
25599 "});
25600 cx.update_editor(|editor, window, cx| {
25601 editor.newline(&Newline, window, cx);
25602 });
25603 cx.run_until_parked();
25604 cx.assert_editor_state(indoc! {"
25605 case \"$1\" in
25606 start)
25607 ˇ
25608 "});
25609
25610 // test correct indent after newline after case pattern
25611 cx.set_state(indoc! {"
25612 case \"$1\" in
25613 start)
25614 ;;
25615 *)ˇ
25616 "});
25617 cx.update_editor(|editor, window, cx| {
25618 editor.newline(&Newline, window, cx);
25619 });
25620 cx.run_until_parked();
25621 cx.assert_editor_state(indoc! {"
25622 case \"$1\" in
25623 start)
25624 ;;
25625 *)
25626 ˇ
25627 "});
25628
25629 // test correct indent after newline after function opening brace
25630 cx.set_state(indoc! {"
25631 function test() {ˇ}
25632 "});
25633 cx.update_editor(|editor, window, cx| {
25634 editor.newline(&Newline, window, cx);
25635 });
25636 cx.run_until_parked();
25637 cx.assert_editor_state(indoc! {"
25638 function test() {
25639 ˇ
25640 }
25641 "});
25642
25643 // test no extra indent after semicolon on same line
25644 cx.set_state(indoc! {"
25645 echo \"test\";ˇ
25646 "});
25647 cx.update_editor(|editor, window, cx| {
25648 editor.newline(&Newline, window, cx);
25649 });
25650 cx.run_until_parked();
25651 cx.assert_editor_state(indoc! {"
25652 echo \"test\";
25653 ˇ
25654 "});
25655}
25656
25657fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25658 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25659 point..point
25660}
25661
25662#[track_caller]
25663fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25664 let (text, ranges) = marked_text_ranges(marked_text, true);
25665 assert_eq!(editor.text(cx), text);
25666 assert_eq!(
25667 editor.selections.ranges(&editor.display_snapshot(cx)),
25668 ranges,
25669 "Assert selections are {}",
25670 marked_text
25671 );
25672}
25673
25674pub fn handle_signature_help_request(
25675 cx: &mut EditorLspTestContext,
25676 mocked_response: lsp::SignatureHelp,
25677) -> impl Future<Output = ()> + use<> {
25678 let mut request =
25679 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25680 let mocked_response = mocked_response.clone();
25681 async move { Ok(Some(mocked_response)) }
25682 });
25683
25684 async move {
25685 request.next().await;
25686 }
25687}
25688
25689#[track_caller]
25690pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25691 cx.update_editor(|editor, _, _| {
25692 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25693 let entries = menu.entries.borrow();
25694 let entries = entries
25695 .iter()
25696 .map(|entry| entry.string.as_str())
25697 .collect::<Vec<_>>();
25698 assert_eq!(entries, expected);
25699 } else {
25700 panic!("Expected completions menu");
25701 }
25702 });
25703}
25704
25705#[gpui::test]
25706async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
25707 init_test(cx, |_| {});
25708 let mut cx = EditorLspTestContext::new_rust(
25709 lsp::ServerCapabilities {
25710 completion_provider: Some(lsp::CompletionOptions {
25711 ..Default::default()
25712 }),
25713 ..Default::default()
25714 },
25715 cx,
25716 )
25717 .await;
25718 cx.lsp
25719 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25720 Ok(Some(lsp::CompletionResponse::Array(vec![
25721 lsp::CompletionItem {
25722 label: "unsafe".into(),
25723 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25724 range: lsp::Range {
25725 start: lsp::Position {
25726 line: 0,
25727 character: 9,
25728 },
25729 end: lsp::Position {
25730 line: 0,
25731 character: 11,
25732 },
25733 },
25734 new_text: "unsafe".to_string(),
25735 })),
25736 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
25737 ..Default::default()
25738 },
25739 ])))
25740 });
25741
25742 cx.update_editor(|editor, _, cx| {
25743 editor.project().unwrap().update(cx, |project, cx| {
25744 project.snippets().update(cx, |snippets, _cx| {
25745 snippets.add_snippet_for_test(
25746 None,
25747 PathBuf::from("test_snippets.json"),
25748 vec![
25749 Arc::new(project::snippet_provider::Snippet {
25750 prefix: vec![
25751 "unlimited word count".to_string(),
25752 "unlimit word count".to_string(),
25753 "unlimited unknown".to_string(),
25754 ],
25755 body: "this is many words".to_string(),
25756 description: Some("description".to_string()),
25757 name: "multi-word snippet test".to_string(),
25758 }),
25759 Arc::new(project::snippet_provider::Snippet {
25760 prefix: vec!["unsnip".to_string(), "@few".to_string()],
25761 body: "fewer words".to_string(),
25762 description: Some("alt description".to_string()),
25763 name: "other name".to_string(),
25764 }),
25765 Arc::new(project::snippet_provider::Snippet {
25766 prefix: vec!["ab aa".to_string()],
25767 body: "abcd".to_string(),
25768 description: None,
25769 name: "alphabet".to_string(),
25770 }),
25771 ],
25772 );
25773 });
25774 })
25775 });
25776
25777 let get_completions = |cx: &mut EditorLspTestContext| {
25778 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
25779 Some(CodeContextMenu::Completions(context_menu)) => {
25780 let entries = context_menu.entries.borrow();
25781 entries
25782 .iter()
25783 .map(|entry| entry.string.clone())
25784 .collect_vec()
25785 }
25786 _ => vec![],
25787 })
25788 };
25789
25790 // snippets:
25791 // @foo
25792 // foo bar
25793 //
25794 // when typing:
25795 //
25796 // when typing:
25797 // - if I type a symbol "open the completions with snippets only"
25798 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
25799 //
25800 // stuff we need:
25801 // - filtering logic change?
25802 // - remember how far back the completion started.
25803
25804 let test_cases: &[(&str, &[&str])] = &[
25805 (
25806 "un",
25807 &[
25808 "unsafe",
25809 "unlimit word count",
25810 "unlimited unknown",
25811 "unlimited word count",
25812 "unsnip",
25813 ],
25814 ),
25815 (
25816 "u ",
25817 &[
25818 "unlimit word count",
25819 "unlimited unknown",
25820 "unlimited word count",
25821 ],
25822 ),
25823 ("u a", &["ab aa", "unsafe"]), // unsAfe
25824 (
25825 "u u",
25826 &[
25827 "unsafe",
25828 "unlimit word count",
25829 "unlimited unknown", // ranked highest among snippets
25830 "unlimited word count",
25831 "unsnip",
25832 ],
25833 ),
25834 ("uw c", &["unlimit word count", "unlimited word count"]),
25835 (
25836 "u w",
25837 &[
25838 "unlimit word count",
25839 "unlimited word count",
25840 "unlimited unknown",
25841 ],
25842 ),
25843 ("u w ", &["unlimit word count", "unlimited word count"]),
25844 (
25845 "u ",
25846 &[
25847 "unlimit word count",
25848 "unlimited unknown",
25849 "unlimited word count",
25850 ],
25851 ),
25852 ("wor", &[]),
25853 ("uf", &["unsafe"]),
25854 ("af", &["unsafe"]),
25855 ("afu", &[]),
25856 (
25857 "ue",
25858 &["unsafe", "unlimited unknown", "unlimited word count"],
25859 ),
25860 ("@", &["@few"]),
25861 ("@few", &["@few"]),
25862 ("@ ", &[]),
25863 ("a@", &["@few"]),
25864 ("a@f", &["@few", "unsafe"]),
25865 ("a@fw", &["@few"]),
25866 ("a", &["ab aa", "unsafe"]),
25867 ("aa", &["ab aa"]),
25868 ("aaa", &["ab aa"]),
25869 ("ab", &["ab aa"]),
25870 ("ab ", &["ab aa"]),
25871 ("ab a", &["ab aa", "unsafe"]),
25872 ("ab ab", &["ab aa"]),
25873 ("ab ab aa", &["ab aa"]),
25874 ];
25875
25876 for &(input_to_simulate, expected_completions) in test_cases {
25877 cx.set_state("fn a() { ˇ }\n");
25878 for c in input_to_simulate.split("") {
25879 cx.simulate_input(c);
25880 cx.run_until_parked();
25881 }
25882 let expected_completions = expected_completions
25883 .iter()
25884 .map(|s| s.to_string())
25885 .collect_vec();
25886 assert_eq!(
25887 get_completions(&mut cx),
25888 expected_completions,
25889 "< actual / expected >, input = {input_to_simulate:?}",
25890 );
25891 }
25892}
25893
25894/// Handle completion request passing a marked string specifying where the completion
25895/// should be triggered from using '|' character, what range should be replaced, and what completions
25896/// should be returned using '<' and '>' to delimit the range.
25897///
25898/// Also see `handle_completion_request_with_insert_and_replace`.
25899#[track_caller]
25900pub fn handle_completion_request(
25901 marked_string: &str,
25902 completions: Vec<&'static str>,
25903 is_incomplete: bool,
25904 counter: Arc<AtomicUsize>,
25905 cx: &mut EditorLspTestContext,
25906) -> impl Future<Output = ()> {
25907 let complete_from_marker: TextRangeMarker = '|'.into();
25908 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25909 let (_, mut marked_ranges) = marked_text_ranges_by(
25910 marked_string,
25911 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25912 );
25913
25914 let complete_from_position =
25915 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25916 let replace_range =
25917 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25918
25919 let mut request =
25920 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25921 let completions = completions.clone();
25922 counter.fetch_add(1, atomic::Ordering::Release);
25923 async move {
25924 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25925 assert_eq!(
25926 params.text_document_position.position,
25927 complete_from_position
25928 );
25929 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25930 is_incomplete,
25931 item_defaults: None,
25932 items: completions
25933 .iter()
25934 .map(|completion_text| lsp::CompletionItem {
25935 label: completion_text.to_string(),
25936 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25937 range: replace_range,
25938 new_text: completion_text.to_string(),
25939 })),
25940 ..Default::default()
25941 })
25942 .collect(),
25943 })))
25944 }
25945 });
25946
25947 async move {
25948 request.next().await;
25949 }
25950}
25951
25952/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25953/// given instead, which also contains an `insert` range.
25954///
25955/// This function uses markers to define ranges:
25956/// - `|` marks the cursor position
25957/// - `<>` marks the replace range
25958/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25959pub fn handle_completion_request_with_insert_and_replace(
25960 cx: &mut EditorLspTestContext,
25961 marked_string: &str,
25962 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25963 counter: Arc<AtomicUsize>,
25964) -> impl Future<Output = ()> {
25965 let complete_from_marker: TextRangeMarker = '|'.into();
25966 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25967 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25968
25969 let (_, mut marked_ranges) = marked_text_ranges_by(
25970 marked_string,
25971 vec![
25972 complete_from_marker.clone(),
25973 replace_range_marker.clone(),
25974 insert_range_marker.clone(),
25975 ],
25976 );
25977
25978 let complete_from_position =
25979 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25980 let replace_range =
25981 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25982
25983 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25984 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25985 _ => lsp::Range {
25986 start: replace_range.start,
25987 end: complete_from_position,
25988 },
25989 };
25990
25991 let mut request =
25992 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25993 let completions = completions.clone();
25994 counter.fetch_add(1, atomic::Ordering::Release);
25995 async move {
25996 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25997 assert_eq!(
25998 params.text_document_position.position, complete_from_position,
25999 "marker `|` position doesn't match",
26000 );
26001 Ok(Some(lsp::CompletionResponse::Array(
26002 completions
26003 .iter()
26004 .map(|(label, new_text)| lsp::CompletionItem {
26005 label: label.to_string(),
26006 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26007 lsp::InsertReplaceEdit {
26008 insert: insert_range,
26009 replace: replace_range,
26010 new_text: new_text.to_string(),
26011 },
26012 )),
26013 ..Default::default()
26014 })
26015 .collect(),
26016 )))
26017 }
26018 });
26019
26020 async move {
26021 request.next().await;
26022 }
26023}
26024
26025fn handle_resolve_completion_request(
26026 cx: &mut EditorLspTestContext,
26027 edits: Option<Vec<(&'static str, &'static str)>>,
26028) -> impl Future<Output = ()> {
26029 let edits = edits.map(|edits| {
26030 edits
26031 .iter()
26032 .map(|(marked_string, new_text)| {
26033 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26034 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
26035 lsp::TextEdit::new(replace_range, new_text.to_string())
26036 })
26037 .collect::<Vec<_>>()
26038 });
26039
26040 let mut request =
26041 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26042 let edits = edits.clone();
26043 async move {
26044 Ok(lsp::CompletionItem {
26045 additional_text_edits: edits,
26046 ..Default::default()
26047 })
26048 }
26049 });
26050
26051 async move {
26052 request.next().await;
26053 }
26054}
26055
26056pub(crate) fn update_test_language_settings(
26057 cx: &mut TestAppContext,
26058 f: impl Fn(&mut AllLanguageSettingsContent),
26059) {
26060 cx.update(|cx| {
26061 SettingsStore::update_global(cx, |store, cx| {
26062 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26063 });
26064 });
26065}
26066
26067pub(crate) fn update_test_project_settings(
26068 cx: &mut TestAppContext,
26069 f: impl Fn(&mut ProjectSettingsContent),
26070) {
26071 cx.update(|cx| {
26072 SettingsStore::update_global(cx, |store, cx| {
26073 store.update_user_settings(cx, |settings| f(&mut settings.project));
26074 });
26075 });
26076}
26077
26078pub(crate) fn update_test_editor_settings(
26079 cx: &mut TestAppContext,
26080 f: impl Fn(&mut EditorSettingsContent),
26081) {
26082 cx.update(|cx| {
26083 SettingsStore::update_global(cx, |store, cx| {
26084 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26085 })
26086 })
26087}
26088
26089pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26090 cx.update(|cx| {
26091 assets::Assets.load_test_fonts(cx);
26092 let store = SettingsStore::test(cx);
26093 cx.set_global(store);
26094 theme::init(theme::LoadThemes::JustBase, cx);
26095 release_channel::init(SemanticVersion::default(), cx);
26096 crate::init(cx);
26097 });
26098 zlog::init_test();
26099 update_test_language_settings(cx, f);
26100}
26101
26102#[track_caller]
26103fn assert_hunk_revert(
26104 not_reverted_text_with_selections: &str,
26105 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
26106 expected_reverted_text_with_selections: &str,
26107 base_text: &str,
26108 cx: &mut EditorLspTestContext,
26109) {
26110 cx.set_state(not_reverted_text_with_selections);
26111 cx.set_head_text(base_text);
26112 cx.executor().run_until_parked();
26113
26114 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
26115 let snapshot = editor.snapshot(window, cx);
26116 let reverted_hunk_statuses = snapshot
26117 .buffer_snapshot()
26118 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
26119 .map(|hunk| hunk.status().kind)
26120 .collect::<Vec<_>>();
26121
26122 editor.git_restore(&Default::default(), window, cx);
26123 reverted_hunk_statuses
26124 });
26125 cx.executor().run_until_parked();
26126 cx.assert_editor_state(expected_reverted_text_with_selections);
26127 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
26128}
26129
26130#[gpui::test(iterations = 10)]
26131async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
26132 init_test(cx, |_| {});
26133
26134 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
26135 let counter = diagnostic_requests.clone();
26136
26137 let fs = FakeFs::new(cx.executor());
26138 fs.insert_tree(
26139 path!("/a"),
26140 json!({
26141 "first.rs": "fn main() { let a = 5; }",
26142 "second.rs": "// Test file",
26143 }),
26144 )
26145 .await;
26146
26147 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26148 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26149 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26150
26151 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26152 language_registry.add(rust_lang());
26153 let mut fake_servers = language_registry.register_fake_lsp(
26154 "Rust",
26155 FakeLspAdapter {
26156 capabilities: lsp::ServerCapabilities {
26157 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
26158 lsp::DiagnosticOptions {
26159 identifier: None,
26160 inter_file_dependencies: true,
26161 workspace_diagnostics: true,
26162 work_done_progress_options: Default::default(),
26163 },
26164 )),
26165 ..Default::default()
26166 },
26167 ..Default::default()
26168 },
26169 );
26170
26171 let editor = workspace
26172 .update(cx, |workspace, window, cx| {
26173 workspace.open_abs_path(
26174 PathBuf::from(path!("/a/first.rs")),
26175 OpenOptions::default(),
26176 window,
26177 cx,
26178 )
26179 })
26180 .unwrap()
26181 .await
26182 .unwrap()
26183 .downcast::<Editor>()
26184 .unwrap();
26185 let fake_server = fake_servers.next().await.unwrap();
26186 let server_id = fake_server.server.server_id();
26187 let mut first_request = fake_server
26188 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
26189 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
26190 let result_id = Some(new_result_id.to_string());
26191 assert_eq!(
26192 params.text_document.uri,
26193 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26194 );
26195 async move {
26196 Ok(lsp::DocumentDiagnosticReportResult::Report(
26197 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
26198 related_documents: None,
26199 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
26200 items: Vec::new(),
26201 result_id,
26202 },
26203 }),
26204 ))
26205 }
26206 });
26207
26208 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
26209 project.update(cx, |project, cx| {
26210 let buffer_id = editor
26211 .read(cx)
26212 .buffer()
26213 .read(cx)
26214 .as_singleton()
26215 .expect("created a singleton buffer")
26216 .read(cx)
26217 .remote_id();
26218 let buffer_result_id = project
26219 .lsp_store()
26220 .read(cx)
26221 .result_id(server_id, buffer_id, cx);
26222 assert_eq!(expected, buffer_result_id);
26223 });
26224 };
26225
26226 ensure_result_id(None, cx);
26227 cx.executor().advance_clock(Duration::from_millis(60));
26228 cx.executor().run_until_parked();
26229 assert_eq!(
26230 diagnostic_requests.load(atomic::Ordering::Acquire),
26231 1,
26232 "Opening file should trigger diagnostic request"
26233 );
26234 first_request
26235 .next()
26236 .await
26237 .expect("should have sent the first diagnostics pull request");
26238 ensure_result_id(Some("1".to_string()), cx);
26239
26240 // Editing should trigger diagnostics
26241 editor.update_in(cx, |editor, window, cx| {
26242 editor.handle_input("2", window, cx)
26243 });
26244 cx.executor().advance_clock(Duration::from_millis(60));
26245 cx.executor().run_until_parked();
26246 assert_eq!(
26247 diagnostic_requests.load(atomic::Ordering::Acquire),
26248 2,
26249 "Editing should trigger diagnostic request"
26250 );
26251 ensure_result_id(Some("2".to_string()), cx);
26252
26253 // Moving cursor should not trigger diagnostic request
26254 editor.update_in(cx, |editor, window, cx| {
26255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26256 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
26257 });
26258 });
26259 cx.executor().advance_clock(Duration::from_millis(60));
26260 cx.executor().run_until_parked();
26261 assert_eq!(
26262 diagnostic_requests.load(atomic::Ordering::Acquire),
26263 2,
26264 "Cursor movement should not trigger diagnostic request"
26265 );
26266 ensure_result_id(Some("2".to_string()), cx);
26267 // Multiple rapid edits should be debounced
26268 for _ in 0..5 {
26269 editor.update_in(cx, |editor, window, cx| {
26270 editor.handle_input("x", window, cx)
26271 });
26272 }
26273 cx.executor().advance_clock(Duration::from_millis(60));
26274 cx.executor().run_until_parked();
26275
26276 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
26277 assert!(
26278 final_requests <= 4,
26279 "Multiple rapid edits should be debounced (got {final_requests} requests)",
26280 );
26281 ensure_result_id(Some(final_requests.to_string()), cx);
26282}
26283
26284#[gpui::test]
26285async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
26286 // Regression test for issue #11671
26287 // Previously, adding a cursor after moving multiple cursors would reset
26288 // the cursor count instead of adding to the existing cursors.
26289 init_test(cx, |_| {});
26290 let mut cx = EditorTestContext::new(cx).await;
26291
26292 // Create a simple buffer with cursor at start
26293 cx.set_state(indoc! {"
26294 ˇaaaa
26295 bbbb
26296 cccc
26297 dddd
26298 eeee
26299 ffff
26300 gggg
26301 hhhh"});
26302
26303 // Add 2 cursors below (so we have 3 total)
26304 cx.update_editor(|editor, window, cx| {
26305 editor.add_selection_below(&Default::default(), window, cx);
26306 editor.add_selection_below(&Default::default(), window, cx);
26307 });
26308
26309 // Verify we have 3 cursors
26310 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
26311 assert_eq!(
26312 initial_count, 3,
26313 "Should have 3 cursors after adding 2 below"
26314 );
26315
26316 // Move down one line
26317 cx.update_editor(|editor, window, cx| {
26318 editor.move_down(&MoveDown, window, cx);
26319 });
26320
26321 // Add another cursor below
26322 cx.update_editor(|editor, window, cx| {
26323 editor.add_selection_below(&Default::default(), window, cx);
26324 });
26325
26326 // Should now have 4 cursors (3 original + 1 new)
26327 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
26328 assert_eq!(
26329 final_count, 4,
26330 "Should have 4 cursors after moving and adding another"
26331 );
26332}
26333
26334#[gpui::test]
26335async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
26336 init_test(cx, |_| {});
26337
26338 let mut cx = EditorTestContext::new(cx).await;
26339
26340 cx.set_state(indoc!(
26341 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
26342 Second line here"#
26343 ));
26344
26345 cx.update_editor(|editor, window, cx| {
26346 // Enable soft wrapping with a narrow width to force soft wrapping and
26347 // confirm that more than 2 rows are being displayed.
26348 editor.set_wrap_width(Some(100.0.into()), cx);
26349 assert!(editor.display_text(cx).lines().count() > 2);
26350
26351 editor.add_selection_below(
26352 &AddSelectionBelow {
26353 skip_soft_wrap: true,
26354 },
26355 window,
26356 cx,
26357 );
26358
26359 assert_eq!(
26360 display_ranges(editor, cx),
26361 &[
26362 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26363 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
26364 ]
26365 );
26366
26367 editor.add_selection_above(
26368 &AddSelectionAbove {
26369 skip_soft_wrap: true,
26370 },
26371 window,
26372 cx,
26373 );
26374
26375 assert_eq!(
26376 display_ranges(editor, cx),
26377 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26378 );
26379
26380 editor.add_selection_below(
26381 &AddSelectionBelow {
26382 skip_soft_wrap: false,
26383 },
26384 window,
26385 cx,
26386 );
26387
26388 assert_eq!(
26389 display_ranges(editor, cx),
26390 &[
26391 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26392 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26393 ]
26394 );
26395
26396 editor.add_selection_above(
26397 &AddSelectionAbove {
26398 skip_soft_wrap: false,
26399 },
26400 window,
26401 cx,
26402 );
26403
26404 assert_eq!(
26405 display_ranges(editor, cx),
26406 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26407 );
26408 });
26409}
26410
26411#[gpui::test(iterations = 10)]
26412async fn test_document_colors(cx: &mut TestAppContext) {
26413 let expected_color = Rgba {
26414 r: 0.33,
26415 g: 0.33,
26416 b: 0.33,
26417 a: 0.33,
26418 };
26419
26420 init_test(cx, |_| {});
26421
26422 let fs = FakeFs::new(cx.executor());
26423 fs.insert_tree(
26424 path!("/a"),
26425 json!({
26426 "first.rs": "fn main() { let a = 5; }",
26427 }),
26428 )
26429 .await;
26430
26431 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26432 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26433 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26434
26435 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26436 language_registry.add(rust_lang());
26437 let mut fake_servers = language_registry.register_fake_lsp(
26438 "Rust",
26439 FakeLspAdapter {
26440 capabilities: lsp::ServerCapabilities {
26441 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26442 ..lsp::ServerCapabilities::default()
26443 },
26444 name: "rust-analyzer",
26445 ..FakeLspAdapter::default()
26446 },
26447 );
26448 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26449 "Rust",
26450 FakeLspAdapter {
26451 capabilities: lsp::ServerCapabilities {
26452 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26453 ..lsp::ServerCapabilities::default()
26454 },
26455 name: "not-rust-analyzer",
26456 ..FakeLspAdapter::default()
26457 },
26458 );
26459
26460 let editor = workspace
26461 .update(cx, |workspace, window, cx| {
26462 workspace.open_abs_path(
26463 PathBuf::from(path!("/a/first.rs")),
26464 OpenOptions::default(),
26465 window,
26466 cx,
26467 )
26468 })
26469 .unwrap()
26470 .await
26471 .unwrap()
26472 .downcast::<Editor>()
26473 .unwrap();
26474 let fake_language_server = fake_servers.next().await.unwrap();
26475 let fake_language_server_without_capabilities =
26476 fake_servers_without_capabilities.next().await.unwrap();
26477 let requests_made = Arc::new(AtomicUsize::new(0));
26478 let closure_requests_made = Arc::clone(&requests_made);
26479 let mut color_request_handle = fake_language_server
26480 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26481 let requests_made = Arc::clone(&closure_requests_made);
26482 async move {
26483 assert_eq!(
26484 params.text_document.uri,
26485 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26486 );
26487 requests_made.fetch_add(1, atomic::Ordering::Release);
26488 Ok(vec![
26489 lsp::ColorInformation {
26490 range: lsp::Range {
26491 start: lsp::Position {
26492 line: 0,
26493 character: 0,
26494 },
26495 end: lsp::Position {
26496 line: 0,
26497 character: 1,
26498 },
26499 },
26500 color: lsp::Color {
26501 red: 0.33,
26502 green: 0.33,
26503 blue: 0.33,
26504 alpha: 0.33,
26505 },
26506 },
26507 lsp::ColorInformation {
26508 range: lsp::Range {
26509 start: lsp::Position {
26510 line: 0,
26511 character: 0,
26512 },
26513 end: lsp::Position {
26514 line: 0,
26515 character: 1,
26516 },
26517 },
26518 color: lsp::Color {
26519 red: 0.33,
26520 green: 0.33,
26521 blue: 0.33,
26522 alpha: 0.33,
26523 },
26524 },
26525 ])
26526 }
26527 });
26528
26529 let _handle = fake_language_server_without_capabilities
26530 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26531 panic!("Should not be called");
26532 });
26533 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26534 color_request_handle.next().await.unwrap();
26535 cx.run_until_parked();
26536 assert_eq!(
26537 1,
26538 requests_made.load(atomic::Ordering::Acquire),
26539 "Should query for colors once per editor open"
26540 );
26541 editor.update_in(cx, |editor, _, cx| {
26542 assert_eq!(
26543 vec![expected_color],
26544 extract_color_inlays(editor, cx),
26545 "Should have an initial inlay"
26546 );
26547 });
26548
26549 // opening another file in a split should not influence the LSP query counter
26550 workspace
26551 .update(cx, |workspace, window, cx| {
26552 assert_eq!(
26553 workspace.panes().len(),
26554 1,
26555 "Should have one pane with one editor"
26556 );
26557 workspace.move_item_to_pane_in_direction(
26558 &MoveItemToPaneInDirection {
26559 direction: SplitDirection::Right,
26560 focus: false,
26561 clone: true,
26562 },
26563 window,
26564 cx,
26565 );
26566 })
26567 .unwrap();
26568 cx.run_until_parked();
26569 workspace
26570 .update(cx, |workspace, _, cx| {
26571 let panes = workspace.panes();
26572 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26573 for pane in panes {
26574 let editor = pane
26575 .read(cx)
26576 .active_item()
26577 .and_then(|item| item.downcast::<Editor>())
26578 .expect("Should have opened an editor in each split");
26579 let editor_file = editor
26580 .read(cx)
26581 .buffer()
26582 .read(cx)
26583 .as_singleton()
26584 .expect("test deals with singleton buffers")
26585 .read(cx)
26586 .file()
26587 .expect("test buffese should have a file")
26588 .path();
26589 assert_eq!(
26590 editor_file.as_ref(),
26591 rel_path("first.rs"),
26592 "Both editors should be opened for the same file"
26593 )
26594 }
26595 })
26596 .unwrap();
26597
26598 cx.executor().advance_clock(Duration::from_millis(500));
26599 let save = editor.update_in(cx, |editor, window, cx| {
26600 editor.move_to_end(&MoveToEnd, window, cx);
26601 editor.handle_input("dirty", window, cx);
26602 editor.save(
26603 SaveOptions {
26604 format: true,
26605 autosave: true,
26606 },
26607 project.clone(),
26608 window,
26609 cx,
26610 )
26611 });
26612 save.await.unwrap();
26613
26614 color_request_handle.next().await.unwrap();
26615 cx.run_until_parked();
26616 assert_eq!(
26617 2,
26618 requests_made.load(atomic::Ordering::Acquire),
26619 "Should query for colors once per save (deduplicated) and once per formatting after save"
26620 );
26621
26622 drop(editor);
26623 let close = workspace
26624 .update(cx, |workspace, window, cx| {
26625 workspace.active_pane().update(cx, |pane, cx| {
26626 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26627 })
26628 })
26629 .unwrap();
26630 close.await.unwrap();
26631 let close = workspace
26632 .update(cx, |workspace, window, cx| {
26633 workspace.active_pane().update(cx, |pane, cx| {
26634 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26635 })
26636 })
26637 .unwrap();
26638 close.await.unwrap();
26639 assert_eq!(
26640 2,
26641 requests_made.load(atomic::Ordering::Acquire),
26642 "After saving and closing all editors, no extra requests should be made"
26643 );
26644 workspace
26645 .update(cx, |workspace, _, cx| {
26646 assert!(
26647 workspace.active_item(cx).is_none(),
26648 "Should close all editors"
26649 )
26650 })
26651 .unwrap();
26652
26653 workspace
26654 .update(cx, |workspace, window, cx| {
26655 workspace.active_pane().update(cx, |pane, cx| {
26656 pane.navigate_backward(&workspace::GoBack, window, cx);
26657 })
26658 })
26659 .unwrap();
26660 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26661 cx.run_until_parked();
26662 let editor = workspace
26663 .update(cx, |workspace, _, cx| {
26664 workspace
26665 .active_item(cx)
26666 .expect("Should have reopened the editor again after navigating back")
26667 .downcast::<Editor>()
26668 .expect("Should be an editor")
26669 })
26670 .unwrap();
26671
26672 assert_eq!(
26673 2,
26674 requests_made.load(atomic::Ordering::Acquire),
26675 "Cache should be reused on buffer close and reopen"
26676 );
26677 editor.update(cx, |editor, cx| {
26678 assert_eq!(
26679 vec![expected_color],
26680 extract_color_inlays(editor, cx),
26681 "Should have an initial inlay"
26682 );
26683 });
26684
26685 drop(color_request_handle);
26686 let closure_requests_made = Arc::clone(&requests_made);
26687 let mut empty_color_request_handle = fake_language_server
26688 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26689 let requests_made = Arc::clone(&closure_requests_made);
26690 async move {
26691 assert_eq!(
26692 params.text_document.uri,
26693 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26694 );
26695 requests_made.fetch_add(1, atomic::Ordering::Release);
26696 Ok(Vec::new())
26697 }
26698 });
26699 let save = editor.update_in(cx, |editor, window, cx| {
26700 editor.move_to_end(&MoveToEnd, window, cx);
26701 editor.handle_input("dirty_again", window, cx);
26702 editor.save(
26703 SaveOptions {
26704 format: false,
26705 autosave: true,
26706 },
26707 project.clone(),
26708 window,
26709 cx,
26710 )
26711 });
26712 save.await.unwrap();
26713
26714 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26715 empty_color_request_handle.next().await.unwrap();
26716 cx.run_until_parked();
26717 assert_eq!(
26718 3,
26719 requests_made.load(atomic::Ordering::Acquire),
26720 "Should query for colors once per save only, as formatting was not requested"
26721 );
26722 editor.update(cx, |editor, cx| {
26723 assert_eq!(
26724 Vec::<Rgba>::new(),
26725 extract_color_inlays(editor, cx),
26726 "Should clear all colors when the server returns an empty response"
26727 );
26728 });
26729}
26730
26731#[gpui::test]
26732async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26733 init_test(cx, |_| {});
26734 let (editor, cx) = cx.add_window_view(Editor::single_line);
26735 editor.update_in(cx, |editor, window, cx| {
26736 editor.set_text("oops\n\nwow\n", window, cx)
26737 });
26738 cx.run_until_parked();
26739 editor.update(cx, |editor, cx| {
26740 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26741 });
26742 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26743 cx.run_until_parked();
26744 editor.update(cx, |editor, cx| {
26745 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26746 });
26747}
26748
26749#[gpui::test]
26750async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26751 init_test(cx, |_| {});
26752
26753 cx.update(|cx| {
26754 register_project_item::<Editor>(cx);
26755 });
26756
26757 let fs = FakeFs::new(cx.executor());
26758 fs.insert_tree("/root1", json!({})).await;
26759 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26760 .await;
26761
26762 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26763 let (workspace, cx) =
26764 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26765
26766 let worktree_id = project.update(cx, |project, cx| {
26767 project.worktrees(cx).next().unwrap().read(cx).id()
26768 });
26769
26770 let handle = workspace
26771 .update_in(cx, |workspace, window, cx| {
26772 let project_path = (worktree_id, rel_path("one.pdf"));
26773 workspace.open_path(project_path, None, true, window, cx)
26774 })
26775 .await
26776 .unwrap();
26777
26778 assert_eq!(
26779 handle.to_any().entity_type(),
26780 TypeId::of::<InvalidItemView>()
26781 );
26782}
26783
26784#[gpui::test]
26785async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26786 init_test(cx, |_| {});
26787
26788 let language = Arc::new(Language::new(
26789 LanguageConfig::default(),
26790 Some(tree_sitter_rust::LANGUAGE.into()),
26791 ));
26792
26793 // Test hierarchical sibling navigation
26794 let text = r#"
26795 fn outer() {
26796 if condition {
26797 let a = 1;
26798 }
26799 let b = 2;
26800 }
26801
26802 fn another() {
26803 let c = 3;
26804 }
26805 "#;
26806
26807 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26808 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26809 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26810
26811 // Wait for parsing to complete
26812 editor
26813 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26814 .await;
26815
26816 editor.update_in(cx, |editor, window, cx| {
26817 // Start by selecting "let a = 1;" inside the if block
26818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26819 s.select_display_ranges([
26820 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26821 ]);
26822 });
26823
26824 let initial_selection = editor
26825 .selections
26826 .display_ranges(&editor.display_snapshot(cx));
26827 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26828
26829 // Test select next sibling - should move up levels to find the next sibling
26830 // Since "let a = 1;" has no siblings in the if block, it should move up
26831 // to find "let b = 2;" which is a sibling of the if block
26832 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26833 let next_selection = editor
26834 .selections
26835 .display_ranges(&editor.display_snapshot(cx));
26836
26837 // Should have a selection and it should be different from the initial
26838 assert_eq!(
26839 next_selection.len(),
26840 1,
26841 "Should have one selection after next"
26842 );
26843 assert_ne!(
26844 next_selection[0], initial_selection[0],
26845 "Next sibling selection should be different"
26846 );
26847
26848 // Test hierarchical navigation by going to the end of the current function
26849 // and trying to navigate to the next function
26850 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26851 s.select_display_ranges([
26852 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26853 ]);
26854 });
26855
26856 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26857 let function_next_selection = editor
26858 .selections
26859 .display_ranges(&editor.display_snapshot(cx));
26860
26861 // Should move to the next function
26862 assert_eq!(
26863 function_next_selection.len(),
26864 1,
26865 "Should have one selection after function next"
26866 );
26867
26868 // Test select previous sibling navigation
26869 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26870 let prev_selection = editor
26871 .selections
26872 .display_ranges(&editor.display_snapshot(cx));
26873
26874 // Should have a selection and it should be different
26875 assert_eq!(
26876 prev_selection.len(),
26877 1,
26878 "Should have one selection after prev"
26879 );
26880 assert_ne!(
26881 prev_selection[0], function_next_selection[0],
26882 "Previous sibling selection should be different from next"
26883 );
26884 });
26885}
26886
26887#[gpui::test]
26888async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26889 init_test(cx, |_| {});
26890
26891 let mut cx = EditorTestContext::new(cx).await;
26892 cx.set_state(
26893 "let ˇvariable = 42;
26894let another = variable + 1;
26895let result = variable * 2;",
26896 );
26897
26898 // Set up document highlights manually (simulating LSP response)
26899 cx.update_editor(|editor, _window, cx| {
26900 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26901
26902 // Create highlights for "variable" occurrences
26903 let highlight_ranges = [
26904 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26905 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26906 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26907 ];
26908
26909 let anchor_ranges: Vec<_> = highlight_ranges
26910 .iter()
26911 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26912 .collect();
26913
26914 editor.highlight_background::<DocumentHighlightRead>(
26915 &anchor_ranges,
26916 |theme| theme.colors().editor_document_highlight_read_background,
26917 cx,
26918 );
26919 });
26920
26921 // Go to next highlight - should move to second "variable"
26922 cx.update_editor(|editor, window, cx| {
26923 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26924 });
26925 cx.assert_editor_state(
26926 "let variable = 42;
26927let another = ˇvariable + 1;
26928let result = variable * 2;",
26929 );
26930
26931 // Go to next highlight - should move to third "variable"
26932 cx.update_editor(|editor, window, cx| {
26933 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26934 });
26935 cx.assert_editor_state(
26936 "let variable = 42;
26937let another = variable + 1;
26938let result = ˇvariable * 2;",
26939 );
26940
26941 // Go to next highlight - should stay at third "variable" (no wrap-around)
26942 cx.update_editor(|editor, window, cx| {
26943 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26944 });
26945 cx.assert_editor_state(
26946 "let variable = 42;
26947let another = variable + 1;
26948let result = ˇvariable * 2;",
26949 );
26950
26951 // Now test going backwards from third position
26952 cx.update_editor(|editor, window, cx| {
26953 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26954 });
26955 cx.assert_editor_state(
26956 "let variable = 42;
26957let another = ˇvariable + 1;
26958let result = variable * 2;",
26959 );
26960
26961 // Go to previous highlight - should move to first "variable"
26962 cx.update_editor(|editor, window, cx| {
26963 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26964 });
26965 cx.assert_editor_state(
26966 "let ˇvariable = 42;
26967let another = variable + 1;
26968let result = variable * 2;",
26969 );
26970
26971 // Go to previous highlight - should stay on first "variable"
26972 cx.update_editor(|editor, window, cx| {
26973 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26974 });
26975 cx.assert_editor_state(
26976 "let ˇvariable = 42;
26977let another = variable + 1;
26978let result = variable * 2;",
26979 );
26980}
26981
26982#[gpui::test]
26983async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26984 cx: &mut gpui::TestAppContext,
26985) {
26986 init_test(cx, |_| {});
26987
26988 let url = "https://zed.dev";
26989
26990 let markdown_language = Arc::new(Language::new(
26991 LanguageConfig {
26992 name: "Markdown".into(),
26993 ..LanguageConfig::default()
26994 },
26995 None,
26996 ));
26997
26998 let mut cx = EditorTestContext::new(cx).await;
26999 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27000 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27001
27002 cx.update_editor(|editor, window, cx| {
27003 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27004 editor.paste(&Paste, window, cx);
27005 });
27006
27007 cx.assert_editor_state(&format!(
27008 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27009 ));
27010}
27011
27012#[gpui::test]
27013async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
27014 cx: &mut gpui::TestAppContext,
27015) {
27016 init_test(cx, |_| {});
27017
27018 let url = "https://zed.dev";
27019
27020 let markdown_language = Arc::new(Language::new(
27021 LanguageConfig {
27022 name: "Markdown".into(),
27023 ..LanguageConfig::default()
27024 },
27025 None,
27026 ));
27027
27028 let mut cx = EditorTestContext::new(cx).await;
27029 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27030 cx.set_state(&format!(
27031 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
27032 ));
27033
27034 cx.update_editor(|editor, window, cx| {
27035 editor.copy(&Copy, window, cx);
27036 });
27037
27038 cx.set_state(&format!(
27039 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
27040 ));
27041
27042 cx.update_editor(|editor, window, cx| {
27043 editor.paste(&Paste, window, cx);
27044 });
27045
27046 cx.assert_editor_state(&format!(
27047 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
27048 ));
27049}
27050
27051#[gpui::test]
27052async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
27053 cx: &mut gpui::TestAppContext,
27054) {
27055 init_test(cx, |_| {});
27056
27057 let url = "https://zed.dev";
27058
27059 let markdown_language = Arc::new(Language::new(
27060 LanguageConfig {
27061 name: "Markdown".into(),
27062 ..LanguageConfig::default()
27063 },
27064 None,
27065 ));
27066
27067 let mut cx = EditorTestContext::new(cx).await;
27068 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27069 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
27070
27071 cx.update_editor(|editor, window, cx| {
27072 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27073 editor.paste(&Paste, window, cx);
27074 });
27075
27076 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
27077}
27078
27079#[gpui::test]
27080async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
27081 cx: &mut gpui::TestAppContext,
27082) {
27083 init_test(cx, |_| {});
27084
27085 let text = "Awesome";
27086
27087 let markdown_language = Arc::new(Language::new(
27088 LanguageConfig {
27089 name: "Markdown".into(),
27090 ..LanguageConfig::default()
27091 },
27092 None,
27093 ));
27094
27095 let mut cx = EditorTestContext::new(cx).await;
27096 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27097 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
27098
27099 cx.update_editor(|editor, window, cx| {
27100 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
27101 editor.paste(&Paste, window, cx);
27102 });
27103
27104 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
27105}
27106
27107#[gpui::test]
27108async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
27109 cx: &mut gpui::TestAppContext,
27110) {
27111 init_test(cx, |_| {});
27112
27113 let url = "https://zed.dev";
27114
27115 let markdown_language = Arc::new(Language::new(
27116 LanguageConfig {
27117 name: "Rust".into(),
27118 ..LanguageConfig::default()
27119 },
27120 None,
27121 ));
27122
27123 let mut cx = EditorTestContext::new(cx).await;
27124 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27125 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
27126
27127 cx.update_editor(|editor, window, cx| {
27128 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27129 editor.paste(&Paste, window, cx);
27130 });
27131
27132 cx.assert_editor_state(&format!(
27133 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
27134 ));
27135}
27136
27137#[gpui::test]
27138async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
27139 cx: &mut TestAppContext,
27140) {
27141 init_test(cx, |_| {});
27142
27143 let url = "https://zed.dev";
27144
27145 let markdown_language = Arc::new(Language::new(
27146 LanguageConfig {
27147 name: "Markdown".into(),
27148 ..LanguageConfig::default()
27149 },
27150 None,
27151 ));
27152
27153 let (editor, cx) = cx.add_window_view(|window, cx| {
27154 let multi_buffer = MultiBuffer::build_multi(
27155 [
27156 ("this will embed -> link", vec![Point::row_range(0..1)]),
27157 ("this will replace -> link", vec![Point::row_range(0..1)]),
27158 ],
27159 cx,
27160 );
27161 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
27162 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27163 s.select_ranges(vec![
27164 Point::new(0, 19)..Point::new(0, 23),
27165 Point::new(1, 21)..Point::new(1, 25),
27166 ])
27167 });
27168 let first_buffer_id = multi_buffer
27169 .read(cx)
27170 .excerpt_buffer_ids()
27171 .into_iter()
27172 .next()
27173 .unwrap();
27174 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
27175 first_buffer.update(cx, |buffer, cx| {
27176 buffer.set_language(Some(markdown_language.clone()), cx);
27177 });
27178
27179 editor
27180 });
27181 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27182
27183 cx.update_editor(|editor, window, cx| {
27184 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27185 editor.paste(&Paste, window, cx);
27186 });
27187
27188 cx.assert_editor_state(&format!(
27189 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
27190 ));
27191}
27192
27193#[gpui::test]
27194async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
27195 init_test(cx, |_| {});
27196
27197 let fs = FakeFs::new(cx.executor());
27198 fs.insert_tree(
27199 path!("/project"),
27200 json!({
27201 "first.rs": "# First Document\nSome content here.",
27202 "second.rs": "Plain text content for second file.",
27203 }),
27204 )
27205 .await;
27206
27207 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
27208 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27209 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27210
27211 let language = rust_lang();
27212 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27213 language_registry.add(language.clone());
27214 let mut fake_servers = language_registry.register_fake_lsp(
27215 "Rust",
27216 FakeLspAdapter {
27217 ..FakeLspAdapter::default()
27218 },
27219 );
27220
27221 let buffer1 = project
27222 .update(cx, |project, cx| {
27223 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
27224 })
27225 .await
27226 .unwrap();
27227 let buffer2 = project
27228 .update(cx, |project, cx| {
27229 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
27230 })
27231 .await
27232 .unwrap();
27233
27234 let multi_buffer = cx.new(|cx| {
27235 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
27236 multi_buffer.set_excerpts_for_path(
27237 PathKey::for_buffer(&buffer1, cx),
27238 buffer1.clone(),
27239 [Point::zero()..buffer1.read(cx).max_point()],
27240 3,
27241 cx,
27242 );
27243 multi_buffer.set_excerpts_for_path(
27244 PathKey::for_buffer(&buffer2, cx),
27245 buffer2.clone(),
27246 [Point::zero()..buffer1.read(cx).max_point()],
27247 3,
27248 cx,
27249 );
27250 multi_buffer
27251 });
27252
27253 let (editor, cx) = cx.add_window_view(|window, cx| {
27254 Editor::new(
27255 EditorMode::full(),
27256 multi_buffer,
27257 Some(project.clone()),
27258 window,
27259 cx,
27260 )
27261 });
27262
27263 let fake_language_server = fake_servers.next().await.unwrap();
27264
27265 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
27266
27267 let save = editor.update_in(cx, |editor, window, cx| {
27268 assert!(editor.is_dirty(cx));
27269
27270 editor.save(
27271 SaveOptions {
27272 format: true,
27273 autosave: true,
27274 },
27275 project,
27276 window,
27277 cx,
27278 )
27279 });
27280 let (start_edit_tx, start_edit_rx) = oneshot::channel();
27281 let (done_edit_tx, done_edit_rx) = oneshot::channel();
27282 let mut done_edit_rx = Some(done_edit_rx);
27283 let mut start_edit_tx = Some(start_edit_tx);
27284
27285 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
27286 start_edit_tx.take().unwrap().send(()).unwrap();
27287 let done_edit_rx = done_edit_rx.take().unwrap();
27288 async move {
27289 done_edit_rx.await.unwrap();
27290 Ok(None)
27291 }
27292 });
27293
27294 start_edit_rx.await.unwrap();
27295 buffer2
27296 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
27297 .unwrap();
27298
27299 done_edit_tx.send(()).unwrap();
27300
27301 save.await.unwrap();
27302 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
27303}
27304
27305#[track_caller]
27306fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
27307 editor
27308 .all_inlays(cx)
27309 .into_iter()
27310 .filter_map(|inlay| inlay.get_color())
27311 .map(Rgba::from)
27312 .collect()
27313}
27314
27315#[gpui::test]
27316fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
27317 init_test(cx, |_| {});
27318
27319 let editor = cx.add_window(|window, cx| {
27320 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
27321 build_editor(buffer, window, cx)
27322 });
27323
27324 editor
27325 .update(cx, |editor, window, cx| {
27326 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27327 s.select_display_ranges([
27328 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
27329 ])
27330 });
27331
27332 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
27333
27334 assert_eq!(
27335 editor.display_text(cx),
27336 "line1\nline2\nline2",
27337 "Duplicating last line upward should create duplicate above, not on same line"
27338 );
27339
27340 assert_eq!(
27341 editor
27342 .selections
27343 .display_ranges(&editor.display_snapshot(cx)),
27344 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
27345 "Selection should move to the duplicated line"
27346 );
27347 })
27348 .unwrap();
27349}
27350
27351#[gpui::test]
27352async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
27353 init_test(cx, |_| {});
27354
27355 let mut cx = EditorTestContext::new(cx).await;
27356
27357 cx.set_state("line1\nline2ˇ");
27358
27359 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
27360
27361 let clipboard_text = cx
27362 .read_from_clipboard()
27363 .and_then(|item| item.text().as_deref().map(str::to_string));
27364
27365 assert_eq!(
27366 clipboard_text,
27367 Some("line2\n".to_string()),
27368 "Copying a line without trailing newline should include a newline"
27369 );
27370
27371 cx.set_state("line1\nˇ");
27372
27373 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27374
27375 cx.assert_editor_state("line1\nline2\nˇ");
27376}
27377
27378#[gpui::test]
27379async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27380 init_test(cx, |_| {});
27381
27382 let mut cx = EditorTestContext::new(cx).await;
27383
27384 cx.set_state("line1\nline2ˇ");
27385 cx.update_editor(|e, window, cx| {
27386 e.set_mode(EditorMode::SingleLine);
27387 assert!(e.key_context(window, cx).contains("end_of_input"));
27388 });
27389 cx.set_state("ˇline1\nline2");
27390 cx.update_editor(|e, window, cx| {
27391 assert!(!e.key_context(window, cx).contains("end_of_input"));
27392 });
27393 cx.set_state("line1ˇ\nline2");
27394 cx.update_editor(|e, window, cx| {
27395 assert!(!e.key_context(window, cx).contains("end_of_input"));
27396 });
27397}
27398
27399#[gpui::test]
27400async fn test_sticky_scroll(cx: &mut TestAppContext) {
27401 init_test(cx, |_| {});
27402 let mut cx = EditorTestContext::new(cx).await;
27403
27404 let buffer = indoc! {"
27405 ˇfn foo() {
27406 let abc = 123;
27407 }
27408 struct Bar;
27409 impl Bar {
27410 fn new() -> Self {
27411 Self
27412 }
27413 }
27414 fn baz() {
27415 }
27416 "};
27417 cx.set_state(&buffer);
27418
27419 cx.update_editor(|e, _, cx| {
27420 e.buffer()
27421 .read(cx)
27422 .as_singleton()
27423 .unwrap()
27424 .update(cx, |buffer, cx| {
27425 buffer.set_language(Some(rust_lang()), cx);
27426 })
27427 });
27428
27429 let mut sticky_headers = |offset: ScrollOffset| {
27430 cx.update_editor(|e, window, cx| {
27431 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27432 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27433 .into_iter()
27434 .map(
27435 |StickyHeader {
27436 start_point,
27437 offset,
27438 ..
27439 }| { (start_point, offset) },
27440 )
27441 .collect::<Vec<_>>()
27442 })
27443 };
27444
27445 let fn_foo = Point { row: 0, column: 0 };
27446 let impl_bar = Point { row: 4, column: 0 };
27447 let fn_new = Point { row: 5, column: 4 };
27448
27449 assert_eq!(sticky_headers(0.0), vec![]);
27450 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27451 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27452 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27453 assert_eq!(sticky_headers(2.0), vec![]);
27454 assert_eq!(sticky_headers(2.5), vec![]);
27455 assert_eq!(sticky_headers(3.0), vec![]);
27456 assert_eq!(sticky_headers(3.5), vec![]);
27457 assert_eq!(sticky_headers(4.0), vec![]);
27458 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27459 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27460 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27461 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27462 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27463 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27464 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27465 assert_eq!(sticky_headers(8.0), vec![]);
27466 assert_eq!(sticky_headers(8.5), vec![]);
27467 assert_eq!(sticky_headers(9.0), vec![]);
27468 assert_eq!(sticky_headers(9.5), vec![]);
27469 assert_eq!(sticky_headers(10.0), vec![]);
27470}
27471
27472#[gpui::test]
27473async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27474 init_test(cx, |_| {});
27475 cx.update(|cx| {
27476 SettingsStore::update_global(cx, |store, cx| {
27477 store.update_user_settings(cx, |settings| {
27478 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27479 enabled: Some(true),
27480 })
27481 });
27482 });
27483 });
27484 let mut cx = EditorTestContext::new(cx).await;
27485
27486 let line_height = cx.editor(|editor, window, _cx| {
27487 editor
27488 .style()
27489 .unwrap()
27490 .text
27491 .line_height_in_pixels(window.rem_size())
27492 });
27493
27494 let buffer = indoc! {"
27495 ˇfn foo() {
27496 let abc = 123;
27497 }
27498 struct Bar;
27499 impl Bar {
27500 fn new() -> Self {
27501 Self
27502 }
27503 }
27504 fn baz() {
27505 }
27506 "};
27507 cx.set_state(&buffer);
27508
27509 cx.update_editor(|e, _, cx| {
27510 e.buffer()
27511 .read(cx)
27512 .as_singleton()
27513 .unwrap()
27514 .update(cx, |buffer, cx| {
27515 buffer.set_language(Some(rust_lang()), cx);
27516 })
27517 });
27518
27519 let fn_foo = || empty_range(0, 0);
27520 let impl_bar = || empty_range(4, 0);
27521 let fn_new = || empty_range(5, 4);
27522
27523 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27524 cx.update_editor(|e, window, cx| {
27525 e.scroll(
27526 gpui::Point {
27527 x: 0.,
27528 y: scroll_offset,
27529 },
27530 None,
27531 window,
27532 cx,
27533 );
27534 });
27535 cx.simulate_click(
27536 gpui::Point {
27537 x: px(0.),
27538 y: click_offset as f32 * line_height,
27539 },
27540 Modifiers::none(),
27541 );
27542 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27543 };
27544
27545 assert_eq!(
27546 scroll_and_click(
27547 4.5, // impl Bar is halfway off the screen
27548 0.0 // click top of screen
27549 ),
27550 // scrolled to impl Bar
27551 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27552 );
27553
27554 assert_eq!(
27555 scroll_and_click(
27556 4.5, // impl Bar is halfway off the screen
27557 0.25 // click middle of impl Bar
27558 ),
27559 // scrolled to impl Bar
27560 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27561 );
27562
27563 assert_eq!(
27564 scroll_and_click(
27565 4.5, // impl Bar is halfway off the screen
27566 1.5 // click below impl Bar (e.g. fn new())
27567 ),
27568 // scrolled to fn new() - this is below the impl Bar header which has persisted
27569 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27570 );
27571
27572 assert_eq!(
27573 scroll_and_click(
27574 5.5, // fn new is halfway underneath impl Bar
27575 0.75 // click on the overlap of impl Bar and fn new()
27576 ),
27577 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27578 );
27579
27580 assert_eq!(
27581 scroll_and_click(
27582 5.5, // fn new is halfway underneath impl Bar
27583 1.25 // click on the visible part of fn new()
27584 ),
27585 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27586 );
27587
27588 assert_eq!(
27589 scroll_and_click(
27590 1.5, // fn foo is halfway off the screen
27591 0.0 // click top of screen
27592 ),
27593 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
27594 );
27595
27596 assert_eq!(
27597 scroll_and_click(
27598 1.5, // fn foo is halfway off the screen
27599 0.75 // click visible part of let abc...
27600 )
27601 .0,
27602 // no change in scroll
27603 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
27604 (gpui::Point { x: 0., y: 1.5 })
27605 );
27606}
27607
27608#[gpui::test]
27609async fn test_next_prev_reference(cx: &mut TestAppContext) {
27610 const CYCLE_POSITIONS: &[&'static str] = &[
27611 indoc! {"
27612 fn foo() {
27613 let ˇabc = 123;
27614 let x = abc + 1;
27615 let y = abc + 2;
27616 let z = abc + 2;
27617 }
27618 "},
27619 indoc! {"
27620 fn foo() {
27621 let abc = 123;
27622 let x = ˇabc + 1;
27623 let y = abc + 2;
27624 let z = abc + 2;
27625 }
27626 "},
27627 indoc! {"
27628 fn foo() {
27629 let abc = 123;
27630 let x = abc + 1;
27631 let y = ˇabc + 2;
27632 let z = abc + 2;
27633 }
27634 "},
27635 indoc! {"
27636 fn foo() {
27637 let abc = 123;
27638 let x = abc + 1;
27639 let y = abc + 2;
27640 let z = ˇabc + 2;
27641 }
27642 "},
27643 ];
27644
27645 init_test(cx, |_| {});
27646
27647 let mut cx = EditorLspTestContext::new_rust(
27648 lsp::ServerCapabilities {
27649 references_provider: Some(lsp::OneOf::Left(true)),
27650 ..Default::default()
27651 },
27652 cx,
27653 )
27654 .await;
27655
27656 // importantly, the cursor is in the middle
27657 cx.set_state(indoc! {"
27658 fn foo() {
27659 let aˇbc = 123;
27660 let x = abc + 1;
27661 let y = abc + 2;
27662 let z = abc + 2;
27663 }
27664 "});
27665
27666 let reference_ranges = [
27667 lsp::Position::new(1, 8),
27668 lsp::Position::new(2, 12),
27669 lsp::Position::new(3, 12),
27670 lsp::Position::new(4, 12),
27671 ]
27672 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27673
27674 cx.lsp
27675 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27676 Ok(Some(
27677 reference_ranges
27678 .map(|range| lsp::Location {
27679 uri: params.text_document_position.text_document.uri.clone(),
27680 range,
27681 })
27682 .to_vec(),
27683 ))
27684 });
27685
27686 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27687 cx.update_editor(|editor, window, cx| {
27688 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27689 })
27690 .unwrap()
27691 .await
27692 .unwrap()
27693 };
27694
27695 _move(Direction::Next, 1, &mut cx).await;
27696 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27697
27698 _move(Direction::Next, 1, &mut cx).await;
27699 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27700
27701 _move(Direction::Next, 1, &mut cx).await;
27702 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27703
27704 // loops back to the start
27705 _move(Direction::Next, 1, &mut cx).await;
27706 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27707
27708 // loops back to the end
27709 _move(Direction::Prev, 1, &mut cx).await;
27710 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27711
27712 _move(Direction::Prev, 1, &mut cx).await;
27713 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27714
27715 _move(Direction::Prev, 1, &mut cx).await;
27716 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27717
27718 _move(Direction::Prev, 1, &mut cx).await;
27719 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27720
27721 _move(Direction::Next, 3, &mut cx).await;
27722 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27723
27724 _move(Direction::Prev, 2, &mut cx).await;
27725 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27726}
27727
27728#[gpui::test]
27729async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
27730 init_test(cx, |_| {});
27731
27732 let (editor, cx) = cx.add_window_view(|window, cx| {
27733 let multi_buffer = MultiBuffer::build_multi(
27734 [
27735 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
27736 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
27737 ],
27738 cx,
27739 );
27740 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
27741 });
27742
27743 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
27744 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
27745
27746 cx.assert_excerpts_with_selections(indoc! {"
27747 [EXCERPT]
27748 ˇ1
27749 2
27750 3
27751 [EXCERPT]
27752 1
27753 2
27754 3
27755 "});
27756
27757 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
27758 cx.update_editor(|editor, window, cx| {
27759 editor.change_selections(None.into(), window, cx, |s| {
27760 s.select_ranges([2..3]);
27761 });
27762 });
27763 cx.assert_excerpts_with_selections(indoc! {"
27764 [EXCERPT]
27765 1
27766 2ˇ
27767 3
27768 [EXCERPT]
27769 1
27770 2
27771 3
27772 "});
27773
27774 cx.update_editor(|editor, window, cx| {
27775 editor
27776 .select_all_matches(&SelectAllMatches, window, cx)
27777 .unwrap();
27778 });
27779 cx.assert_excerpts_with_selections(indoc! {"
27780 [EXCERPT]
27781 1
27782 2ˇ
27783 3
27784 [EXCERPT]
27785 1
27786 2ˇ
27787 3
27788 "});
27789
27790 cx.update_editor(|editor, window, cx| {
27791 editor.handle_input("X", window, cx);
27792 });
27793 cx.assert_excerpts_with_selections(indoc! {"
27794 [EXCERPT]
27795 1
27796 Xˇ
27797 3
27798 [EXCERPT]
27799 1
27800 Xˇ
27801 3
27802 "});
27803
27804 // Scenario 2: Select "2", then fold second buffer before insertion
27805 cx.update_multibuffer(|mb, cx| {
27806 for buffer_id in buffer_ids.iter() {
27807 let buffer = mb.buffer(*buffer_id).unwrap();
27808 buffer.update(cx, |buffer, cx| {
27809 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
27810 });
27811 }
27812 });
27813
27814 // Select "2" and select all matches
27815 cx.update_editor(|editor, window, cx| {
27816 editor.change_selections(None.into(), window, cx, |s| {
27817 s.select_ranges([2..3]);
27818 });
27819 editor
27820 .select_all_matches(&SelectAllMatches, window, cx)
27821 .unwrap();
27822 });
27823
27824 // Fold second buffer - should remove selections from folded buffer
27825 cx.update_editor(|editor, _, cx| {
27826 editor.fold_buffer(buffer_ids[1], cx);
27827 });
27828 cx.assert_excerpts_with_selections(indoc! {"
27829 [EXCERPT]
27830 1
27831 2ˇ
27832 3
27833 [EXCERPT]
27834 [FOLDED]
27835 "});
27836
27837 // Insert text - should only affect first buffer
27838 cx.update_editor(|editor, window, cx| {
27839 editor.handle_input("Y", window, cx);
27840 });
27841 cx.update_editor(|editor, _, cx| {
27842 editor.unfold_buffer(buffer_ids[1], cx);
27843 });
27844 cx.assert_excerpts_with_selections(indoc! {"
27845 [EXCERPT]
27846 1
27847 Yˇ
27848 3
27849 [EXCERPT]
27850 1
27851 2
27852 3
27853 "});
27854
27855 // Scenario 3: Select "2", then fold first buffer before insertion
27856 cx.update_multibuffer(|mb, cx| {
27857 for buffer_id in buffer_ids.iter() {
27858 let buffer = mb.buffer(*buffer_id).unwrap();
27859 buffer.update(cx, |buffer, cx| {
27860 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
27861 });
27862 }
27863 });
27864
27865 // Select "2" and select all matches
27866 cx.update_editor(|editor, window, cx| {
27867 editor.change_selections(None.into(), window, cx, |s| {
27868 s.select_ranges([2..3]);
27869 });
27870 editor
27871 .select_all_matches(&SelectAllMatches, window, cx)
27872 .unwrap();
27873 });
27874
27875 // Fold first buffer - should remove selections from folded buffer
27876 cx.update_editor(|editor, _, cx| {
27877 editor.fold_buffer(buffer_ids[0], cx);
27878 });
27879 cx.assert_excerpts_with_selections(indoc! {"
27880 [EXCERPT]
27881 [FOLDED]
27882 [EXCERPT]
27883 1
27884 2ˇ
27885 3
27886 "});
27887
27888 // Insert text - should only affect second buffer
27889 cx.update_editor(|editor, window, cx| {
27890 editor.handle_input("Z", window, cx);
27891 });
27892 cx.update_editor(|editor, _, cx| {
27893 editor.unfold_buffer(buffer_ids[0], cx);
27894 });
27895 cx.assert_excerpts_with_selections(indoc! {"
27896 [EXCERPT]
27897 1
27898 2
27899 3
27900 [EXCERPT]
27901 1
27902 Zˇ
27903 3
27904 "});
27905
27906 // Edge case scenario: fold all buffers, then try to insert
27907 cx.update_editor(|editor, _, cx| {
27908 editor.fold_buffer(buffer_ids[0], cx);
27909 editor.fold_buffer(buffer_ids[1], cx);
27910 });
27911 cx.assert_excerpts_with_selections(indoc! {"
27912 [EXCERPT]
27913 ˇ[FOLDED]
27914 [EXCERPT]
27915 [FOLDED]
27916 "});
27917
27918 // Insert should work via default selection
27919 cx.update_editor(|editor, window, cx| {
27920 editor.handle_input("W", window, cx);
27921 });
27922 cx.update_editor(|editor, _, cx| {
27923 editor.unfold_buffer(buffer_ids[0], cx);
27924 editor.unfold_buffer(buffer_ids[1], cx);
27925 });
27926 cx.assert_excerpts_with_selections(indoc! {"
27927 [EXCERPT]
27928 Wˇ1
27929 2
27930 3
27931 [EXCERPT]
27932 1
27933 Z
27934 3
27935 "});
27936}