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