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