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