1use super::*;
2use crate::{
3 scroll::scroll_amount::ScrollAmount,
4 test::{
5 assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
6 editor_test_context::EditorTestContext, select_ranges,
7 },
8 JoinLines,
9};
10use drag_and_drop::DragAndDrop;
11use futures::StreamExt;
12use gpui::{
13 div,
14 serde_json::{self, json},
15 Div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
16};
17use indoc::indoc;
18use language::{
19 language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
20 BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
21 Override, Point,
22};
23use parking_lot::Mutex;
24use project::project_settings::{LspSettings, ProjectSettings};
25use project::FakeFs;
26use std::sync::atomic;
27use std::sync::atomic::AtomicUsize;
28use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
29use unindent::Unindent;
30use util::{
31 assert_set_eq,
32 test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
33};
34use workspace::{
35 item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle},
36 NavigationEntry, ViewId,
37};
38
39#[gpui::test]
40fn test_edit_events(cx: &mut TestAppContext) {
41 init_test(cx, |_| {});
42
43 let buffer = cx.build_model(|cx| {
44 let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456");
45 buffer.set_group_interval(Duration::from_secs(1));
46 buffer
47 });
48
49 let events = Rc::new(RefCell::new(Vec::new()));
50 let editor1 = cx.add_window({
51 let events = events.clone();
52 |cx| {
53 cx.subscribe(cx.view(), move |_, _, event, _| {
54 if matches!(event, Event::Edited | Event::BufferEdited) {
55 events.borrow_mut().push(("editor1", event.clone()));
56 }
57 })
58 .detach();
59 Editor::for_buffer(buffer.clone(), None, cx)
60 }
61 });
62
63 let editor2 = cx.add_window({
64 let events = events.clone();
65 |cx| {
66 cx.subscribe(cx.view(), move |_, _, event, _| {
67 if matches!(event, Event::Edited | Event::BufferEdited) {
68 events.borrow_mut().push(("editor2", event.clone()));
69 }
70 })
71 .detach();
72 Editor::for_buffer(buffer.clone(), None, cx)
73 }
74 });
75
76 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
77
78 // Mutating editor 1 will emit an `Edited` event only for that editor.
79 editor1.update(cx, |editor, cx| editor.insert("X", cx));
80 assert_eq!(
81 mem::take(&mut *events.borrow_mut()),
82 [
83 ("editor1", Event::Edited),
84 ("editor1", Event::BufferEdited),
85 ("editor2", Event::BufferEdited),
86 ]
87 );
88
89 // Mutating editor 2 will emit an `Edited` event only for that editor.
90 editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
91 assert_eq!(
92 mem::take(&mut *events.borrow_mut()),
93 [
94 ("editor2", Event::Edited),
95 ("editor1", Event::BufferEdited),
96 ("editor2", Event::BufferEdited),
97 ]
98 );
99
100 // Undoing on editor 1 will emit an `Edited` event only for that editor.
101 editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
102 assert_eq!(
103 mem::take(&mut *events.borrow_mut()),
104 [
105 ("editor1", Event::Edited),
106 ("editor1", Event::BufferEdited),
107 ("editor2", Event::BufferEdited),
108 ]
109 );
110
111 // Redoing on editor 1 will emit an `Edited` event only for that editor.
112 editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
113 assert_eq!(
114 mem::take(&mut *events.borrow_mut()),
115 [
116 ("editor1", Event::Edited),
117 ("editor1", Event::BufferEdited),
118 ("editor2", Event::BufferEdited),
119 ]
120 );
121
122 // Undoing on editor 2 will emit an `Edited` event only for that editor.
123 editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor2", Event::Edited),
128 ("editor1", Event::BufferEdited),
129 ("editor2", Event::BufferEdited),
130 ]
131 );
132
133 // Redoing on editor 2 will emit an `Edited` event only for that editor.
134 editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", Event::Edited),
139 ("editor1", Event::BufferEdited),
140 ("editor2", Event::BufferEdited),
141 ]
142 );
143
144 // No event is emitted when the mutation is a no-op.
145 editor2.update(cx, |editor, cx| {
146 editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
147
148 editor.backspace(&Backspace, cx);
149 });
150 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
151}
152
153#[gpui::test]
154fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
155 init_test(cx, |_| {});
156
157 let mut now = Instant::now();
158 let buffer = cx.build_model(|cx| language::Buffer::new(0, cx.entity_id().as_u64(), "123456"));
159 let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
160 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
161 let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx));
162
163 editor.update(cx, |editor, cx| {
164 editor.start_transaction_at(now, cx);
165 editor.change_selections(None, cx, |s| s.select_ranges([2..4]));
166
167 editor.insert("cd", cx);
168 editor.end_transaction_at(now, cx);
169 assert_eq!(editor.text(cx), "12cd56");
170 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
171
172 editor.start_transaction_at(now, cx);
173 editor.change_selections(None, cx, |s| s.select_ranges([4..5]));
174 editor.insert("e", cx);
175 editor.end_transaction_at(now, cx);
176 assert_eq!(editor.text(cx), "12cde6");
177 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
178
179 now += group_interval + Duration::from_millis(1);
180 editor.change_selections(None, cx, |s| s.select_ranges([2..2]));
181
182 // Simulate an edit in another editor
183 buffer.update(cx, |buffer, cx| {
184 buffer.start_transaction_at(now, cx);
185 buffer.edit([(0..1, "a")], None, cx);
186 buffer.edit([(1..1, "b")], None, cx);
187 buffer.end_transaction_at(now, cx);
188 });
189
190 assert_eq!(editor.text(cx), "ab2cde6");
191 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
192
193 // Last transaction happened past the group interval in a different editor.
194 // Undo it individually and don't restore selections.
195 editor.undo(&Undo, cx);
196 assert_eq!(editor.text(cx), "12cde6");
197 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
198
199 // First two transactions happened within the group interval in this editor.
200 // Undo them together and restore selections.
201 editor.undo(&Undo, cx);
202 editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op.
203 assert_eq!(editor.text(cx), "123456");
204 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
205
206 // Redo the first two transactions together.
207 editor.redo(&Redo, cx);
208 assert_eq!(editor.text(cx), "12cde6");
209 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
210
211 // Redo the last transaction on its own.
212 editor.redo(&Redo, cx);
213 assert_eq!(editor.text(cx), "ab2cde6");
214 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
215
216 // Test empty transactions.
217 editor.start_transaction_at(now, cx);
218 editor.end_transaction_at(now, cx);
219 editor.undo(&Undo, cx);
220 assert_eq!(editor.text(cx), "12cde6");
221 });
222}
223
224#[gpui::test]
225fn test_ime_composition(cx: &mut TestAppContext) {
226 init_test(cx, |_| {});
227
228 let buffer = cx.build_model(|cx| {
229 let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "abcde");
230 // Ensure automatic grouping doesn't occur.
231 buffer.set_group_interval(Duration::ZERO);
232 buffer
233 });
234
235 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
236 cx.add_window(|cx| {
237 let mut editor = build_editor(buffer.clone(), cx);
238
239 // Start a new IME composition.
240 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
241 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx);
242 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx);
243 assert_eq!(editor.text(cx), "äbcde");
244 assert_eq!(
245 editor.marked_text_ranges(cx),
246 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
247 );
248
249 // Finalize IME composition.
250 editor.replace_text_in_range(None, "ā", cx);
251 assert_eq!(editor.text(cx), "ābcde");
252 assert_eq!(editor.marked_text_ranges(cx), None);
253
254 // IME composition edits are grouped and are undone/redone at once.
255 editor.undo(&Default::default(), cx);
256 assert_eq!(editor.text(cx), "abcde");
257 assert_eq!(editor.marked_text_ranges(cx), None);
258 editor.redo(&Default::default(), cx);
259 assert_eq!(editor.text(cx), "ābcde");
260 assert_eq!(editor.marked_text_ranges(cx), None);
261
262 // Start a new IME composition.
263 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx);
264 assert_eq!(
265 editor.marked_text_ranges(cx),
266 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
267 );
268
269 // Undoing during an IME composition cancels it.
270 editor.undo(&Default::default(), cx);
271 assert_eq!(editor.text(cx), "ābcde");
272 assert_eq!(editor.marked_text_ranges(cx), None);
273
274 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
275 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx);
276 assert_eq!(editor.text(cx), "ābcdè");
277 assert_eq!(
278 editor.marked_text_ranges(cx),
279 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
280 );
281
282 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
283 editor.replace_text_in_range(Some(4..999), "ę", cx);
284 assert_eq!(editor.text(cx), "ābcdę");
285 assert_eq!(editor.marked_text_ranges(cx), None);
286
287 // Start a new IME composition with multiple cursors.
288 editor.change_selections(None, cx, |s| {
289 s.select_ranges([
290 OffsetUtf16(1)..OffsetUtf16(1),
291 OffsetUtf16(3)..OffsetUtf16(3),
292 OffsetUtf16(5)..OffsetUtf16(5),
293 ])
294 });
295 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx);
296 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
297 assert_eq!(
298 editor.marked_text_ranges(cx),
299 Some(vec![
300 OffsetUtf16(0)..OffsetUtf16(3),
301 OffsetUtf16(4)..OffsetUtf16(7),
302 OffsetUtf16(8)..OffsetUtf16(11)
303 ])
304 );
305
306 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
307 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx);
308 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
309 assert_eq!(
310 editor.marked_text_ranges(cx),
311 Some(vec![
312 OffsetUtf16(1)..OffsetUtf16(2),
313 OffsetUtf16(5)..OffsetUtf16(6),
314 OffsetUtf16(9)..OffsetUtf16(10)
315 ])
316 );
317
318 // Finalize IME composition with multiple cursors.
319 editor.replace_text_in_range(Some(9..10), "2", cx);
320 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 editor
324 });
325}
326
327#[gpui::test]
328fn test_selection_with_mouse(cx: &mut TestAppContext) {
329 init_test(cx, |_| {});
330
331 let editor = cx.add_window(|cx| {
332 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
333 build_editor(buffer, cx)
334 });
335
336 editor.update(cx, |view, cx| {
337 view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
338 });
339 assert_eq!(
340 editor
341 .update(cx, |view, cx| view.selections.display_ranges(cx))
342 .unwrap(),
343 [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
344 );
345
346 editor.update(cx, |view, cx| {
347 view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
348 });
349
350 assert_eq!(
351 editor
352 .update(cx, |view, cx| view.selections.display_ranges(cx))
353 .unwrap(),
354 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
355 );
356
357 editor.update(cx, |view, cx| {
358 view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
359 });
360
361 assert_eq!(
362 editor
363 .update(cx, |view, cx| view.selections.display_ranges(cx))
364 .unwrap(),
365 [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
366 );
367
368 editor.update(cx, |view, cx| {
369 view.end_selection(cx);
370 view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
371 });
372
373 assert_eq!(
374 editor
375 .update(cx, |view, cx| view.selections.display_ranges(cx))
376 .unwrap(),
377 [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
378 );
379
380 editor.update(cx, |view, cx| {
381 view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
382 view.update_selection(DisplayPoint::new(0, 0), 0, gpui::Point::<f32>::zero(), cx);
383 });
384
385 assert_eq!(
386 editor
387 .update(cx, |view, cx| view.selections.display_ranges(cx))
388 .unwrap(),
389 [
390 DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
391 DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
392 ]
393 );
394
395 editor.update(cx, |view, cx| {
396 view.end_selection(cx);
397 });
398
399 assert_eq!(
400 editor
401 .update(cx, |view, cx| view.selections.display_ranges(cx))
402 .unwrap(),
403 [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
404 );
405}
406
407#[gpui::test]
408fn test_canceling_pending_selection(cx: &mut TestAppContext) {
409 init_test(cx, |_| {});
410
411 let view = cx.add_window(|cx| {
412 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
413 build_editor(buffer, cx)
414 });
415
416 view.update(cx, |view, cx| {
417 view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
418 assert_eq!(
419 view.selections.display_ranges(cx),
420 [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
421 );
422 });
423
424 view.update(cx, |view, cx| {
425 view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::<f32>::zero(), cx);
426 assert_eq!(
427 view.selections.display_ranges(cx),
428 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
429 );
430 });
431
432 view.update(cx, |view, cx| {
433 view.cancel(&Cancel, cx);
434 view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
435 assert_eq!(
436 view.selections.display_ranges(cx),
437 [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
438 );
439 });
440}
441
442#[gpui::test]
443fn test_clone(cx: &mut TestAppContext) {
444 init_test(cx, |_| {});
445
446 let (text, selection_ranges) = marked_text_ranges(
447 indoc! {"
448 one
449 two
450 threeˇ
451 four
452 fiveˇ
453 "},
454 true,
455 );
456
457 let editor = cx.add_window(|cx| {
458 let buffer = MultiBuffer::build_simple(&text, cx);
459 build_editor(buffer, cx)
460 });
461
462 editor.update(cx, |editor, cx| {
463 editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
464 editor.fold_ranges(
465 [
466 Point::new(1, 0)..Point::new(2, 0),
467 Point::new(3, 0)..Point::new(4, 0),
468 ],
469 true,
470 cx,
471 );
472 });
473
474 let cloned_editor = editor
475 .update(cx, |editor, cx| {
476 cx.open_window(Default::default(), |cx| {
477 cx.build_view(|cx| editor.clone(cx))
478 })
479 })
480 .unwrap();
481
482 let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
483 let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
484
485 assert_eq!(
486 cloned_editor
487 .update(cx, |e, cx| e.display_text(cx))
488 .unwrap(),
489 editor.update(cx, |e, cx| e.display_text(cx)).unwrap()
490 );
491 assert_eq!(
492 cloned_snapshot
493 .folds_in_range(0..text.len())
494 .collect::<Vec<_>>(),
495 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
496 );
497 assert_set_eq!(
498 cloned_editor
499 .read_with(cx, |editor, cx| editor.selections.ranges::<Point>(cx))
500 .unwrap(),
501 editor
502 .read_with(cx, |editor, cx| editor.selections.ranges(cx))
503 .unwrap()
504 );
505 assert_set_eq!(
506 cloned_editor
507 .update(cx, |e, cx| e.selections.display_ranges(cx))
508 .unwrap(),
509 editor
510 .update(cx, |e, cx| e.selections.display_ranges(cx))
511 .unwrap()
512 );
513}
514
515#[gpui::test]
516async fn test_navigation_history(cx: &mut TestAppContext) {
517 init_test(cx, |_| {});
518
519 cx.set_global(DragAndDrop::<Workspace>::default());
520 use workspace::item::Item;
521
522 let fs = FakeFs::new(cx.executor());
523 let project = Project::test(fs, [], cx).await;
524 let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
525 let pane = workspace
526 .read_with(cx, |workspace, _| workspace.active_pane().clone())
527 .unwrap();
528
529 workspace.update(cx, |v, cx| {
530 cx.build_view(|cx| {
531 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
532 let mut editor = build_editor(buffer.clone(), cx);
533 let handle = cx.view();
534 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
535
536 fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
537 editor.nav_history.as_mut().unwrap().pop_backward(cx)
538 }
539
540 // Move the cursor a small distance.
541 // Nothing is added to the navigation history.
542 editor.change_selections(None, cx, |s| {
543 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
544 });
545 editor.change_selections(None, cx, |s| {
546 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
547 });
548 assert!(pop_history(&mut editor, cx).is_none());
549
550 // Move the cursor a large distance.
551 // The history can jump back to the previous position.
552 editor.change_selections(None, cx, |s| {
553 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
554 });
555 let nav_entry = pop_history(&mut editor, cx).unwrap();
556 editor.navigate(nav_entry.data.unwrap(), cx);
557 assert_eq!(nav_entry.item.id(), cx.entity_id());
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
561 );
562 assert!(pop_history(&mut editor, cx).is_none());
563
564 // Move the cursor a small distance via the mouse.
565 // Nothing is added to the navigation history.
566 editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
567 editor.end_selection(cx);
568 assert_eq!(
569 editor.selections.display_ranges(cx),
570 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
571 );
572 assert!(pop_history(&mut editor, cx).is_none());
573
574 // Move the cursor a large distance via the mouse.
575 // The history can jump back to the previous position.
576 editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
577 editor.end_selection(cx);
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
581 );
582 let nav_entry = pop_history(&mut editor, cx).unwrap();
583 editor.navigate(nav_entry.data.unwrap(), cx);
584 assert_eq!(nav_entry.item.id(), cx.entity_id());
585 assert_eq!(
586 editor.selections.display_ranges(cx),
587 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
588 );
589 assert!(pop_history(&mut editor, cx).is_none());
590
591 // Set scroll position to check later
592 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), cx);
593 let original_scroll_position = editor.scroll_manager.anchor();
594
595 // Jump to the end of the document and adjust scroll
596 editor.move_to_end(&MoveToEnd, cx);
597 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), cx);
598 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
599
600 let nav_entry = pop_history(&mut editor, cx).unwrap();
601 editor.navigate(nav_entry.data.unwrap(), cx);
602 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
603
604 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
605 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
606 invalid_anchor.text_anchor.buffer_id = Some(999);
607 let invalid_point = Point::new(9999, 0);
608 editor.navigate(
609 Box::new(NavigationData {
610 cursor_anchor: invalid_anchor,
611 cursor_position: invalid_point,
612 scroll_anchor: ScrollAnchor {
613 anchor: invalid_anchor,
614 offset: Default::default(),
615 },
616 scroll_top_row: invalid_point.row,
617 }),
618 cx,
619 );
620 assert_eq!(
621 editor.selections.display_ranges(cx),
622 &[editor.max_point(cx)..editor.max_point(cx)]
623 );
624 assert_eq!(
625 editor.scroll_position(cx),
626 gpui::Point::new(0., editor.max_point(cx).row() as f32)
627 );
628
629 editor
630 })
631 });
632}
633
634#[gpui::test]
635fn test_cancel(cx: &mut TestAppContext) {
636 init_test(cx, |_| {});
637
638 let view = cx.add_window(|cx| {
639 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
640 build_editor(buffer, cx)
641 });
642
643 view.update(cx, |view, cx| {
644 view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
645 view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::<f32>::zero(), cx);
646 view.end_selection(cx);
647
648 view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
649 view.update_selection(DisplayPoint::new(0, 3), 0, gpui::Point::<f32>::zero(), cx);
650 view.end_selection(cx);
651 assert_eq!(
652 view.selections.display_ranges(cx),
653 [
654 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
655 DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1),
656 ]
657 );
658 });
659
660 view.update(cx, |view, cx| {
661 view.cancel(&Cancel, cx);
662 assert_eq!(
663 view.selections.display_ranges(cx),
664 [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)]
665 );
666 });
667
668 view.update(cx, |view, cx| {
669 view.cancel(&Cancel, cx);
670 assert_eq!(
671 view.selections.display_ranges(cx),
672 [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
673 );
674 });
675}
676
677#[gpui::test]
678fn test_fold_action(cx: &mut TestAppContext) {
679 init_test(cx, |_| {});
680
681 let view = cx.add_window(|cx| {
682 let buffer = MultiBuffer::build_simple(
683 &"
684 impl Foo {
685 // Hello!
686
687 fn a() {
688 1
689 }
690
691 fn b() {
692 2
693 }
694
695 fn c() {
696 3
697 }
698 }
699 "
700 .unindent(),
701 cx,
702 );
703 build_editor(buffer.clone(), cx)
704 });
705
706 view.update(cx, |view, cx| {
707 view.change_selections(None, cx, |s| {
708 s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]);
709 });
710 view.fold(&Fold, cx);
711 assert_eq!(
712 view.display_text(cx),
713 "
714 impl Foo {
715 // Hello!
716
717 fn a() {
718 1
719 }
720
721 fn b() {⋯
722 }
723
724 fn c() {⋯
725 }
726 }
727 "
728 .unindent(),
729 );
730
731 view.fold(&Fold, cx);
732 assert_eq!(
733 view.display_text(cx),
734 "
735 impl Foo {⋯
736 }
737 "
738 .unindent(),
739 );
740
741 view.unfold_lines(&UnfoldLines, cx);
742 assert_eq!(
743 view.display_text(cx),
744 "
745 impl Foo {
746 // Hello!
747
748 fn a() {
749 1
750 }
751
752 fn b() {⋯
753 }
754
755 fn c() {⋯
756 }
757 }
758 "
759 .unindent(),
760 );
761
762 view.unfold_lines(&UnfoldLines, cx);
763 assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
764 });
765}
766
767#[gpui::test]
768fn test_move_cursor(cx: &mut TestAppContext) {
769 init_test(cx, |_| {});
770
771 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
772 let view = cx.add_window(|cx| build_editor(buffer.clone(), cx));
773
774 buffer.update(cx, |buffer, cx| {
775 buffer.edit(
776 vec![
777 (Point::new(1, 0)..Point::new(1, 0), "\t"),
778 (Point::new(1, 1)..Point::new(1, 1), "\t"),
779 ],
780 None,
781 cx,
782 );
783 });
784 view.update(cx, |view, cx| {
785 assert_eq!(
786 view.selections.display_ranges(cx),
787 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
788 );
789
790 view.move_down(&MoveDown, cx);
791 assert_eq!(
792 view.selections.display_ranges(cx),
793 &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
794 );
795
796 view.move_right(&MoveRight, cx);
797 assert_eq!(
798 view.selections.display_ranges(cx),
799 &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
800 );
801
802 view.move_left(&MoveLeft, cx);
803 assert_eq!(
804 view.selections.display_ranges(cx),
805 &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
806 );
807
808 view.move_up(&MoveUp, cx);
809 assert_eq!(
810 view.selections.display_ranges(cx),
811 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
812 );
813
814 view.move_to_end(&MoveToEnd, cx);
815 assert_eq!(
816 view.selections.display_ranges(cx),
817 &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
818 );
819
820 view.move_to_beginning(&MoveToBeginning, cx);
821 assert_eq!(
822 view.selections.display_ranges(cx),
823 &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
824 );
825
826 view.change_selections(None, cx, |s| {
827 s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]);
828 });
829 view.select_to_beginning(&SelectToBeginning, cx);
830 assert_eq!(
831 view.selections.display_ranges(cx),
832 &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)]
833 );
834
835 view.select_to_end(&SelectToEnd, cx);
836 assert_eq!(
837 view.selections.display_ranges(cx),
838 &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
839 );
840 });
841}
842
843#[gpui::test]
844fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
845 init_test(cx, |_| {});
846
847 let view = cx.add_window(|cx| {
848 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx);
849 build_editor(buffer.clone(), cx)
850 });
851
852 assert_eq!('ⓐ'.len_utf8(), 3);
853 assert_eq!('α'.len_utf8(), 2);
854
855 view.update(cx, |view, cx| {
856 view.fold_ranges(
857 vec![
858 Point::new(0, 6)..Point::new(0, 12),
859 Point::new(1, 2)..Point::new(1, 4),
860 Point::new(2, 4)..Point::new(2, 8),
861 ],
862 true,
863 cx,
864 );
865 assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε");
866
867 view.move_right(&MoveRight, cx);
868 assert_eq!(
869 view.selections.display_ranges(cx),
870 &[empty_range(0, "ⓐ".len())]
871 );
872 view.move_right(&MoveRight, cx);
873 assert_eq!(
874 view.selections.display_ranges(cx),
875 &[empty_range(0, "ⓐⓑ".len())]
876 );
877 view.move_right(&MoveRight, cx);
878 assert_eq!(
879 view.selections.display_ranges(cx),
880 &[empty_range(0, "ⓐⓑ⋯".len())]
881 );
882
883 view.move_down(&MoveDown, cx);
884 assert_eq!(
885 view.selections.display_ranges(cx),
886 &[empty_range(1, "ab⋯e".len())]
887 );
888 view.move_left(&MoveLeft, cx);
889 assert_eq!(
890 view.selections.display_ranges(cx),
891 &[empty_range(1, "ab⋯".len())]
892 );
893 view.move_left(&MoveLeft, cx);
894 assert_eq!(
895 view.selections.display_ranges(cx),
896 &[empty_range(1, "ab".len())]
897 );
898 view.move_left(&MoveLeft, cx);
899 assert_eq!(
900 view.selections.display_ranges(cx),
901 &[empty_range(1, "a".len())]
902 );
903
904 view.move_down(&MoveDown, cx);
905 assert_eq!(
906 view.selections.display_ranges(cx),
907 &[empty_range(2, "α".len())]
908 );
909 view.move_right(&MoveRight, cx);
910 assert_eq!(
911 view.selections.display_ranges(cx),
912 &[empty_range(2, "αβ".len())]
913 );
914 view.move_right(&MoveRight, cx);
915 assert_eq!(
916 view.selections.display_ranges(cx),
917 &[empty_range(2, "αβ⋯".len())]
918 );
919 view.move_right(&MoveRight, cx);
920 assert_eq!(
921 view.selections.display_ranges(cx),
922 &[empty_range(2, "αβ⋯ε".len())]
923 );
924
925 view.move_up(&MoveUp, cx);
926 assert_eq!(
927 view.selections.display_ranges(cx),
928 &[empty_range(1, "ab⋯e".len())]
929 );
930 view.move_down(&MoveDown, cx);
931 assert_eq!(
932 view.selections.display_ranges(cx),
933 &[empty_range(2, "αβ⋯ε".len())]
934 );
935 view.move_up(&MoveUp, cx);
936 assert_eq!(
937 view.selections.display_ranges(cx),
938 &[empty_range(1, "ab⋯e".len())]
939 );
940
941 view.move_up(&MoveUp, cx);
942 assert_eq!(
943 view.selections.display_ranges(cx),
944 &[empty_range(0, "ⓐⓑ".len())]
945 );
946 view.move_left(&MoveLeft, cx);
947 assert_eq!(
948 view.selections.display_ranges(cx),
949 &[empty_range(0, "ⓐ".len())]
950 );
951 view.move_left(&MoveLeft, cx);
952 assert_eq!(
953 view.selections.display_ranges(cx),
954 &[empty_range(0, "".len())]
955 );
956 });
957}
958
959#[gpui::test]
960fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
961 init_test(cx, |_| {});
962
963 let view = cx.add_window(|cx| {
964 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
965 build_editor(buffer.clone(), cx)
966 });
967 view.update(cx, |view, cx| {
968 view.change_selections(None, cx, |s| {
969 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
970 });
971 view.move_down(&MoveDown, cx);
972 assert_eq!(
973 view.selections.display_ranges(cx),
974 &[empty_range(1, "abcd".len())]
975 );
976
977 view.move_down(&MoveDown, cx);
978 assert_eq!(
979 view.selections.display_ranges(cx),
980 &[empty_range(2, "αβγ".len())]
981 );
982
983 view.move_down(&MoveDown, cx);
984 assert_eq!(
985 view.selections.display_ranges(cx),
986 &[empty_range(3, "abcd".len())]
987 );
988
989 view.move_down(&MoveDown, cx);
990 assert_eq!(
991 view.selections.display_ranges(cx),
992 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
993 );
994
995 view.move_up(&MoveUp, cx);
996 assert_eq!(
997 view.selections.display_ranges(cx),
998 &[empty_range(3, "abcd".len())]
999 );
1000
1001 view.move_up(&MoveUp, cx);
1002 assert_eq!(
1003 view.selections.display_ranges(cx),
1004 &[empty_range(2, "αβγ".len())]
1005 );
1006 });
1007}
1008
1009#[gpui::test]
1010fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1011 init_test(cx, |_| {});
1012
1013 let view = cx.add_window(|cx| {
1014 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1015 build_editor(buffer, cx)
1016 });
1017 view.update(cx, |view, cx| {
1018 view.change_selections(None, cx, |s| {
1019 s.select_display_ranges([
1020 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
1021 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
1022 ]);
1023 });
1024 });
1025
1026 view.update(cx, |view, cx| {
1027 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1028 assert_eq!(
1029 view.selections.display_ranges(cx),
1030 &[
1031 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1032 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1033 ]
1034 );
1035 });
1036
1037 view.update(cx, |view, cx| {
1038 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1039 assert_eq!(
1040 view.selections.display_ranges(cx),
1041 &[
1042 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1043 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
1044 ]
1045 );
1046 });
1047
1048 view.update(cx, |view, cx| {
1049 view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
1050 assert_eq!(
1051 view.selections.display_ranges(cx),
1052 &[
1053 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1054 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1055 ]
1056 );
1057 });
1058
1059 view.update(cx, |view, cx| {
1060 view.move_to_end_of_line(&MoveToEndOfLine, cx);
1061 assert_eq!(
1062 view.selections.display_ranges(cx),
1063 &[
1064 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1065 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
1066 ]
1067 );
1068 });
1069
1070 // Moving to the end of line again is a no-op.
1071 view.update(cx, |view, cx| {
1072 view.move_to_end_of_line(&MoveToEndOfLine, cx);
1073 assert_eq!(
1074 view.selections.display_ranges(cx),
1075 &[
1076 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1077 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
1078 ]
1079 );
1080 });
1081
1082 view.update(cx, |view, cx| {
1083 view.move_left(&MoveLeft, cx);
1084 view.select_to_beginning_of_line(
1085 &SelectToBeginningOfLine {
1086 stop_at_soft_wraps: true,
1087 },
1088 cx,
1089 );
1090 assert_eq!(
1091 view.selections.display_ranges(cx),
1092 &[
1093 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1094 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
1095 ]
1096 );
1097 });
1098
1099 view.update(cx, |view, cx| {
1100 view.select_to_beginning_of_line(
1101 &SelectToBeginningOfLine {
1102 stop_at_soft_wraps: true,
1103 },
1104 cx,
1105 );
1106 assert_eq!(
1107 view.selections.display_ranges(cx),
1108 &[
1109 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1110 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0),
1111 ]
1112 );
1113 });
1114
1115 view.update(cx, |view, cx| {
1116 view.select_to_beginning_of_line(
1117 &SelectToBeginningOfLine {
1118 stop_at_soft_wraps: true,
1119 },
1120 cx,
1121 );
1122 assert_eq!(
1123 view.selections.display_ranges(cx),
1124 &[
1125 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
1126 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
1127 ]
1128 );
1129 });
1130
1131 view.update(cx, |view, cx| {
1132 view.select_to_end_of_line(
1133 &SelectToEndOfLine {
1134 stop_at_soft_wraps: true,
1135 },
1136 cx,
1137 );
1138 assert_eq!(
1139 view.selections.display_ranges(cx),
1140 &[
1141 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
1142 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5),
1143 ]
1144 );
1145 });
1146
1147 view.update(cx, |view, cx| {
1148 view.delete_to_end_of_line(&DeleteToEndOfLine, cx);
1149 assert_eq!(view.display_text(cx), "ab\n de");
1150 assert_eq!(
1151 view.selections.display_ranges(cx),
1152 &[
1153 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1154 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
1155 ]
1156 );
1157 });
1158
1159 view.update(cx, |view, cx| {
1160 view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
1161 assert_eq!(view.display_text(cx), "\n");
1162 assert_eq!(
1163 view.selections.display_ranges(cx),
1164 &[
1165 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
1166 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
1167 ]
1168 );
1169 });
1170}
1171
1172#[gpui::test]
1173fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1174 init_test(cx, |_| {});
1175
1176 let view = cx.add_window(|cx| {
1177 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1178 build_editor(buffer, cx)
1179 });
1180 view.update(cx, |view, cx| {
1181 view.change_selections(None, cx, |s| {
1182 s.select_display_ranges([
1183 DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
1184 DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
1185 ])
1186 });
1187
1188 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1189 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx);
1190
1191 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1192 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx);
1193
1194 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1195 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx);
1196
1197 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1198 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx);
1199
1200 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1201 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx);
1202
1203 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1204 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx);
1205
1206 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1207 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx);
1208
1209 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1210 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx);
1211
1212 view.move_right(&MoveRight, cx);
1213 view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
1214 assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx);
1215
1216 view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
1217 assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx);
1218
1219 view.select_to_next_word_end(&SelectToNextWordEnd, cx);
1220 assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx);
1221 });
1222}
1223
1224#[gpui::test]
1225fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1226 init_test(cx, |_| {});
1227
1228 let view = cx.add_window(|cx| {
1229 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1230 build_editor(buffer, cx)
1231 });
1232
1233 view.update(cx, |view, cx| {
1234 view.set_wrap_width(Some(140.0.into()), cx);
1235 assert_eq!(
1236 view.display_text(cx),
1237 "use one::{\n two::three::\n four::five\n};"
1238 );
1239
1240 view.change_selections(None, cx, |s| {
1241 s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
1242 });
1243
1244 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1245 assert_eq!(
1246 view.selections.display_ranges(cx),
1247 &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
1248 );
1249
1250 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1251 assert_eq!(
1252 view.selections.display_ranges(cx),
1253 &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
1254 );
1255
1256 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1257 assert_eq!(
1258 view.selections.display_ranges(cx),
1259 &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
1260 );
1261
1262 view.move_to_next_word_end(&MoveToNextWordEnd, cx);
1263 assert_eq!(
1264 view.selections.display_ranges(cx),
1265 &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
1266 );
1267
1268 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1269 assert_eq!(
1270 view.selections.display_ranges(cx),
1271 &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
1272 );
1273
1274 view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
1275 assert_eq!(
1276 view.selections.display_ranges(cx),
1277 &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
1278 );
1279 });
1280}
1281
1282#[gpui::test]
1283async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1284 init_test(cx, |_| {});
1285 let mut cx = EditorTestContext::new(cx).await;
1286
1287 let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
1288 let window = cx.window;
1289 window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
1290
1291 cx.set_state(
1292 &r#"ˇone
1293 two
1294
1295 three
1296 fourˇ
1297 five
1298
1299 six"#
1300 .unindent(),
1301 );
1302
1303 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1304 cx.assert_editor_state(
1305 &r#"one
1306 two
1307 ˇ
1308 three
1309 four
1310 five
1311 ˇ
1312 six"#
1313 .unindent(),
1314 );
1315
1316 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1317 cx.assert_editor_state(
1318 &r#"one
1319 two
1320
1321 three
1322 four
1323 five
1324 ˇ
1325 sixˇ"#
1326 .unindent(),
1327 );
1328
1329 cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
1330 cx.assert_editor_state(
1331 &r#"one
1332 two
1333
1334 three
1335 four
1336 five
1337
1338 sixˇ"#
1339 .unindent(),
1340 );
1341
1342 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1343 cx.assert_editor_state(
1344 &r#"one
1345 two
1346
1347 three
1348 four
1349 five
1350 ˇ
1351 six"#
1352 .unindent(),
1353 );
1354
1355 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1356 cx.assert_editor_state(
1357 &r#"one
1358 two
1359 ˇ
1360 three
1361 four
1362 five
1363
1364 six"#
1365 .unindent(),
1366 );
1367
1368 cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
1369 cx.assert_editor_state(
1370 &r#"ˇone
1371 two
1372
1373 three
1374 four
1375 five
1376
1377 six"#
1378 .unindent(),
1379 );
1380}
1381
1382#[gpui::test]
1383async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
1384 init_test(cx, |_| {});
1385 let mut cx = EditorTestContext::new(cx).await;
1386 let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
1387 let window = cx.window;
1388 window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx);
1389
1390 cx.set_state(
1391 &r#"ˇone
1392 two
1393 three
1394 four
1395 five
1396 six
1397 seven
1398 eight
1399 nine
1400 ten
1401 "#,
1402 );
1403
1404 cx.update_editor(|editor, cx| {
1405 assert_eq!(
1406 editor.snapshot(cx).scroll_position(),
1407 gpui::Point::new(0., 0.)
1408 );
1409 editor.scroll_screen(&ScrollAmount::Page(1.), cx);
1410 assert_eq!(
1411 editor.snapshot(cx).scroll_position(),
1412 gpui::Point::new(0., 3.)
1413 );
1414 editor.scroll_screen(&ScrollAmount::Page(1.), cx);
1415 assert_eq!(
1416 editor.snapshot(cx).scroll_position(),
1417 gpui::Point::new(0., 6.)
1418 );
1419 editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
1420 assert_eq!(
1421 editor.snapshot(cx).scroll_position(),
1422 gpui::Point::new(0., 3.)
1423 );
1424
1425 editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
1426 assert_eq!(
1427 editor.snapshot(cx).scroll_position(),
1428 gpui::Point::new(0., 1.)
1429 );
1430 editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
1431 assert_eq!(
1432 editor.snapshot(cx).scroll_position(),
1433 gpui::Point::new(0., 3.)
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
1440 init_test(cx, |_| {});
1441 let mut cx = EditorTestContext::new(cx).await;
1442
1443 let line_height = cx.update_editor(|editor, cx| {
1444 editor.set_vertical_scroll_margin(2, cx);
1445 editor.style(cx).text.line_height(cx.font_cache())
1446 });
1447
1448 let window = cx.window;
1449 window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx);
1450
1451 cx.set_state(
1452 &r#"ˇone
1453 two
1454 three
1455 four
1456 five
1457 six
1458 seven
1459 eight
1460 nine
1461 ten
1462 "#,
1463 );
1464 cx.update_editor(|editor, cx| {
1465 assert_eq!(
1466 editor.snapshot(cx).scroll_position(),
1467 gpui::Point::new(0., 0.0)
1468 );
1469 });
1470
1471 // Add a cursor below the visible area. Since both cursors cannot fit
1472 // on screen, the editor autoscrolls to reveal the newest cursor, and
1473 // allows the vertical scroll margin below that cursor.
1474 cx.update_editor(|editor, cx| {
1475 editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
1476 selections.select_ranges([
1477 Point::new(0, 0)..Point::new(0, 0),
1478 Point::new(6, 0)..Point::new(6, 0),
1479 ]);
1480 })
1481 });
1482 cx.update_editor(|editor, cx| {
1483 assert_eq!(
1484 editor.snapshot(cx).scroll_position(),
1485 gpui::Point::new(0., 3.0)
1486 );
1487 });
1488
1489 // Move down. The editor cursor scrolls down to track the newest cursor.
1490 cx.update_editor(|editor, cx| {
1491 editor.move_down(&Default::default(), cx);
1492 });
1493 cx.update_editor(|editor, cx| {
1494 assert_eq!(
1495 editor.snapshot(cx).scroll_position(),
1496 gpui::Point::new(0., 4.0)
1497 );
1498 });
1499
1500 // Add a cursor above the visible area. Since both cursors fit on screen,
1501 // the editor scrolls to show both.
1502 cx.update_editor(|editor, cx| {
1503 editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
1504 selections.select_ranges([
1505 Point::new(1, 0)..Point::new(1, 0),
1506 Point::new(6, 0)..Point::new(6, 0),
1507 ]);
1508 })
1509 });
1510 cx.update_editor(|editor, cx| {
1511 assert_eq!(
1512 editor.snapshot(cx).scroll_position(),
1513 gpui::Point::new(0., 1.0)
1514 );
1515 });
1516}
1517
1518#[gpui::test]
1519async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
1520 init_test(cx, |_| {});
1521 let mut cx = EditorTestContext::new(cx).await;
1522
1523 let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
1524 let window = cx.window;
1525 window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
1526
1527 cx.set_state(
1528 &r#"
1529 ˇone
1530 two
1531 threeˇ
1532 four
1533 five
1534 six
1535 seven
1536 eight
1537 nine
1538 ten
1539 "#
1540 .unindent(),
1541 );
1542
1543 cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
1544 cx.assert_editor_state(
1545 &r#"
1546 one
1547 two
1548 three
1549 ˇfour
1550 five
1551 sixˇ
1552 seven
1553 eight
1554 nine
1555 ten
1556 "#
1557 .unindent(),
1558 );
1559
1560 cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
1561 cx.assert_editor_state(
1562 &r#"
1563 one
1564 two
1565 three
1566 four
1567 five
1568 six
1569 ˇseven
1570 eight
1571 nineˇ
1572 ten
1573 "#
1574 .unindent(),
1575 );
1576
1577 cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
1578 cx.assert_editor_state(
1579 &r#"
1580 one
1581 two
1582 three
1583 ˇfour
1584 five
1585 sixˇ
1586 seven
1587 eight
1588 nine
1589 ten
1590 "#
1591 .unindent(),
1592 );
1593
1594 cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
1595 cx.assert_editor_state(
1596 &r#"
1597 ˇone
1598 two
1599 threeˇ
1600 four
1601 five
1602 six
1603 seven
1604 eight
1605 nine
1606 ten
1607 "#
1608 .unindent(),
1609 );
1610
1611 // Test select collapsing
1612 cx.update_editor(|editor, cx| {
1613 editor.move_page_down(&MovePageDown::default(), cx);
1614 editor.move_page_down(&MovePageDown::default(), cx);
1615 editor.move_page_down(&MovePageDown::default(), cx);
1616 });
1617 cx.assert_editor_state(
1618 &r#"
1619 one
1620 two
1621 three
1622 four
1623 five
1624 six
1625 seven
1626 eight
1627 nine
1628 ˇten
1629 ˇ"#
1630 .unindent(),
1631 );
1632}
1633
1634#[gpui::test]
1635async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
1636 init_test(cx, |_| {});
1637 let mut cx = EditorTestContext::new(cx).await;
1638 cx.set_state("one «two threeˇ» four");
1639 cx.update_editor(|editor, cx| {
1640 editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
1641 assert_eq!(editor.text(cx), " four");
1642 });
1643}
1644
1645#[gpui::test]
1646fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
1647 init_test(cx, |_| {});
1648
1649 let view = cx.add_window(|cx| {
1650 let buffer = MultiBuffer::build_simple("one two three four", cx);
1651 build_editor(buffer.clone(), cx)
1652 });
1653
1654 view.update(cx, |view, cx| {
1655 view.change_selections(None, cx, |s| {
1656 s.select_display_ranges([
1657 // an empty selection - the preceding word fragment is deleted
1658 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1659 // characters selected - they are deleted
1660 DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
1661 ])
1662 });
1663 view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
1664 assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
1665 });
1666
1667 view.update(cx, |view, cx| {
1668 view.change_selections(None, cx, |s| {
1669 s.select_display_ranges([
1670 // an empty selection - the following word fragment is deleted
1671 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
1672 // characters selected - they are deleted
1673 DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
1674 ])
1675 });
1676 view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
1677 assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
1678 });
1679}
1680
1681#[gpui::test]
1682fn test_newline(cx: &mut TestAppContext) {
1683 init_test(cx, |_| {});
1684
1685 let view = cx.add_window(|cx| {
1686 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
1687 build_editor(buffer.clone(), cx)
1688 });
1689
1690 view.update(cx, |view, cx| {
1691 view.change_selections(None, cx, |s| {
1692 s.select_display_ranges([
1693 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
1694 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
1695 DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
1696 ])
1697 });
1698
1699 view.newline(&Newline, cx);
1700 assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n");
1701 });
1702}
1703
1704#[gpui::test]
1705fn test_newline_with_old_selections(cx: &mut TestAppContext) {
1706 init_test(cx, |_| {});
1707
1708 let editor = cx.add_window(|cx| {
1709 let buffer = MultiBuffer::build_simple(
1710 "
1711 a
1712 b(
1713 X
1714 )
1715 c(
1716 X
1717 )
1718 "
1719 .unindent()
1720 .as_str(),
1721 cx,
1722 );
1723 let mut editor = build_editor(buffer.clone(), cx);
1724 editor.change_selections(None, cx, |s| {
1725 s.select_ranges([
1726 Point::new(2, 4)..Point::new(2, 5),
1727 Point::new(5, 4)..Point::new(5, 5),
1728 ])
1729 });
1730 editor
1731 });
1732
1733 editor.update(cx, |editor, cx| {
1734 // Edit the buffer directly, deleting ranges surrounding the editor's selections
1735 editor.buffer.update(cx, |buffer, cx| {
1736 buffer.edit(
1737 [
1738 (Point::new(1, 2)..Point::new(3, 0), ""),
1739 (Point::new(4, 2)..Point::new(6, 0), ""),
1740 ],
1741 None,
1742 cx,
1743 );
1744 assert_eq!(
1745 buffer.read(cx).text(),
1746 "
1747 a
1748 b()
1749 c()
1750 "
1751 .unindent()
1752 );
1753 });
1754 assert_eq!(
1755 editor.selections.ranges(cx),
1756 &[
1757 Point::new(1, 2)..Point::new(1, 2),
1758 Point::new(2, 2)..Point::new(2, 2),
1759 ],
1760 );
1761
1762 editor.newline(&Newline, cx);
1763 assert_eq!(
1764 editor.text(cx),
1765 "
1766 a
1767 b(
1768 )
1769 c(
1770 )
1771 "
1772 .unindent()
1773 );
1774
1775 // The selections are moved after the inserted newlines
1776 assert_eq!(
1777 editor.selections.ranges(cx),
1778 &[
1779 Point::new(2, 0)..Point::new(2, 0),
1780 Point::new(4, 0)..Point::new(4, 0),
1781 ],
1782 );
1783 });
1784}
1785
1786#[gpui::test]
1787async fn test_newline_above(cx: &mut gpui::TestAppContext) {
1788 init_test(cx, |settings| {
1789 settings.defaults.tab_size = NonZeroU32::new(4)
1790 });
1791
1792 let language = Arc::new(
1793 Language::new(
1794 LanguageConfig::default(),
1795 Some(tree_sitter_rust::language()),
1796 )
1797 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
1798 .unwrap(),
1799 );
1800
1801 let mut cx = EditorTestContext::new(cx).await;
1802 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1803 cx.set_state(indoc! {"
1804 const a: ˇA = (
1805 (ˇ
1806 «const_functionˇ»(ˇ),
1807 so«mˇ»et«hˇ»ing_ˇelse,ˇ
1808 )ˇ
1809 ˇ);ˇ
1810 "});
1811
1812 cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
1813 cx.assert_editor_state(indoc! {"
1814 ˇ
1815 const a: A = (
1816 ˇ
1817 (
1818 ˇ
1819 ˇ
1820 const_function(),
1821 ˇ
1822 ˇ
1823 ˇ
1824 ˇ
1825 something_else,
1826 ˇ
1827 )
1828 ˇ
1829 ˇ
1830 );
1831 "});
1832}
1833
1834#[gpui::test]
1835async fn test_newline_below(cx: &mut gpui::TestAppContext) {
1836 init_test(cx, |settings| {
1837 settings.defaults.tab_size = NonZeroU32::new(4)
1838 });
1839
1840 let language = Arc::new(
1841 Language::new(
1842 LanguageConfig::default(),
1843 Some(tree_sitter_rust::language()),
1844 )
1845 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
1846 .unwrap(),
1847 );
1848
1849 let mut cx = EditorTestContext::new(cx).await;
1850 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1851 cx.set_state(indoc! {"
1852 const a: ˇA = (
1853 (ˇ
1854 «const_functionˇ»(ˇ),
1855 so«mˇ»et«hˇ»ing_ˇelse,ˇ
1856 )ˇ
1857 ˇ);ˇ
1858 "});
1859
1860 cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
1861 cx.assert_editor_state(indoc! {"
1862 const a: A = (
1863 ˇ
1864 (
1865 ˇ
1866 const_function(),
1867 ˇ
1868 ˇ
1869 something_else,
1870 ˇ
1871 ˇ
1872 ˇ
1873 ˇ
1874 )
1875 ˇ
1876 );
1877 ˇ
1878 ˇ
1879 "});
1880}
1881
1882#[gpui::test]
1883async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
1884 init_test(cx, |settings| {
1885 settings.defaults.tab_size = NonZeroU32::new(4)
1886 });
1887
1888 let language = Arc::new(Language::new(
1889 LanguageConfig {
1890 line_comment: Some("//".into()),
1891 ..LanguageConfig::default()
1892 },
1893 None,
1894 ));
1895 {
1896 let mut cx = EditorTestContext::new(cx).await;
1897 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
1898 cx.set_state(indoc! {"
1899 // Fooˇ
1900 "});
1901
1902 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1903 cx.assert_editor_state(indoc! {"
1904 // Foo
1905 //ˇ
1906 "});
1907 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
1908 cx.set_state(indoc! {"
1909 ˇ// Foo
1910 "});
1911 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1912 cx.assert_editor_state(indoc! {"
1913
1914 ˇ// Foo
1915 "});
1916 }
1917 // Ensure that comment continuations can be disabled.
1918 update_test_language_settings(cx, |settings| {
1919 settings.defaults.extend_comment_on_newline = Some(false);
1920 });
1921 let mut cx = EditorTestContext::new(cx).await;
1922 cx.set_state(indoc! {"
1923 // Fooˇ
1924 "});
1925 cx.update_editor(|e, cx| e.newline(&Newline, cx));
1926 cx.assert_editor_state(indoc! {"
1927 // Foo
1928 ˇ
1929 "});
1930}
1931
1932#[gpui::test]
1933fn test_insert_with_old_selections(cx: &mut TestAppContext) {
1934 init_test(cx, |_| {});
1935
1936 let editor = cx.add_window(|cx| {
1937 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
1938 let mut editor = build_editor(buffer.clone(), cx);
1939 editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
1940 editor
1941 });
1942
1943 editor.update(cx, |editor, cx| {
1944 // Edit the buffer directly, deleting ranges surrounding the editor's selections
1945 editor.buffer.update(cx, |buffer, cx| {
1946 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
1947 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
1948 });
1949 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
1950
1951 editor.insert("Z", cx);
1952 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
1953
1954 // The selections are moved after the inserted characters
1955 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
1956 });
1957}
1958
1959#[gpui::test]
1960async fn test_tab(cx: &mut gpui::TestAppContext) {
1961 init_test(cx, |settings| {
1962 settings.defaults.tab_size = NonZeroU32::new(3)
1963 });
1964
1965 let mut cx = EditorTestContext::new(cx).await;
1966 cx.set_state(indoc! {"
1967 ˇabˇc
1968 ˇ🏀ˇ🏀ˇefg
1969 dˇ
1970 "});
1971 cx.update_editor(|e, cx| e.tab(&Tab, cx));
1972 cx.assert_editor_state(indoc! {"
1973 ˇab ˇc
1974 ˇ🏀 ˇ🏀 ˇefg
1975 d ˇ
1976 "});
1977
1978 cx.set_state(indoc! {"
1979 a
1980 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
1981 "});
1982 cx.update_editor(|e, cx| e.tab(&Tab, cx));
1983 cx.assert_editor_state(indoc! {"
1984 a
1985 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
1986 "});
1987}
1988
1989#[gpui::test]
1990async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
1991 init_test(cx, |_| {});
1992
1993 let mut cx = EditorTestContext::new(cx).await;
1994 let language = Arc::new(
1995 Language::new(
1996 LanguageConfig::default(),
1997 Some(tree_sitter_rust::language()),
1998 )
1999 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2000 .unwrap(),
2001 );
2002 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2003
2004 // cursors that are already at the suggested indent level insert
2005 // a soft tab. cursors that are to the left of the suggested indent
2006 // auto-indent their line.
2007 cx.set_state(indoc! {"
2008 ˇ
2009 const a: B = (
2010 c(
2011 d(
2012 ˇ
2013 )
2014 ˇ
2015 ˇ )
2016 );
2017 "});
2018 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2019 cx.assert_editor_state(indoc! {"
2020 ˇ
2021 const a: B = (
2022 c(
2023 d(
2024 ˇ
2025 )
2026 ˇ
2027 ˇ)
2028 );
2029 "});
2030
2031 // handle auto-indent when there are multiple cursors on the same line
2032 cx.set_state(indoc! {"
2033 const a: B = (
2034 c(
2035 ˇ ˇ
2036 ˇ )
2037 );
2038 "});
2039 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2040 cx.assert_editor_state(indoc! {"
2041 const a: B = (
2042 c(
2043 ˇ
2044 ˇ)
2045 );
2046 "});
2047}
2048
2049#[gpui::test]
2050async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
2051 init_test(cx, |settings| {
2052 settings.defaults.tab_size = NonZeroU32::new(4)
2053 });
2054
2055 let language = Arc::new(
2056 Language::new(
2057 LanguageConfig::default(),
2058 Some(tree_sitter_rust::language()),
2059 )
2060 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2061 .unwrap(),
2062 );
2063
2064 let mut cx = EditorTestContext::new(cx).await;
2065 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2066 cx.set_state(indoc! {"
2067 fn a() {
2068 if b {
2069 \t ˇc
2070 }
2071 }
2072 "});
2073
2074 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2075 cx.assert_editor_state(indoc! {"
2076 fn a() {
2077 if b {
2078 ˇc
2079 }
2080 }
2081 "});
2082}
2083
2084#[gpui::test]
2085async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
2086 init_test(cx, |settings| {
2087 settings.defaults.tab_size = NonZeroU32::new(4);
2088 });
2089
2090 let mut cx = EditorTestContext::new(cx).await;
2091
2092 cx.set_state(indoc! {"
2093 «oneˇ» «twoˇ»
2094 three
2095 four
2096 "});
2097 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2098 cx.assert_editor_state(indoc! {"
2099 «oneˇ» «twoˇ»
2100 three
2101 four
2102 "});
2103
2104 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2105 cx.assert_editor_state(indoc! {"
2106 «oneˇ» «twoˇ»
2107 three
2108 four
2109 "});
2110
2111 // select across line ending
2112 cx.set_state(indoc! {"
2113 one two
2114 t«hree
2115 ˇ» four
2116 "});
2117 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2118 cx.assert_editor_state(indoc! {"
2119 one two
2120 t«hree
2121 ˇ» four
2122 "});
2123
2124 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2125 cx.assert_editor_state(indoc! {"
2126 one two
2127 t«hree
2128 ˇ» four
2129 "});
2130
2131 // Ensure that indenting/outdenting works when the cursor is at column 0.
2132 cx.set_state(indoc! {"
2133 one two
2134 ˇthree
2135 four
2136 "});
2137 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2138 cx.assert_editor_state(indoc! {"
2139 one two
2140 ˇthree
2141 four
2142 "});
2143
2144 cx.set_state(indoc! {"
2145 one two
2146 ˇ three
2147 four
2148 "});
2149 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2150 cx.assert_editor_state(indoc! {"
2151 one two
2152 ˇthree
2153 four
2154 "});
2155}
2156
2157#[gpui::test]
2158async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
2159 init_test(cx, |settings| {
2160 settings.defaults.hard_tabs = Some(true);
2161 });
2162
2163 let mut cx = EditorTestContext::new(cx).await;
2164
2165 // select two ranges on one line
2166 cx.set_state(indoc! {"
2167 «oneˇ» «twoˇ»
2168 three
2169 four
2170 "});
2171 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2172 cx.assert_editor_state(indoc! {"
2173 \t«oneˇ» «twoˇ»
2174 three
2175 four
2176 "});
2177 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2178 cx.assert_editor_state(indoc! {"
2179 \t\t«oneˇ» «twoˇ»
2180 three
2181 four
2182 "});
2183 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2184 cx.assert_editor_state(indoc! {"
2185 \t«oneˇ» «twoˇ»
2186 three
2187 four
2188 "});
2189 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2190 cx.assert_editor_state(indoc! {"
2191 «oneˇ» «twoˇ»
2192 three
2193 four
2194 "});
2195
2196 // select across a line ending
2197 cx.set_state(indoc! {"
2198 one two
2199 t«hree
2200 ˇ»four
2201 "});
2202 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2203 cx.assert_editor_state(indoc! {"
2204 one two
2205 \tt«hree
2206 ˇ»four
2207 "});
2208 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2209 cx.assert_editor_state(indoc! {"
2210 one two
2211 \t\tt«hree
2212 ˇ»four
2213 "});
2214 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2215 cx.assert_editor_state(indoc! {"
2216 one two
2217 \tt«hree
2218 ˇ»four
2219 "});
2220 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2221 cx.assert_editor_state(indoc! {"
2222 one two
2223 t«hree
2224 ˇ»four
2225 "});
2226
2227 // Ensure that indenting/outdenting works when the cursor is at column 0.
2228 cx.set_state(indoc! {"
2229 one two
2230 ˇthree
2231 four
2232 "});
2233 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2234 cx.assert_editor_state(indoc! {"
2235 one two
2236 ˇthree
2237 four
2238 "});
2239 cx.update_editor(|e, cx| e.tab(&Tab, cx));
2240 cx.assert_editor_state(indoc! {"
2241 one two
2242 \tˇthree
2243 four
2244 "});
2245 cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
2246 cx.assert_editor_state(indoc! {"
2247 one two
2248 ˇthree
2249 four
2250 "});
2251}
2252
2253#[gpui::test]
2254fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
2255 init_test(cx, |settings| {
2256 settings.languages.extend([
2257 (
2258 "TOML".into(),
2259 LanguageSettingsContent {
2260 tab_size: NonZeroU32::new(2),
2261 ..Default::default()
2262 },
2263 ),
2264 (
2265 "Rust".into(),
2266 LanguageSettingsContent {
2267 tab_size: NonZeroU32::new(4),
2268 ..Default::default()
2269 },
2270 ),
2271 ]);
2272 });
2273
2274 let toml_language = Arc::new(Language::new(
2275 LanguageConfig {
2276 name: "TOML".into(),
2277 ..Default::default()
2278 },
2279 None,
2280 ));
2281 let rust_language = Arc::new(Language::new(
2282 LanguageConfig {
2283 name: "Rust".into(),
2284 ..Default::default()
2285 },
2286 None,
2287 ));
2288
2289 let toml_buffer = cx.build_model(|cx| {
2290 Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n").with_language(toml_language, cx)
2291 });
2292 let rust_buffer = cx.build_model(|cx| {
2293 Buffer::new(0, cx.entity_id().as_u64(), "const c: usize = 3;\n")
2294 .with_language(rust_language, cx)
2295 });
2296 let multibuffer = cx.build_model(|cx| {
2297 let mut multibuffer = MultiBuffer::new(0);
2298 multibuffer.push_excerpts(
2299 toml_buffer.clone(),
2300 [ExcerptRange {
2301 context: Point::new(0, 0)..Point::new(2, 0),
2302 primary: None,
2303 }],
2304 cx,
2305 );
2306 multibuffer.push_excerpts(
2307 rust_buffer.clone(),
2308 [ExcerptRange {
2309 context: Point::new(0, 0)..Point::new(1, 0),
2310 primary: None,
2311 }],
2312 cx,
2313 );
2314 multibuffer
2315 });
2316
2317 cx.add_window(|cx| {
2318 let mut editor = build_editor(multibuffer, cx);
2319
2320 assert_eq!(
2321 editor.text(cx),
2322 indoc! {"
2323 a = 1
2324 b = 2
2325
2326 const c: usize = 3;
2327 "}
2328 );
2329
2330 select_ranges(
2331 &mut editor,
2332 indoc! {"
2333 «aˇ» = 1
2334 b = 2
2335
2336 «const c:ˇ» usize = 3;
2337 "},
2338 cx,
2339 );
2340
2341 editor.tab(&Tab, cx);
2342 assert_text_with_selections(
2343 &mut editor,
2344 indoc! {"
2345 «aˇ» = 1
2346 b = 2
2347
2348 «const c:ˇ» usize = 3;
2349 "},
2350 cx,
2351 );
2352 editor.tab_prev(&TabPrev, cx);
2353 assert_text_with_selections(
2354 &mut editor,
2355 indoc! {"
2356 «aˇ» = 1
2357 b = 2
2358
2359 «const c:ˇ» usize = 3;
2360 "},
2361 cx,
2362 );
2363
2364 editor
2365 });
2366}
2367
2368#[gpui::test]
2369async fn test_backspace(cx: &mut gpui::TestAppContext) {
2370 init_test(cx, |_| {});
2371
2372 let mut cx = EditorTestContext::new(cx).await;
2373
2374 // Basic backspace
2375 cx.set_state(indoc! {"
2376 onˇe two three
2377 fou«rˇ» five six
2378 seven «ˇeight nine
2379 »ten
2380 "});
2381 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2382 cx.assert_editor_state(indoc! {"
2383 oˇe two three
2384 fouˇ five six
2385 seven ˇten
2386 "});
2387
2388 // Test backspace inside and around indents
2389 cx.set_state(indoc! {"
2390 zero
2391 ˇone
2392 ˇtwo
2393 ˇ ˇ ˇ three
2394 ˇ ˇ four
2395 "});
2396 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2397 cx.assert_editor_state(indoc! {"
2398 zero
2399 ˇone
2400 ˇtwo
2401 ˇ threeˇ four
2402 "});
2403
2404 // Test backspace with line_mode set to true
2405 cx.update_editor(|e, _| e.selections.line_mode = true);
2406 cx.set_state(indoc! {"
2407 The ˇquick ˇbrown
2408 fox jumps over
2409 the lazy dog
2410 ˇThe qu«ick bˇ»rown"});
2411 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2412 cx.assert_editor_state(indoc! {"
2413 ˇfox jumps over
2414 the lazy dogˇ"});
2415}
2416
2417#[gpui::test]
2418async fn test_delete(cx: &mut gpui::TestAppContext) {
2419 init_test(cx, |_| {});
2420
2421 let mut cx = EditorTestContext::new(cx).await;
2422 cx.set_state(indoc! {"
2423 onˇe two three
2424 fou«rˇ» five six
2425 seven «ˇeight nine
2426 »ten
2427 "});
2428 cx.update_editor(|e, cx| e.delete(&Delete, cx));
2429 cx.assert_editor_state(indoc! {"
2430 onˇ two three
2431 fouˇ five six
2432 seven ˇten
2433 "});
2434
2435 // Test backspace with line_mode set to true
2436 cx.update_editor(|e, _| e.selections.line_mode = true);
2437 cx.set_state(indoc! {"
2438 The ˇquick ˇbrown
2439 fox «ˇjum»ps over
2440 the lazy dog
2441 ˇThe qu«ick bˇ»rown"});
2442 cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
2443 cx.assert_editor_state("ˇthe lazy dogˇ");
2444}
2445
2446#[gpui::test]
2447fn test_delete_line(cx: &mut TestAppContext) {
2448 init_test(cx, |_| {});
2449
2450 let view = cx.add_window(|cx| {
2451 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2452 build_editor(buffer, cx)
2453 });
2454 view.update(cx, |view, cx| {
2455 view.change_selections(None, cx, |s| {
2456 s.select_display_ranges([
2457 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
2458 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
2459 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
2460 ])
2461 });
2462 view.delete_line(&DeleteLine, cx);
2463 assert_eq!(view.display_text(cx), "ghi");
2464 assert_eq!(
2465 view.selections.display_ranges(cx),
2466 vec![
2467 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
2468 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)
2469 ]
2470 );
2471 });
2472
2473 let view = cx.add_window(|cx| {
2474 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2475 build_editor(buffer, cx)
2476 });
2477 view.update(cx, |view, cx| {
2478 view.change_selections(None, cx, |s| {
2479 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
2480 });
2481 view.delete_line(&DeleteLine, cx);
2482 assert_eq!(view.display_text(cx), "ghi\n");
2483 assert_eq!(
2484 view.selections.display_ranges(cx),
2485 vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
2486 );
2487 });
2488}
2489
2490#[gpui::test]
2491fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
2492 init_test(cx, |_| {});
2493
2494 cx.add_window(|cx| {
2495 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
2496 let mut editor = build_editor(buffer.clone(), cx);
2497 let buffer = buffer.read(cx).as_singleton().unwrap();
2498
2499 assert_eq!(
2500 editor.selections.ranges::<Point>(cx),
2501 &[Point::new(0, 0)..Point::new(0, 0)]
2502 );
2503
2504 // When on single line, replace newline at end by space
2505 editor.join_lines(&JoinLines, cx);
2506 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
2507 assert_eq!(
2508 editor.selections.ranges::<Point>(cx),
2509 &[Point::new(0, 3)..Point::new(0, 3)]
2510 );
2511
2512 // When multiple lines are selected, remove newlines that are spanned by the selection
2513 editor.change_selections(None, cx, |s| {
2514 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
2515 });
2516 editor.join_lines(&JoinLines, cx);
2517 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
2518 assert_eq!(
2519 editor.selections.ranges::<Point>(cx),
2520 &[Point::new(0, 11)..Point::new(0, 11)]
2521 );
2522
2523 // Undo should be transactional
2524 editor.undo(&Undo, cx);
2525 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
2526 assert_eq!(
2527 editor.selections.ranges::<Point>(cx),
2528 &[Point::new(0, 5)..Point::new(2, 2)]
2529 );
2530
2531 // When joining an empty line don't insert a space
2532 editor.change_selections(None, cx, |s| {
2533 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
2534 });
2535 editor.join_lines(&JoinLines, cx);
2536 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
2537 assert_eq!(
2538 editor.selections.ranges::<Point>(cx),
2539 [Point::new(2, 3)..Point::new(2, 3)]
2540 );
2541
2542 // We can remove trailing newlines
2543 editor.join_lines(&JoinLines, cx);
2544 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
2545 assert_eq!(
2546 editor.selections.ranges::<Point>(cx),
2547 [Point::new(2, 3)..Point::new(2, 3)]
2548 );
2549
2550 // We don't blow up on the last line
2551 editor.join_lines(&JoinLines, cx);
2552 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
2553 assert_eq!(
2554 editor.selections.ranges::<Point>(cx),
2555 [Point::new(2, 3)..Point::new(2, 3)]
2556 );
2557
2558 // reset to test indentation
2559 editor.buffer.update(cx, |buffer, cx| {
2560 buffer.edit(
2561 [
2562 (Point::new(1, 0)..Point::new(1, 2), " "),
2563 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
2564 ],
2565 None,
2566 cx,
2567 )
2568 });
2569
2570 // We remove any leading spaces
2571 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
2572 editor.change_selections(None, cx, |s| {
2573 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
2574 });
2575 editor.join_lines(&JoinLines, cx);
2576 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
2577
2578 // We don't insert a space for a line containing only spaces
2579 editor.join_lines(&JoinLines, cx);
2580 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
2581
2582 // We ignore any leading tabs
2583 editor.join_lines(&JoinLines, cx);
2584 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
2585
2586 editor
2587 });
2588}
2589
2590#[gpui::test]
2591fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
2592 init_test(cx, |_| {});
2593
2594 cx.add_window(|cx| {
2595 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
2596 let mut editor = build_editor(buffer.clone(), cx);
2597 let buffer = buffer.read(cx).as_singleton().unwrap();
2598
2599 editor.change_selections(None, cx, |s| {
2600 s.select_ranges([
2601 Point::new(0, 2)..Point::new(1, 1),
2602 Point::new(1, 2)..Point::new(1, 2),
2603 Point::new(3, 1)..Point::new(3, 2),
2604 ])
2605 });
2606
2607 editor.join_lines(&JoinLines, cx);
2608 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
2609
2610 assert_eq!(
2611 editor.selections.ranges::<Point>(cx),
2612 [
2613 Point::new(0, 7)..Point::new(0, 7),
2614 Point::new(1, 3)..Point::new(1, 3)
2615 ]
2616 );
2617 editor
2618 });
2619}
2620
2621#[gpui::test]
2622async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
2623 init_test(cx, |_| {});
2624
2625 let mut cx = EditorTestContext::new(cx).await;
2626
2627 // Test sort_lines_case_insensitive()
2628 cx.set_state(indoc! {"
2629 «z
2630 y
2631 x
2632 Z
2633 Y
2634 Xˇ»
2635 "});
2636 cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
2637 cx.assert_editor_state(indoc! {"
2638 «x
2639 X
2640 y
2641 Y
2642 z
2643 Zˇ»
2644 "});
2645
2646 // Test reverse_lines()
2647 cx.set_state(indoc! {"
2648 «5
2649 4
2650 3
2651 2
2652 1ˇ»
2653 "});
2654 cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
2655 cx.assert_editor_state(indoc! {"
2656 «1
2657 2
2658 3
2659 4
2660 5ˇ»
2661 "});
2662
2663 // Skip testing shuffle_line()
2664
2665 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
2666 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
2667
2668 // Don't manipulate when cursor is on single line, but expand the selection
2669 cx.set_state(indoc! {"
2670 ddˇdd
2671 ccc
2672 bb
2673 a
2674 "});
2675 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2676 cx.assert_editor_state(indoc! {"
2677 «ddddˇ»
2678 ccc
2679 bb
2680 a
2681 "});
2682
2683 // Basic manipulate case
2684 // Start selection moves to column 0
2685 // End of selection shrinks to fit shorter line
2686 cx.set_state(indoc! {"
2687 dd«d
2688 ccc
2689 bb
2690 aaaaaˇ»
2691 "});
2692 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2693 cx.assert_editor_state(indoc! {"
2694 «aaaaa
2695 bb
2696 ccc
2697 dddˇ»
2698 "});
2699
2700 // Manipulate case with newlines
2701 cx.set_state(indoc! {"
2702 dd«d
2703 ccc
2704
2705 bb
2706 aaaaa
2707
2708 ˇ»
2709 "});
2710 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2711 cx.assert_editor_state(indoc! {"
2712 «
2713
2714 aaaaa
2715 bb
2716 ccc
2717 dddˇ»
2718
2719 "});
2720}
2721
2722#[gpui::test]
2723async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
2724 init_test(cx, |_| {});
2725
2726 let mut cx = EditorTestContext::new(cx).await;
2727
2728 // Manipulate with multiple selections on a single line
2729 cx.set_state(indoc! {"
2730 dd«dd
2731 cˇ»c«c
2732 bb
2733 aaaˇ»aa
2734 "});
2735 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2736 cx.assert_editor_state(indoc! {"
2737 «aaaaa
2738 bb
2739 ccc
2740 ddddˇ»
2741 "});
2742
2743 // Manipulate with multiple disjoin selections
2744 cx.set_state(indoc! {"
2745 5«
2746 4
2747 3
2748 2
2749 1ˇ»
2750
2751 dd«dd
2752 ccc
2753 bb
2754 aaaˇ»aa
2755 "});
2756 cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
2757 cx.assert_editor_state(indoc! {"
2758 «1
2759 2
2760 3
2761 4
2762 5ˇ»
2763
2764 «aaaaa
2765 bb
2766 ccc
2767 ddddˇ»
2768 "});
2769}
2770
2771#[gpui::test]
2772async fn test_manipulate_text(cx: &mut TestAppContext) {
2773 init_test(cx, |_| {});
2774
2775 let mut cx = EditorTestContext::new(cx).await;
2776
2777 // Test convert_to_upper_case()
2778 cx.set_state(indoc! {"
2779 «hello worldˇ»
2780 "});
2781 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
2782 cx.assert_editor_state(indoc! {"
2783 «HELLO WORLDˇ»
2784 "});
2785
2786 // Test convert_to_lower_case()
2787 cx.set_state(indoc! {"
2788 «HELLO WORLDˇ»
2789 "});
2790 cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
2791 cx.assert_editor_state(indoc! {"
2792 «hello worldˇ»
2793 "});
2794
2795 // Test multiple line, single selection case
2796 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
2797 cx.set_state(indoc! {"
2798 «The quick brown
2799 fox jumps over
2800 the lazy dogˇ»
2801 "});
2802 cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
2803 cx.assert_editor_state(indoc! {"
2804 «The Quick Brown
2805 Fox Jumps Over
2806 The Lazy Dogˇ»
2807 "});
2808
2809 // Test multiple line, single selection case
2810 // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
2811 cx.set_state(indoc! {"
2812 «The quick brown
2813 fox jumps over
2814 the lazy dogˇ»
2815 "});
2816 cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
2817 cx.assert_editor_state(indoc! {"
2818 «TheQuickBrown
2819 FoxJumpsOver
2820 TheLazyDogˇ»
2821 "});
2822
2823 // From here on out, test more complex cases of manipulate_text()
2824
2825 // Test no selection case - should affect words cursors are in
2826 // Cursor at beginning, middle, and end of word
2827 cx.set_state(indoc! {"
2828 ˇhello big beauˇtiful worldˇ
2829 "});
2830 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
2831 cx.assert_editor_state(indoc! {"
2832 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
2833 "});
2834
2835 // Test multiple selections on a single line and across multiple lines
2836 cx.set_state(indoc! {"
2837 «Theˇ» quick «brown
2838 foxˇ» jumps «overˇ»
2839 the «lazyˇ» dog
2840 "});
2841 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
2842 cx.assert_editor_state(indoc! {"
2843 «THEˇ» quick «BROWN
2844 FOXˇ» jumps «OVERˇ»
2845 the «LAZYˇ» dog
2846 "});
2847
2848 // Test case where text length grows
2849 cx.set_state(indoc! {"
2850 «tschüߡ»
2851 "});
2852 cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
2853 cx.assert_editor_state(indoc! {"
2854 «TSCHÜSSˇ»
2855 "});
2856
2857 // Test to make sure we don't crash when text shrinks
2858 cx.set_state(indoc! {"
2859 aaa_bbbˇ
2860 "});
2861 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
2862 cx.assert_editor_state(indoc! {"
2863 «aaaBbbˇ»
2864 "});
2865
2866 // Test to make sure we all aware of the fact that each word can grow and shrink
2867 // Final selections should be aware of this fact
2868 cx.set_state(indoc! {"
2869 aaa_bˇbb bbˇb_ccc ˇccc_ddd
2870 "});
2871 cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
2872 cx.assert_editor_state(indoc! {"
2873 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
2874 "});
2875}
2876
2877#[gpui::test]
2878fn test_duplicate_line(cx: &mut TestAppContext) {
2879 init_test(cx, |_| {});
2880
2881 let view = cx.add_window(|cx| {
2882 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2883 build_editor(buffer, cx)
2884 });
2885 view.update(cx, |view, cx| {
2886 view.change_selections(None, cx, |s| {
2887 s.select_display_ranges([
2888 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
2889 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
2890 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
2891 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
2892 ])
2893 });
2894 view.duplicate_line(&DuplicateLine, cx);
2895 assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
2896 assert_eq!(
2897 view.selections.display_ranges(cx),
2898 vec![
2899 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
2900 DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
2901 DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
2902 DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
2903 ]
2904 );
2905 });
2906
2907 let view = cx.add_window(|cx| {
2908 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
2909 build_editor(buffer, cx)
2910 });
2911 view.update(cx, |view, cx| {
2912 view.change_selections(None, cx, |s| {
2913 s.select_display_ranges([
2914 DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
2915 DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
2916 ])
2917 });
2918 view.duplicate_line(&DuplicateLine, cx);
2919 assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
2920 assert_eq!(
2921 view.selections.display_ranges(cx),
2922 vec![
2923 DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
2924 DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
2925 ]
2926 );
2927 });
2928}
2929
2930#[gpui::test]
2931fn test_move_line_up_down(cx: &mut TestAppContext) {
2932 init_test(cx, |_| {});
2933
2934 let view = cx.add_window(|cx| {
2935 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
2936 build_editor(buffer, cx)
2937 });
2938 view.update(cx, |view, cx| {
2939 view.fold_ranges(
2940 vec![
2941 Point::new(0, 2)..Point::new(1, 2),
2942 Point::new(2, 3)..Point::new(4, 1),
2943 Point::new(7, 0)..Point::new(8, 4),
2944 ],
2945 true,
2946 cx,
2947 );
2948 view.change_selections(None, cx, |s| {
2949 s.select_display_ranges([
2950 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
2951 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
2952 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
2953 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
2954 ])
2955 });
2956 assert_eq!(
2957 view.display_text(cx),
2958 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
2959 );
2960
2961 view.move_line_up(&MoveLineUp, cx);
2962 assert_eq!(
2963 view.display_text(cx),
2964 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
2965 );
2966 assert_eq!(
2967 view.selections.display_ranges(cx),
2968 vec![
2969 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
2970 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
2971 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
2972 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
2973 ]
2974 );
2975 });
2976
2977 view.update(cx, |view, cx| {
2978 view.move_line_down(&MoveLineDown, cx);
2979 assert_eq!(
2980 view.display_text(cx),
2981 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
2982 );
2983 assert_eq!(
2984 view.selections.display_ranges(cx),
2985 vec![
2986 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
2987 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
2988 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
2989 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
2990 ]
2991 );
2992 });
2993
2994 view.update(cx, |view, cx| {
2995 view.move_line_down(&MoveLineDown, cx);
2996 assert_eq!(
2997 view.display_text(cx),
2998 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
2999 );
3000 assert_eq!(
3001 view.selections.display_ranges(cx),
3002 vec![
3003 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3004 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
3005 DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
3006 DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
3007 ]
3008 );
3009 });
3010
3011 view.update(cx, |view, cx| {
3012 view.move_line_up(&MoveLineUp, cx);
3013 assert_eq!(
3014 view.display_text(cx),
3015 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
3016 );
3017 assert_eq!(
3018 view.selections.display_ranges(cx),
3019 vec![
3020 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
3021 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
3022 DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
3023 DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
3024 ]
3025 );
3026 });
3027}
3028
3029#[gpui::test]
3030fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
3031 init_test(cx, |_| {});
3032
3033 let editor = cx.add_window(|cx| {
3034 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
3035 build_editor(buffer, cx)
3036 });
3037 editor.update(cx, |editor, cx| {
3038 let snapshot = editor.buffer.read(cx).snapshot(cx);
3039 editor.insert_blocks(
3040 [BlockProperties {
3041 style: BlockStyle::Fixed,
3042 position: snapshot.anchor_after(Point::new(2, 0)),
3043 disposition: BlockDisposition::Below,
3044 height: 1,
3045 render: Arc::new(|_| Empty::new().into_any()),
3046 }],
3047 Some(Autoscroll::fit()),
3048 cx,
3049 );
3050 editor.change_selections(None, cx, |s| {
3051 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
3052 });
3053 editor.move_line_down(&MoveLineDown, cx);
3054 });
3055}
3056
3057#[gpui::test]
3058fn test_transpose(cx: &mut TestAppContext) {
3059 init_test(cx, |_| {});
3060
3061 _ = cx.add_window(|cx| {
3062 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
3063
3064 editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
3065 editor.transpose(&Default::default(), cx);
3066 assert_eq!(editor.text(cx), "bac");
3067 assert_eq!(editor.selections.ranges(cx), [2..2]);
3068
3069 editor.transpose(&Default::default(), cx);
3070 assert_eq!(editor.text(cx), "bca");
3071 assert_eq!(editor.selections.ranges(cx), [3..3]);
3072
3073 editor.transpose(&Default::default(), cx);
3074 assert_eq!(editor.text(cx), "bac");
3075 assert_eq!(editor.selections.ranges(cx), [3..3]);
3076
3077 editor
3078 });
3079
3080 _ = cx.add_window(|cx| {
3081 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3082
3083 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
3084 editor.transpose(&Default::default(), cx);
3085 assert_eq!(editor.text(cx), "acb\nde");
3086 assert_eq!(editor.selections.ranges(cx), [3..3]);
3087
3088 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3089 editor.transpose(&Default::default(), cx);
3090 assert_eq!(editor.text(cx), "acbd\ne");
3091 assert_eq!(editor.selections.ranges(cx), [5..5]);
3092
3093 editor.transpose(&Default::default(), cx);
3094 assert_eq!(editor.text(cx), "acbde\n");
3095 assert_eq!(editor.selections.ranges(cx), [6..6]);
3096
3097 editor.transpose(&Default::default(), cx);
3098 assert_eq!(editor.text(cx), "acbd\ne");
3099 assert_eq!(editor.selections.ranges(cx), [6..6]);
3100
3101 editor
3102 });
3103
3104 _ = cx.add_window(|cx| {
3105 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
3106
3107 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
3108 editor.transpose(&Default::default(), cx);
3109 assert_eq!(editor.text(cx), "bacd\ne");
3110 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
3111
3112 editor.transpose(&Default::default(), cx);
3113 assert_eq!(editor.text(cx), "bcade\n");
3114 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
3115
3116 editor.transpose(&Default::default(), cx);
3117 assert_eq!(editor.text(cx), "bcda\ne");
3118 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3119
3120 editor.transpose(&Default::default(), cx);
3121 assert_eq!(editor.text(cx), "bcade\n");
3122 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
3123
3124 editor.transpose(&Default::default(), cx);
3125 assert_eq!(editor.text(cx), "bcaed\n");
3126 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
3127
3128 editor
3129 });
3130
3131 _ = cx.add_window(|cx| {
3132 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
3133
3134 editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
3135 editor.transpose(&Default::default(), cx);
3136 assert_eq!(editor.text(cx), "🏀🍐✋");
3137 assert_eq!(editor.selections.ranges(cx), [8..8]);
3138
3139 editor.transpose(&Default::default(), cx);
3140 assert_eq!(editor.text(cx), "🏀✋🍐");
3141 assert_eq!(editor.selections.ranges(cx), [11..11]);
3142
3143 editor.transpose(&Default::default(), cx);
3144 assert_eq!(editor.text(cx), "🏀🍐✋");
3145 assert_eq!(editor.selections.ranges(cx), [11..11]);
3146
3147 editor
3148 });
3149}
3150
3151#[gpui::test]
3152async fn test_clipboard(cx: &mut gpui::TestAppContext) {
3153 init_test(cx, |_| {});
3154
3155 let mut cx = EditorTestContext::new(cx).await;
3156
3157 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
3158 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3159 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
3160
3161 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
3162 cx.set_state("two ˇfour ˇsix ˇ");
3163 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3164 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
3165
3166 // Paste again but with only two cursors. Since the number of cursors doesn't
3167 // match the number of slices in the clipboard, the entire clipboard text
3168 // is pasted at each cursor.
3169 cx.set_state("ˇtwo one✅ four three six five ˇ");
3170 cx.update_editor(|e, cx| {
3171 e.handle_input("( ", cx);
3172 e.paste(&Paste, cx);
3173 e.handle_input(") ", cx);
3174 });
3175 cx.assert_editor_state(
3176 &([
3177 "( one✅ ",
3178 "three ",
3179 "five ) ˇtwo one✅ four three six five ( one✅ ",
3180 "three ",
3181 "five ) ˇ",
3182 ]
3183 .join("\n")),
3184 );
3185
3186 // Cut with three selections, one of which is full-line.
3187 cx.set_state(indoc! {"
3188 1«2ˇ»3
3189 4ˇ567
3190 «8ˇ»9"});
3191 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3192 cx.assert_editor_state(indoc! {"
3193 1ˇ3
3194 ˇ9"});
3195
3196 // Paste with three selections, noticing how the copied selection that was full-line
3197 // gets inserted before the second cursor.
3198 cx.set_state(indoc! {"
3199 1ˇ3
3200 9ˇ
3201 «oˇ»ne"});
3202 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3203 cx.assert_editor_state(indoc! {"
3204 12ˇ3
3205 4567
3206 9ˇ
3207 8ˇne"});
3208
3209 // Copy with a single cursor only, which writes the whole line into the clipboard.
3210 cx.set_state(indoc! {"
3211 The quick brown
3212 fox juˇmps over
3213 the lazy dog"});
3214 cx.update_editor(|e, cx| e.copy(&Copy, cx));
3215 cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
3216
3217 // Paste with three selections, noticing how the copied full-line selection is inserted
3218 // before the empty selections but replaces the selection that is non-empty.
3219 cx.set_state(indoc! {"
3220 Tˇhe quick brown
3221 «foˇ»x jumps over
3222 tˇhe lazy dog"});
3223 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3224 cx.assert_editor_state(indoc! {"
3225 fox jumps over
3226 Tˇhe quick brown
3227 fox jumps over
3228 ˇx jumps over
3229 fox jumps over
3230 tˇhe lazy dog"});
3231}
3232
3233#[gpui::test]
3234async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
3235 init_test(cx, |_| {});
3236
3237 let mut cx = EditorTestContext::new(cx).await;
3238 let language = Arc::new(Language::new(
3239 LanguageConfig::default(),
3240 Some(tree_sitter_rust::language()),
3241 ));
3242 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3243
3244 // Cut an indented block, without the leading whitespace.
3245 cx.set_state(indoc! {"
3246 const a: B = (
3247 c(),
3248 «d(
3249 e,
3250 f
3251 )ˇ»
3252 );
3253 "});
3254 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3255 cx.assert_editor_state(indoc! {"
3256 const a: B = (
3257 c(),
3258 ˇ
3259 );
3260 "});
3261
3262 // Paste it at the same position.
3263 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3264 cx.assert_editor_state(indoc! {"
3265 const a: B = (
3266 c(),
3267 d(
3268 e,
3269 f
3270 )ˇ
3271 );
3272 "});
3273
3274 // Paste it at a line with a lower indent level.
3275 cx.set_state(indoc! {"
3276 ˇ
3277 const a: B = (
3278 c(),
3279 );
3280 "});
3281 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3282 cx.assert_editor_state(indoc! {"
3283 d(
3284 e,
3285 f
3286 )ˇ
3287 const a: B = (
3288 c(),
3289 );
3290 "});
3291
3292 // Cut an indented block, with the leading whitespace.
3293 cx.set_state(indoc! {"
3294 const a: B = (
3295 c(),
3296 « d(
3297 e,
3298 f
3299 )
3300 ˇ»);
3301 "});
3302 cx.update_editor(|e, cx| e.cut(&Cut, cx));
3303 cx.assert_editor_state(indoc! {"
3304 const a: B = (
3305 c(),
3306 ˇ);
3307 "});
3308
3309 // Paste it at the same position.
3310 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3311 cx.assert_editor_state(indoc! {"
3312 const a: B = (
3313 c(),
3314 d(
3315 e,
3316 f
3317 )
3318 ˇ);
3319 "});
3320
3321 // Paste it at a line with a higher indent level.
3322 cx.set_state(indoc! {"
3323 const a: B = (
3324 c(),
3325 d(
3326 e,
3327 fˇ
3328 )
3329 );
3330 "});
3331 cx.update_editor(|e, cx| e.paste(&Paste, cx));
3332 cx.assert_editor_state(indoc! {"
3333 const a: B = (
3334 c(),
3335 d(
3336 e,
3337 f d(
3338 e,
3339 f
3340 )
3341 ˇ
3342 )
3343 );
3344 "});
3345}
3346
3347#[gpui::test]
3348fn test_select_all(cx: &mut TestAppContext) {
3349 init_test(cx, |_| {});
3350
3351 let view = cx.add_window(|cx| {
3352 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
3353 build_editor(buffer, cx)
3354 });
3355 view.update(cx, |view, cx| {
3356 view.select_all(&SelectAll, cx);
3357 assert_eq!(
3358 view.selections.display_ranges(cx),
3359 &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
3360 );
3361 });
3362}
3363
3364#[gpui::test]
3365fn test_select_line(cx: &mut TestAppContext) {
3366 init_test(cx, |_| {});
3367
3368 let view = cx.add_window(|cx| {
3369 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
3370 build_editor(buffer, cx)
3371 });
3372 view.update(cx, |view, cx| {
3373 view.change_selections(None, cx, |s| {
3374 s.select_display_ranges([
3375 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3376 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3377 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3378 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
3379 ])
3380 });
3381 view.select_line(&SelectLine, cx);
3382 assert_eq!(
3383 view.selections.display_ranges(cx),
3384 vec![
3385 DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
3386 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
3387 ]
3388 );
3389 });
3390
3391 view.update(cx, |view, cx| {
3392 view.select_line(&SelectLine, cx);
3393 assert_eq!(
3394 view.selections.display_ranges(cx),
3395 vec![
3396 DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
3397 DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
3398 ]
3399 );
3400 });
3401
3402 view.update(cx, |view, cx| {
3403 view.select_line(&SelectLine, cx);
3404 assert_eq!(
3405 view.selections.display_ranges(cx),
3406 vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
3407 );
3408 });
3409}
3410
3411#[gpui::test]
3412fn test_split_selection_into_lines(cx: &mut TestAppContext) {
3413 init_test(cx, |_| {});
3414
3415 let view = cx.add_window(|cx| {
3416 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
3417 build_editor(buffer, cx)
3418 });
3419 view.update(cx, |view, cx| {
3420 view.fold_ranges(
3421 vec![
3422 Point::new(0, 2)..Point::new(1, 2),
3423 Point::new(2, 3)..Point::new(4, 1),
3424 Point::new(7, 0)..Point::new(8, 4),
3425 ],
3426 true,
3427 cx,
3428 );
3429 view.change_selections(None, cx, |s| {
3430 s.select_display_ranges([
3431 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
3432 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3433 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
3434 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
3435 ])
3436 });
3437 assert_eq!(view.display_text(cx), "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i");
3438 });
3439
3440 view.update(cx, |view, cx| {
3441 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3442 assert_eq!(
3443 view.display_text(cx),
3444 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
3445 );
3446 assert_eq!(
3447 view.selections.display_ranges(cx),
3448 [
3449 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
3450 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
3451 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
3452 DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
3453 ]
3454 );
3455 });
3456
3457 view.update(cx, |view, cx| {
3458 view.change_selections(None, cx, |s| {
3459 s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
3460 });
3461 view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
3462 assert_eq!(
3463 view.display_text(cx),
3464 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
3465 );
3466 assert_eq!(
3467 view.selections.display_ranges(cx),
3468 [
3469 DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
3470 DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
3471 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
3472 DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
3473 DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
3474 DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
3475 DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
3476 DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
3477 ]
3478 );
3479 });
3480}
3481
3482#[gpui::test]
3483fn test_add_selection_above_below(cx: &mut TestAppContext) {
3484 init_test(cx, |_| {});
3485
3486 let view = cx.add_window(|cx| {
3487 let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
3488 build_editor(buffer, cx)
3489 });
3490
3491 view.update(cx, |view, cx| {
3492 view.change_selections(None, cx, |s| {
3493 s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)])
3494 });
3495 });
3496 view.update(cx, |view, cx| {
3497 view.add_selection_above(&AddSelectionAbove, cx);
3498 assert_eq!(
3499 view.selections.display_ranges(cx),
3500 vec![
3501 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
3502 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
3503 ]
3504 );
3505 });
3506
3507 view.update(cx, |view, cx| {
3508 view.add_selection_above(&AddSelectionAbove, cx);
3509 assert_eq!(
3510 view.selections.display_ranges(cx),
3511 vec![
3512 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
3513 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
3514 ]
3515 );
3516 });
3517
3518 view.update(cx, |view, cx| {
3519 view.add_selection_below(&AddSelectionBelow, cx);
3520 assert_eq!(
3521 view.selections.display_ranges(cx),
3522 vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
3523 );
3524
3525 view.undo_selection(&UndoSelection, cx);
3526 assert_eq!(
3527 view.selections.display_ranges(cx),
3528 vec![
3529 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
3530 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
3531 ]
3532 );
3533
3534 view.redo_selection(&RedoSelection, cx);
3535 assert_eq!(
3536 view.selections.display_ranges(cx),
3537 vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
3538 );
3539 });
3540
3541 view.update(cx, |view, cx| {
3542 view.add_selection_below(&AddSelectionBelow, cx);
3543 assert_eq!(
3544 view.selections.display_ranges(cx),
3545 vec![
3546 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3),
3547 DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)
3548 ]
3549 );
3550 });
3551
3552 view.update(cx, |view, cx| {
3553 view.add_selection_below(&AddSelectionBelow, cx);
3554 assert_eq!(
3555 view.selections.display_ranges(cx),
3556 vec![
3557 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3),
3558 DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)
3559 ]
3560 );
3561 });
3562
3563 view.update(cx, |view, cx| {
3564 view.change_selections(None, cx, |s| {
3565 s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)])
3566 });
3567 });
3568 view.update(cx, |view, cx| {
3569 view.add_selection_below(&AddSelectionBelow, cx);
3570 assert_eq!(
3571 view.selections.display_ranges(cx),
3572 vec![
3573 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
3574 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3)
3575 ]
3576 );
3577 });
3578
3579 view.update(cx, |view, cx| {
3580 view.add_selection_below(&AddSelectionBelow, cx);
3581 assert_eq!(
3582 view.selections.display_ranges(cx),
3583 vec![
3584 DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
3585 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3)
3586 ]
3587 );
3588 });
3589
3590 view.update(cx, |view, cx| {
3591 view.add_selection_above(&AddSelectionAbove, cx);
3592 assert_eq!(
3593 view.selections.display_ranges(cx),
3594 vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]
3595 );
3596 });
3597
3598 view.update(cx, |view, cx| {
3599 view.add_selection_above(&AddSelectionAbove, cx);
3600 assert_eq!(
3601 view.selections.display_ranges(cx),
3602 vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]
3603 );
3604 });
3605
3606 view.update(cx, |view, cx| {
3607 view.change_selections(None, cx, |s| {
3608 s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)])
3609 });
3610 view.add_selection_below(&AddSelectionBelow, cx);
3611 assert_eq!(
3612 view.selections.display_ranges(cx),
3613 vec![
3614 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
3615 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
3616 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
3617 ]
3618 );
3619 });
3620
3621 view.update(cx, |view, cx| {
3622 view.add_selection_below(&AddSelectionBelow, cx);
3623 assert_eq!(
3624 view.selections.display_ranges(cx),
3625 vec![
3626 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
3627 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
3628 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
3629 DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4),
3630 ]
3631 );
3632 });
3633
3634 view.update(cx, |view, cx| {
3635 view.add_selection_above(&AddSelectionAbove, cx);
3636 assert_eq!(
3637 view.selections.display_ranges(cx),
3638 vec![
3639 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
3640 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
3641 DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
3642 ]
3643 );
3644 });
3645
3646 view.update(cx, |view, cx| {
3647 view.change_selections(None, cx, |s| {
3648 s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)])
3649 });
3650 });
3651 view.update(cx, |view, cx| {
3652 view.add_selection_above(&AddSelectionAbove, cx);
3653 assert_eq!(
3654 view.selections.display_ranges(cx),
3655 vec![
3656 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1),
3657 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1),
3658 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1),
3659 DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
3660 ]
3661 );
3662 });
3663
3664 view.update(cx, |view, cx| {
3665 view.add_selection_below(&AddSelectionBelow, cx);
3666 assert_eq!(
3667 view.selections.display_ranges(cx),
3668 vec![
3669 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1),
3670 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1),
3671 DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
3672 ]
3673 );
3674 });
3675}
3676
3677#[gpui::test]
3678async fn test_select_next(cx: &mut gpui::TestAppContext) {
3679 init_test(cx, |_| {});
3680
3681 let mut cx = EditorTestContext::new(cx).await;
3682 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
3683
3684 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3685 .unwrap();
3686 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
3687
3688 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3689 .unwrap();
3690 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
3691
3692 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
3693 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
3694
3695 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
3696 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
3697
3698 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3699 .unwrap();
3700 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
3701
3702 cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
3703 .unwrap();
3704 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
3705}
3706
3707#[gpui::test]
3708async fn test_select_previous(cx: &mut gpui::TestAppContext) {
3709 init_test(cx, |_| {});
3710 {
3711 // `Select previous` without a selection (selects wordwise)
3712 let mut cx = EditorTestContext::new(cx).await;
3713 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
3714
3715 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3716 .unwrap();
3717 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
3718
3719 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3720 .unwrap();
3721 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
3722
3723 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
3724 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
3725
3726 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
3727 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
3728
3729 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3730 .unwrap();
3731 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
3732
3733 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3734 .unwrap();
3735 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
3736 }
3737 {
3738 // `Select previous` with a selection
3739 let mut cx = EditorTestContext::new(cx).await;
3740 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
3741
3742 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3743 .unwrap();
3744 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
3745
3746 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3747 .unwrap();
3748 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
3749
3750 cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
3751 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
3752
3753 cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
3754 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
3755
3756 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3757 .unwrap();
3758 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
3759
3760 cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
3761 .unwrap();
3762 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
3763 }
3764}
3765
3766#[gpui::test]
3767async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
3768 init_test(cx, |_| {});
3769
3770 let language = Arc::new(Language::new(
3771 LanguageConfig::default(),
3772 Some(tree_sitter_rust::language()),
3773 ));
3774
3775 let text = r#"
3776 use mod1::mod2::{mod3, mod4};
3777
3778 fn fn_1(param1: bool, param2: &str) {
3779 let var1 = "text";
3780 }
3781 "#
3782 .unindent();
3783
3784 let buffer = cx.build_model(|cx| {
3785 Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
3786 });
3787 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
3788 let view = cx.add_window(|cx| build_editor(buffer, cx));
3789 view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
3790 .await;
3791
3792 view.update(cx, |view, cx| {
3793 view.change_selections(None, cx, |s| {
3794 s.select_display_ranges([
3795 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
3796 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
3797 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
3798 ]);
3799 });
3800 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
3801 });
3802 assert_eq!(
3803 view.update(cx, |view, cx| { view.selections.display_ranges(cx) })
3804 .unwrap(),
3805 &[
3806 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
3807 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
3808 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
3809 ]
3810 );
3811
3812 view.update(cx, |view, cx| {
3813 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
3814 });
3815 assert_eq!(
3816 view.update(cx, |view, cx| view.selections.display_ranges(cx))
3817 .unwrap(),
3818 &[
3819 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
3820 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
3821 ]
3822 );
3823
3824 view.update(cx, |view, cx| {
3825 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
3826 });
3827 assert_eq!(
3828 view.update(cx, |view, cx| view.selections.display_ranges(cx))
3829 .unwrap(),
3830 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
3831 );
3832
3833 // Trying to expand the selected syntax node one more time has no effect.
3834 view.update(cx, |view, cx| {
3835 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
3836 });
3837 assert_eq!(
3838 view.update(cx, |view, cx| view.selections.display_ranges(cx))
3839 .unwrap(),
3840 &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
3841 );
3842
3843 view.update(cx, |view, cx| {
3844 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
3845 });
3846 assert_eq!(
3847 view.update(cx, |view, cx| view.selections.display_ranges(cx))
3848 .unwrap(),
3849 &[
3850 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
3851 DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
3852 ]
3853 );
3854
3855 view.update(cx, |view, cx| {
3856 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
3857 });
3858 assert_eq!(
3859 view.update(cx, |view, cx| view.selections.display_ranges(cx))
3860 .unwrap(),
3861 &[
3862 DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
3863 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
3864 DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
3865 ]
3866 );
3867
3868 view.update(cx, |view, cx| {
3869 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
3870 });
3871 assert_eq!(
3872 view.update(cx, |view, cx| view.selections.display_ranges(cx))
3873 .unwrap(),
3874 &[
3875 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
3876 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
3877 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
3878 ]
3879 );
3880
3881 // Trying to shrink the selected syntax node one more time has no effect.
3882 view.update(cx, |view, cx| {
3883 view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
3884 });
3885 assert_eq!(
3886 view.update(cx, |view, cx| view.selections.display_ranges(cx))
3887 .unwrap(),
3888 &[
3889 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
3890 DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
3891 DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
3892 ]
3893 );
3894
3895 // Ensure that we keep expanding the selection if the larger selection starts or ends within
3896 // a fold.
3897 view.update(cx, |view, cx| {
3898 view.fold_ranges(
3899 vec![
3900 Point::new(0, 21)..Point::new(0, 24),
3901 Point::new(3, 20)..Point::new(3, 22),
3902 ],
3903 true,
3904 cx,
3905 );
3906 view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
3907 });
3908 assert_eq!(
3909 view.update(cx, |view, cx| view.selections.display_ranges(cx))
3910 .unwrap(),
3911 &[
3912 DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
3913 DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
3914 DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
3915 ]
3916 );
3917}
3918
3919#[gpui::test]
3920async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
3921 init_test(cx, |_| {});
3922
3923 let language = Arc::new(
3924 Language::new(
3925 LanguageConfig {
3926 brackets: BracketPairConfig {
3927 pairs: vec![
3928 BracketPair {
3929 start: "{".to_string(),
3930 end: "}".to_string(),
3931 close: false,
3932 newline: true,
3933 },
3934 BracketPair {
3935 start: "(".to_string(),
3936 end: ")".to_string(),
3937 close: false,
3938 newline: true,
3939 },
3940 ],
3941 ..Default::default()
3942 },
3943 ..Default::default()
3944 },
3945 Some(tree_sitter_rust::language()),
3946 )
3947 .with_indents_query(
3948 r#"
3949 (_ "(" ")" @end) @indent
3950 (_ "{" "}" @end) @indent
3951 "#,
3952 )
3953 .unwrap(),
3954 );
3955
3956 let text = "fn a() {}";
3957
3958 let buffer = cx.build_model(|cx| {
3959 Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
3960 });
3961 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
3962 let editor = cx.add_window(|cx| build_editor(buffer, cx));
3963 editor
3964 .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
3965 .await;
3966
3967 editor.update(cx, |editor, cx| {
3968 editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
3969 editor.newline(&Newline, cx);
3970 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
3971 assert_eq!(
3972 editor.selections.ranges(cx),
3973 &[
3974 Point::new(1, 4)..Point::new(1, 4),
3975 Point::new(3, 4)..Point::new(3, 4),
3976 Point::new(5, 0)..Point::new(5, 0)
3977 ]
3978 );
3979 });
3980}
3981
3982#[gpui::test]
3983async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
3984 init_test(cx, |_| {});
3985
3986 let mut cx = EditorTestContext::new(cx).await;
3987
3988 let language = Arc::new(Language::new(
3989 LanguageConfig {
3990 brackets: BracketPairConfig {
3991 pairs: vec![
3992 BracketPair {
3993 start: "{".to_string(),
3994 end: "}".to_string(),
3995 close: true,
3996 newline: true,
3997 },
3998 BracketPair {
3999 start: "(".to_string(),
4000 end: ")".to_string(),
4001 close: true,
4002 newline: true,
4003 },
4004 BracketPair {
4005 start: "/*".to_string(),
4006 end: " */".to_string(),
4007 close: true,
4008 newline: true,
4009 },
4010 BracketPair {
4011 start: "[".to_string(),
4012 end: "]".to_string(),
4013 close: false,
4014 newline: true,
4015 },
4016 BracketPair {
4017 start: "\"".to_string(),
4018 end: "\"".to_string(),
4019 close: true,
4020 newline: false,
4021 },
4022 ],
4023 ..Default::default()
4024 },
4025 autoclose_before: "})]".to_string(),
4026 ..Default::default()
4027 },
4028 Some(tree_sitter_rust::language()),
4029 ));
4030
4031 let registry = Arc::new(LanguageRegistry::test());
4032 registry.add(language.clone());
4033 cx.update_buffer(|buffer, cx| {
4034 buffer.set_language_registry(registry);
4035 buffer.set_language(Some(language), cx);
4036 });
4037
4038 cx.set_state(
4039 &r#"
4040 🏀ˇ
4041 εˇ
4042 ❤️ˇ
4043 "#
4044 .unindent(),
4045 );
4046
4047 // autoclose multiple nested brackets at multiple cursors
4048 cx.update_editor(|view, cx| {
4049 view.handle_input("{", cx);
4050 view.handle_input("{", cx);
4051 view.handle_input("{", cx);
4052 });
4053 cx.assert_editor_state(
4054 &"
4055 🏀{{{ˇ}}}
4056 ε{{{ˇ}}}
4057 ❤️{{{ˇ}}}
4058 "
4059 .unindent(),
4060 );
4061
4062 // insert a different closing bracket
4063 cx.update_editor(|view, cx| {
4064 view.handle_input(")", cx);
4065 });
4066 cx.assert_editor_state(
4067 &"
4068 🏀{{{)ˇ}}}
4069 ε{{{)ˇ}}}
4070 ❤️{{{)ˇ}}}
4071 "
4072 .unindent(),
4073 );
4074
4075 // skip over the auto-closed brackets when typing a closing bracket
4076 cx.update_editor(|view, cx| {
4077 view.move_right(&MoveRight, cx);
4078 view.handle_input("}", cx);
4079 view.handle_input("}", cx);
4080 view.handle_input("}", cx);
4081 });
4082 cx.assert_editor_state(
4083 &"
4084 🏀{{{)}}}}ˇ
4085 ε{{{)}}}}ˇ
4086 ❤️{{{)}}}}ˇ
4087 "
4088 .unindent(),
4089 );
4090
4091 // autoclose multi-character pairs
4092 cx.set_state(
4093 &"
4094 ˇ
4095 ˇ
4096 "
4097 .unindent(),
4098 );
4099 cx.update_editor(|view, cx| {
4100 view.handle_input("/", cx);
4101 view.handle_input("*", cx);
4102 });
4103 cx.assert_editor_state(
4104 &"
4105 /*ˇ */
4106 /*ˇ */
4107 "
4108 .unindent(),
4109 );
4110
4111 // one cursor autocloses a multi-character pair, one cursor
4112 // does not autoclose.
4113 cx.set_state(
4114 &"
4115 /ˇ
4116 ˇ
4117 "
4118 .unindent(),
4119 );
4120 cx.update_editor(|view, cx| view.handle_input("*", cx));
4121 cx.assert_editor_state(
4122 &"
4123 /*ˇ */
4124 *ˇ
4125 "
4126 .unindent(),
4127 );
4128
4129 // Don't autoclose if the next character isn't whitespace and isn't
4130 // listed in the language's "autoclose_before" section.
4131 cx.set_state("ˇa b");
4132 cx.update_editor(|view, cx| view.handle_input("{", cx));
4133 cx.assert_editor_state("{ˇa b");
4134
4135 // Don't autoclose if `close` is false for the bracket pair
4136 cx.set_state("ˇ");
4137 cx.update_editor(|view, cx| view.handle_input("[", cx));
4138 cx.assert_editor_state("[ˇ");
4139
4140 // Surround with brackets if text is selected
4141 cx.set_state("«aˇ» b");
4142 cx.update_editor(|view, cx| view.handle_input("{", cx));
4143 cx.assert_editor_state("{«aˇ»} b");
4144
4145 // Autclose pair where the start and end characters are the same
4146 cx.set_state("aˇ");
4147 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4148 cx.assert_editor_state("a\"ˇ\"");
4149 cx.update_editor(|view, cx| view.handle_input("\"", cx));
4150 cx.assert_editor_state("a\"\"ˇ");
4151}
4152
4153#[gpui::test]
4154async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
4155 init_test(cx, |_| {});
4156
4157 let mut cx = EditorTestContext::new(cx).await;
4158
4159 let html_language = Arc::new(
4160 Language::new(
4161 LanguageConfig {
4162 name: "HTML".into(),
4163 brackets: BracketPairConfig {
4164 pairs: vec![
4165 BracketPair {
4166 start: "<".into(),
4167 end: ">".into(),
4168 close: true,
4169 ..Default::default()
4170 },
4171 BracketPair {
4172 start: "{".into(),
4173 end: "}".into(),
4174 close: true,
4175 ..Default::default()
4176 },
4177 BracketPair {
4178 start: "(".into(),
4179 end: ")".into(),
4180 close: true,
4181 ..Default::default()
4182 },
4183 ],
4184 ..Default::default()
4185 },
4186 autoclose_before: "})]>".into(),
4187 ..Default::default()
4188 },
4189 Some(tree_sitter_html::language()),
4190 )
4191 .with_injection_query(
4192 r#"
4193 (script_element
4194 (raw_text) @content
4195 (#set! "language" "javascript"))
4196 "#,
4197 )
4198 .unwrap(),
4199 );
4200
4201 let javascript_language = Arc::new(Language::new(
4202 LanguageConfig {
4203 name: "JavaScript".into(),
4204 brackets: BracketPairConfig {
4205 pairs: vec![
4206 BracketPair {
4207 start: "/*".into(),
4208 end: " */".into(),
4209 close: true,
4210 ..Default::default()
4211 },
4212 BracketPair {
4213 start: "{".into(),
4214 end: "}".into(),
4215 close: true,
4216 ..Default::default()
4217 },
4218 BracketPair {
4219 start: "(".into(),
4220 end: ")".into(),
4221 close: true,
4222 ..Default::default()
4223 },
4224 ],
4225 ..Default::default()
4226 },
4227 autoclose_before: "})]>".into(),
4228 ..Default::default()
4229 },
4230 Some(tree_sitter_typescript::language_tsx()),
4231 ));
4232
4233 let registry = Arc::new(LanguageRegistry::test());
4234 registry.add(html_language.clone());
4235 registry.add(javascript_language.clone());
4236
4237 cx.update_buffer(|buffer, cx| {
4238 buffer.set_language_registry(registry);
4239 buffer.set_language(Some(html_language), cx);
4240 });
4241
4242 cx.set_state(
4243 &r#"
4244 <body>ˇ
4245 <script>
4246 var x = 1;ˇ
4247 </script>
4248 </body>ˇ
4249 "#
4250 .unindent(),
4251 );
4252
4253 // Precondition: different languages are active at different locations.
4254 cx.update_editor(|editor, cx| {
4255 let snapshot = editor.snapshot(cx);
4256 let cursors = editor.selections.ranges::<usize>(cx);
4257 let languages = cursors
4258 .iter()
4259 .map(|c| snapshot.language_at(c.start).unwrap().name())
4260 .collect::<Vec<_>>();
4261 assert_eq!(
4262 languages,
4263 &["HTML".into(), "JavaScript".into(), "HTML".into()]
4264 );
4265 });
4266
4267 // Angle brackets autoclose in HTML, but not JavaScript.
4268 cx.update_editor(|editor, cx| {
4269 editor.handle_input("<", cx);
4270 editor.handle_input("a", cx);
4271 });
4272 cx.assert_editor_state(
4273 &r#"
4274 <body><aˇ>
4275 <script>
4276 var x = 1;<aˇ
4277 </script>
4278 </body><aˇ>
4279 "#
4280 .unindent(),
4281 );
4282
4283 // Curly braces and parens autoclose in both HTML and JavaScript.
4284 cx.update_editor(|editor, cx| {
4285 editor.handle_input(" b=", cx);
4286 editor.handle_input("{", cx);
4287 editor.handle_input("c", cx);
4288 editor.handle_input("(", cx);
4289 });
4290 cx.assert_editor_state(
4291 &r#"
4292 <body><a b={c(ˇ)}>
4293 <script>
4294 var x = 1;<a b={c(ˇ)}
4295 </script>
4296 </body><a b={c(ˇ)}>
4297 "#
4298 .unindent(),
4299 );
4300
4301 // Brackets that were already autoclosed are skipped.
4302 cx.update_editor(|editor, cx| {
4303 editor.handle_input(")", cx);
4304 editor.handle_input("d", cx);
4305 editor.handle_input("}", cx);
4306 });
4307 cx.assert_editor_state(
4308 &r#"
4309 <body><a b={c()d}ˇ>
4310 <script>
4311 var x = 1;<a b={c()d}ˇ
4312 </script>
4313 </body><a b={c()d}ˇ>
4314 "#
4315 .unindent(),
4316 );
4317 cx.update_editor(|editor, cx| {
4318 editor.handle_input(">", cx);
4319 });
4320 cx.assert_editor_state(
4321 &r#"
4322 <body><a b={c()d}>ˇ
4323 <script>
4324 var x = 1;<a b={c()d}>ˇ
4325 </script>
4326 </body><a b={c()d}>ˇ
4327 "#
4328 .unindent(),
4329 );
4330
4331 // Reset
4332 cx.set_state(
4333 &r#"
4334 <body>ˇ
4335 <script>
4336 var x = 1;ˇ
4337 </script>
4338 </body>ˇ
4339 "#
4340 .unindent(),
4341 );
4342
4343 cx.update_editor(|editor, cx| {
4344 editor.handle_input("<", cx);
4345 });
4346 cx.assert_editor_state(
4347 &r#"
4348 <body><ˇ>
4349 <script>
4350 var x = 1;<ˇ
4351 </script>
4352 </body><ˇ>
4353 "#
4354 .unindent(),
4355 );
4356
4357 // When backspacing, the closing angle brackets are removed.
4358 cx.update_editor(|editor, cx| {
4359 editor.backspace(&Backspace, cx);
4360 });
4361 cx.assert_editor_state(
4362 &r#"
4363 <body>ˇ
4364 <script>
4365 var x = 1;ˇ
4366 </script>
4367 </body>ˇ
4368 "#
4369 .unindent(),
4370 );
4371
4372 // Block comments autoclose in JavaScript, but not HTML.
4373 cx.update_editor(|editor, cx| {
4374 editor.handle_input("/", cx);
4375 editor.handle_input("*", cx);
4376 });
4377 cx.assert_editor_state(
4378 &r#"
4379 <body>/*ˇ
4380 <script>
4381 var x = 1;/*ˇ */
4382 </script>
4383 </body>/*ˇ
4384 "#
4385 .unindent(),
4386 );
4387}
4388
4389#[gpui::test]
4390async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
4391 init_test(cx, |_| {});
4392
4393 let mut cx = EditorTestContext::new(cx).await;
4394
4395 let rust_language = Arc::new(
4396 Language::new(
4397 LanguageConfig {
4398 name: "Rust".into(),
4399 brackets: serde_json::from_value(json!([
4400 { "start": "{", "end": "}", "close": true, "newline": true },
4401 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
4402 ]))
4403 .unwrap(),
4404 autoclose_before: "})]>".into(),
4405 ..Default::default()
4406 },
4407 Some(tree_sitter_rust::language()),
4408 )
4409 .with_override_query("(string_literal) @string")
4410 .unwrap(),
4411 );
4412
4413 let registry = Arc::new(LanguageRegistry::test());
4414 registry.add(rust_language.clone());
4415
4416 cx.update_buffer(|buffer, cx| {
4417 buffer.set_language_registry(registry);
4418 buffer.set_language(Some(rust_language), cx);
4419 });
4420
4421 cx.set_state(
4422 &r#"
4423 let x = ˇ
4424 "#
4425 .unindent(),
4426 );
4427
4428 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
4429 cx.update_editor(|editor, cx| {
4430 editor.handle_input("\"", cx);
4431 });
4432 cx.assert_editor_state(
4433 &r#"
4434 let x = "ˇ"
4435 "#
4436 .unindent(),
4437 );
4438
4439 // Inserting another quotation mark. The cursor moves across the existing
4440 // automatically-inserted quotation mark.
4441 cx.update_editor(|editor, cx| {
4442 editor.handle_input("\"", cx);
4443 });
4444 cx.assert_editor_state(
4445 &r#"
4446 let x = ""ˇ
4447 "#
4448 .unindent(),
4449 );
4450
4451 // Reset
4452 cx.set_state(
4453 &r#"
4454 let x = ˇ
4455 "#
4456 .unindent(),
4457 );
4458
4459 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
4460 cx.update_editor(|editor, cx| {
4461 editor.handle_input("\"", cx);
4462 editor.handle_input(" ", cx);
4463 editor.move_left(&Default::default(), cx);
4464 editor.handle_input("\\", cx);
4465 editor.handle_input("\"", cx);
4466 });
4467 cx.assert_editor_state(
4468 &r#"
4469 let x = "\"ˇ "
4470 "#
4471 .unindent(),
4472 );
4473
4474 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
4475 // mark. Nothing is inserted.
4476 cx.update_editor(|editor, cx| {
4477 editor.move_right(&Default::default(), cx);
4478 editor.handle_input("\"", cx);
4479 });
4480 cx.assert_editor_state(
4481 &r#"
4482 let x = "\" "ˇ
4483 "#
4484 .unindent(),
4485 );
4486}
4487
4488#[gpui::test]
4489async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
4490 init_test(cx, |_| {});
4491
4492 let language = Arc::new(Language::new(
4493 LanguageConfig {
4494 brackets: BracketPairConfig {
4495 pairs: vec![
4496 BracketPair {
4497 start: "{".to_string(),
4498 end: "}".to_string(),
4499 close: true,
4500 newline: true,
4501 },
4502 BracketPair {
4503 start: "/* ".to_string(),
4504 end: "*/".to_string(),
4505 close: true,
4506 ..Default::default()
4507 },
4508 ],
4509 ..Default::default()
4510 },
4511 ..Default::default()
4512 },
4513 Some(tree_sitter_rust::language()),
4514 ));
4515
4516 let text = r#"
4517 a
4518 b
4519 c
4520 "#
4521 .unindent();
4522
4523 let buffer = cx.build_model(|cx| {
4524 Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
4525 });
4526 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
4527 let view = cx.add_window(|cx| build_editor(buffer, cx));
4528 view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
4529 .await;
4530
4531 view.update(cx, |view, cx| {
4532 view.change_selections(None, cx, |s| {
4533 s.select_display_ranges([
4534 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
4535 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
4536 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
4537 ])
4538 });
4539
4540 view.handle_input("{", cx);
4541 view.handle_input("{", cx);
4542 view.handle_input("{", cx);
4543 assert_eq!(
4544 view.text(cx),
4545 "
4546 {{{a}}}
4547 {{{b}}}
4548 {{{c}}}
4549 "
4550 .unindent()
4551 );
4552 assert_eq!(
4553 view.selections.display_ranges(cx),
4554 [
4555 DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
4556 DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
4557 DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
4558 ]
4559 );
4560
4561 view.undo(&Undo, cx);
4562 view.undo(&Undo, cx);
4563 view.undo(&Undo, cx);
4564 assert_eq!(
4565 view.text(cx),
4566 "
4567 a
4568 b
4569 c
4570 "
4571 .unindent()
4572 );
4573 assert_eq!(
4574 view.selections.display_ranges(cx),
4575 [
4576 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
4577 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
4578 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
4579 ]
4580 );
4581
4582 // Ensure inserting the first character of a multi-byte bracket pair
4583 // doesn't surround the selections with the bracket.
4584 view.handle_input("/", cx);
4585 assert_eq!(
4586 view.text(cx),
4587 "
4588 /
4589 /
4590 /
4591 "
4592 .unindent()
4593 );
4594 assert_eq!(
4595 view.selections.display_ranges(cx),
4596 [
4597 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
4598 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
4599 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
4600 ]
4601 );
4602
4603 view.undo(&Undo, cx);
4604 assert_eq!(
4605 view.text(cx),
4606 "
4607 a
4608 b
4609 c
4610 "
4611 .unindent()
4612 );
4613 assert_eq!(
4614 view.selections.display_ranges(cx),
4615 [
4616 DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
4617 DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
4618 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
4619 ]
4620 );
4621
4622 // Ensure inserting the last character of a multi-byte bracket pair
4623 // doesn't surround the selections with the bracket.
4624 view.handle_input("*", cx);
4625 assert_eq!(
4626 view.text(cx),
4627 "
4628 *
4629 *
4630 *
4631 "
4632 .unindent()
4633 );
4634 assert_eq!(
4635 view.selections.display_ranges(cx),
4636 [
4637 DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
4638 DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
4639 DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
4640 ]
4641 );
4642 });
4643}
4644
4645#[gpui::test]
4646async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
4647 init_test(cx, |_| {});
4648
4649 let language = Arc::new(Language::new(
4650 LanguageConfig {
4651 brackets: BracketPairConfig {
4652 pairs: vec![BracketPair {
4653 start: "{".to_string(),
4654 end: "}".to_string(),
4655 close: true,
4656 newline: true,
4657 }],
4658 ..Default::default()
4659 },
4660 autoclose_before: "}".to_string(),
4661 ..Default::default()
4662 },
4663 Some(tree_sitter_rust::language()),
4664 ));
4665
4666 let text = r#"
4667 a
4668 b
4669 c
4670 "#
4671 .unindent();
4672
4673 let buffer = cx.build_model(|cx| {
4674 Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
4675 });
4676 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
4677 let editor = cx.add_window(|cx| build_editor(buffer, cx));
4678 editor
4679 .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
4680 .await;
4681
4682 editor.update(cx, |editor, cx| {
4683 editor.change_selections(None, cx, |s| {
4684 s.select_ranges([
4685 Point::new(0, 1)..Point::new(0, 1),
4686 Point::new(1, 1)..Point::new(1, 1),
4687 Point::new(2, 1)..Point::new(2, 1),
4688 ])
4689 });
4690
4691 editor.handle_input("{", cx);
4692 editor.handle_input("{", cx);
4693 editor.handle_input("_", cx);
4694 assert_eq!(
4695 editor.text(cx),
4696 "
4697 a{{_}}
4698 b{{_}}
4699 c{{_}}
4700 "
4701 .unindent()
4702 );
4703 assert_eq!(
4704 editor.selections.ranges::<Point>(cx),
4705 [
4706 Point::new(0, 4)..Point::new(0, 4),
4707 Point::new(1, 4)..Point::new(1, 4),
4708 Point::new(2, 4)..Point::new(2, 4)
4709 ]
4710 );
4711
4712 editor.backspace(&Default::default(), cx);
4713 editor.backspace(&Default::default(), cx);
4714 assert_eq!(
4715 editor.text(cx),
4716 "
4717 a{}
4718 b{}
4719 c{}
4720 "
4721 .unindent()
4722 );
4723 assert_eq!(
4724 editor.selections.ranges::<Point>(cx),
4725 [
4726 Point::new(0, 2)..Point::new(0, 2),
4727 Point::new(1, 2)..Point::new(1, 2),
4728 Point::new(2, 2)..Point::new(2, 2)
4729 ]
4730 );
4731
4732 editor.delete_to_previous_word_start(&Default::default(), cx);
4733 assert_eq!(
4734 editor.text(cx),
4735 "
4736 a
4737 b
4738 c
4739 "
4740 .unindent()
4741 );
4742 assert_eq!(
4743 editor.selections.ranges::<Point>(cx),
4744 [
4745 Point::new(0, 1)..Point::new(0, 1),
4746 Point::new(1, 1)..Point::new(1, 1),
4747 Point::new(2, 1)..Point::new(2, 1)
4748 ]
4749 );
4750 });
4751}
4752
4753#[gpui::test]
4754async fn test_snippets(cx: &mut gpui::TestAppContext) {
4755 init_test(cx, |_| {});
4756
4757 let (text, insertion_ranges) = marked_text_ranges(
4758 indoc! {"
4759 a.ˇ b
4760 a.ˇ b
4761 a.ˇ b
4762 "},
4763 false,
4764 );
4765
4766 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
4767 let editor = cx.add_window(|cx| build_editor(buffer, cx));
4768
4769 editor.update(cx, |editor, cx| {
4770 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
4771
4772 editor
4773 .insert_snippet(&insertion_ranges, snippet, cx)
4774 .unwrap();
4775
4776 fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
4777 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
4778 assert_eq!(editor.text(cx), expected_text);
4779 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
4780 }
4781
4782 assert(
4783 editor,
4784 cx,
4785 indoc! {"
4786 a.f(«one», two, «three») b
4787 a.f(«one», two, «three») b
4788 a.f(«one», two, «three») b
4789 "},
4790 );
4791
4792 // Can't move earlier than the first tab stop
4793 assert!(!editor.move_to_prev_snippet_tabstop(cx));
4794 assert(
4795 editor,
4796 cx,
4797 indoc! {"
4798 a.f(«one», two, «three») b
4799 a.f(«one», two, «three») b
4800 a.f(«one», two, «three») b
4801 "},
4802 );
4803
4804 assert!(editor.move_to_next_snippet_tabstop(cx));
4805 assert(
4806 editor,
4807 cx,
4808 indoc! {"
4809 a.f(one, «two», three) b
4810 a.f(one, «two», three) b
4811 a.f(one, «two», three) b
4812 "},
4813 );
4814
4815 editor.move_to_prev_snippet_tabstop(cx);
4816 assert(
4817 editor,
4818 cx,
4819 indoc! {"
4820 a.f(«one», two, «three») b
4821 a.f(«one», two, «three») b
4822 a.f(«one», two, «three») b
4823 "},
4824 );
4825
4826 assert!(editor.move_to_next_snippet_tabstop(cx));
4827 assert(
4828 editor,
4829 cx,
4830 indoc! {"
4831 a.f(one, «two», three) b
4832 a.f(one, «two», three) b
4833 a.f(one, «two», three) b
4834 "},
4835 );
4836 assert!(editor.move_to_next_snippet_tabstop(cx));
4837 assert(
4838 editor,
4839 cx,
4840 indoc! {"
4841 a.f(one, two, three)ˇ b
4842 a.f(one, two, three)ˇ b
4843 a.f(one, two, three)ˇ b
4844 "},
4845 );
4846
4847 // As soon as the last tab stop is reached, snippet state is gone
4848 editor.move_to_prev_snippet_tabstop(cx);
4849 assert(
4850 editor,
4851 cx,
4852 indoc! {"
4853 a.f(one, two, three)ˇ b
4854 a.f(one, two, three)ˇ b
4855 a.f(one, two, three)ˇ b
4856 "},
4857 );
4858 });
4859}
4860
4861#[gpui::test]
4862async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
4863 init_test(cx, |_| {});
4864
4865 let mut language = Language::new(
4866 LanguageConfig {
4867 name: "Rust".into(),
4868 path_suffixes: vec!["rs".to_string()],
4869 ..Default::default()
4870 },
4871 Some(tree_sitter_rust::language()),
4872 );
4873 let mut fake_servers = language
4874 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4875 capabilities: lsp::ServerCapabilities {
4876 document_formatting_provider: Some(lsp::OneOf::Left(true)),
4877 ..Default::default()
4878 },
4879 ..Default::default()
4880 }))
4881 .await;
4882
4883 let fs = FakeFs::new(cx.executor());
4884 fs.insert_file("/file.rs", Default::default()).await;
4885
4886 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
4887 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
4888 let buffer = project
4889 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
4890 .await
4891 .unwrap();
4892
4893 cx.executor().start_waiting();
4894 let fake_server = fake_servers.next().await.unwrap();
4895
4896 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
4897 let editor = cx.add_window(|cx| build_editor(buffer, cx));
4898 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
4899 assert!(cx.read(|cx| editor.is_dirty(cx)));
4900
4901 let save = editor
4902 .update(cx, |editor, cx| editor.save(project.clone(), cx))
4903 .unwrap();
4904 fake_server
4905 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
4906 assert_eq!(
4907 params.text_document.uri,
4908 lsp::Url::from_file_path("/file.rs").unwrap()
4909 );
4910 assert_eq!(params.options.tab_size, 4);
4911 Ok(Some(vec![lsp::TextEdit::new(
4912 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
4913 ", ".to_string(),
4914 )]))
4915 })
4916 .next()
4917 .await;
4918 cx.executor().start_waiting();
4919 save.await.unwrap();
4920 assert_eq!(
4921 editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(),
4922 "one, two\nthree\n"
4923 );
4924 assert!(!cx.read(|cx| editor.is_dirty(cx)));
4925
4926 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
4927 assert!(cx.read(|cx| editor.is_dirty(cx)));
4928
4929 // Ensure we can still save even if formatting hangs.
4930 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
4931 assert_eq!(
4932 params.text_document.uri,
4933 lsp::Url::from_file_path("/file.rs").unwrap()
4934 );
4935 futures::future::pending::<()>().await;
4936 unreachable!()
4937 });
4938 let save = editor
4939 .update(cx, |editor, cx| editor.save(project.clone(), cx))
4940 .unwrap();
4941 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
4942 cx.executor().start_waiting();
4943 save.await.unwrap();
4944 assert_eq!(
4945 editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(),
4946 "one\ntwo\nthree\n"
4947 );
4948 assert!(!cx.read(|cx| editor.is_dirty(cx)));
4949
4950 // Set rust language override and assert overridden tabsize is sent to language server
4951 update_test_language_settings(cx, |settings| {
4952 settings.languages.insert(
4953 "Rust".into(),
4954 LanguageSettingsContent {
4955 tab_size: NonZeroU32::new(8),
4956 ..Default::default()
4957 },
4958 );
4959 });
4960
4961 let save = editor
4962 .update(cx, |editor, cx| editor.save(project.clone(), cx))
4963 .unwrap();
4964 fake_server
4965 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
4966 assert_eq!(
4967 params.text_document.uri,
4968 lsp::Url::from_file_path("/file.rs").unwrap()
4969 );
4970 assert_eq!(params.options.tab_size, 8);
4971 Ok(Some(vec![]))
4972 })
4973 .next()
4974 .await;
4975 cx.executor().start_waiting();
4976 save.await.unwrap();
4977}
4978
4979#[gpui::test]
4980async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
4981 init_test(cx, |_| {});
4982
4983 let mut language = Language::new(
4984 LanguageConfig {
4985 name: "Rust".into(),
4986 path_suffixes: vec!["rs".to_string()],
4987 ..Default::default()
4988 },
4989 Some(tree_sitter_rust::language()),
4990 );
4991 let mut fake_servers = language
4992 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4993 capabilities: lsp::ServerCapabilities {
4994 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
4995 ..Default::default()
4996 },
4997 ..Default::default()
4998 }))
4999 .await;
5000
5001 let fs = FakeFs::new(cx.executor());
5002 fs.insert_file("/file.rs", Default::default()).await;
5003
5004 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5005 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
5006 let buffer = project
5007 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5008 .await
5009 .unwrap();
5010
5011 cx.executor().start_waiting();
5012 let fake_server = fake_servers.next().await.unwrap();
5013
5014 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
5015 let editor = cx.add_window(|cx| build_editor(buffer, cx));
5016 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5017 assert!(cx.read(|cx| editor.is_dirty(cx)));
5018
5019 let save = editor
5020 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5021 .unwrap();
5022 fake_server
5023 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
5024 assert_eq!(
5025 params.text_document.uri,
5026 lsp::Url::from_file_path("/file.rs").unwrap()
5027 );
5028 assert_eq!(params.options.tab_size, 4);
5029 Ok(Some(vec![lsp::TextEdit::new(
5030 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5031 ", ".to_string(),
5032 )]))
5033 })
5034 .next()
5035 .await;
5036 cx.executor().start_waiting();
5037 save.await.unwrap();
5038 assert_eq!(
5039 editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(),
5040 "one, two\nthree\n"
5041 );
5042 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5043
5044 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5045 assert!(cx.read(|cx| editor.is_dirty(cx)));
5046
5047 // Ensure we can still save even if formatting hangs.
5048 fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
5049 move |params, _| async move {
5050 assert_eq!(
5051 params.text_document.uri,
5052 lsp::Url::from_file_path("/file.rs").unwrap()
5053 );
5054 futures::future::pending::<()>().await;
5055 unreachable!()
5056 },
5057 );
5058 let save = editor
5059 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5060 .unwrap();
5061 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5062 cx.executor().start_waiting();
5063 save.await.unwrap();
5064 assert_eq!(
5065 editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(),
5066 "one\ntwo\nthree\n"
5067 );
5068 assert!(!cx.read(|cx| editor.is_dirty(cx)));
5069
5070 // Set rust language override and assert overridden tabsize is sent to language server
5071 update_test_language_settings(cx, |settings| {
5072 settings.languages.insert(
5073 "Rust".into(),
5074 LanguageSettingsContent {
5075 tab_size: NonZeroU32::new(8),
5076 ..Default::default()
5077 },
5078 );
5079 });
5080
5081 let save = editor
5082 .update(cx, |editor, cx| editor.save(project.clone(), cx))
5083 .unwrap();
5084 fake_server
5085 .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
5086 assert_eq!(
5087 params.text_document.uri,
5088 lsp::Url::from_file_path("/file.rs").unwrap()
5089 );
5090 assert_eq!(params.options.tab_size, 8);
5091 Ok(Some(vec![]))
5092 })
5093 .next()
5094 .await;
5095 cx.executor().start_waiting();
5096 save.await.unwrap();
5097}
5098
5099#[gpui::test]
5100async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
5101 init_test(cx, |settings| {
5102 settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
5103 });
5104
5105 let mut language = Language::new(
5106 LanguageConfig {
5107 name: "Rust".into(),
5108 path_suffixes: vec!["rs".to_string()],
5109 // Enable Prettier formatting for the same buffer, and ensure
5110 // LSP is called instead of Prettier.
5111 prettier_parser_name: Some("test_parser".to_string()),
5112 ..Default::default()
5113 },
5114 Some(tree_sitter_rust::language()),
5115 );
5116 let mut fake_servers = language
5117 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5118 capabilities: lsp::ServerCapabilities {
5119 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5120 ..Default::default()
5121 },
5122 ..Default::default()
5123 }))
5124 .await;
5125
5126 let fs = FakeFs::new(cx.executor());
5127 fs.insert_file("/file.rs", Default::default()).await;
5128
5129 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
5130 project.update(cx, |project, _| {
5131 project.languages().add(Arc::new(language));
5132 });
5133 let buffer = project
5134 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
5135 .await
5136 .unwrap();
5137
5138 cx.executor().start_waiting();
5139 let fake_server = fake_servers.next().await.unwrap();
5140
5141 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
5142 let editor = cx.add_window(|cx| build_editor(buffer, cx));
5143 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5144
5145 let format = editor
5146 .update(cx, |editor, cx| {
5147 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
5148 })
5149 .unwrap();
5150 fake_server
5151 .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5152 assert_eq!(
5153 params.text_document.uri,
5154 lsp::Url::from_file_path("/file.rs").unwrap()
5155 );
5156 assert_eq!(params.options.tab_size, 4);
5157 Ok(Some(vec![lsp::TextEdit::new(
5158 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
5159 ", ".to_string(),
5160 )]))
5161 })
5162 .next()
5163 .await;
5164 cx.executor().start_waiting();
5165 format.await.unwrap();
5166 assert_eq!(
5167 editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(),
5168 "one, two\nthree\n"
5169 );
5170
5171 editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
5172 // Ensure we don't lock if formatting hangs.
5173 fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
5174 assert_eq!(
5175 params.text_document.uri,
5176 lsp::Url::from_file_path("/file.rs").unwrap()
5177 );
5178 futures::future::pending::<()>().await;
5179 unreachable!()
5180 });
5181 let format = editor
5182 .update(cx, |editor, cx| {
5183 editor.perform_format(project, FormatTrigger::Manual, cx)
5184 })
5185 .unwrap();
5186 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
5187 cx.executor().start_waiting();
5188 format.await.unwrap();
5189 assert_eq!(
5190 editor.read_with(cx, |editor, cx| editor.text(cx)).unwrap(),
5191 "one\ntwo\nthree\n"
5192 );
5193}
5194
5195#[gpui::test]
5196async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
5197 init_test(cx, |_| {});
5198
5199 let mut cx = EditorLspTestContext::new_rust(
5200 lsp::ServerCapabilities {
5201 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5202 ..Default::default()
5203 },
5204 cx,
5205 )
5206 .await;
5207
5208 cx.set_state(indoc! {"
5209 one.twoˇ
5210 "});
5211
5212 // The format request takes a long time. When it completes, it inserts
5213 // a newline and an indent before the `.`
5214 cx.lsp
5215 .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
5216 let executor = cx.background_executor();
5217 async move {
5218 executor.timer(Duration::from_millis(100)).await;
5219 Ok(Some(vec![lsp::TextEdit {
5220 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
5221 new_text: "\n ".into(),
5222 }]))
5223 }
5224 });
5225
5226 // Submit a format request.
5227 let format_1 = cx
5228 .update_editor(|editor, cx| editor.format(&Format, cx))
5229 .unwrap();
5230 cx.executor().run_until_parked();
5231
5232 // Submit a second format request.
5233 let format_2 = cx
5234 .update_editor(|editor, cx| editor.format(&Format, cx))
5235 .unwrap();
5236 cx.executor().run_until_parked();
5237
5238 // Wait for both format requests to complete
5239 cx.executor().advance_clock(Duration::from_millis(200));
5240 cx.executor().start_waiting();
5241 format_1.await.unwrap();
5242 cx.executor().start_waiting();
5243 format_2.await.unwrap();
5244
5245 // The formatting edits only happens once.
5246 cx.assert_editor_state(indoc! {"
5247 one
5248 .twoˇ
5249 "});
5250}
5251
5252#[gpui::test]
5253async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
5254 init_test(cx, |settings| {
5255 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
5256 });
5257
5258 let mut cx = EditorLspTestContext::new_rust(
5259 lsp::ServerCapabilities {
5260 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5261 ..Default::default()
5262 },
5263 cx,
5264 )
5265 .await;
5266
5267 // Set up a buffer white some trailing whitespace and no trailing newline.
5268 cx.set_state(
5269 &[
5270 "one ", //
5271 "twoˇ", //
5272 "three ", //
5273 "four", //
5274 ]
5275 .join("\n"),
5276 );
5277
5278 // Submit a format request.
5279 let format = cx
5280 .update_editor(|editor, cx| editor.format(&Format, cx))
5281 .unwrap();
5282
5283 // Record which buffer changes have been sent to the language server
5284 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
5285 cx.lsp
5286 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
5287 let buffer_changes = buffer_changes.clone();
5288 move |params, _| {
5289 buffer_changes.lock().extend(
5290 params
5291 .content_changes
5292 .into_iter()
5293 .map(|e| (e.range.unwrap(), e.text)),
5294 );
5295 }
5296 });
5297
5298 // Handle formatting requests to the language server.
5299 cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
5300 let buffer_changes = buffer_changes.clone();
5301 move |_, _| {
5302 // When formatting is requested, trailing whitespace has already been stripped,
5303 // and the trailing newline has already been added.
5304 assert_eq!(
5305 &buffer_changes.lock()[1..],
5306 &[
5307 (
5308 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
5309 "".into()
5310 ),
5311 (
5312 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
5313 "".into()
5314 ),
5315 (
5316 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
5317 "\n".into()
5318 ),
5319 ]
5320 );
5321
5322 // Insert blank lines between each line of the buffer.
5323 async move {
5324 Ok(Some(vec![
5325 lsp::TextEdit {
5326 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
5327 new_text: "\n".into(),
5328 },
5329 lsp::TextEdit {
5330 range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
5331 new_text: "\n".into(),
5332 },
5333 ]))
5334 }
5335 }
5336 });
5337
5338 // After formatting the buffer, the trailing whitespace is stripped,
5339 // a newline is appended, and the edits provided by the language server
5340 // have been applied.
5341 format.await.unwrap();
5342 cx.assert_editor_state(
5343 &[
5344 "one", //
5345 "", //
5346 "twoˇ", //
5347 "", //
5348 "three", //
5349 "four", //
5350 "", //
5351 ]
5352 .join("\n"),
5353 );
5354
5355 // Undoing the formatting undoes the trailing whitespace removal, the
5356 // trailing newline, and the LSP edits.
5357 cx.update_buffer(|buffer, cx| buffer.undo(cx));
5358 cx.assert_editor_state(
5359 &[
5360 "one ", //
5361 "twoˇ", //
5362 "three ", //
5363 "four", //
5364 ]
5365 .join("\n"),
5366 );
5367}
5368
5369#[gpui::test]
5370async fn test_completion(cx: &mut gpui::TestAppContext) {
5371 init_test(cx, |_| {});
5372
5373 let mut cx = EditorLspTestContext::new_rust(
5374 lsp::ServerCapabilities {
5375 completion_provider: Some(lsp::CompletionOptions {
5376 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
5377 resolve_provider: Some(true),
5378 ..Default::default()
5379 }),
5380 ..Default::default()
5381 },
5382 cx,
5383 )
5384 .await;
5385
5386 cx.set_state(indoc! {"
5387 oneˇ
5388 two
5389 three
5390 "});
5391 cx.simulate_keystroke(".");
5392 handle_completion_request(
5393 &mut cx,
5394 indoc! {"
5395 one.|<>
5396 two
5397 three
5398 "},
5399 vec!["first_completion", "second_completion"],
5400 )
5401 .await;
5402 cx.condition(|editor, _| editor.context_menu_visible())
5403 .await;
5404 let apply_additional_edits = cx.update_editor(|editor, cx| {
5405 editor.context_menu_next(&Default::default(), cx);
5406 editor
5407 .confirm_completion(&ConfirmCompletion::default(), cx)
5408 .unwrap()
5409 });
5410 cx.assert_editor_state(indoc! {"
5411 one.second_completionˇ
5412 two
5413 three
5414 "});
5415
5416 handle_resolve_completion_request(
5417 &mut cx,
5418 Some(vec![
5419 (
5420 //This overlaps with the primary completion edit which is
5421 //misbehavior from the LSP spec, test that we filter it out
5422 indoc! {"
5423 one.second_ˇcompletion
5424 two
5425 threeˇ
5426 "},
5427 "overlapping additional edit",
5428 ),
5429 (
5430 indoc! {"
5431 one.second_completion
5432 two
5433 threeˇ
5434 "},
5435 "\nadditional edit",
5436 ),
5437 ]),
5438 )
5439 .await;
5440 apply_additional_edits.await.unwrap();
5441 cx.assert_editor_state(indoc! {"
5442 one.second_completionˇ
5443 two
5444 three
5445 additional edit
5446 "});
5447
5448 cx.set_state(indoc! {"
5449 one.second_completion
5450 twoˇ
5451 threeˇ
5452 additional edit
5453 "});
5454 cx.simulate_keystroke(" ");
5455 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
5456 cx.simulate_keystroke("s");
5457 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
5458
5459 cx.assert_editor_state(indoc! {"
5460 one.second_completion
5461 two sˇ
5462 three sˇ
5463 additional edit
5464 "});
5465 handle_completion_request(
5466 &mut cx,
5467 indoc! {"
5468 one.second_completion
5469 two s
5470 three <s|>
5471 additional edit
5472 "},
5473 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
5474 )
5475 .await;
5476 cx.condition(|editor, _| editor.context_menu_visible())
5477 .await;
5478
5479 cx.simulate_keystroke("i");
5480
5481 handle_completion_request(
5482 &mut cx,
5483 indoc! {"
5484 one.second_completion
5485 two si
5486 three <si|>
5487 additional edit
5488 "},
5489 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
5490 )
5491 .await;
5492 cx.condition(|editor, _| editor.context_menu_visible())
5493 .await;
5494
5495 let apply_additional_edits = cx.update_editor(|editor, cx| {
5496 editor
5497 .confirm_completion(&ConfirmCompletion::default(), cx)
5498 .unwrap()
5499 });
5500 cx.assert_editor_state(indoc! {"
5501 one.second_completion
5502 two sixth_completionˇ
5503 three sixth_completionˇ
5504 additional edit
5505 "});
5506
5507 handle_resolve_completion_request(&mut cx, None).await;
5508 apply_additional_edits.await.unwrap();
5509
5510 cx.update(|cx| {
5511 cx.update_global::<SettingsStore, _, _>(|settings, cx| {
5512 settings.update_user_settings::<EditorSettings>(cx, |settings| {
5513 settings.show_completions_on_input = Some(false);
5514 });
5515 })
5516 });
5517 cx.set_state("editorˇ");
5518 cx.simulate_keystroke(".");
5519 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
5520 cx.simulate_keystroke("c");
5521 cx.simulate_keystroke("l");
5522 cx.simulate_keystroke("o");
5523 cx.assert_editor_state("editor.cloˇ");
5524 assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
5525 cx.update_editor(|editor, cx| {
5526 editor.show_completions(&ShowCompletions, cx);
5527 });
5528 handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
5529 cx.condition(|editor, _| editor.context_menu_visible())
5530 .await;
5531 let apply_additional_edits = cx.update_editor(|editor, cx| {
5532 editor
5533 .confirm_completion(&ConfirmCompletion::default(), cx)
5534 .unwrap()
5535 });
5536 cx.assert_editor_state("editor.closeˇ");
5537 handle_resolve_completion_request(&mut cx, None).await;
5538 apply_additional_edits.await.unwrap();
5539}
5540
5541#[gpui::test]
5542async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
5543 init_test(cx, |_| {});
5544 let mut cx = EditorTestContext::new(cx).await;
5545 let language = Arc::new(Language::new(
5546 LanguageConfig {
5547 line_comment: Some("// ".into()),
5548 ..Default::default()
5549 },
5550 Some(tree_sitter_rust::language()),
5551 ));
5552 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5553
5554 // If multiple selections intersect a line, the line is only toggled once.
5555 cx.set_state(indoc! {"
5556 fn a() {
5557 «//b();
5558 ˇ»// «c();
5559 //ˇ» d();
5560 }
5561 "});
5562
5563 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5564
5565 cx.assert_editor_state(indoc! {"
5566 fn a() {
5567 «b();
5568 c();
5569 ˇ» d();
5570 }
5571 "});
5572
5573 // The comment prefix is inserted at the same column for every line in a
5574 // selection.
5575 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5576
5577 cx.assert_editor_state(indoc! {"
5578 fn a() {
5579 // «b();
5580 // c();
5581 ˇ»// d();
5582 }
5583 "});
5584
5585 // If a selection ends at the beginning of a line, that line is not toggled.
5586 cx.set_selections_state(indoc! {"
5587 fn a() {
5588 // b();
5589 «// c();
5590 ˇ» // d();
5591 }
5592 "});
5593
5594 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5595
5596 cx.assert_editor_state(indoc! {"
5597 fn a() {
5598 // b();
5599 «c();
5600 ˇ» // d();
5601 }
5602 "});
5603
5604 // If a selection span a single line and is empty, the line is toggled.
5605 cx.set_state(indoc! {"
5606 fn a() {
5607 a();
5608 b();
5609 ˇ
5610 }
5611 "});
5612
5613 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5614
5615 cx.assert_editor_state(indoc! {"
5616 fn a() {
5617 a();
5618 b();
5619 //•ˇ
5620 }
5621 "});
5622
5623 // If a selection span multiple lines, empty lines are not toggled.
5624 cx.set_state(indoc! {"
5625 fn a() {
5626 «a();
5627
5628 c();ˇ»
5629 }
5630 "});
5631
5632 cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
5633
5634 cx.assert_editor_state(indoc! {"
5635 fn a() {
5636 // «a();
5637
5638 // c();ˇ»
5639 }
5640 "});
5641}
5642
5643#[gpui::test]
5644async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
5645 init_test(cx, |_| {});
5646
5647 let language = Arc::new(Language::new(
5648 LanguageConfig {
5649 line_comment: Some("// ".into()),
5650 ..Default::default()
5651 },
5652 Some(tree_sitter_rust::language()),
5653 ));
5654
5655 let registry = Arc::new(LanguageRegistry::test());
5656 registry.add(language.clone());
5657
5658 let mut cx = EditorTestContext::new(cx).await;
5659 cx.update_buffer(|buffer, cx| {
5660 buffer.set_language_registry(registry);
5661 buffer.set_language(Some(language), cx);
5662 });
5663
5664 let toggle_comments = &ToggleComments {
5665 advance_downwards: true,
5666 };
5667
5668 // Single cursor on one line -> advance
5669 // Cursor moves horizontally 3 characters as well on non-blank line
5670 cx.set_state(indoc!(
5671 "fn a() {
5672 ˇdog();
5673 cat();
5674 }"
5675 ));
5676 cx.update_editor(|editor, cx| {
5677 editor.toggle_comments(toggle_comments, cx);
5678 });
5679 cx.assert_editor_state(indoc!(
5680 "fn a() {
5681 // dog();
5682 catˇ();
5683 }"
5684 ));
5685
5686 // Single selection on one line -> don't advance
5687 cx.set_state(indoc!(
5688 "fn a() {
5689 «dog()ˇ»;
5690 cat();
5691 }"
5692 ));
5693 cx.update_editor(|editor, cx| {
5694 editor.toggle_comments(toggle_comments, cx);
5695 });
5696 cx.assert_editor_state(indoc!(
5697 "fn a() {
5698 // «dog()ˇ»;
5699 cat();
5700 }"
5701 ));
5702
5703 // Multiple cursors on one line -> advance
5704 cx.set_state(indoc!(
5705 "fn a() {
5706 ˇdˇog();
5707 cat();
5708 }"
5709 ));
5710 cx.update_editor(|editor, cx| {
5711 editor.toggle_comments(toggle_comments, cx);
5712 });
5713 cx.assert_editor_state(indoc!(
5714 "fn a() {
5715 // dog();
5716 catˇ(ˇ);
5717 }"
5718 ));
5719
5720 // Multiple cursors on one line, with selection -> don't advance
5721 cx.set_state(indoc!(
5722 "fn a() {
5723 ˇdˇog«()ˇ»;
5724 cat();
5725 }"
5726 ));
5727 cx.update_editor(|editor, cx| {
5728 editor.toggle_comments(toggle_comments, cx);
5729 });
5730 cx.assert_editor_state(indoc!(
5731 "fn a() {
5732 // ˇdˇog«()ˇ»;
5733 cat();
5734 }"
5735 ));
5736
5737 // Single cursor on one line -> advance
5738 // Cursor moves to column 0 on blank line
5739 cx.set_state(indoc!(
5740 "fn a() {
5741 ˇdog();
5742
5743 cat();
5744 }"
5745 ));
5746 cx.update_editor(|editor, cx| {
5747 editor.toggle_comments(toggle_comments, cx);
5748 });
5749 cx.assert_editor_state(indoc!(
5750 "fn a() {
5751 // dog();
5752 ˇ
5753 cat();
5754 }"
5755 ));
5756
5757 // Single cursor on one line -> advance
5758 // Cursor starts and ends at column 0
5759 cx.set_state(indoc!(
5760 "fn a() {
5761 ˇ dog();
5762 cat();
5763 }"
5764 ));
5765 cx.update_editor(|editor, cx| {
5766 editor.toggle_comments(toggle_comments, cx);
5767 });
5768 cx.assert_editor_state(indoc!(
5769 "fn a() {
5770 // dog();
5771 ˇ cat();
5772 }"
5773 ));
5774}
5775
5776#[gpui::test]
5777async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
5778 init_test(cx, |_| {});
5779
5780 let mut cx = EditorTestContext::new(cx).await;
5781
5782 let html_language = Arc::new(
5783 Language::new(
5784 LanguageConfig {
5785 name: "HTML".into(),
5786 block_comment: Some(("<!-- ".into(), " -->".into())),
5787 ..Default::default()
5788 },
5789 Some(tree_sitter_html::language()),
5790 )
5791 .with_injection_query(
5792 r#"
5793 (script_element
5794 (raw_text) @content
5795 (#set! "language" "javascript"))
5796 "#,
5797 )
5798 .unwrap(),
5799 );
5800
5801 let javascript_language = Arc::new(Language::new(
5802 LanguageConfig {
5803 name: "JavaScript".into(),
5804 line_comment: Some("// ".into()),
5805 ..Default::default()
5806 },
5807 Some(tree_sitter_typescript::language_tsx()),
5808 ));
5809
5810 let registry = Arc::new(LanguageRegistry::test());
5811 registry.add(html_language.clone());
5812 registry.add(javascript_language.clone());
5813
5814 cx.update_buffer(|buffer, cx| {
5815 buffer.set_language_registry(registry);
5816 buffer.set_language(Some(html_language), cx);
5817 });
5818
5819 // Toggle comments for empty selections
5820 cx.set_state(
5821 &r#"
5822 <p>A</p>ˇ
5823 <p>B</p>ˇ
5824 <p>C</p>ˇ
5825 "#
5826 .unindent(),
5827 );
5828 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
5829 cx.assert_editor_state(
5830 &r#"
5831 <!-- <p>A</p>ˇ -->
5832 <!-- <p>B</p>ˇ -->
5833 <!-- <p>C</p>ˇ -->
5834 "#
5835 .unindent(),
5836 );
5837 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
5838 cx.assert_editor_state(
5839 &r#"
5840 <p>A</p>ˇ
5841 <p>B</p>ˇ
5842 <p>C</p>ˇ
5843 "#
5844 .unindent(),
5845 );
5846
5847 // Toggle comments for mixture of empty and non-empty selections, where
5848 // multiple selections occupy a given line.
5849 cx.set_state(
5850 &r#"
5851 <p>A«</p>
5852 <p>ˇ»B</p>ˇ
5853 <p>C«</p>
5854 <p>ˇ»D</p>ˇ
5855 "#
5856 .unindent(),
5857 );
5858
5859 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
5860 cx.assert_editor_state(
5861 &r#"
5862 <!-- <p>A«</p>
5863 <p>ˇ»B</p>ˇ -->
5864 <!-- <p>C«</p>
5865 <p>ˇ»D</p>ˇ -->
5866 "#
5867 .unindent(),
5868 );
5869 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
5870 cx.assert_editor_state(
5871 &r#"
5872 <p>A«</p>
5873 <p>ˇ»B</p>ˇ
5874 <p>C«</p>
5875 <p>ˇ»D</p>ˇ
5876 "#
5877 .unindent(),
5878 );
5879
5880 // Toggle comments when different languages are active for different
5881 // selections.
5882 cx.set_state(
5883 &r#"
5884 ˇ<script>
5885 ˇvar x = new Y();
5886 ˇ</script>
5887 "#
5888 .unindent(),
5889 );
5890 cx.executor().run_until_parked();
5891 cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
5892 cx.assert_editor_state(
5893 &r#"
5894 <!-- ˇ<script> -->
5895 // ˇvar x = new Y();
5896 <!-- ˇ</script> -->
5897 "#
5898 .unindent(),
5899 );
5900}
5901
5902#[gpui::test]
5903fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
5904 init_test(cx, |_| {});
5905
5906 let buffer =
5907 cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
5908 let multibuffer = cx.build_model(|cx| {
5909 let mut multibuffer = MultiBuffer::new(0);
5910 multibuffer.push_excerpts(
5911 buffer.clone(),
5912 [
5913 ExcerptRange {
5914 context: Point::new(0, 0)..Point::new(0, 4),
5915 primary: None,
5916 },
5917 ExcerptRange {
5918 context: Point::new(1, 0)..Point::new(1, 4),
5919 primary: None,
5920 },
5921 ],
5922 cx,
5923 );
5924 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
5925 multibuffer
5926 });
5927
5928 let view = cx.add_window(|cx| build_editor(multibuffer, cx));
5929 view.update(cx, |view, cx| {
5930 assert_eq!(view.text(cx), "aaaa\nbbbb");
5931 view.change_selections(None, cx, |s| {
5932 s.select_ranges([
5933 Point::new(0, 0)..Point::new(0, 0),
5934 Point::new(1, 0)..Point::new(1, 0),
5935 ])
5936 });
5937
5938 view.handle_input("X", cx);
5939 assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
5940 assert_eq!(
5941 view.selections.ranges(cx),
5942 [
5943 Point::new(0, 1)..Point::new(0, 1),
5944 Point::new(1, 1)..Point::new(1, 1),
5945 ]
5946 );
5947
5948 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
5949 view.change_selections(None, cx, |s| {
5950 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
5951 });
5952 view.backspace(&Default::default(), cx);
5953 assert_eq!(view.text(cx), "Xa\nbbb");
5954 assert_eq!(
5955 view.selections.ranges(cx),
5956 [Point::new(1, 0)..Point::new(1, 0)]
5957 );
5958
5959 view.change_selections(None, cx, |s| {
5960 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
5961 });
5962 view.backspace(&Default::default(), cx);
5963 assert_eq!(view.text(cx), "X\nbb");
5964 assert_eq!(
5965 view.selections.ranges(cx),
5966 [Point::new(0, 1)..Point::new(0, 1)]
5967 );
5968 });
5969}
5970
5971#[gpui::test]
5972fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
5973 init_test(cx, |_| {});
5974
5975 let markers = vec![('[', ']').into(), ('(', ')').into()];
5976 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
5977 indoc! {"
5978 [aaaa
5979 (bbbb]
5980 cccc)",
5981 },
5982 markers.clone(),
5983 );
5984 let excerpt_ranges = markers.into_iter().map(|marker| {
5985 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
5986 ExcerptRange {
5987 context,
5988 primary: None,
5989 }
5990 });
5991 let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text));
5992 let multibuffer = cx.build_model(|cx| {
5993 let mut multibuffer = MultiBuffer::new(0);
5994 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
5995 multibuffer
5996 });
5997
5998 let view = cx.add_window(|cx| build_editor(multibuffer, cx));
5999 view.update(cx, |view, cx| {
6000 let (expected_text, selection_ranges) = marked_text_ranges(
6001 indoc! {"
6002 aaaa
6003 bˇbbb
6004 bˇbbˇb
6005 cccc"
6006 },
6007 true,
6008 );
6009 assert_eq!(view.text(cx), expected_text);
6010 view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
6011
6012 view.handle_input("X", cx);
6013
6014 let (expected_text, expected_selections) = marked_text_ranges(
6015 indoc! {"
6016 aaaa
6017 bXˇbbXb
6018 bXˇbbXˇb
6019 cccc"
6020 },
6021 false,
6022 );
6023 assert_eq!(view.text(cx), expected_text);
6024 assert_eq!(view.selections.ranges(cx), expected_selections);
6025
6026 view.newline(&Newline, cx);
6027 let (expected_text, expected_selections) = marked_text_ranges(
6028 indoc! {"
6029 aaaa
6030 bX
6031 ˇbbX
6032 b
6033 bX
6034 ˇbbX
6035 ˇb
6036 cccc"
6037 },
6038 false,
6039 );
6040 assert_eq!(view.text(cx), expected_text);
6041 assert_eq!(view.selections.ranges(cx), expected_selections);
6042 });
6043}
6044
6045#[gpui::test]
6046fn test_refresh_selections(cx: &mut TestAppContext) {
6047 init_test(cx, |_| {});
6048
6049 let buffer =
6050 cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
6051 let mut excerpt1_id = None;
6052 let multibuffer = cx.build_model(|cx| {
6053 let mut multibuffer = MultiBuffer::new(0);
6054 excerpt1_id = multibuffer
6055 .push_excerpts(
6056 buffer.clone(),
6057 [
6058 ExcerptRange {
6059 context: Point::new(0, 0)..Point::new(1, 4),
6060 primary: None,
6061 },
6062 ExcerptRange {
6063 context: Point::new(1, 0)..Point::new(2, 4),
6064 primary: None,
6065 },
6066 ],
6067 cx,
6068 )
6069 .into_iter()
6070 .next();
6071 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
6072 multibuffer
6073 });
6074
6075 let editor = cx.add_window(|cx| {
6076 let mut editor = build_editor(multibuffer.clone(), cx);
6077 let snapshot = editor.snapshot(cx);
6078 editor.change_selections(None, cx, |s| {
6079 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
6080 });
6081 editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
6082 assert_eq!(
6083 editor.selections.ranges(cx),
6084 [
6085 Point::new(1, 3)..Point::new(1, 3),
6086 Point::new(2, 1)..Point::new(2, 1),
6087 ]
6088 );
6089 editor
6090 });
6091
6092 // Refreshing selections is a no-op when excerpts haven't changed.
6093 editor.update(cx, |editor, cx| {
6094 editor.change_selections(None, cx, |s| s.refresh());
6095 assert_eq!(
6096 editor.selections.ranges(cx),
6097 [
6098 Point::new(1, 3)..Point::new(1, 3),
6099 Point::new(2, 1)..Point::new(2, 1),
6100 ]
6101 );
6102 });
6103
6104 multibuffer.update(cx, |multibuffer, cx| {
6105 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
6106 });
6107 editor.update(cx, |editor, cx| {
6108 // Removing an excerpt causes the first selection to become degenerate.
6109 assert_eq!(
6110 editor.selections.ranges(cx),
6111 [
6112 Point::new(0, 0)..Point::new(0, 0),
6113 Point::new(0, 1)..Point::new(0, 1)
6114 ]
6115 );
6116
6117 // Refreshing selections will relocate the first selection to the original buffer
6118 // location.
6119 editor.change_selections(None, cx, |s| s.refresh());
6120 assert_eq!(
6121 editor.selections.ranges(cx),
6122 [
6123 Point::new(0, 1)..Point::new(0, 1),
6124 Point::new(0, 3)..Point::new(0, 3)
6125 ]
6126 );
6127 assert!(editor.selections.pending_anchor().is_some());
6128 });
6129}
6130
6131#[gpui::test]
6132fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
6133 init_test(cx, |_| {});
6134
6135 let buffer =
6136 cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
6137 let mut excerpt1_id = None;
6138 let multibuffer = cx.build_model(|cx| {
6139 let mut multibuffer = MultiBuffer::new(0);
6140 excerpt1_id = multibuffer
6141 .push_excerpts(
6142 buffer.clone(),
6143 [
6144 ExcerptRange {
6145 context: Point::new(0, 0)..Point::new(1, 4),
6146 primary: None,
6147 },
6148 ExcerptRange {
6149 context: Point::new(1, 0)..Point::new(2, 4),
6150 primary: None,
6151 },
6152 ],
6153 cx,
6154 )
6155 .into_iter()
6156 .next();
6157 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
6158 multibuffer
6159 });
6160
6161 let editor = cx.add_window(|cx| {
6162 let mut editor = build_editor(multibuffer.clone(), cx);
6163 let snapshot = editor.snapshot(cx);
6164 editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
6165 assert_eq!(
6166 editor.selections.ranges(cx),
6167 [Point::new(1, 3)..Point::new(1, 3)]
6168 );
6169 editor
6170 });
6171
6172 multibuffer.update(cx, |multibuffer, cx| {
6173 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
6174 });
6175 editor.update(cx, |editor, cx| {
6176 assert_eq!(
6177 editor.selections.ranges(cx),
6178 [Point::new(0, 0)..Point::new(0, 0)]
6179 );
6180
6181 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
6182 editor.change_selections(None, cx, |s| s.refresh());
6183 assert_eq!(
6184 editor.selections.ranges(cx),
6185 [Point::new(0, 3)..Point::new(0, 3)]
6186 );
6187 assert!(editor.selections.pending_anchor().is_some());
6188 });
6189}
6190
6191#[gpui::test]
6192async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
6193 init_test(cx, |_| {});
6194
6195 let language = Arc::new(
6196 Language::new(
6197 LanguageConfig {
6198 brackets: BracketPairConfig {
6199 pairs: vec![
6200 BracketPair {
6201 start: "{".to_string(),
6202 end: "}".to_string(),
6203 close: true,
6204 newline: true,
6205 },
6206 BracketPair {
6207 start: "/* ".to_string(),
6208 end: " */".to_string(),
6209 close: true,
6210 newline: true,
6211 },
6212 ],
6213 ..Default::default()
6214 },
6215 ..Default::default()
6216 },
6217 Some(tree_sitter_rust::language()),
6218 )
6219 .with_indents_query("")
6220 .unwrap(),
6221 );
6222
6223 let text = concat!(
6224 "{ }\n", //
6225 " x\n", //
6226 " /* */\n", //
6227 "x\n", //
6228 "{{} }\n", //
6229 );
6230
6231 let buffer = cx.build_model(|cx| {
6232 Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
6233 });
6234 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
6235 let view = cx.add_window(|cx| build_editor(buffer, cx));
6236 view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
6237 .await;
6238
6239 view.update(cx, |view, cx| {
6240 view.change_selections(None, cx, |s| {
6241 s.select_display_ranges([
6242 DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
6243 DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
6244 DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
6245 ])
6246 });
6247 view.newline(&Newline, cx);
6248
6249 assert_eq!(
6250 view.buffer().read(cx).read(cx).text(),
6251 concat!(
6252 "{ \n", // Suppress rustfmt
6253 "\n", //
6254 "}\n", //
6255 " x\n", //
6256 " /* \n", //
6257 " \n", //
6258 " */\n", //
6259 "x\n", //
6260 "{{} \n", //
6261 "}\n", //
6262 )
6263 );
6264 });
6265}
6266
6267#[gpui::test]
6268fn test_highlighted_ranges(cx: &mut TestAppContext) {
6269 init_test(cx, |_| {});
6270
6271 let editor = cx.add_window(|cx| {
6272 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
6273 build_editor(buffer.clone(), cx)
6274 });
6275
6276 editor.update(cx, |editor, cx| {
6277 struct Type1;
6278 struct Type2;
6279
6280 let buffer = editor.buffer.read(cx).snapshot(cx);
6281
6282 let anchor_range =
6283 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
6284
6285 editor.highlight_background::<Type1>(
6286 vec![
6287 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
6288 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
6289 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
6290 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
6291 ],
6292 |_| Hsla::red(),
6293 cx,
6294 );
6295 editor.highlight_background::<Type2>(
6296 vec![
6297 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
6298 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
6299 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
6300 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
6301 ],
6302 |_| Hsla::green(),
6303 cx,
6304 );
6305
6306 let snapshot = editor.snapshot(cx);
6307 let mut highlighted_ranges = editor.background_highlights_in_range(
6308 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
6309 &snapshot,
6310 theme::current(cx).as_ref(),
6311 );
6312 // Enforce a consistent ordering based on color without relying on the ordering of the
6313 // highlight's `TypeId` which is non-executor.
6314 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
6315 assert_eq!(
6316 highlighted_ranges,
6317 &[
6318 (
6319 DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
6320 Hsla::green(),
6321 ),
6322 (
6323 DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
6324 Hsla::green(),
6325 ),
6326 (
6327 DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
6328 Hsla::red(),
6329 ),
6330 (
6331 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
6332 Hsla::red(),
6333 ),
6334 ]
6335 );
6336 assert_eq!(
6337 editor.background_highlights_in_range(
6338 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
6339 &snapshot,
6340 theme::current(cx).as_ref(),
6341 ),
6342 &[(
6343 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
6344 Hsla::red(),
6345 )]
6346 );
6347 });
6348}
6349
6350#[gpui::test]
6351async fn test_following(cx: &mut gpui::TestAppContext) {
6352 init_test(cx, |_| {});
6353
6354 let fs = FakeFs::new(cx.executor());
6355 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
6356
6357 let buffer = project.update(cx, |project, cx| {
6358 let buffer = project
6359 .create_buffer(&sample_text(16, 8, 'a'), None, cx)
6360 .unwrap();
6361 cx.build_model(|cx| MultiBuffer::singleton(buffer, cx))
6362 });
6363 let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx));
6364 let follower = cx.update(|cx| {
6365 cx.open_window(
6366 WindowOptions {
6367 bounds: WindowBounds::Fixed(Bounds::from_corners(
6368 gpui::Point::new((0. as f64).into(), (0. as f64).into()),
6369 gpui::Point::new((10. as f64).into(), (80. as f64).into()),
6370 )),
6371 ..Default::default()
6372 },
6373 |cx| cx.build_view(|cx| build_editor(buffer.clone(), cx)),
6374 )
6375 });
6376
6377 let is_still_following = Rc::new(RefCell::new(true));
6378 let follower_edit_event_count = Rc::new(RefCell::new(0));
6379 let pending_update = Rc::new(RefCell::new(None));
6380 follower.update(cx, {
6381 let update = pending_update.clone();
6382 let is_still_following = is_still_following.clone();
6383 let follower_edit_event_count = follower_edit_event_count.clone();
6384 |_, cx| {
6385 cx.subscribe(
6386 &leader.root_view(cx).unwrap(),
6387 move |_, leader, event, cx| {
6388 leader
6389 .read(cx)
6390 .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
6391 },
6392 )
6393 .detach();
6394
6395 cx.subscribe(
6396 &follower.root_view(cx).unwrap(),
6397 move |_, _, event: &Event, cx| {
6398 if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) {
6399 *is_still_following.borrow_mut() = false;
6400 }
6401
6402 if let Event::BufferEdited = event {
6403 *follower_edit_event_count.borrow_mut() += 1;
6404 }
6405 },
6406 )
6407 .detach();
6408 }
6409 });
6410
6411 // Update the selections only
6412 leader.update(cx, |leader, cx| {
6413 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
6414 });
6415 follower
6416 .update(cx, |follower, cx| {
6417 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6418 })
6419 .unwrap()
6420 .await
6421 .unwrap();
6422 follower.read_with(cx, |follower, cx| {
6423 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
6424 });
6425 assert_eq!(*is_still_following.borrow(), true);
6426 assert_eq!(*follower_edit_event_count.borrow(), 0);
6427
6428 // Update the scroll position only
6429 leader.update(cx, |leader, cx| {
6430 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
6431 });
6432 follower
6433 .update(cx, |follower, cx| {
6434 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6435 })
6436 .unwrap()
6437 .await
6438 .unwrap();
6439 assert_eq!(
6440 follower
6441 .update(cx, |follower, cx| follower.scroll_position(cx))
6442 .unwrap(),
6443 gpui::Point::new(1.5, 3.5)
6444 );
6445 assert_eq!(*is_still_following.borrow(), true);
6446 assert_eq!(*follower_edit_event_count.borrow(), 0);
6447
6448 // Update the selections and scroll position. The follower's scroll position is updated
6449 // via autoscroll, not via the leader's exact scroll position.
6450 leader.update(cx, |leader, cx| {
6451 leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
6452 leader.request_autoscroll(Autoscroll::newest(), cx);
6453 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx);
6454 });
6455 follower
6456 .update(cx, |follower, cx| {
6457 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6458 })
6459 .unwrap()
6460 .await
6461 .unwrap();
6462 follower.update(cx, |follower, cx| {
6463 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
6464 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
6465 });
6466 assert_eq!(*is_still_following.borrow(), true);
6467
6468 // Creating a pending selection that precedes another selection
6469 leader.update(cx, |leader, cx| {
6470 leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
6471 leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
6472 });
6473 follower
6474 .update(cx, |follower, cx| {
6475 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6476 })
6477 .unwrap()
6478 .await
6479 .unwrap();
6480 follower.read_with(cx, |follower, cx| {
6481 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
6482 });
6483 assert_eq!(*is_still_following.borrow(), true);
6484
6485 // Extend the pending selection so that it surrounds another selection
6486 leader.update(cx, |leader, cx| {
6487 leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
6488 });
6489 follower
6490 .update(cx, |follower, cx| {
6491 follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
6492 })
6493 .unwrap()
6494 .await
6495 .unwrap();
6496 follower.read_with(cx, |follower, cx| {
6497 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
6498 });
6499
6500 // Scrolling locally breaks the follow
6501 follower.update(cx, |follower, cx| {
6502 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
6503 follower.set_scroll_anchor(
6504 ScrollAnchor {
6505 anchor: top_anchor,
6506 offset: gpui::Point::new(0.0, 0.5),
6507 },
6508 cx,
6509 );
6510 });
6511 assert_eq!(*is_still_following.borrow(), false);
6512}
6513
6514#[gpui::test]
6515async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
6516 init_test(cx, |_| {});
6517
6518 let fs = FakeFs::new(cx.executor());
6519 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
6520 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
6521 let pane = workspace
6522 .read_with(cx, |workspace, _| workspace.active_pane().clone())
6523 .unwrap();
6524
6525 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
6526
6527 let leader = pane.update(cx, |_, cx| {
6528 let multibuffer = cx.build_model(|_| MultiBuffer::new(0));
6529 cx.build_view(|cx| build_editor(multibuffer.clone(), cx))
6530 });
6531
6532 // Start following the editor when it has no excerpts.
6533 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
6534 let follower_1 = cx
6535 .update(|cx| {
6536 Editor::from_state_proto(
6537 pane.clone(),
6538 workspace.root_view(cx).unwrap(),
6539 ViewId {
6540 creator: Default::default(),
6541 id: 0,
6542 },
6543 &mut state_message,
6544 cx,
6545 )
6546 })
6547 .unwrap()
6548 .await
6549 .unwrap();
6550
6551 let update_message = Rc::new(RefCell::new(None));
6552 follower_1.update(cx, {
6553 let update = update_message.clone();
6554 |_, cx| {
6555 cx.subscribe(&leader, move |_, leader, event, cx| {
6556 leader
6557 .read(cx)
6558 .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
6559 })
6560 .detach();
6561 }
6562 });
6563
6564 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
6565 (
6566 project
6567 .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
6568 .unwrap(),
6569 project
6570 .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
6571 .unwrap(),
6572 )
6573 });
6574
6575 // Insert some excerpts.
6576 leader.update(cx, |leader, cx| {
6577 leader.buffer.update(cx, |multibuffer, cx| {
6578 let excerpt_ids = multibuffer.push_excerpts(
6579 buffer_1.clone(),
6580 [
6581 ExcerptRange {
6582 context: 1..6,
6583 primary: None,
6584 },
6585 ExcerptRange {
6586 context: 12..15,
6587 primary: None,
6588 },
6589 ExcerptRange {
6590 context: 0..3,
6591 primary: None,
6592 },
6593 ],
6594 cx,
6595 );
6596 multibuffer.insert_excerpts_after(
6597 excerpt_ids[0],
6598 buffer_2.clone(),
6599 [
6600 ExcerptRange {
6601 context: 8..12,
6602 primary: None,
6603 },
6604 ExcerptRange {
6605 context: 0..6,
6606 primary: None,
6607 },
6608 ],
6609 cx,
6610 );
6611 });
6612 });
6613
6614 // Apply the update of adding the excerpts.
6615 follower_1
6616 .update(cx, |follower, cx| {
6617 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
6618 })
6619 .await
6620 .unwrap();
6621 assert_eq!(
6622 follower_1.update(cx, |editor, cx| editor.text(cx)),
6623 leader.update(cx, |editor, cx| editor.text(cx))
6624 );
6625 update_message.borrow_mut().take();
6626
6627 // Start following separately after it already has excerpts.
6628 let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
6629 let follower_2 = cx
6630 .update(|cx| {
6631 Editor::from_state_proto(
6632 pane.clone(),
6633 workspace.clone(),
6634 ViewId {
6635 creator: Default::default(),
6636 id: 0,
6637 },
6638 &mut state_message,
6639 cx,
6640 )
6641 })
6642 .unwrap()
6643 .await
6644 .unwrap();
6645 assert_eq!(
6646 follower_2.update(cx, |editor, cx| editor.text(cx)),
6647 leader.update(cx, |editor, cx| editor.text(cx))
6648 );
6649
6650 // Remove some excerpts.
6651 leader.update(cx, |leader, cx| {
6652 leader.buffer.update(cx, |multibuffer, cx| {
6653 let excerpt_ids = multibuffer.excerpt_ids();
6654 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
6655 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
6656 });
6657 });
6658
6659 // Apply the update of removing the excerpts.
6660 follower_1
6661 .update(cx, |follower, cx| {
6662 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
6663 })
6664 .await
6665 .unwrap();
6666 follower_2
6667 .update(cx, |follower, cx| {
6668 follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
6669 })
6670 .await
6671 .unwrap();
6672 update_message.borrow_mut().take();
6673 assert_eq!(
6674 follower_1.update(cx, |editor, cx| editor.text(cx)),
6675 leader.update(cx, |editor, cx| editor.text(cx))
6676 );
6677}
6678
6679#[test]
6680fn test_combine_syntax_and_fuzzy_match_highlights() {
6681 let string = "abcdefghijklmnop";
6682 let syntax_ranges = [
6683 (
6684 0..3,
6685 HighlightStyle {
6686 color: Some(Hsla::red()),
6687 ..Default::default()
6688 },
6689 ),
6690 (
6691 4..8,
6692 HighlightStyle {
6693 color: Some(Hsla::green()),
6694 ..Default::default()
6695 },
6696 ),
6697 ];
6698 let match_indices = [4, 6, 7, 8];
6699 assert_eq!(
6700 combine_syntax_and_fuzzy_match_highlights(
6701 string,
6702 Default::default(),
6703 syntax_ranges.into_iter(),
6704 &match_indices,
6705 ),
6706 &[
6707 (
6708 0..3,
6709 HighlightStyle {
6710 color: Some(Hsla::red()),
6711 ..Default::default()
6712 },
6713 ),
6714 (
6715 4..5,
6716 HighlightStyle {
6717 color: Some(Hsla::green()),
6718 font_weight: Some(gpui::FontWeight::BOLD),
6719 ..Default::default()
6720 },
6721 ),
6722 (
6723 5..6,
6724 HighlightStyle {
6725 color: Some(Hsla::green()),
6726 ..Default::default()
6727 },
6728 ),
6729 (
6730 6..8,
6731 HighlightStyle {
6732 color: Some(Hsla::green()),
6733 font_weight: Some(gpui::FontWeight::BOLD),
6734 ..Default::default()
6735 },
6736 ),
6737 (
6738 8..9,
6739 HighlightStyle {
6740 font_weight: Some(gpui::FontWeight::BOLD),
6741 ..Default::default()
6742 },
6743 ),
6744 ]
6745 );
6746}
6747
6748#[gpui::test]
6749async fn go_to_prev_overlapping_diagnostic(
6750 executor: BackgroundExecutor,
6751 cx: &mut gpui::TestAppContext,
6752) {
6753 init_test(cx, |_| {});
6754
6755 let mut cx = EditorTestContext::new(cx).await;
6756 let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
6757
6758 cx.set_state(indoc! {"
6759 ˇfn func(abc def: i32) -> u32 {
6760 }
6761 "});
6762
6763 cx.update(|cx| {
6764 project.update(cx, |project, cx| {
6765 project
6766 .update_diagnostics(
6767 LanguageServerId(0),
6768 lsp::PublishDiagnosticsParams {
6769 uri: lsp::Url::from_file_path("/root/file").unwrap(),
6770 version: None,
6771 diagnostics: vec![
6772 lsp::Diagnostic {
6773 range: lsp::Range::new(
6774 lsp::Position::new(0, 11),
6775 lsp::Position::new(0, 12),
6776 ),
6777 severity: Some(lsp::DiagnosticSeverity::ERROR),
6778 ..Default::default()
6779 },
6780 lsp::Diagnostic {
6781 range: lsp::Range::new(
6782 lsp::Position::new(0, 12),
6783 lsp::Position::new(0, 15),
6784 ),
6785 severity: Some(lsp::DiagnosticSeverity::ERROR),
6786 ..Default::default()
6787 },
6788 lsp::Diagnostic {
6789 range: lsp::Range::new(
6790 lsp::Position::new(0, 25),
6791 lsp::Position::new(0, 28),
6792 ),
6793 severity: Some(lsp::DiagnosticSeverity::ERROR),
6794 ..Default::default()
6795 },
6796 ],
6797 },
6798 &[],
6799 cx,
6800 )
6801 .unwrap()
6802 });
6803 });
6804
6805 executor.run_until_parked();
6806
6807 cx.update_editor(|editor, cx| {
6808 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
6809 });
6810
6811 cx.assert_editor_state(indoc! {"
6812 fn func(abc def: i32) -> ˇu32 {
6813 }
6814 "});
6815
6816 cx.update_editor(|editor, cx| {
6817 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
6818 });
6819
6820 cx.assert_editor_state(indoc! {"
6821 fn func(abc ˇdef: i32) -> u32 {
6822 }
6823 "});
6824
6825 cx.update_editor(|editor, cx| {
6826 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
6827 });
6828
6829 cx.assert_editor_state(indoc! {"
6830 fn func(abcˇ def: i32) -> u32 {
6831 }
6832 "});
6833
6834 cx.update_editor(|editor, cx| {
6835 editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
6836 });
6837
6838 cx.assert_editor_state(indoc! {"
6839 fn func(abc def: i32) -> ˇu32 {
6840 }
6841 "});
6842}
6843
6844#[gpui::test]
6845async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
6846 init_test(cx, |_| {});
6847
6848 let mut cx = EditorTestContext::new(cx).await;
6849
6850 let diff_base = r#"
6851 use some::mod;
6852
6853 const A: u32 = 42;
6854
6855 fn main() {
6856 println!("hello");
6857
6858 println!("world");
6859 }
6860 "#
6861 .unindent();
6862
6863 // Edits are modified, removed, modified, added
6864 cx.set_state(
6865 &r#"
6866 use some::modified;
6867
6868 ˇ
6869 fn main() {
6870 println!("hello there");
6871
6872 println!("around the");
6873 println!("world");
6874 }
6875 "#
6876 .unindent(),
6877 );
6878
6879 cx.set_diff_base(Some(&diff_base));
6880 executor.run_until_parked();
6881
6882 cx.update_editor(|editor, cx| {
6883 //Wrap around the bottom of the buffer
6884 for _ in 0..3 {
6885 editor.go_to_hunk(&GoToHunk, cx);
6886 }
6887 });
6888
6889 cx.assert_editor_state(
6890 &r#"
6891 ˇuse some::modified;
6892
6893 fn main() {
6894 println!("hello there");
6895
6896 println!("around the");
6897 println!("world");
6898 }
6899 "#
6900 .unindent(),
6901 );
6902
6903 cx.update_editor(|editor, cx| {
6904 //Wrap around the top of the buffer
6905 for _ in 0..2 {
6906 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
6907 }
6908 });
6909
6910 cx.assert_editor_state(
6911 &r#"
6912 use some::modified;
6913
6914 fn main() {
6915 ˇ println!("hello there");
6916
6917 println!("around the");
6918 println!("world");
6919 }
6920 "#
6921 .unindent(),
6922 );
6923
6924 cx.update_editor(|editor, cx| {
6925 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
6926 });
6927
6928 cx.assert_editor_state(
6929 &r#"
6930 use some::modified;
6931
6932 ˇ
6933 fn main() {
6934 println!("hello there");
6935
6936 println!("around the");
6937 println!("world");
6938 }
6939 "#
6940 .unindent(),
6941 );
6942
6943 cx.update_editor(|editor, cx| {
6944 for _ in 0..3 {
6945 editor.go_to_prev_hunk(&GoToPrevHunk, cx);
6946 }
6947 });
6948
6949 cx.assert_editor_state(
6950 &r#"
6951 use some::modified;
6952
6953 fn main() {
6954 ˇ println!("hello there");
6955
6956 println!("around the");
6957 println!("world");
6958 }
6959 "#
6960 .unindent(),
6961 );
6962
6963 cx.update_editor(|editor, cx| {
6964 editor.fold(&Fold, cx);
6965
6966 //Make sure that the fold only gets one hunk
6967 for _ in 0..4 {
6968 editor.go_to_hunk(&GoToHunk, cx);
6969 }
6970 });
6971
6972 cx.assert_editor_state(
6973 &r#"
6974 ˇuse some::modified;
6975
6976 fn main() {
6977 println!("hello there");
6978
6979 println!("around the");
6980 println!("world");
6981 }
6982 "#
6983 .unindent(),
6984 );
6985}
6986
6987#[test]
6988fn test_split_words() {
6989 fn split<'a>(text: &'a str) -> Vec<&'a str> {
6990 split_words(text).collect()
6991 }
6992
6993 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
6994 assert_eq!(split("hello_world"), &["hello_", "world"]);
6995 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
6996 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
6997 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
6998 assert_eq!(split("helloworld"), &["helloworld"]);
6999}
7000
7001#[gpui::test]
7002async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
7003 init_test(cx, |_| {});
7004
7005 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
7006 let mut assert = |before, after| {
7007 let _state_context = cx.set_state(before);
7008 cx.update_editor(|editor, cx| {
7009 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
7010 });
7011 cx.assert_editor_state(after);
7012 };
7013
7014 // Outside bracket jumps to outside of matching bracket
7015 assert("console.logˇ(var);", "console.log(var)ˇ;");
7016 assert("console.log(var)ˇ;", "console.logˇ(var);");
7017
7018 // Inside bracket jumps to inside of matching bracket
7019 assert("console.log(ˇvar);", "console.log(varˇ);");
7020 assert("console.log(varˇ);", "console.log(ˇvar);");
7021
7022 // When outside a bracket and inside, favor jumping to the inside bracket
7023 assert(
7024 "console.log('foo', [1, 2, 3]ˇ);",
7025 "console.log(ˇ'foo', [1, 2, 3]);",
7026 );
7027 assert(
7028 "console.log(ˇ'foo', [1, 2, 3]);",
7029 "console.log('foo', [1, 2, 3]ˇ);",
7030 );
7031
7032 // Bias forward if two options are equally likely
7033 assert(
7034 "let result = curried_fun()ˇ();",
7035 "let result = curried_fun()()ˇ;",
7036 );
7037
7038 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
7039 assert(
7040 indoc! {"
7041 function test() {
7042 console.log('test')ˇ
7043 }"},
7044 indoc! {"
7045 function test() {
7046 console.logˇ('test')
7047 }"},
7048 );
7049}
7050
7051#[gpui::test(iterations = 10)]
7052async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7053 init_test(cx, |_| {});
7054
7055 let (copilot, copilot_lsp) = Copilot::fake(cx);
7056 cx.update(|cx| cx.set_global(copilot));
7057 let mut cx = EditorLspTestContext::new_rust(
7058 lsp::ServerCapabilities {
7059 completion_provider: Some(lsp::CompletionOptions {
7060 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
7061 ..Default::default()
7062 }),
7063 ..Default::default()
7064 },
7065 cx,
7066 )
7067 .await;
7068
7069 // When inserting, ensure autocompletion is favored over Copilot suggestions.
7070 cx.set_state(indoc! {"
7071 oneˇ
7072 two
7073 three
7074 "});
7075 cx.simulate_keystroke(".");
7076 let _ = handle_completion_request(
7077 &mut cx,
7078 indoc! {"
7079 one.|<>
7080 two
7081 three
7082 "},
7083 vec!["completion_a", "completion_b"],
7084 );
7085 handle_copilot_completion_request(
7086 &copilot_lsp,
7087 vec![copilot::request::Completion {
7088 text: "one.copilot1".into(),
7089 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7090 ..Default::default()
7091 }],
7092 vec![],
7093 );
7094 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7095 cx.update_editor(|editor, cx| {
7096 assert!(editor.context_menu_visible());
7097 assert!(!editor.has_active_copilot_suggestion(cx));
7098
7099 // Confirming a completion inserts it and hides the context menu, without showing
7100 // the copilot suggestion afterwards.
7101 editor
7102 .confirm_completion(&Default::default(), cx)
7103 .unwrap()
7104 .detach();
7105 assert!(!editor.context_menu_visible());
7106 assert!(!editor.has_active_copilot_suggestion(cx));
7107 assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
7108 assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
7109 });
7110
7111 // Ensure Copilot suggestions are shown right away if no autocompletion is available.
7112 cx.set_state(indoc! {"
7113 oneˇ
7114 two
7115 three
7116 "});
7117 cx.simulate_keystroke(".");
7118 let _ = handle_completion_request(
7119 &mut cx,
7120 indoc! {"
7121 one.|<>
7122 two
7123 three
7124 "},
7125 vec![],
7126 );
7127 handle_copilot_completion_request(
7128 &copilot_lsp,
7129 vec![copilot::request::Completion {
7130 text: "one.copilot1".into(),
7131 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7132 ..Default::default()
7133 }],
7134 vec![],
7135 );
7136 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7137 cx.update_editor(|editor, cx| {
7138 assert!(!editor.context_menu_visible());
7139 assert!(editor.has_active_copilot_suggestion(cx));
7140 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7141 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
7142 });
7143
7144 // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
7145 cx.set_state(indoc! {"
7146 oneˇ
7147 two
7148 three
7149 "});
7150 cx.simulate_keystroke(".");
7151 let _ = handle_completion_request(
7152 &mut cx,
7153 indoc! {"
7154 one.|<>
7155 two
7156 three
7157 "},
7158 vec!["completion_a", "completion_b"],
7159 );
7160 handle_copilot_completion_request(
7161 &copilot_lsp,
7162 vec![copilot::request::Completion {
7163 text: "one.copilot1".into(),
7164 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
7165 ..Default::default()
7166 }],
7167 vec![],
7168 );
7169 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7170 cx.update_editor(|editor, cx| {
7171 assert!(editor.context_menu_visible());
7172 assert!(!editor.has_active_copilot_suggestion(cx));
7173
7174 // When hiding the context menu, the Copilot suggestion becomes visible.
7175 editor.hide_context_menu(cx);
7176 assert!(!editor.context_menu_visible());
7177 assert!(editor.has_active_copilot_suggestion(cx));
7178 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7179 assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
7180 });
7181
7182 // Ensure existing completion is interpolated when inserting again.
7183 cx.simulate_keystroke("c");
7184 executor.run_until_parked();
7185 cx.update_editor(|editor, cx| {
7186 assert!(!editor.context_menu_visible());
7187 assert!(editor.has_active_copilot_suggestion(cx));
7188 assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
7189 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7190 });
7191
7192 // After debouncing, new Copilot completions should be requested.
7193 handle_copilot_completion_request(
7194 &copilot_lsp,
7195 vec![copilot::request::Completion {
7196 text: "one.copilot2".into(),
7197 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
7198 ..Default::default()
7199 }],
7200 vec![],
7201 );
7202 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7203 cx.update_editor(|editor, cx| {
7204 assert!(!editor.context_menu_visible());
7205 assert!(editor.has_active_copilot_suggestion(cx));
7206 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7207 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7208
7209 // Canceling should remove the active Copilot suggestion.
7210 editor.cancel(&Default::default(), cx);
7211 assert!(!editor.has_active_copilot_suggestion(cx));
7212 assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
7213 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7214
7215 // After canceling, tabbing shouldn't insert the previously shown suggestion.
7216 editor.tab(&Default::default(), cx);
7217 assert!(!editor.has_active_copilot_suggestion(cx));
7218 assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
7219 assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
7220
7221 // When undoing the previously active suggestion is shown again.
7222 editor.undo(&Default::default(), cx);
7223 assert!(editor.has_active_copilot_suggestion(cx));
7224 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7225 assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
7226 });
7227
7228 // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
7229 cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
7230 cx.update_editor(|editor, cx| {
7231 assert!(editor.has_active_copilot_suggestion(cx));
7232 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7233 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7234
7235 // Tabbing when there is an active suggestion inserts it.
7236 editor.tab(&Default::default(), cx);
7237 assert!(!editor.has_active_copilot_suggestion(cx));
7238 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7239 assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
7240
7241 // When undoing the previously active suggestion is shown again.
7242 editor.undo(&Default::default(), cx);
7243 assert!(editor.has_active_copilot_suggestion(cx));
7244 assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
7245 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7246
7247 // Hide suggestion.
7248 editor.cancel(&Default::default(), cx);
7249 assert!(!editor.has_active_copilot_suggestion(cx));
7250 assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
7251 assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
7252 });
7253
7254 // If an edit occurs outside of this editor but no suggestion is being shown,
7255 // we won't make it visible.
7256 cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
7257 cx.update_editor(|editor, cx| {
7258 assert!(!editor.has_active_copilot_suggestion(cx));
7259 assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
7260 assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
7261 });
7262
7263 // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
7264 cx.update_editor(|editor, cx| {
7265 editor.set_text("fn foo() {\n \n}", cx);
7266 editor.change_selections(None, cx, |s| {
7267 s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
7268 });
7269 });
7270 handle_copilot_completion_request(
7271 &copilot_lsp,
7272 vec![copilot::request::Completion {
7273 text: " let x = 4;".into(),
7274 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
7275 ..Default::default()
7276 }],
7277 vec![],
7278 );
7279
7280 cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
7281 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7282 cx.update_editor(|editor, cx| {
7283 assert!(editor.has_active_copilot_suggestion(cx));
7284 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7285 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
7286
7287 // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
7288 editor.tab(&Default::default(), cx);
7289 assert!(editor.has_active_copilot_suggestion(cx));
7290 assert_eq!(editor.text(cx), "fn foo() {\n \n}");
7291 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7292
7293 // Tabbing again accepts the suggestion.
7294 editor.tab(&Default::default(), cx);
7295 assert!(!editor.has_active_copilot_suggestion(cx));
7296 assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
7297 assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
7298 });
7299}
7300
7301#[gpui::test]
7302async fn test_copilot_completion_invalidation(
7303 executor: BackgroundExecutor,
7304 cx: &mut gpui::TestAppContext,
7305) {
7306 init_test(cx, |_| {});
7307
7308 let (copilot, copilot_lsp) = Copilot::fake(cx);
7309 cx.update(|cx| cx.set_global(copilot));
7310 let mut cx = EditorLspTestContext::new_rust(
7311 lsp::ServerCapabilities {
7312 completion_provider: Some(lsp::CompletionOptions {
7313 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
7314 ..Default::default()
7315 }),
7316 ..Default::default()
7317 },
7318 cx,
7319 )
7320 .await;
7321
7322 cx.set_state(indoc! {"
7323 one
7324 twˇ
7325 three
7326 "});
7327
7328 handle_copilot_completion_request(
7329 &copilot_lsp,
7330 vec![copilot::request::Completion {
7331 text: "two.foo()".into(),
7332 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
7333 ..Default::default()
7334 }],
7335 vec![],
7336 );
7337 cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
7338 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7339 cx.update_editor(|editor, cx| {
7340 assert!(editor.has_active_copilot_suggestion(cx));
7341 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
7342 assert_eq!(editor.text(cx), "one\ntw\nthree\n");
7343
7344 editor.backspace(&Default::default(), cx);
7345 assert!(editor.has_active_copilot_suggestion(cx));
7346 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
7347 assert_eq!(editor.text(cx), "one\nt\nthree\n");
7348
7349 editor.backspace(&Default::default(), cx);
7350 assert!(editor.has_active_copilot_suggestion(cx));
7351 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
7352 assert_eq!(editor.text(cx), "one\n\nthree\n");
7353
7354 // Deleting across the original suggestion range invalidates it.
7355 editor.backspace(&Default::default(), cx);
7356 assert!(!editor.has_active_copilot_suggestion(cx));
7357 assert_eq!(editor.display_text(cx), "one\nthree\n");
7358 assert_eq!(editor.text(cx), "one\nthree\n");
7359
7360 // Undoing the deletion restores the suggestion.
7361 editor.undo(&Default::default(), cx);
7362 assert!(editor.has_active_copilot_suggestion(cx));
7363 assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
7364 assert_eq!(editor.text(cx), "one\n\nthree\n");
7365 });
7366}
7367
7368#[gpui::test]
7369async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7370 init_test(cx, |_| {});
7371
7372 let (copilot, copilot_lsp) = Copilot::fake(cx);
7373 cx.update(|cx| cx.set_global(copilot));
7374
7375 let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n"));
7376 let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n"));
7377 let multibuffer = cx.build_model(|cx| {
7378 let mut multibuffer = MultiBuffer::new(0);
7379 multibuffer.push_excerpts(
7380 buffer_1.clone(),
7381 [ExcerptRange {
7382 context: Point::new(0, 0)..Point::new(2, 0),
7383 primary: None,
7384 }],
7385 cx,
7386 );
7387 multibuffer.push_excerpts(
7388 buffer_2.clone(),
7389 [ExcerptRange {
7390 context: Point::new(0, 0)..Point::new(2, 0),
7391 primary: None,
7392 }],
7393 cx,
7394 );
7395 multibuffer
7396 });
7397 let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
7398
7399 handle_copilot_completion_request(
7400 &copilot_lsp,
7401 vec![copilot::request::Completion {
7402 text: "b = 2 + a".into(),
7403 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
7404 ..Default::default()
7405 }],
7406 vec![],
7407 );
7408 editor.update(cx, |editor, cx| {
7409 // Ensure copilot suggestions are shown for the first excerpt.
7410 editor.change_selections(None, cx, |s| {
7411 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
7412 });
7413 editor.next_copilot_suggestion(&Default::default(), cx);
7414 });
7415 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7416 editor.update(cx, |editor, cx| {
7417 assert!(editor.has_active_copilot_suggestion(cx));
7418 assert_eq!(
7419 editor.display_text(cx),
7420 "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
7421 );
7422 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
7423 });
7424
7425 handle_copilot_completion_request(
7426 &copilot_lsp,
7427 vec![copilot::request::Completion {
7428 text: "d = 4 + c".into(),
7429 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
7430 ..Default::default()
7431 }],
7432 vec![],
7433 );
7434 editor.update(cx, |editor, cx| {
7435 // Move to another excerpt, ensuring the suggestion gets cleared.
7436 editor.change_selections(None, cx, |s| {
7437 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
7438 });
7439 assert!(!editor.has_active_copilot_suggestion(cx));
7440 assert_eq!(
7441 editor.display_text(cx),
7442 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
7443 );
7444 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
7445
7446 // Type a character, ensuring we don't even try to interpolate the previous suggestion.
7447 editor.handle_input(" ", cx);
7448 assert!(!editor.has_active_copilot_suggestion(cx));
7449 assert_eq!(
7450 editor.display_text(cx),
7451 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
7452 );
7453 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
7454 });
7455
7456 // Ensure the new suggestion is displayed when the debounce timeout expires.
7457 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7458 editor.update(cx, |editor, cx| {
7459 assert!(editor.has_active_copilot_suggestion(cx));
7460 assert_eq!(
7461 editor.display_text(cx),
7462 "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
7463 );
7464 assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
7465 });
7466}
7467
7468#[gpui::test]
7469async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
7470 init_test(cx, |settings| {
7471 settings
7472 .copilot
7473 .get_or_insert(Default::default())
7474 .disabled_globs = Some(vec![".env*".to_string()]);
7475 });
7476
7477 let (copilot, copilot_lsp) = Copilot::fake(cx);
7478 cx.update(|cx| cx.set_global(copilot));
7479
7480 let fs = FakeFs::new(cx.executor());
7481 fs.insert_tree(
7482 "/test",
7483 json!({
7484 ".env": "SECRET=something\n",
7485 "README.md": "hello\n"
7486 }),
7487 )
7488 .await;
7489 let project = Project::test(fs, ["/test".as_ref()], cx).await;
7490
7491 let private_buffer = project
7492 .update(cx, |project, cx| {
7493 project.open_local_buffer("/test/.env", cx)
7494 })
7495 .await
7496 .unwrap();
7497 let public_buffer = project
7498 .update(cx, |project, cx| {
7499 project.open_local_buffer("/test/README.md", cx)
7500 })
7501 .await
7502 .unwrap();
7503
7504 let multibuffer = cx.build_model(|cx| {
7505 let mut multibuffer = MultiBuffer::new(0);
7506 multibuffer.push_excerpts(
7507 private_buffer.clone(),
7508 [ExcerptRange {
7509 context: Point::new(0, 0)..Point::new(1, 0),
7510 primary: None,
7511 }],
7512 cx,
7513 );
7514 multibuffer.push_excerpts(
7515 public_buffer.clone(),
7516 [ExcerptRange {
7517 context: Point::new(0, 0)..Point::new(1, 0),
7518 primary: None,
7519 }],
7520 cx,
7521 );
7522 multibuffer
7523 });
7524 let editor = cx.add_window(|cx| build_editor(multibuffer, cx));
7525
7526 let mut copilot_requests = copilot_lsp
7527 .handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move {
7528 Ok(copilot::request::GetCompletionsResult {
7529 completions: vec![copilot::request::Completion {
7530 text: "next line".into(),
7531 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
7532 ..Default::default()
7533 }],
7534 })
7535 });
7536
7537 editor.update(cx, |editor, cx| {
7538 editor.change_selections(None, cx, |selections| {
7539 selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
7540 });
7541 editor.next_copilot_suggestion(&Default::default(), cx);
7542 });
7543
7544 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7545 assert!(copilot_requests.try_next().is_err());
7546
7547 editor.update(cx, |editor, cx| {
7548 editor.change_selections(None, cx, |s| {
7549 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
7550 });
7551 editor.next_copilot_suggestion(&Default::default(), cx);
7552 });
7553
7554 executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
7555 assert!(copilot_requests.try_next().is_ok());
7556}
7557
7558#[gpui::test]
7559async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
7560 init_test(cx, |_| {});
7561
7562 let mut language = Language::new(
7563 LanguageConfig {
7564 name: "Rust".into(),
7565 path_suffixes: vec!["rs".to_string()],
7566 brackets: BracketPairConfig {
7567 pairs: vec![BracketPair {
7568 start: "{".to_string(),
7569 end: "}".to_string(),
7570 close: true,
7571 newline: true,
7572 }],
7573 disabled_scopes_by_bracket_ix: Vec::new(),
7574 },
7575 ..Default::default()
7576 },
7577 Some(tree_sitter_rust::language()),
7578 );
7579 let mut fake_servers = language
7580 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7581 capabilities: lsp::ServerCapabilities {
7582 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
7583 first_trigger_character: "{".to_string(),
7584 more_trigger_character: None,
7585 }),
7586 ..Default::default()
7587 },
7588 ..Default::default()
7589 }))
7590 .await;
7591
7592 let fs = FakeFs::new(cx.executor());
7593 fs.insert_tree(
7594 "/a",
7595 json!({
7596 "main.rs": "fn main() { let a = 5; }",
7597 "other.rs": "// Test file",
7598 }),
7599 )
7600 .await;
7601 let project = Project::test(fs, ["/a".as_ref()], cx).await;
7602 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
7603 let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
7604
7605 let cx = &mut VisualTestContext::from_window(*workspace, cx);
7606
7607 let worktree_id = workspace
7608 .update(cx, |workspace, cx| {
7609 workspace.project().read_with(cx, |project, cx| {
7610 project.worktrees().next().unwrap().read(cx).id()
7611 })
7612 })
7613 .unwrap();
7614
7615 let buffer = project
7616 .update(cx, |project, cx| {
7617 project.open_local_buffer("/a/main.rs", cx)
7618 })
7619 .await
7620 .unwrap();
7621 cx.executor().run_until_parked();
7622 cx.executor().start_waiting();
7623 let fake_server = fake_servers.next().await.unwrap();
7624 let editor_handle = workspace
7625 .update(cx, |workspace, cx| {
7626 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
7627 })
7628 .unwrap()
7629 .await
7630 .unwrap()
7631 .downcast::<Editor>()
7632 .unwrap();
7633
7634 fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
7635 assert_eq!(
7636 params.text_document_position.text_document.uri,
7637 lsp::Url::from_file_path("/a/main.rs").unwrap(),
7638 );
7639 assert_eq!(
7640 params.text_document_position.position,
7641 lsp::Position::new(0, 21),
7642 );
7643
7644 Ok(Some(vec![lsp::TextEdit {
7645 new_text: "]".to_string(),
7646 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
7647 }]))
7648 });
7649
7650 editor_handle.update(cx, |editor, cx| {
7651 cx.focus(&editor_handle);
7652 editor.change_selections(None, cx, |s| {
7653 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
7654 });
7655 editor.handle_input("{", cx);
7656 });
7657
7658 cx.executor().run_until_parked();
7659
7660 buffer.read_with(cx, |buffer, _| {
7661 assert_eq!(
7662 buffer.text(),
7663 "fn main() { let a = {5}; }",
7664 "No extra braces from on type formatting should appear in the buffer"
7665 )
7666 });
7667}
7668
7669#[gpui::test]
7670async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
7671 init_test(cx, |_| {});
7672
7673 let language_name: Arc<str> = "Rust".into();
7674 let mut language = Language::new(
7675 LanguageConfig {
7676 name: Arc::clone(&language_name),
7677 path_suffixes: vec!["rs".to_string()],
7678 ..Default::default()
7679 },
7680 Some(tree_sitter_rust::language()),
7681 );
7682
7683 let server_restarts = Arc::new(AtomicUsize::new(0));
7684 let closure_restarts = Arc::clone(&server_restarts);
7685 let language_server_name = "test language server";
7686 let mut fake_servers = language
7687 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
7688 name: language_server_name,
7689 initialization_options: Some(json!({
7690 "testOptionValue": true
7691 })),
7692 initializer: Some(Box::new(move |fake_server| {
7693 let task_restarts = Arc::clone(&closure_restarts);
7694 fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
7695 task_restarts.fetch_add(1, atomic::Ordering::Release);
7696 futures::future::ready(Ok(()))
7697 });
7698 })),
7699 ..Default::default()
7700 }))
7701 .await;
7702
7703 let fs = FakeFs::new(cx.executor());
7704 fs.insert_tree(
7705 "/a",
7706 json!({
7707 "main.rs": "fn main() { let a = 5; }",
7708 "other.rs": "// Test file",
7709 }),
7710 )
7711 .await;
7712 let project = Project::test(fs, ["/a".as_ref()], cx).await;
7713 project.update(cx, |project, _| project.languages().add(Arc::new(language)));
7714 let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
7715 let _buffer = project
7716 .update(cx, |project, cx| {
7717 project.open_local_buffer("/a/main.rs", cx)
7718 })
7719 .await
7720 .unwrap();
7721 let _fake_server = fake_servers.next().await.unwrap();
7722 update_test_language_settings(cx, |language_settings| {
7723 language_settings.languages.insert(
7724 Arc::clone(&language_name),
7725 LanguageSettingsContent {
7726 tab_size: NonZeroU32::new(8),
7727 ..Default::default()
7728 },
7729 );
7730 });
7731 cx.executor().run_until_parked();
7732 assert_eq!(
7733 server_restarts.load(atomic::Ordering::Acquire),
7734 0,
7735 "Should not restart LSP server on an unrelated change"
7736 );
7737
7738 update_test_project_settings(cx, |project_settings| {
7739 project_settings.lsp.insert(
7740 "Some other server name".into(),
7741 LspSettings {
7742 initialization_options: Some(json!({
7743 "some other init value": false
7744 })),
7745 },
7746 );
7747 });
7748 cx.executor().run_until_parked();
7749 assert_eq!(
7750 server_restarts.load(atomic::Ordering::Acquire),
7751 0,
7752 "Should not restart LSP server on an unrelated LSP settings change"
7753 );
7754
7755 update_test_project_settings(cx, |project_settings| {
7756 project_settings.lsp.insert(
7757 language_server_name.into(),
7758 LspSettings {
7759 initialization_options: Some(json!({
7760 "anotherInitValue": false
7761 })),
7762 },
7763 );
7764 });
7765 cx.executor().run_until_parked();
7766 assert_eq!(
7767 server_restarts.load(atomic::Ordering::Acquire),
7768 1,
7769 "Should restart LSP server on a related LSP settings change"
7770 );
7771
7772 update_test_project_settings(cx, |project_settings| {
7773 project_settings.lsp.insert(
7774 language_server_name.into(),
7775 LspSettings {
7776 initialization_options: Some(json!({
7777 "anotherInitValue": false
7778 })),
7779 },
7780 );
7781 });
7782 cx.executor().run_until_parked();
7783 assert_eq!(
7784 server_restarts.load(atomic::Ordering::Acquire),
7785 1,
7786 "Should not restart LSP server on a related LSP settings change that is the same"
7787 );
7788
7789 update_test_project_settings(cx, |project_settings| {
7790 project_settings.lsp.insert(
7791 language_server_name.into(),
7792 LspSettings {
7793 initialization_options: None,
7794 },
7795 );
7796 });
7797 cx.executor().run_until_parked();
7798 assert_eq!(
7799 server_restarts.load(atomic::Ordering::Acquire),
7800 2,
7801 "Should restart LSP server on another related LSP settings change"
7802 );
7803}
7804
7805#[gpui::test]
7806async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
7807 init_test(cx, |_| {});
7808
7809 let mut cx = EditorLspTestContext::new_rust(
7810 lsp::ServerCapabilities {
7811 completion_provider: Some(lsp::CompletionOptions {
7812 trigger_characters: Some(vec![".".to_string()]),
7813 resolve_provider: Some(true),
7814 ..Default::default()
7815 }),
7816 ..Default::default()
7817 },
7818 cx,
7819 )
7820 .await;
7821
7822 cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
7823 cx.simulate_keystroke(".");
7824 let completion_item = lsp::CompletionItem {
7825 label: "some".into(),
7826 kind: Some(lsp::CompletionItemKind::SNIPPET),
7827 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
7828 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
7829 kind: lsp::MarkupKind::Markdown,
7830 value: "```rust\nSome(2)\n```".to_string(),
7831 })),
7832 deprecated: Some(false),
7833 sort_text: Some("fffffff2".to_string()),
7834 filter_text: Some("some".to_string()),
7835 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
7836 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
7837 range: lsp::Range {
7838 start: lsp::Position {
7839 line: 0,
7840 character: 22,
7841 },
7842 end: lsp::Position {
7843 line: 0,
7844 character: 22,
7845 },
7846 },
7847 new_text: "Some(2)".to_string(),
7848 })),
7849 additional_text_edits: Some(vec![lsp::TextEdit {
7850 range: lsp::Range {
7851 start: lsp::Position {
7852 line: 0,
7853 character: 20,
7854 },
7855 end: lsp::Position {
7856 line: 0,
7857 character: 22,
7858 },
7859 },
7860 new_text: "".to_string(),
7861 }]),
7862 ..Default::default()
7863 };
7864
7865 let closure_completion_item = completion_item.clone();
7866 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
7867 let task_completion_item = closure_completion_item.clone();
7868 async move {
7869 Ok(Some(lsp::CompletionResponse::Array(vec![
7870 task_completion_item,
7871 ])))
7872 }
7873 });
7874
7875 request.next().await;
7876
7877 cx.condition(|editor, _| editor.context_menu_visible())
7878 .await;
7879 let apply_additional_edits = cx.update_editor(|editor, cx| {
7880 editor
7881 .confirm_completion(&ConfirmCompletion::default(), cx)
7882 .unwrap()
7883 });
7884 cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
7885
7886 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
7887 let task_completion_item = completion_item.clone();
7888 async move { Ok(task_completion_item) }
7889 })
7890 .next()
7891 .await
7892 .unwrap();
7893 apply_additional_edits.await.unwrap();
7894 cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
7895}
7896
7897#[gpui::test]
7898async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
7899 init_test(cx, |_| {});
7900
7901 let mut cx = EditorLspTestContext::new(
7902 Language::new(
7903 LanguageConfig {
7904 path_suffixes: vec!["jsx".into()],
7905 overrides: [(
7906 "element".into(),
7907 LanguageConfigOverride {
7908 word_characters: Override::Set(['-'].into_iter().collect()),
7909 ..Default::default()
7910 },
7911 )]
7912 .into_iter()
7913 .collect(),
7914 ..Default::default()
7915 },
7916 Some(tree_sitter_typescript::language_tsx()),
7917 )
7918 .with_override_query("(jsx_self_closing_element) @element")
7919 .unwrap(),
7920 lsp::ServerCapabilities {
7921 completion_provider: Some(lsp::CompletionOptions {
7922 trigger_characters: Some(vec![":".to_string()]),
7923 ..Default::default()
7924 }),
7925 ..Default::default()
7926 },
7927 cx,
7928 )
7929 .await;
7930
7931 cx.lsp
7932 .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
7933 Ok(Some(lsp::CompletionResponse::Array(vec![
7934 lsp::CompletionItem {
7935 label: "bg-blue".into(),
7936 ..Default::default()
7937 },
7938 lsp::CompletionItem {
7939 label: "bg-red".into(),
7940 ..Default::default()
7941 },
7942 lsp::CompletionItem {
7943 label: "bg-yellow".into(),
7944 ..Default::default()
7945 },
7946 ])))
7947 });
7948
7949 cx.set_state(r#"<p class="bgˇ" />"#);
7950
7951 // Trigger completion when typing a dash, because the dash is an extra
7952 // word character in the 'element' scope, which contains the cursor.
7953 cx.simulate_keystroke("-");
7954 cx.executor().run_until_parked();
7955 cx.update_editor(|editor, _| {
7956 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
7957 assert_eq!(
7958 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
7959 &["bg-red", "bg-blue", "bg-yellow"]
7960 );
7961 } else {
7962 panic!("expected completion menu to be open");
7963 }
7964 });
7965
7966 cx.simulate_keystroke("l");
7967 cx.executor().run_until_parked();
7968 cx.update_editor(|editor, _| {
7969 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
7970 assert_eq!(
7971 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
7972 &["bg-blue", "bg-yellow"]
7973 );
7974 } else {
7975 panic!("expected completion menu to be open");
7976 }
7977 });
7978
7979 // When filtering completions, consider the character after the '-' to
7980 // be the start of a subword.
7981 cx.set_state(r#"<p class="yelˇ" />"#);
7982 cx.simulate_keystroke("l");
7983 cx.executor().run_until_parked();
7984 cx.update_editor(|editor, _| {
7985 if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
7986 assert_eq!(
7987 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
7988 &["bg-yellow"]
7989 );
7990 } else {
7991 panic!("expected completion menu to be open");
7992 }
7993 });
7994}
7995
7996#[gpui::test]
7997async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
7998 init_test(cx, |settings| {
7999 settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
8000 });
8001
8002 let mut language = Language::new(
8003 LanguageConfig {
8004 name: "Rust".into(),
8005 path_suffixes: vec!["rs".to_string()],
8006 prettier_parser_name: Some("test_parser".to_string()),
8007 ..Default::default()
8008 },
8009 Some(tree_sitter_rust::language()),
8010 );
8011
8012 let test_plugin = "test_plugin";
8013 let _ = language
8014 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
8015 prettier_plugins: vec![test_plugin],
8016 ..Default::default()
8017 }))
8018 .await;
8019
8020 let fs = FakeFs::new(cx.executor());
8021 fs.insert_file("/file.rs", Default::default()).await;
8022
8023 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
8024 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
8025 project.update(cx, |project, _| {
8026 project.languages().add(Arc::new(language));
8027 });
8028 let buffer = project
8029 .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
8030 .await
8031 .unwrap();
8032
8033 let buffer_text = "one\ntwo\nthree\n";
8034 let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
8035 let editor = cx.add_window(|cx| build_editor(buffer, cx));
8036 editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
8037
8038 let format = editor
8039 .update(cx, |editor, cx| {
8040 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8041 })
8042 .unwrap();
8043 format.await.unwrap();
8044 assert_eq!(
8045 editor.read_with(cx, |editor, cx| editor.text(cx)),
8046 buffer_text.to_string() + prettier_format_suffix,
8047 "Test prettier formatting was not applied to the original buffer text",
8048 );
8049
8050 update_test_language_settings(cx, |settings| {
8051 settings.defaults.formatter = Some(language_settings::Formatter::Auto)
8052 });
8053 let format = editor.update(cx, |editor, cx| {
8054 editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
8055 });
8056 format.await.unwrap();
8057 assert_eq!(
8058 editor.read_with(cx, |editor, cx| editor.text(cx)),
8059 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
8060 "Autoformatting (via test prettier) was not applied to the original buffer text",
8061 );
8062}
8063
8064fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
8065 let point = DisplayPoint::new(row as u32, column as u32);
8066 point..point
8067}
8068
8069fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
8070 let (text, ranges) = marked_text_ranges(marked_text, true);
8071 assert_eq!(view.text(cx), text);
8072 assert_eq!(
8073 view.selections.ranges(cx),
8074 ranges,
8075 "Assert selections are {}",
8076 marked_text
8077 );
8078}
8079
8080/// Handle completion request passing a marked string specifying where the completion
8081/// should be triggered from using '|' character, what range should be replaced, and what completions
8082/// should be returned using '<' and '>' to delimit the range
8083pub fn handle_completion_request<'a>(
8084 cx: &mut EditorLspTestContext<'a>,
8085 marked_string: &str,
8086 completions: Vec<&'static str>,
8087) -> impl Future<Output = ()> {
8088 let complete_from_marker: TextRangeMarker = '|'.into();
8089 let replace_range_marker: TextRangeMarker = ('<', '>').into();
8090 let (_, mut marked_ranges) = marked_text_ranges_by(
8091 marked_string,
8092 vec![complete_from_marker.clone(), replace_range_marker.clone()],
8093 );
8094
8095 let complete_from_position =
8096 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
8097 let replace_range =
8098 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
8099
8100 let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
8101 let completions = completions.clone();
8102 async move {
8103 assert_eq!(params.text_document_position.text_document.uri, url.clone());
8104 assert_eq!(
8105 params.text_document_position.position,
8106 complete_from_position
8107 );
8108 Ok(Some(lsp::CompletionResponse::Array(
8109 completions
8110 .iter()
8111 .map(|completion_text| lsp::CompletionItem {
8112 label: completion_text.to_string(),
8113 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
8114 range: replace_range,
8115 new_text: completion_text.to_string(),
8116 })),
8117 ..Default::default()
8118 })
8119 .collect(),
8120 )))
8121 }
8122 });
8123
8124 async move {
8125 request.next().await;
8126 }
8127}
8128
8129fn handle_resolve_completion_request<'a>(
8130 cx: &mut EditorLspTestContext<'a>,
8131 edits: Option<Vec<(&'static str, &'static str)>>,
8132) -> impl Future<Output = ()> {
8133 let edits = edits.map(|edits| {
8134 edits
8135 .iter()
8136 .map(|(marked_string, new_text)| {
8137 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
8138 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
8139 lsp::TextEdit::new(replace_range, new_text.to_string())
8140 })
8141 .collect::<Vec<_>>()
8142 });
8143
8144 let mut request =
8145 cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
8146 let edits = edits.clone();
8147 async move {
8148 Ok(lsp::CompletionItem {
8149 additional_text_edits: edits,
8150 ..Default::default()
8151 })
8152 }
8153 });
8154
8155 async move {
8156 request.next().await;
8157 }
8158}
8159
8160fn handle_copilot_completion_request(
8161 lsp: &lsp::FakeLanguageServer,
8162 completions: Vec<copilot::request::Completion>,
8163 completions_cycling: Vec<copilot::request::Completion>,
8164) {
8165 lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
8166 let completions = completions.clone();
8167 async move {
8168 Ok(copilot::request::GetCompletionsResult {
8169 completions: completions.clone(),
8170 })
8171 }
8172 });
8173 lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
8174 let completions_cycling = completions_cycling.clone();
8175 async move {
8176 Ok(copilot::request::GetCompletionsResult {
8177 completions: completions_cycling.clone(),
8178 })
8179 }
8180 });
8181}
8182
8183pub(crate) fn update_test_language_settings(
8184 cx: &mut TestAppContext,
8185 f: impl Fn(&mut AllLanguageSettingsContent),
8186) {
8187 cx.update(|cx| {
8188 cx.update_global::<SettingsStore, _, _>(|store, cx| {
8189 store.update_user_settings::<AllLanguageSettings>(cx, f);
8190 });
8191 });
8192}
8193
8194pub(crate) fn update_test_project_settings(
8195 cx: &mut TestAppContext,
8196 f: impl Fn(&mut ProjectSettings),
8197) {
8198 cx.update(|cx| {
8199 cx.update_global::<SettingsStore, _, _>(|store, cx| {
8200 store.update_user_settings::<ProjectSettings>(cx, f);
8201 });
8202 });
8203}
8204
8205pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
8206 cx.update(|cx| {
8207 cx.set_global(SettingsStore::test(cx));
8208 theme::init(cx);
8209 client::init_settings(cx);
8210 language::init(cx);
8211 Project::init_settings(cx);
8212 workspace::init_settings(cx);
8213 crate::init(cx);
8214 });
8215
8216 update_test_language_settings(cx, f);
8217}