1use super::*;
2use crate::{
3 JoinLines,
4 scroll::scroll_amount::ScrollAmount,
5 test::{
6 assert_text_with_selections, build_editor,
7 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
8 editor_test_context::EditorTestContext,
9 select_ranges,
10 },
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions, div,
17};
18use indoc::indoc;
19use language::{
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, Point,
24 language_settings::{
25 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
26 LanguageSettingsContent, LspInsertMode, PrettierSettings,
27 },
28};
29use language_settings::{Formatter, FormatterList, IndentGuideSettings};
30use lsp::CompletionParams;
31use multi_buffer::{IndentGuide, PathKey};
32use parking_lot::Mutex;
33use pretty_assertions::{assert_eq, assert_ne};
34use project::{
35 FakeFs,
36 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
37 project_settings::{LspSettings, ProjectSettings},
38};
39use serde_json::{self, json};
40use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
41use std::{
42 iter,
43 sync::atomic::{self, AtomicUsize},
44};
45use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
46use text::ToPoint as _;
47use unindent::Unindent;
48use util::{
49 assert_set_eq, path,
50 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
51 uri,
52};
53use workspace::{
54 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
55 item::{FollowEvent, FollowableItem, Item, ItemHandle},
56};
57
58#[gpui::test]
59fn test_edit_events(cx: &mut TestAppContext) {
60 init_test(cx, |_| {});
61
62 let buffer = cx.new(|cx| {
63 let mut buffer = language::Buffer::local("123456", cx);
64 buffer.set_group_interval(Duration::from_secs(1));
65 buffer
66 });
67
68 let events = Rc::new(RefCell::new(Vec::new()));
69 let editor1 = cx.add_window({
70 let events = events.clone();
71 |window, cx| {
72 let entity = cx.entity().clone();
73 cx.subscribe_in(
74 &entity,
75 window,
76 move |_, _, event: &EditorEvent, _, _| match event {
77 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
78 EditorEvent::BufferEdited => {
79 events.borrow_mut().push(("editor1", "buffer edited"))
80 }
81 _ => {}
82 },
83 )
84 .detach();
85 Editor::for_buffer(buffer.clone(), None, window, cx)
86 }
87 });
88
89 let editor2 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 cx.subscribe_in(
93 &cx.entity().clone(),
94 window,
95 move |_, _, event: &EditorEvent, _, _| match event {
96 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
97 EditorEvent::BufferEdited => {
98 events.borrow_mut().push(("editor2", "buffer edited"))
99 }
100 _ => {}
101 },
102 )
103 .detach();
104 Editor::for_buffer(buffer.clone(), None, window, cx)
105 }
106 });
107
108 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
109
110 // Mutating editor 1 will emit an `Edited` event only for that editor.
111 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
112 assert_eq!(
113 mem::take(&mut *events.borrow_mut()),
114 [
115 ("editor1", "edited"),
116 ("editor1", "buffer edited"),
117 ("editor2", "buffer edited"),
118 ]
119 );
120
121 // Mutating editor 2 will emit an `Edited` event only for that editor.
122 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor2", "edited"),
127 ("editor1", "buffer edited"),
128 ("editor2", "buffer edited"),
129 ]
130 );
131
132 // Undoing on editor 1 will emit an `Edited` event only for that editor.
133 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor1", "edited"),
138 ("editor1", "buffer edited"),
139 ("editor2", "buffer edited"),
140 ]
141 );
142
143 // Redoing on editor 1 will emit an `Edited` event only for that editor.
144 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
145 assert_eq!(
146 mem::take(&mut *events.borrow_mut()),
147 [
148 ("editor1", "edited"),
149 ("editor1", "buffer edited"),
150 ("editor2", "buffer edited"),
151 ]
152 );
153
154 // Undoing on editor 2 will emit an `Edited` event only for that editor.
155 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
156 assert_eq!(
157 mem::take(&mut *events.borrow_mut()),
158 [
159 ("editor2", "edited"),
160 ("editor1", "buffer edited"),
161 ("editor2", "buffer edited"),
162 ]
163 );
164
165 // Redoing on editor 2 will emit an `Edited` event only for that editor.
166 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
167 assert_eq!(
168 mem::take(&mut *events.borrow_mut()),
169 [
170 ("editor2", "edited"),
171 ("editor1", "buffer edited"),
172 ("editor2", "buffer edited"),
173 ]
174 );
175
176 // No event is emitted when the mutation is a no-op.
177 _ = editor2.update(cx, |editor, window, cx| {
178 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
179
180 editor.backspace(&Backspace, window, cx);
181 });
182 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
183}
184
185#[gpui::test]
186fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
187 init_test(cx, |_| {});
188
189 let mut now = Instant::now();
190 let group_interval = Duration::from_millis(1);
191 let buffer = cx.new(|cx| {
192 let mut buf = language::Buffer::local("123456", cx);
193 buf.set_group_interval(group_interval);
194 buf
195 });
196 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
197 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
198
199 _ = editor.update(cx, |editor, window, cx| {
200 editor.start_transaction_at(now, window, cx);
201 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
202
203 editor.insert("cd", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cd56");
206 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
207
208 editor.start_transaction_at(now, window, cx);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
210 editor.insert("e", window, cx);
211 editor.end_transaction_at(now, cx);
212 assert_eq!(editor.text(cx), "12cde6");
213 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
214
215 now += group_interval + Duration::from_millis(1);
216 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
217
218 // Simulate an edit in another editor
219 buffer.update(cx, |buffer, cx| {
220 buffer.start_transaction_at(now, cx);
221 buffer.edit([(0..1, "a")], None, cx);
222 buffer.edit([(1..1, "b")], None, cx);
223 buffer.end_transaction_at(now, cx);
224 });
225
226 assert_eq!(editor.text(cx), "ab2cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
228
229 // Last transaction happened past the group interval in a different editor.
230 // Undo it individually and don't restore selections.
231 editor.undo(&Undo, window, cx);
232 assert_eq!(editor.text(cx), "12cde6");
233 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
234
235 // First two transactions happened within the group interval in this editor.
236 // Undo them together and restore selections.
237 editor.undo(&Undo, window, cx);
238 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
239 assert_eq!(editor.text(cx), "123456");
240 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
241
242 // Redo the first two transactions together.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
246
247 // Redo the last transaction on its own.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "ab2cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
251
252 // Test empty transactions.
253 editor.start_transaction_at(now, window, cx);
254 editor.end_transaction_at(now, cx);
255 editor.undo(&Undo, window, cx);
256 assert_eq!(editor.text(cx), "12cde6");
257 });
258}
259
260#[gpui::test]
261fn test_ime_composition(cx: &mut TestAppContext) {
262 init_test(cx, |_| {});
263
264 let buffer = cx.new(|cx| {
265 let mut buffer = language::Buffer::local("abcde", cx);
266 // Ensure automatic grouping doesn't occur.
267 buffer.set_group_interval(Duration::ZERO);
268 buffer
269 });
270
271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
272 cx.add_window(|window, cx| {
273 let mut editor = build_editor(buffer.clone(), window, cx);
274
275 // Start a new IME composition.
276 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
277 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
278 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
279 assert_eq!(editor.text(cx), "äbcde");
280 assert_eq!(
281 editor.marked_text_ranges(cx),
282 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
283 );
284
285 // Finalize IME composition.
286 editor.replace_text_in_range(None, "ā", window, cx);
287 assert_eq!(editor.text(cx), "ābcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // IME composition edits are grouped and are undone/redone at once.
291 editor.undo(&Default::default(), window, cx);
292 assert_eq!(editor.text(cx), "abcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294 editor.redo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "ābcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297
298 // Start a new IME composition.
299 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Undoing during an IME composition cancels it.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
311 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
312 assert_eq!(editor.text(cx), "ābcdè");
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
316 );
317
318 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
319 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
320 assert_eq!(editor.text(cx), "ābcdę");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with multiple cursors.
324 editor.change_selections(None, window, cx, |s| {
325 s.select_ranges([
326 OffsetUtf16(1)..OffsetUtf16(1),
327 OffsetUtf16(3)..OffsetUtf16(3),
328 OffsetUtf16(5)..OffsetUtf16(5),
329 ])
330 });
331 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
332 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![
336 OffsetUtf16(0)..OffsetUtf16(3),
337 OffsetUtf16(4)..OffsetUtf16(7),
338 OffsetUtf16(8)..OffsetUtf16(11)
339 ])
340 );
341
342 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
343 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
344 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
345 assert_eq!(
346 editor.marked_text_ranges(cx),
347 Some(vec![
348 OffsetUtf16(1)..OffsetUtf16(2),
349 OffsetUtf16(5)..OffsetUtf16(6),
350 OffsetUtf16(9)..OffsetUtf16(10)
351 ])
352 );
353
354 // Finalize IME composition with multiple cursors.
355 editor.replace_text_in_range(Some(9..10), "2", window, cx);
356 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 editor
360 });
361}
362
363#[gpui::test]
364fn test_selection_with_mouse(cx: &mut TestAppContext) {
365 init_test(cx, |_| {});
366
367 let editor = cx.add_window(|window, cx| {
368 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
369 build_editor(buffer, window, cx)
370 });
371
372 _ = editor.update(cx, |editor, window, cx| {
373 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
374 });
375 assert_eq!(
376 editor
377 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
378 .unwrap(),
379 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
380 );
381
382 _ = editor.update(cx, |editor, window, cx| {
383 editor.update_selection(
384 DisplayPoint::new(DisplayRow(3), 3),
385 0,
386 gpui::Point::<f32>::default(),
387 window,
388 cx,
389 );
390 });
391
392 assert_eq!(
393 editor
394 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
395 .unwrap(),
396 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
397 );
398
399 _ = editor.update(cx, |editor, window, cx| {
400 editor.update_selection(
401 DisplayPoint::new(DisplayRow(1), 1),
402 0,
403 gpui::Point::<f32>::default(),
404 window,
405 cx,
406 );
407 });
408
409 assert_eq!(
410 editor
411 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
412 .unwrap(),
413 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
414 );
415
416 _ = editor.update(cx, |editor, window, cx| {
417 editor.end_selection(window, cx);
418 editor.update_selection(
419 DisplayPoint::new(DisplayRow(3), 3),
420 0,
421 gpui::Point::<f32>::default(),
422 window,
423 cx,
424 );
425 });
426
427 assert_eq!(
428 editor
429 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
430 .unwrap(),
431 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
432 );
433
434 _ = editor.update(cx, |editor, window, cx| {
435 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
436 editor.update_selection(
437 DisplayPoint::new(DisplayRow(0), 0),
438 0,
439 gpui::Point::<f32>::default(),
440 window,
441 cx,
442 );
443 });
444
445 assert_eq!(
446 editor
447 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
448 .unwrap(),
449 [
450 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
451 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
452 ]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.end_selection(window, cx);
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
464 );
465}
466
467#[gpui::test]
468fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
469 init_test(cx, |_| {});
470
471 let editor = cx.add_window(|window, cx| {
472 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
473 build_editor(buffer, window, cx)
474 });
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.end_selection(window, cx);
490 });
491
492 assert_eq!(
493 editor
494 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
495 .unwrap(),
496 [
497 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
498 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
499 ]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
504 });
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.end_selection(window, cx);
508 });
509
510 assert_eq!(
511 editor
512 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
513 .unwrap(),
514 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
515 );
516}
517
518#[gpui::test]
519fn test_canceling_pending_selection(cx: &mut TestAppContext) {
520 init_test(cx, |_| {});
521
522 let editor = cx.add_window(|window, cx| {
523 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
524 build_editor(buffer, window, cx)
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
529 assert_eq!(
530 editor.selections.display_ranges(cx),
531 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
532 );
533 });
534
535 _ = editor.update(cx, |editor, window, cx| {
536 editor.update_selection(
537 DisplayPoint::new(DisplayRow(3), 3),
538 0,
539 gpui::Point::<f32>::default(),
540 window,
541 cx,
542 );
543 assert_eq!(
544 editor.selections.display_ranges(cx),
545 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
546 );
547 });
548
549 _ = editor.update(cx, |editor, window, cx| {
550 editor.cancel(&Cancel, window, cx);
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(1), 1),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563}
564
565#[gpui::test]
566fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
567 init_test(cx, |_| {});
568
569 let editor = cx.add_window(|window, cx| {
570 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
571 build_editor(buffer, window, cx)
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
576 assert_eq!(
577 editor.selections.display_ranges(cx),
578 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
579 );
580
581 editor.move_down(&Default::default(), window, cx);
582 assert_eq!(
583 editor.selections.display_ranges(cx),
584 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
585 );
586
587 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
591 );
592
593 editor.move_up(&Default::default(), window, cx);
594 assert_eq!(
595 editor.selections.display_ranges(cx),
596 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
597 );
598 });
599}
600
601#[gpui::test]
602fn test_clone(cx: &mut TestAppContext) {
603 init_test(cx, |_| {});
604
605 let (text, selection_ranges) = marked_text_ranges(
606 indoc! {"
607 one
608 two
609 threeˇ
610 four
611 fiveˇ
612 "},
613 true,
614 );
615
616 let editor = cx.add_window(|window, cx| {
617 let buffer = MultiBuffer::build_simple(&text, cx);
618 build_editor(buffer, window, cx)
619 });
620
621 _ = editor.update(cx, |editor, window, cx| {
622 editor.change_selections(None, window, cx, |s| {
623 s.select_ranges(selection_ranges.clone())
624 });
625 editor.fold_creases(
626 vec![
627 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
628 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
629 ],
630 true,
631 window,
632 cx,
633 );
634 });
635
636 let cloned_editor = editor
637 .update(cx, |editor, _, cx| {
638 cx.open_window(Default::default(), |window, cx| {
639 cx.new(|cx| editor.clone(window, cx))
640 })
641 })
642 .unwrap()
643 .unwrap();
644
645 let snapshot = editor
646 .update(cx, |e, window, cx| e.snapshot(window, cx))
647 .unwrap();
648 let cloned_snapshot = cloned_editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651
652 assert_eq!(
653 cloned_editor
654 .update(cx, |e, _, cx| e.display_text(cx))
655 .unwrap(),
656 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
657 );
658 assert_eq!(
659 cloned_snapshot
660 .folds_in_range(0..text.len())
661 .collect::<Vec<_>>(),
662 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
663 );
664 assert_set_eq!(
665 cloned_editor
666 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
667 .unwrap(),
668 editor
669 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
670 .unwrap()
671 );
672 assert_set_eq!(
673 cloned_editor
674 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
675 .unwrap(),
676 editor
677 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
678 .unwrap()
679 );
680}
681
682#[gpui::test]
683async fn test_navigation_history(cx: &mut TestAppContext) {
684 init_test(cx, |_| {});
685
686 use workspace::item::Item;
687
688 let fs = FakeFs::new(cx.executor());
689 let project = Project::test(fs, [], cx).await;
690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
691 let pane = workspace
692 .update(cx, |workspace, _, _| workspace.active_pane().clone())
693 .unwrap();
694
695 _ = workspace.update(cx, |_v, window, cx| {
696 cx.new(|cx| {
697 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
698 let mut editor = build_editor(buffer.clone(), window, cx);
699 let handle = cx.entity();
700 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
701
702 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
703 editor.nav_history.as_mut().unwrap().pop_backward(cx)
704 }
705
706 // Move the cursor a small distance.
707 // Nothing is added to the navigation history.
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
711 ])
712 });
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
716 ])
717 });
718 assert!(pop_history(&mut editor, cx).is_none());
719
720 // Move the cursor a large distance.
721 // The history can jump back to the previous position.
722 editor.change_selections(None, window, cx, |s| {
723 s.select_display_ranges([
724 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
725 ])
726 });
727 let nav_entry = pop_history(&mut editor, cx).unwrap();
728 editor.navigate(nav_entry.data.unwrap(), window, cx);
729 assert_eq!(nav_entry.item.id(), cx.entity_id());
730 assert_eq!(
731 editor.selections.display_ranges(cx),
732 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
733 );
734 assert!(pop_history(&mut editor, cx).is_none());
735
736 // Move the cursor a small distance via the mouse.
737 // Nothing is added to the navigation history.
738 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
739 editor.end_selection(window, cx);
740 assert_eq!(
741 editor.selections.display_ranges(cx),
742 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
743 );
744 assert!(pop_history(&mut editor, cx).is_none());
745
746 // Move the cursor a large distance via the mouse.
747 // The history can jump back to the previous position.
748 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
749 editor.end_selection(window, cx);
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
753 );
754 let nav_entry = pop_history(&mut editor, cx).unwrap();
755 editor.navigate(nav_entry.data.unwrap(), window, cx);
756 assert_eq!(nav_entry.item.id(), cx.entity_id());
757 assert_eq!(
758 editor.selections.display_ranges(cx),
759 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
760 );
761 assert!(pop_history(&mut editor, cx).is_none());
762
763 // Set scroll position to check later
764 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
765 let original_scroll_position = editor.scroll_manager.anchor();
766
767 // Jump to the end of the document and adjust scroll
768 editor.move_to_end(&MoveToEnd, window, cx);
769 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
770 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
771
772 let nav_entry = pop_history(&mut editor, cx).unwrap();
773 editor.navigate(nav_entry.data.unwrap(), window, cx);
774 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
777 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
778 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
779 let invalid_point = Point::new(9999, 0);
780 editor.navigate(
781 Box::new(NavigationData {
782 cursor_anchor: invalid_anchor,
783 cursor_position: invalid_point,
784 scroll_anchor: ScrollAnchor {
785 anchor: invalid_anchor,
786 offset: Default::default(),
787 },
788 scroll_top_row: invalid_point.row,
789 }),
790 window,
791 cx,
792 );
793 assert_eq!(
794 editor.selections.display_ranges(cx),
795 &[editor.max_point(cx)..editor.max_point(cx)]
796 );
797 assert_eq!(
798 editor.scroll_position(cx),
799 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
800 );
801
802 editor
803 })
804 });
805}
806
807#[gpui::test]
808fn test_cancel(cx: &mut TestAppContext) {
809 init_test(cx, |_| {});
810
811 let editor = cx.add_window(|window, cx| {
812 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
813 build_editor(buffer, window, cx)
814 });
815
816 _ = editor.update(cx, |editor, window, cx| {
817 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
818 editor.update_selection(
819 DisplayPoint::new(DisplayRow(1), 1),
820 0,
821 gpui::Point::<f32>::default(),
822 window,
823 cx,
824 );
825 editor.end_selection(window, cx);
826
827 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
828 editor.update_selection(
829 DisplayPoint::new(DisplayRow(0), 3),
830 0,
831 gpui::Point::<f32>::default(),
832 window,
833 cx,
834 );
835 editor.end_selection(window, cx);
836 assert_eq!(
837 editor.selections.display_ranges(cx),
838 [
839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
840 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
841 ]
842 );
843 });
844
845 _ = editor.update(cx, |editor, window, cx| {
846 editor.cancel(&Cancel, window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
850 );
851 });
852
853 _ = editor.update(cx, |editor, window, cx| {
854 editor.cancel(&Cancel, window, cx);
855 assert_eq!(
856 editor.selections.display_ranges(cx),
857 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
858 );
859 });
860}
861
862#[gpui::test]
863fn test_fold_action(cx: &mut TestAppContext) {
864 init_test(cx, |_| {});
865
866 let editor = cx.add_window(|window, cx| {
867 let buffer = MultiBuffer::build_simple(
868 &"
869 impl Foo {
870 // Hello!
871
872 fn a() {
873 1
874 }
875
876 fn b() {
877 2
878 }
879
880 fn c() {
881 3
882 }
883 }
884 "
885 .unindent(),
886 cx,
887 );
888 build_editor(buffer.clone(), window, cx)
889 });
890
891 _ = editor.update(cx, |editor, window, cx| {
892 editor.change_selections(None, window, cx, |s| {
893 s.select_display_ranges([
894 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
895 ]);
896 });
897 editor.fold(&Fold, window, cx);
898 assert_eq!(
899 editor.display_text(cx),
900 "
901 impl Foo {
902 // Hello!
903
904 fn a() {
905 1
906 }
907
908 fn b() {⋯
909 }
910
911 fn c() {⋯
912 }
913 }
914 "
915 .unindent(),
916 );
917
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {⋯
923 }
924 "
925 .unindent(),
926 );
927
928 editor.unfold_lines(&UnfoldLines, window, cx);
929 assert_eq!(
930 editor.display_text(cx),
931 "
932 impl Foo {
933 // Hello!
934
935 fn a() {
936 1
937 }
938
939 fn b() {⋯
940 }
941
942 fn c() {⋯
943 }
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 editor.buffer.read(cx).read(cx).text()
953 );
954 });
955}
956
957#[gpui::test]
958fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
959 init_test(cx, |_| {});
960
961 let editor = cx.add_window(|window, cx| {
962 let buffer = MultiBuffer::build_simple(
963 &"
964 class Foo:
965 # Hello!
966
967 def a():
968 print(1)
969
970 def b():
971 print(2)
972
973 def c():
974 print(3)
975 "
976 .unindent(),
977 cx,
978 );
979 build_editor(buffer.clone(), window, cx)
980 });
981
982 _ = editor.update(cx, |editor, window, cx| {
983 editor.change_selections(None, window, cx, |s| {
984 s.select_display_ranges([
985 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
986 ]);
987 });
988 editor.fold(&Fold, window, cx);
989 assert_eq!(
990 editor.display_text(cx),
991 "
992 class Foo:
993 # Hello!
994
995 def a():
996 print(1)
997
998 def b():⋯
999
1000 def c():⋯
1001 "
1002 .unindent(),
1003 );
1004
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 class Foo:⋯
1010 "
1011 .unindent(),
1012 );
1013
1014 editor.unfold_lines(&UnfoldLines, window, cx);
1015 assert_eq!(
1016 editor.display_text(cx),
1017 "
1018 class Foo:
1019 # Hello!
1020
1021 def a():
1022 print(1)
1023
1024 def b():⋯
1025
1026 def c():⋯
1027 "
1028 .unindent(),
1029 );
1030
1031 editor.unfold_lines(&UnfoldLines, window, cx);
1032 assert_eq!(
1033 editor.display_text(cx),
1034 editor.buffer.read(cx).read(cx).text()
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 class Foo:
1047 # Hello!
1048
1049 def a():
1050 print(1)
1051
1052 def b():
1053 print(2)
1054
1055
1056 def c():
1057 print(3)
1058
1059
1060 "
1061 .unindent(),
1062 cx,
1063 );
1064 build_editor(buffer.clone(), window, cx)
1065 });
1066
1067 _ = editor.update(cx, |editor, window, cx| {
1068 editor.change_selections(None, window, cx, |s| {
1069 s.select_display_ranges([
1070 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1071 ]);
1072 });
1073 editor.fold(&Fold, window, cx);
1074 assert_eq!(
1075 editor.display_text(cx),
1076 "
1077 class Foo:
1078 # Hello!
1079
1080 def a():
1081 print(1)
1082
1083 def b():⋯
1084
1085
1086 def c():⋯
1087
1088
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:⋯
1098
1099
1100 "
1101 .unindent(),
1102 );
1103
1104 editor.unfold_lines(&UnfoldLines, window, cx);
1105 assert_eq!(
1106 editor.display_text(cx),
1107 "
1108 class Foo:
1109 # Hello!
1110
1111 def a():
1112 print(1)
1113
1114 def b():⋯
1115
1116
1117 def c():⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_at_level(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148
1149 class Bar:
1150 # World!
1151
1152 def a():
1153 print(1)
1154
1155 def b():
1156 print(2)
1157
1158
1159 "
1160 .unindent(),
1161 cx,
1162 );
1163 build_editor(buffer.clone(), window, cx)
1164 });
1165
1166 _ = editor.update(cx, |editor, window, cx| {
1167 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():⋯
1175
1176 def b():⋯
1177
1178
1179 class Bar:
1180 # World!
1181
1182 def a():⋯
1183
1184 def b():⋯
1185
1186
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:⋯
1196
1197
1198 class Bar:⋯
1199
1200
1201 "
1202 .unindent(),
1203 );
1204
1205 editor.unfold_all(&UnfoldAll, window, cx);
1206 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():
1217 print(2)
1218
1219
1220 class Bar:
1221 # World!
1222
1223 def a():
1224 print(1)
1225
1226 def b():
1227 print(2)
1228
1229
1230 "
1231 .unindent(),
1232 );
1233
1234 assert_eq!(
1235 editor.display_text(cx),
1236 editor.buffer.read(cx).read(cx).text()
1237 );
1238 });
1239}
1240
1241#[gpui::test]
1242fn test_move_cursor(cx: &mut TestAppContext) {
1243 init_test(cx, |_| {});
1244
1245 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1246 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1247
1248 buffer.update(cx, |buffer, cx| {
1249 buffer.edit(
1250 vec![
1251 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1252 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1253 ],
1254 None,
1255 cx,
1256 );
1257 });
1258 _ = editor.update(cx, |editor, window, cx| {
1259 assert_eq!(
1260 editor.selections.display_ranges(cx),
1261 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1262 );
1263
1264 editor.move_down(&MoveDown, window, cx);
1265 assert_eq!(
1266 editor.selections.display_ranges(cx),
1267 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1268 );
1269
1270 editor.move_right(&MoveRight, window, cx);
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1274 );
1275
1276 editor.move_left(&MoveLeft, window, cx);
1277 assert_eq!(
1278 editor.selections.display_ranges(cx),
1279 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1280 );
1281
1282 editor.move_up(&MoveUp, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1286 );
1287
1288 editor.move_to_end(&MoveToEnd, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1292 );
1293
1294 editor.move_to_beginning(&MoveToBeginning, window, cx);
1295 assert_eq!(
1296 editor.selections.display_ranges(cx),
1297 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1298 );
1299
1300 editor.change_selections(None, window, cx, |s| {
1301 s.select_display_ranges([
1302 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1303 ]);
1304 });
1305 editor.select_to_beginning(&SelectToBeginning, window, cx);
1306 assert_eq!(
1307 editor.selections.display_ranges(cx),
1308 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1309 );
1310
1311 editor.select_to_end(&SelectToEnd, window, cx);
1312 assert_eq!(
1313 editor.selections.display_ranges(cx),
1314 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1315 );
1316 });
1317}
1318
1319// TODO: Re-enable this test
1320#[cfg(target_os = "macos")]
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["//".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 //ˇ
2775 "});
2776 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2777 cx.set_state(indoc! {"
2778 ˇ// Foo
2779 "});
2780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2781 cx.assert_editor_state(indoc! {"
2782
2783 ˇ// Foo
2784 "});
2785 }
2786 // Ensure that comment continuations can be disabled.
2787 update_test_language_settings(cx, |settings| {
2788 settings.defaults.extend_comment_on_newline = Some(false);
2789 });
2790 let mut cx = EditorTestContext::new(cx).await;
2791 cx.set_state(indoc! {"
2792 // Fooˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 ˇ
2798 "});
2799}
2800
2801#[gpui::test]
2802fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2803 init_test(cx, |_| {});
2804
2805 let editor = cx.add_window(|window, cx| {
2806 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2807 let mut editor = build_editor(buffer.clone(), window, cx);
2808 editor.change_selections(None, window, cx, |s| {
2809 s.select_ranges([3..4, 11..12, 19..20])
2810 });
2811 editor
2812 });
2813
2814 _ = editor.update(cx, |editor, window, cx| {
2815 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2816 editor.buffer.update(cx, |buffer, cx| {
2817 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2818 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2819 });
2820 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2821
2822 editor.insert("Z", window, cx);
2823 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2824
2825 // The selections are moved after the inserted characters
2826 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2827 });
2828}
2829
2830#[gpui::test]
2831async fn test_tab(cx: &mut TestAppContext) {
2832 init_test(cx, |settings| {
2833 settings.defaults.tab_size = NonZeroU32::new(3)
2834 });
2835
2836 let mut cx = EditorTestContext::new(cx).await;
2837 cx.set_state(indoc! {"
2838 ˇabˇc
2839 ˇ🏀ˇ🏀ˇefg
2840 dˇ
2841 "});
2842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2843 cx.assert_editor_state(indoc! {"
2844 ˇab ˇc
2845 ˇ🏀 ˇ🏀 ˇefg
2846 d ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 a
2851 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2852 "});
2853 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 a
2856 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2857 "});
2858}
2859
2860#[gpui::test]
2861async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let mut cx = EditorTestContext::new(cx).await;
2865 let language = Arc::new(
2866 Language::new(
2867 LanguageConfig::default(),
2868 Some(tree_sitter_rust::LANGUAGE.into()),
2869 )
2870 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2871 .unwrap(),
2872 );
2873 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2874
2875 // cursors that are already at the suggested indent level insert
2876 // a soft tab. cursors that are to the left of the suggested indent
2877 // auto-indent their line.
2878 cx.set_state(indoc! {"
2879 ˇ
2880 const a: B = (
2881 c(
2882 d(
2883 ˇ
2884 )
2885 ˇ
2886 ˇ )
2887 );
2888 "});
2889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2890 cx.assert_editor_state(indoc! {"
2891 ˇ
2892 const a: B = (
2893 c(
2894 d(
2895 ˇ
2896 )
2897 ˇ
2898 ˇ)
2899 );
2900 "});
2901
2902 // handle auto-indent when there are multiple cursors on the same line
2903 cx.set_state(indoc! {"
2904 const a: B = (
2905 c(
2906 ˇ ˇ
2907 ˇ )
2908 );
2909 "});
2910 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 const a: B = (
2913 c(
2914 ˇ
2915 ˇ)
2916 );
2917 "});
2918}
2919
2920#[gpui::test]
2921async fn test_tab_with_mixed_whitespace(cx: &mut TestAppContext) {
2922 init_test(cx, |settings| {
2923 settings.defaults.tab_size = NonZeroU32::new(4)
2924 });
2925
2926 let language = Arc::new(
2927 Language::new(
2928 LanguageConfig::default(),
2929 Some(tree_sitter_rust::LANGUAGE.into()),
2930 )
2931 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2932 .unwrap(),
2933 );
2934
2935 let mut cx = EditorTestContext::new(cx).await;
2936 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2937 cx.set_state(indoc! {"
2938 fn a() {
2939 if b {
2940 \t ˇc
2941 }
2942 }
2943 "});
2944
2945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2946 cx.assert_editor_state(indoc! {"
2947 fn a() {
2948 if b {
2949 ˇc
2950 }
2951 }
2952 "});
2953}
2954
2955#[gpui::test]
2956async fn test_indent_outdent(cx: &mut TestAppContext) {
2957 init_test(cx, |settings| {
2958 settings.defaults.tab_size = NonZeroU32::new(4);
2959 });
2960
2961 let mut cx = EditorTestContext::new(cx).await;
2962
2963 cx.set_state(indoc! {"
2964 «oneˇ» «twoˇ»
2965 three
2966 four
2967 "});
2968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 «oneˇ» «twoˇ»
2971 three
2972 four
2973 "});
2974
2975 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2976 cx.assert_editor_state(indoc! {"
2977 «oneˇ» «twoˇ»
2978 three
2979 four
2980 "});
2981
2982 // select across line ending
2983 cx.set_state(indoc! {"
2984 one two
2985 t«hree
2986 ˇ» four
2987 "});
2988 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2989 cx.assert_editor_state(indoc! {"
2990 one two
2991 t«hree
2992 ˇ» four
2993 "});
2994
2995 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2996 cx.assert_editor_state(indoc! {"
2997 one two
2998 t«hree
2999 ˇ» four
3000 "});
3001
3002 // Ensure that indenting/outdenting works when the cursor is at column 0.
3003 cx.set_state(indoc! {"
3004 one two
3005 ˇthree
3006 four
3007 "});
3008 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3009 cx.assert_editor_state(indoc! {"
3010 one two
3011 ˇthree
3012 four
3013 "});
3014
3015 cx.set_state(indoc! {"
3016 one two
3017 ˇ three
3018 four
3019 "});
3020 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 one two
3023 ˇthree
3024 four
3025 "});
3026}
3027
3028#[gpui::test]
3029async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3030 init_test(cx, |settings| {
3031 settings.defaults.hard_tabs = Some(true);
3032 });
3033
3034 let mut cx = EditorTestContext::new(cx).await;
3035
3036 // select two ranges on one line
3037 cx.set_state(indoc! {"
3038 «oneˇ» «twoˇ»
3039 three
3040 four
3041 "});
3042 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3043 cx.assert_editor_state(indoc! {"
3044 \t«oneˇ» «twoˇ»
3045 three
3046 four
3047 "});
3048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 \t\t«oneˇ» «twoˇ»
3051 three
3052 four
3053 "});
3054 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3055 cx.assert_editor_state(indoc! {"
3056 \t«oneˇ» «twoˇ»
3057 three
3058 four
3059 "});
3060 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3061 cx.assert_editor_state(indoc! {"
3062 «oneˇ» «twoˇ»
3063 three
3064 four
3065 "});
3066
3067 // select across a line ending
3068 cx.set_state(indoc! {"
3069 one two
3070 t«hree
3071 ˇ»four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 one two
3076 \tt«hree
3077 ˇ»four
3078 "});
3079 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 one two
3082 \t\tt«hree
3083 ˇ»four
3084 "});
3085 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3086 cx.assert_editor_state(indoc! {"
3087 one two
3088 \tt«hree
3089 ˇ»four
3090 "});
3091 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 one two
3094 t«hree
3095 ˇ»four
3096 "});
3097
3098 // Ensure that indenting/outdenting works when the cursor is at column 0.
3099 cx.set_state(indoc! {"
3100 one two
3101 ˇthree
3102 four
3103 "});
3104 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 ˇthree
3108 four
3109 "});
3110 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 \tˇthree
3114 four
3115 "});
3116 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 one two
3119 ˇthree
3120 four
3121 "});
3122}
3123
3124#[gpui::test]
3125fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3126 init_test(cx, |settings| {
3127 settings.languages.extend([
3128 (
3129 "TOML".into(),
3130 LanguageSettingsContent {
3131 tab_size: NonZeroU32::new(2),
3132 ..Default::default()
3133 },
3134 ),
3135 (
3136 "Rust".into(),
3137 LanguageSettingsContent {
3138 tab_size: NonZeroU32::new(4),
3139 ..Default::default()
3140 },
3141 ),
3142 ]);
3143 });
3144
3145 let toml_language = Arc::new(Language::new(
3146 LanguageConfig {
3147 name: "TOML".into(),
3148 ..Default::default()
3149 },
3150 None,
3151 ));
3152 let rust_language = Arc::new(Language::new(
3153 LanguageConfig {
3154 name: "Rust".into(),
3155 ..Default::default()
3156 },
3157 None,
3158 ));
3159
3160 let toml_buffer =
3161 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3162 let rust_buffer =
3163 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3164 let multibuffer = cx.new(|cx| {
3165 let mut multibuffer = MultiBuffer::new(ReadWrite);
3166 multibuffer.push_excerpts(
3167 toml_buffer.clone(),
3168 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3169 cx,
3170 );
3171 multibuffer.push_excerpts(
3172 rust_buffer.clone(),
3173 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3174 cx,
3175 );
3176 multibuffer
3177 });
3178
3179 cx.add_window(|window, cx| {
3180 let mut editor = build_editor(multibuffer, window, cx);
3181
3182 assert_eq!(
3183 editor.text(cx),
3184 indoc! {"
3185 a = 1
3186 b = 2
3187
3188 const c: usize = 3;
3189 "}
3190 );
3191
3192 select_ranges(
3193 &mut editor,
3194 indoc! {"
3195 «aˇ» = 1
3196 b = 2
3197
3198 «const c:ˇ» usize = 3;
3199 "},
3200 window,
3201 cx,
3202 );
3203
3204 editor.tab(&Tab, window, cx);
3205 assert_text_with_selections(
3206 &mut editor,
3207 indoc! {"
3208 «aˇ» = 1
3209 b = 2
3210
3211 «const c:ˇ» usize = 3;
3212 "},
3213 cx,
3214 );
3215 editor.backtab(&Backtab, window, cx);
3216 assert_text_with_selections(
3217 &mut editor,
3218 indoc! {"
3219 «aˇ» = 1
3220 b = 2
3221
3222 «const c:ˇ» usize = 3;
3223 "},
3224 cx,
3225 );
3226
3227 editor
3228 });
3229}
3230
3231#[gpui::test]
3232async fn test_backspace(cx: &mut TestAppContext) {
3233 init_test(cx, |_| {});
3234
3235 let mut cx = EditorTestContext::new(cx).await;
3236
3237 // Basic backspace
3238 cx.set_state(indoc! {"
3239 onˇe two three
3240 fou«rˇ» five six
3241 seven «ˇeight nine
3242 »ten
3243 "});
3244 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3245 cx.assert_editor_state(indoc! {"
3246 oˇe two three
3247 fouˇ five six
3248 seven ˇten
3249 "});
3250
3251 // Test backspace inside and around indents
3252 cx.set_state(indoc! {"
3253 zero
3254 ˇone
3255 ˇtwo
3256 ˇ ˇ ˇ three
3257 ˇ ˇ four
3258 "});
3259 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3260 cx.assert_editor_state(indoc! {"
3261 zero
3262 ˇone
3263 ˇtwo
3264 ˇ threeˇ four
3265 "});
3266}
3267
3268#[gpui::test]
3269async fn test_delete(cx: &mut TestAppContext) {
3270 init_test(cx, |_| {});
3271
3272 let mut cx = EditorTestContext::new(cx).await;
3273 cx.set_state(indoc! {"
3274 onˇe two three
3275 fou«rˇ» five six
3276 seven «ˇeight nine
3277 »ten
3278 "});
3279 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3280 cx.assert_editor_state(indoc! {"
3281 onˇ two three
3282 fouˇ five six
3283 seven ˇten
3284 "});
3285}
3286
3287#[gpui::test]
3288fn test_delete_line(cx: &mut TestAppContext) {
3289 init_test(cx, |_| {});
3290
3291 let editor = cx.add_window(|window, cx| {
3292 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3293 build_editor(buffer, window, cx)
3294 });
3295 _ = editor.update(cx, |editor, window, cx| {
3296 editor.change_selections(None, window, cx, |s| {
3297 s.select_display_ranges([
3298 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3299 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3300 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3301 ])
3302 });
3303 editor.delete_line(&DeleteLine, window, cx);
3304 assert_eq!(editor.display_text(cx), "ghi");
3305 assert_eq!(
3306 editor.selections.display_ranges(cx),
3307 vec![
3308 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3309 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3310 ]
3311 );
3312 });
3313
3314 let editor = cx.add_window(|window, cx| {
3315 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3316 build_editor(buffer, window, cx)
3317 });
3318 _ = editor.update(cx, |editor, window, cx| {
3319 editor.change_selections(None, window, cx, |s| {
3320 s.select_display_ranges([
3321 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3322 ])
3323 });
3324 editor.delete_line(&DeleteLine, window, cx);
3325 assert_eq!(editor.display_text(cx), "ghi\n");
3326 assert_eq!(
3327 editor.selections.display_ranges(cx),
3328 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3329 );
3330 });
3331}
3332
3333#[gpui::test]
3334fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3335 init_test(cx, |_| {});
3336
3337 cx.add_window(|window, cx| {
3338 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3339 let mut editor = build_editor(buffer.clone(), window, cx);
3340 let buffer = buffer.read(cx).as_singleton().unwrap();
3341
3342 assert_eq!(
3343 editor.selections.ranges::<Point>(cx),
3344 &[Point::new(0, 0)..Point::new(0, 0)]
3345 );
3346
3347 // When on single line, replace newline at end by space
3348 editor.join_lines(&JoinLines, window, cx);
3349 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3350 assert_eq!(
3351 editor.selections.ranges::<Point>(cx),
3352 &[Point::new(0, 3)..Point::new(0, 3)]
3353 );
3354
3355 // When multiple lines are selected, remove newlines that are spanned by the selection
3356 editor.change_selections(None, window, cx, |s| {
3357 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3358 });
3359 editor.join_lines(&JoinLines, window, cx);
3360 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3361 assert_eq!(
3362 editor.selections.ranges::<Point>(cx),
3363 &[Point::new(0, 11)..Point::new(0, 11)]
3364 );
3365
3366 // Undo should be transactional
3367 editor.undo(&Undo, window, cx);
3368 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3369 assert_eq!(
3370 editor.selections.ranges::<Point>(cx),
3371 &[Point::new(0, 5)..Point::new(2, 2)]
3372 );
3373
3374 // When joining an empty line don't insert a space
3375 editor.change_selections(None, window, cx, |s| {
3376 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3377 });
3378 editor.join_lines(&JoinLines, window, cx);
3379 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3380 assert_eq!(
3381 editor.selections.ranges::<Point>(cx),
3382 [Point::new(2, 3)..Point::new(2, 3)]
3383 );
3384
3385 // We can remove trailing newlines
3386 editor.join_lines(&JoinLines, window, cx);
3387 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3388 assert_eq!(
3389 editor.selections.ranges::<Point>(cx),
3390 [Point::new(2, 3)..Point::new(2, 3)]
3391 );
3392
3393 // We don't blow up on the last line
3394 editor.join_lines(&JoinLines, window, cx);
3395 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3396 assert_eq!(
3397 editor.selections.ranges::<Point>(cx),
3398 [Point::new(2, 3)..Point::new(2, 3)]
3399 );
3400
3401 // reset to test indentation
3402 editor.buffer.update(cx, |buffer, cx| {
3403 buffer.edit(
3404 [
3405 (Point::new(1, 0)..Point::new(1, 2), " "),
3406 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3407 ],
3408 None,
3409 cx,
3410 )
3411 });
3412
3413 // We remove any leading spaces
3414 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3415 editor.change_selections(None, window, cx, |s| {
3416 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3417 });
3418 editor.join_lines(&JoinLines, window, cx);
3419 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3420
3421 // We don't insert a space for a line containing only spaces
3422 editor.join_lines(&JoinLines, window, cx);
3423 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3424
3425 // We ignore any leading tabs
3426 editor.join_lines(&JoinLines, window, cx);
3427 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3428
3429 editor
3430 });
3431}
3432
3433#[gpui::test]
3434fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3435 init_test(cx, |_| {});
3436
3437 cx.add_window(|window, cx| {
3438 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3439 let mut editor = build_editor(buffer.clone(), window, cx);
3440 let buffer = buffer.read(cx).as_singleton().unwrap();
3441
3442 editor.change_selections(None, window, cx, |s| {
3443 s.select_ranges([
3444 Point::new(0, 2)..Point::new(1, 1),
3445 Point::new(1, 2)..Point::new(1, 2),
3446 Point::new(3, 1)..Point::new(3, 2),
3447 ])
3448 });
3449
3450 editor.join_lines(&JoinLines, window, cx);
3451 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3452
3453 assert_eq!(
3454 editor.selections.ranges::<Point>(cx),
3455 [
3456 Point::new(0, 7)..Point::new(0, 7),
3457 Point::new(1, 3)..Point::new(1, 3)
3458 ]
3459 );
3460 editor
3461 });
3462}
3463
3464#[gpui::test]
3465async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3466 init_test(cx, |_| {});
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 let diff_base = r#"
3471 Line 0
3472 Line 1
3473 Line 2
3474 Line 3
3475 "#
3476 .unindent();
3477
3478 cx.set_state(
3479 &r#"
3480 ˇLine 0
3481 Line 1
3482 Line 2
3483 Line 3
3484 "#
3485 .unindent(),
3486 );
3487
3488 cx.set_head_text(&diff_base);
3489 executor.run_until_parked();
3490
3491 // Join lines
3492 cx.update_editor(|editor, window, cx| {
3493 editor.join_lines(&JoinLines, window, cx);
3494 });
3495 executor.run_until_parked();
3496
3497 cx.assert_editor_state(
3498 &r#"
3499 Line 0ˇ Line 1
3500 Line 2
3501 Line 3
3502 "#
3503 .unindent(),
3504 );
3505 // Join again
3506 cx.update_editor(|editor, window, cx| {
3507 editor.join_lines(&JoinLines, window, cx);
3508 });
3509 executor.run_until_parked();
3510
3511 cx.assert_editor_state(
3512 &r#"
3513 Line 0 Line 1ˇ Line 2
3514 Line 3
3515 "#
3516 .unindent(),
3517 );
3518}
3519
3520#[gpui::test]
3521async fn test_custom_newlines_cause_no_false_positive_diffs(
3522 executor: BackgroundExecutor,
3523 cx: &mut TestAppContext,
3524) {
3525 init_test(cx, |_| {});
3526 let mut cx = EditorTestContext::new(cx).await;
3527 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3528 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3529 executor.run_until_parked();
3530
3531 cx.update_editor(|editor, window, cx| {
3532 let snapshot = editor.snapshot(window, cx);
3533 assert_eq!(
3534 snapshot
3535 .buffer_snapshot
3536 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3537 .collect::<Vec<_>>(),
3538 Vec::new(),
3539 "Should not have any diffs for files with custom newlines"
3540 );
3541 });
3542}
3543
3544#[gpui::test]
3545async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3546 init_test(cx, |_| {});
3547
3548 let mut cx = EditorTestContext::new(cx).await;
3549
3550 // Test sort_lines_case_insensitive()
3551 cx.set_state(indoc! {"
3552 «z
3553 y
3554 x
3555 Z
3556 Y
3557 Xˇ»
3558 "});
3559 cx.update_editor(|e, window, cx| {
3560 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3561 });
3562 cx.assert_editor_state(indoc! {"
3563 «x
3564 X
3565 y
3566 Y
3567 z
3568 Zˇ»
3569 "});
3570
3571 // Test reverse_lines()
3572 cx.set_state(indoc! {"
3573 «5
3574 4
3575 3
3576 2
3577 1ˇ»
3578 "});
3579 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 «1
3582 2
3583 3
3584 4
3585 5ˇ»
3586 "});
3587
3588 // Skip testing shuffle_line()
3589
3590 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3591 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3592
3593 // Don't manipulate when cursor is on single line, but expand the selection
3594 cx.set_state(indoc! {"
3595 ddˇdd
3596 ccc
3597 bb
3598 a
3599 "});
3600 cx.update_editor(|e, window, cx| {
3601 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3602 });
3603 cx.assert_editor_state(indoc! {"
3604 «ddddˇ»
3605 ccc
3606 bb
3607 a
3608 "});
3609
3610 // Basic manipulate case
3611 // Start selection moves to column 0
3612 // End of selection shrinks to fit shorter line
3613 cx.set_state(indoc! {"
3614 dd«d
3615 ccc
3616 bb
3617 aaaaaˇ»
3618 "});
3619 cx.update_editor(|e, window, cx| {
3620 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3621 });
3622 cx.assert_editor_state(indoc! {"
3623 «aaaaa
3624 bb
3625 ccc
3626 dddˇ»
3627 "});
3628
3629 // Manipulate case with newlines
3630 cx.set_state(indoc! {"
3631 dd«d
3632 ccc
3633
3634 bb
3635 aaaaa
3636
3637 ˇ»
3638 "});
3639 cx.update_editor(|e, window, cx| {
3640 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3641 });
3642 cx.assert_editor_state(indoc! {"
3643 «
3644
3645 aaaaa
3646 bb
3647 ccc
3648 dddˇ»
3649
3650 "});
3651
3652 // Adding new line
3653 cx.set_state(indoc! {"
3654 aa«a
3655 bbˇ»b
3656 "});
3657 cx.update_editor(|e, window, cx| {
3658 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3659 });
3660 cx.assert_editor_state(indoc! {"
3661 «aaa
3662 bbb
3663 added_lineˇ»
3664 "});
3665
3666 // Removing line
3667 cx.set_state(indoc! {"
3668 aa«a
3669 bbbˇ»
3670 "});
3671 cx.update_editor(|e, window, cx| {
3672 e.manipulate_lines(window, cx, |lines| {
3673 lines.pop();
3674 })
3675 });
3676 cx.assert_editor_state(indoc! {"
3677 «aaaˇ»
3678 "});
3679
3680 // Removing all lines
3681 cx.set_state(indoc! {"
3682 aa«a
3683 bbbˇ»
3684 "});
3685 cx.update_editor(|e, window, cx| {
3686 e.manipulate_lines(window, cx, |lines| {
3687 lines.drain(..);
3688 })
3689 });
3690 cx.assert_editor_state(indoc! {"
3691 ˇ
3692 "});
3693}
3694
3695#[gpui::test]
3696async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3697 init_test(cx, |_| {});
3698
3699 let mut cx = EditorTestContext::new(cx).await;
3700
3701 // Consider continuous selection as single selection
3702 cx.set_state(indoc! {"
3703 Aaa«aa
3704 cˇ»c«c
3705 bb
3706 aaaˇ»aa
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3710 });
3711 cx.assert_editor_state(indoc! {"
3712 «Aaaaa
3713 ccc
3714 bb
3715 aaaaaˇ»
3716 "});
3717
3718 cx.set_state(indoc! {"
3719 Aaa«aa
3720 cˇ»c«c
3721 bb
3722 aaaˇ»aa
3723 "});
3724 cx.update_editor(|e, window, cx| {
3725 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3726 });
3727 cx.assert_editor_state(indoc! {"
3728 «Aaaaa
3729 ccc
3730 bbˇ»
3731 "});
3732
3733 // Consider non continuous selection as distinct dedup operations
3734 cx.set_state(indoc! {"
3735 «aaaaa
3736 bb
3737 aaaaa
3738 aaaaaˇ»
3739
3740 aaa«aaˇ»
3741 "});
3742 cx.update_editor(|e, window, cx| {
3743 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3744 });
3745 cx.assert_editor_state(indoc! {"
3746 «aaaaa
3747 bbˇ»
3748
3749 «aaaaaˇ»
3750 "});
3751}
3752
3753#[gpui::test]
3754async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3755 init_test(cx, |_| {});
3756
3757 let mut cx = EditorTestContext::new(cx).await;
3758
3759 cx.set_state(indoc! {"
3760 «Aaa
3761 aAa
3762 Aaaˇ»
3763 "});
3764 cx.update_editor(|e, window, cx| {
3765 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3766 });
3767 cx.assert_editor_state(indoc! {"
3768 «Aaa
3769 aAaˇ»
3770 "});
3771
3772 cx.set_state(indoc! {"
3773 «Aaa
3774 aAa
3775 aaAˇ»
3776 "});
3777 cx.update_editor(|e, window, cx| {
3778 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3779 });
3780 cx.assert_editor_state(indoc! {"
3781 «Aaaˇ»
3782 "});
3783}
3784
3785#[gpui::test]
3786async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3787 init_test(cx, |_| {});
3788
3789 let mut cx = EditorTestContext::new(cx).await;
3790
3791 // Manipulate with multiple selections on a single line
3792 cx.set_state(indoc! {"
3793 dd«dd
3794 cˇ»c«c
3795 bb
3796 aaaˇ»aa
3797 "});
3798 cx.update_editor(|e, window, cx| {
3799 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3800 });
3801 cx.assert_editor_state(indoc! {"
3802 «aaaaa
3803 bb
3804 ccc
3805 ddddˇ»
3806 "});
3807
3808 // Manipulate with multiple disjoin selections
3809 cx.set_state(indoc! {"
3810 5«
3811 4
3812 3
3813 2
3814 1ˇ»
3815
3816 dd«dd
3817 ccc
3818 bb
3819 aaaˇ»aa
3820 "});
3821 cx.update_editor(|e, window, cx| {
3822 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3823 });
3824 cx.assert_editor_state(indoc! {"
3825 «1
3826 2
3827 3
3828 4
3829 5ˇ»
3830
3831 «aaaaa
3832 bb
3833 ccc
3834 ddddˇ»
3835 "});
3836
3837 // Adding lines on each selection
3838 cx.set_state(indoc! {"
3839 2«
3840 1ˇ»
3841
3842 bb«bb
3843 aaaˇ»aa
3844 "});
3845 cx.update_editor(|e, window, cx| {
3846 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3847 });
3848 cx.assert_editor_state(indoc! {"
3849 «2
3850 1
3851 added lineˇ»
3852
3853 «bbbb
3854 aaaaa
3855 added lineˇ»
3856 "});
3857
3858 // Removing lines on each selection
3859 cx.set_state(indoc! {"
3860 2«
3861 1ˇ»
3862
3863 bb«bb
3864 aaaˇ»aa
3865 "});
3866 cx.update_editor(|e, window, cx| {
3867 e.manipulate_lines(window, cx, |lines| {
3868 lines.pop();
3869 })
3870 });
3871 cx.assert_editor_state(indoc! {"
3872 «2ˇ»
3873
3874 «bbbbˇ»
3875 "});
3876}
3877
3878#[gpui::test]
3879async fn test_manipulate_text(cx: &mut TestAppContext) {
3880 init_test(cx, |_| {});
3881
3882 let mut cx = EditorTestContext::new(cx).await;
3883
3884 // Test convert_to_upper_case()
3885 cx.set_state(indoc! {"
3886 «hello worldˇ»
3887 "});
3888 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3889 cx.assert_editor_state(indoc! {"
3890 «HELLO WORLDˇ»
3891 "});
3892
3893 // Test convert_to_lower_case()
3894 cx.set_state(indoc! {"
3895 «HELLO WORLDˇ»
3896 "});
3897 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3898 cx.assert_editor_state(indoc! {"
3899 «hello worldˇ»
3900 "});
3901
3902 // Test multiple line, single selection case
3903 cx.set_state(indoc! {"
3904 «The quick brown
3905 fox jumps over
3906 the lazy dogˇ»
3907 "});
3908 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3909 cx.assert_editor_state(indoc! {"
3910 «The Quick Brown
3911 Fox Jumps Over
3912 The Lazy Dogˇ»
3913 "});
3914
3915 // Test multiple line, single selection case
3916 cx.set_state(indoc! {"
3917 «The quick brown
3918 fox jumps over
3919 the lazy dogˇ»
3920 "});
3921 cx.update_editor(|e, window, cx| {
3922 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3923 });
3924 cx.assert_editor_state(indoc! {"
3925 «TheQuickBrown
3926 FoxJumpsOver
3927 TheLazyDogˇ»
3928 "});
3929
3930 // From here on out, test more complex cases of manipulate_text()
3931
3932 // Test no selection case - should affect words cursors are in
3933 // Cursor at beginning, middle, and end of word
3934 cx.set_state(indoc! {"
3935 ˇhello big beauˇtiful worldˇ
3936 "});
3937 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3938 cx.assert_editor_state(indoc! {"
3939 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3940 "});
3941
3942 // Test multiple selections on a single line and across multiple lines
3943 cx.set_state(indoc! {"
3944 «Theˇ» quick «brown
3945 foxˇ» jumps «overˇ»
3946 the «lazyˇ» dog
3947 "});
3948 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 «THEˇ» quick «BROWN
3951 FOXˇ» jumps «OVERˇ»
3952 the «LAZYˇ» dog
3953 "});
3954
3955 // Test case where text length grows
3956 cx.set_state(indoc! {"
3957 «tschüߡ»
3958 "});
3959 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3960 cx.assert_editor_state(indoc! {"
3961 «TSCHÜSSˇ»
3962 "});
3963
3964 // Test to make sure we don't crash when text shrinks
3965 cx.set_state(indoc! {"
3966 aaa_bbbˇ
3967 "});
3968 cx.update_editor(|e, window, cx| {
3969 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3970 });
3971 cx.assert_editor_state(indoc! {"
3972 «aaaBbbˇ»
3973 "});
3974
3975 // Test to make sure we all aware of the fact that each word can grow and shrink
3976 // Final selections should be aware of this fact
3977 cx.set_state(indoc! {"
3978 aaa_bˇbb bbˇb_ccc ˇccc_ddd
3979 "});
3980 cx.update_editor(|e, window, cx| {
3981 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
3982 });
3983 cx.assert_editor_state(indoc! {"
3984 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
3985 "});
3986
3987 cx.set_state(indoc! {"
3988 «hElLo, WoRld!ˇ»
3989 "});
3990 cx.update_editor(|e, window, cx| {
3991 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
3992 });
3993 cx.assert_editor_state(indoc! {"
3994 «HeLlO, wOrLD!ˇ»
3995 "});
3996}
3997
3998#[gpui::test]
3999fn test_duplicate_line(cx: &mut TestAppContext) {
4000 init_test(cx, |_| {});
4001
4002 let editor = cx.add_window(|window, cx| {
4003 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4004 build_editor(buffer, window, cx)
4005 });
4006 _ = editor.update(cx, |editor, window, cx| {
4007 editor.change_selections(None, window, cx, |s| {
4008 s.select_display_ranges([
4009 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4010 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4011 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4012 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4013 ])
4014 });
4015 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4016 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4017 assert_eq!(
4018 editor.selections.display_ranges(cx),
4019 vec![
4020 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4021 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4022 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4023 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4024 ]
4025 );
4026 });
4027
4028 let editor = cx.add_window(|window, cx| {
4029 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4030 build_editor(buffer, window, cx)
4031 });
4032 _ = editor.update(cx, |editor, window, cx| {
4033 editor.change_selections(None, window, cx, |s| {
4034 s.select_display_ranges([
4035 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4036 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4037 ])
4038 });
4039 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4040 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4041 assert_eq!(
4042 editor.selections.display_ranges(cx),
4043 vec![
4044 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4045 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4046 ]
4047 );
4048 });
4049
4050 // With `move_upwards` the selections stay in place, except for
4051 // the lines inserted above them
4052 let editor = cx.add_window(|window, cx| {
4053 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4054 build_editor(buffer, window, cx)
4055 });
4056 _ = editor.update(cx, |editor, window, cx| {
4057 editor.change_selections(None, window, cx, |s| {
4058 s.select_display_ranges([
4059 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4060 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4061 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4062 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4063 ])
4064 });
4065 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4066 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4067 assert_eq!(
4068 editor.selections.display_ranges(cx),
4069 vec![
4070 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4071 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4072 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4073 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4074 ]
4075 );
4076 });
4077
4078 let editor = cx.add_window(|window, cx| {
4079 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4080 build_editor(buffer, window, cx)
4081 });
4082 _ = editor.update(cx, |editor, window, cx| {
4083 editor.change_selections(None, window, cx, |s| {
4084 s.select_display_ranges([
4085 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4086 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4087 ])
4088 });
4089 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4090 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4091 assert_eq!(
4092 editor.selections.display_ranges(cx),
4093 vec![
4094 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4095 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4096 ]
4097 );
4098 });
4099
4100 let editor = cx.add_window(|window, cx| {
4101 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4102 build_editor(buffer, window, cx)
4103 });
4104 _ = editor.update(cx, |editor, window, cx| {
4105 editor.change_selections(None, window, cx, |s| {
4106 s.select_display_ranges([
4107 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4108 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4109 ])
4110 });
4111 editor.duplicate_selection(&DuplicateSelection, window, cx);
4112 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4113 assert_eq!(
4114 editor.selections.display_ranges(cx),
4115 vec![
4116 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4117 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4118 ]
4119 );
4120 });
4121}
4122
4123#[gpui::test]
4124fn test_move_line_up_down(cx: &mut TestAppContext) {
4125 init_test(cx, |_| {});
4126
4127 let editor = cx.add_window(|window, cx| {
4128 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4129 build_editor(buffer, window, cx)
4130 });
4131 _ = editor.update(cx, |editor, window, cx| {
4132 editor.fold_creases(
4133 vec![
4134 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4135 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4136 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4137 ],
4138 true,
4139 window,
4140 cx,
4141 );
4142 editor.change_selections(None, window, cx, |s| {
4143 s.select_display_ranges([
4144 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4145 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4146 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4147 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4148 ])
4149 });
4150 assert_eq!(
4151 editor.display_text(cx),
4152 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4153 );
4154
4155 editor.move_line_up(&MoveLineUp, window, cx);
4156 assert_eq!(
4157 editor.display_text(cx),
4158 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4159 );
4160 assert_eq!(
4161 editor.selections.display_ranges(cx),
4162 vec![
4163 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4164 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4165 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4166 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4167 ]
4168 );
4169 });
4170
4171 _ = editor.update(cx, |editor, window, cx| {
4172 editor.move_line_down(&MoveLineDown, window, cx);
4173 assert_eq!(
4174 editor.display_text(cx),
4175 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4176 );
4177 assert_eq!(
4178 editor.selections.display_ranges(cx),
4179 vec![
4180 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4181 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4182 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4183 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4184 ]
4185 );
4186 });
4187
4188 _ = editor.update(cx, |editor, window, cx| {
4189 editor.move_line_down(&MoveLineDown, window, cx);
4190 assert_eq!(
4191 editor.display_text(cx),
4192 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4193 );
4194 assert_eq!(
4195 editor.selections.display_ranges(cx),
4196 vec![
4197 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4198 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4199 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4200 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4201 ]
4202 );
4203 });
4204
4205 _ = editor.update(cx, |editor, window, cx| {
4206 editor.move_line_up(&MoveLineUp, window, cx);
4207 assert_eq!(
4208 editor.display_text(cx),
4209 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4210 );
4211 assert_eq!(
4212 editor.selections.display_ranges(cx),
4213 vec![
4214 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4215 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4216 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4217 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4218 ]
4219 );
4220 });
4221}
4222
4223#[gpui::test]
4224fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4225 init_test(cx, |_| {});
4226
4227 let editor = cx.add_window(|window, cx| {
4228 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4229 build_editor(buffer, window, cx)
4230 });
4231 _ = editor.update(cx, |editor, window, cx| {
4232 let snapshot = editor.buffer.read(cx).snapshot(cx);
4233 editor.insert_blocks(
4234 [BlockProperties {
4235 style: BlockStyle::Fixed,
4236 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4237 height: Some(1),
4238 render: Arc::new(|_| div().into_any()),
4239 priority: 0,
4240 }],
4241 Some(Autoscroll::fit()),
4242 cx,
4243 );
4244 editor.change_selections(None, window, cx, |s| {
4245 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4246 });
4247 editor.move_line_down(&MoveLineDown, window, cx);
4248 });
4249}
4250
4251#[gpui::test]
4252async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4253 init_test(cx, |_| {});
4254
4255 let mut cx = EditorTestContext::new(cx).await;
4256 cx.set_state(
4257 &"
4258 ˇzero
4259 one
4260 two
4261 three
4262 four
4263 five
4264 "
4265 .unindent(),
4266 );
4267
4268 // Create a four-line block that replaces three lines of text.
4269 cx.update_editor(|editor, window, cx| {
4270 let snapshot = editor.snapshot(window, cx);
4271 let snapshot = &snapshot.buffer_snapshot;
4272 let placement = BlockPlacement::Replace(
4273 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4274 );
4275 editor.insert_blocks(
4276 [BlockProperties {
4277 placement,
4278 height: Some(4),
4279 style: BlockStyle::Sticky,
4280 render: Arc::new(|_| gpui::div().into_any_element()),
4281 priority: 0,
4282 }],
4283 None,
4284 cx,
4285 );
4286 });
4287
4288 // Move down so that the cursor touches the block.
4289 cx.update_editor(|editor, window, cx| {
4290 editor.move_down(&Default::default(), window, cx);
4291 });
4292 cx.assert_editor_state(
4293 &"
4294 zero
4295 «one
4296 two
4297 threeˇ»
4298 four
4299 five
4300 "
4301 .unindent(),
4302 );
4303
4304 // Move down past the block.
4305 cx.update_editor(|editor, window, cx| {
4306 editor.move_down(&Default::default(), window, cx);
4307 });
4308 cx.assert_editor_state(
4309 &"
4310 zero
4311 one
4312 two
4313 three
4314 ˇfour
4315 five
4316 "
4317 .unindent(),
4318 );
4319}
4320
4321#[gpui::test]
4322fn test_transpose(cx: &mut TestAppContext) {
4323 init_test(cx, |_| {});
4324
4325 _ = cx.add_window(|window, cx| {
4326 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4327 editor.set_style(EditorStyle::default(), window, cx);
4328 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4329 editor.transpose(&Default::default(), window, cx);
4330 assert_eq!(editor.text(cx), "bac");
4331 assert_eq!(editor.selections.ranges(cx), [2..2]);
4332
4333 editor.transpose(&Default::default(), window, cx);
4334 assert_eq!(editor.text(cx), "bca");
4335 assert_eq!(editor.selections.ranges(cx), [3..3]);
4336
4337 editor.transpose(&Default::default(), window, cx);
4338 assert_eq!(editor.text(cx), "bac");
4339 assert_eq!(editor.selections.ranges(cx), [3..3]);
4340
4341 editor
4342 });
4343
4344 _ = cx.add_window(|window, cx| {
4345 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4346 editor.set_style(EditorStyle::default(), window, cx);
4347 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4348 editor.transpose(&Default::default(), window, cx);
4349 assert_eq!(editor.text(cx), "acb\nde");
4350 assert_eq!(editor.selections.ranges(cx), [3..3]);
4351
4352 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4353 editor.transpose(&Default::default(), window, cx);
4354 assert_eq!(editor.text(cx), "acbd\ne");
4355 assert_eq!(editor.selections.ranges(cx), [5..5]);
4356
4357 editor.transpose(&Default::default(), window, cx);
4358 assert_eq!(editor.text(cx), "acbde\n");
4359 assert_eq!(editor.selections.ranges(cx), [6..6]);
4360
4361 editor.transpose(&Default::default(), window, cx);
4362 assert_eq!(editor.text(cx), "acbd\ne");
4363 assert_eq!(editor.selections.ranges(cx), [6..6]);
4364
4365 editor
4366 });
4367
4368 _ = cx.add_window(|window, cx| {
4369 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4370 editor.set_style(EditorStyle::default(), window, cx);
4371 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4372 editor.transpose(&Default::default(), window, cx);
4373 assert_eq!(editor.text(cx), "bacd\ne");
4374 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4375
4376 editor.transpose(&Default::default(), window, cx);
4377 assert_eq!(editor.text(cx), "bcade\n");
4378 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4379
4380 editor.transpose(&Default::default(), window, cx);
4381 assert_eq!(editor.text(cx), "bcda\ne");
4382 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4383
4384 editor.transpose(&Default::default(), window, cx);
4385 assert_eq!(editor.text(cx), "bcade\n");
4386 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4387
4388 editor.transpose(&Default::default(), window, cx);
4389 assert_eq!(editor.text(cx), "bcaed\n");
4390 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4391
4392 editor
4393 });
4394
4395 _ = cx.add_window(|window, cx| {
4396 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4397 editor.set_style(EditorStyle::default(), window, cx);
4398 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4399 editor.transpose(&Default::default(), window, cx);
4400 assert_eq!(editor.text(cx), "🏀🍐✋");
4401 assert_eq!(editor.selections.ranges(cx), [8..8]);
4402
4403 editor.transpose(&Default::default(), window, cx);
4404 assert_eq!(editor.text(cx), "🏀✋🍐");
4405 assert_eq!(editor.selections.ranges(cx), [11..11]);
4406
4407 editor.transpose(&Default::default(), window, cx);
4408 assert_eq!(editor.text(cx), "🏀🍐✋");
4409 assert_eq!(editor.selections.ranges(cx), [11..11]);
4410
4411 editor
4412 });
4413}
4414
4415#[gpui::test]
4416async fn test_rewrap(cx: &mut TestAppContext) {
4417 init_test(cx, |settings| {
4418 settings.languages.extend([
4419 (
4420 "Markdown".into(),
4421 LanguageSettingsContent {
4422 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4423 ..Default::default()
4424 },
4425 ),
4426 (
4427 "Plain Text".into(),
4428 LanguageSettingsContent {
4429 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4430 ..Default::default()
4431 },
4432 ),
4433 ])
4434 });
4435
4436 let mut cx = EditorTestContext::new(cx).await;
4437
4438 let language_with_c_comments = Arc::new(Language::new(
4439 LanguageConfig {
4440 line_comments: vec!["// ".into()],
4441 ..LanguageConfig::default()
4442 },
4443 None,
4444 ));
4445 let language_with_pound_comments = Arc::new(Language::new(
4446 LanguageConfig {
4447 line_comments: vec!["# ".into()],
4448 ..LanguageConfig::default()
4449 },
4450 None,
4451 ));
4452 let markdown_language = Arc::new(Language::new(
4453 LanguageConfig {
4454 name: "Markdown".into(),
4455 ..LanguageConfig::default()
4456 },
4457 None,
4458 ));
4459 let language_with_doc_comments = Arc::new(Language::new(
4460 LanguageConfig {
4461 line_comments: vec!["// ".into(), "/// ".into()],
4462 ..LanguageConfig::default()
4463 },
4464 Some(tree_sitter_rust::LANGUAGE.into()),
4465 ));
4466
4467 let plaintext_language = Arc::new(Language::new(
4468 LanguageConfig {
4469 name: "Plain Text".into(),
4470 ..LanguageConfig::default()
4471 },
4472 None,
4473 ));
4474
4475 assert_rewrap(
4476 indoc! {"
4477 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4478 "},
4479 indoc! {"
4480 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4481 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4482 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4483 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4484 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4485 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4486 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4487 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4488 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4489 // porttitor id. Aliquam id accumsan eros.
4490 "},
4491 language_with_c_comments.clone(),
4492 &mut cx,
4493 );
4494
4495 // Test that rewrapping works inside of a selection
4496 assert_rewrap(
4497 indoc! {"
4498 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4499 "},
4500 indoc! {"
4501 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4502 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4503 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4504 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4505 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4506 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4507 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4508 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4509 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4510 // porttitor id. Aliquam id accumsan eros.ˇ»
4511 "},
4512 language_with_c_comments.clone(),
4513 &mut cx,
4514 );
4515
4516 // Test that cursors that expand to the same region are collapsed.
4517 assert_rewrap(
4518 indoc! {"
4519 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4520 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4521 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4522 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4523 "},
4524 indoc! {"
4525 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4526 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4527 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4528 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4529 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4530 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4531 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4532 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4533 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4534 // porttitor id. Aliquam id accumsan eros.
4535 "},
4536 language_with_c_comments.clone(),
4537 &mut cx,
4538 );
4539
4540 // Test that non-contiguous selections are treated separately.
4541 assert_rewrap(
4542 indoc! {"
4543 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4544 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4545 //
4546 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4547 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4548 "},
4549 indoc! {"
4550 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4551 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4552 // auctor, eu lacinia sapien scelerisque.
4553 //
4554 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4555 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4556 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4557 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4558 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4559 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4560 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4561 "},
4562 language_with_c_comments.clone(),
4563 &mut cx,
4564 );
4565
4566 // Test that different comment prefixes are supported.
4567 assert_rewrap(
4568 indoc! {"
4569 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4570 "},
4571 indoc! {"
4572 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4573 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4574 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4575 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4576 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4577 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4578 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4579 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4580 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4581 # accumsan eros.
4582 "},
4583 language_with_pound_comments.clone(),
4584 &mut cx,
4585 );
4586
4587 // Test that rewrapping is ignored outside of comments in most languages.
4588 assert_rewrap(
4589 indoc! {"
4590 /// Adds two numbers.
4591 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4592 fn add(a: u32, b: u32) -> u32 {
4593 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4594 }
4595 "},
4596 indoc! {"
4597 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4598 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4599 fn add(a: u32, b: u32) -> u32 {
4600 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4601 }
4602 "},
4603 language_with_doc_comments.clone(),
4604 &mut cx,
4605 );
4606
4607 // Test that rewrapping works in Markdown and Plain Text languages.
4608 assert_rewrap(
4609 indoc! {"
4610 # Hello
4611
4612 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4613 "},
4614 indoc! {"
4615 # Hello
4616
4617 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4618 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4619 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4620 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4621 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4622 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4623 Integer sit amet scelerisque nisi.
4624 "},
4625 markdown_language,
4626 &mut cx,
4627 );
4628
4629 assert_rewrap(
4630 indoc! {"
4631 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4632 "},
4633 indoc! {"
4634 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4635 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4636 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4637 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4638 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4639 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4640 Integer sit amet scelerisque nisi.
4641 "},
4642 plaintext_language,
4643 &mut cx,
4644 );
4645
4646 // Test rewrapping unaligned comments in a selection.
4647 assert_rewrap(
4648 indoc! {"
4649 fn foo() {
4650 if true {
4651 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4652 // Praesent semper egestas tellus id dignissim.ˇ»
4653 do_something();
4654 } else {
4655 //
4656 }
4657 }
4658 "},
4659 indoc! {"
4660 fn foo() {
4661 if true {
4662 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4663 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4664 // egestas tellus id dignissim.ˇ»
4665 do_something();
4666 } else {
4667 //
4668 }
4669 }
4670 "},
4671 language_with_doc_comments.clone(),
4672 &mut cx,
4673 );
4674
4675 assert_rewrap(
4676 indoc! {"
4677 fn foo() {
4678 if true {
4679 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4680 // Praesent semper egestas tellus id dignissim.»
4681 do_something();
4682 } else {
4683 //
4684 }
4685
4686 }
4687 "},
4688 indoc! {"
4689 fn foo() {
4690 if true {
4691 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4692 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4693 // egestas tellus id dignissim.»
4694 do_something();
4695 } else {
4696 //
4697 }
4698
4699 }
4700 "},
4701 language_with_doc_comments.clone(),
4702 &mut cx,
4703 );
4704
4705 #[track_caller]
4706 fn assert_rewrap(
4707 unwrapped_text: &str,
4708 wrapped_text: &str,
4709 language: Arc<Language>,
4710 cx: &mut EditorTestContext,
4711 ) {
4712 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4713 cx.set_state(unwrapped_text);
4714 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4715 cx.assert_editor_state(wrapped_text);
4716 }
4717}
4718
4719#[gpui::test]
4720async fn test_hard_wrap(cx: &mut TestAppContext) {
4721 init_test(cx, |_| {});
4722 let mut cx = EditorTestContext::new(cx).await;
4723
4724 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4725 cx.update_editor(|editor, _, cx| {
4726 editor.set_hard_wrap(Some(14), cx);
4727 });
4728
4729 cx.set_state(indoc!(
4730 "
4731 one two three ˇ
4732 "
4733 ));
4734 cx.simulate_input("four");
4735 cx.run_until_parked();
4736
4737 cx.assert_editor_state(indoc!(
4738 "
4739 one two three
4740 fourˇ
4741 "
4742 ));
4743
4744 cx.update_editor(|editor, window, cx| {
4745 editor.newline(&Default::default(), window, cx);
4746 });
4747 cx.run_until_parked();
4748 cx.assert_editor_state(indoc!(
4749 "
4750 one two three
4751 four
4752 ˇ
4753 "
4754 ));
4755
4756 cx.simulate_input("five");
4757 cx.run_until_parked();
4758 cx.assert_editor_state(indoc!(
4759 "
4760 one two three
4761 four
4762 fiveˇ
4763 "
4764 ));
4765
4766 cx.update_editor(|editor, window, cx| {
4767 editor.newline(&Default::default(), window, cx);
4768 });
4769 cx.run_until_parked();
4770 cx.simulate_input("# ");
4771 cx.run_until_parked();
4772 cx.assert_editor_state(indoc!(
4773 "
4774 one two three
4775 four
4776 five
4777 # ˇ
4778 "
4779 ));
4780
4781 cx.update_editor(|editor, window, cx| {
4782 editor.newline(&Default::default(), window, cx);
4783 });
4784 cx.run_until_parked();
4785 cx.assert_editor_state(indoc!(
4786 "
4787 one two three
4788 four
4789 five
4790 #\x20
4791 #ˇ
4792 "
4793 ));
4794
4795 cx.simulate_input(" 6");
4796 cx.run_until_parked();
4797 cx.assert_editor_state(indoc!(
4798 "
4799 one two three
4800 four
4801 five
4802 #
4803 # 6ˇ
4804 "
4805 ));
4806}
4807
4808#[gpui::test]
4809async fn test_clipboard(cx: &mut TestAppContext) {
4810 init_test(cx, |_| {});
4811
4812 let mut cx = EditorTestContext::new(cx).await;
4813
4814 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4815 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4816 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4817
4818 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4819 cx.set_state("two ˇfour ˇsix ˇ");
4820 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4821 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4822
4823 // Paste again but with only two cursors. Since the number of cursors doesn't
4824 // match the number of slices in the clipboard, the entire clipboard text
4825 // is pasted at each cursor.
4826 cx.set_state("ˇtwo one✅ four three six five ˇ");
4827 cx.update_editor(|e, window, cx| {
4828 e.handle_input("( ", window, cx);
4829 e.paste(&Paste, window, cx);
4830 e.handle_input(") ", window, cx);
4831 });
4832 cx.assert_editor_state(
4833 &([
4834 "( one✅ ",
4835 "three ",
4836 "five ) ˇtwo one✅ four three six five ( one✅ ",
4837 "three ",
4838 "five ) ˇ",
4839 ]
4840 .join("\n")),
4841 );
4842
4843 // Cut with three selections, one of which is full-line.
4844 cx.set_state(indoc! {"
4845 1«2ˇ»3
4846 4ˇ567
4847 «8ˇ»9"});
4848 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4849 cx.assert_editor_state(indoc! {"
4850 1ˇ3
4851 ˇ9"});
4852
4853 // Paste with three selections, noticing how the copied selection that was full-line
4854 // gets inserted before the second cursor.
4855 cx.set_state(indoc! {"
4856 1ˇ3
4857 9ˇ
4858 «oˇ»ne"});
4859 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4860 cx.assert_editor_state(indoc! {"
4861 12ˇ3
4862 4567
4863 9ˇ
4864 8ˇne"});
4865
4866 // Copy with a single cursor only, which writes the whole line into the clipboard.
4867 cx.set_state(indoc! {"
4868 The quick brown
4869 fox juˇmps over
4870 the lazy dog"});
4871 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4872 assert_eq!(
4873 cx.read_from_clipboard()
4874 .and_then(|item| item.text().as_deref().map(str::to_string)),
4875 Some("fox jumps over\n".to_string())
4876 );
4877
4878 // Paste with three selections, noticing how the copied full-line selection is inserted
4879 // before the empty selections but replaces the selection that is non-empty.
4880 cx.set_state(indoc! {"
4881 Tˇhe quick brown
4882 «foˇ»x jumps over
4883 tˇhe lazy dog"});
4884 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4885 cx.assert_editor_state(indoc! {"
4886 fox jumps over
4887 Tˇhe quick brown
4888 fox jumps over
4889 ˇx jumps over
4890 fox jumps over
4891 tˇhe lazy dog"});
4892}
4893
4894#[gpui::test]
4895async fn test_copy_trim(cx: &mut TestAppContext) {
4896 init_test(cx, |_| {});
4897
4898 let mut cx = EditorTestContext::new(cx).await;
4899 cx.set_state(
4900 r#" «for selection in selections.iter() {
4901 let mut start = selection.start;
4902 let mut end = selection.end;
4903 let is_entire_line = selection.is_empty();
4904 if is_entire_line {
4905 start = Point::new(start.row, 0);ˇ»
4906 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4907 }
4908 "#,
4909 );
4910 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4911 assert_eq!(
4912 cx.read_from_clipboard()
4913 .and_then(|item| item.text().as_deref().map(str::to_string)),
4914 Some(
4915 "for selection in selections.iter() {
4916 let mut start = selection.start;
4917 let mut end = selection.end;
4918 let is_entire_line = selection.is_empty();
4919 if is_entire_line {
4920 start = Point::new(start.row, 0);"
4921 .to_string()
4922 ),
4923 "Regular copying preserves all indentation selected",
4924 );
4925 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4926 assert_eq!(
4927 cx.read_from_clipboard()
4928 .and_then(|item| item.text().as_deref().map(str::to_string)),
4929 Some(
4930 "for selection in selections.iter() {
4931let mut start = selection.start;
4932let mut end = selection.end;
4933let is_entire_line = selection.is_empty();
4934if is_entire_line {
4935 start = Point::new(start.row, 0);"
4936 .to_string()
4937 ),
4938 "Copying with stripping should strip all leading whitespaces"
4939 );
4940
4941 cx.set_state(
4942 r#" « for selection in selections.iter() {
4943 let mut start = selection.start;
4944 let mut end = selection.end;
4945 let is_entire_line = selection.is_empty();
4946 if is_entire_line {
4947 start = Point::new(start.row, 0);ˇ»
4948 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4949 }
4950 "#,
4951 );
4952 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4953 assert_eq!(
4954 cx.read_from_clipboard()
4955 .and_then(|item| item.text().as_deref().map(str::to_string)),
4956 Some(
4957 " for selection in selections.iter() {
4958 let mut start = selection.start;
4959 let mut end = selection.end;
4960 let is_entire_line = selection.is_empty();
4961 if is_entire_line {
4962 start = Point::new(start.row, 0);"
4963 .to_string()
4964 ),
4965 "Regular copying preserves all indentation selected",
4966 );
4967 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4968 assert_eq!(
4969 cx.read_from_clipboard()
4970 .and_then(|item| item.text().as_deref().map(str::to_string)),
4971 Some(
4972 "for selection in selections.iter() {
4973let mut start = selection.start;
4974let mut end = selection.end;
4975let is_entire_line = selection.is_empty();
4976if is_entire_line {
4977 start = Point::new(start.row, 0);"
4978 .to_string()
4979 ),
4980 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
4981 );
4982
4983 cx.set_state(
4984 r#" «ˇ for selection in selections.iter() {
4985 let mut start = selection.start;
4986 let mut end = selection.end;
4987 let is_entire_line = selection.is_empty();
4988 if is_entire_line {
4989 start = Point::new(start.row, 0);»
4990 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4991 }
4992 "#,
4993 );
4994 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4995 assert_eq!(
4996 cx.read_from_clipboard()
4997 .and_then(|item| item.text().as_deref().map(str::to_string)),
4998 Some(
4999 " for selection in selections.iter() {
5000 let mut start = selection.start;
5001 let mut end = selection.end;
5002 let is_entire_line = selection.is_empty();
5003 if is_entire_line {
5004 start = Point::new(start.row, 0);"
5005 .to_string()
5006 ),
5007 "Regular copying for reverse selection works the same",
5008 );
5009 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5010 assert_eq!(
5011 cx.read_from_clipboard()
5012 .and_then(|item| item.text().as_deref().map(str::to_string)),
5013 Some(
5014 "for selection in selections.iter() {
5015let mut start = selection.start;
5016let mut end = selection.end;
5017let is_entire_line = selection.is_empty();
5018if is_entire_line {
5019 start = Point::new(start.row, 0);"
5020 .to_string()
5021 ),
5022 "Copying with stripping for reverse selection works the same"
5023 );
5024
5025 cx.set_state(
5026 r#" for selection «in selections.iter() {
5027 let mut start = selection.start;
5028 let mut end = selection.end;
5029 let is_entire_line = selection.is_empty();
5030 if is_entire_line {
5031 start = Point::new(start.row, 0);ˇ»
5032 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5033 }
5034 "#,
5035 );
5036 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5037 assert_eq!(
5038 cx.read_from_clipboard()
5039 .and_then(|item| item.text().as_deref().map(str::to_string)),
5040 Some(
5041 "in selections.iter() {
5042 let mut start = selection.start;
5043 let mut end = selection.end;
5044 let is_entire_line = selection.is_empty();
5045 if is_entire_line {
5046 start = Point::new(start.row, 0);"
5047 .to_string()
5048 ),
5049 "When selecting past the indent, the copying works as usual",
5050 );
5051 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5052 assert_eq!(
5053 cx.read_from_clipboard()
5054 .and_then(|item| item.text().as_deref().map(str::to_string)),
5055 Some(
5056 "in selections.iter() {
5057 let mut start = selection.start;
5058 let mut end = selection.end;
5059 let is_entire_line = selection.is_empty();
5060 if is_entire_line {
5061 start = Point::new(start.row, 0);"
5062 .to_string()
5063 ),
5064 "When selecting past the indent, nothing is trimmed"
5065 );
5066}
5067
5068#[gpui::test]
5069async fn test_paste_multiline(cx: &mut TestAppContext) {
5070 init_test(cx, |_| {});
5071
5072 let mut cx = EditorTestContext::new(cx).await;
5073 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5074
5075 // Cut an indented block, without the leading whitespace.
5076 cx.set_state(indoc! {"
5077 const a: B = (
5078 c(),
5079 «d(
5080 e,
5081 f
5082 )ˇ»
5083 );
5084 "});
5085 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5086 cx.assert_editor_state(indoc! {"
5087 const a: B = (
5088 c(),
5089 ˇ
5090 );
5091 "});
5092
5093 // Paste it at the same position.
5094 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5095 cx.assert_editor_state(indoc! {"
5096 const a: B = (
5097 c(),
5098 d(
5099 e,
5100 f
5101 )ˇ
5102 );
5103 "});
5104
5105 // Paste it at a line with a lower indent level.
5106 cx.set_state(indoc! {"
5107 ˇ
5108 const a: B = (
5109 c(),
5110 );
5111 "});
5112 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5113 cx.assert_editor_state(indoc! {"
5114 d(
5115 e,
5116 f
5117 )ˇ
5118 const a: B = (
5119 c(),
5120 );
5121 "});
5122
5123 // Cut an indented block, with the leading whitespace.
5124 cx.set_state(indoc! {"
5125 const a: B = (
5126 c(),
5127 « d(
5128 e,
5129 f
5130 )
5131 ˇ»);
5132 "});
5133 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5134 cx.assert_editor_state(indoc! {"
5135 const a: B = (
5136 c(),
5137 ˇ);
5138 "});
5139
5140 // Paste it at the same position.
5141 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5142 cx.assert_editor_state(indoc! {"
5143 const a: B = (
5144 c(),
5145 d(
5146 e,
5147 f
5148 )
5149 ˇ);
5150 "});
5151
5152 // Paste it at a line with a higher indent level.
5153 cx.set_state(indoc! {"
5154 const a: B = (
5155 c(),
5156 d(
5157 e,
5158 fˇ
5159 )
5160 );
5161 "});
5162 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5163 cx.assert_editor_state(indoc! {"
5164 const a: B = (
5165 c(),
5166 d(
5167 e,
5168 f d(
5169 e,
5170 f
5171 )
5172 ˇ
5173 )
5174 );
5175 "});
5176
5177 // Copy an indented block, starting mid-line
5178 cx.set_state(indoc! {"
5179 const a: B = (
5180 c(),
5181 somethin«g(
5182 e,
5183 f
5184 )ˇ»
5185 );
5186 "});
5187 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5188
5189 // Paste it on a line with a lower indent level
5190 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5191 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5192 cx.assert_editor_state(indoc! {"
5193 const a: B = (
5194 c(),
5195 something(
5196 e,
5197 f
5198 )
5199 );
5200 g(
5201 e,
5202 f
5203 )ˇ"});
5204}
5205
5206#[gpui::test]
5207async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5208 init_test(cx, |_| {});
5209
5210 cx.write_to_clipboard(ClipboardItem::new_string(
5211 " d(\n e\n );\n".into(),
5212 ));
5213
5214 let mut cx = EditorTestContext::new(cx).await;
5215 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5216
5217 cx.set_state(indoc! {"
5218 fn a() {
5219 b();
5220 if c() {
5221 ˇ
5222 }
5223 }
5224 "});
5225
5226 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5227 cx.assert_editor_state(indoc! {"
5228 fn a() {
5229 b();
5230 if c() {
5231 d(
5232 e
5233 );
5234 ˇ
5235 }
5236 }
5237 "});
5238
5239 cx.set_state(indoc! {"
5240 fn a() {
5241 b();
5242 ˇ
5243 }
5244 "});
5245
5246 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5247 cx.assert_editor_state(indoc! {"
5248 fn a() {
5249 b();
5250 d(
5251 e
5252 );
5253 ˇ
5254 }
5255 "});
5256}
5257
5258#[gpui::test]
5259fn test_select_all(cx: &mut TestAppContext) {
5260 init_test(cx, |_| {});
5261
5262 let editor = cx.add_window(|window, cx| {
5263 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5264 build_editor(buffer, window, cx)
5265 });
5266 _ = editor.update(cx, |editor, window, cx| {
5267 editor.select_all(&SelectAll, window, cx);
5268 assert_eq!(
5269 editor.selections.display_ranges(cx),
5270 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5271 );
5272 });
5273}
5274
5275#[gpui::test]
5276fn test_select_line(cx: &mut TestAppContext) {
5277 init_test(cx, |_| {});
5278
5279 let editor = cx.add_window(|window, cx| {
5280 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5281 build_editor(buffer, window, cx)
5282 });
5283 _ = editor.update(cx, |editor, window, cx| {
5284 editor.change_selections(None, window, cx, |s| {
5285 s.select_display_ranges([
5286 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5287 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5288 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5289 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5290 ])
5291 });
5292 editor.select_line(&SelectLine, window, cx);
5293 assert_eq!(
5294 editor.selections.display_ranges(cx),
5295 vec![
5296 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5297 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5298 ]
5299 );
5300 });
5301
5302 _ = editor.update(cx, |editor, window, cx| {
5303 editor.select_line(&SelectLine, window, cx);
5304 assert_eq!(
5305 editor.selections.display_ranges(cx),
5306 vec![
5307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5308 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5309 ]
5310 );
5311 });
5312
5313 _ = editor.update(cx, |editor, window, cx| {
5314 editor.select_line(&SelectLine, window, cx);
5315 assert_eq!(
5316 editor.selections.display_ranges(cx),
5317 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5318 );
5319 });
5320}
5321
5322#[gpui::test]
5323async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5324 init_test(cx, |_| {});
5325 let mut cx = EditorTestContext::new(cx).await;
5326
5327 #[track_caller]
5328 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5329 cx.set_state(initial_state);
5330 cx.update_editor(|e, window, cx| {
5331 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5332 });
5333 cx.assert_editor_state(expected_state);
5334 }
5335
5336 // Selection starts and ends at the middle of lines, left-to-right
5337 test(
5338 &mut cx,
5339 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5340 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5341 );
5342 // Same thing, right-to-left
5343 test(
5344 &mut cx,
5345 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5346 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5347 );
5348
5349 // Whole buffer, left-to-right, last line *doesn't* end with newline
5350 test(
5351 &mut cx,
5352 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5353 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5354 );
5355 // Same thing, right-to-left
5356 test(
5357 &mut cx,
5358 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5359 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5360 );
5361
5362 // Whole buffer, left-to-right, last line ends with newline
5363 test(
5364 &mut cx,
5365 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5366 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5367 );
5368 // Same thing, right-to-left
5369 test(
5370 &mut cx,
5371 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5372 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5373 );
5374
5375 // Starts at the end of a line, ends at the start of another
5376 test(
5377 &mut cx,
5378 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5379 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5380 );
5381}
5382
5383#[gpui::test]
5384async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5385 init_test(cx, |_| {});
5386
5387 let editor = cx.add_window(|window, cx| {
5388 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5389 build_editor(buffer, window, cx)
5390 });
5391
5392 // setup
5393 _ = editor.update(cx, |editor, window, cx| {
5394 editor.fold_creases(
5395 vec![
5396 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5397 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5398 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5399 ],
5400 true,
5401 window,
5402 cx,
5403 );
5404 assert_eq!(
5405 editor.display_text(cx),
5406 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5407 );
5408 });
5409
5410 _ = editor.update(cx, |editor, window, cx| {
5411 editor.change_selections(None, window, cx, |s| {
5412 s.select_display_ranges([
5413 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5414 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5415 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5416 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5417 ])
5418 });
5419 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5420 assert_eq!(
5421 editor.display_text(cx),
5422 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5423 );
5424 });
5425 EditorTestContext::for_editor(editor, cx)
5426 .await
5427 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5428
5429 _ = editor.update(cx, |editor, window, cx| {
5430 editor.change_selections(None, window, cx, |s| {
5431 s.select_display_ranges([
5432 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5433 ])
5434 });
5435 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5436 assert_eq!(
5437 editor.display_text(cx),
5438 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5439 );
5440 assert_eq!(
5441 editor.selections.display_ranges(cx),
5442 [
5443 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5444 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5445 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5446 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5447 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5448 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5449 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5450 ]
5451 );
5452 });
5453 EditorTestContext::for_editor(editor, cx)
5454 .await
5455 .assert_editor_state(
5456 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5457 );
5458}
5459
5460#[gpui::test]
5461async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5462 init_test(cx, |_| {});
5463
5464 let mut cx = EditorTestContext::new(cx).await;
5465
5466 cx.set_state(indoc!(
5467 r#"abc
5468 defˇghi
5469
5470 jk
5471 nlmo
5472 "#
5473 ));
5474
5475 cx.update_editor(|editor, window, cx| {
5476 editor.add_selection_above(&Default::default(), window, cx);
5477 });
5478
5479 cx.assert_editor_state(indoc!(
5480 r#"abcˇ
5481 defˇghi
5482
5483 jk
5484 nlmo
5485 "#
5486 ));
5487
5488 cx.update_editor(|editor, window, cx| {
5489 editor.add_selection_above(&Default::default(), window, cx);
5490 });
5491
5492 cx.assert_editor_state(indoc!(
5493 r#"abcˇ
5494 defˇghi
5495
5496 jk
5497 nlmo
5498 "#
5499 ));
5500
5501 cx.update_editor(|editor, window, cx| {
5502 editor.add_selection_below(&Default::default(), window, cx);
5503 });
5504
5505 cx.assert_editor_state(indoc!(
5506 r#"abc
5507 defˇghi
5508
5509 jk
5510 nlmo
5511 "#
5512 ));
5513
5514 cx.update_editor(|editor, window, cx| {
5515 editor.undo_selection(&Default::default(), window, cx);
5516 });
5517
5518 cx.assert_editor_state(indoc!(
5519 r#"abcˇ
5520 defˇghi
5521
5522 jk
5523 nlmo
5524 "#
5525 ));
5526
5527 cx.update_editor(|editor, window, cx| {
5528 editor.redo_selection(&Default::default(), window, cx);
5529 });
5530
5531 cx.assert_editor_state(indoc!(
5532 r#"abc
5533 defˇghi
5534
5535 jk
5536 nlmo
5537 "#
5538 ));
5539
5540 cx.update_editor(|editor, window, cx| {
5541 editor.add_selection_below(&Default::default(), window, cx);
5542 });
5543
5544 cx.assert_editor_state(indoc!(
5545 r#"abc
5546 defˇghi
5547
5548 jk
5549 nlmˇo
5550 "#
5551 ));
5552
5553 cx.update_editor(|editor, window, cx| {
5554 editor.add_selection_below(&Default::default(), window, cx);
5555 });
5556
5557 cx.assert_editor_state(indoc!(
5558 r#"abc
5559 defˇghi
5560
5561 jk
5562 nlmˇo
5563 "#
5564 ));
5565
5566 // change selections
5567 cx.set_state(indoc!(
5568 r#"abc
5569 def«ˇg»hi
5570
5571 jk
5572 nlmo
5573 "#
5574 ));
5575
5576 cx.update_editor(|editor, window, cx| {
5577 editor.add_selection_below(&Default::default(), window, cx);
5578 });
5579
5580 cx.assert_editor_state(indoc!(
5581 r#"abc
5582 def«ˇg»hi
5583
5584 jk
5585 nlm«ˇo»
5586 "#
5587 ));
5588
5589 cx.update_editor(|editor, window, cx| {
5590 editor.add_selection_below(&Default::default(), window, cx);
5591 });
5592
5593 cx.assert_editor_state(indoc!(
5594 r#"abc
5595 def«ˇg»hi
5596
5597 jk
5598 nlm«ˇo»
5599 "#
5600 ));
5601
5602 cx.update_editor(|editor, window, cx| {
5603 editor.add_selection_above(&Default::default(), window, cx);
5604 });
5605
5606 cx.assert_editor_state(indoc!(
5607 r#"abc
5608 def«ˇg»hi
5609
5610 jk
5611 nlmo
5612 "#
5613 ));
5614
5615 cx.update_editor(|editor, window, cx| {
5616 editor.add_selection_above(&Default::default(), window, cx);
5617 });
5618
5619 cx.assert_editor_state(indoc!(
5620 r#"abc
5621 def«ˇg»hi
5622
5623 jk
5624 nlmo
5625 "#
5626 ));
5627
5628 // Change selections again
5629 cx.set_state(indoc!(
5630 r#"a«bc
5631 defgˇ»hi
5632
5633 jk
5634 nlmo
5635 "#
5636 ));
5637
5638 cx.update_editor(|editor, window, cx| {
5639 editor.add_selection_below(&Default::default(), window, cx);
5640 });
5641
5642 cx.assert_editor_state(indoc!(
5643 r#"a«bcˇ»
5644 d«efgˇ»hi
5645
5646 j«kˇ»
5647 nlmo
5648 "#
5649 ));
5650
5651 cx.update_editor(|editor, window, cx| {
5652 editor.add_selection_below(&Default::default(), window, cx);
5653 });
5654 cx.assert_editor_state(indoc!(
5655 r#"a«bcˇ»
5656 d«efgˇ»hi
5657
5658 j«kˇ»
5659 n«lmoˇ»
5660 "#
5661 ));
5662 cx.update_editor(|editor, window, cx| {
5663 editor.add_selection_above(&Default::default(), window, cx);
5664 });
5665
5666 cx.assert_editor_state(indoc!(
5667 r#"a«bcˇ»
5668 d«efgˇ»hi
5669
5670 j«kˇ»
5671 nlmo
5672 "#
5673 ));
5674
5675 // Change selections again
5676 cx.set_state(indoc!(
5677 r#"abc
5678 d«ˇefghi
5679
5680 jk
5681 nlm»o
5682 "#
5683 ));
5684
5685 cx.update_editor(|editor, window, cx| {
5686 editor.add_selection_above(&Default::default(), window, cx);
5687 });
5688
5689 cx.assert_editor_state(indoc!(
5690 r#"a«ˇbc»
5691 d«ˇef»ghi
5692
5693 j«ˇk»
5694 n«ˇlm»o
5695 "#
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.add_selection_below(&Default::default(), window, cx);
5700 });
5701
5702 cx.assert_editor_state(indoc!(
5703 r#"abc
5704 d«ˇef»ghi
5705
5706 j«ˇk»
5707 n«ˇlm»o
5708 "#
5709 ));
5710}
5711
5712#[gpui::test]
5713async fn test_select_next(cx: &mut TestAppContext) {
5714 init_test(cx, |_| {});
5715
5716 let mut cx = EditorTestContext::new(cx).await;
5717 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5718
5719 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5720 .unwrap();
5721 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5722
5723 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5724 .unwrap();
5725 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5726
5727 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5728 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5729
5730 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5731 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5732
5733 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5734 .unwrap();
5735 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5736
5737 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5738 .unwrap();
5739 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5740}
5741
5742#[gpui::test]
5743async fn test_select_all_matches(cx: &mut TestAppContext) {
5744 init_test(cx, |_| {});
5745
5746 let mut cx = EditorTestContext::new(cx).await;
5747
5748 // Test caret-only selections
5749 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5750 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5751 .unwrap();
5752 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5753
5754 // Test left-to-right selections
5755 cx.set_state("abc\n«abcˇ»\nabc");
5756 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5757 .unwrap();
5758 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5759
5760 // Test right-to-left selections
5761 cx.set_state("abc\n«ˇabc»\nabc");
5762 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5763 .unwrap();
5764 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5765
5766 // Test selecting whitespace with caret selection
5767 cx.set_state("abc\nˇ abc\nabc");
5768 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5769 .unwrap();
5770 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5771
5772 // Test selecting whitespace with left-to-right selection
5773 cx.set_state("abc\n«ˇ »abc\nabc");
5774 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5775 .unwrap();
5776 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5777
5778 // Test no matches with right-to-left selection
5779 cx.set_state("abc\n« ˇ»abc\nabc");
5780 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5781 .unwrap();
5782 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5783}
5784
5785#[gpui::test]
5786async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5787 init_test(cx, |_| {});
5788
5789 let mut cx = EditorTestContext::new(cx).await;
5790 cx.set_state(
5791 r#"let foo = 2;
5792lˇet foo = 2;
5793let fooˇ = 2;
5794let foo = 2;
5795let foo = ˇ2;"#,
5796 );
5797
5798 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5799 .unwrap();
5800 cx.assert_editor_state(
5801 r#"let foo = 2;
5802«letˇ» foo = 2;
5803let «fooˇ» = 2;
5804let foo = 2;
5805let foo = «2ˇ»;"#,
5806 );
5807
5808 // noop for multiple selections with different contents
5809 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5810 .unwrap();
5811 cx.assert_editor_state(
5812 r#"let foo = 2;
5813«letˇ» foo = 2;
5814let «fooˇ» = 2;
5815let foo = 2;
5816let foo = «2ˇ»;"#,
5817 );
5818}
5819
5820#[gpui::test]
5821async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5822 init_test(cx, |_| {});
5823
5824 let mut cx =
5825 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5826
5827 cx.assert_editor_state(indoc! {"
5828 ˇbbb
5829 ccc
5830
5831 bbb
5832 ccc
5833 "});
5834 cx.dispatch_action(SelectPrevious::default());
5835 cx.assert_editor_state(indoc! {"
5836 «bbbˇ»
5837 ccc
5838
5839 bbb
5840 ccc
5841 "});
5842 cx.dispatch_action(SelectPrevious::default());
5843 cx.assert_editor_state(indoc! {"
5844 «bbbˇ»
5845 ccc
5846
5847 «bbbˇ»
5848 ccc
5849 "});
5850}
5851
5852#[gpui::test]
5853async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5854 init_test(cx, |_| {});
5855
5856 let mut cx = EditorTestContext::new(cx).await;
5857 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5858
5859 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5860 .unwrap();
5861 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5862
5863 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5864 .unwrap();
5865 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5866
5867 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5868 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5869
5870 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5871 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5872
5873 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5874 .unwrap();
5875 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5876
5877 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5878 .unwrap();
5879 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5880
5881 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5882 .unwrap();
5883 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5884}
5885
5886#[gpui::test]
5887async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5888 init_test(cx, |_| {});
5889
5890 let mut cx = EditorTestContext::new(cx).await;
5891 cx.set_state("aˇ");
5892
5893 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5894 .unwrap();
5895 cx.assert_editor_state("«aˇ»");
5896 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5897 .unwrap();
5898 cx.assert_editor_state("«aˇ»");
5899}
5900
5901#[gpui::test]
5902async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5903 init_test(cx, |_| {});
5904
5905 let mut cx = EditorTestContext::new(cx).await;
5906 cx.set_state(
5907 r#"let foo = 2;
5908lˇet foo = 2;
5909let fooˇ = 2;
5910let foo = 2;
5911let foo = ˇ2;"#,
5912 );
5913
5914 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5915 .unwrap();
5916 cx.assert_editor_state(
5917 r#"let foo = 2;
5918«letˇ» foo = 2;
5919let «fooˇ» = 2;
5920let foo = 2;
5921let foo = «2ˇ»;"#,
5922 );
5923
5924 // noop for multiple selections with different contents
5925 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5926 .unwrap();
5927 cx.assert_editor_state(
5928 r#"let foo = 2;
5929«letˇ» foo = 2;
5930let «fooˇ» = 2;
5931let foo = 2;
5932let foo = «2ˇ»;"#,
5933 );
5934}
5935
5936#[gpui::test]
5937async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5938 init_test(cx, |_| {});
5939
5940 let mut cx = EditorTestContext::new(cx).await;
5941 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5942
5943 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5944 .unwrap();
5945 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5946
5947 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5948 .unwrap();
5949 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5950
5951 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5952 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
5953
5954 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5955 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
5956
5957 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5958 .unwrap();
5959 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
5960
5961 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5962 .unwrap();
5963 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5964}
5965
5966#[gpui::test]
5967async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
5968 init_test(cx, |_| {});
5969
5970 let language = Arc::new(Language::new(
5971 LanguageConfig::default(),
5972 Some(tree_sitter_rust::LANGUAGE.into()),
5973 ));
5974
5975 let text = r#"
5976 use mod1::mod2::{mod3, mod4};
5977
5978 fn fn_1(param1: bool, param2: &str) {
5979 let var1 = "text";
5980 }
5981 "#
5982 .unindent();
5983
5984 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
5985 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
5986 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
5987
5988 editor
5989 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
5990 .await;
5991
5992 editor.update_in(cx, |editor, window, cx| {
5993 editor.change_selections(None, window, cx, |s| {
5994 s.select_display_ranges([
5995 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
5996 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
5997 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
5998 ]);
5999 });
6000 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6001 });
6002 editor.update(cx, |editor, cx| {
6003 assert_text_with_selections(
6004 editor,
6005 indoc! {r#"
6006 use mod1::mod2::{mod3, «mod4ˇ»};
6007
6008 fn fn_1«ˇ(param1: bool, param2: &str)» {
6009 let var1 = "«ˇtext»";
6010 }
6011 "#},
6012 cx,
6013 );
6014 });
6015
6016 editor.update_in(cx, |editor, window, cx| {
6017 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6018 });
6019 editor.update(cx, |editor, cx| {
6020 assert_text_with_selections(
6021 editor,
6022 indoc! {r#"
6023 use mod1::mod2::«{mod3, mod4}ˇ»;
6024
6025 «ˇfn fn_1(param1: bool, param2: &str) {
6026 let var1 = "text";
6027 }»
6028 "#},
6029 cx,
6030 );
6031 });
6032
6033 editor.update_in(cx, |editor, window, cx| {
6034 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6035 });
6036 assert_eq!(
6037 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6038 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6039 );
6040
6041 // Trying to expand the selected syntax node one more time has no effect.
6042 editor.update_in(cx, |editor, window, cx| {
6043 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6044 });
6045 assert_eq!(
6046 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6047 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6048 );
6049
6050 editor.update_in(cx, |editor, window, cx| {
6051 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6052 });
6053 editor.update(cx, |editor, cx| {
6054 assert_text_with_selections(
6055 editor,
6056 indoc! {r#"
6057 use mod1::mod2::«{mod3, mod4}ˇ»;
6058
6059 «ˇfn fn_1(param1: bool, param2: &str) {
6060 let var1 = "text";
6061 }»
6062 "#},
6063 cx,
6064 );
6065 });
6066
6067 editor.update_in(cx, |editor, window, cx| {
6068 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6069 });
6070 editor.update(cx, |editor, cx| {
6071 assert_text_with_selections(
6072 editor,
6073 indoc! {r#"
6074 use mod1::mod2::{mod3, «mod4ˇ»};
6075
6076 fn fn_1«ˇ(param1: bool, param2: &str)» {
6077 let var1 = "«ˇtext»";
6078 }
6079 "#},
6080 cx,
6081 );
6082 });
6083
6084 editor.update_in(cx, |editor, window, cx| {
6085 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6086 });
6087 editor.update(cx, |editor, cx| {
6088 assert_text_with_selections(
6089 editor,
6090 indoc! {r#"
6091 use mod1::mod2::{mod3, mo«ˇ»d4};
6092
6093 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6094 let var1 = "te«ˇ»xt";
6095 }
6096 "#},
6097 cx,
6098 );
6099 });
6100
6101 // Trying to shrink the selected syntax node one more time has no effect.
6102 editor.update_in(cx, |editor, window, cx| {
6103 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6104 });
6105 editor.update_in(cx, |editor, _, cx| {
6106 assert_text_with_selections(
6107 editor,
6108 indoc! {r#"
6109 use mod1::mod2::{mod3, mo«ˇ»d4};
6110
6111 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6112 let var1 = "te«ˇ»xt";
6113 }
6114 "#},
6115 cx,
6116 );
6117 });
6118
6119 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6120 // a fold.
6121 editor.update_in(cx, |editor, window, cx| {
6122 editor.fold_creases(
6123 vec![
6124 Crease::simple(
6125 Point::new(0, 21)..Point::new(0, 24),
6126 FoldPlaceholder::test(),
6127 ),
6128 Crease::simple(
6129 Point::new(3, 20)..Point::new(3, 22),
6130 FoldPlaceholder::test(),
6131 ),
6132 ],
6133 true,
6134 window,
6135 cx,
6136 );
6137 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6138 });
6139 editor.update(cx, |editor, cx| {
6140 assert_text_with_selections(
6141 editor,
6142 indoc! {r#"
6143 use mod1::mod2::«{mod3, mod4}ˇ»;
6144
6145 fn fn_1«ˇ(param1: bool, param2: &str)» {
6146 «ˇlet var1 = "text";»
6147 }
6148 "#},
6149 cx,
6150 );
6151 });
6152}
6153
6154#[gpui::test]
6155async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6156 init_test(cx, |_| {});
6157
6158 let base_text = r#"
6159 impl A {
6160 // this is an uncommitted comment
6161
6162 fn b() {
6163 c();
6164 }
6165
6166 // this is another uncommitted comment
6167
6168 fn d() {
6169 // e
6170 // f
6171 }
6172 }
6173
6174 fn g() {
6175 // h
6176 }
6177 "#
6178 .unindent();
6179
6180 let text = r#"
6181 ˇimpl A {
6182
6183 fn b() {
6184 c();
6185 }
6186
6187 fn d() {
6188 // e
6189 // f
6190 }
6191 }
6192
6193 fn g() {
6194 // h
6195 }
6196 "#
6197 .unindent();
6198
6199 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6200 cx.set_state(&text);
6201 cx.set_head_text(&base_text);
6202 cx.update_editor(|editor, window, cx| {
6203 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6204 });
6205
6206 cx.assert_state_with_diff(
6207 "
6208 ˇimpl A {
6209 - // this is an uncommitted comment
6210
6211 fn b() {
6212 c();
6213 }
6214
6215 - // this is another uncommitted comment
6216 -
6217 fn d() {
6218 // e
6219 // f
6220 }
6221 }
6222
6223 fn g() {
6224 // h
6225 }
6226 "
6227 .unindent(),
6228 );
6229
6230 let expected_display_text = "
6231 impl A {
6232 // this is an uncommitted comment
6233
6234 fn b() {
6235 ⋯
6236 }
6237
6238 // this is another uncommitted comment
6239
6240 fn d() {
6241 ⋯
6242 }
6243 }
6244
6245 fn g() {
6246 ⋯
6247 }
6248 "
6249 .unindent();
6250
6251 cx.update_editor(|editor, window, cx| {
6252 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6253 assert_eq!(editor.display_text(cx), expected_display_text);
6254 });
6255}
6256
6257#[gpui::test]
6258async fn test_autoindent(cx: &mut TestAppContext) {
6259 init_test(cx, |_| {});
6260
6261 let language = Arc::new(
6262 Language::new(
6263 LanguageConfig {
6264 brackets: BracketPairConfig {
6265 pairs: vec![
6266 BracketPair {
6267 start: "{".to_string(),
6268 end: "}".to_string(),
6269 close: false,
6270 surround: false,
6271 newline: true,
6272 },
6273 BracketPair {
6274 start: "(".to_string(),
6275 end: ")".to_string(),
6276 close: false,
6277 surround: false,
6278 newline: true,
6279 },
6280 ],
6281 ..Default::default()
6282 },
6283 ..Default::default()
6284 },
6285 Some(tree_sitter_rust::LANGUAGE.into()),
6286 )
6287 .with_indents_query(
6288 r#"
6289 (_ "(" ")" @end) @indent
6290 (_ "{" "}" @end) @indent
6291 "#,
6292 )
6293 .unwrap(),
6294 );
6295
6296 let text = "fn a() {}";
6297
6298 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6299 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6300 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6301 editor
6302 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6303 .await;
6304
6305 editor.update_in(cx, |editor, window, cx| {
6306 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6307 editor.newline(&Newline, window, cx);
6308 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6309 assert_eq!(
6310 editor.selections.ranges(cx),
6311 &[
6312 Point::new(1, 4)..Point::new(1, 4),
6313 Point::new(3, 4)..Point::new(3, 4),
6314 Point::new(5, 0)..Point::new(5, 0)
6315 ]
6316 );
6317 });
6318}
6319
6320#[gpui::test]
6321async fn test_autoindent_selections(cx: &mut TestAppContext) {
6322 init_test(cx, |_| {});
6323
6324 {
6325 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6326 cx.set_state(indoc! {"
6327 impl A {
6328
6329 fn b() {}
6330
6331 «fn c() {
6332
6333 }ˇ»
6334 }
6335 "});
6336
6337 cx.update_editor(|editor, window, cx| {
6338 editor.autoindent(&Default::default(), window, cx);
6339 });
6340
6341 cx.assert_editor_state(indoc! {"
6342 impl A {
6343
6344 fn b() {}
6345
6346 «fn c() {
6347
6348 }ˇ»
6349 }
6350 "});
6351 }
6352
6353 {
6354 let mut cx = EditorTestContext::new_multibuffer(
6355 cx,
6356 [indoc! { "
6357 impl A {
6358 «
6359 // a
6360 fn b(){}
6361 »
6362 «
6363 }
6364 fn c(){}
6365 »
6366 "}],
6367 );
6368
6369 let buffer = cx.update_editor(|editor, _, cx| {
6370 let buffer = editor.buffer().update(cx, |buffer, _| {
6371 buffer.all_buffers().iter().next().unwrap().clone()
6372 });
6373 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6374 buffer
6375 });
6376
6377 cx.run_until_parked();
6378 cx.update_editor(|editor, window, cx| {
6379 editor.select_all(&Default::default(), window, cx);
6380 editor.autoindent(&Default::default(), window, cx)
6381 });
6382 cx.run_until_parked();
6383
6384 cx.update(|_, cx| {
6385 assert_eq!(
6386 buffer.read(cx).text(),
6387 indoc! { "
6388 impl A {
6389
6390 // a
6391 fn b(){}
6392
6393
6394 }
6395 fn c(){}
6396
6397 " }
6398 )
6399 });
6400 }
6401}
6402
6403#[gpui::test]
6404async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6405 init_test(cx, |_| {});
6406
6407 let mut cx = EditorTestContext::new(cx).await;
6408
6409 let language = Arc::new(Language::new(
6410 LanguageConfig {
6411 brackets: BracketPairConfig {
6412 pairs: vec![
6413 BracketPair {
6414 start: "{".to_string(),
6415 end: "}".to_string(),
6416 close: true,
6417 surround: true,
6418 newline: true,
6419 },
6420 BracketPair {
6421 start: "(".to_string(),
6422 end: ")".to_string(),
6423 close: true,
6424 surround: true,
6425 newline: true,
6426 },
6427 BracketPair {
6428 start: "/*".to_string(),
6429 end: " */".to_string(),
6430 close: true,
6431 surround: true,
6432 newline: true,
6433 },
6434 BracketPair {
6435 start: "[".to_string(),
6436 end: "]".to_string(),
6437 close: false,
6438 surround: false,
6439 newline: true,
6440 },
6441 BracketPair {
6442 start: "\"".to_string(),
6443 end: "\"".to_string(),
6444 close: true,
6445 surround: true,
6446 newline: false,
6447 },
6448 BracketPair {
6449 start: "<".to_string(),
6450 end: ">".to_string(),
6451 close: false,
6452 surround: true,
6453 newline: true,
6454 },
6455 ],
6456 ..Default::default()
6457 },
6458 autoclose_before: "})]".to_string(),
6459 ..Default::default()
6460 },
6461 Some(tree_sitter_rust::LANGUAGE.into()),
6462 ));
6463
6464 cx.language_registry().add(language.clone());
6465 cx.update_buffer(|buffer, cx| {
6466 buffer.set_language(Some(language), cx);
6467 });
6468
6469 cx.set_state(
6470 &r#"
6471 🏀ˇ
6472 εˇ
6473 ❤️ˇ
6474 "#
6475 .unindent(),
6476 );
6477
6478 // autoclose multiple nested brackets at multiple cursors
6479 cx.update_editor(|editor, window, cx| {
6480 editor.handle_input("{", window, cx);
6481 editor.handle_input("{", window, cx);
6482 editor.handle_input("{", window, cx);
6483 });
6484 cx.assert_editor_state(
6485 &"
6486 🏀{{{ˇ}}}
6487 ε{{{ˇ}}}
6488 ❤️{{{ˇ}}}
6489 "
6490 .unindent(),
6491 );
6492
6493 // insert a different closing bracket
6494 cx.update_editor(|editor, window, cx| {
6495 editor.handle_input(")", window, cx);
6496 });
6497 cx.assert_editor_state(
6498 &"
6499 🏀{{{)ˇ}}}
6500 ε{{{)ˇ}}}
6501 ❤️{{{)ˇ}}}
6502 "
6503 .unindent(),
6504 );
6505
6506 // skip over the auto-closed brackets when typing a closing bracket
6507 cx.update_editor(|editor, window, cx| {
6508 editor.move_right(&MoveRight, window, cx);
6509 editor.handle_input("}", window, cx);
6510 editor.handle_input("}", window, cx);
6511 editor.handle_input("}", window, cx);
6512 });
6513 cx.assert_editor_state(
6514 &"
6515 🏀{{{)}}}}ˇ
6516 ε{{{)}}}}ˇ
6517 ❤️{{{)}}}}ˇ
6518 "
6519 .unindent(),
6520 );
6521
6522 // autoclose multi-character pairs
6523 cx.set_state(
6524 &"
6525 ˇ
6526 ˇ
6527 "
6528 .unindent(),
6529 );
6530 cx.update_editor(|editor, window, cx| {
6531 editor.handle_input("/", window, cx);
6532 editor.handle_input("*", window, cx);
6533 });
6534 cx.assert_editor_state(
6535 &"
6536 /*ˇ */
6537 /*ˇ */
6538 "
6539 .unindent(),
6540 );
6541
6542 // one cursor autocloses a multi-character pair, one cursor
6543 // does not autoclose.
6544 cx.set_state(
6545 &"
6546 /ˇ
6547 ˇ
6548 "
6549 .unindent(),
6550 );
6551 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6552 cx.assert_editor_state(
6553 &"
6554 /*ˇ */
6555 *ˇ
6556 "
6557 .unindent(),
6558 );
6559
6560 // Don't autoclose if the next character isn't whitespace and isn't
6561 // listed in the language's "autoclose_before" section.
6562 cx.set_state("ˇa b");
6563 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6564 cx.assert_editor_state("{ˇa b");
6565
6566 // Don't autoclose if `close` is false for the bracket pair
6567 cx.set_state("ˇ");
6568 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6569 cx.assert_editor_state("[ˇ");
6570
6571 // Surround with brackets if text is selected
6572 cx.set_state("«aˇ» b");
6573 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6574 cx.assert_editor_state("{«aˇ»} b");
6575
6576 // Autoclose when not immediately after a word character
6577 cx.set_state("a ˇ");
6578 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6579 cx.assert_editor_state("a \"ˇ\"");
6580
6581 // Autoclose pair where the start and end characters are the same
6582 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6583 cx.assert_editor_state("a \"\"ˇ");
6584
6585 // Don't autoclose when immediately after a word character
6586 cx.set_state("aˇ");
6587 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6588 cx.assert_editor_state("a\"ˇ");
6589
6590 // Do autoclose when after a non-word character
6591 cx.set_state("{ˇ");
6592 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6593 cx.assert_editor_state("{\"ˇ\"");
6594
6595 // Non identical pairs autoclose regardless of preceding character
6596 cx.set_state("aˇ");
6597 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6598 cx.assert_editor_state("a{ˇ}");
6599
6600 // Don't autoclose pair if autoclose is disabled
6601 cx.set_state("ˇ");
6602 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6603 cx.assert_editor_state("<ˇ");
6604
6605 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6606 cx.set_state("«aˇ» b");
6607 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6608 cx.assert_editor_state("<«aˇ»> b");
6609}
6610
6611#[gpui::test]
6612async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6613 init_test(cx, |settings| {
6614 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6615 });
6616
6617 let mut cx = EditorTestContext::new(cx).await;
6618
6619 let language = Arc::new(Language::new(
6620 LanguageConfig {
6621 brackets: BracketPairConfig {
6622 pairs: vec![
6623 BracketPair {
6624 start: "{".to_string(),
6625 end: "}".to_string(),
6626 close: true,
6627 surround: true,
6628 newline: true,
6629 },
6630 BracketPair {
6631 start: "(".to_string(),
6632 end: ")".to_string(),
6633 close: true,
6634 surround: true,
6635 newline: true,
6636 },
6637 BracketPair {
6638 start: "[".to_string(),
6639 end: "]".to_string(),
6640 close: false,
6641 surround: false,
6642 newline: true,
6643 },
6644 ],
6645 ..Default::default()
6646 },
6647 autoclose_before: "})]".to_string(),
6648 ..Default::default()
6649 },
6650 Some(tree_sitter_rust::LANGUAGE.into()),
6651 ));
6652
6653 cx.language_registry().add(language.clone());
6654 cx.update_buffer(|buffer, cx| {
6655 buffer.set_language(Some(language), cx);
6656 });
6657
6658 cx.set_state(
6659 &"
6660 ˇ
6661 ˇ
6662 ˇ
6663 "
6664 .unindent(),
6665 );
6666
6667 // ensure only matching closing brackets are skipped over
6668 cx.update_editor(|editor, window, cx| {
6669 editor.handle_input("}", window, cx);
6670 editor.move_left(&MoveLeft, window, cx);
6671 editor.handle_input(")", window, cx);
6672 editor.move_left(&MoveLeft, window, cx);
6673 });
6674 cx.assert_editor_state(
6675 &"
6676 ˇ)}
6677 ˇ)}
6678 ˇ)}
6679 "
6680 .unindent(),
6681 );
6682
6683 // skip-over closing brackets at multiple cursors
6684 cx.update_editor(|editor, window, cx| {
6685 editor.handle_input(")", window, cx);
6686 editor.handle_input("}", window, cx);
6687 });
6688 cx.assert_editor_state(
6689 &"
6690 )}ˇ
6691 )}ˇ
6692 )}ˇ
6693 "
6694 .unindent(),
6695 );
6696
6697 // ignore non-close brackets
6698 cx.update_editor(|editor, window, cx| {
6699 editor.handle_input("]", window, cx);
6700 editor.move_left(&MoveLeft, window, cx);
6701 editor.handle_input("]", window, cx);
6702 });
6703 cx.assert_editor_state(
6704 &"
6705 )}]ˇ]
6706 )}]ˇ]
6707 )}]ˇ]
6708 "
6709 .unindent(),
6710 );
6711}
6712
6713#[gpui::test]
6714async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6715 init_test(cx, |_| {});
6716
6717 let mut cx = EditorTestContext::new(cx).await;
6718
6719 let html_language = Arc::new(
6720 Language::new(
6721 LanguageConfig {
6722 name: "HTML".into(),
6723 brackets: BracketPairConfig {
6724 pairs: vec![
6725 BracketPair {
6726 start: "<".into(),
6727 end: ">".into(),
6728 close: true,
6729 ..Default::default()
6730 },
6731 BracketPair {
6732 start: "{".into(),
6733 end: "}".into(),
6734 close: true,
6735 ..Default::default()
6736 },
6737 BracketPair {
6738 start: "(".into(),
6739 end: ")".into(),
6740 close: true,
6741 ..Default::default()
6742 },
6743 ],
6744 ..Default::default()
6745 },
6746 autoclose_before: "})]>".into(),
6747 ..Default::default()
6748 },
6749 Some(tree_sitter_html::LANGUAGE.into()),
6750 )
6751 .with_injection_query(
6752 r#"
6753 (script_element
6754 (raw_text) @injection.content
6755 (#set! injection.language "javascript"))
6756 "#,
6757 )
6758 .unwrap(),
6759 );
6760
6761 let javascript_language = Arc::new(Language::new(
6762 LanguageConfig {
6763 name: "JavaScript".into(),
6764 brackets: BracketPairConfig {
6765 pairs: vec![
6766 BracketPair {
6767 start: "/*".into(),
6768 end: " */".into(),
6769 close: true,
6770 ..Default::default()
6771 },
6772 BracketPair {
6773 start: "{".into(),
6774 end: "}".into(),
6775 close: true,
6776 ..Default::default()
6777 },
6778 BracketPair {
6779 start: "(".into(),
6780 end: ")".into(),
6781 close: true,
6782 ..Default::default()
6783 },
6784 ],
6785 ..Default::default()
6786 },
6787 autoclose_before: "})]>".into(),
6788 ..Default::default()
6789 },
6790 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6791 ));
6792
6793 cx.language_registry().add(html_language.clone());
6794 cx.language_registry().add(javascript_language.clone());
6795
6796 cx.update_buffer(|buffer, cx| {
6797 buffer.set_language(Some(html_language), cx);
6798 });
6799
6800 cx.set_state(
6801 &r#"
6802 <body>ˇ
6803 <script>
6804 var x = 1;ˇ
6805 </script>
6806 </body>ˇ
6807 "#
6808 .unindent(),
6809 );
6810
6811 // Precondition: different languages are active at different locations.
6812 cx.update_editor(|editor, window, cx| {
6813 let snapshot = editor.snapshot(window, cx);
6814 let cursors = editor.selections.ranges::<usize>(cx);
6815 let languages = cursors
6816 .iter()
6817 .map(|c| snapshot.language_at(c.start).unwrap().name())
6818 .collect::<Vec<_>>();
6819 assert_eq!(
6820 languages,
6821 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6822 );
6823 });
6824
6825 // Angle brackets autoclose in HTML, but not JavaScript.
6826 cx.update_editor(|editor, window, cx| {
6827 editor.handle_input("<", window, cx);
6828 editor.handle_input("a", window, cx);
6829 });
6830 cx.assert_editor_state(
6831 &r#"
6832 <body><aˇ>
6833 <script>
6834 var x = 1;<aˇ
6835 </script>
6836 </body><aˇ>
6837 "#
6838 .unindent(),
6839 );
6840
6841 // Curly braces and parens autoclose in both HTML and JavaScript.
6842 cx.update_editor(|editor, window, cx| {
6843 editor.handle_input(" b=", window, cx);
6844 editor.handle_input("{", window, cx);
6845 editor.handle_input("c", window, cx);
6846 editor.handle_input("(", window, cx);
6847 });
6848 cx.assert_editor_state(
6849 &r#"
6850 <body><a b={c(ˇ)}>
6851 <script>
6852 var x = 1;<a b={c(ˇ)}
6853 </script>
6854 </body><a b={c(ˇ)}>
6855 "#
6856 .unindent(),
6857 );
6858
6859 // Brackets that were already autoclosed are skipped.
6860 cx.update_editor(|editor, window, cx| {
6861 editor.handle_input(")", window, cx);
6862 editor.handle_input("d", window, cx);
6863 editor.handle_input("}", window, cx);
6864 });
6865 cx.assert_editor_state(
6866 &r#"
6867 <body><a b={c()d}ˇ>
6868 <script>
6869 var x = 1;<a b={c()d}ˇ
6870 </script>
6871 </body><a b={c()d}ˇ>
6872 "#
6873 .unindent(),
6874 );
6875 cx.update_editor(|editor, window, cx| {
6876 editor.handle_input(">", window, cx);
6877 });
6878 cx.assert_editor_state(
6879 &r#"
6880 <body><a b={c()d}>ˇ
6881 <script>
6882 var x = 1;<a b={c()d}>ˇ
6883 </script>
6884 </body><a b={c()d}>ˇ
6885 "#
6886 .unindent(),
6887 );
6888
6889 // Reset
6890 cx.set_state(
6891 &r#"
6892 <body>ˇ
6893 <script>
6894 var x = 1;ˇ
6895 </script>
6896 </body>ˇ
6897 "#
6898 .unindent(),
6899 );
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.handle_input("<", window, cx);
6903 });
6904 cx.assert_editor_state(
6905 &r#"
6906 <body><ˇ>
6907 <script>
6908 var x = 1;<ˇ
6909 </script>
6910 </body><ˇ>
6911 "#
6912 .unindent(),
6913 );
6914
6915 // When backspacing, the closing angle brackets are removed.
6916 cx.update_editor(|editor, window, cx| {
6917 editor.backspace(&Backspace, window, cx);
6918 });
6919 cx.assert_editor_state(
6920 &r#"
6921 <body>ˇ
6922 <script>
6923 var x = 1;ˇ
6924 </script>
6925 </body>ˇ
6926 "#
6927 .unindent(),
6928 );
6929
6930 // Block comments autoclose in JavaScript, but not HTML.
6931 cx.update_editor(|editor, window, cx| {
6932 editor.handle_input("/", window, cx);
6933 editor.handle_input("*", window, cx);
6934 });
6935 cx.assert_editor_state(
6936 &r#"
6937 <body>/*ˇ
6938 <script>
6939 var x = 1;/*ˇ */
6940 </script>
6941 </body>/*ˇ
6942 "#
6943 .unindent(),
6944 );
6945}
6946
6947#[gpui::test]
6948async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
6949 init_test(cx, |_| {});
6950
6951 let mut cx = EditorTestContext::new(cx).await;
6952
6953 let rust_language = Arc::new(
6954 Language::new(
6955 LanguageConfig {
6956 name: "Rust".into(),
6957 brackets: serde_json::from_value(json!([
6958 { "start": "{", "end": "}", "close": true, "newline": true },
6959 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
6960 ]))
6961 .unwrap(),
6962 autoclose_before: "})]>".into(),
6963 ..Default::default()
6964 },
6965 Some(tree_sitter_rust::LANGUAGE.into()),
6966 )
6967 .with_override_query("(string_literal) @string")
6968 .unwrap(),
6969 );
6970
6971 cx.language_registry().add(rust_language.clone());
6972 cx.update_buffer(|buffer, cx| {
6973 buffer.set_language(Some(rust_language), cx);
6974 });
6975
6976 cx.set_state(
6977 &r#"
6978 let x = ˇ
6979 "#
6980 .unindent(),
6981 );
6982
6983 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
6984 cx.update_editor(|editor, window, cx| {
6985 editor.handle_input("\"", window, cx);
6986 });
6987 cx.assert_editor_state(
6988 &r#"
6989 let x = "ˇ"
6990 "#
6991 .unindent(),
6992 );
6993
6994 // Inserting another quotation mark. The cursor moves across the existing
6995 // automatically-inserted quotation mark.
6996 cx.update_editor(|editor, window, cx| {
6997 editor.handle_input("\"", window, cx);
6998 });
6999 cx.assert_editor_state(
7000 &r#"
7001 let x = ""ˇ
7002 "#
7003 .unindent(),
7004 );
7005
7006 // Reset
7007 cx.set_state(
7008 &r#"
7009 let x = ˇ
7010 "#
7011 .unindent(),
7012 );
7013
7014 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7015 cx.update_editor(|editor, window, cx| {
7016 editor.handle_input("\"", window, cx);
7017 editor.handle_input(" ", window, cx);
7018 editor.move_left(&Default::default(), window, cx);
7019 editor.handle_input("\\", window, cx);
7020 editor.handle_input("\"", window, cx);
7021 });
7022 cx.assert_editor_state(
7023 &r#"
7024 let x = "\"ˇ "
7025 "#
7026 .unindent(),
7027 );
7028
7029 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7030 // mark. Nothing is inserted.
7031 cx.update_editor(|editor, window, cx| {
7032 editor.move_right(&Default::default(), window, cx);
7033 editor.handle_input("\"", window, cx);
7034 });
7035 cx.assert_editor_state(
7036 &r#"
7037 let x = "\" "ˇ
7038 "#
7039 .unindent(),
7040 );
7041}
7042
7043#[gpui::test]
7044async fn test_surround_with_pair(cx: &mut TestAppContext) {
7045 init_test(cx, |_| {});
7046
7047 let language = Arc::new(Language::new(
7048 LanguageConfig {
7049 brackets: BracketPairConfig {
7050 pairs: vec![
7051 BracketPair {
7052 start: "{".to_string(),
7053 end: "}".to_string(),
7054 close: true,
7055 surround: true,
7056 newline: true,
7057 },
7058 BracketPair {
7059 start: "/* ".to_string(),
7060 end: "*/".to_string(),
7061 close: true,
7062 surround: true,
7063 ..Default::default()
7064 },
7065 ],
7066 ..Default::default()
7067 },
7068 ..Default::default()
7069 },
7070 Some(tree_sitter_rust::LANGUAGE.into()),
7071 ));
7072
7073 let text = r#"
7074 a
7075 b
7076 c
7077 "#
7078 .unindent();
7079
7080 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7081 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7082 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7083 editor
7084 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7085 .await;
7086
7087 editor.update_in(cx, |editor, window, cx| {
7088 editor.change_selections(None, window, cx, |s| {
7089 s.select_display_ranges([
7090 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7091 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7092 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7093 ])
7094 });
7095
7096 editor.handle_input("{", window, cx);
7097 editor.handle_input("{", window, cx);
7098 editor.handle_input("{", window, cx);
7099 assert_eq!(
7100 editor.text(cx),
7101 "
7102 {{{a}}}
7103 {{{b}}}
7104 {{{c}}}
7105 "
7106 .unindent()
7107 );
7108 assert_eq!(
7109 editor.selections.display_ranges(cx),
7110 [
7111 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7112 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7113 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7114 ]
7115 );
7116
7117 editor.undo(&Undo, window, cx);
7118 editor.undo(&Undo, window, cx);
7119 editor.undo(&Undo, window, cx);
7120 assert_eq!(
7121 editor.text(cx),
7122 "
7123 a
7124 b
7125 c
7126 "
7127 .unindent()
7128 );
7129 assert_eq!(
7130 editor.selections.display_ranges(cx),
7131 [
7132 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7133 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7134 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7135 ]
7136 );
7137
7138 // Ensure inserting the first character of a multi-byte bracket pair
7139 // doesn't surround the selections with the bracket.
7140 editor.handle_input("/", window, cx);
7141 assert_eq!(
7142 editor.text(cx),
7143 "
7144 /
7145 /
7146 /
7147 "
7148 .unindent()
7149 );
7150 assert_eq!(
7151 editor.selections.display_ranges(cx),
7152 [
7153 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7154 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7155 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7156 ]
7157 );
7158
7159 editor.undo(&Undo, window, cx);
7160 assert_eq!(
7161 editor.text(cx),
7162 "
7163 a
7164 b
7165 c
7166 "
7167 .unindent()
7168 );
7169 assert_eq!(
7170 editor.selections.display_ranges(cx),
7171 [
7172 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7173 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7174 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7175 ]
7176 );
7177
7178 // Ensure inserting the last character of a multi-byte bracket pair
7179 // doesn't surround the selections with the bracket.
7180 editor.handle_input("*", window, cx);
7181 assert_eq!(
7182 editor.text(cx),
7183 "
7184 *
7185 *
7186 *
7187 "
7188 .unindent()
7189 );
7190 assert_eq!(
7191 editor.selections.display_ranges(cx),
7192 [
7193 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7194 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7195 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7196 ]
7197 );
7198 });
7199}
7200
7201#[gpui::test]
7202async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7203 init_test(cx, |_| {});
7204
7205 let language = Arc::new(Language::new(
7206 LanguageConfig {
7207 brackets: BracketPairConfig {
7208 pairs: vec![BracketPair {
7209 start: "{".to_string(),
7210 end: "}".to_string(),
7211 close: true,
7212 surround: true,
7213 newline: true,
7214 }],
7215 ..Default::default()
7216 },
7217 autoclose_before: "}".to_string(),
7218 ..Default::default()
7219 },
7220 Some(tree_sitter_rust::LANGUAGE.into()),
7221 ));
7222
7223 let text = r#"
7224 a
7225 b
7226 c
7227 "#
7228 .unindent();
7229
7230 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7231 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7232 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7233 editor
7234 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7235 .await;
7236
7237 editor.update_in(cx, |editor, window, cx| {
7238 editor.change_selections(None, window, cx, |s| {
7239 s.select_ranges([
7240 Point::new(0, 1)..Point::new(0, 1),
7241 Point::new(1, 1)..Point::new(1, 1),
7242 Point::new(2, 1)..Point::new(2, 1),
7243 ])
7244 });
7245
7246 editor.handle_input("{", window, cx);
7247 editor.handle_input("{", window, cx);
7248 editor.handle_input("_", window, cx);
7249 assert_eq!(
7250 editor.text(cx),
7251 "
7252 a{{_}}
7253 b{{_}}
7254 c{{_}}
7255 "
7256 .unindent()
7257 );
7258 assert_eq!(
7259 editor.selections.ranges::<Point>(cx),
7260 [
7261 Point::new(0, 4)..Point::new(0, 4),
7262 Point::new(1, 4)..Point::new(1, 4),
7263 Point::new(2, 4)..Point::new(2, 4)
7264 ]
7265 );
7266
7267 editor.backspace(&Default::default(), window, cx);
7268 editor.backspace(&Default::default(), window, cx);
7269 assert_eq!(
7270 editor.text(cx),
7271 "
7272 a{}
7273 b{}
7274 c{}
7275 "
7276 .unindent()
7277 );
7278 assert_eq!(
7279 editor.selections.ranges::<Point>(cx),
7280 [
7281 Point::new(0, 2)..Point::new(0, 2),
7282 Point::new(1, 2)..Point::new(1, 2),
7283 Point::new(2, 2)..Point::new(2, 2)
7284 ]
7285 );
7286
7287 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7288 assert_eq!(
7289 editor.text(cx),
7290 "
7291 a
7292 b
7293 c
7294 "
7295 .unindent()
7296 );
7297 assert_eq!(
7298 editor.selections.ranges::<Point>(cx),
7299 [
7300 Point::new(0, 1)..Point::new(0, 1),
7301 Point::new(1, 1)..Point::new(1, 1),
7302 Point::new(2, 1)..Point::new(2, 1)
7303 ]
7304 );
7305 });
7306}
7307
7308#[gpui::test]
7309async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7310 init_test(cx, |settings| {
7311 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7312 });
7313
7314 let mut cx = EditorTestContext::new(cx).await;
7315
7316 let language = Arc::new(Language::new(
7317 LanguageConfig {
7318 brackets: BracketPairConfig {
7319 pairs: vec![
7320 BracketPair {
7321 start: "{".to_string(),
7322 end: "}".to_string(),
7323 close: true,
7324 surround: true,
7325 newline: true,
7326 },
7327 BracketPair {
7328 start: "(".to_string(),
7329 end: ")".to_string(),
7330 close: true,
7331 surround: true,
7332 newline: true,
7333 },
7334 BracketPair {
7335 start: "[".to_string(),
7336 end: "]".to_string(),
7337 close: false,
7338 surround: true,
7339 newline: true,
7340 },
7341 ],
7342 ..Default::default()
7343 },
7344 autoclose_before: "})]".to_string(),
7345 ..Default::default()
7346 },
7347 Some(tree_sitter_rust::LANGUAGE.into()),
7348 ));
7349
7350 cx.language_registry().add(language.clone());
7351 cx.update_buffer(|buffer, cx| {
7352 buffer.set_language(Some(language), cx);
7353 });
7354
7355 cx.set_state(
7356 &"
7357 {(ˇ)}
7358 [[ˇ]]
7359 {(ˇ)}
7360 "
7361 .unindent(),
7362 );
7363
7364 cx.update_editor(|editor, window, cx| {
7365 editor.backspace(&Default::default(), window, cx);
7366 editor.backspace(&Default::default(), window, cx);
7367 });
7368
7369 cx.assert_editor_state(
7370 &"
7371 ˇ
7372 ˇ]]
7373 ˇ
7374 "
7375 .unindent(),
7376 );
7377
7378 cx.update_editor(|editor, window, cx| {
7379 editor.handle_input("{", window, cx);
7380 editor.handle_input("{", window, cx);
7381 editor.move_right(&MoveRight, window, cx);
7382 editor.move_right(&MoveRight, window, cx);
7383 editor.move_left(&MoveLeft, window, cx);
7384 editor.move_left(&MoveLeft, window, cx);
7385 editor.backspace(&Default::default(), window, cx);
7386 });
7387
7388 cx.assert_editor_state(
7389 &"
7390 {ˇ}
7391 {ˇ}]]
7392 {ˇ}
7393 "
7394 .unindent(),
7395 );
7396
7397 cx.update_editor(|editor, window, cx| {
7398 editor.backspace(&Default::default(), window, cx);
7399 });
7400
7401 cx.assert_editor_state(
7402 &"
7403 ˇ
7404 ˇ]]
7405 ˇ
7406 "
7407 .unindent(),
7408 );
7409}
7410
7411#[gpui::test]
7412async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7413 init_test(cx, |_| {});
7414
7415 let language = Arc::new(Language::new(
7416 LanguageConfig::default(),
7417 Some(tree_sitter_rust::LANGUAGE.into()),
7418 ));
7419
7420 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7421 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7422 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7423 editor
7424 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7425 .await;
7426
7427 editor.update_in(cx, |editor, window, cx| {
7428 editor.set_auto_replace_emoji_shortcode(true);
7429
7430 editor.handle_input("Hello ", window, cx);
7431 editor.handle_input(":wave", window, cx);
7432 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7433
7434 editor.handle_input(":", window, cx);
7435 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7436
7437 editor.handle_input(" :smile", window, cx);
7438 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7439
7440 editor.handle_input(":", window, cx);
7441 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7442
7443 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7444 editor.handle_input(":wave", window, cx);
7445 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7446
7447 editor.handle_input(":", window, cx);
7448 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7449
7450 editor.handle_input(":1", window, cx);
7451 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7452
7453 editor.handle_input(":", window, cx);
7454 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7455
7456 // Ensure shortcode does not get replaced when it is part of a word
7457 editor.handle_input(" Test:wave", window, cx);
7458 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7459
7460 editor.handle_input(":", window, cx);
7461 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7462
7463 editor.set_auto_replace_emoji_shortcode(false);
7464
7465 // Ensure shortcode does not get replaced when auto replace is off
7466 editor.handle_input(" :wave", window, cx);
7467 assert_eq!(
7468 editor.text(cx),
7469 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7470 );
7471
7472 editor.handle_input(":", window, cx);
7473 assert_eq!(
7474 editor.text(cx),
7475 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7476 );
7477 });
7478}
7479
7480#[gpui::test]
7481async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7482 init_test(cx, |_| {});
7483
7484 let (text, insertion_ranges) = marked_text_ranges(
7485 indoc! {"
7486 ˇ
7487 "},
7488 false,
7489 );
7490
7491 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7492 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7493
7494 _ = editor.update_in(cx, |editor, window, cx| {
7495 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7496
7497 editor
7498 .insert_snippet(&insertion_ranges, snippet, window, cx)
7499 .unwrap();
7500
7501 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7502 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7503 assert_eq!(editor.text(cx), expected_text);
7504 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7505 }
7506
7507 assert(
7508 editor,
7509 cx,
7510 indoc! {"
7511 type «» =•
7512 "},
7513 );
7514
7515 assert!(editor.context_menu_visible(), "There should be a matches");
7516 });
7517}
7518
7519#[gpui::test]
7520async fn test_snippets(cx: &mut TestAppContext) {
7521 init_test(cx, |_| {});
7522
7523 let (text, insertion_ranges) = marked_text_ranges(
7524 indoc! {"
7525 a.ˇ b
7526 a.ˇ b
7527 a.ˇ b
7528 "},
7529 false,
7530 );
7531
7532 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7533 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7534
7535 editor.update_in(cx, |editor, window, cx| {
7536 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7537
7538 editor
7539 .insert_snippet(&insertion_ranges, snippet, window, cx)
7540 .unwrap();
7541
7542 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7543 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7544 assert_eq!(editor.text(cx), expected_text);
7545 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7546 }
7547
7548 assert(
7549 editor,
7550 cx,
7551 indoc! {"
7552 a.f(«one», two, «three») b
7553 a.f(«one», two, «three») b
7554 a.f(«one», two, «three») b
7555 "},
7556 );
7557
7558 // Can't move earlier than the first tab stop
7559 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7560 assert(
7561 editor,
7562 cx,
7563 indoc! {"
7564 a.f(«one», two, «three») b
7565 a.f(«one», two, «three») b
7566 a.f(«one», two, «three») b
7567 "},
7568 );
7569
7570 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7571 assert(
7572 editor,
7573 cx,
7574 indoc! {"
7575 a.f(one, «two», three) b
7576 a.f(one, «two», three) b
7577 a.f(one, «two», three) b
7578 "},
7579 );
7580
7581 editor.move_to_prev_snippet_tabstop(window, cx);
7582 assert(
7583 editor,
7584 cx,
7585 indoc! {"
7586 a.f(«one», two, «three») b
7587 a.f(«one», two, «three») b
7588 a.f(«one», two, «three») b
7589 "},
7590 );
7591
7592 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7593 assert(
7594 editor,
7595 cx,
7596 indoc! {"
7597 a.f(one, «two», three) b
7598 a.f(one, «two», three) b
7599 a.f(one, «two», three) b
7600 "},
7601 );
7602 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7603 assert(
7604 editor,
7605 cx,
7606 indoc! {"
7607 a.f(one, two, three)ˇ b
7608 a.f(one, two, three)ˇ b
7609 a.f(one, two, three)ˇ b
7610 "},
7611 );
7612
7613 // As soon as the last tab stop is reached, snippet state is gone
7614 editor.move_to_prev_snippet_tabstop(window, cx);
7615 assert(
7616 editor,
7617 cx,
7618 indoc! {"
7619 a.f(one, two, three)ˇ b
7620 a.f(one, two, three)ˇ b
7621 a.f(one, two, three)ˇ b
7622 "},
7623 );
7624 });
7625}
7626
7627#[gpui::test]
7628async fn test_document_format_during_save(cx: &mut TestAppContext) {
7629 init_test(cx, |_| {});
7630
7631 let fs = FakeFs::new(cx.executor());
7632 fs.insert_file(path!("/file.rs"), Default::default()).await;
7633
7634 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7635
7636 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7637 language_registry.add(rust_lang());
7638 let mut fake_servers = language_registry.register_fake_lsp(
7639 "Rust",
7640 FakeLspAdapter {
7641 capabilities: lsp::ServerCapabilities {
7642 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7643 ..Default::default()
7644 },
7645 ..Default::default()
7646 },
7647 );
7648
7649 let buffer = project
7650 .update(cx, |project, cx| {
7651 project.open_local_buffer(path!("/file.rs"), cx)
7652 })
7653 .await
7654 .unwrap();
7655
7656 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7657 let (editor, cx) = cx.add_window_view(|window, cx| {
7658 build_editor_with_project(project.clone(), buffer, window, cx)
7659 });
7660 editor.update_in(cx, |editor, window, cx| {
7661 editor.set_text("one\ntwo\nthree\n", window, cx)
7662 });
7663 assert!(cx.read(|cx| editor.is_dirty(cx)));
7664
7665 cx.executor().start_waiting();
7666 let fake_server = fake_servers.next().await.unwrap();
7667
7668 let save = editor
7669 .update_in(cx, |editor, window, cx| {
7670 editor.save(true, project.clone(), window, cx)
7671 })
7672 .unwrap();
7673 fake_server
7674 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7675 assert_eq!(
7676 params.text_document.uri,
7677 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7678 );
7679 assert_eq!(params.options.tab_size, 4);
7680 Ok(Some(vec![lsp::TextEdit::new(
7681 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7682 ", ".to_string(),
7683 )]))
7684 })
7685 .next()
7686 .await;
7687 cx.executor().start_waiting();
7688 save.await;
7689
7690 assert_eq!(
7691 editor.update(cx, |editor, cx| editor.text(cx)),
7692 "one, two\nthree\n"
7693 );
7694 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7695
7696 editor.update_in(cx, |editor, window, cx| {
7697 editor.set_text("one\ntwo\nthree\n", window, cx)
7698 });
7699 assert!(cx.read(|cx| editor.is_dirty(cx)));
7700
7701 // Ensure we can still save even if formatting hangs.
7702 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7703 move |params, _| async move {
7704 assert_eq!(
7705 params.text_document.uri,
7706 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7707 );
7708 futures::future::pending::<()>().await;
7709 unreachable!()
7710 },
7711 );
7712 let save = editor
7713 .update_in(cx, |editor, window, cx| {
7714 editor.save(true, project.clone(), window, cx)
7715 })
7716 .unwrap();
7717 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7718 cx.executor().start_waiting();
7719 save.await;
7720 assert_eq!(
7721 editor.update(cx, |editor, cx| editor.text(cx)),
7722 "one\ntwo\nthree\n"
7723 );
7724 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7725
7726 // For non-dirty buffer, no formatting request should be sent
7727 let save = editor
7728 .update_in(cx, |editor, window, cx| {
7729 editor.save(true, project.clone(), window, cx)
7730 })
7731 .unwrap();
7732 let _pending_format_request = fake_server
7733 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7734 panic!("Should not be invoked on non-dirty buffer");
7735 })
7736 .next();
7737 cx.executor().start_waiting();
7738 save.await;
7739
7740 // Set rust language override and assert overridden tabsize is sent to language server
7741 update_test_language_settings(cx, |settings| {
7742 settings.languages.insert(
7743 "Rust".into(),
7744 LanguageSettingsContent {
7745 tab_size: NonZeroU32::new(8),
7746 ..Default::default()
7747 },
7748 );
7749 });
7750
7751 editor.update_in(cx, |editor, window, cx| {
7752 editor.set_text("somehting_new\n", window, cx)
7753 });
7754 assert!(cx.read(|cx| editor.is_dirty(cx)));
7755 let save = editor
7756 .update_in(cx, |editor, window, cx| {
7757 editor.save(true, project.clone(), window, cx)
7758 })
7759 .unwrap();
7760 fake_server
7761 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7762 assert_eq!(
7763 params.text_document.uri,
7764 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7765 );
7766 assert_eq!(params.options.tab_size, 8);
7767 Ok(Some(vec![]))
7768 })
7769 .next()
7770 .await;
7771 cx.executor().start_waiting();
7772 save.await;
7773}
7774
7775#[gpui::test]
7776async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7777 init_test(cx, |_| {});
7778
7779 let cols = 4;
7780 let rows = 10;
7781 let sample_text_1 = sample_text(rows, cols, 'a');
7782 assert_eq!(
7783 sample_text_1,
7784 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7785 );
7786 let sample_text_2 = sample_text(rows, cols, 'l');
7787 assert_eq!(
7788 sample_text_2,
7789 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7790 );
7791 let sample_text_3 = sample_text(rows, cols, 'v');
7792 assert_eq!(
7793 sample_text_3,
7794 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7795 );
7796
7797 let fs = FakeFs::new(cx.executor());
7798 fs.insert_tree(
7799 path!("/a"),
7800 json!({
7801 "main.rs": sample_text_1,
7802 "other.rs": sample_text_2,
7803 "lib.rs": sample_text_3,
7804 }),
7805 )
7806 .await;
7807
7808 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7810 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7811
7812 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7813 language_registry.add(rust_lang());
7814 let mut fake_servers = language_registry.register_fake_lsp(
7815 "Rust",
7816 FakeLspAdapter {
7817 capabilities: lsp::ServerCapabilities {
7818 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7819 ..Default::default()
7820 },
7821 ..Default::default()
7822 },
7823 );
7824
7825 let worktree = project.update(cx, |project, cx| {
7826 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7827 assert_eq!(worktrees.len(), 1);
7828 worktrees.pop().unwrap()
7829 });
7830 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7831
7832 let buffer_1 = project
7833 .update(cx, |project, cx| {
7834 project.open_buffer((worktree_id, "main.rs"), cx)
7835 })
7836 .await
7837 .unwrap();
7838 let buffer_2 = project
7839 .update(cx, |project, cx| {
7840 project.open_buffer((worktree_id, "other.rs"), cx)
7841 })
7842 .await
7843 .unwrap();
7844 let buffer_3 = project
7845 .update(cx, |project, cx| {
7846 project.open_buffer((worktree_id, "lib.rs"), cx)
7847 })
7848 .await
7849 .unwrap();
7850
7851 let multi_buffer = cx.new(|cx| {
7852 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7853 multi_buffer.push_excerpts(
7854 buffer_1.clone(),
7855 [
7856 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7857 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7858 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7859 ],
7860 cx,
7861 );
7862 multi_buffer.push_excerpts(
7863 buffer_2.clone(),
7864 [
7865 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7866 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7867 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7868 ],
7869 cx,
7870 );
7871 multi_buffer.push_excerpts(
7872 buffer_3.clone(),
7873 [
7874 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7875 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7876 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7877 ],
7878 cx,
7879 );
7880 multi_buffer
7881 });
7882 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7883 Editor::new(
7884 EditorMode::Full,
7885 multi_buffer,
7886 Some(project.clone()),
7887 window,
7888 cx,
7889 )
7890 });
7891
7892 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7893 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7894 s.select_ranges(Some(1..2))
7895 });
7896 editor.insert("|one|two|three|", window, cx);
7897 });
7898 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7899 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7900 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7901 s.select_ranges(Some(60..70))
7902 });
7903 editor.insert("|four|five|six|", window, cx);
7904 });
7905 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7906
7907 // First two buffers should be edited, but not the third one.
7908 assert_eq!(
7909 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7910 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
7911 );
7912 buffer_1.update(cx, |buffer, _| {
7913 assert!(buffer.is_dirty());
7914 assert_eq!(
7915 buffer.text(),
7916 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7917 )
7918 });
7919 buffer_2.update(cx, |buffer, _| {
7920 assert!(buffer.is_dirty());
7921 assert_eq!(
7922 buffer.text(),
7923 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7924 )
7925 });
7926 buffer_3.update(cx, |buffer, _| {
7927 assert!(!buffer.is_dirty());
7928 assert_eq!(buffer.text(), sample_text_3,)
7929 });
7930 cx.executor().run_until_parked();
7931
7932 cx.executor().start_waiting();
7933 let save = multi_buffer_editor
7934 .update_in(cx, |editor, window, cx| {
7935 editor.save(true, project.clone(), window, cx)
7936 })
7937 .unwrap();
7938
7939 let fake_server = fake_servers.next().await.unwrap();
7940 fake_server
7941 .server
7942 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
7943 Ok(Some(vec![lsp::TextEdit::new(
7944 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7945 format!("[{} formatted]", params.text_document.uri),
7946 )]))
7947 })
7948 .detach();
7949 save.await;
7950
7951 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
7952 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
7953 assert_eq!(
7954 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7955 uri!(
7956 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
7957 ),
7958 );
7959 buffer_1.update(cx, |buffer, _| {
7960 assert!(!buffer.is_dirty());
7961 assert_eq!(
7962 buffer.text(),
7963 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
7964 )
7965 });
7966 buffer_2.update(cx, |buffer, _| {
7967 assert!(!buffer.is_dirty());
7968 assert_eq!(
7969 buffer.text(),
7970 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
7971 )
7972 });
7973 buffer_3.update(cx, |buffer, _| {
7974 assert!(!buffer.is_dirty());
7975 assert_eq!(buffer.text(), sample_text_3,)
7976 });
7977}
7978
7979#[gpui::test]
7980async fn test_range_format_during_save(cx: &mut TestAppContext) {
7981 init_test(cx, |_| {});
7982
7983 let fs = FakeFs::new(cx.executor());
7984 fs.insert_file(path!("/file.rs"), Default::default()).await;
7985
7986 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
7987
7988 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7989 language_registry.add(rust_lang());
7990 let mut fake_servers = language_registry.register_fake_lsp(
7991 "Rust",
7992 FakeLspAdapter {
7993 capabilities: lsp::ServerCapabilities {
7994 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
7995 ..Default::default()
7996 },
7997 ..Default::default()
7998 },
7999 );
8000
8001 let buffer = project
8002 .update(cx, |project, cx| {
8003 project.open_local_buffer(path!("/file.rs"), cx)
8004 })
8005 .await
8006 .unwrap();
8007
8008 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8009 let (editor, cx) = cx.add_window_view(|window, cx| {
8010 build_editor_with_project(project.clone(), buffer, window, cx)
8011 });
8012 editor.update_in(cx, |editor, window, cx| {
8013 editor.set_text("one\ntwo\nthree\n", window, cx)
8014 });
8015 assert!(cx.read(|cx| editor.is_dirty(cx)));
8016
8017 cx.executor().start_waiting();
8018 let fake_server = fake_servers.next().await.unwrap();
8019
8020 let save = editor
8021 .update_in(cx, |editor, window, cx| {
8022 editor.save(true, project.clone(), window, cx)
8023 })
8024 .unwrap();
8025 fake_server
8026 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8027 assert_eq!(
8028 params.text_document.uri,
8029 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8030 );
8031 assert_eq!(params.options.tab_size, 4);
8032 Ok(Some(vec![lsp::TextEdit::new(
8033 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8034 ", ".to_string(),
8035 )]))
8036 })
8037 .next()
8038 .await;
8039 cx.executor().start_waiting();
8040 save.await;
8041 assert_eq!(
8042 editor.update(cx, |editor, cx| editor.text(cx)),
8043 "one, two\nthree\n"
8044 );
8045 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8046
8047 editor.update_in(cx, |editor, window, cx| {
8048 editor.set_text("one\ntwo\nthree\n", window, cx)
8049 });
8050 assert!(cx.read(|cx| editor.is_dirty(cx)));
8051
8052 // Ensure we can still save even if formatting hangs.
8053 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8054 move |params, _| async move {
8055 assert_eq!(
8056 params.text_document.uri,
8057 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8058 );
8059 futures::future::pending::<()>().await;
8060 unreachable!()
8061 },
8062 );
8063 let save = editor
8064 .update_in(cx, |editor, window, cx| {
8065 editor.save(true, project.clone(), window, cx)
8066 })
8067 .unwrap();
8068 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8069 cx.executor().start_waiting();
8070 save.await;
8071 assert_eq!(
8072 editor.update(cx, |editor, cx| editor.text(cx)),
8073 "one\ntwo\nthree\n"
8074 );
8075 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8076
8077 // For non-dirty buffer, no formatting request should be sent
8078 let save = editor
8079 .update_in(cx, |editor, window, cx| {
8080 editor.save(true, project.clone(), window, cx)
8081 })
8082 .unwrap();
8083 let _pending_format_request = fake_server
8084 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8085 panic!("Should not be invoked on non-dirty buffer");
8086 })
8087 .next();
8088 cx.executor().start_waiting();
8089 save.await;
8090
8091 // Set Rust language override and assert overridden tabsize is sent to language server
8092 update_test_language_settings(cx, |settings| {
8093 settings.languages.insert(
8094 "Rust".into(),
8095 LanguageSettingsContent {
8096 tab_size: NonZeroU32::new(8),
8097 ..Default::default()
8098 },
8099 );
8100 });
8101
8102 editor.update_in(cx, |editor, window, cx| {
8103 editor.set_text("somehting_new\n", window, cx)
8104 });
8105 assert!(cx.read(|cx| editor.is_dirty(cx)));
8106 let save = editor
8107 .update_in(cx, |editor, window, cx| {
8108 editor.save(true, project.clone(), window, cx)
8109 })
8110 .unwrap();
8111 fake_server
8112 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8113 assert_eq!(
8114 params.text_document.uri,
8115 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8116 );
8117 assert_eq!(params.options.tab_size, 8);
8118 Ok(Some(vec![]))
8119 })
8120 .next()
8121 .await;
8122 cx.executor().start_waiting();
8123 save.await;
8124}
8125
8126#[gpui::test]
8127async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8128 init_test(cx, |settings| {
8129 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8130 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8131 ))
8132 });
8133
8134 let fs = FakeFs::new(cx.executor());
8135 fs.insert_file(path!("/file.rs"), Default::default()).await;
8136
8137 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8138
8139 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8140 language_registry.add(Arc::new(Language::new(
8141 LanguageConfig {
8142 name: "Rust".into(),
8143 matcher: LanguageMatcher {
8144 path_suffixes: vec!["rs".to_string()],
8145 ..Default::default()
8146 },
8147 ..LanguageConfig::default()
8148 },
8149 Some(tree_sitter_rust::LANGUAGE.into()),
8150 )));
8151 update_test_language_settings(cx, |settings| {
8152 // Enable Prettier formatting for the same buffer, and ensure
8153 // LSP is called instead of Prettier.
8154 settings.defaults.prettier = Some(PrettierSettings {
8155 allowed: true,
8156 ..PrettierSettings::default()
8157 });
8158 });
8159 let mut fake_servers = language_registry.register_fake_lsp(
8160 "Rust",
8161 FakeLspAdapter {
8162 capabilities: lsp::ServerCapabilities {
8163 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8164 ..Default::default()
8165 },
8166 ..Default::default()
8167 },
8168 );
8169
8170 let buffer = project
8171 .update(cx, |project, cx| {
8172 project.open_local_buffer(path!("/file.rs"), cx)
8173 })
8174 .await
8175 .unwrap();
8176
8177 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8178 let (editor, cx) = cx.add_window_view(|window, cx| {
8179 build_editor_with_project(project.clone(), buffer, window, cx)
8180 });
8181 editor.update_in(cx, |editor, window, cx| {
8182 editor.set_text("one\ntwo\nthree\n", window, cx)
8183 });
8184
8185 cx.executor().start_waiting();
8186 let fake_server = fake_servers.next().await.unwrap();
8187
8188 let format = editor
8189 .update_in(cx, |editor, window, cx| {
8190 editor.perform_format(
8191 project.clone(),
8192 FormatTrigger::Manual,
8193 FormatTarget::Buffers,
8194 window,
8195 cx,
8196 )
8197 })
8198 .unwrap();
8199 fake_server
8200 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8201 assert_eq!(
8202 params.text_document.uri,
8203 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8204 );
8205 assert_eq!(params.options.tab_size, 4);
8206 Ok(Some(vec![lsp::TextEdit::new(
8207 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8208 ", ".to_string(),
8209 )]))
8210 })
8211 .next()
8212 .await;
8213 cx.executor().start_waiting();
8214 format.await;
8215 assert_eq!(
8216 editor.update(cx, |editor, cx| editor.text(cx)),
8217 "one, two\nthree\n"
8218 );
8219
8220 editor.update_in(cx, |editor, window, cx| {
8221 editor.set_text("one\ntwo\nthree\n", window, cx)
8222 });
8223 // Ensure we don't lock if formatting hangs.
8224 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8225 move |params, _| async move {
8226 assert_eq!(
8227 params.text_document.uri,
8228 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8229 );
8230 futures::future::pending::<()>().await;
8231 unreachable!()
8232 },
8233 );
8234 let format = editor
8235 .update_in(cx, |editor, window, cx| {
8236 editor.perform_format(
8237 project,
8238 FormatTrigger::Manual,
8239 FormatTarget::Buffers,
8240 window,
8241 cx,
8242 )
8243 })
8244 .unwrap();
8245 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8246 cx.executor().start_waiting();
8247 format.await;
8248 assert_eq!(
8249 editor.update(cx, |editor, cx| editor.text(cx)),
8250 "one\ntwo\nthree\n"
8251 );
8252}
8253
8254#[gpui::test]
8255async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8256 init_test(cx, |settings| {
8257 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8258 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8259 ))
8260 });
8261
8262 let fs = FakeFs::new(cx.executor());
8263 fs.insert_file(path!("/file.ts"), Default::default()).await;
8264
8265 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8266
8267 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8268 language_registry.add(Arc::new(Language::new(
8269 LanguageConfig {
8270 name: "TypeScript".into(),
8271 matcher: LanguageMatcher {
8272 path_suffixes: vec!["ts".to_string()],
8273 ..Default::default()
8274 },
8275 ..LanguageConfig::default()
8276 },
8277 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8278 )));
8279 update_test_language_settings(cx, |settings| {
8280 settings.defaults.prettier = Some(PrettierSettings {
8281 allowed: true,
8282 ..PrettierSettings::default()
8283 });
8284 });
8285 let mut fake_servers = language_registry.register_fake_lsp(
8286 "TypeScript",
8287 FakeLspAdapter {
8288 capabilities: lsp::ServerCapabilities {
8289 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8290 ..Default::default()
8291 },
8292 ..Default::default()
8293 },
8294 );
8295
8296 let buffer = project
8297 .update(cx, |project, cx| {
8298 project.open_local_buffer(path!("/file.ts"), cx)
8299 })
8300 .await
8301 .unwrap();
8302
8303 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8304 let (editor, cx) = cx.add_window_view(|window, cx| {
8305 build_editor_with_project(project.clone(), buffer, window, cx)
8306 });
8307 editor.update_in(cx, |editor, window, cx| {
8308 editor.set_text(
8309 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8310 window,
8311 cx,
8312 )
8313 });
8314
8315 cx.executor().start_waiting();
8316 let fake_server = fake_servers.next().await.unwrap();
8317
8318 let format = editor
8319 .update_in(cx, |editor, window, cx| {
8320 editor.perform_code_action_kind(
8321 project.clone(),
8322 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8323 window,
8324 cx,
8325 )
8326 })
8327 .unwrap();
8328 fake_server
8329 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8330 assert_eq!(
8331 params.text_document.uri,
8332 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8333 );
8334 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8335 lsp::CodeAction {
8336 title: "Organize Imports".to_string(),
8337 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8338 edit: Some(lsp::WorkspaceEdit {
8339 changes: Some(
8340 [(
8341 params.text_document.uri.clone(),
8342 vec![lsp::TextEdit::new(
8343 lsp::Range::new(
8344 lsp::Position::new(1, 0),
8345 lsp::Position::new(2, 0),
8346 ),
8347 "".to_string(),
8348 )],
8349 )]
8350 .into_iter()
8351 .collect(),
8352 ),
8353 ..Default::default()
8354 }),
8355 ..Default::default()
8356 },
8357 )]))
8358 })
8359 .next()
8360 .await;
8361 cx.executor().start_waiting();
8362 format.await;
8363 assert_eq!(
8364 editor.update(cx, |editor, cx| editor.text(cx)),
8365 "import { a } from 'module';\n\nconst x = a;\n"
8366 );
8367
8368 editor.update_in(cx, |editor, window, cx| {
8369 editor.set_text(
8370 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8371 window,
8372 cx,
8373 )
8374 });
8375 // Ensure we don't lock if code action hangs.
8376 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8377 move |params, _| async move {
8378 assert_eq!(
8379 params.text_document.uri,
8380 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8381 );
8382 futures::future::pending::<()>().await;
8383 unreachable!()
8384 },
8385 );
8386 let format = editor
8387 .update_in(cx, |editor, window, cx| {
8388 editor.perform_code_action_kind(
8389 project,
8390 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8391 window,
8392 cx,
8393 )
8394 })
8395 .unwrap();
8396 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8397 cx.executor().start_waiting();
8398 format.await;
8399 assert_eq!(
8400 editor.update(cx, |editor, cx| editor.text(cx)),
8401 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8402 );
8403}
8404
8405#[gpui::test]
8406async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8407 init_test(cx, |_| {});
8408
8409 let mut cx = EditorLspTestContext::new_rust(
8410 lsp::ServerCapabilities {
8411 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8412 ..Default::default()
8413 },
8414 cx,
8415 )
8416 .await;
8417
8418 cx.set_state(indoc! {"
8419 one.twoˇ
8420 "});
8421
8422 // The format request takes a long time. When it completes, it inserts
8423 // a newline and an indent before the `.`
8424 cx.lsp
8425 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8426 let executor = cx.background_executor().clone();
8427 async move {
8428 executor.timer(Duration::from_millis(100)).await;
8429 Ok(Some(vec![lsp::TextEdit {
8430 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8431 new_text: "\n ".into(),
8432 }]))
8433 }
8434 });
8435
8436 // Submit a format request.
8437 let format_1 = cx
8438 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8439 .unwrap();
8440 cx.executor().run_until_parked();
8441
8442 // Submit a second format request.
8443 let format_2 = cx
8444 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8445 .unwrap();
8446 cx.executor().run_until_parked();
8447
8448 // Wait for both format requests to complete
8449 cx.executor().advance_clock(Duration::from_millis(200));
8450 cx.executor().start_waiting();
8451 format_1.await.unwrap();
8452 cx.executor().start_waiting();
8453 format_2.await.unwrap();
8454
8455 // The formatting edits only happens once.
8456 cx.assert_editor_state(indoc! {"
8457 one
8458 .twoˇ
8459 "});
8460}
8461
8462#[gpui::test]
8463async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8464 init_test(cx, |settings| {
8465 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8466 });
8467
8468 let mut cx = EditorLspTestContext::new_rust(
8469 lsp::ServerCapabilities {
8470 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8471 ..Default::default()
8472 },
8473 cx,
8474 )
8475 .await;
8476
8477 // Set up a buffer white some trailing whitespace and no trailing newline.
8478 cx.set_state(
8479 &[
8480 "one ", //
8481 "twoˇ", //
8482 "three ", //
8483 "four", //
8484 ]
8485 .join("\n"),
8486 );
8487
8488 // Submit a format request.
8489 let format = cx
8490 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8491 .unwrap();
8492
8493 // Record which buffer changes have been sent to the language server
8494 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8495 cx.lsp
8496 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8497 let buffer_changes = buffer_changes.clone();
8498 move |params, _| {
8499 buffer_changes.lock().extend(
8500 params
8501 .content_changes
8502 .into_iter()
8503 .map(|e| (e.range.unwrap(), e.text)),
8504 );
8505 }
8506 });
8507
8508 // Handle formatting requests to the language server.
8509 cx.lsp
8510 .set_request_handler::<lsp::request::Formatting, _, _>({
8511 let buffer_changes = buffer_changes.clone();
8512 move |_, _| {
8513 // When formatting is requested, trailing whitespace has already been stripped,
8514 // and the trailing newline has already been added.
8515 assert_eq!(
8516 &buffer_changes.lock()[1..],
8517 &[
8518 (
8519 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8520 "".into()
8521 ),
8522 (
8523 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8524 "".into()
8525 ),
8526 (
8527 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8528 "\n".into()
8529 ),
8530 ]
8531 );
8532
8533 // Insert blank lines between each line of the buffer.
8534 async move {
8535 Ok(Some(vec![
8536 lsp::TextEdit {
8537 range: lsp::Range::new(
8538 lsp::Position::new(1, 0),
8539 lsp::Position::new(1, 0),
8540 ),
8541 new_text: "\n".into(),
8542 },
8543 lsp::TextEdit {
8544 range: lsp::Range::new(
8545 lsp::Position::new(2, 0),
8546 lsp::Position::new(2, 0),
8547 ),
8548 new_text: "\n".into(),
8549 },
8550 ]))
8551 }
8552 }
8553 });
8554
8555 // After formatting the buffer, the trailing whitespace is stripped,
8556 // a newline is appended, and the edits provided by the language server
8557 // have been applied.
8558 format.await.unwrap();
8559 cx.assert_editor_state(
8560 &[
8561 "one", //
8562 "", //
8563 "twoˇ", //
8564 "", //
8565 "three", //
8566 "four", //
8567 "", //
8568 ]
8569 .join("\n"),
8570 );
8571
8572 // Undoing the formatting undoes the trailing whitespace removal, the
8573 // trailing newline, and the LSP edits.
8574 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8575 cx.assert_editor_state(
8576 &[
8577 "one ", //
8578 "twoˇ", //
8579 "three ", //
8580 "four", //
8581 ]
8582 .join("\n"),
8583 );
8584}
8585
8586#[gpui::test]
8587async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8588 cx: &mut TestAppContext,
8589) {
8590 init_test(cx, |_| {});
8591
8592 cx.update(|cx| {
8593 cx.update_global::<SettingsStore, _>(|settings, cx| {
8594 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8595 settings.auto_signature_help = Some(true);
8596 });
8597 });
8598 });
8599
8600 let mut cx = EditorLspTestContext::new_rust(
8601 lsp::ServerCapabilities {
8602 signature_help_provider: Some(lsp::SignatureHelpOptions {
8603 ..Default::default()
8604 }),
8605 ..Default::default()
8606 },
8607 cx,
8608 )
8609 .await;
8610
8611 let language = Language::new(
8612 LanguageConfig {
8613 name: "Rust".into(),
8614 brackets: BracketPairConfig {
8615 pairs: vec![
8616 BracketPair {
8617 start: "{".to_string(),
8618 end: "}".to_string(),
8619 close: true,
8620 surround: true,
8621 newline: true,
8622 },
8623 BracketPair {
8624 start: "(".to_string(),
8625 end: ")".to_string(),
8626 close: true,
8627 surround: true,
8628 newline: true,
8629 },
8630 BracketPair {
8631 start: "/*".to_string(),
8632 end: " */".to_string(),
8633 close: true,
8634 surround: true,
8635 newline: true,
8636 },
8637 BracketPair {
8638 start: "[".to_string(),
8639 end: "]".to_string(),
8640 close: false,
8641 surround: false,
8642 newline: true,
8643 },
8644 BracketPair {
8645 start: "\"".to_string(),
8646 end: "\"".to_string(),
8647 close: true,
8648 surround: true,
8649 newline: false,
8650 },
8651 BracketPair {
8652 start: "<".to_string(),
8653 end: ">".to_string(),
8654 close: false,
8655 surround: true,
8656 newline: true,
8657 },
8658 ],
8659 ..Default::default()
8660 },
8661 autoclose_before: "})]".to_string(),
8662 ..Default::default()
8663 },
8664 Some(tree_sitter_rust::LANGUAGE.into()),
8665 );
8666 let language = Arc::new(language);
8667
8668 cx.language_registry().add(language.clone());
8669 cx.update_buffer(|buffer, cx| {
8670 buffer.set_language(Some(language), cx);
8671 });
8672
8673 cx.set_state(
8674 &r#"
8675 fn main() {
8676 sampleˇ
8677 }
8678 "#
8679 .unindent(),
8680 );
8681
8682 cx.update_editor(|editor, window, cx| {
8683 editor.handle_input("(", window, cx);
8684 });
8685 cx.assert_editor_state(
8686 &"
8687 fn main() {
8688 sample(ˇ)
8689 }
8690 "
8691 .unindent(),
8692 );
8693
8694 let mocked_response = lsp::SignatureHelp {
8695 signatures: vec![lsp::SignatureInformation {
8696 label: "fn sample(param1: u8, param2: u8)".to_string(),
8697 documentation: None,
8698 parameters: Some(vec![
8699 lsp::ParameterInformation {
8700 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8701 documentation: None,
8702 },
8703 lsp::ParameterInformation {
8704 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8705 documentation: None,
8706 },
8707 ]),
8708 active_parameter: None,
8709 }],
8710 active_signature: Some(0),
8711 active_parameter: Some(0),
8712 };
8713 handle_signature_help_request(&mut cx, mocked_response).await;
8714
8715 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8716 .await;
8717
8718 cx.editor(|editor, _, _| {
8719 let signature_help_state = editor.signature_help_state.popover().cloned();
8720 assert_eq!(
8721 signature_help_state.unwrap().label,
8722 "param1: u8, param2: u8"
8723 );
8724 });
8725}
8726
8727#[gpui::test]
8728async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8729 init_test(cx, |_| {});
8730
8731 cx.update(|cx| {
8732 cx.update_global::<SettingsStore, _>(|settings, cx| {
8733 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8734 settings.auto_signature_help = Some(false);
8735 settings.show_signature_help_after_edits = Some(false);
8736 });
8737 });
8738 });
8739
8740 let mut cx = EditorLspTestContext::new_rust(
8741 lsp::ServerCapabilities {
8742 signature_help_provider: Some(lsp::SignatureHelpOptions {
8743 ..Default::default()
8744 }),
8745 ..Default::default()
8746 },
8747 cx,
8748 )
8749 .await;
8750
8751 let language = Language::new(
8752 LanguageConfig {
8753 name: "Rust".into(),
8754 brackets: BracketPairConfig {
8755 pairs: vec![
8756 BracketPair {
8757 start: "{".to_string(),
8758 end: "}".to_string(),
8759 close: true,
8760 surround: true,
8761 newline: true,
8762 },
8763 BracketPair {
8764 start: "(".to_string(),
8765 end: ")".to_string(),
8766 close: true,
8767 surround: true,
8768 newline: true,
8769 },
8770 BracketPair {
8771 start: "/*".to_string(),
8772 end: " */".to_string(),
8773 close: true,
8774 surround: true,
8775 newline: true,
8776 },
8777 BracketPair {
8778 start: "[".to_string(),
8779 end: "]".to_string(),
8780 close: false,
8781 surround: false,
8782 newline: true,
8783 },
8784 BracketPair {
8785 start: "\"".to_string(),
8786 end: "\"".to_string(),
8787 close: true,
8788 surround: true,
8789 newline: false,
8790 },
8791 BracketPair {
8792 start: "<".to_string(),
8793 end: ">".to_string(),
8794 close: false,
8795 surround: true,
8796 newline: true,
8797 },
8798 ],
8799 ..Default::default()
8800 },
8801 autoclose_before: "})]".to_string(),
8802 ..Default::default()
8803 },
8804 Some(tree_sitter_rust::LANGUAGE.into()),
8805 );
8806 let language = Arc::new(language);
8807
8808 cx.language_registry().add(language.clone());
8809 cx.update_buffer(|buffer, cx| {
8810 buffer.set_language(Some(language), cx);
8811 });
8812
8813 // Ensure that signature_help is not called when no signature help is enabled.
8814 cx.set_state(
8815 &r#"
8816 fn main() {
8817 sampleˇ
8818 }
8819 "#
8820 .unindent(),
8821 );
8822 cx.update_editor(|editor, window, cx| {
8823 editor.handle_input("(", window, cx);
8824 });
8825 cx.assert_editor_state(
8826 &"
8827 fn main() {
8828 sample(ˇ)
8829 }
8830 "
8831 .unindent(),
8832 );
8833 cx.editor(|editor, _, _| {
8834 assert!(editor.signature_help_state.task().is_none());
8835 });
8836
8837 let mocked_response = lsp::SignatureHelp {
8838 signatures: vec![lsp::SignatureInformation {
8839 label: "fn sample(param1: u8, param2: u8)".to_string(),
8840 documentation: None,
8841 parameters: Some(vec![
8842 lsp::ParameterInformation {
8843 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8844 documentation: None,
8845 },
8846 lsp::ParameterInformation {
8847 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8848 documentation: None,
8849 },
8850 ]),
8851 active_parameter: None,
8852 }],
8853 active_signature: Some(0),
8854 active_parameter: Some(0),
8855 };
8856
8857 // Ensure that signature_help is called when enabled afte edits
8858 cx.update(|_, cx| {
8859 cx.update_global::<SettingsStore, _>(|settings, cx| {
8860 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8861 settings.auto_signature_help = Some(false);
8862 settings.show_signature_help_after_edits = Some(true);
8863 });
8864 });
8865 });
8866 cx.set_state(
8867 &r#"
8868 fn main() {
8869 sampleˇ
8870 }
8871 "#
8872 .unindent(),
8873 );
8874 cx.update_editor(|editor, window, cx| {
8875 editor.handle_input("(", window, cx);
8876 });
8877 cx.assert_editor_state(
8878 &"
8879 fn main() {
8880 sample(ˇ)
8881 }
8882 "
8883 .unindent(),
8884 );
8885 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8886 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8887 .await;
8888 cx.update_editor(|editor, _, _| {
8889 let signature_help_state = editor.signature_help_state.popover().cloned();
8890 assert!(signature_help_state.is_some());
8891 assert_eq!(
8892 signature_help_state.unwrap().label,
8893 "param1: u8, param2: u8"
8894 );
8895 editor.signature_help_state = SignatureHelpState::default();
8896 });
8897
8898 // Ensure that signature_help is called when auto signature help override is enabled
8899 cx.update(|_, cx| {
8900 cx.update_global::<SettingsStore, _>(|settings, cx| {
8901 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8902 settings.auto_signature_help = Some(true);
8903 settings.show_signature_help_after_edits = Some(false);
8904 });
8905 });
8906 });
8907 cx.set_state(
8908 &r#"
8909 fn main() {
8910 sampleˇ
8911 }
8912 "#
8913 .unindent(),
8914 );
8915 cx.update_editor(|editor, window, cx| {
8916 editor.handle_input("(", window, cx);
8917 });
8918 cx.assert_editor_state(
8919 &"
8920 fn main() {
8921 sample(ˇ)
8922 }
8923 "
8924 .unindent(),
8925 );
8926 handle_signature_help_request(&mut cx, mocked_response).await;
8927 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8928 .await;
8929 cx.editor(|editor, _, _| {
8930 let signature_help_state = editor.signature_help_state.popover().cloned();
8931 assert!(signature_help_state.is_some());
8932 assert_eq!(
8933 signature_help_state.unwrap().label,
8934 "param1: u8, param2: u8"
8935 );
8936 });
8937}
8938
8939#[gpui::test]
8940async fn test_signature_help(cx: &mut TestAppContext) {
8941 init_test(cx, |_| {});
8942 cx.update(|cx| {
8943 cx.update_global::<SettingsStore, _>(|settings, cx| {
8944 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8945 settings.auto_signature_help = Some(true);
8946 });
8947 });
8948 });
8949
8950 let mut cx = EditorLspTestContext::new_rust(
8951 lsp::ServerCapabilities {
8952 signature_help_provider: Some(lsp::SignatureHelpOptions {
8953 ..Default::default()
8954 }),
8955 ..Default::default()
8956 },
8957 cx,
8958 )
8959 .await;
8960
8961 // A test that directly calls `show_signature_help`
8962 cx.update_editor(|editor, window, cx| {
8963 editor.show_signature_help(&ShowSignatureHelp, window, cx);
8964 });
8965
8966 let mocked_response = lsp::SignatureHelp {
8967 signatures: vec![lsp::SignatureInformation {
8968 label: "fn sample(param1: u8, param2: u8)".to_string(),
8969 documentation: None,
8970 parameters: Some(vec![
8971 lsp::ParameterInformation {
8972 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8973 documentation: None,
8974 },
8975 lsp::ParameterInformation {
8976 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8977 documentation: None,
8978 },
8979 ]),
8980 active_parameter: None,
8981 }],
8982 active_signature: Some(0),
8983 active_parameter: Some(0),
8984 };
8985 handle_signature_help_request(&mut cx, mocked_response).await;
8986
8987 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8988 .await;
8989
8990 cx.editor(|editor, _, _| {
8991 let signature_help_state = editor.signature_help_state.popover().cloned();
8992 assert!(signature_help_state.is_some());
8993 assert_eq!(
8994 signature_help_state.unwrap().label,
8995 "param1: u8, param2: u8"
8996 );
8997 });
8998
8999 // When exiting outside from inside the brackets, `signature_help` is closed.
9000 cx.set_state(indoc! {"
9001 fn main() {
9002 sample(ˇ);
9003 }
9004
9005 fn sample(param1: u8, param2: u8) {}
9006 "});
9007
9008 cx.update_editor(|editor, window, cx| {
9009 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9010 });
9011
9012 let mocked_response = lsp::SignatureHelp {
9013 signatures: Vec::new(),
9014 active_signature: None,
9015 active_parameter: None,
9016 };
9017 handle_signature_help_request(&mut cx, mocked_response).await;
9018
9019 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9020 .await;
9021
9022 cx.editor(|editor, _, _| {
9023 assert!(!editor.signature_help_state.is_shown());
9024 });
9025
9026 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9027 cx.set_state(indoc! {"
9028 fn main() {
9029 sample(ˇ);
9030 }
9031
9032 fn sample(param1: u8, param2: u8) {}
9033 "});
9034
9035 let mocked_response = lsp::SignatureHelp {
9036 signatures: vec![lsp::SignatureInformation {
9037 label: "fn sample(param1: u8, param2: u8)".to_string(),
9038 documentation: None,
9039 parameters: Some(vec![
9040 lsp::ParameterInformation {
9041 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9042 documentation: None,
9043 },
9044 lsp::ParameterInformation {
9045 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9046 documentation: None,
9047 },
9048 ]),
9049 active_parameter: None,
9050 }],
9051 active_signature: Some(0),
9052 active_parameter: Some(0),
9053 };
9054 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9055 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9056 .await;
9057 cx.editor(|editor, _, _| {
9058 assert!(editor.signature_help_state.is_shown());
9059 });
9060
9061 // Restore the popover with more parameter input
9062 cx.set_state(indoc! {"
9063 fn main() {
9064 sample(param1, param2ˇ);
9065 }
9066
9067 fn sample(param1: u8, param2: u8) {}
9068 "});
9069
9070 let mocked_response = lsp::SignatureHelp {
9071 signatures: vec![lsp::SignatureInformation {
9072 label: "fn sample(param1: u8, param2: u8)".to_string(),
9073 documentation: None,
9074 parameters: Some(vec![
9075 lsp::ParameterInformation {
9076 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9077 documentation: None,
9078 },
9079 lsp::ParameterInformation {
9080 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9081 documentation: None,
9082 },
9083 ]),
9084 active_parameter: None,
9085 }],
9086 active_signature: Some(0),
9087 active_parameter: Some(1),
9088 };
9089 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9090 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9091 .await;
9092
9093 // When selecting a range, the popover is gone.
9094 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9095 cx.update_editor(|editor, window, cx| {
9096 editor.change_selections(None, window, cx, |s| {
9097 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9098 })
9099 });
9100 cx.assert_editor_state(indoc! {"
9101 fn main() {
9102 sample(param1, «ˇparam2»);
9103 }
9104
9105 fn sample(param1: u8, param2: u8) {}
9106 "});
9107 cx.editor(|editor, _, _| {
9108 assert!(!editor.signature_help_state.is_shown());
9109 });
9110
9111 // When unselecting again, the popover is back if within the brackets.
9112 cx.update_editor(|editor, window, cx| {
9113 editor.change_selections(None, window, cx, |s| {
9114 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9115 })
9116 });
9117 cx.assert_editor_state(indoc! {"
9118 fn main() {
9119 sample(param1, ˇparam2);
9120 }
9121
9122 fn sample(param1: u8, param2: u8) {}
9123 "});
9124 handle_signature_help_request(&mut cx, mocked_response).await;
9125 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9126 .await;
9127 cx.editor(|editor, _, _| {
9128 assert!(editor.signature_help_state.is_shown());
9129 });
9130
9131 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9132 cx.update_editor(|editor, window, cx| {
9133 editor.change_selections(None, window, cx, |s| {
9134 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9135 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9136 })
9137 });
9138 cx.assert_editor_state(indoc! {"
9139 fn main() {
9140 sample(param1, ˇparam2);
9141 }
9142
9143 fn sample(param1: u8, param2: u8) {}
9144 "});
9145
9146 let mocked_response = lsp::SignatureHelp {
9147 signatures: vec![lsp::SignatureInformation {
9148 label: "fn sample(param1: u8, param2: u8)".to_string(),
9149 documentation: None,
9150 parameters: Some(vec![
9151 lsp::ParameterInformation {
9152 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9153 documentation: None,
9154 },
9155 lsp::ParameterInformation {
9156 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9157 documentation: None,
9158 },
9159 ]),
9160 active_parameter: None,
9161 }],
9162 active_signature: Some(0),
9163 active_parameter: Some(1),
9164 };
9165 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9166 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9167 .await;
9168 cx.update_editor(|editor, _, cx| {
9169 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9170 });
9171 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9172 .await;
9173 cx.update_editor(|editor, window, cx| {
9174 editor.change_selections(None, window, cx, |s| {
9175 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9176 })
9177 });
9178 cx.assert_editor_state(indoc! {"
9179 fn main() {
9180 sample(param1, «ˇparam2»);
9181 }
9182
9183 fn sample(param1: u8, param2: u8) {}
9184 "});
9185 cx.update_editor(|editor, window, cx| {
9186 editor.change_selections(None, window, cx, |s| {
9187 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9188 })
9189 });
9190 cx.assert_editor_state(indoc! {"
9191 fn main() {
9192 sample(param1, ˇparam2);
9193 }
9194
9195 fn sample(param1: u8, param2: u8) {}
9196 "});
9197 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9198 .await;
9199}
9200
9201#[gpui::test]
9202async fn test_completion_mode(cx: &mut TestAppContext) {
9203 init_test(cx, |_| {});
9204 let mut cx = EditorLspTestContext::new_rust(
9205 lsp::ServerCapabilities {
9206 completion_provider: Some(lsp::CompletionOptions {
9207 resolve_provider: Some(true),
9208 ..Default::default()
9209 }),
9210 ..Default::default()
9211 },
9212 cx,
9213 )
9214 .await;
9215
9216 struct Run {
9217 run_description: &'static str,
9218 initial_state: String,
9219 buffer_marked_text: String,
9220 completion_text: &'static str,
9221 expected_with_insertion_mode: String,
9222 expected_with_replace_mode: String,
9223 expected_with_replace_subsequence_mode: String,
9224 expected_with_replace_suffix_mode: String,
9225 }
9226
9227 let runs = [
9228 Run {
9229 run_description: "Start of word matches completion text",
9230 initial_state: "before ediˇ after".into(),
9231 buffer_marked_text: "before <edi|> after".into(),
9232 completion_text: "editor",
9233 expected_with_insertion_mode: "before editorˇ after".into(),
9234 expected_with_replace_mode: "before editorˇ after".into(),
9235 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9236 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9237 },
9238 Run {
9239 run_description: "Accept same text at the middle of the word",
9240 initial_state: "before ediˇtor after".into(),
9241 buffer_marked_text: "before <edi|tor> after".into(),
9242 completion_text: "editor",
9243 expected_with_insertion_mode: "before editorˇtor after".into(),
9244 expected_with_replace_mode: "before ediˇtor after".into(),
9245 expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
9246 expected_with_replace_suffix_mode: "before ediˇtor after".into(),
9247 },
9248 Run {
9249 run_description: "End of word matches completion text -- cursor at end",
9250 initial_state: "before torˇ after".into(),
9251 buffer_marked_text: "before <tor|> after".into(),
9252 completion_text: "editor",
9253 expected_with_insertion_mode: "before editorˇ after".into(),
9254 expected_with_replace_mode: "before editorˇ after".into(),
9255 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9256 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9257 },
9258 Run {
9259 run_description: "End of word matches completion text -- cursor at start",
9260 initial_state: "before ˇtor after".into(),
9261 buffer_marked_text: "before <|tor> after".into(),
9262 completion_text: "editor",
9263 expected_with_insertion_mode: "before editorˇtor after".into(),
9264 expected_with_replace_mode: "before editorˇ after".into(),
9265 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9266 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9267 },
9268 Run {
9269 run_description: "Prepend text containing whitespace",
9270 initial_state: "pˇfield: bool".into(),
9271 buffer_marked_text: "<p|field>: bool".into(),
9272 completion_text: "pub ",
9273 expected_with_insertion_mode: "pub ˇfield: bool".into(),
9274 expected_with_replace_mode: "pub ˇ: bool".into(),
9275 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9276 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9277 },
9278 Run {
9279 run_description: "Add element to start of list",
9280 initial_state: "[element_ˇelement_2]".into(),
9281 buffer_marked_text: "[<element_|element_2>]".into(),
9282 completion_text: "element_1",
9283 expected_with_insertion_mode: "[element_1ˇelement_2]".into(),
9284 expected_with_replace_mode: "[element_1ˇ]".into(),
9285 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9286 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9287 },
9288 Run {
9289 run_description: "Add element to start of list -- first and second elements are equal",
9290 initial_state: "[elˇelement]".into(),
9291 buffer_marked_text: "[<el|element>]".into(),
9292 completion_text: "element",
9293 expected_with_insertion_mode: "[elementˇelement]".into(),
9294 expected_with_replace_mode: "[elˇement]".into(),
9295 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9296 expected_with_replace_suffix_mode: "[elˇement]".into(),
9297 },
9298 Run {
9299 run_description: "Ends with matching suffix",
9300 initial_state: "SubˇError".into(),
9301 buffer_marked_text: "<Sub|Error>".into(),
9302 completion_text: "SubscriptionError",
9303 expected_with_insertion_mode: "SubscriptionErrorˇError".into(),
9304 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9305 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9306 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9307 },
9308 Run {
9309 run_description: "Suffix is a subsequence -- contiguous",
9310 initial_state: "SubˇErr".into(),
9311 buffer_marked_text: "<Sub|Err>".into(),
9312 completion_text: "SubscriptionError",
9313 expected_with_insertion_mode: "SubscriptionErrorˇErr".into(),
9314 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9315 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9316 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9317 },
9318 Run {
9319 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9320 initial_state: "Suˇscrirr".into(),
9321 buffer_marked_text: "<Su|scrirr>".into(),
9322 completion_text: "SubscriptionError",
9323 expected_with_insertion_mode: "SubscriptionErrorˇscrirr".into(),
9324 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9325 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9326 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9327 },
9328 Run {
9329 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9330 initial_state: "foo(indˇix)".into(),
9331 buffer_marked_text: "foo(<ind|ix>)".into(),
9332 completion_text: "node_index",
9333 expected_with_insertion_mode: "foo(node_indexˇix)".into(),
9334 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9335 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9336 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9337 },
9338 ];
9339
9340 for run in runs {
9341 let run_variations = [
9342 (LspInsertMode::Insert, run.expected_with_insertion_mode),
9343 (LspInsertMode::Replace, run.expected_with_replace_mode),
9344 (
9345 LspInsertMode::ReplaceSubsequence,
9346 run.expected_with_replace_subsequence_mode,
9347 ),
9348 (
9349 LspInsertMode::ReplaceSuffix,
9350 run.expected_with_replace_suffix_mode,
9351 ),
9352 ];
9353
9354 for (lsp_insert_mode, expected_text) in run_variations {
9355 eprintln!(
9356 "run = {:?}, mode = {lsp_insert_mode:.?}",
9357 run.run_description,
9358 );
9359
9360 update_test_language_settings(&mut cx, |settings| {
9361 settings.defaults.completions = Some(CompletionSettings {
9362 lsp_insert_mode,
9363 words: WordsCompletionMode::Disabled,
9364 lsp: true,
9365 lsp_fetch_timeout_ms: 0,
9366 });
9367 });
9368
9369 cx.set_state(&run.initial_state);
9370 cx.update_editor(|editor, window, cx| {
9371 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9372 });
9373
9374 let counter = Arc::new(AtomicUsize::new(0));
9375 handle_completion_request_with_insert_and_replace(
9376 &mut cx,
9377 &run.buffer_marked_text,
9378 vec![run.completion_text],
9379 counter.clone(),
9380 )
9381 .await;
9382 cx.condition(|editor, _| editor.context_menu_visible())
9383 .await;
9384 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9385
9386 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9387 editor
9388 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9389 .unwrap()
9390 });
9391 cx.assert_editor_state(&expected_text);
9392 handle_resolve_completion_request(&mut cx, None).await;
9393 apply_additional_edits.await.unwrap();
9394 }
9395 }
9396}
9397
9398#[gpui::test]
9399async fn test_completion(cx: &mut TestAppContext) {
9400 init_test(cx, |_| {});
9401
9402 let mut cx = EditorLspTestContext::new_rust(
9403 lsp::ServerCapabilities {
9404 completion_provider: Some(lsp::CompletionOptions {
9405 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9406 resolve_provider: Some(true),
9407 ..Default::default()
9408 }),
9409 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9410 ..Default::default()
9411 },
9412 cx,
9413 )
9414 .await;
9415 let counter = Arc::new(AtomicUsize::new(0));
9416
9417 cx.set_state(indoc! {"
9418 oneˇ
9419 two
9420 three
9421 "});
9422 cx.simulate_keystroke(".");
9423 handle_completion_request(
9424 &mut cx,
9425 indoc! {"
9426 one.|<>
9427 two
9428 three
9429 "},
9430 vec!["first_completion", "second_completion"],
9431 counter.clone(),
9432 )
9433 .await;
9434 cx.condition(|editor, _| editor.context_menu_visible())
9435 .await;
9436 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9437
9438 let _handler = handle_signature_help_request(
9439 &mut cx,
9440 lsp::SignatureHelp {
9441 signatures: vec![lsp::SignatureInformation {
9442 label: "test signature".to_string(),
9443 documentation: None,
9444 parameters: Some(vec![lsp::ParameterInformation {
9445 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9446 documentation: None,
9447 }]),
9448 active_parameter: None,
9449 }],
9450 active_signature: None,
9451 active_parameter: None,
9452 },
9453 );
9454 cx.update_editor(|editor, window, cx| {
9455 assert!(
9456 !editor.signature_help_state.is_shown(),
9457 "No signature help was called for"
9458 );
9459 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9460 });
9461 cx.run_until_parked();
9462 cx.update_editor(|editor, _, _| {
9463 assert!(
9464 !editor.signature_help_state.is_shown(),
9465 "No signature help should be shown when completions menu is open"
9466 );
9467 });
9468
9469 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9470 editor.context_menu_next(&Default::default(), window, cx);
9471 editor
9472 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9473 .unwrap()
9474 });
9475 cx.assert_editor_state(indoc! {"
9476 one.second_completionˇ
9477 two
9478 three
9479 "});
9480
9481 handle_resolve_completion_request(
9482 &mut cx,
9483 Some(vec![
9484 (
9485 //This overlaps with the primary completion edit which is
9486 //misbehavior from the LSP spec, test that we filter it out
9487 indoc! {"
9488 one.second_ˇcompletion
9489 two
9490 threeˇ
9491 "},
9492 "overlapping additional edit",
9493 ),
9494 (
9495 indoc! {"
9496 one.second_completion
9497 two
9498 threeˇ
9499 "},
9500 "\nadditional edit",
9501 ),
9502 ]),
9503 )
9504 .await;
9505 apply_additional_edits.await.unwrap();
9506 cx.assert_editor_state(indoc! {"
9507 one.second_completionˇ
9508 two
9509 three
9510 additional edit
9511 "});
9512
9513 cx.set_state(indoc! {"
9514 one.second_completion
9515 twoˇ
9516 threeˇ
9517 additional edit
9518 "});
9519 cx.simulate_keystroke(" ");
9520 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9521 cx.simulate_keystroke("s");
9522 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9523
9524 cx.assert_editor_state(indoc! {"
9525 one.second_completion
9526 two sˇ
9527 three sˇ
9528 additional edit
9529 "});
9530 handle_completion_request(
9531 &mut cx,
9532 indoc! {"
9533 one.second_completion
9534 two s
9535 three <s|>
9536 additional edit
9537 "},
9538 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9539 counter.clone(),
9540 )
9541 .await;
9542 cx.condition(|editor, _| editor.context_menu_visible())
9543 .await;
9544 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9545
9546 cx.simulate_keystroke("i");
9547
9548 handle_completion_request(
9549 &mut cx,
9550 indoc! {"
9551 one.second_completion
9552 two si
9553 three <si|>
9554 additional edit
9555 "},
9556 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9557 counter.clone(),
9558 )
9559 .await;
9560 cx.condition(|editor, _| editor.context_menu_visible())
9561 .await;
9562 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9563
9564 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9565 editor
9566 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9567 .unwrap()
9568 });
9569 cx.assert_editor_state(indoc! {"
9570 one.second_completion
9571 two sixth_completionˇ
9572 three sixth_completionˇ
9573 additional edit
9574 "});
9575
9576 apply_additional_edits.await.unwrap();
9577
9578 update_test_language_settings(&mut cx, |settings| {
9579 settings.defaults.show_completions_on_input = Some(false);
9580 });
9581 cx.set_state("editorˇ");
9582 cx.simulate_keystroke(".");
9583 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9584 cx.simulate_keystrokes("c l o");
9585 cx.assert_editor_state("editor.cloˇ");
9586 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9587 cx.update_editor(|editor, window, cx| {
9588 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9589 });
9590 handle_completion_request(
9591 &mut cx,
9592 "editor.<clo|>",
9593 vec!["close", "clobber"],
9594 counter.clone(),
9595 )
9596 .await;
9597 cx.condition(|editor, _| editor.context_menu_visible())
9598 .await;
9599 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9600
9601 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9602 editor
9603 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9604 .unwrap()
9605 });
9606 cx.assert_editor_state("editor.closeˇ");
9607 handle_resolve_completion_request(&mut cx, None).await;
9608 apply_additional_edits.await.unwrap();
9609}
9610
9611#[gpui::test]
9612async fn test_word_completion(cx: &mut TestAppContext) {
9613 let lsp_fetch_timeout_ms = 10;
9614 init_test(cx, |language_settings| {
9615 language_settings.defaults.completions = Some(CompletionSettings {
9616 words: WordsCompletionMode::Fallback,
9617 lsp: true,
9618 lsp_fetch_timeout_ms: 10,
9619 lsp_insert_mode: LspInsertMode::Insert,
9620 });
9621 });
9622
9623 let mut cx = EditorLspTestContext::new_rust(
9624 lsp::ServerCapabilities {
9625 completion_provider: Some(lsp::CompletionOptions {
9626 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9627 ..lsp::CompletionOptions::default()
9628 }),
9629 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9630 ..lsp::ServerCapabilities::default()
9631 },
9632 cx,
9633 )
9634 .await;
9635
9636 let throttle_completions = Arc::new(AtomicBool::new(false));
9637
9638 let lsp_throttle_completions = throttle_completions.clone();
9639 let _completion_requests_handler =
9640 cx.lsp
9641 .server
9642 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9643 let lsp_throttle_completions = lsp_throttle_completions.clone();
9644 let cx = cx.clone();
9645 async move {
9646 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9647 cx.background_executor()
9648 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9649 .await;
9650 }
9651 Ok(Some(lsp::CompletionResponse::Array(vec![
9652 lsp::CompletionItem {
9653 label: "first".into(),
9654 ..lsp::CompletionItem::default()
9655 },
9656 lsp::CompletionItem {
9657 label: "last".into(),
9658 ..lsp::CompletionItem::default()
9659 },
9660 ])))
9661 }
9662 });
9663
9664 cx.set_state(indoc! {"
9665 oneˇ
9666 two
9667 three
9668 "});
9669 cx.simulate_keystroke(".");
9670 cx.executor().run_until_parked();
9671 cx.condition(|editor, _| editor.context_menu_visible())
9672 .await;
9673 cx.update_editor(|editor, window, cx| {
9674 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9675 {
9676 assert_eq!(
9677 completion_menu_entries(&menu),
9678 &["first", "last"],
9679 "When LSP server is fast to reply, no fallback word completions are used"
9680 );
9681 } else {
9682 panic!("expected completion menu to be open");
9683 }
9684 editor.cancel(&Cancel, window, cx);
9685 });
9686 cx.executor().run_until_parked();
9687 cx.condition(|editor, _| !editor.context_menu_visible())
9688 .await;
9689
9690 throttle_completions.store(true, atomic::Ordering::Release);
9691 cx.simulate_keystroke(".");
9692 cx.executor()
9693 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9694 cx.executor().run_until_parked();
9695 cx.condition(|editor, _| editor.context_menu_visible())
9696 .await;
9697 cx.update_editor(|editor, _, _| {
9698 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9699 {
9700 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9701 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9702 } else {
9703 panic!("expected completion menu to be open");
9704 }
9705 });
9706}
9707
9708#[gpui::test]
9709async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9710 init_test(cx, |language_settings| {
9711 language_settings.defaults.completions = Some(CompletionSettings {
9712 words: WordsCompletionMode::Enabled,
9713 lsp: true,
9714 lsp_fetch_timeout_ms: 0,
9715 lsp_insert_mode: LspInsertMode::Insert,
9716 });
9717 });
9718
9719 let mut cx = EditorLspTestContext::new_rust(
9720 lsp::ServerCapabilities {
9721 completion_provider: Some(lsp::CompletionOptions {
9722 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9723 ..lsp::CompletionOptions::default()
9724 }),
9725 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9726 ..lsp::ServerCapabilities::default()
9727 },
9728 cx,
9729 )
9730 .await;
9731
9732 let _completion_requests_handler =
9733 cx.lsp
9734 .server
9735 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9736 Ok(Some(lsp::CompletionResponse::Array(vec![
9737 lsp::CompletionItem {
9738 label: "first".into(),
9739 ..lsp::CompletionItem::default()
9740 },
9741 lsp::CompletionItem {
9742 label: "last".into(),
9743 ..lsp::CompletionItem::default()
9744 },
9745 ])))
9746 });
9747
9748 cx.set_state(indoc! {"ˇ
9749 first
9750 last
9751 second
9752 "});
9753 cx.simulate_keystroke(".");
9754 cx.executor().run_until_parked();
9755 cx.condition(|editor, _| editor.context_menu_visible())
9756 .await;
9757 cx.update_editor(|editor, _, _| {
9758 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9759 {
9760 assert_eq!(
9761 completion_menu_entries(&menu),
9762 &["first", "last", "second"],
9763 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9764 );
9765 } else {
9766 panic!("expected completion menu to be open");
9767 }
9768 });
9769}
9770
9771#[gpui::test]
9772async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9773 init_test(cx, |language_settings| {
9774 language_settings.defaults.completions = Some(CompletionSettings {
9775 words: WordsCompletionMode::Disabled,
9776 lsp: true,
9777 lsp_fetch_timeout_ms: 0,
9778 lsp_insert_mode: LspInsertMode::Insert,
9779 });
9780 });
9781
9782 let mut cx = EditorLspTestContext::new_rust(
9783 lsp::ServerCapabilities {
9784 completion_provider: Some(lsp::CompletionOptions {
9785 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9786 ..lsp::CompletionOptions::default()
9787 }),
9788 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9789 ..lsp::ServerCapabilities::default()
9790 },
9791 cx,
9792 )
9793 .await;
9794
9795 let _completion_requests_handler =
9796 cx.lsp
9797 .server
9798 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9799 panic!("LSP completions should not be queried when dealing with word completions")
9800 });
9801
9802 cx.set_state(indoc! {"ˇ
9803 first
9804 last
9805 second
9806 "});
9807 cx.update_editor(|editor, window, cx| {
9808 editor.show_word_completions(&ShowWordCompletions, window, cx);
9809 });
9810 cx.executor().run_until_parked();
9811 cx.condition(|editor, _| editor.context_menu_visible())
9812 .await;
9813 cx.update_editor(|editor, _, _| {
9814 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9815 {
9816 assert_eq!(
9817 completion_menu_entries(&menu),
9818 &["first", "last", "second"],
9819 "`ShowWordCompletions` action should show word completions"
9820 );
9821 } else {
9822 panic!("expected completion menu to be open");
9823 }
9824 });
9825
9826 cx.simulate_keystroke("l");
9827 cx.executor().run_until_parked();
9828 cx.condition(|editor, _| editor.context_menu_visible())
9829 .await;
9830 cx.update_editor(|editor, _, _| {
9831 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9832 {
9833 assert_eq!(
9834 completion_menu_entries(&menu),
9835 &["last"],
9836 "After showing word completions, further editing should filter them and not query the LSP"
9837 );
9838 } else {
9839 panic!("expected completion menu to be open");
9840 }
9841 });
9842}
9843
9844#[gpui::test]
9845async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9846 init_test(cx, |language_settings| {
9847 language_settings.defaults.completions = Some(CompletionSettings {
9848 words: WordsCompletionMode::Fallback,
9849 lsp: false,
9850 lsp_fetch_timeout_ms: 0,
9851 lsp_insert_mode: LspInsertMode::Insert,
9852 });
9853 });
9854
9855 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
9856
9857 cx.set_state(indoc! {"ˇ
9858 0_usize
9859 let
9860 33
9861 4.5f32
9862 "});
9863 cx.update_editor(|editor, window, cx| {
9864 editor.show_completions(&ShowCompletions::default(), window, cx);
9865 });
9866 cx.executor().run_until_parked();
9867 cx.condition(|editor, _| editor.context_menu_visible())
9868 .await;
9869 cx.update_editor(|editor, window, cx| {
9870 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9871 {
9872 assert_eq!(
9873 completion_menu_entries(&menu),
9874 &["let"],
9875 "With no digits in the completion query, no digits should be in the word completions"
9876 );
9877 } else {
9878 panic!("expected completion menu to be open");
9879 }
9880 editor.cancel(&Cancel, window, cx);
9881 });
9882
9883 cx.set_state(indoc! {"3ˇ
9884 0_usize
9885 let
9886 3
9887 33.35f32
9888 "});
9889 cx.update_editor(|editor, window, cx| {
9890 editor.show_completions(&ShowCompletions::default(), window, cx);
9891 });
9892 cx.executor().run_until_parked();
9893 cx.condition(|editor, _| editor.context_menu_visible())
9894 .await;
9895 cx.update_editor(|editor, _, _| {
9896 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9897 {
9898 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
9899 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
9900 } else {
9901 panic!("expected completion menu to be open");
9902 }
9903 });
9904}
9905
9906fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
9907 let position = || lsp::Position {
9908 line: params.text_document_position.position.line,
9909 character: params.text_document_position.position.character,
9910 };
9911 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
9912 range: lsp::Range {
9913 start: position(),
9914 end: position(),
9915 },
9916 new_text: text.to_string(),
9917 }))
9918}
9919
9920#[gpui::test]
9921async fn test_multiline_completion(cx: &mut TestAppContext) {
9922 init_test(cx, |_| {});
9923
9924 let fs = FakeFs::new(cx.executor());
9925 fs.insert_tree(
9926 path!("/a"),
9927 json!({
9928 "main.ts": "a",
9929 }),
9930 )
9931 .await;
9932
9933 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9934 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9935 let typescript_language = Arc::new(Language::new(
9936 LanguageConfig {
9937 name: "TypeScript".into(),
9938 matcher: LanguageMatcher {
9939 path_suffixes: vec!["ts".to_string()],
9940 ..LanguageMatcher::default()
9941 },
9942 line_comments: vec!["// ".into()],
9943 ..LanguageConfig::default()
9944 },
9945 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9946 ));
9947 language_registry.add(typescript_language.clone());
9948 let mut fake_servers = language_registry.register_fake_lsp(
9949 "TypeScript",
9950 FakeLspAdapter {
9951 capabilities: lsp::ServerCapabilities {
9952 completion_provider: Some(lsp::CompletionOptions {
9953 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9954 ..lsp::CompletionOptions::default()
9955 }),
9956 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9957 ..lsp::ServerCapabilities::default()
9958 },
9959 // Emulate vtsls label generation
9960 label_for_completion: Some(Box::new(|item, _| {
9961 let text = if let Some(description) = item
9962 .label_details
9963 .as_ref()
9964 .and_then(|label_details| label_details.description.as_ref())
9965 {
9966 format!("{} {}", item.label, description)
9967 } else if let Some(detail) = &item.detail {
9968 format!("{} {}", item.label, detail)
9969 } else {
9970 item.label.clone()
9971 };
9972 let len = text.len();
9973 Some(language::CodeLabel {
9974 text,
9975 runs: Vec::new(),
9976 filter_range: 0..len,
9977 })
9978 })),
9979 ..FakeLspAdapter::default()
9980 },
9981 );
9982 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9983 let cx = &mut VisualTestContext::from_window(*workspace, cx);
9984 let worktree_id = workspace
9985 .update(cx, |workspace, _window, cx| {
9986 workspace.project().update(cx, |project, cx| {
9987 project.worktrees(cx).next().unwrap().read(cx).id()
9988 })
9989 })
9990 .unwrap();
9991 let _buffer = project
9992 .update(cx, |project, cx| {
9993 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
9994 })
9995 .await
9996 .unwrap();
9997 let editor = workspace
9998 .update(cx, |workspace, window, cx| {
9999 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10000 })
10001 .unwrap()
10002 .await
10003 .unwrap()
10004 .downcast::<Editor>()
10005 .unwrap();
10006 let fake_server = fake_servers.next().await.unwrap();
10007
10008 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10009 let multiline_label_2 = "a\nb\nc\n";
10010 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10011 let multiline_description = "d\ne\nf\n";
10012 let multiline_detail_2 = "g\nh\ni\n";
10013
10014 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10015 move |params, _| async move {
10016 Ok(Some(lsp::CompletionResponse::Array(vec![
10017 lsp::CompletionItem {
10018 label: multiline_label.to_string(),
10019 text_edit: gen_text_edit(¶ms, "new_text_1"),
10020 ..lsp::CompletionItem::default()
10021 },
10022 lsp::CompletionItem {
10023 label: "single line label 1".to_string(),
10024 detail: Some(multiline_detail.to_string()),
10025 text_edit: gen_text_edit(¶ms, "new_text_2"),
10026 ..lsp::CompletionItem::default()
10027 },
10028 lsp::CompletionItem {
10029 label: "single line label 2".to_string(),
10030 label_details: Some(lsp::CompletionItemLabelDetails {
10031 description: Some(multiline_description.to_string()),
10032 detail: None,
10033 }),
10034 text_edit: gen_text_edit(¶ms, "new_text_2"),
10035 ..lsp::CompletionItem::default()
10036 },
10037 lsp::CompletionItem {
10038 label: multiline_label_2.to_string(),
10039 detail: Some(multiline_detail_2.to_string()),
10040 text_edit: gen_text_edit(¶ms, "new_text_3"),
10041 ..lsp::CompletionItem::default()
10042 },
10043 lsp::CompletionItem {
10044 label: "Label with many spaces and \t but without newlines".to_string(),
10045 detail: Some(
10046 "Details with many spaces and \t but without newlines".to_string(),
10047 ),
10048 text_edit: gen_text_edit(¶ms, "new_text_4"),
10049 ..lsp::CompletionItem::default()
10050 },
10051 ])))
10052 },
10053 );
10054
10055 editor.update_in(cx, |editor, window, cx| {
10056 cx.focus_self(window);
10057 editor.move_to_end(&MoveToEnd, window, cx);
10058 editor.handle_input(".", window, cx);
10059 });
10060 cx.run_until_parked();
10061 completion_handle.next().await.unwrap();
10062
10063 editor.update(cx, |editor, _| {
10064 assert!(editor.context_menu_visible());
10065 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10066 {
10067 let completion_labels = menu
10068 .completions
10069 .borrow()
10070 .iter()
10071 .map(|c| c.label.text.clone())
10072 .collect::<Vec<_>>();
10073 assert_eq!(
10074 completion_labels,
10075 &[
10076 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10077 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10078 "single line label 2 d e f ",
10079 "a b c g h i ",
10080 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
10081 ],
10082 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
10083 );
10084
10085 for completion in menu
10086 .completions
10087 .borrow()
10088 .iter() {
10089 assert_eq!(
10090 completion.label.filter_range,
10091 0..completion.label.text.len(),
10092 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
10093 );
10094 }
10095 } else {
10096 panic!("expected completion menu to be open");
10097 }
10098 });
10099}
10100
10101#[gpui::test]
10102async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
10103 init_test(cx, |_| {});
10104 let mut cx = EditorLspTestContext::new_rust(
10105 lsp::ServerCapabilities {
10106 completion_provider: Some(lsp::CompletionOptions {
10107 trigger_characters: Some(vec![".".to_string()]),
10108 ..Default::default()
10109 }),
10110 ..Default::default()
10111 },
10112 cx,
10113 )
10114 .await;
10115 cx.lsp
10116 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10117 Ok(Some(lsp::CompletionResponse::Array(vec![
10118 lsp::CompletionItem {
10119 label: "first".into(),
10120 ..Default::default()
10121 },
10122 lsp::CompletionItem {
10123 label: "last".into(),
10124 ..Default::default()
10125 },
10126 ])))
10127 });
10128 cx.set_state("variableˇ");
10129 cx.simulate_keystroke(".");
10130 cx.executor().run_until_parked();
10131
10132 cx.update_editor(|editor, _, _| {
10133 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10134 {
10135 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
10136 } else {
10137 panic!("expected completion menu to be open");
10138 }
10139 });
10140
10141 cx.update_editor(|editor, window, cx| {
10142 editor.move_page_down(&MovePageDown::default(), window, cx);
10143 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10144 {
10145 assert!(
10146 menu.selected_item == 1,
10147 "expected PageDown to select the last item from the context menu"
10148 );
10149 } else {
10150 panic!("expected completion menu to stay open after PageDown");
10151 }
10152 });
10153
10154 cx.update_editor(|editor, window, cx| {
10155 editor.move_page_up(&MovePageUp::default(), window, cx);
10156 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10157 {
10158 assert!(
10159 menu.selected_item == 0,
10160 "expected PageUp to select the first item from the context menu"
10161 );
10162 } else {
10163 panic!("expected completion menu to stay open after PageUp");
10164 }
10165 });
10166}
10167
10168#[gpui::test]
10169async fn test_completion_sort(cx: &mut TestAppContext) {
10170 init_test(cx, |_| {});
10171 let mut cx = EditorLspTestContext::new_rust(
10172 lsp::ServerCapabilities {
10173 completion_provider: Some(lsp::CompletionOptions {
10174 trigger_characters: Some(vec![".".to_string()]),
10175 ..Default::default()
10176 }),
10177 ..Default::default()
10178 },
10179 cx,
10180 )
10181 .await;
10182 cx.lsp
10183 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10184 Ok(Some(lsp::CompletionResponse::Array(vec![
10185 lsp::CompletionItem {
10186 label: "Range".into(),
10187 sort_text: Some("a".into()),
10188 ..Default::default()
10189 },
10190 lsp::CompletionItem {
10191 label: "r".into(),
10192 sort_text: Some("b".into()),
10193 ..Default::default()
10194 },
10195 lsp::CompletionItem {
10196 label: "ret".into(),
10197 sort_text: Some("c".into()),
10198 ..Default::default()
10199 },
10200 lsp::CompletionItem {
10201 label: "return".into(),
10202 sort_text: Some("d".into()),
10203 ..Default::default()
10204 },
10205 lsp::CompletionItem {
10206 label: "slice".into(),
10207 sort_text: Some("d".into()),
10208 ..Default::default()
10209 },
10210 ])))
10211 });
10212 cx.set_state("rˇ");
10213 cx.executor().run_until_parked();
10214 cx.update_editor(|editor, window, cx| {
10215 editor.show_completions(
10216 &ShowCompletions {
10217 trigger: Some("r".into()),
10218 },
10219 window,
10220 cx,
10221 );
10222 });
10223 cx.executor().run_until_parked();
10224
10225 cx.update_editor(|editor, _, _| {
10226 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10227 {
10228 assert_eq!(
10229 completion_menu_entries(&menu),
10230 &["r", "ret", "Range", "return"]
10231 );
10232 } else {
10233 panic!("expected completion menu to be open");
10234 }
10235 });
10236}
10237
10238#[gpui::test]
10239async fn test_as_is_completions(cx: &mut TestAppContext) {
10240 init_test(cx, |_| {});
10241 let mut cx = EditorLspTestContext::new_rust(
10242 lsp::ServerCapabilities {
10243 completion_provider: Some(lsp::CompletionOptions {
10244 ..Default::default()
10245 }),
10246 ..Default::default()
10247 },
10248 cx,
10249 )
10250 .await;
10251 cx.lsp
10252 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10253 Ok(Some(lsp::CompletionResponse::Array(vec![
10254 lsp::CompletionItem {
10255 label: "unsafe".into(),
10256 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10257 range: lsp::Range {
10258 start: lsp::Position {
10259 line: 1,
10260 character: 2,
10261 },
10262 end: lsp::Position {
10263 line: 1,
10264 character: 3,
10265 },
10266 },
10267 new_text: "unsafe".to_string(),
10268 })),
10269 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
10270 ..Default::default()
10271 },
10272 ])))
10273 });
10274 cx.set_state("fn a() {}\n nˇ");
10275 cx.executor().run_until_parked();
10276 cx.update_editor(|editor, window, cx| {
10277 editor.show_completions(
10278 &ShowCompletions {
10279 trigger: Some("\n".into()),
10280 },
10281 window,
10282 cx,
10283 );
10284 });
10285 cx.executor().run_until_parked();
10286
10287 cx.update_editor(|editor, window, cx| {
10288 editor.confirm_completion(&Default::default(), window, cx)
10289 });
10290 cx.executor().run_until_parked();
10291 cx.assert_editor_state("fn a() {}\n unsafeˇ");
10292}
10293
10294#[gpui::test]
10295async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10296 init_test(cx, |_| {});
10297
10298 let mut cx = EditorLspTestContext::new_rust(
10299 lsp::ServerCapabilities {
10300 completion_provider: Some(lsp::CompletionOptions {
10301 trigger_characters: Some(vec![".".to_string()]),
10302 resolve_provider: Some(true),
10303 ..Default::default()
10304 }),
10305 ..Default::default()
10306 },
10307 cx,
10308 )
10309 .await;
10310
10311 cx.set_state("fn main() { let a = 2ˇ; }");
10312 cx.simulate_keystroke(".");
10313 let completion_item = lsp::CompletionItem {
10314 label: "Some".into(),
10315 kind: Some(lsp::CompletionItemKind::SNIPPET),
10316 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10317 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10318 kind: lsp::MarkupKind::Markdown,
10319 value: "```rust\nSome(2)\n```".to_string(),
10320 })),
10321 deprecated: Some(false),
10322 sort_text: Some("Some".to_string()),
10323 filter_text: Some("Some".to_string()),
10324 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10325 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10326 range: lsp::Range {
10327 start: lsp::Position {
10328 line: 0,
10329 character: 22,
10330 },
10331 end: lsp::Position {
10332 line: 0,
10333 character: 22,
10334 },
10335 },
10336 new_text: "Some(2)".to_string(),
10337 })),
10338 additional_text_edits: Some(vec![lsp::TextEdit {
10339 range: lsp::Range {
10340 start: lsp::Position {
10341 line: 0,
10342 character: 20,
10343 },
10344 end: lsp::Position {
10345 line: 0,
10346 character: 22,
10347 },
10348 },
10349 new_text: "".to_string(),
10350 }]),
10351 ..Default::default()
10352 };
10353
10354 let closure_completion_item = completion_item.clone();
10355 let counter = Arc::new(AtomicUsize::new(0));
10356 let counter_clone = counter.clone();
10357 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10358 let task_completion_item = closure_completion_item.clone();
10359 counter_clone.fetch_add(1, atomic::Ordering::Release);
10360 async move {
10361 Ok(Some(lsp::CompletionResponse::Array(vec![
10362 task_completion_item,
10363 ])))
10364 }
10365 });
10366
10367 cx.condition(|editor, _| editor.context_menu_visible())
10368 .await;
10369 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10370 assert!(request.next().await.is_some());
10371 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10372
10373 cx.simulate_keystrokes("S o m");
10374 cx.condition(|editor, _| editor.context_menu_visible())
10375 .await;
10376 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10377 assert!(request.next().await.is_some());
10378 assert!(request.next().await.is_some());
10379 assert!(request.next().await.is_some());
10380 request.close();
10381 assert!(request.next().await.is_none());
10382 assert_eq!(
10383 counter.load(atomic::Ordering::Acquire),
10384 4,
10385 "With the completions menu open, only one LSP request should happen per input"
10386 );
10387}
10388
10389#[gpui::test]
10390async fn test_toggle_comment(cx: &mut TestAppContext) {
10391 init_test(cx, |_| {});
10392 let mut cx = EditorTestContext::new(cx).await;
10393 let language = Arc::new(Language::new(
10394 LanguageConfig {
10395 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10396 ..Default::default()
10397 },
10398 Some(tree_sitter_rust::LANGUAGE.into()),
10399 ));
10400 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10401
10402 // If multiple selections intersect a line, the line is only toggled once.
10403 cx.set_state(indoc! {"
10404 fn a() {
10405 «//b();
10406 ˇ»// «c();
10407 //ˇ» d();
10408 }
10409 "});
10410
10411 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10412
10413 cx.assert_editor_state(indoc! {"
10414 fn a() {
10415 «b();
10416 c();
10417 ˇ» d();
10418 }
10419 "});
10420
10421 // The comment prefix is inserted at the same column for every line in a
10422 // selection.
10423 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10424
10425 cx.assert_editor_state(indoc! {"
10426 fn a() {
10427 // «b();
10428 // c();
10429 ˇ»// d();
10430 }
10431 "});
10432
10433 // If a selection ends at the beginning of a line, that line is not toggled.
10434 cx.set_selections_state(indoc! {"
10435 fn a() {
10436 // b();
10437 «// c();
10438 ˇ» // d();
10439 }
10440 "});
10441
10442 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10443
10444 cx.assert_editor_state(indoc! {"
10445 fn a() {
10446 // b();
10447 «c();
10448 ˇ» // d();
10449 }
10450 "});
10451
10452 // If a selection span a single line and is empty, the line is toggled.
10453 cx.set_state(indoc! {"
10454 fn a() {
10455 a();
10456 b();
10457 ˇ
10458 }
10459 "});
10460
10461 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10462
10463 cx.assert_editor_state(indoc! {"
10464 fn a() {
10465 a();
10466 b();
10467 //•ˇ
10468 }
10469 "});
10470
10471 // If a selection span multiple lines, empty lines are not toggled.
10472 cx.set_state(indoc! {"
10473 fn a() {
10474 «a();
10475
10476 c();ˇ»
10477 }
10478 "});
10479
10480 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10481
10482 cx.assert_editor_state(indoc! {"
10483 fn a() {
10484 // «a();
10485
10486 // c();ˇ»
10487 }
10488 "});
10489
10490 // If a selection includes multiple comment prefixes, all lines are uncommented.
10491 cx.set_state(indoc! {"
10492 fn a() {
10493 «// a();
10494 /// b();
10495 //! c();ˇ»
10496 }
10497 "});
10498
10499 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10500
10501 cx.assert_editor_state(indoc! {"
10502 fn a() {
10503 «a();
10504 b();
10505 c();ˇ»
10506 }
10507 "});
10508}
10509
10510#[gpui::test]
10511async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10512 init_test(cx, |_| {});
10513 let mut cx = EditorTestContext::new(cx).await;
10514 let language = Arc::new(Language::new(
10515 LanguageConfig {
10516 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10517 ..Default::default()
10518 },
10519 Some(tree_sitter_rust::LANGUAGE.into()),
10520 ));
10521 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10522
10523 let toggle_comments = &ToggleComments {
10524 advance_downwards: false,
10525 ignore_indent: true,
10526 };
10527
10528 // If multiple selections intersect a line, the line is only toggled once.
10529 cx.set_state(indoc! {"
10530 fn a() {
10531 // «b();
10532 // c();
10533 // ˇ» d();
10534 }
10535 "});
10536
10537 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10538
10539 cx.assert_editor_state(indoc! {"
10540 fn a() {
10541 «b();
10542 c();
10543 ˇ» d();
10544 }
10545 "});
10546
10547 // The comment prefix is inserted at the beginning of each line
10548 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10549
10550 cx.assert_editor_state(indoc! {"
10551 fn a() {
10552 // «b();
10553 // c();
10554 // ˇ» d();
10555 }
10556 "});
10557
10558 // If a selection ends at the beginning of a line, that line is not toggled.
10559 cx.set_selections_state(indoc! {"
10560 fn a() {
10561 // b();
10562 // «c();
10563 ˇ»// d();
10564 }
10565 "});
10566
10567 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10568
10569 cx.assert_editor_state(indoc! {"
10570 fn a() {
10571 // b();
10572 «c();
10573 ˇ»// d();
10574 }
10575 "});
10576
10577 // If a selection span a single line and is empty, the line is toggled.
10578 cx.set_state(indoc! {"
10579 fn a() {
10580 a();
10581 b();
10582 ˇ
10583 }
10584 "});
10585
10586 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10587
10588 cx.assert_editor_state(indoc! {"
10589 fn a() {
10590 a();
10591 b();
10592 //ˇ
10593 }
10594 "});
10595
10596 // If a selection span multiple lines, empty lines are not toggled.
10597 cx.set_state(indoc! {"
10598 fn a() {
10599 «a();
10600
10601 c();ˇ»
10602 }
10603 "});
10604
10605 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10606
10607 cx.assert_editor_state(indoc! {"
10608 fn a() {
10609 // «a();
10610
10611 // c();ˇ»
10612 }
10613 "});
10614
10615 // If a selection includes multiple comment prefixes, all lines are uncommented.
10616 cx.set_state(indoc! {"
10617 fn a() {
10618 // «a();
10619 /// b();
10620 //! c();ˇ»
10621 }
10622 "});
10623
10624 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10625
10626 cx.assert_editor_state(indoc! {"
10627 fn a() {
10628 «a();
10629 b();
10630 c();ˇ»
10631 }
10632 "});
10633}
10634
10635#[gpui::test]
10636async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10637 init_test(cx, |_| {});
10638
10639 let language = Arc::new(Language::new(
10640 LanguageConfig {
10641 line_comments: vec!["// ".into()],
10642 ..Default::default()
10643 },
10644 Some(tree_sitter_rust::LANGUAGE.into()),
10645 ));
10646
10647 let mut cx = EditorTestContext::new(cx).await;
10648
10649 cx.language_registry().add(language.clone());
10650 cx.update_buffer(|buffer, cx| {
10651 buffer.set_language(Some(language), cx);
10652 });
10653
10654 let toggle_comments = &ToggleComments {
10655 advance_downwards: true,
10656 ignore_indent: false,
10657 };
10658
10659 // Single cursor on one line -> advance
10660 // Cursor moves horizontally 3 characters as well on non-blank line
10661 cx.set_state(indoc!(
10662 "fn a() {
10663 ˇdog();
10664 cat();
10665 }"
10666 ));
10667 cx.update_editor(|editor, window, cx| {
10668 editor.toggle_comments(toggle_comments, window, cx);
10669 });
10670 cx.assert_editor_state(indoc!(
10671 "fn a() {
10672 // dog();
10673 catˇ();
10674 }"
10675 ));
10676
10677 // Single selection on one line -> don't advance
10678 cx.set_state(indoc!(
10679 "fn a() {
10680 «dog()ˇ»;
10681 cat();
10682 }"
10683 ));
10684 cx.update_editor(|editor, window, cx| {
10685 editor.toggle_comments(toggle_comments, window, cx);
10686 });
10687 cx.assert_editor_state(indoc!(
10688 "fn a() {
10689 // «dog()ˇ»;
10690 cat();
10691 }"
10692 ));
10693
10694 // Multiple cursors on one line -> advance
10695 cx.set_state(indoc!(
10696 "fn a() {
10697 ˇdˇog();
10698 cat();
10699 }"
10700 ));
10701 cx.update_editor(|editor, window, cx| {
10702 editor.toggle_comments(toggle_comments, window, cx);
10703 });
10704 cx.assert_editor_state(indoc!(
10705 "fn a() {
10706 // dog();
10707 catˇ(ˇ);
10708 }"
10709 ));
10710
10711 // Multiple cursors on one line, with selection -> don't advance
10712 cx.set_state(indoc!(
10713 "fn a() {
10714 ˇdˇog«()ˇ»;
10715 cat();
10716 }"
10717 ));
10718 cx.update_editor(|editor, window, cx| {
10719 editor.toggle_comments(toggle_comments, window, cx);
10720 });
10721 cx.assert_editor_state(indoc!(
10722 "fn a() {
10723 // ˇdˇog«()ˇ»;
10724 cat();
10725 }"
10726 ));
10727
10728 // Single cursor on one line -> advance
10729 // Cursor moves to column 0 on blank line
10730 cx.set_state(indoc!(
10731 "fn a() {
10732 ˇdog();
10733
10734 cat();
10735 }"
10736 ));
10737 cx.update_editor(|editor, window, cx| {
10738 editor.toggle_comments(toggle_comments, window, cx);
10739 });
10740 cx.assert_editor_state(indoc!(
10741 "fn a() {
10742 // dog();
10743 ˇ
10744 cat();
10745 }"
10746 ));
10747
10748 // Single cursor on one line -> advance
10749 // Cursor starts and ends at column 0
10750 cx.set_state(indoc!(
10751 "fn a() {
10752 ˇ dog();
10753 cat();
10754 }"
10755 ));
10756 cx.update_editor(|editor, window, cx| {
10757 editor.toggle_comments(toggle_comments, window, cx);
10758 });
10759 cx.assert_editor_state(indoc!(
10760 "fn a() {
10761 // dog();
10762 ˇ cat();
10763 }"
10764 ));
10765}
10766
10767#[gpui::test]
10768async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10769 init_test(cx, |_| {});
10770
10771 let mut cx = EditorTestContext::new(cx).await;
10772
10773 let html_language = Arc::new(
10774 Language::new(
10775 LanguageConfig {
10776 name: "HTML".into(),
10777 block_comment: Some(("<!-- ".into(), " -->".into())),
10778 ..Default::default()
10779 },
10780 Some(tree_sitter_html::LANGUAGE.into()),
10781 )
10782 .with_injection_query(
10783 r#"
10784 (script_element
10785 (raw_text) @injection.content
10786 (#set! injection.language "javascript"))
10787 "#,
10788 )
10789 .unwrap(),
10790 );
10791
10792 let javascript_language = Arc::new(Language::new(
10793 LanguageConfig {
10794 name: "JavaScript".into(),
10795 line_comments: vec!["// ".into()],
10796 ..Default::default()
10797 },
10798 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10799 ));
10800
10801 cx.language_registry().add(html_language.clone());
10802 cx.language_registry().add(javascript_language.clone());
10803 cx.update_buffer(|buffer, cx| {
10804 buffer.set_language(Some(html_language), cx);
10805 });
10806
10807 // Toggle comments for empty selections
10808 cx.set_state(
10809 &r#"
10810 <p>A</p>ˇ
10811 <p>B</p>ˇ
10812 <p>C</p>ˇ
10813 "#
10814 .unindent(),
10815 );
10816 cx.update_editor(|editor, window, cx| {
10817 editor.toggle_comments(&ToggleComments::default(), window, cx)
10818 });
10819 cx.assert_editor_state(
10820 &r#"
10821 <!-- <p>A</p>ˇ -->
10822 <!-- <p>B</p>ˇ -->
10823 <!-- <p>C</p>ˇ -->
10824 "#
10825 .unindent(),
10826 );
10827 cx.update_editor(|editor, window, cx| {
10828 editor.toggle_comments(&ToggleComments::default(), window, cx)
10829 });
10830 cx.assert_editor_state(
10831 &r#"
10832 <p>A</p>ˇ
10833 <p>B</p>ˇ
10834 <p>C</p>ˇ
10835 "#
10836 .unindent(),
10837 );
10838
10839 // Toggle comments for mixture of empty and non-empty selections, where
10840 // multiple selections occupy a given line.
10841 cx.set_state(
10842 &r#"
10843 <p>A«</p>
10844 <p>ˇ»B</p>ˇ
10845 <p>C«</p>
10846 <p>ˇ»D</p>ˇ
10847 "#
10848 .unindent(),
10849 );
10850
10851 cx.update_editor(|editor, window, cx| {
10852 editor.toggle_comments(&ToggleComments::default(), window, cx)
10853 });
10854 cx.assert_editor_state(
10855 &r#"
10856 <!-- <p>A«</p>
10857 <p>ˇ»B</p>ˇ -->
10858 <!-- <p>C«</p>
10859 <p>ˇ»D</p>ˇ -->
10860 "#
10861 .unindent(),
10862 );
10863 cx.update_editor(|editor, window, cx| {
10864 editor.toggle_comments(&ToggleComments::default(), window, cx)
10865 });
10866 cx.assert_editor_state(
10867 &r#"
10868 <p>A«</p>
10869 <p>ˇ»B</p>ˇ
10870 <p>C«</p>
10871 <p>ˇ»D</p>ˇ
10872 "#
10873 .unindent(),
10874 );
10875
10876 // Toggle comments when different languages are active for different
10877 // selections.
10878 cx.set_state(
10879 &r#"
10880 ˇ<script>
10881 ˇvar x = new Y();
10882 ˇ</script>
10883 "#
10884 .unindent(),
10885 );
10886 cx.executor().run_until_parked();
10887 cx.update_editor(|editor, window, cx| {
10888 editor.toggle_comments(&ToggleComments::default(), window, cx)
10889 });
10890 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
10891 // Uncommenting and commenting from this position brings in even more wrong artifacts.
10892 cx.assert_editor_state(
10893 &r#"
10894 <!-- ˇ<script> -->
10895 // ˇvar x = new Y();
10896 <!-- ˇ</script> -->
10897 "#
10898 .unindent(),
10899 );
10900}
10901
10902#[gpui::test]
10903fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
10904 init_test(cx, |_| {});
10905
10906 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
10907 let multibuffer = cx.new(|cx| {
10908 let mut multibuffer = MultiBuffer::new(ReadWrite);
10909 multibuffer.push_excerpts(
10910 buffer.clone(),
10911 [
10912 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
10913 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
10914 ],
10915 cx,
10916 );
10917 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
10918 multibuffer
10919 });
10920
10921 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10922 editor.update_in(cx, |editor, window, cx| {
10923 assert_eq!(editor.text(cx), "aaaa\nbbbb");
10924 editor.change_selections(None, window, cx, |s| {
10925 s.select_ranges([
10926 Point::new(0, 0)..Point::new(0, 0),
10927 Point::new(1, 0)..Point::new(1, 0),
10928 ])
10929 });
10930
10931 editor.handle_input("X", window, cx);
10932 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
10933 assert_eq!(
10934 editor.selections.ranges(cx),
10935 [
10936 Point::new(0, 1)..Point::new(0, 1),
10937 Point::new(1, 1)..Point::new(1, 1),
10938 ]
10939 );
10940
10941 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
10942 editor.change_selections(None, window, cx, |s| {
10943 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
10944 });
10945 editor.backspace(&Default::default(), window, cx);
10946 assert_eq!(editor.text(cx), "Xa\nbbb");
10947 assert_eq!(
10948 editor.selections.ranges(cx),
10949 [Point::new(1, 0)..Point::new(1, 0)]
10950 );
10951
10952 editor.change_selections(None, window, cx, |s| {
10953 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
10954 });
10955 editor.backspace(&Default::default(), window, cx);
10956 assert_eq!(editor.text(cx), "X\nbb");
10957 assert_eq!(
10958 editor.selections.ranges(cx),
10959 [Point::new(0, 1)..Point::new(0, 1)]
10960 );
10961 });
10962}
10963
10964#[gpui::test]
10965fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
10966 init_test(cx, |_| {});
10967
10968 let markers = vec![('[', ']').into(), ('(', ')').into()];
10969 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
10970 indoc! {"
10971 [aaaa
10972 (bbbb]
10973 cccc)",
10974 },
10975 markers.clone(),
10976 );
10977 let excerpt_ranges = markers.into_iter().map(|marker| {
10978 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
10979 ExcerptRange::new(context.clone())
10980 });
10981 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
10982 let multibuffer = cx.new(|cx| {
10983 let mut multibuffer = MultiBuffer::new(ReadWrite);
10984 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
10985 multibuffer
10986 });
10987
10988 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
10989 editor.update_in(cx, |editor, window, cx| {
10990 let (expected_text, selection_ranges) = marked_text_ranges(
10991 indoc! {"
10992 aaaa
10993 bˇbbb
10994 bˇbbˇb
10995 cccc"
10996 },
10997 true,
10998 );
10999 assert_eq!(editor.text(cx), expected_text);
11000 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11001
11002 editor.handle_input("X", window, cx);
11003
11004 let (expected_text, expected_selections) = marked_text_ranges(
11005 indoc! {"
11006 aaaa
11007 bXˇbbXb
11008 bXˇbbXˇb
11009 cccc"
11010 },
11011 false,
11012 );
11013 assert_eq!(editor.text(cx), expected_text);
11014 assert_eq!(editor.selections.ranges(cx), expected_selections);
11015
11016 editor.newline(&Newline, window, cx);
11017 let (expected_text, expected_selections) = marked_text_ranges(
11018 indoc! {"
11019 aaaa
11020 bX
11021 ˇbbX
11022 b
11023 bX
11024 ˇbbX
11025 ˇb
11026 cccc"
11027 },
11028 false,
11029 );
11030 assert_eq!(editor.text(cx), expected_text);
11031 assert_eq!(editor.selections.ranges(cx), expected_selections);
11032 });
11033}
11034
11035#[gpui::test]
11036fn test_refresh_selections(cx: &mut TestAppContext) {
11037 init_test(cx, |_| {});
11038
11039 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11040 let mut excerpt1_id = None;
11041 let multibuffer = cx.new(|cx| {
11042 let mut multibuffer = MultiBuffer::new(ReadWrite);
11043 excerpt1_id = multibuffer
11044 .push_excerpts(
11045 buffer.clone(),
11046 [
11047 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11048 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11049 ],
11050 cx,
11051 )
11052 .into_iter()
11053 .next();
11054 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11055 multibuffer
11056 });
11057
11058 let editor = cx.add_window(|window, cx| {
11059 let mut editor = build_editor(multibuffer.clone(), window, cx);
11060 let snapshot = editor.snapshot(window, cx);
11061 editor.change_selections(None, window, cx, |s| {
11062 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11063 });
11064 editor.begin_selection(
11065 Point::new(2, 1).to_display_point(&snapshot),
11066 true,
11067 1,
11068 window,
11069 cx,
11070 );
11071 assert_eq!(
11072 editor.selections.ranges(cx),
11073 [
11074 Point::new(1, 3)..Point::new(1, 3),
11075 Point::new(2, 1)..Point::new(2, 1),
11076 ]
11077 );
11078 editor
11079 });
11080
11081 // Refreshing selections is a no-op when excerpts haven't changed.
11082 _ = editor.update(cx, |editor, window, cx| {
11083 editor.change_selections(None, window, cx, |s| s.refresh());
11084 assert_eq!(
11085 editor.selections.ranges(cx),
11086 [
11087 Point::new(1, 3)..Point::new(1, 3),
11088 Point::new(2, 1)..Point::new(2, 1),
11089 ]
11090 );
11091 });
11092
11093 multibuffer.update(cx, |multibuffer, cx| {
11094 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11095 });
11096 _ = editor.update(cx, |editor, window, cx| {
11097 // Removing an excerpt causes the first selection to become degenerate.
11098 assert_eq!(
11099 editor.selections.ranges(cx),
11100 [
11101 Point::new(0, 0)..Point::new(0, 0),
11102 Point::new(0, 1)..Point::new(0, 1)
11103 ]
11104 );
11105
11106 // Refreshing selections will relocate the first selection to the original buffer
11107 // location.
11108 editor.change_selections(None, window, cx, |s| s.refresh());
11109 assert_eq!(
11110 editor.selections.ranges(cx),
11111 [
11112 Point::new(0, 1)..Point::new(0, 1),
11113 Point::new(0, 3)..Point::new(0, 3)
11114 ]
11115 );
11116 assert!(editor.selections.pending_anchor().is_some());
11117 });
11118}
11119
11120#[gpui::test]
11121fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
11122 init_test(cx, |_| {});
11123
11124 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11125 let mut excerpt1_id = None;
11126 let multibuffer = cx.new(|cx| {
11127 let mut multibuffer = MultiBuffer::new(ReadWrite);
11128 excerpt1_id = multibuffer
11129 .push_excerpts(
11130 buffer.clone(),
11131 [
11132 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11133 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11134 ],
11135 cx,
11136 )
11137 .into_iter()
11138 .next();
11139 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11140 multibuffer
11141 });
11142
11143 let editor = cx.add_window(|window, cx| {
11144 let mut editor = build_editor(multibuffer.clone(), window, cx);
11145 let snapshot = editor.snapshot(window, cx);
11146 editor.begin_selection(
11147 Point::new(1, 3).to_display_point(&snapshot),
11148 false,
11149 1,
11150 window,
11151 cx,
11152 );
11153 assert_eq!(
11154 editor.selections.ranges(cx),
11155 [Point::new(1, 3)..Point::new(1, 3)]
11156 );
11157 editor
11158 });
11159
11160 multibuffer.update(cx, |multibuffer, cx| {
11161 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11162 });
11163 _ = editor.update(cx, |editor, window, cx| {
11164 assert_eq!(
11165 editor.selections.ranges(cx),
11166 [Point::new(0, 0)..Point::new(0, 0)]
11167 );
11168
11169 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
11170 editor.change_selections(None, window, cx, |s| s.refresh());
11171 assert_eq!(
11172 editor.selections.ranges(cx),
11173 [Point::new(0, 3)..Point::new(0, 3)]
11174 );
11175 assert!(editor.selections.pending_anchor().is_some());
11176 });
11177}
11178
11179#[gpui::test]
11180async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
11181 init_test(cx, |_| {});
11182
11183 let language = Arc::new(
11184 Language::new(
11185 LanguageConfig {
11186 brackets: BracketPairConfig {
11187 pairs: vec![
11188 BracketPair {
11189 start: "{".to_string(),
11190 end: "}".to_string(),
11191 close: true,
11192 surround: true,
11193 newline: true,
11194 },
11195 BracketPair {
11196 start: "/* ".to_string(),
11197 end: " */".to_string(),
11198 close: true,
11199 surround: true,
11200 newline: true,
11201 },
11202 ],
11203 ..Default::default()
11204 },
11205 ..Default::default()
11206 },
11207 Some(tree_sitter_rust::LANGUAGE.into()),
11208 )
11209 .with_indents_query("")
11210 .unwrap(),
11211 );
11212
11213 let text = concat!(
11214 "{ }\n", //
11215 " x\n", //
11216 " /* */\n", //
11217 "x\n", //
11218 "{{} }\n", //
11219 );
11220
11221 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11222 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11223 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11224 editor
11225 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11226 .await;
11227
11228 editor.update_in(cx, |editor, window, cx| {
11229 editor.change_selections(None, window, cx, |s| {
11230 s.select_display_ranges([
11231 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11232 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11233 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11234 ])
11235 });
11236 editor.newline(&Newline, window, cx);
11237
11238 assert_eq!(
11239 editor.buffer().read(cx).read(cx).text(),
11240 concat!(
11241 "{ \n", // Suppress rustfmt
11242 "\n", //
11243 "}\n", //
11244 " x\n", //
11245 " /* \n", //
11246 " \n", //
11247 " */\n", //
11248 "x\n", //
11249 "{{} \n", //
11250 "}\n", //
11251 )
11252 );
11253 });
11254}
11255
11256#[gpui::test]
11257fn test_highlighted_ranges(cx: &mut TestAppContext) {
11258 init_test(cx, |_| {});
11259
11260 let editor = cx.add_window(|window, cx| {
11261 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11262 build_editor(buffer.clone(), window, cx)
11263 });
11264
11265 _ = editor.update(cx, |editor, window, cx| {
11266 struct Type1;
11267 struct Type2;
11268
11269 let buffer = editor.buffer.read(cx).snapshot(cx);
11270
11271 let anchor_range =
11272 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11273
11274 editor.highlight_background::<Type1>(
11275 &[
11276 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11277 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11278 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11279 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11280 ],
11281 |_| Hsla::red(),
11282 cx,
11283 );
11284 editor.highlight_background::<Type2>(
11285 &[
11286 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11287 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11288 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11289 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11290 ],
11291 |_| Hsla::green(),
11292 cx,
11293 );
11294
11295 let snapshot = editor.snapshot(window, cx);
11296 let mut highlighted_ranges = editor.background_highlights_in_range(
11297 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11298 &snapshot,
11299 cx.theme().colors(),
11300 );
11301 // Enforce a consistent ordering based on color without relying on the ordering of the
11302 // highlight's `TypeId` which is non-executor.
11303 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11304 assert_eq!(
11305 highlighted_ranges,
11306 &[
11307 (
11308 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11309 Hsla::red(),
11310 ),
11311 (
11312 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11313 Hsla::red(),
11314 ),
11315 (
11316 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11317 Hsla::green(),
11318 ),
11319 (
11320 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11321 Hsla::green(),
11322 ),
11323 ]
11324 );
11325 assert_eq!(
11326 editor.background_highlights_in_range(
11327 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11328 &snapshot,
11329 cx.theme().colors(),
11330 ),
11331 &[(
11332 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11333 Hsla::red(),
11334 )]
11335 );
11336 });
11337}
11338
11339#[gpui::test]
11340async fn test_following(cx: &mut TestAppContext) {
11341 init_test(cx, |_| {});
11342
11343 let fs = FakeFs::new(cx.executor());
11344 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11345
11346 let buffer = project.update(cx, |project, cx| {
11347 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11348 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11349 });
11350 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11351 let follower = cx.update(|cx| {
11352 cx.open_window(
11353 WindowOptions {
11354 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11355 gpui::Point::new(px(0.), px(0.)),
11356 gpui::Point::new(px(10.), px(80.)),
11357 ))),
11358 ..Default::default()
11359 },
11360 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11361 )
11362 .unwrap()
11363 });
11364
11365 let is_still_following = Rc::new(RefCell::new(true));
11366 let follower_edit_event_count = Rc::new(RefCell::new(0));
11367 let pending_update = Rc::new(RefCell::new(None));
11368 let leader_entity = leader.root(cx).unwrap();
11369 let follower_entity = follower.root(cx).unwrap();
11370 _ = follower.update(cx, {
11371 let update = pending_update.clone();
11372 let is_still_following = is_still_following.clone();
11373 let follower_edit_event_count = follower_edit_event_count.clone();
11374 |_, window, cx| {
11375 cx.subscribe_in(
11376 &leader_entity,
11377 window,
11378 move |_, leader, event, window, cx| {
11379 leader.read(cx).add_event_to_update_proto(
11380 event,
11381 &mut update.borrow_mut(),
11382 window,
11383 cx,
11384 );
11385 },
11386 )
11387 .detach();
11388
11389 cx.subscribe_in(
11390 &follower_entity,
11391 window,
11392 move |_, _, event: &EditorEvent, _window, _cx| {
11393 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11394 *is_still_following.borrow_mut() = false;
11395 }
11396
11397 if let EditorEvent::BufferEdited = event {
11398 *follower_edit_event_count.borrow_mut() += 1;
11399 }
11400 },
11401 )
11402 .detach();
11403 }
11404 });
11405
11406 // Update the selections only
11407 _ = leader.update(cx, |leader, window, cx| {
11408 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11409 });
11410 follower
11411 .update(cx, |follower, window, cx| {
11412 follower.apply_update_proto(
11413 &project,
11414 pending_update.borrow_mut().take().unwrap(),
11415 window,
11416 cx,
11417 )
11418 })
11419 .unwrap()
11420 .await
11421 .unwrap();
11422 _ = follower.update(cx, |follower, _, cx| {
11423 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11424 });
11425 assert!(*is_still_following.borrow());
11426 assert_eq!(*follower_edit_event_count.borrow(), 0);
11427
11428 // Update the scroll position only
11429 _ = leader.update(cx, |leader, window, cx| {
11430 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11431 });
11432 follower
11433 .update(cx, |follower, window, cx| {
11434 follower.apply_update_proto(
11435 &project,
11436 pending_update.borrow_mut().take().unwrap(),
11437 window,
11438 cx,
11439 )
11440 })
11441 .unwrap()
11442 .await
11443 .unwrap();
11444 assert_eq!(
11445 follower
11446 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11447 .unwrap(),
11448 gpui::Point::new(1.5, 3.5)
11449 );
11450 assert!(*is_still_following.borrow());
11451 assert_eq!(*follower_edit_event_count.borrow(), 0);
11452
11453 // Update the selections and scroll position. The follower's scroll position is updated
11454 // via autoscroll, not via the leader's exact scroll position.
11455 _ = leader.update(cx, |leader, window, cx| {
11456 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11457 leader.request_autoscroll(Autoscroll::newest(), cx);
11458 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11459 });
11460 follower
11461 .update(cx, |follower, window, cx| {
11462 follower.apply_update_proto(
11463 &project,
11464 pending_update.borrow_mut().take().unwrap(),
11465 window,
11466 cx,
11467 )
11468 })
11469 .unwrap()
11470 .await
11471 .unwrap();
11472 _ = follower.update(cx, |follower, _, cx| {
11473 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11474 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11475 });
11476 assert!(*is_still_following.borrow());
11477
11478 // Creating a pending selection that precedes another selection
11479 _ = leader.update(cx, |leader, window, cx| {
11480 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11481 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11482 });
11483 follower
11484 .update(cx, |follower, window, cx| {
11485 follower.apply_update_proto(
11486 &project,
11487 pending_update.borrow_mut().take().unwrap(),
11488 window,
11489 cx,
11490 )
11491 })
11492 .unwrap()
11493 .await
11494 .unwrap();
11495 _ = follower.update(cx, |follower, _, cx| {
11496 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11497 });
11498 assert!(*is_still_following.borrow());
11499
11500 // Extend the pending selection so that it surrounds another selection
11501 _ = leader.update(cx, |leader, window, cx| {
11502 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11503 });
11504 follower
11505 .update(cx, |follower, window, cx| {
11506 follower.apply_update_proto(
11507 &project,
11508 pending_update.borrow_mut().take().unwrap(),
11509 window,
11510 cx,
11511 )
11512 })
11513 .unwrap()
11514 .await
11515 .unwrap();
11516 _ = follower.update(cx, |follower, _, cx| {
11517 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11518 });
11519
11520 // Scrolling locally breaks the follow
11521 _ = follower.update(cx, |follower, window, cx| {
11522 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11523 follower.set_scroll_anchor(
11524 ScrollAnchor {
11525 anchor: top_anchor,
11526 offset: gpui::Point::new(0.0, 0.5),
11527 },
11528 window,
11529 cx,
11530 );
11531 });
11532 assert!(!(*is_still_following.borrow()));
11533}
11534
11535#[gpui::test]
11536async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11537 init_test(cx, |_| {});
11538
11539 let fs = FakeFs::new(cx.executor());
11540 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11541 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11542 let pane = workspace
11543 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11544 .unwrap();
11545
11546 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11547
11548 let leader = pane.update_in(cx, |_, window, cx| {
11549 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11550 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11551 });
11552
11553 // Start following the editor when it has no excerpts.
11554 let mut state_message =
11555 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11556 let workspace_entity = workspace.root(cx).unwrap();
11557 let follower_1 = cx
11558 .update_window(*workspace.deref(), |_, window, cx| {
11559 Editor::from_state_proto(
11560 workspace_entity,
11561 ViewId {
11562 creator: Default::default(),
11563 id: 0,
11564 },
11565 &mut state_message,
11566 window,
11567 cx,
11568 )
11569 })
11570 .unwrap()
11571 .unwrap()
11572 .await
11573 .unwrap();
11574
11575 let update_message = Rc::new(RefCell::new(None));
11576 follower_1.update_in(cx, {
11577 let update = update_message.clone();
11578 |_, window, cx| {
11579 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11580 leader.read(cx).add_event_to_update_proto(
11581 event,
11582 &mut update.borrow_mut(),
11583 window,
11584 cx,
11585 );
11586 })
11587 .detach();
11588 }
11589 });
11590
11591 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11592 (
11593 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11594 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11595 )
11596 });
11597
11598 // Insert some excerpts.
11599 leader.update(cx, |leader, cx| {
11600 leader.buffer.update(cx, |multibuffer, cx| {
11601 let excerpt_ids = multibuffer.push_excerpts(
11602 buffer_1.clone(),
11603 [
11604 ExcerptRange::new(1..6),
11605 ExcerptRange::new(12..15),
11606 ExcerptRange::new(0..3),
11607 ],
11608 cx,
11609 );
11610 multibuffer.insert_excerpts_after(
11611 excerpt_ids[0],
11612 buffer_2.clone(),
11613 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
11614 cx,
11615 );
11616 });
11617 });
11618
11619 // Apply the update of adding the excerpts.
11620 follower_1
11621 .update_in(cx, |follower, window, cx| {
11622 follower.apply_update_proto(
11623 &project,
11624 update_message.borrow().clone().unwrap(),
11625 window,
11626 cx,
11627 )
11628 })
11629 .await
11630 .unwrap();
11631 assert_eq!(
11632 follower_1.update(cx, |editor, cx| editor.text(cx)),
11633 leader.update(cx, |editor, cx| editor.text(cx))
11634 );
11635 update_message.borrow_mut().take();
11636
11637 // Start following separately after it already has excerpts.
11638 let mut state_message =
11639 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11640 let workspace_entity = workspace.root(cx).unwrap();
11641 let follower_2 = cx
11642 .update_window(*workspace.deref(), |_, window, cx| {
11643 Editor::from_state_proto(
11644 workspace_entity,
11645 ViewId {
11646 creator: Default::default(),
11647 id: 0,
11648 },
11649 &mut state_message,
11650 window,
11651 cx,
11652 )
11653 })
11654 .unwrap()
11655 .unwrap()
11656 .await
11657 .unwrap();
11658 assert_eq!(
11659 follower_2.update(cx, |editor, cx| editor.text(cx)),
11660 leader.update(cx, |editor, cx| editor.text(cx))
11661 );
11662
11663 // Remove some excerpts.
11664 leader.update(cx, |leader, cx| {
11665 leader.buffer.update(cx, |multibuffer, cx| {
11666 let excerpt_ids = multibuffer.excerpt_ids();
11667 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11668 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11669 });
11670 });
11671
11672 // Apply the update of removing the excerpts.
11673 follower_1
11674 .update_in(cx, |follower, window, cx| {
11675 follower.apply_update_proto(
11676 &project,
11677 update_message.borrow().clone().unwrap(),
11678 window,
11679 cx,
11680 )
11681 })
11682 .await
11683 .unwrap();
11684 follower_2
11685 .update_in(cx, |follower, window, cx| {
11686 follower.apply_update_proto(
11687 &project,
11688 update_message.borrow().clone().unwrap(),
11689 window,
11690 cx,
11691 )
11692 })
11693 .await
11694 .unwrap();
11695 update_message.borrow_mut().take();
11696 assert_eq!(
11697 follower_1.update(cx, |editor, cx| editor.text(cx)),
11698 leader.update(cx, |editor, cx| editor.text(cx))
11699 );
11700}
11701
11702#[gpui::test]
11703async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11704 init_test(cx, |_| {});
11705
11706 let mut cx = EditorTestContext::new(cx).await;
11707 let lsp_store =
11708 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11709
11710 cx.set_state(indoc! {"
11711 ˇfn func(abc def: i32) -> u32 {
11712 }
11713 "});
11714
11715 cx.update(|_, cx| {
11716 lsp_store.update(cx, |lsp_store, cx| {
11717 lsp_store
11718 .update_diagnostics(
11719 LanguageServerId(0),
11720 lsp::PublishDiagnosticsParams {
11721 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11722 version: None,
11723 diagnostics: vec![
11724 lsp::Diagnostic {
11725 range: lsp::Range::new(
11726 lsp::Position::new(0, 11),
11727 lsp::Position::new(0, 12),
11728 ),
11729 severity: Some(lsp::DiagnosticSeverity::ERROR),
11730 ..Default::default()
11731 },
11732 lsp::Diagnostic {
11733 range: lsp::Range::new(
11734 lsp::Position::new(0, 12),
11735 lsp::Position::new(0, 15),
11736 ),
11737 severity: Some(lsp::DiagnosticSeverity::ERROR),
11738 ..Default::default()
11739 },
11740 lsp::Diagnostic {
11741 range: lsp::Range::new(
11742 lsp::Position::new(0, 25),
11743 lsp::Position::new(0, 28),
11744 ),
11745 severity: Some(lsp::DiagnosticSeverity::ERROR),
11746 ..Default::default()
11747 },
11748 ],
11749 },
11750 &[],
11751 cx,
11752 )
11753 .unwrap()
11754 });
11755 });
11756
11757 executor.run_until_parked();
11758
11759 cx.update_editor(|editor, window, cx| {
11760 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11761 });
11762
11763 cx.assert_editor_state(indoc! {"
11764 fn func(abc def: i32) -> ˇu32 {
11765 }
11766 "});
11767
11768 cx.update_editor(|editor, window, cx| {
11769 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11770 });
11771
11772 cx.assert_editor_state(indoc! {"
11773 fn func(abc ˇdef: i32) -> u32 {
11774 }
11775 "});
11776
11777 cx.update_editor(|editor, window, cx| {
11778 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11779 });
11780
11781 cx.assert_editor_state(indoc! {"
11782 fn func(abcˇ def: i32) -> u32 {
11783 }
11784 "});
11785
11786 cx.update_editor(|editor, window, cx| {
11787 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11788 });
11789
11790 cx.assert_editor_state(indoc! {"
11791 fn func(abc def: i32) -> ˇu32 {
11792 }
11793 "});
11794}
11795
11796#[gpui::test]
11797async fn cycle_through_same_place_diagnostics(
11798 executor: BackgroundExecutor,
11799 cx: &mut TestAppContext,
11800) {
11801 init_test(cx, |_| {});
11802
11803 let mut cx = EditorTestContext::new(cx).await;
11804 let lsp_store =
11805 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11806
11807 cx.set_state(indoc! {"
11808 ˇfn func(abc def: i32) -> u32 {
11809 }
11810 "});
11811
11812 cx.update(|_, cx| {
11813 lsp_store.update(cx, |lsp_store, cx| {
11814 lsp_store
11815 .update_diagnostics(
11816 LanguageServerId(0),
11817 lsp::PublishDiagnosticsParams {
11818 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11819 version: None,
11820 diagnostics: vec![
11821 lsp::Diagnostic {
11822 range: lsp::Range::new(
11823 lsp::Position::new(0, 11),
11824 lsp::Position::new(0, 12),
11825 ),
11826 severity: Some(lsp::DiagnosticSeverity::ERROR),
11827 ..Default::default()
11828 },
11829 lsp::Diagnostic {
11830 range: lsp::Range::new(
11831 lsp::Position::new(0, 12),
11832 lsp::Position::new(0, 15),
11833 ),
11834 severity: Some(lsp::DiagnosticSeverity::ERROR),
11835 ..Default::default()
11836 },
11837 lsp::Diagnostic {
11838 range: lsp::Range::new(
11839 lsp::Position::new(0, 12),
11840 lsp::Position::new(0, 15),
11841 ),
11842 severity: Some(lsp::DiagnosticSeverity::ERROR),
11843 ..Default::default()
11844 },
11845 lsp::Diagnostic {
11846 range: lsp::Range::new(
11847 lsp::Position::new(0, 25),
11848 lsp::Position::new(0, 28),
11849 ),
11850 severity: Some(lsp::DiagnosticSeverity::ERROR),
11851 ..Default::default()
11852 },
11853 ],
11854 },
11855 &[],
11856 cx,
11857 )
11858 .unwrap()
11859 });
11860 });
11861 executor.run_until_parked();
11862
11863 //// Backward
11864
11865 // Fourth diagnostic
11866 cx.update_editor(|editor, window, cx| {
11867 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11868 });
11869 cx.assert_editor_state(indoc! {"
11870 fn func(abc def: i32) -> ˇu32 {
11871 }
11872 "});
11873
11874 // Third diagnostic
11875 cx.update_editor(|editor, window, cx| {
11876 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11877 });
11878 cx.assert_editor_state(indoc! {"
11879 fn func(abc ˇdef: i32) -> u32 {
11880 }
11881 "});
11882
11883 // Second diagnostic, same place
11884 cx.update_editor(|editor, window, cx| {
11885 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11886 });
11887 cx.assert_editor_state(indoc! {"
11888 fn func(abc ˇdef: i32) -> u32 {
11889 }
11890 "});
11891
11892 // First diagnostic
11893 cx.update_editor(|editor, window, cx| {
11894 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11895 });
11896 cx.assert_editor_state(indoc! {"
11897 fn func(abcˇ def: i32) -> u32 {
11898 }
11899 "});
11900
11901 // Wrapped over, fourth diagnostic
11902 cx.update_editor(|editor, window, cx| {
11903 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11904 });
11905 cx.assert_editor_state(indoc! {"
11906 fn func(abc def: i32) -> ˇu32 {
11907 }
11908 "});
11909
11910 cx.update_editor(|editor, window, cx| {
11911 editor.move_to_beginning(&MoveToBeginning, window, cx);
11912 });
11913 cx.assert_editor_state(indoc! {"
11914 ˇfn func(abc def: i32) -> u32 {
11915 }
11916 "});
11917
11918 //// Forward
11919
11920 // First diagnostic
11921 cx.update_editor(|editor, window, cx| {
11922 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11923 });
11924 cx.assert_editor_state(indoc! {"
11925 fn func(abcˇ def: i32) -> u32 {
11926 }
11927 "});
11928
11929 // Second diagnostic
11930 cx.update_editor(|editor, window, cx| {
11931 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11932 });
11933 cx.assert_editor_state(indoc! {"
11934 fn func(abc ˇdef: i32) -> u32 {
11935 }
11936 "});
11937
11938 // Third diagnostic, same place
11939 cx.update_editor(|editor, window, cx| {
11940 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11941 });
11942 cx.assert_editor_state(indoc! {"
11943 fn func(abc ˇdef: i32) -> u32 {
11944 }
11945 "});
11946
11947 // Fourth diagnostic
11948 cx.update_editor(|editor, window, cx| {
11949 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11950 });
11951 cx.assert_editor_state(indoc! {"
11952 fn func(abc def: i32) -> ˇu32 {
11953 }
11954 "});
11955
11956 // Wrapped around, first diagnostic
11957 cx.update_editor(|editor, window, cx| {
11958 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
11959 });
11960 cx.assert_editor_state(indoc! {"
11961 fn func(abcˇ def: i32) -> u32 {
11962 }
11963 "});
11964}
11965
11966#[gpui::test]
11967async fn active_diagnostics_dismiss_after_invalidation(
11968 executor: BackgroundExecutor,
11969 cx: &mut TestAppContext,
11970) {
11971 init_test(cx, |_| {});
11972
11973 let mut cx = EditorTestContext::new(cx).await;
11974 let lsp_store =
11975 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11976
11977 cx.set_state(indoc! {"
11978 ˇfn func(abc def: i32) -> u32 {
11979 }
11980 "});
11981
11982 let message = "Something's wrong!";
11983 cx.update(|_, cx| {
11984 lsp_store.update(cx, |lsp_store, cx| {
11985 lsp_store
11986 .update_diagnostics(
11987 LanguageServerId(0),
11988 lsp::PublishDiagnosticsParams {
11989 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11990 version: None,
11991 diagnostics: vec![lsp::Diagnostic {
11992 range: lsp::Range::new(
11993 lsp::Position::new(0, 11),
11994 lsp::Position::new(0, 12),
11995 ),
11996 severity: Some(lsp::DiagnosticSeverity::ERROR),
11997 message: message.to_string(),
11998 ..Default::default()
11999 }],
12000 },
12001 &[],
12002 cx,
12003 )
12004 .unwrap()
12005 });
12006 });
12007 executor.run_until_parked();
12008
12009 cx.update_editor(|editor, window, cx| {
12010 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12011 assert_eq!(
12012 editor
12013 .active_diagnostics
12014 .as_ref()
12015 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
12016 Some(message),
12017 "Should have a diagnostics group activated"
12018 );
12019 });
12020 cx.assert_editor_state(indoc! {"
12021 fn func(abcˇ def: i32) -> u32 {
12022 }
12023 "});
12024
12025 cx.update(|_, cx| {
12026 lsp_store.update(cx, |lsp_store, cx| {
12027 lsp_store
12028 .update_diagnostics(
12029 LanguageServerId(0),
12030 lsp::PublishDiagnosticsParams {
12031 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12032 version: None,
12033 diagnostics: Vec::new(),
12034 },
12035 &[],
12036 cx,
12037 )
12038 .unwrap()
12039 });
12040 });
12041 executor.run_until_parked();
12042 cx.update_editor(|editor, _, _| {
12043 assert_eq!(
12044 editor.active_diagnostics, None,
12045 "After no diagnostics set to the editor, no diagnostics should be active"
12046 );
12047 });
12048 cx.assert_editor_state(indoc! {"
12049 fn func(abcˇ def: i32) -> u32 {
12050 }
12051 "});
12052
12053 cx.update_editor(|editor, window, cx| {
12054 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12055 assert_eq!(
12056 editor.active_diagnostics, None,
12057 "Should be no diagnostics to go to and activate"
12058 );
12059 });
12060 cx.assert_editor_state(indoc! {"
12061 fn func(abcˇ def: i32) -> u32 {
12062 }
12063 "});
12064}
12065
12066#[gpui::test]
12067async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12068 init_test(cx, |_| {});
12069
12070 let mut cx = EditorTestContext::new(cx).await;
12071
12072 cx.set_state(indoc! {"
12073 fn func(abˇc def: i32) -> u32 {
12074 }
12075 "});
12076 let lsp_store =
12077 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12078
12079 cx.update(|_, cx| {
12080 lsp_store.update(cx, |lsp_store, cx| {
12081 lsp_store.update_diagnostics(
12082 LanguageServerId(0),
12083 lsp::PublishDiagnosticsParams {
12084 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12085 version: None,
12086 diagnostics: vec![lsp::Diagnostic {
12087 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12088 severity: Some(lsp::DiagnosticSeverity::ERROR),
12089 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12090 ..Default::default()
12091 }],
12092 },
12093 &[],
12094 cx,
12095 )
12096 })
12097 }).unwrap();
12098 cx.run_until_parked();
12099 cx.update_editor(|editor, window, cx| {
12100 hover_popover::hover(editor, &Default::default(), window, cx)
12101 });
12102 cx.run_until_parked();
12103 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12104}
12105
12106#[gpui::test]
12107async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12108 init_test(cx, |_| {});
12109
12110 let mut cx = EditorTestContext::new(cx).await;
12111
12112 let diff_base = r#"
12113 use some::mod;
12114
12115 const A: u32 = 42;
12116
12117 fn main() {
12118 println!("hello");
12119
12120 println!("world");
12121 }
12122 "#
12123 .unindent();
12124
12125 // Edits are modified, removed, modified, added
12126 cx.set_state(
12127 &r#"
12128 use some::modified;
12129
12130 ˇ
12131 fn main() {
12132 println!("hello there");
12133
12134 println!("around the");
12135 println!("world");
12136 }
12137 "#
12138 .unindent(),
12139 );
12140
12141 cx.set_head_text(&diff_base);
12142 executor.run_until_parked();
12143
12144 cx.update_editor(|editor, window, cx| {
12145 //Wrap around the bottom of the buffer
12146 for _ in 0..3 {
12147 editor.go_to_next_hunk(&GoToHunk, window, cx);
12148 }
12149 });
12150
12151 cx.assert_editor_state(
12152 &r#"
12153 ˇuse some::modified;
12154
12155
12156 fn main() {
12157 println!("hello there");
12158
12159 println!("around the");
12160 println!("world");
12161 }
12162 "#
12163 .unindent(),
12164 );
12165
12166 cx.update_editor(|editor, window, cx| {
12167 //Wrap around the top of the buffer
12168 for _ in 0..2 {
12169 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12170 }
12171 });
12172
12173 cx.assert_editor_state(
12174 &r#"
12175 use some::modified;
12176
12177
12178 fn main() {
12179 ˇ println!("hello there");
12180
12181 println!("around the");
12182 println!("world");
12183 }
12184 "#
12185 .unindent(),
12186 );
12187
12188 cx.update_editor(|editor, window, cx| {
12189 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12190 });
12191
12192 cx.assert_editor_state(
12193 &r#"
12194 use some::modified;
12195
12196 ˇ
12197 fn main() {
12198 println!("hello there");
12199
12200 println!("around the");
12201 println!("world");
12202 }
12203 "#
12204 .unindent(),
12205 );
12206
12207 cx.update_editor(|editor, window, cx| {
12208 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12209 });
12210
12211 cx.assert_editor_state(
12212 &r#"
12213 ˇuse some::modified;
12214
12215
12216 fn main() {
12217 println!("hello there");
12218
12219 println!("around the");
12220 println!("world");
12221 }
12222 "#
12223 .unindent(),
12224 );
12225
12226 cx.update_editor(|editor, window, cx| {
12227 for _ in 0..2 {
12228 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12229 }
12230 });
12231
12232 cx.assert_editor_state(
12233 &r#"
12234 use some::modified;
12235
12236
12237 fn main() {
12238 ˇ println!("hello there");
12239
12240 println!("around the");
12241 println!("world");
12242 }
12243 "#
12244 .unindent(),
12245 );
12246
12247 cx.update_editor(|editor, window, cx| {
12248 editor.fold(&Fold, window, cx);
12249 });
12250
12251 cx.update_editor(|editor, window, cx| {
12252 editor.go_to_next_hunk(&GoToHunk, window, cx);
12253 });
12254
12255 cx.assert_editor_state(
12256 &r#"
12257 ˇuse some::modified;
12258
12259
12260 fn main() {
12261 println!("hello there");
12262
12263 println!("around the");
12264 println!("world");
12265 }
12266 "#
12267 .unindent(),
12268 );
12269}
12270
12271#[test]
12272fn test_split_words() {
12273 fn split(text: &str) -> Vec<&str> {
12274 split_words(text).collect()
12275 }
12276
12277 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12278 assert_eq!(split("hello_world"), &["hello_", "world"]);
12279 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12280 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12281 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12282 assert_eq!(split("helloworld"), &["helloworld"]);
12283
12284 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12285}
12286
12287#[gpui::test]
12288async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12289 init_test(cx, |_| {});
12290
12291 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12292 let mut assert = |before, after| {
12293 let _state_context = cx.set_state(before);
12294 cx.run_until_parked();
12295 cx.update_editor(|editor, window, cx| {
12296 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12297 });
12298 cx.run_until_parked();
12299 cx.assert_editor_state(after);
12300 };
12301
12302 // Outside bracket jumps to outside of matching bracket
12303 assert("console.logˇ(var);", "console.log(var)ˇ;");
12304 assert("console.log(var)ˇ;", "console.logˇ(var);");
12305
12306 // Inside bracket jumps to inside of matching bracket
12307 assert("console.log(ˇvar);", "console.log(varˇ);");
12308 assert("console.log(varˇ);", "console.log(ˇvar);");
12309
12310 // When outside a bracket and inside, favor jumping to the inside bracket
12311 assert(
12312 "console.log('foo', [1, 2, 3]ˇ);",
12313 "console.log(ˇ'foo', [1, 2, 3]);",
12314 );
12315 assert(
12316 "console.log(ˇ'foo', [1, 2, 3]);",
12317 "console.log('foo', [1, 2, 3]ˇ);",
12318 );
12319
12320 // Bias forward if two options are equally likely
12321 assert(
12322 "let result = curried_fun()ˇ();",
12323 "let result = curried_fun()()ˇ;",
12324 );
12325
12326 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12327 assert(
12328 indoc! {"
12329 function test() {
12330 console.log('test')ˇ
12331 }"},
12332 indoc! {"
12333 function test() {
12334 console.logˇ('test')
12335 }"},
12336 );
12337}
12338
12339#[gpui::test]
12340async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12341 init_test(cx, |_| {});
12342
12343 let fs = FakeFs::new(cx.executor());
12344 fs.insert_tree(
12345 path!("/a"),
12346 json!({
12347 "main.rs": "fn main() { let a = 5; }",
12348 "other.rs": "// Test file",
12349 }),
12350 )
12351 .await;
12352 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12353
12354 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12355 language_registry.add(Arc::new(Language::new(
12356 LanguageConfig {
12357 name: "Rust".into(),
12358 matcher: LanguageMatcher {
12359 path_suffixes: vec!["rs".to_string()],
12360 ..Default::default()
12361 },
12362 brackets: BracketPairConfig {
12363 pairs: vec![BracketPair {
12364 start: "{".to_string(),
12365 end: "}".to_string(),
12366 close: true,
12367 surround: true,
12368 newline: true,
12369 }],
12370 disabled_scopes_by_bracket_ix: Vec::new(),
12371 },
12372 ..Default::default()
12373 },
12374 Some(tree_sitter_rust::LANGUAGE.into()),
12375 )));
12376 let mut fake_servers = language_registry.register_fake_lsp(
12377 "Rust",
12378 FakeLspAdapter {
12379 capabilities: lsp::ServerCapabilities {
12380 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12381 first_trigger_character: "{".to_string(),
12382 more_trigger_character: None,
12383 }),
12384 ..Default::default()
12385 },
12386 ..Default::default()
12387 },
12388 );
12389
12390 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12391
12392 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12393
12394 let worktree_id = workspace
12395 .update(cx, |workspace, _, cx| {
12396 workspace.project().update(cx, |project, cx| {
12397 project.worktrees(cx).next().unwrap().read(cx).id()
12398 })
12399 })
12400 .unwrap();
12401
12402 let buffer = project
12403 .update(cx, |project, cx| {
12404 project.open_local_buffer(path!("/a/main.rs"), cx)
12405 })
12406 .await
12407 .unwrap();
12408 let editor_handle = workspace
12409 .update(cx, |workspace, window, cx| {
12410 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12411 })
12412 .unwrap()
12413 .await
12414 .unwrap()
12415 .downcast::<Editor>()
12416 .unwrap();
12417
12418 cx.executor().start_waiting();
12419 let fake_server = fake_servers.next().await.unwrap();
12420
12421 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12422 |params, _| async move {
12423 assert_eq!(
12424 params.text_document_position.text_document.uri,
12425 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12426 );
12427 assert_eq!(
12428 params.text_document_position.position,
12429 lsp::Position::new(0, 21),
12430 );
12431
12432 Ok(Some(vec![lsp::TextEdit {
12433 new_text: "]".to_string(),
12434 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12435 }]))
12436 },
12437 );
12438
12439 editor_handle.update_in(cx, |editor, window, cx| {
12440 window.focus(&editor.focus_handle(cx));
12441 editor.change_selections(None, window, cx, |s| {
12442 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12443 });
12444 editor.handle_input("{", window, cx);
12445 });
12446
12447 cx.executor().run_until_parked();
12448
12449 buffer.update(cx, |buffer, _| {
12450 assert_eq!(
12451 buffer.text(),
12452 "fn main() { let a = {5}; }",
12453 "No extra braces from on type formatting should appear in the buffer"
12454 )
12455 });
12456}
12457
12458#[gpui::test]
12459async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12460 init_test(cx, |_| {});
12461
12462 let fs = FakeFs::new(cx.executor());
12463 fs.insert_tree(
12464 path!("/a"),
12465 json!({
12466 "main.rs": "fn main() { let a = 5; }",
12467 "other.rs": "// Test file",
12468 }),
12469 )
12470 .await;
12471
12472 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12473
12474 let server_restarts = Arc::new(AtomicUsize::new(0));
12475 let closure_restarts = Arc::clone(&server_restarts);
12476 let language_server_name = "test language server";
12477 let language_name: LanguageName = "Rust".into();
12478
12479 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12480 language_registry.add(Arc::new(Language::new(
12481 LanguageConfig {
12482 name: language_name.clone(),
12483 matcher: LanguageMatcher {
12484 path_suffixes: vec!["rs".to_string()],
12485 ..Default::default()
12486 },
12487 ..Default::default()
12488 },
12489 Some(tree_sitter_rust::LANGUAGE.into()),
12490 )));
12491 let mut fake_servers = language_registry.register_fake_lsp(
12492 "Rust",
12493 FakeLspAdapter {
12494 name: language_server_name,
12495 initialization_options: Some(json!({
12496 "testOptionValue": true
12497 })),
12498 initializer: Some(Box::new(move |fake_server| {
12499 let task_restarts = Arc::clone(&closure_restarts);
12500 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12501 task_restarts.fetch_add(1, atomic::Ordering::Release);
12502 futures::future::ready(Ok(()))
12503 });
12504 })),
12505 ..Default::default()
12506 },
12507 );
12508
12509 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12510 let _buffer = project
12511 .update(cx, |project, cx| {
12512 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12513 })
12514 .await
12515 .unwrap();
12516 let _fake_server = fake_servers.next().await.unwrap();
12517 update_test_language_settings(cx, |language_settings| {
12518 language_settings.languages.insert(
12519 language_name.clone(),
12520 LanguageSettingsContent {
12521 tab_size: NonZeroU32::new(8),
12522 ..Default::default()
12523 },
12524 );
12525 });
12526 cx.executor().run_until_parked();
12527 assert_eq!(
12528 server_restarts.load(atomic::Ordering::Acquire),
12529 0,
12530 "Should not restart LSP server on an unrelated change"
12531 );
12532
12533 update_test_project_settings(cx, |project_settings| {
12534 project_settings.lsp.insert(
12535 "Some other server name".into(),
12536 LspSettings {
12537 binary: None,
12538 settings: None,
12539 initialization_options: Some(json!({
12540 "some other init value": false
12541 })),
12542 },
12543 );
12544 });
12545 cx.executor().run_until_parked();
12546 assert_eq!(
12547 server_restarts.load(atomic::Ordering::Acquire),
12548 0,
12549 "Should not restart LSP server on an unrelated LSP settings change"
12550 );
12551
12552 update_test_project_settings(cx, |project_settings| {
12553 project_settings.lsp.insert(
12554 language_server_name.into(),
12555 LspSettings {
12556 binary: None,
12557 settings: None,
12558 initialization_options: Some(json!({
12559 "anotherInitValue": false
12560 })),
12561 },
12562 );
12563 });
12564 cx.executor().run_until_parked();
12565 assert_eq!(
12566 server_restarts.load(atomic::Ordering::Acquire),
12567 1,
12568 "Should restart LSP server on a related LSP settings change"
12569 );
12570
12571 update_test_project_settings(cx, |project_settings| {
12572 project_settings.lsp.insert(
12573 language_server_name.into(),
12574 LspSettings {
12575 binary: None,
12576 settings: None,
12577 initialization_options: Some(json!({
12578 "anotherInitValue": false
12579 })),
12580 },
12581 );
12582 });
12583 cx.executor().run_until_parked();
12584 assert_eq!(
12585 server_restarts.load(atomic::Ordering::Acquire),
12586 1,
12587 "Should not restart LSP server on a related LSP settings change that is the same"
12588 );
12589
12590 update_test_project_settings(cx, |project_settings| {
12591 project_settings.lsp.insert(
12592 language_server_name.into(),
12593 LspSettings {
12594 binary: None,
12595 settings: None,
12596 initialization_options: None,
12597 },
12598 );
12599 });
12600 cx.executor().run_until_parked();
12601 assert_eq!(
12602 server_restarts.load(atomic::Ordering::Acquire),
12603 2,
12604 "Should restart LSP server on another related LSP settings change"
12605 );
12606}
12607
12608#[gpui::test]
12609async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12610 init_test(cx, |_| {});
12611
12612 let mut cx = EditorLspTestContext::new_rust(
12613 lsp::ServerCapabilities {
12614 completion_provider: Some(lsp::CompletionOptions {
12615 trigger_characters: Some(vec![".".to_string()]),
12616 resolve_provider: Some(true),
12617 ..Default::default()
12618 }),
12619 ..Default::default()
12620 },
12621 cx,
12622 )
12623 .await;
12624
12625 cx.set_state("fn main() { let a = 2ˇ; }");
12626 cx.simulate_keystroke(".");
12627 let completion_item = lsp::CompletionItem {
12628 label: "some".into(),
12629 kind: Some(lsp::CompletionItemKind::SNIPPET),
12630 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12631 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12632 kind: lsp::MarkupKind::Markdown,
12633 value: "```rust\nSome(2)\n```".to_string(),
12634 })),
12635 deprecated: Some(false),
12636 sort_text: Some("fffffff2".to_string()),
12637 filter_text: Some("some".to_string()),
12638 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12639 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12640 range: lsp::Range {
12641 start: lsp::Position {
12642 line: 0,
12643 character: 22,
12644 },
12645 end: lsp::Position {
12646 line: 0,
12647 character: 22,
12648 },
12649 },
12650 new_text: "Some(2)".to_string(),
12651 })),
12652 additional_text_edits: Some(vec![lsp::TextEdit {
12653 range: lsp::Range {
12654 start: lsp::Position {
12655 line: 0,
12656 character: 20,
12657 },
12658 end: lsp::Position {
12659 line: 0,
12660 character: 22,
12661 },
12662 },
12663 new_text: "".to_string(),
12664 }]),
12665 ..Default::default()
12666 };
12667
12668 let closure_completion_item = completion_item.clone();
12669 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12670 let task_completion_item = closure_completion_item.clone();
12671 async move {
12672 Ok(Some(lsp::CompletionResponse::Array(vec![
12673 task_completion_item,
12674 ])))
12675 }
12676 });
12677
12678 request.next().await;
12679
12680 cx.condition(|editor, _| editor.context_menu_visible())
12681 .await;
12682 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12683 editor
12684 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12685 .unwrap()
12686 });
12687 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12688
12689 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12690 let task_completion_item = completion_item.clone();
12691 async move { Ok(task_completion_item) }
12692 })
12693 .next()
12694 .await
12695 .unwrap();
12696 apply_additional_edits.await.unwrap();
12697 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12698}
12699
12700#[gpui::test]
12701async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12702 init_test(cx, |_| {});
12703
12704 let mut cx = EditorLspTestContext::new_rust(
12705 lsp::ServerCapabilities {
12706 completion_provider: Some(lsp::CompletionOptions {
12707 trigger_characters: Some(vec![".".to_string()]),
12708 resolve_provider: Some(true),
12709 ..Default::default()
12710 }),
12711 ..Default::default()
12712 },
12713 cx,
12714 )
12715 .await;
12716
12717 cx.set_state("fn main() { let a = 2ˇ; }");
12718 cx.simulate_keystroke(".");
12719
12720 let item1 = lsp::CompletionItem {
12721 label: "method id()".to_string(),
12722 filter_text: Some("id".to_string()),
12723 detail: None,
12724 documentation: None,
12725 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12726 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12727 new_text: ".id".to_string(),
12728 })),
12729 ..lsp::CompletionItem::default()
12730 };
12731
12732 let item2 = lsp::CompletionItem {
12733 label: "other".to_string(),
12734 filter_text: Some("other".to_string()),
12735 detail: None,
12736 documentation: None,
12737 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12738 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12739 new_text: ".other".to_string(),
12740 })),
12741 ..lsp::CompletionItem::default()
12742 };
12743
12744 let item1 = item1.clone();
12745 cx.set_request_handler::<lsp::request::Completion, _, _>({
12746 let item1 = item1.clone();
12747 move |_, _, _| {
12748 let item1 = item1.clone();
12749 let item2 = item2.clone();
12750 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12751 }
12752 })
12753 .next()
12754 .await;
12755
12756 cx.condition(|editor, _| editor.context_menu_visible())
12757 .await;
12758 cx.update_editor(|editor, _, _| {
12759 let context_menu = editor.context_menu.borrow_mut();
12760 let context_menu = context_menu
12761 .as_ref()
12762 .expect("Should have the context menu deployed");
12763 match context_menu {
12764 CodeContextMenu::Completions(completions_menu) => {
12765 let completions = completions_menu.completions.borrow_mut();
12766 assert_eq!(
12767 completions
12768 .iter()
12769 .map(|completion| &completion.label.text)
12770 .collect::<Vec<_>>(),
12771 vec!["method id()", "other"]
12772 )
12773 }
12774 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12775 }
12776 });
12777
12778 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12779 let item1 = item1.clone();
12780 move |_, item_to_resolve, _| {
12781 let item1 = item1.clone();
12782 async move {
12783 if item1 == item_to_resolve {
12784 Ok(lsp::CompletionItem {
12785 label: "method id()".to_string(),
12786 filter_text: Some("id".to_string()),
12787 detail: Some("Now resolved!".to_string()),
12788 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12789 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12790 range: lsp::Range::new(
12791 lsp::Position::new(0, 22),
12792 lsp::Position::new(0, 22),
12793 ),
12794 new_text: ".id".to_string(),
12795 })),
12796 ..lsp::CompletionItem::default()
12797 })
12798 } else {
12799 Ok(item_to_resolve)
12800 }
12801 }
12802 }
12803 })
12804 .next()
12805 .await
12806 .unwrap();
12807 cx.run_until_parked();
12808
12809 cx.update_editor(|editor, window, cx| {
12810 editor.context_menu_next(&Default::default(), window, cx);
12811 });
12812
12813 cx.update_editor(|editor, _, _| {
12814 let context_menu = editor.context_menu.borrow_mut();
12815 let context_menu = context_menu
12816 .as_ref()
12817 .expect("Should have the context menu deployed");
12818 match context_menu {
12819 CodeContextMenu::Completions(completions_menu) => {
12820 let completions = completions_menu.completions.borrow_mut();
12821 assert_eq!(
12822 completions
12823 .iter()
12824 .map(|completion| &completion.label.text)
12825 .collect::<Vec<_>>(),
12826 vec!["method id() Now resolved!", "other"],
12827 "Should update first completion label, but not second as the filter text did not match."
12828 );
12829 }
12830 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12831 }
12832 });
12833}
12834
12835#[gpui::test]
12836async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12837 init_test(cx, |_| {});
12838
12839 let mut cx = EditorLspTestContext::new_rust(
12840 lsp::ServerCapabilities {
12841 completion_provider: Some(lsp::CompletionOptions {
12842 trigger_characters: Some(vec![".".to_string()]),
12843 resolve_provider: Some(true),
12844 ..Default::default()
12845 }),
12846 ..Default::default()
12847 },
12848 cx,
12849 )
12850 .await;
12851
12852 cx.set_state("fn main() { let a = 2ˇ; }");
12853 cx.simulate_keystroke(".");
12854
12855 let unresolved_item_1 = lsp::CompletionItem {
12856 label: "id".to_string(),
12857 filter_text: Some("id".to_string()),
12858 detail: None,
12859 documentation: None,
12860 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12861 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12862 new_text: ".id".to_string(),
12863 })),
12864 ..lsp::CompletionItem::default()
12865 };
12866 let resolved_item_1 = lsp::CompletionItem {
12867 additional_text_edits: Some(vec![lsp::TextEdit {
12868 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12869 new_text: "!!".to_string(),
12870 }]),
12871 ..unresolved_item_1.clone()
12872 };
12873 let unresolved_item_2 = lsp::CompletionItem {
12874 label: "other".to_string(),
12875 filter_text: Some("other".to_string()),
12876 detail: None,
12877 documentation: None,
12878 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12879 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12880 new_text: ".other".to_string(),
12881 })),
12882 ..lsp::CompletionItem::default()
12883 };
12884 let resolved_item_2 = lsp::CompletionItem {
12885 additional_text_edits: Some(vec![lsp::TextEdit {
12886 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12887 new_text: "??".to_string(),
12888 }]),
12889 ..unresolved_item_2.clone()
12890 };
12891
12892 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12893 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12894 cx.lsp
12895 .server
12896 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12897 let unresolved_item_1 = unresolved_item_1.clone();
12898 let resolved_item_1 = resolved_item_1.clone();
12899 let unresolved_item_2 = unresolved_item_2.clone();
12900 let resolved_item_2 = resolved_item_2.clone();
12901 let resolve_requests_1 = resolve_requests_1.clone();
12902 let resolve_requests_2 = resolve_requests_2.clone();
12903 move |unresolved_request, _| {
12904 let unresolved_item_1 = unresolved_item_1.clone();
12905 let resolved_item_1 = resolved_item_1.clone();
12906 let unresolved_item_2 = unresolved_item_2.clone();
12907 let resolved_item_2 = resolved_item_2.clone();
12908 let resolve_requests_1 = resolve_requests_1.clone();
12909 let resolve_requests_2 = resolve_requests_2.clone();
12910 async move {
12911 if unresolved_request == unresolved_item_1 {
12912 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12913 Ok(resolved_item_1.clone())
12914 } else if unresolved_request == unresolved_item_2 {
12915 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12916 Ok(resolved_item_2.clone())
12917 } else {
12918 panic!("Unexpected completion item {unresolved_request:?}")
12919 }
12920 }
12921 }
12922 })
12923 .detach();
12924
12925 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12926 let unresolved_item_1 = unresolved_item_1.clone();
12927 let unresolved_item_2 = unresolved_item_2.clone();
12928 async move {
12929 Ok(Some(lsp::CompletionResponse::Array(vec![
12930 unresolved_item_1,
12931 unresolved_item_2,
12932 ])))
12933 }
12934 })
12935 .next()
12936 .await;
12937
12938 cx.condition(|editor, _| editor.context_menu_visible())
12939 .await;
12940 cx.update_editor(|editor, _, _| {
12941 let context_menu = editor.context_menu.borrow_mut();
12942 let context_menu = context_menu
12943 .as_ref()
12944 .expect("Should have the context menu deployed");
12945 match context_menu {
12946 CodeContextMenu::Completions(completions_menu) => {
12947 let completions = completions_menu.completions.borrow_mut();
12948 assert_eq!(
12949 completions
12950 .iter()
12951 .map(|completion| &completion.label.text)
12952 .collect::<Vec<_>>(),
12953 vec!["id", "other"]
12954 )
12955 }
12956 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12957 }
12958 });
12959 cx.run_until_parked();
12960
12961 cx.update_editor(|editor, window, cx| {
12962 editor.context_menu_next(&ContextMenuNext, window, cx);
12963 });
12964 cx.run_until_parked();
12965 cx.update_editor(|editor, window, cx| {
12966 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12967 });
12968 cx.run_until_parked();
12969 cx.update_editor(|editor, window, cx| {
12970 editor.context_menu_next(&ContextMenuNext, window, cx);
12971 });
12972 cx.run_until_parked();
12973 cx.update_editor(|editor, window, cx| {
12974 editor
12975 .compose_completion(&ComposeCompletion::default(), window, cx)
12976 .expect("No task returned")
12977 })
12978 .await
12979 .expect("Completion failed");
12980 cx.run_until_parked();
12981
12982 cx.update_editor(|editor, _, cx| {
12983 assert_eq!(
12984 resolve_requests_1.load(atomic::Ordering::Acquire),
12985 1,
12986 "Should always resolve once despite multiple selections"
12987 );
12988 assert_eq!(
12989 resolve_requests_2.load(atomic::Ordering::Acquire),
12990 1,
12991 "Should always resolve once after multiple selections and applying the completion"
12992 );
12993 assert_eq!(
12994 editor.text(cx),
12995 "fn main() { let a = ??.other; }",
12996 "Should use resolved data when applying the completion"
12997 );
12998 });
12999}
13000
13001#[gpui::test]
13002async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13003 init_test(cx, |_| {});
13004
13005 let item_0 = lsp::CompletionItem {
13006 label: "abs".into(),
13007 insert_text: Some("abs".into()),
13008 data: Some(json!({ "very": "special"})),
13009 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13010 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13011 lsp::InsertReplaceEdit {
13012 new_text: "abs".to_string(),
13013 insert: lsp::Range::default(),
13014 replace: lsp::Range::default(),
13015 },
13016 )),
13017 ..lsp::CompletionItem::default()
13018 };
13019 let items = iter::once(item_0.clone())
13020 .chain((11..51).map(|i| lsp::CompletionItem {
13021 label: format!("item_{}", i),
13022 insert_text: Some(format!("item_{}", i)),
13023 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13024 ..lsp::CompletionItem::default()
13025 }))
13026 .collect::<Vec<_>>();
13027
13028 let default_commit_characters = vec!["?".to_string()];
13029 let default_data = json!({ "default": "data"});
13030 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13031 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13032 let default_edit_range = lsp::Range {
13033 start: lsp::Position {
13034 line: 0,
13035 character: 5,
13036 },
13037 end: lsp::Position {
13038 line: 0,
13039 character: 5,
13040 },
13041 };
13042
13043 let mut cx = EditorLspTestContext::new_rust(
13044 lsp::ServerCapabilities {
13045 completion_provider: Some(lsp::CompletionOptions {
13046 trigger_characters: Some(vec![".".to_string()]),
13047 resolve_provider: Some(true),
13048 ..Default::default()
13049 }),
13050 ..Default::default()
13051 },
13052 cx,
13053 )
13054 .await;
13055
13056 cx.set_state("fn main() { let a = 2ˇ; }");
13057 cx.simulate_keystroke(".");
13058
13059 let completion_data = default_data.clone();
13060 let completion_characters = default_commit_characters.clone();
13061 let completion_items = items.clone();
13062 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13063 let default_data = completion_data.clone();
13064 let default_commit_characters = completion_characters.clone();
13065 let items = completion_items.clone();
13066 async move {
13067 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13068 items,
13069 item_defaults: Some(lsp::CompletionListItemDefaults {
13070 data: Some(default_data.clone()),
13071 commit_characters: Some(default_commit_characters.clone()),
13072 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13073 default_edit_range,
13074 )),
13075 insert_text_format: Some(default_insert_text_format),
13076 insert_text_mode: Some(default_insert_text_mode),
13077 }),
13078 ..lsp::CompletionList::default()
13079 })))
13080 }
13081 })
13082 .next()
13083 .await;
13084
13085 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13086 cx.lsp
13087 .server
13088 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13089 let closure_resolved_items = resolved_items.clone();
13090 move |item_to_resolve, _| {
13091 let closure_resolved_items = closure_resolved_items.clone();
13092 async move {
13093 closure_resolved_items.lock().push(item_to_resolve.clone());
13094 Ok(item_to_resolve)
13095 }
13096 }
13097 })
13098 .detach();
13099
13100 cx.condition(|editor, _| editor.context_menu_visible())
13101 .await;
13102 cx.run_until_parked();
13103 cx.update_editor(|editor, _, _| {
13104 let menu = editor.context_menu.borrow_mut();
13105 match menu.as_ref().expect("should have the completions menu") {
13106 CodeContextMenu::Completions(completions_menu) => {
13107 assert_eq!(
13108 completions_menu
13109 .entries
13110 .borrow()
13111 .iter()
13112 .map(|mat| mat.string.clone())
13113 .collect::<Vec<String>>(),
13114 items
13115 .iter()
13116 .map(|completion| completion.label.clone())
13117 .collect::<Vec<String>>()
13118 );
13119 }
13120 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13121 }
13122 });
13123 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13124 // with 4 from the end.
13125 assert_eq!(
13126 *resolved_items.lock(),
13127 [&items[0..16], &items[items.len() - 4..items.len()]]
13128 .concat()
13129 .iter()
13130 .cloned()
13131 .map(|mut item| {
13132 if item.data.is_none() {
13133 item.data = Some(default_data.clone());
13134 }
13135 item
13136 })
13137 .collect::<Vec<lsp::CompletionItem>>(),
13138 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13139 );
13140 resolved_items.lock().clear();
13141
13142 cx.update_editor(|editor, window, cx| {
13143 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13144 });
13145 cx.run_until_parked();
13146 // Completions that have already been resolved are skipped.
13147 assert_eq!(
13148 *resolved_items.lock(),
13149 items[items.len() - 16..items.len() - 4]
13150 .iter()
13151 .cloned()
13152 .map(|mut item| {
13153 if item.data.is_none() {
13154 item.data = Some(default_data.clone());
13155 }
13156 item
13157 })
13158 .collect::<Vec<lsp::CompletionItem>>()
13159 );
13160 resolved_items.lock().clear();
13161}
13162
13163#[gpui::test]
13164async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13165 init_test(cx, |_| {});
13166
13167 let mut cx = EditorLspTestContext::new(
13168 Language::new(
13169 LanguageConfig {
13170 matcher: LanguageMatcher {
13171 path_suffixes: vec!["jsx".into()],
13172 ..Default::default()
13173 },
13174 overrides: [(
13175 "element".into(),
13176 LanguageConfigOverride {
13177 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13178 ..Default::default()
13179 },
13180 )]
13181 .into_iter()
13182 .collect(),
13183 ..Default::default()
13184 },
13185 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13186 )
13187 .with_override_query("(jsx_self_closing_element) @element")
13188 .unwrap(),
13189 lsp::ServerCapabilities {
13190 completion_provider: Some(lsp::CompletionOptions {
13191 trigger_characters: Some(vec![":".to_string()]),
13192 ..Default::default()
13193 }),
13194 ..Default::default()
13195 },
13196 cx,
13197 )
13198 .await;
13199
13200 cx.lsp
13201 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13202 Ok(Some(lsp::CompletionResponse::Array(vec![
13203 lsp::CompletionItem {
13204 label: "bg-blue".into(),
13205 ..Default::default()
13206 },
13207 lsp::CompletionItem {
13208 label: "bg-red".into(),
13209 ..Default::default()
13210 },
13211 lsp::CompletionItem {
13212 label: "bg-yellow".into(),
13213 ..Default::default()
13214 },
13215 ])))
13216 });
13217
13218 cx.set_state(r#"<p class="bgˇ" />"#);
13219
13220 // Trigger completion when typing a dash, because the dash is an extra
13221 // word character in the 'element' scope, which contains the cursor.
13222 cx.simulate_keystroke("-");
13223 cx.executor().run_until_parked();
13224 cx.update_editor(|editor, _, _| {
13225 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13226 {
13227 assert_eq!(
13228 completion_menu_entries(&menu),
13229 &["bg-red", "bg-blue", "bg-yellow"]
13230 );
13231 } else {
13232 panic!("expected completion menu to be open");
13233 }
13234 });
13235
13236 cx.simulate_keystroke("l");
13237 cx.executor().run_until_parked();
13238 cx.update_editor(|editor, _, _| {
13239 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13240 {
13241 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13242 } else {
13243 panic!("expected completion menu to be open");
13244 }
13245 });
13246
13247 // When filtering completions, consider the character after the '-' to
13248 // be the start of a subword.
13249 cx.set_state(r#"<p class="yelˇ" />"#);
13250 cx.simulate_keystroke("l");
13251 cx.executor().run_until_parked();
13252 cx.update_editor(|editor, _, _| {
13253 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13254 {
13255 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13256 } else {
13257 panic!("expected completion menu to be open");
13258 }
13259 });
13260}
13261
13262fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13263 let entries = menu.entries.borrow();
13264 entries.iter().map(|mat| mat.string.clone()).collect()
13265}
13266
13267#[gpui::test]
13268async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13269 init_test(cx, |settings| {
13270 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13271 FormatterList(vec![Formatter::Prettier].into()),
13272 ))
13273 });
13274
13275 let fs = FakeFs::new(cx.executor());
13276 fs.insert_file(path!("/file.ts"), Default::default()).await;
13277
13278 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13279 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13280
13281 language_registry.add(Arc::new(Language::new(
13282 LanguageConfig {
13283 name: "TypeScript".into(),
13284 matcher: LanguageMatcher {
13285 path_suffixes: vec!["ts".to_string()],
13286 ..Default::default()
13287 },
13288 ..Default::default()
13289 },
13290 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13291 )));
13292 update_test_language_settings(cx, |settings| {
13293 settings.defaults.prettier = Some(PrettierSettings {
13294 allowed: true,
13295 ..PrettierSettings::default()
13296 });
13297 });
13298
13299 let test_plugin = "test_plugin";
13300 let _ = language_registry.register_fake_lsp(
13301 "TypeScript",
13302 FakeLspAdapter {
13303 prettier_plugins: vec![test_plugin],
13304 ..Default::default()
13305 },
13306 );
13307
13308 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13309 let buffer = project
13310 .update(cx, |project, cx| {
13311 project.open_local_buffer(path!("/file.ts"), cx)
13312 })
13313 .await
13314 .unwrap();
13315
13316 let buffer_text = "one\ntwo\nthree\n";
13317 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13318 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13319 editor.update_in(cx, |editor, window, cx| {
13320 editor.set_text(buffer_text, window, cx)
13321 });
13322
13323 editor
13324 .update_in(cx, |editor, window, cx| {
13325 editor.perform_format(
13326 project.clone(),
13327 FormatTrigger::Manual,
13328 FormatTarget::Buffers,
13329 window,
13330 cx,
13331 )
13332 })
13333 .unwrap()
13334 .await;
13335 assert_eq!(
13336 editor.update(cx, |editor, cx| editor.text(cx)),
13337 buffer_text.to_string() + prettier_format_suffix,
13338 "Test prettier formatting was not applied to the original buffer text",
13339 );
13340
13341 update_test_language_settings(cx, |settings| {
13342 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13343 });
13344 let format = editor.update_in(cx, |editor, window, cx| {
13345 editor.perform_format(
13346 project.clone(),
13347 FormatTrigger::Manual,
13348 FormatTarget::Buffers,
13349 window,
13350 cx,
13351 )
13352 });
13353 format.await.unwrap();
13354 assert_eq!(
13355 editor.update(cx, |editor, cx| editor.text(cx)),
13356 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13357 "Autoformatting (via test prettier) was not applied to the original buffer text",
13358 );
13359}
13360
13361#[gpui::test]
13362async fn test_addition_reverts(cx: &mut TestAppContext) {
13363 init_test(cx, |_| {});
13364 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13365 let base_text = indoc! {r#"
13366 struct Row;
13367 struct Row1;
13368 struct Row2;
13369
13370 struct Row4;
13371 struct Row5;
13372 struct Row6;
13373
13374 struct Row8;
13375 struct Row9;
13376 struct Row10;"#};
13377
13378 // When addition hunks are not adjacent to carets, no hunk revert is performed
13379 assert_hunk_revert(
13380 indoc! {r#"struct Row;
13381 struct Row1;
13382 struct Row1.1;
13383 struct Row1.2;
13384 struct Row2;ˇ
13385
13386 struct Row4;
13387 struct Row5;
13388 struct Row6;
13389
13390 struct Row8;
13391 ˇstruct Row9;
13392 struct Row9.1;
13393 struct Row9.2;
13394 struct Row9.3;
13395 struct Row10;"#},
13396 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13397 indoc! {r#"struct Row;
13398 struct Row1;
13399 struct Row1.1;
13400 struct Row1.2;
13401 struct Row2;ˇ
13402
13403 struct Row4;
13404 struct Row5;
13405 struct Row6;
13406
13407 struct Row8;
13408 ˇstruct Row9;
13409 struct Row9.1;
13410 struct Row9.2;
13411 struct Row9.3;
13412 struct Row10;"#},
13413 base_text,
13414 &mut cx,
13415 );
13416 // Same for selections
13417 assert_hunk_revert(
13418 indoc! {r#"struct Row;
13419 struct Row1;
13420 struct Row2;
13421 struct Row2.1;
13422 struct Row2.2;
13423 «ˇ
13424 struct Row4;
13425 struct» Row5;
13426 «struct Row6;
13427 ˇ»
13428 struct Row9.1;
13429 struct Row9.2;
13430 struct Row9.3;
13431 struct Row8;
13432 struct Row9;
13433 struct Row10;"#},
13434 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13435 indoc! {r#"struct Row;
13436 struct Row1;
13437 struct Row2;
13438 struct Row2.1;
13439 struct Row2.2;
13440 «ˇ
13441 struct Row4;
13442 struct» Row5;
13443 «struct Row6;
13444 ˇ»
13445 struct Row9.1;
13446 struct Row9.2;
13447 struct Row9.3;
13448 struct Row8;
13449 struct Row9;
13450 struct Row10;"#},
13451 base_text,
13452 &mut cx,
13453 );
13454
13455 // When carets and selections intersect the addition hunks, those are reverted.
13456 // Adjacent carets got merged.
13457 assert_hunk_revert(
13458 indoc! {r#"struct Row;
13459 ˇ// something on the top
13460 struct Row1;
13461 struct Row2;
13462 struct Roˇw3.1;
13463 struct Row2.2;
13464 struct Row2.3;ˇ
13465
13466 struct Row4;
13467 struct ˇRow5.1;
13468 struct Row5.2;
13469 struct «Rowˇ»5.3;
13470 struct Row5;
13471 struct Row6;
13472 ˇ
13473 struct Row9.1;
13474 struct «Rowˇ»9.2;
13475 struct «ˇRow»9.3;
13476 struct Row8;
13477 struct Row9;
13478 «ˇ// something on bottom»
13479 struct Row10;"#},
13480 vec![
13481 DiffHunkStatusKind::Added,
13482 DiffHunkStatusKind::Added,
13483 DiffHunkStatusKind::Added,
13484 DiffHunkStatusKind::Added,
13485 DiffHunkStatusKind::Added,
13486 ],
13487 indoc! {r#"struct Row;
13488 ˇstruct Row1;
13489 struct Row2;
13490 ˇ
13491 struct Row4;
13492 ˇstruct Row5;
13493 struct Row6;
13494 ˇ
13495 ˇstruct Row8;
13496 struct Row9;
13497 ˇstruct Row10;"#},
13498 base_text,
13499 &mut cx,
13500 );
13501}
13502
13503#[gpui::test]
13504async fn test_modification_reverts(cx: &mut TestAppContext) {
13505 init_test(cx, |_| {});
13506 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13507 let base_text = indoc! {r#"
13508 struct Row;
13509 struct Row1;
13510 struct Row2;
13511
13512 struct Row4;
13513 struct Row5;
13514 struct Row6;
13515
13516 struct Row8;
13517 struct Row9;
13518 struct Row10;"#};
13519
13520 // Modification hunks behave the same as the addition ones.
13521 assert_hunk_revert(
13522 indoc! {r#"struct Row;
13523 struct Row1;
13524 struct Row33;
13525 ˇ
13526 struct Row4;
13527 struct Row5;
13528 struct Row6;
13529 ˇ
13530 struct Row99;
13531 struct Row9;
13532 struct Row10;"#},
13533 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13534 indoc! {r#"struct Row;
13535 struct Row1;
13536 struct Row33;
13537 ˇ
13538 struct Row4;
13539 struct Row5;
13540 struct Row6;
13541 ˇ
13542 struct Row99;
13543 struct Row9;
13544 struct Row10;"#},
13545 base_text,
13546 &mut cx,
13547 );
13548 assert_hunk_revert(
13549 indoc! {r#"struct Row;
13550 struct Row1;
13551 struct Row33;
13552 «ˇ
13553 struct Row4;
13554 struct» Row5;
13555 «struct Row6;
13556 ˇ»
13557 struct Row99;
13558 struct Row9;
13559 struct Row10;"#},
13560 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13561 indoc! {r#"struct Row;
13562 struct Row1;
13563 struct Row33;
13564 «ˇ
13565 struct Row4;
13566 struct» Row5;
13567 «struct Row6;
13568 ˇ»
13569 struct Row99;
13570 struct Row9;
13571 struct Row10;"#},
13572 base_text,
13573 &mut cx,
13574 );
13575
13576 assert_hunk_revert(
13577 indoc! {r#"ˇstruct Row1.1;
13578 struct Row1;
13579 «ˇstr»uct Row22;
13580
13581 struct ˇRow44;
13582 struct Row5;
13583 struct «Rˇ»ow66;ˇ
13584
13585 «struˇ»ct Row88;
13586 struct Row9;
13587 struct Row1011;ˇ"#},
13588 vec![
13589 DiffHunkStatusKind::Modified,
13590 DiffHunkStatusKind::Modified,
13591 DiffHunkStatusKind::Modified,
13592 DiffHunkStatusKind::Modified,
13593 DiffHunkStatusKind::Modified,
13594 DiffHunkStatusKind::Modified,
13595 ],
13596 indoc! {r#"struct Row;
13597 ˇstruct Row1;
13598 struct Row2;
13599 ˇ
13600 struct Row4;
13601 ˇstruct Row5;
13602 struct Row6;
13603 ˇ
13604 struct Row8;
13605 ˇstruct Row9;
13606 struct Row10;ˇ"#},
13607 base_text,
13608 &mut cx,
13609 );
13610}
13611
13612#[gpui::test]
13613async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13614 init_test(cx, |_| {});
13615 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13616 let base_text = indoc! {r#"
13617 one
13618
13619 two
13620 three
13621 "#};
13622
13623 cx.set_head_text(base_text);
13624 cx.set_state("\nˇ\n");
13625 cx.executor().run_until_parked();
13626 cx.update_editor(|editor, _window, cx| {
13627 editor.expand_selected_diff_hunks(cx);
13628 });
13629 cx.executor().run_until_parked();
13630 cx.update_editor(|editor, window, cx| {
13631 editor.backspace(&Default::default(), window, cx);
13632 });
13633 cx.run_until_parked();
13634 cx.assert_state_with_diff(
13635 indoc! {r#"
13636
13637 - two
13638 - threeˇ
13639 +
13640 "#}
13641 .to_string(),
13642 );
13643}
13644
13645#[gpui::test]
13646async fn test_deletion_reverts(cx: &mut TestAppContext) {
13647 init_test(cx, |_| {});
13648 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13649 let base_text = indoc! {r#"struct Row;
13650struct Row1;
13651struct Row2;
13652
13653struct Row4;
13654struct Row5;
13655struct Row6;
13656
13657struct Row8;
13658struct Row9;
13659struct Row10;"#};
13660
13661 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13662 assert_hunk_revert(
13663 indoc! {r#"struct Row;
13664 struct Row2;
13665
13666 ˇstruct Row4;
13667 struct Row5;
13668 struct Row6;
13669 ˇ
13670 struct Row8;
13671 struct Row10;"#},
13672 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13673 indoc! {r#"struct Row;
13674 struct Row2;
13675
13676 ˇstruct Row4;
13677 struct Row5;
13678 struct Row6;
13679 ˇ
13680 struct Row8;
13681 struct Row10;"#},
13682 base_text,
13683 &mut cx,
13684 );
13685 assert_hunk_revert(
13686 indoc! {r#"struct Row;
13687 struct Row2;
13688
13689 «ˇstruct Row4;
13690 struct» Row5;
13691 «struct Row6;
13692 ˇ»
13693 struct Row8;
13694 struct Row10;"#},
13695 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13696 indoc! {r#"struct Row;
13697 struct Row2;
13698
13699 «ˇstruct Row4;
13700 struct» Row5;
13701 «struct Row6;
13702 ˇ»
13703 struct Row8;
13704 struct Row10;"#},
13705 base_text,
13706 &mut cx,
13707 );
13708
13709 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13710 assert_hunk_revert(
13711 indoc! {r#"struct Row;
13712 ˇstruct Row2;
13713
13714 struct Row4;
13715 struct Row5;
13716 struct Row6;
13717
13718 struct Row8;ˇ
13719 struct Row10;"#},
13720 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13721 indoc! {r#"struct Row;
13722 struct Row1;
13723 ˇstruct Row2;
13724
13725 struct Row4;
13726 struct Row5;
13727 struct Row6;
13728
13729 struct Row8;ˇ
13730 struct Row9;
13731 struct Row10;"#},
13732 base_text,
13733 &mut cx,
13734 );
13735 assert_hunk_revert(
13736 indoc! {r#"struct Row;
13737 struct Row2«ˇ;
13738 struct Row4;
13739 struct» Row5;
13740 «struct Row6;
13741
13742 struct Row8;ˇ»
13743 struct Row10;"#},
13744 vec![
13745 DiffHunkStatusKind::Deleted,
13746 DiffHunkStatusKind::Deleted,
13747 DiffHunkStatusKind::Deleted,
13748 ],
13749 indoc! {r#"struct Row;
13750 struct Row1;
13751 struct Row2«ˇ;
13752
13753 struct Row4;
13754 struct» Row5;
13755 «struct Row6;
13756
13757 struct Row8;ˇ»
13758 struct Row9;
13759 struct Row10;"#},
13760 base_text,
13761 &mut cx,
13762 );
13763}
13764
13765#[gpui::test]
13766async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13767 init_test(cx, |_| {});
13768
13769 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13770 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13771 let base_text_3 =
13772 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13773
13774 let text_1 = edit_first_char_of_every_line(base_text_1);
13775 let text_2 = edit_first_char_of_every_line(base_text_2);
13776 let text_3 = edit_first_char_of_every_line(base_text_3);
13777
13778 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13779 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13780 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13781
13782 let multibuffer = cx.new(|cx| {
13783 let mut multibuffer = MultiBuffer::new(ReadWrite);
13784 multibuffer.push_excerpts(
13785 buffer_1.clone(),
13786 [
13787 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13788 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13789 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13790 ],
13791 cx,
13792 );
13793 multibuffer.push_excerpts(
13794 buffer_2.clone(),
13795 [
13796 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13797 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13798 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13799 ],
13800 cx,
13801 );
13802 multibuffer.push_excerpts(
13803 buffer_3.clone(),
13804 [
13805 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13806 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13807 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13808 ],
13809 cx,
13810 );
13811 multibuffer
13812 });
13813
13814 let fs = FakeFs::new(cx.executor());
13815 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13816 let (editor, cx) = cx
13817 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13818 editor.update_in(cx, |editor, _window, cx| {
13819 for (buffer, diff_base) in [
13820 (buffer_1.clone(), base_text_1),
13821 (buffer_2.clone(), base_text_2),
13822 (buffer_3.clone(), base_text_3),
13823 ] {
13824 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13825 editor
13826 .buffer
13827 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13828 }
13829 });
13830 cx.executor().run_until_parked();
13831
13832 editor.update_in(cx, |editor, window, cx| {
13833 assert_eq!(editor.text(cx), "Xaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}");
13834 editor.select_all(&SelectAll, window, cx);
13835 editor.git_restore(&Default::default(), window, cx);
13836 });
13837 cx.executor().run_until_parked();
13838
13839 // When all ranges are selected, all buffer hunks are reverted.
13840 editor.update(cx, |editor, cx| {
13841 assert_eq!(editor.text(cx), "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n");
13842 });
13843 buffer_1.update(cx, |buffer, _| {
13844 assert_eq!(buffer.text(), base_text_1);
13845 });
13846 buffer_2.update(cx, |buffer, _| {
13847 assert_eq!(buffer.text(), base_text_2);
13848 });
13849 buffer_3.update(cx, |buffer, _| {
13850 assert_eq!(buffer.text(), base_text_3);
13851 });
13852
13853 editor.update_in(cx, |editor, window, cx| {
13854 editor.undo(&Default::default(), window, cx);
13855 });
13856
13857 editor.update_in(cx, |editor, window, cx| {
13858 editor.change_selections(None, window, cx, |s| {
13859 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13860 });
13861 editor.git_restore(&Default::default(), window, cx);
13862 });
13863
13864 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13865 // but not affect buffer_2 and its related excerpts.
13866 editor.update(cx, |editor, cx| {
13867 assert_eq!(
13868 editor.text(cx),
13869 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}"
13870 );
13871 });
13872 buffer_1.update(cx, |buffer, _| {
13873 assert_eq!(buffer.text(), base_text_1);
13874 });
13875 buffer_2.update(cx, |buffer, _| {
13876 assert_eq!(
13877 buffer.text(),
13878 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13879 );
13880 });
13881 buffer_3.update(cx, |buffer, _| {
13882 assert_eq!(
13883 buffer.text(),
13884 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13885 );
13886 });
13887
13888 fn edit_first_char_of_every_line(text: &str) -> String {
13889 text.split('\n')
13890 .map(|line| format!("X{}", &line[1..]))
13891 .collect::<Vec<_>>()
13892 .join("\n")
13893 }
13894}
13895
13896#[gpui::test]
13897async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13898 init_test(cx, |_| {});
13899
13900 let cols = 4;
13901 let rows = 10;
13902 let sample_text_1 = sample_text(rows, cols, 'a');
13903 assert_eq!(
13904 sample_text_1,
13905 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13906 );
13907 let sample_text_2 = sample_text(rows, cols, 'l');
13908 assert_eq!(
13909 sample_text_2,
13910 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13911 );
13912 let sample_text_3 = sample_text(rows, cols, 'v');
13913 assert_eq!(
13914 sample_text_3,
13915 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13916 );
13917
13918 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13919 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13920 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13921
13922 let multi_buffer = cx.new(|cx| {
13923 let mut multibuffer = MultiBuffer::new(ReadWrite);
13924 multibuffer.push_excerpts(
13925 buffer_1.clone(),
13926 [
13927 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13928 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13929 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13930 ],
13931 cx,
13932 );
13933 multibuffer.push_excerpts(
13934 buffer_2.clone(),
13935 [
13936 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13937 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13938 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13939 ],
13940 cx,
13941 );
13942 multibuffer.push_excerpts(
13943 buffer_3.clone(),
13944 [
13945 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13946 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13947 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13948 ],
13949 cx,
13950 );
13951 multibuffer
13952 });
13953
13954 let fs = FakeFs::new(cx.executor());
13955 fs.insert_tree(
13956 "/a",
13957 json!({
13958 "main.rs": sample_text_1,
13959 "other.rs": sample_text_2,
13960 "lib.rs": sample_text_3,
13961 }),
13962 )
13963 .await;
13964 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13965 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13966 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13967 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13968 Editor::new(
13969 EditorMode::Full,
13970 multi_buffer,
13971 Some(project.clone()),
13972 window,
13973 cx,
13974 )
13975 });
13976 let multibuffer_item_id = workspace
13977 .update(cx, |workspace, window, cx| {
13978 assert!(
13979 workspace.active_item(cx).is_none(),
13980 "active item should be None before the first item is added"
13981 );
13982 workspace.add_item_to_active_pane(
13983 Box::new(multi_buffer_editor.clone()),
13984 None,
13985 true,
13986 window,
13987 cx,
13988 );
13989 let active_item = workspace
13990 .active_item(cx)
13991 .expect("should have an active item after adding the multi buffer");
13992 assert!(
13993 !active_item.is_singleton(cx),
13994 "A multi buffer was expected to active after adding"
13995 );
13996 active_item.item_id()
13997 })
13998 .unwrap();
13999 cx.executor().run_until_parked();
14000
14001 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14002 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14003 s.select_ranges(Some(1..2))
14004 });
14005 editor.open_excerpts(&OpenExcerpts, window, cx);
14006 });
14007 cx.executor().run_until_parked();
14008 let first_item_id = workspace
14009 .update(cx, |workspace, window, cx| {
14010 let active_item = workspace
14011 .active_item(cx)
14012 .expect("should have an active item after navigating into the 1st buffer");
14013 let first_item_id = active_item.item_id();
14014 assert_ne!(
14015 first_item_id, multibuffer_item_id,
14016 "Should navigate into the 1st buffer and activate it"
14017 );
14018 assert!(
14019 active_item.is_singleton(cx),
14020 "New active item should be a singleton buffer"
14021 );
14022 assert_eq!(
14023 active_item
14024 .act_as::<Editor>(cx)
14025 .expect("should have navigated into an editor for the 1st buffer")
14026 .read(cx)
14027 .text(cx),
14028 sample_text_1
14029 );
14030
14031 workspace
14032 .go_back(workspace.active_pane().downgrade(), window, cx)
14033 .detach_and_log_err(cx);
14034
14035 first_item_id
14036 })
14037 .unwrap();
14038 cx.executor().run_until_parked();
14039 workspace
14040 .update(cx, |workspace, _, cx| {
14041 let active_item = workspace
14042 .active_item(cx)
14043 .expect("should have an active item after navigating back");
14044 assert_eq!(
14045 active_item.item_id(),
14046 multibuffer_item_id,
14047 "Should navigate back to the multi buffer"
14048 );
14049 assert!(!active_item.is_singleton(cx));
14050 })
14051 .unwrap();
14052
14053 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14054 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14055 s.select_ranges(Some(39..40))
14056 });
14057 editor.open_excerpts(&OpenExcerpts, window, cx);
14058 });
14059 cx.executor().run_until_parked();
14060 let second_item_id = workspace
14061 .update(cx, |workspace, window, cx| {
14062 let active_item = workspace
14063 .active_item(cx)
14064 .expect("should have an active item after navigating into the 2nd buffer");
14065 let second_item_id = active_item.item_id();
14066 assert_ne!(
14067 second_item_id, multibuffer_item_id,
14068 "Should navigate away from the multibuffer"
14069 );
14070 assert_ne!(
14071 second_item_id, first_item_id,
14072 "Should navigate into the 2nd buffer and activate it"
14073 );
14074 assert!(
14075 active_item.is_singleton(cx),
14076 "New active item should be a singleton buffer"
14077 );
14078 assert_eq!(
14079 active_item
14080 .act_as::<Editor>(cx)
14081 .expect("should have navigated into an editor")
14082 .read(cx)
14083 .text(cx),
14084 sample_text_2
14085 );
14086
14087 workspace
14088 .go_back(workspace.active_pane().downgrade(), window, cx)
14089 .detach_and_log_err(cx);
14090
14091 second_item_id
14092 })
14093 .unwrap();
14094 cx.executor().run_until_parked();
14095 workspace
14096 .update(cx, |workspace, _, cx| {
14097 let active_item = workspace
14098 .active_item(cx)
14099 .expect("should have an active item after navigating back from the 2nd buffer");
14100 assert_eq!(
14101 active_item.item_id(),
14102 multibuffer_item_id,
14103 "Should navigate back from the 2nd buffer to the multi buffer"
14104 );
14105 assert!(!active_item.is_singleton(cx));
14106 })
14107 .unwrap();
14108
14109 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14110 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14111 s.select_ranges(Some(70..70))
14112 });
14113 editor.open_excerpts(&OpenExcerpts, window, cx);
14114 });
14115 cx.executor().run_until_parked();
14116 workspace
14117 .update(cx, |workspace, window, cx| {
14118 let active_item = workspace
14119 .active_item(cx)
14120 .expect("should have an active item after navigating into the 3rd buffer");
14121 let third_item_id = active_item.item_id();
14122 assert_ne!(
14123 third_item_id, multibuffer_item_id,
14124 "Should navigate into the 3rd buffer and activate it"
14125 );
14126 assert_ne!(third_item_id, first_item_id);
14127 assert_ne!(third_item_id, second_item_id);
14128 assert!(
14129 active_item.is_singleton(cx),
14130 "New active item should be a singleton buffer"
14131 );
14132 assert_eq!(
14133 active_item
14134 .act_as::<Editor>(cx)
14135 .expect("should have navigated into an editor")
14136 .read(cx)
14137 .text(cx),
14138 sample_text_3
14139 );
14140
14141 workspace
14142 .go_back(workspace.active_pane().downgrade(), window, cx)
14143 .detach_and_log_err(cx);
14144 })
14145 .unwrap();
14146 cx.executor().run_until_parked();
14147 workspace
14148 .update(cx, |workspace, _, cx| {
14149 let active_item = workspace
14150 .active_item(cx)
14151 .expect("should have an active item after navigating back from the 3rd buffer");
14152 assert_eq!(
14153 active_item.item_id(),
14154 multibuffer_item_id,
14155 "Should navigate back from the 3rd buffer to the multi buffer"
14156 );
14157 assert!(!active_item.is_singleton(cx));
14158 })
14159 .unwrap();
14160}
14161
14162#[gpui::test]
14163async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14164 init_test(cx, |_| {});
14165
14166 let mut cx = EditorTestContext::new(cx).await;
14167
14168 let diff_base = r#"
14169 use some::mod;
14170
14171 const A: u32 = 42;
14172
14173 fn main() {
14174 println!("hello");
14175
14176 println!("world");
14177 }
14178 "#
14179 .unindent();
14180
14181 cx.set_state(
14182 &r#"
14183 use some::modified;
14184
14185 ˇ
14186 fn main() {
14187 println!("hello there");
14188
14189 println!("around the");
14190 println!("world");
14191 }
14192 "#
14193 .unindent(),
14194 );
14195
14196 cx.set_head_text(&diff_base);
14197 executor.run_until_parked();
14198
14199 cx.update_editor(|editor, window, cx| {
14200 editor.go_to_next_hunk(&GoToHunk, window, cx);
14201 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14202 });
14203 executor.run_until_parked();
14204 cx.assert_state_with_diff(
14205 r#"
14206 use some::modified;
14207
14208
14209 fn main() {
14210 - println!("hello");
14211 + ˇ println!("hello there");
14212
14213 println!("around the");
14214 println!("world");
14215 }
14216 "#
14217 .unindent(),
14218 );
14219
14220 cx.update_editor(|editor, window, cx| {
14221 for _ in 0..2 {
14222 editor.go_to_next_hunk(&GoToHunk, window, cx);
14223 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14224 }
14225 });
14226 executor.run_until_parked();
14227 cx.assert_state_with_diff(
14228 r#"
14229 - use some::mod;
14230 + ˇuse some::modified;
14231
14232
14233 fn main() {
14234 - println!("hello");
14235 + println!("hello there");
14236
14237 + println!("around the");
14238 println!("world");
14239 }
14240 "#
14241 .unindent(),
14242 );
14243
14244 cx.update_editor(|editor, window, cx| {
14245 editor.go_to_next_hunk(&GoToHunk, window, cx);
14246 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14247 });
14248 executor.run_until_parked();
14249 cx.assert_state_with_diff(
14250 r#"
14251 - use some::mod;
14252 + use some::modified;
14253
14254 - const A: u32 = 42;
14255 ˇ
14256 fn main() {
14257 - println!("hello");
14258 + println!("hello there");
14259
14260 + println!("around the");
14261 println!("world");
14262 }
14263 "#
14264 .unindent(),
14265 );
14266
14267 cx.update_editor(|editor, window, cx| {
14268 editor.cancel(&Cancel, window, cx);
14269 });
14270
14271 cx.assert_state_with_diff(
14272 r#"
14273 use some::modified;
14274
14275 ˇ
14276 fn main() {
14277 println!("hello there");
14278
14279 println!("around the");
14280 println!("world");
14281 }
14282 "#
14283 .unindent(),
14284 );
14285}
14286
14287#[gpui::test]
14288async fn test_diff_base_change_with_expanded_diff_hunks(
14289 executor: BackgroundExecutor,
14290 cx: &mut TestAppContext,
14291) {
14292 init_test(cx, |_| {});
14293
14294 let mut cx = EditorTestContext::new(cx).await;
14295
14296 let diff_base = r#"
14297 use some::mod1;
14298 use some::mod2;
14299
14300 const A: u32 = 42;
14301 const B: u32 = 42;
14302 const C: u32 = 42;
14303
14304 fn main() {
14305 println!("hello");
14306
14307 println!("world");
14308 }
14309 "#
14310 .unindent();
14311
14312 cx.set_state(
14313 &r#"
14314 use some::mod2;
14315
14316 const A: u32 = 42;
14317 const C: u32 = 42;
14318
14319 fn main(ˇ) {
14320 //println!("hello");
14321
14322 println!("world");
14323 //
14324 //
14325 }
14326 "#
14327 .unindent(),
14328 );
14329
14330 cx.set_head_text(&diff_base);
14331 executor.run_until_parked();
14332
14333 cx.update_editor(|editor, window, cx| {
14334 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14335 });
14336 executor.run_until_parked();
14337 cx.assert_state_with_diff(
14338 r#"
14339 - use some::mod1;
14340 use some::mod2;
14341
14342 const A: u32 = 42;
14343 - const B: u32 = 42;
14344 const C: u32 = 42;
14345
14346 fn main(ˇ) {
14347 - println!("hello");
14348 + //println!("hello");
14349
14350 println!("world");
14351 + //
14352 + //
14353 }
14354 "#
14355 .unindent(),
14356 );
14357
14358 cx.set_head_text("new diff base!");
14359 executor.run_until_parked();
14360 cx.assert_state_with_diff(
14361 r#"
14362 - new diff base!
14363 + use some::mod2;
14364 +
14365 + const A: u32 = 42;
14366 + const C: u32 = 42;
14367 +
14368 + fn main(ˇ) {
14369 + //println!("hello");
14370 +
14371 + println!("world");
14372 + //
14373 + //
14374 + }
14375 "#
14376 .unindent(),
14377 );
14378}
14379
14380#[gpui::test]
14381async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14382 init_test(cx, |_| {});
14383
14384 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14385 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14386 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14387 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14388 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14389 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14390
14391 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14392 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14393 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14394
14395 let multi_buffer = cx.new(|cx| {
14396 let mut multibuffer = MultiBuffer::new(ReadWrite);
14397 multibuffer.push_excerpts(
14398 buffer_1.clone(),
14399 [
14400 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14401 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14402 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14403 ],
14404 cx,
14405 );
14406 multibuffer.push_excerpts(
14407 buffer_2.clone(),
14408 [
14409 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14410 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14411 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14412 ],
14413 cx,
14414 );
14415 multibuffer.push_excerpts(
14416 buffer_3.clone(),
14417 [
14418 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14419 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14420 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14421 ],
14422 cx,
14423 );
14424 multibuffer
14425 });
14426
14427 let editor =
14428 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14429 editor
14430 .update(cx, |editor, _window, cx| {
14431 for (buffer, diff_base) in [
14432 (buffer_1.clone(), file_1_old),
14433 (buffer_2.clone(), file_2_old),
14434 (buffer_3.clone(), file_3_old),
14435 ] {
14436 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14437 editor
14438 .buffer
14439 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14440 }
14441 })
14442 .unwrap();
14443
14444 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14445 cx.run_until_parked();
14446
14447 cx.assert_editor_state(
14448 &"
14449 ˇaaa
14450 ccc
14451 ddd
14452
14453 ggg
14454 hhh
14455
14456
14457 lll
14458 mmm
14459 NNN
14460
14461 qqq
14462 rrr
14463
14464 uuu
14465 111
14466 222
14467 333
14468
14469 666
14470 777
14471
14472 000
14473 !!!"
14474 .unindent(),
14475 );
14476
14477 cx.update_editor(|editor, window, cx| {
14478 editor.select_all(&SelectAll, window, cx);
14479 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14480 });
14481 cx.executor().run_until_parked();
14482
14483 cx.assert_state_with_diff(
14484 "
14485 «aaa
14486 - bbb
14487 ccc
14488 ddd
14489
14490 ggg
14491 hhh
14492
14493
14494 lll
14495 mmm
14496 - nnn
14497 + NNN
14498
14499 qqq
14500 rrr
14501
14502 uuu
14503 111
14504 222
14505 333
14506
14507 + 666
14508 777
14509
14510 000
14511 !!!ˇ»"
14512 .unindent(),
14513 );
14514}
14515
14516#[gpui::test]
14517async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14518 init_test(cx, |_| {});
14519
14520 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14521 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14522
14523 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14524 let multi_buffer = cx.new(|cx| {
14525 let mut multibuffer = MultiBuffer::new(ReadWrite);
14526 multibuffer.push_excerpts(
14527 buffer.clone(),
14528 [
14529 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
14530 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
14531 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
14532 ],
14533 cx,
14534 );
14535 multibuffer
14536 });
14537
14538 let editor =
14539 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14540 editor
14541 .update(cx, |editor, _window, cx| {
14542 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14543 editor
14544 .buffer
14545 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14546 })
14547 .unwrap();
14548
14549 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14550 cx.run_until_parked();
14551
14552 cx.update_editor(|editor, window, cx| {
14553 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14554 });
14555 cx.executor().run_until_parked();
14556
14557 // When the start of a hunk coincides with the start of its excerpt,
14558 // the hunk is expanded. When the start of a a hunk is earlier than
14559 // the start of its excerpt, the hunk is not expanded.
14560 cx.assert_state_with_diff(
14561 "
14562 ˇaaa
14563 - bbb
14564 + BBB
14565
14566 - ddd
14567 - eee
14568 + DDD
14569 + EEE
14570 fff
14571
14572 iii
14573 "
14574 .unindent(),
14575 );
14576}
14577
14578#[gpui::test]
14579async fn test_edits_around_expanded_insertion_hunks(
14580 executor: BackgroundExecutor,
14581 cx: &mut TestAppContext,
14582) {
14583 init_test(cx, |_| {});
14584
14585 let mut cx = EditorTestContext::new(cx).await;
14586
14587 let diff_base = r#"
14588 use some::mod1;
14589 use some::mod2;
14590
14591 const A: u32 = 42;
14592
14593 fn main() {
14594 println!("hello");
14595
14596 println!("world");
14597 }
14598 "#
14599 .unindent();
14600 executor.run_until_parked();
14601 cx.set_state(
14602 &r#"
14603 use some::mod1;
14604 use some::mod2;
14605
14606 const A: u32 = 42;
14607 const B: u32 = 42;
14608 const C: u32 = 42;
14609 ˇ
14610
14611 fn main() {
14612 println!("hello");
14613
14614 println!("world");
14615 }
14616 "#
14617 .unindent(),
14618 );
14619
14620 cx.set_head_text(&diff_base);
14621 executor.run_until_parked();
14622
14623 cx.update_editor(|editor, window, cx| {
14624 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14625 });
14626 executor.run_until_parked();
14627
14628 cx.assert_state_with_diff(
14629 r#"
14630 use some::mod1;
14631 use some::mod2;
14632
14633 const A: u32 = 42;
14634 + const B: u32 = 42;
14635 + const C: u32 = 42;
14636 + ˇ
14637
14638 fn main() {
14639 println!("hello");
14640
14641 println!("world");
14642 }
14643 "#
14644 .unindent(),
14645 );
14646
14647 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14648 executor.run_until_parked();
14649
14650 cx.assert_state_with_diff(
14651 r#"
14652 use some::mod1;
14653 use some::mod2;
14654
14655 const A: u32 = 42;
14656 + const B: u32 = 42;
14657 + const C: u32 = 42;
14658 + const D: u32 = 42;
14659 + ˇ
14660
14661 fn main() {
14662 println!("hello");
14663
14664 println!("world");
14665 }
14666 "#
14667 .unindent(),
14668 );
14669
14670 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14671 executor.run_until_parked();
14672
14673 cx.assert_state_with_diff(
14674 r#"
14675 use some::mod1;
14676 use some::mod2;
14677
14678 const A: u32 = 42;
14679 + const B: u32 = 42;
14680 + const C: u32 = 42;
14681 + const D: u32 = 42;
14682 + const E: u32 = 42;
14683 + ˇ
14684
14685 fn main() {
14686 println!("hello");
14687
14688 println!("world");
14689 }
14690 "#
14691 .unindent(),
14692 );
14693
14694 cx.update_editor(|editor, window, cx| {
14695 editor.delete_line(&DeleteLine, window, cx);
14696 });
14697 executor.run_until_parked();
14698
14699 cx.assert_state_with_diff(
14700 r#"
14701 use some::mod1;
14702 use some::mod2;
14703
14704 const A: u32 = 42;
14705 + const B: u32 = 42;
14706 + const C: u32 = 42;
14707 + const D: u32 = 42;
14708 + const E: u32 = 42;
14709 ˇ
14710 fn main() {
14711 println!("hello");
14712
14713 println!("world");
14714 }
14715 "#
14716 .unindent(),
14717 );
14718
14719 cx.update_editor(|editor, window, cx| {
14720 editor.move_up(&MoveUp, window, cx);
14721 editor.delete_line(&DeleteLine, window, cx);
14722 editor.move_up(&MoveUp, window, cx);
14723 editor.delete_line(&DeleteLine, window, cx);
14724 editor.move_up(&MoveUp, window, cx);
14725 editor.delete_line(&DeleteLine, window, cx);
14726 });
14727 executor.run_until_parked();
14728 cx.assert_state_with_diff(
14729 r#"
14730 use some::mod1;
14731 use some::mod2;
14732
14733 const A: u32 = 42;
14734 + const B: u32 = 42;
14735 ˇ
14736 fn main() {
14737 println!("hello");
14738
14739 println!("world");
14740 }
14741 "#
14742 .unindent(),
14743 );
14744
14745 cx.update_editor(|editor, window, cx| {
14746 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14747 editor.delete_line(&DeleteLine, window, cx);
14748 });
14749 executor.run_until_parked();
14750 cx.assert_state_with_diff(
14751 r#"
14752 ˇ
14753 fn main() {
14754 println!("hello");
14755
14756 println!("world");
14757 }
14758 "#
14759 .unindent(),
14760 );
14761}
14762
14763#[gpui::test]
14764async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14765 init_test(cx, |_| {});
14766
14767 let mut cx = EditorTestContext::new(cx).await;
14768 cx.set_head_text(indoc! { "
14769 one
14770 two
14771 three
14772 four
14773 five
14774 "
14775 });
14776 cx.set_state(indoc! { "
14777 one
14778 ˇthree
14779 five
14780 "});
14781 cx.run_until_parked();
14782 cx.update_editor(|editor, window, cx| {
14783 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14784 });
14785 cx.assert_state_with_diff(
14786 indoc! { "
14787 one
14788 - two
14789 ˇthree
14790 - four
14791 five
14792 "}
14793 .to_string(),
14794 );
14795 cx.update_editor(|editor, window, cx| {
14796 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14797 });
14798
14799 cx.assert_state_with_diff(
14800 indoc! { "
14801 one
14802 ˇthree
14803 five
14804 "}
14805 .to_string(),
14806 );
14807
14808 cx.set_state(indoc! { "
14809 one
14810 ˇTWO
14811 three
14812 four
14813 five
14814 "});
14815 cx.run_until_parked();
14816 cx.update_editor(|editor, window, cx| {
14817 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14818 });
14819
14820 cx.assert_state_with_diff(
14821 indoc! { "
14822 one
14823 - two
14824 + ˇTWO
14825 three
14826 four
14827 five
14828 "}
14829 .to_string(),
14830 );
14831 cx.update_editor(|editor, window, cx| {
14832 editor.move_up(&Default::default(), window, cx);
14833 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14834 });
14835 cx.assert_state_with_diff(
14836 indoc! { "
14837 one
14838 ˇTWO
14839 three
14840 four
14841 five
14842 "}
14843 .to_string(),
14844 );
14845}
14846
14847#[gpui::test]
14848async fn test_edits_around_expanded_deletion_hunks(
14849 executor: BackgroundExecutor,
14850 cx: &mut TestAppContext,
14851) {
14852 init_test(cx, |_| {});
14853
14854 let mut cx = EditorTestContext::new(cx).await;
14855
14856 let diff_base = r#"
14857 use some::mod1;
14858 use some::mod2;
14859
14860 const A: u32 = 42;
14861 const B: u32 = 42;
14862 const C: u32 = 42;
14863
14864
14865 fn main() {
14866 println!("hello");
14867
14868 println!("world");
14869 }
14870 "#
14871 .unindent();
14872 executor.run_until_parked();
14873 cx.set_state(
14874 &r#"
14875 use some::mod1;
14876 use some::mod2;
14877
14878 ˇconst B: u32 = 42;
14879 const C: u32 = 42;
14880
14881
14882 fn main() {
14883 println!("hello");
14884
14885 println!("world");
14886 }
14887 "#
14888 .unindent(),
14889 );
14890
14891 cx.set_head_text(&diff_base);
14892 executor.run_until_parked();
14893
14894 cx.update_editor(|editor, window, cx| {
14895 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14896 });
14897 executor.run_until_parked();
14898
14899 cx.assert_state_with_diff(
14900 r#"
14901 use some::mod1;
14902 use some::mod2;
14903
14904 - const A: u32 = 42;
14905 ˇconst B: u32 = 42;
14906 const C: u32 = 42;
14907
14908
14909 fn main() {
14910 println!("hello");
14911
14912 println!("world");
14913 }
14914 "#
14915 .unindent(),
14916 );
14917
14918 cx.update_editor(|editor, window, cx| {
14919 editor.delete_line(&DeleteLine, window, cx);
14920 });
14921 executor.run_until_parked();
14922 cx.assert_state_with_diff(
14923 r#"
14924 use some::mod1;
14925 use some::mod2;
14926
14927 - const A: u32 = 42;
14928 - const B: u32 = 42;
14929 ˇconst C: u32 = 42;
14930
14931
14932 fn main() {
14933 println!("hello");
14934
14935 println!("world");
14936 }
14937 "#
14938 .unindent(),
14939 );
14940
14941 cx.update_editor(|editor, window, cx| {
14942 editor.delete_line(&DeleteLine, window, cx);
14943 });
14944 executor.run_until_parked();
14945 cx.assert_state_with_diff(
14946 r#"
14947 use some::mod1;
14948 use some::mod2;
14949
14950 - const A: u32 = 42;
14951 - const B: u32 = 42;
14952 - const C: u32 = 42;
14953 ˇ
14954
14955 fn main() {
14956 println!("hello");
14957
14958 println!("world");
14959 }
14960 "#
14961 .unindent(),
14962 );
14963
14964 cx.update_editor(|editor, window, cx| {
14965 editor.handle_input("replacement", window, cx);
14966 });
14967 executor.run_until_parked();
14968 cx.assert_state_with_diff(
14969 r#"
14970 use some::mod1;
14971 use some::mod2;
14972
14973 - const A: u32 = 42;
14974 - const B: u32 = 42;
14975 - const C: u32 = 42;
14976 -
14977 + replacementˇ
14978
14979 fn main() {
14980 println!("hello");
14981
14982 println!("world");
14983 }
14984 "#
14985 .unindent(),
14986 );
14987}
14988
14989#[gpui::test]
14990async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14991 init_test(cx, |_| {});
14992
14993 let mut cx = EditorTestContext::new(cx).await;
14994
14995 let base_text = r#"
14996 one
14997 two
14998 three
14999 four
15000 five
15001 "#
15002 .unindent();
15003 executor.run_until_parked();
15004 cx.set_state(
15005 &r#"
15006 one
15007 two
15008 fˇour
15009 five
15010 "#
15011 .unindent(),
15012 );
15013
15014 cx.set_head_text(&base_text);
15015 executor.run_until_parked();
15016
15017 cx.update_editor(|editor, window, cx| {
15018 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15019 });
15020 executor.run_until_parked();
15021
15022 cx.assert_state_with_diff(
15023 r#"
15024 one
15025 two
15026 - three
15027 fˇour
15028 five
15029 "#
15030 .unindent(),
15031 );
15032
15033 cx.update_editor(|editor, window, cx| {
15034 editor.backspace(&Backspace, window, cx);
15035 editor.backspace(&Backspace, window, cx);
15036 });
15037 executor.run_until_parked();
15038 cx.assert_state_with_diff(
15039 r#"
15040 one
15041 two
15042 - threeˇ
15043 - four
15044 + our
15045 five
15046 "#
15047 .unindent(),
15048 );
15049}
15050
15051#[gpui::test]
15052async fn test_edit_after_expanded_modification_hunk(
15053 executor: BackgroundExecutor,
15054 cx: &mut TestAppContext,
15055) {
15056 init_test(cx, |_| {});
15057
15058 let mut cx = EditorTestContext::new(cx).await;
15059
15060 let diff_base = r#"
15061 use some::mod1;
15062 use some::mod2;
15063
15064 const A: u32 = 42;
15065 const B: u32 = 42;
15066 const C: u32 = 42;
15067 const D: u32 = 42;
15068
15069
15070 fn main() {
15071 println!("hello");
15072
15073 println!("world");
15074 }"#
15075 .unindent();
15076
15077 cx.set_state(
15078 &r#"
15079 use some::mod1;
15080 use some::mod2;
15081
15082 const A: u32 = 42;
15083 const B: u32 = 42;
15084 const C: u32 = 43ˇ
15085 const D: u32 = 42;
15086
15087
15088 fn main() {
15089 println!("hello");
15090
15091 println!("world");
15092 }"#
15093 .unindent(),
15094 );
15095
15096 cx.set_head_text(&diff_base);
15097 executor.run_until_parked();
15098 cx.update_editor(|editor, window, cx| {
15099 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15100 });
15101 executor.run_until_parked();
15102
15103 cx.assert_state_with_diff(
15104 r#"
15105 use some::mod1;
15106 use some::mod2;
15107
15108 const A: u32 = 42;
15109 const B: u32 = 42;
15110 - const C: u32 = 42;
15111 + const C: u32 = 43ˇ
15112 const D: u32 = 42;
15113
15114
15115 fn main() {
15116 println!("hello");
15117
15118 println!("world");
15119 }"#
15120 .unindent(),
15121 );
15122
15123 cx.update_editor(|editor, window, cx| {
15124 editor.handle_input("\nnew_line\n", window, cx);
15125 });
15126 executor.run_until_parked();
15127
15128 cx.assert_state_with_diff(
15129 r#"
15130 use some::mod1;
15131 use some::mod2;
15132
15133 const A: u32 = 42;
15134 const B: u32 = 42;
15135 - const C: u32 = 42;
15136 + const C: u32 = 43
15137 + new_line
15138 + ˇ
15139 const D: u32 = 42;
15140
15141
15142 fn main() {
15143 println!("hello");
15144
15145 println!("world");
15146 }"#
15147 .unindent(),
15148 );
15149}
15150
15151#[gpui::test]
15152async fn test_stage_and_unstage_added_file_hunk(
15153 executor: BackgroundExecutor,
15154 cx: &mut TestAppContext,
15155) {
15156 init_test(cx, |_| {});
15157
15158 let mut cx = EditorTestContext::new(cx).await;
15159 cx.update_editor(|editor, _, cx| {
15160 editor.set_expand_all_diff_hunks(cx);
15161 });
15162
15163 let working_copy = r#"
15164 ˇfn main() {
15165 println!("hello, world!");
15166 }
15167 "#
15168 .unindent();
15169
15170 cx.set_state(&working_copy);
15171 executor.run_until_parked();
15172
15173 cx.assert_state_with_diff(
15174 r#"
15175 + ˇfn main() {
15176 + println!("hello, world!");
15177 + }
15178 "#
15179 .unindent(),
15180 );
15181 cx.assert_index_text(None);
15182
15183 cx.update_editor(|editor, window, cx| {
15184 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15185 });
15186 executor.run_until_parked();
15187 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15188 cx.assert_state_with_diff(
15189 r#"
15190 + ˇfn main() {
15191 + println!("hello, world!");
15192 + }
15193 "#
15194 .unindent(),
15195 );
15196
15197 cx.update_editor(|editor, window, cx| {
15198 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15199 });
15200 executor.run_until_parked();
15201 cx.assert_index_text(None);
15202}
15203
15204async fn setup_indent_guides_editor(
15205 text: &str,
15206 cx: &mut TestAppContext,
15207) -> (BufferId, EditorTestContext) {
15208 init_test(cx, |_| {});
15209
15210 let mut cx = EditorTestContext::new(cx).await;
15211
15212 let buffer_id = cx.update_editor(|editor, window, cx| {
15213 editor.set_text(text, window, cx);
15214 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15215
15216 buffer_ids[0]
15217 });
15218
15219 (buffer_id, cx)
15220}
15221
15222fn assert_indent_guides(
15223 range: Range<u32>,
15224 expected: Vec<IndentGuide>,
15225 active_indices: Option<Vec<usize>>,
15226 cx: &mut EditorTestContext,
15227) {
15228 let indent_guides = cx.update_editor(|editor, window, cx| {
15229 let snapshot = editor.snapshot(window, cx).display_snapshot;
15230 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15231 editor,
15232 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15233 true,
15234 &snapshot,
15235 cx,
15236 );
15237
15238 indent_guides.sort_by(|a, b| {
15239 a.depth.cmp(&b.depth).then(
15240 a.start_row
15241 .cmp(&b.start_row)
15242 .then(a.end_row.cmp(&b.end_row)),
15243 )
15244 });
15245 indent_guides
15246 });
15247
15248 if let Some(expected) = active_indices {
15249 let active_indices = cx.update_editor(|editor, window, cx| {
15250 let snapshot = editor.snapshot(window, cx).display_snapshot;
15251 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15252 });
15253
15254 assert_eq!(
15255 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15256 expected,
15257 "Active indent guide indices do not match"
15258 );
15259 }
15260
15261 assert_eq!(indent_guides, expected, "Indent guides do not match");
15262}
15263
15264fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15265 IndentGuide {
15266 buffer_id,
15267 start_row: MultiBufferRow(start_row),
15268 end_row: MultiBufferRow(end_row),
15269 depth,
15270 tab_size: 4,
15271 settings: IndentGuideSettings {
15272 enabled: true,
15273 line_width: 1,
15274 active_line_width: 1,
15275 ..Default::default()
15276 },
15277 }
15278}
15279
15280#[gpui::test]
15281async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15282 let (buffer_id, mut cx) = setup_indent_guides_editor(
15283 &"
15284 fn main() {
15285 let a = 1;
15286 }"
15287 .unindent(),
15288 cx,
15289 )
15290 .await;
15291
15292 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15293}
15294
15295#[gpui::test]
15296async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15297 let (buffer_id, mut cx) = setup_indent_guides_editor(
15298 &"
15299 fn main() {
15300 let a = 1;
15301 let b = 2;
15302 }"
15303 .unindent(),
15304 cx,
15305 )
15306 .await;
15307
15308 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15309}
15310
15311#[gpui::test]
15312async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15313 let (buffer_id, mut cx) = setup_indent_guides_editor(
15314 &"
15315 fn main() {
15316 let a = 1;
15317 if a == 3 {
15318 let b = 2;
15319 } else {
15320 let c = 3;
15321 }
15322 }"
15323 .unindent(),
15324 cx,
15325 )
15326 .await;
15327
15328 assert_indent_guides(
15329 0..8,
15330 vec![
15331 indent_guide(buffer_id, 1, 6, 0),
15332 indent_guide(buffer_id, 3, 3, 1),
15333 indent_guide(buffer_id, 5, 5, 1),
15334 ],
15335 None,
15336 &mut cx,
15337 );
15338}
15339
15340#[gpui::test]
15341async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15342 let (buffer_id, mut cx) = setup_indent_guides_editor(
15343 &"
15344 fn main() {
15345 let a = 1;
15346 let b = 2;
15347 let c = 3;
15348 }"
15349 .unindent(),
15350 cx,
15351 )
15352 .await;
15353
15354 assert_indent_guides(
15355 0..5,
15356 vec![
15357 indent_guide(buffer_id, 1, 3, 0),
15358 indent_guide(buffer_id, 2, 2, 1),
15359 ],
15360 None,
15361 &mut cx,
15362 );
15363}
15364
15365#[gpui::test]
15366async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15367 let (buffer_id, mut cx) = setup_indent_guides_editor(
15368 &"
15369 fn main() {
15370 let a = 1;
15371
15372 let c = 3;
15373 }"
15374 .unindent(),
15375 cx,
15376 )
15377 .await;
15378
15379 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15380}
15381
15382#[gpui::test]
15383async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15384 let (buffer_id, mut cx) = setup_indent_guides_editor(
15385 &"
15386 fn main() {
15387 let a = 1;
15388
15389 let c = 3;
15390
15391 if a == 3 {
15392 let b = 2;
15393 } else {
15394 let c = 3;
15395 }
15396 }"
15397 .unindent(),
15398 cx,
15399 )
15400 .await;
15401
15402 assert_indent_guides(
15403 0..11,
15404 vec![
15405 indent_guide(buffer_id, 1, 9, 0),
15406 indent_guide(buffer_id, 6, 6, 1),
15407 indent_guide(buffer_id, 8, 8, 1),
15408 ],
15409 None,
15410 &mut cx,
15411 );
15412}
15413
15414#[gpui::test]
15415async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15416 let (buffer_id, mut cx) = setup_indent_guides_editor(
15417 &"
15418 fn main() {
15419 let a = 1;
15420
15421 let c = 3;
15422
15423 if a == 3 {
15424 let b = 2;
15425 } else {
15426 let c = 3;
15427 }
15428 }"
15429 .unindent(),
15430 cx,
15431 )
15432 .await;
15433
15434 assert_indent_guides(
15435 1..11,
15436 vec![
15437 indent_guide(buffer_id, 1, 9, 0),
15438 indent_guide(buffer_id, 6, 6, 1),
15439 indent_guide(buffer_id, 8, 8, 1),
15440 ],
15441 None,
15442 &mut cx,
15443 );
15444}
15445
15446#[gpui::test]
15447async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15448 let (buffer_id, mut cx) = setup_indent_guides_editor(
15449 &"
15450 fn main() {
15451 let a = 1;
15452
15453 let c = 3;
15454
15455 if a == 3 {
15456 let b = 2;
15457 } else {
15458 let c = 3;
15459 }
15460 }"
15461 .unindent(),
15462 cx,
15463 )
15464 .await;
15465
15466 assert_indent_guides(
15467 1..10,
15468 vec![
15469 indent_guide(buffer_id, 1, 9, 0),
15470 indent_guide(buffer_id, 6, 6, 1),
15471 indent_guide(buffer_id, 8, 8, 1),
15472 ],
15473 None,
15474 &mut cx,
15475 );
15476}
15477
15478#[gpui::test]
15479async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15480 let (buffer_id, mut cx) = setup_indent_guides_editor(
15481 &"
15482 block1
15483 block2
15484 block3
15485 block4
15486 block2
15487 block1
15488 block1"
15489 .unindent(),
15490 cx,
15491 )
15492 .await;
15493
15494 assert_indent_guides(
15495 1..10,
15496 vec![
15497 indent_guide(buffer_id, 1, 4, 0),
15498 indent_guide(buffer_id, 2, 3, 1),
15499 indent_guide(buffer_id, 3, 3, 2),
15500 ],
15501 None,
15502 &mut cx,
15503 );
15504}
15505
15506#[gpui::test]
15507async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15508 let (buffer_id, mut cx) = setup_indent_guides_editor(
15509 &"
15510 block1
15511 block2
15512 block3
15513
15514 block1
15515 block1"
15516 .unindent(),
15517 cx,
15518 )
15519 .await;
15520
15521 assert_indent_guides(
15522 0..6,
15523 vec![
15524 indent_guide(buffer_id, 1, 2, 0),
15525 indent_guide(buffer_id, 2, 2, 1),
15526 ],
15527 None,
15528 &mut cx,
15529 );
15530}
15531
15532#[gpui::test]
15533async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15534 let (buffer_id, mut cx) = setup_indent_guides_editor(
15535 &"
15536 block1
15537
15538
15539
15540 block2
15541 "
15542 .unindent(),
15543 cx,
15544 )
15545 .await;
15546
15547 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15548}
15549
15550#[gpui::test]
15551async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15552 let (buffer_id, mut cx) = setup_indent_guides_editor(
15553 &"
15554 def a:
15555 \tb = 3
15556 \tif True:
15557 \t\tc = 4
15558 \t\td = 5
15559 \tprint(b)
15560 "
15561 .unindent(),
15562 cx,
15563 )
15564 .await;
15565
15566 assert_indent_guides(
15567 0..6,
15568 vec![
15569 indent_guide(buffer_id, 1, 6, 0),
15570 indent_guide(buffer_id, 3, 4, 1),
15571 ],
15572 None,
15573 &mut cx,
15574 );
15575}
15576
15577#[gpui::test]
15578async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15579 let (buffer_id, mut cx) = setup_indent_guides_editor(
15580 &"
15581 fn main() {
15582 let a = 1;
15583 }"
15584 .unindent(),
15585 cx,
15586 )
15587 .await;
15588
15589 cx.update_editor(|editor, window, cx| {
15590 editor.change_selections(None, window, cx, |s| {
15591 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15592 });
15593 });
15594
15595 assert_indent_guides(
15596 0..3,
15597 vec![indent_guide(buffer_id, 1, 1, 0)],
15598 Some(vec![0]),
15599 &mut cx,
15600 );
15601}
15602
15603#[gpui::test]
15604async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15605 let (buffer_id, mut cx) = setup_indent_guides_editor(
15606 &"
15607 fn main() {
15608 if 1 == 2 {
15609 let a = 1;
15610 }
15611 }"
15612 .unindent(),
15613 cx,
15614 )
15615 .await;
15616
15617 cx.update_editor(|editor, window, cx| {
15618 editor.change_selections(None, window, cx, |s| {
15619 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15620 });
15621 });
15622
15623 assert_indent_guides(
15624 0..4,
15625 vec![
15626 indent_guide(buffer_id, 1, 3, 0),
15627 indent_guide(buffer_id, 2, 2, 1),
15628 ],
15629 Some(vec![1]),
15630 &mut cx,
15631 );
15632
15633 cx.update_editor(|editor, window, cx| {
15634 editor.change_selections(None, window, cx, |s| {
15635 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15636 });
15637 });
15638
15639 assert_indent_guides(
15640 0..4,
15641 vec![
15642 indent_guide(buffer_id, 1, 3, 0),
15643 indent_guide(buffer_id, 2, 2, 1),
15644 ],
15645 Some(vec![1]),
15646 &mut cx,
15647 );
15648
15649 cx.update_editor(|editor, window, cx| {
15650 editor.change_selections(None, window, cx, |s| {
15651 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15652 });
15653 });
15654
15655 assert_indent_guides(
15656 0..4,
15657 vec![
15658 indent_guide(buffer_id, 1, 3, 0),
15659 indent_guide(buffer_id, 2, 2, 1),
15660 ],
15661 Some(vec![0]),
15662 &mut cx,
15663 );
15664}
15665
15666#[gpui::test]
15667async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15668 let (buffer_id, mut cx) = setup_indent_guides_editor(
15669 &"
15670 fn main() {
15671 let a = 1;
15672
15673 let b = 2;
15674 }"
15675 .unindent(),
15676 cx,
15677 )
15678 .await;
15679
15680 cx.update_editor(|editor, window, cx| {
15681 editor.change_selections(None, window, cx, |s| {
15682 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15683 });
15684 });
15685
15686 assert_indent_guides(
15687 0..5,
15688 vec![indent_guide(buffer_id, 1, 3, 0)],
15689 Some(vec![0]),
15690 &mut cx,
15691 );
15692}
15693
15694#[gpui::test]
15695async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15696 let (buffer_id, mut cx) = setup_indent_guides_editor(
15697 &"
15698 def m:
15699 a = 1
15700 pass"
15701 .unindent(),
15702 cx,
15703 )
15704 .await;
15705
15706 cx.update_editor(|editor, window, cx| {
15707 editor.change_selections(None, window, cx, |s| {
15708 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15709 });
15710 });
15711
15712 assert_indent_guides(
15713 0..3,
15714 vec![indent_guide(buffer_id, 1, 2, 0)],
15715 Some(vec![0]),
15716 &mut cx,
15717 );
15718}
15719
15720#[gpui::test]
15721async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15722 init_test(cx, |_| {});
15723 let mut cx = EditorTestContext::new(cx).await;
15724 let text = indoc! {
15725 "
15726 impl A {
15727 fn b() {
15728 0;
15729 3;
15730 5;
15731 6;
15732 7;
15733 }
15734 }
15735 "
15736 };
15737 let base_text = indoc! {
15738 "
15739 impl A {
15740 fn b() {
15741 0;
15742 1;
15743 2;
15744 3;
15745 4;
15746 }
15747 fn c() {
15748 5;
15749 6;
15750 7;
15751 }
15752 }
15753 "
15754 };
15755
15756 cx.update_editor(|editor, window, cx| {
15757 editor.set_text(text, window, cx);
15758
15759 editor.buffer().update(cx, |multibuffer, cx| {
15760 let buffer = multibuffer.as_singleton().unwrap();
15761 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15762
15763 multibuffer.set_all_diff_hunks_expanded(cx);
15764 multibuffer.add_diff(diff, cx);
15765
15766 buffer.read(cx).remote_id()
15767 })
15768 });
15769 cx.run_until_parked();
15770
15771 cx.assert_state_with_diff(
15772 indoc! { "
15773 impl A {
15774 fn b() {
15775 0;
15776 - 1;
15777 - 2;
15778 3;
15779 - 4;
15780 - }
15781 - fn c() {
15782 5;
15783 6;
15784 7;
15785 }
15786 }
15787 ˇ"
15788 }
15789 .to_string(),
15790 );
15791
15792 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15793 editor
15794 .snapshot(window, cx)
15795 .buffer_snapshot
15796 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15797 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15798 .collect::<Vec<_>>()
15799 });
15800 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15801 assert_eq!(
15802 actual_guides,
15803 vec![
15804 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15805 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15806 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15807 ]
15808 );
15809}
15810
15811#[gpui::test]
15812async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15813 init_test(cx, |_| {});
15814 let mut cx = EditorTestContext::new(cx).await;
15815
15816 let diff_base = r#"
15817 a
15818 b
15819 c
15820 "#
15821 .unindent();
15822
15823 cx.set_state(
15824 &r#"
15825 ˇA
15826 b
15827 C
15828 "#
15829 .unindent(),
15830 );
15831 cx.set_head_text(&diff_base);
15832 cx.update_editor(|editor, window, cx| {
15833 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15834 });
15835 executor.run_until_parked();
15836
15837 let both_hunks_expanded = r#"
15838 - a
15839 + ˇA
15840 b
15841 - c
15842 + C
15843 "#
15844 .unindent();
15845
15846 cx.assert_state_with_diff(both_hunks_expanded.clone());
15847
15848 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15849 let snapshot = editor.snapshot(window, cx);
15850 let hunks = editor
15851 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15852 .collect::<Vec<_>>();
15853 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15854 let buffer_id = hunks[0].buffer_id;
15855 hunks
15856 .into_iter()
15857 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15858 .collect::<Vec<_>>()
15859 });
15860 assert_eq!(hunk_ranges.len(), 2);
15861
15862 cx.update_editor(|editor, _, cx| {
15863 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15864 });
15865 executor.run_until_parked();
15866
15867 let second_hunk_expanded = r#"
15868 ˇA
15869 b
15870 - c
15871 + C
15872 "#
15873 .unindent();
15874
15875 cx.assert_state_with_diff(second_hunk_expanded);
15876
15877 cx.update_editor(|editor, _, cx| {
15878 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15879 });
15880 executor.run_until_parked();
15881
15882 cx.assert_state_with_diff(both_hunks_expanded.clone());
15883
15884 cx.update_editor(|editor, _, cx| {
15885 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15886 });
15887 executor.run_until_parked();
15888
15889 let first_hunk_expanded = r#"
15890 - a
15891 + ˇA
15892 b
15893 C
15894 "#
15895 .unindent();
15896
15897 cx.assert_state_with_diff(first_hunk_expanded);
15898
15899 cx.update_editor(|editor, _, cx| {
15900 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15901 });
15902 executor.run_until_parked();
15903
15904 cx.assert_state_with_diff(both_hunks_expanded);
15905
15906 cx.set_state(
15907 &r#"
15908 ˇA
15909 b
15910 "#
15911 .unindent(),
15912 );
15913 cx.run_until_parked();
15914
15915 // TODO this cursor position seems bad
15916 cx.assert_state_with_diff(
15917 r#"
15918 - ˇa
15919 + A
15920 b
15921 "#
15922 .unindent(),
15923 );
15924
15925 cx.update_editor(|editor, window, cx| {
15926 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15927 });
15928
15929 cx.assert_state_with_diff(
15930 r#"
15931 - ˇa
15932 + A
15933 b
15934 - c
15935 "#
15936 .unindent(),
15937 );
15938
15939 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15940 let snapshot = editor.snapshot(window, cx);
15941 let hunks = editor
15942 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15943 .collect::<Vec<_>>();
15944 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15945 let buffer_id = hunks[0].buffer_id;
15946 hunks
15947 .into_iter()
15948 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15949 .collect::<Vec<_>>()
15950 });
15951 assert_eq!(hunk_ranges.len(), 2);
15952
15953 cx.update_editor(|editor, _, cx| {
15954 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15955 });
15956 executor.run_until_parked();
15957
15958 cx.assert_state_with_diff(
15959 r#"
15960 - ˇa
15961 + A
15962 b
15963 "#
15964 .unindent(),
15965 );
15966}
15967
15968#[gpui::test]
15969async fn test_toggle_deletion_hunk_at_start_of_file(
15970 executor: BackgroundExecutor,
15971 cx: &mut TestAppContext,
15972) {
15973 init_test(cx, |_| {});
15974 let mut cx = EditorTestContext::new(cx).await;
15975
15976 let diff_base = r#"
15977 a
15978 b
15979 c
15980 "#
15981 .unindent();
15982
15983 cx.set_state(
15984 &r#"
15985 ˇb
15986 c
15987 "#
15988 .unindent(),
15989 );
15990 cx.set_head_text(&diff_base);
15991 cx.update_editor(|editor, window, cx| {
15992 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15993 });
15994 executor.run_until_parked();
15995
15996 let hunk_expanded = r#"
15997 - a
15998 ˇb
15999 c
16000 "#
16001 .unindent();
16002
16003 cx.assert_state_with_diff(hunk_expanded.clone());
16004
16005 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16006 let snapshot = editor.snapshot(window, cx);
16007 let hunks = editor
16008 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16009 .collect::<Vec<_>>();
16010 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16011 let buffer_id = hunks[0].buffer_id;
16012 hunks
16013 .into_iter()
16014 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16015 .collect::<Vec<_>>()
16016 });
16017 assert_eq!(hunk_ranges.len(), 1);
16018
16019 cx.update_editor(|editor, _, cx| {
16020 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16021 });
16022 executor.run_until_parked();
16023
16024 let hunk_collapsed = r#"
16025 ˇb
16026 c
16027 "#
16028 .unindent();
16029
16030 cx.assert_state_with_diff(hunk_collapsed);
16031
16032 cx.update_editor(|editor, _, cx| {
16033 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16034 });
16035 executor.run_until_parked();
16036
16037 cx.assert_state_with_diff(hunk_expanded.clone());
16038}
16039
16040#[gpui::test]
16041async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16042 init_test(cx, |_| {});
16043
16044 let fs = FakeFs::new(cx.executor());
16045 fs.insert_tree(
16046 path!("/test"),
16047 json!({
16048 ".git": {},
16049 "file-1": "ONE\n",
16050 "file-2": "TWO\n",
16051 "file-3": "THREE\n",
16052 }),
16053 )
16054 .await;
16055
16056 fs.set_head_for_repo(
16057 path!("/test/.git").as_ref(),
16058 &[
16059 ("file-1".into(), "one\n".into()),
16060 ("file-2".into(), "two\n".into()),
16061 ("file-3".into(), "three\n".into()),
16062 ],
16063 );
16064
16065 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16066 let mut buffers = vec![];
16067 for i in 1..=3 {
16068 let buffer = project
16069 .update(cx, |project, cx| {
16070 let path = format!(path!("/test/file-{}"), i);
16071 project.open_local_buffer(path, cx)
16072 })
16073 .await
16074 .unwrap();
16075 buffers.push(buffer);
16076 }
16077
16078 let multibuffer = cx.new(|cx| {
16079 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16080 multibuffer.set_all_diff_hunks_expanded(cx);
16081 for buffer in &buffers {
16082 let snapshot = buffer.read(cx).snapshot();
16083 multibuffer.set_excerpts_for_path(
16084 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16085 buffer.clone(),
16086 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16087 DEFAULT_MULTIBUFFER_CONTEXT,
16088 cx,
16089 );
16090 }
16091 multibuffer
16092 });
16093
16094 let editor = cx.add_window(|window, cx| {
16095 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16096 });
16097 cx.run_until_parked();
16098
16099 let snapshot = editor
16100 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16101 .unwrap();
16102 let hunks = snapshot
16103 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16104 .map(|hunk| match hunk {
16105 DisplayDiffHunk::Unfolded {
16106 display_row_range, ..
16107 } => display_row_range,
16108 DisplayDiffHunk::Folded { .. } => unreachable!(),
16109 })
16110 .collect::<Vec<_>>();
16111 assert_eq!(
16112 hunks,
16113 [
16114 DisplayRow(2)..DisplayRow(4),
16115 DisplayRow(7)..DisplayRow(9),
16116 DisplayRow(12)..DisplayRow(14),
16117 ]
16118 );
16119}
16120
16121#[gpui::test]
16122async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16123 init_test(cx, |_| {});
16124
16125 let mut cx = EditorTestContext::new(cx).await;
16126 cx.set_head_text(indoc! { "
16127 one
16128 two
16129 three
16130 four
16131 five
16132 "
16133 });
16134 cx.set_index_text(indoc! { "
16135 one
16136 two
16137 three
16138 four
16139 five
16140 "
16141 });
16142 cx.set_state(indoc! {"
16143 one
16144 TWO
16145 ˇTHREE
16146 FOUR
16147 five
16148 "});
16149 cx.run_until_parked();
16150 cx.update_editor(|editor, window, cx| {
16151 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16152 });
16153 cx.run_until_parked();
16154 cx.assert_index_text(Some(indoc! {"
16155 one
16156 TWO
16157 THREE
16158 FOUR
16159 five
16160 "}));
16161 cx.set_state(indoc! { "
16162 one
16163 TWO
16164 ˇTHREE-HUNDRED
16165 FOUR
16166 five
16167 "});
16168 cx.run_until_parked();
16169 cx.update_editor(|editor, window, cx| {
16170 let snapshot = editor.snapshot(window, cx);
16171 let hunks = editor
16172 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16173 .collect::<Vec<_>>();
16174 assert_eq!(hunks.len(), 1);
16175 assert_eq!(
16176 hunks[0].status(),
16177 DiffHunkStatus {
16178 kind: DiffHunkStatusKind::Modified,
16179 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16180 }
16181 );
16182
16183 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16184 });
16185 cx.run_until_parked();
16186 cx.assert_index_text(Some(indoc! {"
16187 one
16188 TWO
16189 THREE-HUNDRED
16190 FOUR
16191 five
16192 "}));
16193}
16194
16195#[gpui::test]
16196fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16197 init_test(cx, |_| {});
16198
16199 let editor = cx.add_window(|window, cx| {
16200 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16201 build_editor(buffer, window, cx)
16202 });
16203
16204 let render_args = Arc::new(Mutex::new(None));
16205 let snapshot = editor
16206 .update(cx, |editor, window, cx| {
16207 let snapshot = editor.buffer().read(cx).snapshot(cx);
16208 let range =
16209 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16210
16211 struct RenderArgs {
16212 row: MultiBufferRow,
16213 folded: bool,
16214 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16215 }
16216
16217 let crease = Crease::inline(
16218 range,
16219 FoldPlaceholder::test(),
16220 {
16221 let toggle_callback = render_args.clone();
16222 move |row, folded, callback, _window, _cx| {
16223 *toggle_callback.lock() = Some(RenderArgs {
16224 row,
16225 folded,
16226 callback,
16227 });
16228 div()
16229 }
16230 },
16231 |_row, _folded, _window, _cx| div(),
16232 );
16233
16234 editor.insert_creases(Some(crease), cx);
16235 let snapshot = editor.snapshot(window, cx);
16236 let _div = snapshot.render_crease_toggle(
16237 MultiBufferRow(1),
16238 false,
16239 cx.entity().clone(),
16240 window,
16241 cx,
16242 );
16243 snapshot
16244 })
16245 .unwrap();
16246
16247 let render_args = render_args.lock().take().unwrap();
16248 assert_eq!(render_args.row, MultiBufferRow(1));
16249 assert!(!render_args.folded);
16250 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16251
16252 cx.update_window(*editor, |_, window, cx| {
16253 (render_args.callback)(true, window, cx)
16254 })
16255 .unwrap();
16256 let snapshot = editor
16257 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16258 .unwrap();
16259 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16260
16261 cx.update_window(*editor, |_, window, cx| {
16262 (render_args.callback)(false, window, cx)
16263 })
16264 .unwrap();
16265 let snapshot = editor
16266 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16267 .unwrap();
16268 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16269}
16270
16271#[gpui::test]
16272async fn test_input_text(cx: &mut TestAppContext) {
16273 init_test(cx, |_| {});
16274 let mut cx = EditorTestContext::new(cx).await;
16275
16276 cx.set_state(
16277 &r#"ˇone
16278 two
16279
16280 three
16281 fourˇ
16282 five
16283
16284 siˇx"#
16285 .unindent(),
16286 );
16287
16288 cx.dispatch_action(HandleInput(String::new()));
16289 cx.assert_editor_state(
16290 &r#"ˇone
16291 two
16292
16293 three
16294 fourˇ
16295 five
16296
16297 siˇx"#
16298 .unindent(),
16299 );
16300
16301 cx.dispatch_action(HandleInput("AAAA".to_string()));
16302 cx.assert_editor_state(
16303 &r#"AAAAˇone
16304 two
16305
16306 three
16307 fourAAAAˇ
16308 five
16309
16310 siAAAAˇx"#
16311 .unindent(),
16312 );
16313}
16314
16315#[gpui::test]
16316async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16317 init_test(cx, |_| {});
16318
16319 let mut cx = EditorTestContext::new(cx).await;
16320 cx.set_state(
16321 r#"let foo = 1;
16322let foo = 2;
16323let foo = 3;
16324let fooˇ = 4;
16325let foo = 5;
16326let foo = 6;
16327let foo = 7;
16328let foo = 8;
16329let foo = 9;
16330let foo = 10;
16331let foo = 11;
16332let foo = 12;
16333let foo = 13;
16334let foo = 14;
16335let foo = 15;"#,
16336 );
16337
16338 cx.update_editor(|e, window, cx| {
16339 assert_eq!(
16340 e.next_scroll_position,
16341 NextScrollCursorCenterTopBottom::Center,
16342 "Default next scroll direction is center",
16343 );
16344
16345 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16346 assert_eq!(
16347 e.next_scroll_position,
16348 NextScrollCursorCenterTopBottom::Top,
16349 "After center, next scroll direction should be top",
16350 );
16351
16352 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16353 assert_eq!(
16354 e.next_scroll_position,
16355 NextScrollCursorCenterTopBottom::Bottom,
16356 "After top, next scroll direction should be bottom",
16357 );
16358
16359 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16360 assert_eq!(
16361 e.next_scroll_position,
16362 NextScrollCursorCenterTopBottom::Center,
16363 "After bottom, scrolling should start over",
16364 );
16365
16366 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16367 assert_eq!(
16368 e.next_scroll_position,
16369 NextScrollCursorCenterTopBottom::Top,
16370 "Scrolling continues if retriggered fast enough"
16371 );
16372 });
16373
16374 cx.executor()
16375 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16376 cx.executor().run_until_parked();
16377 cx.update_editor(|e, _, _| {
16378 assert_eq!(
16379 e.next_scroll_position,
16380 NextScrollCursorCenterTopBottom::Center,
16381 "If scrolling is not triggered fast enough, it should reset"
16382 );
16383 });
16384}
16385
16386#[gpui::test]
16387async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16388 init_test(cx, |_| {});
16389 let mut cx = EditorLspTestContext::new_rust(
16390 lsp::ServerCapabilities {
16391 definition_provider: Some(lsp::OneOf::Left(true)),
16392 references_provider: Some(lsp::OneOf::Left(true)),
16393 ..lsp::ServerCapabilities::default()
16394 },
16395 cx,
16396 )
16397 .await;
16398
16399 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16400 let go_to_definition = cx
16401 .lsp
16402 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16403 move |params, _| async move {
16404 if empty_go_to_definition {
16405 Ok(None)
16406 } else {
16407 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16408 uri: params.text_document_position_params.text_document.uri,
16409 range: lsp::Range::new(
16410 lsp::Position::new(4, 3),
16411 lsp::Position::new(4, 6),
16412 ),
16413 })))
16414 }
16415 },
16416 );
16417 let references = cx
16418 .lsp
16419 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16420 Ok(Some(vec![lsp::Location {
16421 uri: params.text_document_position.text_document.uri,
16422 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16423 }]))
16424 });
16425 (go_to_definition, references)
16426 };
16427
16428 cx.set_state(
16429 &r#"fn one() {
16430 let mut a = ˇtwo();
16431 }
16432
16433 fn two() {}"#
16434 .unindent(),
16435 );
16436 set_up_lsp_handlers(false, &mut cx);
16437 let navigated = cx
16438 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16439 .await
16440 .expect("Failed to navigate to definition");
16441 assert_eq!(
16442 navigated,
16443 Navigated::Yes,
16444 "Should have navigated to definition from the GetDefinition response"
16445 );
16446 cx.assert_editor_state(
16447 &r#"fn one() {
16448 let mut a = two();
16449 }
16450
16451 fn «twoˇ»() {}"#
16452 .unindent(),
16453 );
16454
16455 let editors = cx.update_workspace(|workspace, _, cx| {
16456 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16457 });
16458 cx.update_editor(|_, _, test_editor_cx| {
16459 assert_eq!(
16460 editors.len(),
16461 1,
16462 "Initially, only one, test, editor should be open in the workspace"
16463 );
16464 assert_eq!(
16465 test_editor_cx.entity(),
16466 editors.last().expect("Asserted len is 1").clone()
16467 );
16468 });
16469
16470 set_up_lsp_handlers(true, &mut cx);
16471 let navigated = cx
16472 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16473 .await
16474 .expect("Failed to navigate to lookup references");
16475 assert_eq!(
16476 navigated,
16477 Navigated::Yes,
16478 "Should have navigated to references as a fallback after empty GoToDefinition response"
16479 );
16480 // We should not change the selections in the existing file,
16481 // if opening another milti buffer with the references
16482 cx.assert_editor_state(
16483 &r#"fn one() {
16484 let mut a = two();
16485 }
16486
16487 fn «twoˇ»() {}"#
16488 .unindent(),
16489 );
16490 let editors = cx.update_workspace(|workspace, _, cx| {
16491 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16492 });
16493 cx.update_editor(|_, _, test_editor_cx| {
16494 assert_eq!(
16495 editors.len(),
16496 2,
16497 "After falling back to references search, we open a new editor with the results"
16498 );
16499 let references_fallback_text = editors
16500 .into_iter()
16501 .find(|new_editor| *new_editor != test_editor_cx.entity())
16502 .expect("Should have one non-test editor now")
16503 .read(test_editor_cx)
16504 .text(test_editor_cx);
16505 assert_eq!(
16506 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16507 "Should use the range from the references response and not the GoToDefinition one"
16508 );
16509 });
16510}
16511
16512#[gpui::test]
16513async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16514 init_test(cx, |_| {});
16515 cx.update(|cx| {
16516 let mut editor_settings = EditorSettings::get_global(cx).clone();
16517 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16518 EditorSettings::override_global(editor_settings, cx);
16519 });
16520 let mut cx = EditorLspTestContext::new_rust(
16521 lsp::ServerCapabilities {
16522 definition_provider: Some(lsp::OneOf::Left(true)),
16523 references_provider: Some(lsp::OneOf::Left(true)),
16524 ..lsp::ServerCapabilities::default()
16525 },
16526 cx,
16527 )
16528 .await;
16529 let original_state = r#"fn one() {
16530 let mut a = ˇtwo();
16531 }
16532
16533 fn two() {}"#
16534 .unindent();
16535 cx.set_state(&original_state);
16536
16537 let mut go_to_definition = cx
16538 .lsp
16539 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16540 move |_, _| async move { Ok(None) },
16541 );
16542 let _references = cx
16543 .lsp
16544 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16545 panic!("Should not call for references with no go to definition fallback")
16546 });
16547
16548 let navigated = cx
16549 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16550 .await
16551 .expect("Failed to navigate to lookup references");
16552 go_to_definition
16553 .next()
16554 .await
16555 .expect("Should have called the go_to_definition handler");
16556
16557 assert_eq!(
16558 navigated,
16559 Navigated::No,
16560 "Should have navigated to references as a fallback after empty GoToDefinition response"
16561 );
16562 cx.assert_editor_state(&original_state);
16563 let editors = cx.update_workspace(|workspace, _, cx| {
16564 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16565 });
16566 cx.update_editor(|_, _, _| {
16567 assert_eq!(
16568 editors.len(),
16569 1,
16570 "After unsuccessful fallback, no other editor should have been opened"
16571 );
16572 });
16573}
16574
16575#[gpui::test]
16576async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16577 init_test(cx, |_| {});
16578
16579 let language = Arc::new(Language::new(
16580 LanguageConfig::default(),
16581 Some(tree_sitter_rust::LANGUAGE.into()),
16582 ));
16583
16584 let text = r#"
16585 #[cfg(test)]
16586 mod tests() {
16587 #[test]
16588 fn runnable_1() {
16589 let a = 1;
16590 }
16591
16592 #[test]
16593 fn runnable_2() {
16594 let a = 1;
16595 let b = 2;
16596 }
16597 }
16598 "#
16599 .unindent();
16600
16601 let fs = FakeFs::new(cx.executor());
16602 fs.insert_file("/file.rs", Default::default()).await;
16603
16604 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16605 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16606 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16607 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16608 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16609
16610 let editor = cx.new_window_entity(|window, cx| {
16611 Editor::new(
16612 EditorMode::Full,
16613 multi_buffer,
16614 Some(project.clone()),
16615 window,
16616 cx,
16617 )
16618 });
16619
16620 editor.update_in(cx, |editor, window, cx| {
16621 let snapshot = editor.buffer().read(cx).snapshot(cx);
16622 editor.tasks.insert(
16623 (buffer.read(cx).remote_id(), 3),
16624 RunnableTasks {
16625 templates: vec![],
16626 offset: snapshot.anchor_before(43),
16627 column: 0,
16628 extra_variables: HashMap::default(),
16629 context_range: BufferOffset(43)..BufferOffset(85),
16630 },
16631 );
16632 editor.tasks.insert(
16633 (buffer.read(cx).remote_id(), 8),
16634 RunnableTasks {
16635 templates: vec![],
16636 offset: snapshot.anchor_before(86),
16637 column: 0,
16638 extra_variables: HashMap::default(),
16639 context_range: BufferOffset(86)..BufferOffset(191),
16640 },
16641 );
16642
16643 // Test finding task when cursor is inside function body
16644 editor.change_selections(None, window, cx, |s| {
16645 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16646 });
16647 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16648 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16649
16650 // Test finding task when cursor is on function name
16651 editor.change_selections(None, window, cx, |s| {
16652 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16653 });
16654 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16655 assert_eq!(row, 8, "Should find task when cursor is on function name");
16656 });
16657}
16658
16659#[gpui::test]
16660async fn test_folding_buffers(cx: &mut TestAppContext) {
16661 init_test(cx, |_| {});
16662
16663 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16664 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16665 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16666
16667 let fs = FakeFs::new(cx.executor());
16668 fs.insert_tree(
16669 path!("/a"),
16670 json!({
16671 "first.rs": sample_text_1,
16672 "second.rs": sample_text_2,
16673 "third.rs": sample_text_3,
16674 }),
16675 )
16676 .await;
16677 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16678 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16679 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16680 let worktree = project.update(cx, |project, cx| {
16681 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16682 assert_eq!(worktrees.len(), 1);
16683 worktrees.pop().unwrap()
16684 });
16685 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16686
16687 let buffer_1 = project
16688 .update(cx, |project, cx| {
16689 project.open_buffer((worktree_id, "first.rs"), cx)
16690 })
16691 .await
16692 .unwrap();
16693 let buffer_2 = project
16694 .update(cx, |project, cx| {
16695 project.open_buffer((worktree_id, "second.rs"), cx)
16696 })
16697 .await
16698 .unwrap();
16699 let buffer_3 = project
16700 .update(cx, |project, cx| {
16701 project.open_buffer((worktree_id, "third.rs"), cx)
16702 })
16703 .await
16704 .unwrap();
16705
16706 let multi_buffer = cx.new(|cx| {
16707 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16708 multi_buffer.push_excerpts(
16709 buffer_1.clone(),
16710 [
16711 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16712 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16713 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16714 ],
16715 cx,
16716 );
16717 multi_buffer.push_excerpts(
16718 buffer_2.clone(),
16719 [
16720 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16721 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16722 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16723 ],
16724 cx,
16725 );
16726 multi_buffer.push_excerpts(
16727 buffer_3.clone(),
16728 [
16729 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16730 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16731 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16732 ],
16733 cx,
16734 );
16735 multi_buffer
16736 });
16737 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16738 Editor::new(
16739 EditorMode::Full,
16740 multi_buffer.clone(),
16741 Some(project.clone()),
16742 window,
16743 cx,
16744 )
16745 });
16746
16747 assert_eq!(
16748 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16749 "\n\naaaa\nbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16750 );
16751
16752 multi_buffer_editor.update(cx, |editor, cx| {
16753 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16754 });
16755 assert_eq!(
16756 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16757 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16758 "After folding the first buffer, its text should not be displayed"
16759 );
16760
16761 multi_buffer_editor.update(cx, |editor, cx| {
16762 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16763 });
16764 assert_eq!(
16765 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16766 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16767 "After folding the second buffer, its text should not be displayed"
16768 );
16769
16770 multi_buffer_editor.update(cx, |editor, cx| {
16771 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16772 });
16773 assert_eq!(
16774 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16775 "\n\n\n\n\n",
16776 "After folding the third buffer, its text should not be displayed"
16777 );
16778
16779 // Emulate selection inside the fold logic, that should work
16780 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16781 editor
16782 .snapshot(window, cx)
16783 .next_line_boundary(Point::new(0, 4));
16784 });
16785
16786 multi_buffer_editor.update(cx, |editor, cx| {
16787 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16788 });
16789 assert_eq!(
16790 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16791 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16792 "After unfolding the second buffer, its text should be displayed"
16793 );
16794
16795 // Typing inside of buffer 1 causes that buffer to be unfolded.
16796 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16797 assert_eq!(
16798 multi_buffer
16799 .read(cx)
16800 .snapshot(cx)
16801 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16802 .collect::<String>(),
16803 "bbbb"
16804 );
16805 editor.change_selections(None, window, cx, |selections| {
16806 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16807 });
16808 editor.handle_input("B", window, cx);
16809 });
16810
16811 assert_eq!(
16812 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16813 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16814 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16815 );
16816
16817 multi_buffer_editor.update(cx, |editor, cx| {
16818 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16819 });
16820 assert_eq!(
16821 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16822 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16823 "After unfolding the all buffers, all original text should be displayed"
16824 );
16825}
16826
16827#[gpui::test]
16828async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16829 init_test(cx, |_| {});
16830
16831 let sample_text_1 = "1111\n2222\n3333".to_string();
16832 let sample_text_2 = "4444\n5555\n6666".to_string();
16833 let sample_text_3 = "7777\n8888\n9999".to_string();
16834
16835 let fs = FakeFs::new(cx.executor());
16836 fs.insert_tree(
16837 path!("/a"),
16838 json!({
16839 "first.rs": sample_text_1,
16840 "second.rs": sample_text_2,
16841 "third.rs": sample_text_3,
16842 }),
16843 )
16844 .await;
16845 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16846 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16847 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16848 let worktree = project.update(cx, |project, cx| {
16849 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16850 assert_eq!(worktrees.len(), 1);
16851 worktrees.pop().unwrap()
16852 });
16853 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16854
16855 let buffer_1 = project
16856 .update(cx, |project, cx| {
16857 project.open_buffer((worktree_id, "first.rs"), cx)
16858 })
16859 .await
16860 .unwrap();
16861 let buffer_2 = project
16862 .update(cx, |project, cx| {
16863 project.open_buffer((worktree_id, "second.rs"), cx)
16864 })
16865 .await
16866 .unwrap();
16867 let buffer_3 = project
16868 .update(cx, |project, cx| {
16869 project.open_buffer((worktree_id, "third.rs"), cx)
16870 })
16871 .await
16872 .unwrap();
16873
16874 let multi_buffer = cx.new(|cx| {
16875 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16876 multi_buffer.push_excerpts(
16877 buffer_1.clone(),
16878 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16879 cx,
16880 );
16881 multi_buffer.push_excerpts(
16882 buffer_2.clone(),
16883 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16884 cx,
16885 );
16886 multi_buffer.push_excerpts(
16887 buffer_3.clone(),
16888 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16889 cx,
16890 );
16891 multi_buffer
16892 });
16893
16894 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16895 Editor::new(
16896 EditorMode::Full,
16897 multi_buffer,
16898 Some(project.clone()),
16899 window,
16900 cx,
16901 )
16902 });
16903
16904 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
16905 assert_eq!(
16906 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16907 full_text,
16908 );
16909
16910 multi_buffer_editor.update(cx, |editor, cx| {
16911 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16912 });
16913 assert_eq!(
16914 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16915 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
16916 "After folding the first buffer, its text should not be displayed"
16917 );
16918
16919 multi_buffer_editor.update(cx, |editor, cx| {
16920 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16921 });
16922
16923 assert_eq!(
16924 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16925 "\n\n\n\n\n\n7777\n8888\n9999",
16926 "After folding the second buffer, its text should not be displayed"
16927 );
16928
16929 multi_buffer_editor.update(cx, |editor, cx| {
16930 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16931 });
16932 assert_eq!(
16933 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16934 "\n\n\n\n\n",
16935 "After folding the third buffer, its text should not be displayed"
16936 );
16937
16938 multi_buffer_editor.update(cx, |editor, cx| {
16939 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16940 });
16941 assert_eq!(
16942 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16943 "\n\n\n\n4444\n5555\n6666\n\n",
16944 "After unfolding the second buffer, its text should be displayed"
16945 );
16946
16947 multi_buffer_editor.update(cx, |editor, cx| {
16948 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16949 });
16950 assert_eq!(
16951 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16952 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
16953 "After unfolding the first buffer, its text should be displayed"
16954 );
16955
16956 multi_buffer_editor.update(cx, |editor, cx| {
16957 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16958 });
16959 assert_eq!(
16960 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16961 full_text,
16962 "After unfolding all buffers, all original text should be displayed"
16963 );
16964}
16965
16966#[gpui::test]
16967async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16968 init_test(cx, |_| {});
16969
16970 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16971
16972 let fs = FakeFs::new(cx.executor());
16973 fs.insert_tree(
16974 path!("/a"),
16975 json!({
16976 "main.rs": sample_text,
16977 }),
16978 )
16979 .await;
16980 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16981 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16982 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16983 let worktree = project.update(cx, |project, cx| {
16984 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16985 assert_eq!(worktrees.len(), 1);
16986 worktrees.pop().unwrap()
16987 });
16988 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16989
16990 let buffer_1 = project
16991 .update(cx, |project, cx| {
16992 project.open_buffer((worktree_id, "main.rs"), cx)
16993 })
16994 .await
16995 .unwrap();
16996
16997 let multi_buffer = cx.new(|cx| {
16998 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16999 multi_buffer.push_excerpts(
17000 buffer_1.clone(),
17001 [ExcerptRange::new(
17002 Point::new(0, 0)
17003 ..Point::new(
17004 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17005 0,
17006 ),
17007 )],
17008 cx,
17009 );
17010 multi_buffer
17011 });
17012 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17013 Editor::new(
17014 EditorMode::Full,
17015 multi_buffer,
17016 Some(project.clone()),
17017 window,
17018 cx,
17019 )
17020 });
17021
17022 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17023 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17024 enum TestHighlight {}
17025 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17026 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17027 editor.highlight_text::<TestHighlight>(
17028 vec![highlight_range.clone()],
17029 HighlightStyle::color(Hsla::green()),
17030 cx,
17031 );
17032 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17033 });
17034
17035 let full_text = format!("\n\n{sample_text}");
17036 assert_eq!(
17037 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17038 full_text,
17039 );
17040}
17041
17042#[gpui::test]
17043async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17044 init_test(cx, |_| {});
17045 cx.update(|cx| {
17046 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17047 "keymaps/default-linux.json",
17048 cx,
17049 )
17050 .unwrap();
17051 cx.bind_keys(default_key_bindings);
17052 });
17053
17054 let (editor, cx) = cx.add_window_view(|window, cx| {
17055 let multi_buffer = MultiBuffer::build_multi(
17056 [
17057 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17058 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17059 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17060 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17061 ],
17062 cx,
17063 );
17064 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17065
17066 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17067 // fold all but the second buffer, so that we test navigating between two
17068 // adjacent folded buffers, as well as folded buffers at the start and
17069 // end the multibuffer
17070 editor.fold_buffer(buffer_ids[0], cx);
17071 editor.fold_buffer(buffer_ids[2], cx);
17072 editor.fold_buffer(buffer_ids[3], cx);
17073
17074 editor
17075 });
17076 cx.simulate_resize(size(px(1000.), px(1000.)));
17077
17078 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17079 cx.assert_excerpts_with_selections(indoc! {"
17080 [EXCERPT]
17081 ˇ[FOLDED]
17082 [EXCERPT]
17083 a1
17084 b1
17085 [EXCERPT]
17086 [FOLDED]
17087 [EXCERPT]
17088 [FOLDED]
17089 "
17090 });
17091 cx.simulate_keystroke("down");
17092 cx.assert_excerpts_with_selections(indoc! {"
17093 [EXCERPT]
17094 [FOLDED]
17095 [EXCERPT]
17096 ˇa1
17097 b1
17098 [EXCERPT]
17099 [FOLDED]
17100 [EXCERPT]
17101 [FOLDED]
17102 "
17103 });
17104 cx.simulate_keystroke("down");
17105 cx.assert_excerpts_with_selections(indoc! {"
17106 [EXCERPT]
17107 [FOLDED]
17108 [EXCERPT]
17109 a1
17110 ˇb1
17111 [EXCERPT]
17112 [FOLDED]
17113 [EXCERPT]
17114 [FOLDED]
17115 "
17116 });
17117 cx.simulate_keystroke("down");
17118 cx.assert_excerpts_with_selections(indoc! {"
17119 [EXCERPT]
17120 [FOLDED]
17121 [EXCERPT]
17122 a1
17123 b1
17124 ˇ[EXCERPT]
17125 [FOLDED]
17126 [EXCERPT]
17127 [FOLDED]
17128 "
17129 });
17130 cx.simulate_keystroke("down");
17131 cx.assert_excerpts_with_selections(indoc! {"
17132 [EXCERPT]
17133 [FOLDED]
17134 [EXCERPT]
17135 a1
17136 b1
17137 [EXCERPT]
17138 ˇ[FOLDED]
17139 [EXCERPT]
17140 [FOLDED]
17141 "
17142 });
17143 for _ in 0..5 {
17144 cx.simulate_keystroke("down");
17145 cx.assert_excerpts_with_selections(indoc! {"
17146 [EXCERPT]
17147 [FOLDED]
17148 [EXCERPT]
17149 a1
17150 b1
17151 [EXCERPT]
17152 [FOLDED]
17153 [EXCERPT]
17154 ˇ[FOLDED]
17155 "
17156 });
17157 }
17158
17159 cx.simulate_keystroke("up");
17160 cx.assert_excerpts_with_selections(indoc! {"
17161 [EXCERPT]
17162 [FOLDED]
17163 [EXCERPT]
17164 a1
17165 b1
17166 [EXCERPT]
17167 ˇ[FOLDED]
17168 [EXCERPT]
17169 [FOLDED]
17170 "
17171 });
17172 cx.simulate_keystroke("up");
17173 cx.assert_excerpts_with_selections(indoc! {"
17174 [EXCERPT]
17175 [FOLDED]
17176 [EXCERPT]
17177 a1
17178 b1
17179 ˇ[EXCERPT]
17180 [FOLDED]
17181 [EXCERPT]
17182 [FOLDED]
17183 "
17184 });
17185 cx.simulate_keystroke("up");
17186 cx.assert_excerpts_with_selections(indoc! {"
17187 [EXCERPT]
17188 [FOLDED]
17189 [EXCERPT]
17190 a1
17191 ˇb1
17192 [EXCERPT]
17193 [FOLDED]
17194 [EXCERPT]
17195 [FOLDED]
17196 "
17197 });
17198 cx.simulate_keystroke("up");
17199 cx.assert_excerpts_with_selections(indoc! {"
17200 [EXCERPT]
17201 [FOLDED]
17202 [EXCERPT]
17203 ˇa1
17204 b1
17205 [EXCERPT]
17206 [FOLDED]
17207 [EXCERPT]
17208 [FOLDED]
17209 "
17210 });
17211 for _ in 0..5 {
17212 cx.simulate_keystroke("up");
17213 cx.assert_excerpts_with_selections(indoc! {"
17214 [EXCERPT]
17215 ˇ[FOLDED]
17216 [EXCERPT]
17217 a1
17218 b1
17219 [EXCERPT]
17220 [FOLDED]
17221 [EXCERPT]
17222 [FOLDED]
17223 "
17224 });
17225 }
17226}
17227
17228#[gpui::test]
17229async fn test_inline_completion_text(cx: &mut TestAppContext) {
17230 init_test(cx, |_| {});
17231
17232 // Simple insertion
17233 assert_highlighted_edits(
17234 "Hello, world!",
17235 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17236 true,
17237 cx,
17238 |highlighted_edits, cx| {
17239 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17240 assert_eq!(highlighted_edits.highlights.len(), 1);
17241 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17242 assert_eq!(
17243 highlighted_edits.highlights[0].1.background_color,
17244 Some(cx.theme().status().created_background)
17245 );
17246 },
17247 )
17248 .await;
17249
17250 // Replacement
17251 assert_highlighted_edits(
17252 "This is a test.",
17253 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17254 false,
17255 cx,
17256 |highlighted_edits, cx| {
17257 assert_eq!(highlighted_edits.text, "That is a test.");
17258 assert_eq!(highlighted_edits.highlights.len(), 1);
17259 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17260 assert_eq!(
17261 highlighted_edits.highlights[0].1.background_color,
17262 Some(cx.theme().status().created_background)
17263 );
17264 },
17265 )
17266 .await;
17267
17268 // Multiple edits
17269 assert_highlighted_edits(
17270 "Hello, world!",
17271 vec![
17272 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17273 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17274 ],
17275 false,
17276 cx,
17277 |highlighted_edits, cx| {
17278 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17279 assert_eq!(highlighted_edits.highlights.len(), 2);
17280 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17281 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17282 assert_eq!(
17283 highlighted_edits.highlights[0].1.background_color,
17284 Some(cx.theme().status().created_background)
17285 );
17286 assert_eq!(
17287 highlighted_edits.highlights[1].1.background_color,
17288 Some(cx.theme().status().created_background)
17289 );
17290 },
17291 )
17292 .await;
17293
17294 // Multiple lines with edits
17295 assert_highlighted_edits(
17296 "First line\nSecond line\nThird line\nFourth line",
17297 vec![
17298 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17299 (
17300 Point::new(2, 0)..Point::new(2, 10),
17301 "New third line".to_string(),
17302 ),
17303 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17304 ],
17305 false,
17306 cx,
17307 |highlighted_edits, cx| {
17308 assert_eq!(
17309 highlighted_edits.text,
17310 "Second modified\nNew third line\nFourth updated line"
17311 );
17312 assert_eq!(highlighted_edits.highlights.len(), 3);
17313 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17314 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17315 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17316 for highlight in &highlighted_edits.highlights {
17317 assert_eq!(
17318 highlight.1.background_color,
17319 Some(cx.theme().status().created_background)
17320 );
17321 }
17322 },
17323 )
17324 .await;
17325}
17326
17327#[gpui::test]
17328async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17329 init_test(cx, |_| {});
17330
17331 // Deletion
17332 assert_highlighted_edits(
17333 "Hello, world!",
17334 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17335 true,
17336 cx,
17337 |highlighted_edits, cx| {
17338 assert_eq!(highlighted_edits.text, "Hello, world!");
17339 assert_eq!(highlighted_edits.highlights.len(), 1);
17340 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17341 assert_eq!(
17342 highlighted_edits.highlights[0].1.background_color,
17343 Some(cx.theme().status().deleted_background)
17344 );
17345 },
17346 )
17347 .await;
17348
17349 // Insertion
17350 assert_highlighted_edits(
17351 "Hello, world!",
17352 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17353 true,
17354 cx,
17355 |highlighted_edits, cx| {
17356 assert_eq!(highlighted_edits.highlights.len(), 1);
17357 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17358 assert_eq!(
17359 highlighted_edits.highlights[0].1.background_color,
17360 Some(cx.theme().status().created_background)
17361 );
17362 },
17363 )
17364 .await;
17365}
17366
17367async fn assert_highlighted_edits(
17368 text: &str,
17369 edits: Vec<(Range<Point>, String)>,
17370 include_deletions: bool,
17371 cx: &mut TestAppContext,
17372 assertion_fn: impl Fn(HighlightedText, &App),
17373) {
17374 let window = cx.add_window(|window, cx| {
17375 let buffer = MultiBuffer::build_simple(text, cx);
17376 Editor::new(EditorMode::Full, buffer, None, window, cx)
17377 });
17378 let cx = &mut VisualTestContext::from_window(*window, cx);
17379
17380 let (buffer, snapshot) = window
17381 .update(cx, |editor, _window, cx| {
17382 (
17383 editor.buffer().clone(),
17384 editor.buffer().read(cx).snapshot(cx),
17385 )
17386 })
17387 .unwrap();
17388
17389 let edits = edits
17390 .into_iter()
17391 .map(|(range, edit)| {
17392 (
17393 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17394 edit,
17395 )
17396 })
17397 .collect::<Vec<_>>();
17398
17399 let text_anchor_edits = edits
17400 .clone()
17401 .into_iter()
17402 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17403 .collect::<Vec<_>>();
17404
17405 let edit_preview = window
17406 .update(cx, |_, _window, cx| {
17407 buffer
17408 .read(cx)
17409 .as_singleton()
17410 .unwrap()
17411 .read(cx)
17412 .preview_edits(text_anchor_edits.into(), cx)
17413 })
17414 .unwrap()
17415 .await;
17416
17417 cx.update(|_window, cx| {
17418 let highlighted_edits = inline_completion_edit_text(
17419 &snapshot.as_singleton().unwrap().2,
17420 &edits,
17421 &edit_preview,
17422 include_deletions,
17423 cx,
17424 );
17425 assertion_fn(highlighted_edits, cx)
17426 });
17427}
17428
17429#[track_caller]
17430fn assert_breakpoint(
17431 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17432 path: &Arc<Path>,
17433 expected: Vec<(u32, Breakpoint)>,
17434) {
17435 if expected.len() == 0usize {
17436 assert!(!breakpoints.contains_key(path), "{}", path.display());
17437 } else {
17438 let mut breakpoint = breakpoints
17439 .get(path)
17440 .unwrap()
17441 .into_iter()
17442 .map(|breakpoint| {
17443 (
17444 breakpoint.row,
17445 Breakpoint {
17446 message: breakpoint.message.clone(),
17447 state: breakpoint.state,
17448 condition: breakpoint.condition.clone(),
17449 hit_condition: breakpoint.hit_condition.clone(),
17450 },
17451 )
17452 })
17453 .collect::<Vec<_>>();
17454
17455 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17456
17457 assert_eq!(expected, breakpoint);
17458 }
17459}
17460
17461fn add_log_breakpoint_at_cursor(
17462 editor: &mut Editor,
17463 log_message: &str,
17464 window: &mut Window,
17465 cx: &mut Context<Editor>,
17466) {
17467 let (anchor, bp) = editor
17468 .breakpoint_at_cursor_head(window, cx)
17469 .unwrap_or_else(|| {
17470 let cursor_position: Point = editor.selections.newest(cx).head();
17471
17472 let breakpoint_position = editor
17473 .snapshot(window, cx)
17474 .display_snapshot
17475 .buffer_snapshot
17476 .anchor_before(Point::new(cursor_position.row, 0));
17477
17478 (breakpoint_position, Breakpoint::new_log(&log_message))
17479 });
17480
17481 editor.edit_breakpoint_at_anchor(
17482 anchor,
17483 bp,
17484 BreakpointEditAction::EditLogMessage(log_message.into()),
17485 cx,
17486 );
17487}
17488
17489#[gpui::test]
17490async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17491 init_test(cx, |_| {});
17492
17493 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17494 let fs = FakeFs::new(cx.executor());
17495 fs.insert_tree(
17496 path!("/a"),
17497 json!({
17498 "main.rs": sample_text,
17499 }),
17500 )
17501 .await;
17502 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17503 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17504 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17505
17506 let fs = FakeFs::new(cx.executor());
17507 fs.insert_tree(
17508 path!("/a"),
17509 json!({
17510 "main.rs": sample_text,
17511 }),
17512 )
17513 .await;
17514 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17515 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17516 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17517 let worktree_id = workspace
17518 .update(cx, |workspace, _window, cx| {
17519 workspace.project().update(cx, |project, cx| {
17520 project.worktrees(cx).next().unwrap().read(cx).id()
17521 })
17522 })
17523 .unwrap();
17524
17525 let buffer = project
17526 .update(cx, |project, cx| {
17527 project.open_buffer((worktree_id, "main.rs"), cx)
17528 })
17529 .await
17530 .unwrap();
17531
17532 let (editor, cx) = cx.add_window_view(|window, cx| {
17533 Editor::new(
17534 EditorMode::Full,
17535 MultiBuffer::build_from_buffer(buffer, cx),
17536 Some(project.clone()),
17537 window,
17538 cx,
17539 )
17540 });
17541
17542 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17543 let abs_path = project.read_with(cx, |project, cx| {
17544 project
17545 .absolute_path(&project_path, cx)
17546 .map(|path_buf| Arc::from(path_buf.to_owned()))
17547 .unwrap()
17548 });
17549
17550 // assert we can add breakpoint on the first line
17551 editor.update_in(cx, |editor, window, cx| {
17552 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17553 editor.move_to_end(&MoveToEnd, window, cx);
17554 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17555 });
17556
17557 let breakpoints = editor.update(cx, |editor, cx| {
17558 editor
17559 .breakpoint_store()
17560 .as_ref()
17561 .unwrap()
17562 .read(cx)
17563 .all_breakpoints(cx)
17564 .clone()
17565 });
17566
17567 assert_eq!(1, breakpoints.len());
17568 assert_breakpoint(
17569 &breakpoints,
17570 &abs_path,
17571 vec![
17572 (0, Breakpoint::new_standard()),
17573 (3, Breakpoint::new_standard()),
17574 ],
17575 );
17576
17577 editor.update_in(cx, |editor, window, cx| {
17578 editor.move_to_beginning(&MoveToBeginning, window, cx);
17579 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17580 });
17581
17582 let breakpoints = editor.update(cx, |editor, cx| {
17583 editor
17584 .breakpoint_store()
17585 .as_ref()
17586 .unwrap()
17587 .read(cx)
17588 .all_breakpoints(cx)
17589 .clone()
17590 });
17591
17592 assert_eq!(1, breakpoints.len());
17593 assert_breakpoint(
17594 &breakpoints,
17595 &abs_path,
17596 vec![(3, Breakpoint::new_standard())],
17597 );
17598
17599 editor.update_in(cx, |editor, window, cx| {
17600 editor.move_to_end(&MoveToEnd, window, cx);
17601 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17602 });
17603
17604 let breakpoints = editor.update(cx, |editor, cx| {
17605 editor
17606 .breakpoint_store()
17607 .as_ref()
17608 .unwrap()
17609 .read(cx)
17610 .all_breakpoints(cx)
17611 .clone()
17612 });
17613
17614 assert_eq!(0, breakpoints.len());
17615 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17616}
17617
17618#[gpui::test]
17619async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17620 init_test(cx, |_| {});
17621
17622 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17623
17624 let fs = FakeFs::new(cx.executor());
17625 fs.insert_tree(
17626 path!("/a"),
17627 json!({
17628 "main.rs": sample_text,
17629 }),
17630 )
17631 .await;
17632 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17633 let (workspace, cx) =
17634 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17635
17636 let worktree_id = workspace.update(cx, |workspace, cx| {
17637 workspace.project().update(cx, |project, cx| {
17638 project.worktrees(cx).next().unwrap().read(cx).id()
17639 })
17640 });
17641
17642 let buffer = project
17643 .update(cx, |project, cx| {
17644 project.open_buffer((worktree_id, "main.rs"), cx)
17645 })
17646 .await
17647 .unwrap();
17648
17649 let (editor, cx) = cx.add_window_view(|window, cx| {
17650 Editor::new(
17651 EditorMode::Full,
17652 MultiBuffer::build_from_buffer(buffer, cx),
17653 Some(project.clone()),
17654 window,
17655 cx,
17656 )
17657 });
17658
17659 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17660 let abs_path = project.read_with(cx, |project, cx| {
17661 project
17662 .absolute_path(&project_path, cx)
17663 .map(|path_buf| Arc::from(path_buf.to_owned()))
17664 .unwrap()
17665 });
17666
17667 editor.update_in(cx, |editor, window, cx| {
17668 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17669 });
17670
17671 let breakpoints = editor.update(cx, |editor, cx| {
17672 editor
17673 .breakpoint_store()
17674 .as_ref()
17675 .unwrap()
17676 .read(cx)
17677 .all_breakpoints(cx)
17678 .clone()
17679 });
17680
17681 assert_breakpoint(
17682 &breakpoints,
17683 &abs_path,
17684 vec![(0, Breakpoint::new_log("hello world"))],
17685 );
17686
17687 // Removing a log message from a log breakpoint should remove it
17688 editor.update_in(cx, |editor, window, cx| {
17689 add_log_breakpoint_at_cursor(editor, "", window, cx);
17690 });
17691
17692 let breakpoints = editor.update(cx, |editor, cx| {
17693 editor
17694 .breakpoint_store()
17695 .as_ref()
17696 .unwrap()
17697 .read(cx)
17698 .all_breakpoints(cx)
17699 .clone()
17700 });
17701
17702 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17703
17704 editor.update_in(cx, |editor, window, cx| {
17705 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17706 editor.move_to_end(&MoveToEnd, window, cx);
17707 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17708 // Not adding a log message to a standard breakpoint shouldn't remove it
17709 add_log_breakpoint_at_cursor(editor, "", window, cx);
17710 });
17711
17712 let breakpoints = editor.update(cx, |editor, cx| {
17713 editor
17714 .breakpoint_store()
17715 .as_ref()
17716 .unwrap()
17717 .read(cx)
17718 .all_breakpoints(cx)
17719 .clone()
17720 });
17721
17722 assert_breakpoint(
17723 &breakpoints,
17724 &abs_path,
17725 vec![
17726 (0, Breakpoint::new_standard()),
17727 (3, Breakpoint::new_standard()),
17728 ],
17729 );
17730
17731 editor.update_in(cx, |editor, window, cx| {
17732 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17733 });
17734
17735 let breakpoints = editor.update(cx, |editor, cx| {
17736 editor
17737 .breakpoint_store()
17738 .as_ref()
17739 .unwrap()
17740 .read(cx)
17741 .all_breakpoints(cx)
17742 .clone()
17743 });
17744
17745 assert_breakpoint(
17746 &breakpoints,
17747 &abs_path,
17748 vec![
17749 (0, Breakpoint::new_standard()),
17750 (3, Breakpoint::new_log("hello world")),
17751 ],
17752 );
17753
17754 editor.update_in(cx, |editor, window, cx| {
17755 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17756 });
17757
17758 let breakpoints = editor.update(cx, |editor, cx| {
17759 editor
17760 .breakpoint_store()
17761 .as_ref()
17762 .unwrap()
17763 .read(cx)
17764 .all_breakpoints(cx)
17765 .clone()
17766 });
17767
17768 assert_breakpoint(
17769 &breakpoints,
17770 &abs_path,
17771 vec![
17772 (0, Breakpoint::new_standard()),
17773 (3, Breakpoint::new_log("hello Earth!!")),
17774 ],
17775 );
17776}
17777
17778/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17779/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17780/// or when breakpoints were placed out of order. This tests for a regression too
17781#[gpui::test]
17782async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17783 init_test(cx, |_| {});
17784
17785 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17786 let fs = FakeFs::new(cx.executor());
17787 fs.insert_tree(
17788 path!("/a"),
17789 json!({
17790 "main.rs": sample_text,
17791 }),
17792 )
17793 .await;
17794 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17795 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17796 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17797
17798 let fs = FakeFs::new(cx.executor());
17799 fs.insert_tree(
17800 path!("/a"),
17801 json!({
17802 "main.rs": sample_text,
17803 }),
17804 )
17805 .await;
17806 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17807 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17808 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17809 let worktree_id = workspace
17810 .update(cx, |workspace, _window, cx| {
17811 workspace.project().update(cx, |project, cx| {
17812 project.worktrees(cx).next().unwrap().read(cx).id()
17813 })
17814 })
17815 .unwrap();
17816
17817 let buffer = project
17818 .update(cx, |project, cx| {
17819 project.open_buffer((worktree_id, "main.rs"), cx)
17820 })
17821 .await
17822 .unwrap();
17823
17824 let (editor, cx) = cx.add_window_view(|window, cx| {
17825 Editor::new(
17826 EditorMode::Full,
17827 MultiBuffer::build_from_buffer(buffer, cx),
17828 Some(project.clone()),
17829 window,
17830 cx,
17831 )
17832 });
17833
17834 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17835 let abs_path = project.read_with(cx, |project, cx| {
17836 project
17837 .absolute_path(&project_path, cx)
17838 .map(|path_buf| Arc::from(path_buf.to_owned()))
17839 .unwrap()
17840 });
17841
17842 // assert we can add breakpoint on the first line
17843 editor.update_in(cx, |editor, window, cx| {
17844 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17845 editor.move_to_end(&MoveToEnd, window, cx);
17846 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17847 editor.move_up(&MoveUp, window, cx);
17848 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17849 });
17850
17851 let breakpoints = editor.update(cx, |editor, cx| {
17852 editor
17853 .breakpoint_store()
17854 .as_ref()
17855 .unwrap()
17856 .read(cx)
17857 .all_breakpoints(cx)
17858 .clone()
17859 });
17860
17861 assert_eq!(1, breakpoints.len());
17862 assert_breakpoint(
17863 &breakpoints,
17864 &abs_path,
17865 vec![
17866 (0, Breakpoint::new_standard()),
17867 (2, Breakpoint::new_standard()),
17868 (3, Breakpoint::new_standard()),
17869 ],
17870 );
17871
17872 editor.update_in(cx, |editor, window, cx| {
17873 editor.move_to_beginning(&MoveToBeginning, window, cx);
17874 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17875 editor.move_to_end(&MoveToEnd, window, cx);
17876 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17877 // Disabling a breakpoint that doesn't exist should do nothing
17878 editor.move_up(&MoveUp, window, cx);
17879 editor.move_up(&MoveUp, window, cx);
17880 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17881 });
17882
17883 let breakpoints = editor.update(cx, |editor, cx| {
17884 editor
17885 .breakpoint_store()
17886 .as_ref()
17887 .unwrap()
17888 .read(cx)
17889 .all_breakpoints(cx)
17890 .clone()
17891 });
17892
17893 let disable_breakpoint = {
17894 let mut bp = Breakpoint::new_standard();
17895 bp.state = BreakpointState::Disabled;
17896 bp
17897 };
17898
17899 assert_eq!(1, breakpoints.len());
17900 assert_breakpoint(
17901 &breakpoints,
17902 &abs_path,
17903 vec![
17904 (0, disable_breakpoint.clone()),
17905 (2, Breakpoint::new_standard()),
17906 (3, disable_breakpoint.clone()),
17907 ],
17908 );
17909
17910 editor.update_in(cx, |editor, window, cx| {
17911 editor.move_to_beginning(&MoveToBeginning, window, cx);
17912 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17913 editor.move_to_end(&MoveToEnd, window, cx);
17914 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17915 editor.move_up(&MoveUp, window, cx);
17916 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17917 });
17918
17919 let breakpoints = editor.update(cx, |editor, cx| {
17920 editor
17921 .breakpoint_store()
17922 .as_ref()
17923 .unwrap()
17924 .read(cx)
17925 .all_breakpoints(cx)
17926 .clone()
17927 });
17928
17929 assert_eq!(1, breakpoints.len());
17930 assert_breakpoint(
17931 &breakpoints,
17932 &abs_path,
17933 vec![
17934 (0, Breakpoint::new_standard()),
17935 (2, disable_breakpoint),
17936 (3, Breakpoint::new_standard()),
17937 ],
17938 );
17939}
17940
17941#[gpui::test]
17942async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17943 init_test(cx, |_| {});
17944 let capabilities = lsp::ServerCapabilities {
17945 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
17946 prepare_provider: Some(true),
17947 work_done_progress_options: Default::default(),
17948 })),
17949 ..Default::default()
17950 };
17951 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17952
17953 cx.set_state(indoc! {"
17954 struct Fˇoo {}
17955 "});
17956
17957 cx.update_editor(|editor, _, cx| {
17958 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17959 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17960 editor.highlight_background::<DocumentHighlightRead>(
17961 &[highlight_range],
17962 |c| c.editor_document_highlight_read_background,
17963 cx,
17964 );
17965 });
17966
17967 let mut prepare_rename_handler = cx
17968 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
17969 move |_, _, _| async move {
17970 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
17971 start: lsp::Position {
17972 line: 0,
17973 character: 7,
17974 },
17975 end: lsp::Position {
17976 line: 0,
17977 character: 10,
17978 },
17979 })))
17980 },
17981 );
17982 let prepare_rename_task = cx
17983 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17984 .expect("Prepare rename was not started");
17985 prepare_rename_handler.next().await.unwrap();
17986 prepare_rename_task.await.expect("Prepare rename failed");
17987
17988 let mut rename_handler =
17989 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17990 let edit = lsp::TextEdit {
17991 range: lsp::Range {
17992 start: lsp::Position {
17993 line: 0,
17994 character: 7,
17995 },
17996 end: lsp::Position {
17997 line: 0,
17998 character: 10,
17999 },
18000 },
18001 new_text: "FooRenamed".to_string(),
18002 };
18003 Ok(Some(lsp::WorkspaceEdit::new(
18004 // Specify the same edit twice
18005 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18006 )))
18007 });
18008 let rename_task = cx
18009 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18010 .expect("Confirm rename was not started");
18011 rename_handler.next().await.unwrap();
18012 rename_task.await.expect("Confirm rename failed");
18013 cx.run_until_parked();
18014
18015 // Despite two edits, only one is actually applied as those are identical
18016 cx.assert_editor_state(indoc! {"
18017 struct FooRenamedˇ {}
18018 "});
18019}
18020
18021#[gpui::test]
18022async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18023 init_test(cx, |_| {});
18024 // These capabilities indicate that the server does not support prepare rename.
18025 let capabilities = lsp::ServerCapabilities {
18026 rename_provider: Some(lsp::OneOf::Left(true)),
18027 ..Default::default()
18028 };
18029 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18030
18031 cx.set_state(indoc! {"
18032 struct Fˇoo {}
18033 "});
18034
18035 cx.update_editor(|editor, _window, cx| {
18036 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18037 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18038 editor.highlight_background::<DocumentHighlightRead>(
18039 &[highlight_range],
18040 |c| c.editor_document_highlight_read_background,
18041 cx,
18042 );
18043 });
18044
18045 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18046 .expect("Prepare rename was not started")
18047 .await
18048 .expect("Prepare rename failed");
18049
18050 let mut rename_handler =
18051 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18052 let edit = lsp::TextEdit {
18053 range: lsp::Range {
18054 start: lsp::Position {
18055 line: 0,
18056 character: 7,
18057 },
18058 end: lsp::Position {
18059 line: 0,
18060 character: 10,
18061 },
18062 },
18063 new_text: "FooRenamed".to_string(),
18064 };
18065 Ok(Some(lsp::WorkspaceEdit::new(
18066 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18067 )))
18068 });
18069 let rename_task = cx
18070 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18071 .expect("Confirm rename was not started");
18072 rename_handler.next().await.unwrap();
18073 rename_task.await.expect("Confirm rename failed");
18074 cx.run_until_parked();
18075
18076 // Correct range is renamed, as `surrounding_word` is used to find it.
18077 cx.assert_editor_state(indoc! {"
18078 struct FooRenamedˇ {}
18079 "});
18080}
18081
18082#[gpui::test]
18083async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18084 init_test(cx, |_| {});
18085 let mut cx = EditorTestContext::new(cx).await;
18086
18087 let language = Arc::new(
18088 Language::new(
18089 LanguageConfig::default(),
18090 Some(tree_sitter_html::LANGUAGE.into()),
18091 )
18092 .with_brackets_query(
18093 r#"
18094 ("<" @open "/>" @close)
18095 ("</" @open ">" @close)
18096 ("<" @open ">" @close)
18097 ("\"" @open "\"" @close)
18098 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18099 "#,
18100 )
18101 .unwrap(),
18102 );
18103 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18104
18105 cx.set_state(indoc! {"
18106 <span>ˇ</span>
18107 "});
18108 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18109 cx.assert_editor_state(indoc! {"
18110 <span>
18111 ˇ
18112 </span>
18113 "});
18114
18115 cx.set_state(indoc! {"
18116 <span><span></span>ˇ</span>
18117 "});
18118 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18119 cx.assert_editor_state(indoc! {"
18120 <span><span></span>
18121 ˇ</span>
18122 "});
18123
18124 cx.set_state(indoc! {"
18125 <span>ˇ
18126 </span>
18127 "});
18128 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18129 cx.assert_editor_state(indoc! {"
18130 <span>
18131 ˇ
18132 </span>
18133 "});
18134}
18135
18136#[gpui::test(iterations = 10)]
18137async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18138 init_test(cx, |_| {});
18139
18140 let fs = FakeFs::new(cx.executor());
18141 fs.insert_tree(
18142 path!("/dir"),
18143 json!({
18144 "a.ts": "a",
18145 }),
18146 )
18147 .await;
18148
18149 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18150 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18151 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18152
18153 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18154 language_registry.add(Arc::new(Language::new(
18155 LanguageConfig {
18156 name: "TypeScript".into(),
18157 matcher: LanguageMatcher {
18158 path_suffixes: vec!["ts".to_string()],
18159 ..Default::default()
18160 },
18161 ..Default::default()
18162 },
18163 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18164 )));
18165 let mut fake_language_servers = language_registry.register_fake_lsp(
18166 "TypeScript",
18167 FakeLspAdapter {
18168 capabilities: lsp::ServerCapabilities {
18169 code_lens_provider: Some(lsp::CodeLensOptions {
18170 resolve_provider: Some(true),
18171 }),
18172 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18173 commands: vec!["_the/command".to_string()],
18174 ..lsp::ExecuteCommandOptions::default()
18175 }),
18176 ..lsp::ServerCapabilities::default()
18177 },
18178 ..FakeLspAdapter::default()
18179 },
18180 );
18181
18182 let (buffer, _handle) = project
18183 .update(cx, |p, cx| {
18184 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18185 })
18186 .await
18187 .unwrap();
18188 cx.executor().run_until_parked();
18189
18190 let fake_server = fake_language_servers.next().await.unwrap();
18191
18192 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18193 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18194 drop(buffer_snapshot);
18195 let actions = cx
18196 .update_window(*workspace, |_, window, cx| {
18197 project.code_actions(&buffer, anchor..anchor, window, cx)
18198 })
18199 .unwrap();
18200
18201 fake_server
18202 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18203 Ok(Some(vec![
18204 lsp::CodeLens {
18205 range: lsp::Range::default(),
18206 command: Some(lsp::Command {
18207 title: "Code lens command".to_owned(),
18208 command: "_the/command".to_owned(),
18209 arguments: None,
18210 }),
18211 data: None,
18212 },
18213 lsp::CodeLens {
18214 range: lsp::Range::default(),
18215 command: Some(lsp::Command {
18216 title: "Command not in capabilities".to_owned(),
18217 command: "not in capabilities".to_owned(),
18218 arguments: None,
18219 }),
18220 data: None,
18221 },
18222 lsp::CodeLens {
18223 range: lsp::Range {
18224 start: lsp::Position {
18225 line: 1,
18226 character: 1,
18227 },
18228 end: lsp::Position {
18229 line: 1,
18230 character: 1,
18231 },
18232 },
18233 command: Some(lsp::Command {
18234 title: "Command not in range".to_owned(),
18235 command: "_the/command".to_owned(),
18236 arguments: None,
18237 }),
18238 data: None,
18239 },
18240 ]))
18241 })
18242 .next()
18243 .await;
18244
18245 let actions = actions.await.unwrap();
18246 assert_eq!(
18247 actions.len(),
18248 1,
18249 "Should have only one valid action for the 0..0 range"
18250 );
18251 let action = actions[0].clone();
18252 let apply = project.update(cx, |project, cx| {
18253 project.apply_code_action(buffer.clone(), action, true, cx)
18254 });
18255
18256 // Resolving the code action does not populate its edits. In absence of
18257 // edits, we must execute the given command.
18258 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18259 |mut lens, _| async move {
18260 let lens_command = lens.command.as_mut().expect("should have a command");
18261 assert_eq!(lens_command.title, "Code lens command");
18262 lens_command.arguments = Some(vec![json!("the-argument")]);
18263 Ok(lens)
18264 },
18265 );
18266
18267 // While executing the command, the language server sends the editor
18268 // a `workspaceEdit` request.
18269 fake_server
18270 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18271 let fake = fake_server.clone();
18272 move |params, _| {
18273 assert_eq!(params.command, "_the/command");
18274 let fake = fake.clone();
18275 async move {
18276 fake.server
18277 .request::<lsp::request::ApplyWorkspaceEdit>(
18278 lsp::ApplyWorkspaceEditParams {
18279 label: None,
18280 edit: lsp::WorkspaceEdit {
18281 changes: Some(
18282 [(
18283 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18284 vec![lsp::TextEdit {
18285 range: lsp::Range::new(
18286 lsp::Position::new(0, 0),
18287 lsp::Position::new(0, 0),
18288 ),
18289 new_text: "X".into(),
18290 }],
18291 )]
18292 .into_iter()
18293 .collect(),
18294 ),
18295 ..Default::default()
18296 },
18297 },
18298 )
18299 .await
18300 .unwrap();
18301 Ok(Some(json!(null)))
18302 }
18303 }
18304 })
18305 .next()
18306 .await;
18307
18308 // Applying the code lens command returns a project transaction containing the edits
18309 // sent by the language server in its `workspaceEdit` request.
18310 let transaction = apply.await.unwrap();
18311 assert!(transaction.0.contains_key(&buffer));
18312 buffer.update(cx, |buffer, cx| {
18313 assert_eq!(buffer.text(), "Xa");
18314 buffer.undo(cx);
18315 assert_eq!(buffer.text(), "a");
18316 });
18317}
18318
18319#[gpui::test]
18320async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18321 init_test(cx, |_| {});
18322
18323 let fs = FakeFs::new(cx.executor());
18324 let main_text = r#"fn main() {
18325println!("1");
18326println!("2");
18327println!("3");
18328println!("4");
18329println!("5");
18330}"#;
18331 let lib_text = "mod foo {}";
18332 fs.insert_tree(
18333 path!("/a"),
18334 json!({
18335 "lib.rs": lib_text,
18336 "main.rs": main_text,
18337 }),
18338 )
18339 .await;
18340
18341 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18342 let (workspace, cx) =
18343 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18344 let worktree_id = workspace.update(cx, |workspace, cx| {
18345 workspace.project().update(cx, |project, cx| {
18346 project.worktrees(cx).next().unwrap().read(cx).id()
18347 })
18348 });
18349
18350 let expected_ranges = vec![
18351 Point::new(0, 0)..Point::new(0, 0),
18352 Point::new(1, 0)..Point::new(1, 1),
18353 Point::new(2, 0)..Point::new(2, 2),
18354 Point::new(3, 0)..Point::new(3, 3),
18355 ];
18356
18357 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18358 let editor_1 = workspace
18359 .update_in(cx, |workspace, window, cx| {
18360 workspace.open_path(
18361 (worktree_id, "main.rs"),
18362 Some(pane_1.downgrade()),
18363 true,
18364 window,
18365 cx,
18366 )
18367 })
18368 .unwrap()
18369 .await
18370 .downcast::<Editor>()
18371 .unwrap();
18372 pane_1.update(cx, |pane, cx| {
18373 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18374 open_editor.update(cx, |editor, cx| {
18375 assert_eq!(
18376 editor.display_text(cx),
18377 main_text,
18378 "Original main.rs text on initial open",
18379 );
18380 assert_eq!(
18381 editor
18382 .selections
18383 .all::<Point>(cx)
18384 .into_iter()
18385 .map(|s| s.range())
18386 .collect::<Vec<_>>(),
18387 vec![Point::zero()..Point::zero()],
18388 "Default selections on initial open",
18389 );
18390 })
18391 });
18392 editor_1.update_in(cx, |editor, window, cx| {
18393 editor.change_selections(None, window, cx, |s| {
18394 s.select_ranges(expected_ranges.clone());
18395 });
18396 });
18397
18398 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18399 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18400 });
18401 let editor_2 = workspace
18402 .update_in(cx, |workspace, window, cx| {
18403 workspace.open_path(
18404 (worktree_id, "main.rs"),
18405 Some(pane_2.downgrade()),
18406 true,
18407 window,
18408 cx,
18409 )
18410 })
18411 .unwrap()
18412 .await
18413 .downcast::<Editor>()
18414 .unwrap();
18415 pane_2.update(cx, |pane, cx| {
18416 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18417 open_editor.update(cx, |editor, cx| {
18418 assert_eq!(
18419 editor.display_text(cx),
18420 main_text,
18421 "Original main.rs text on initial open in another panel",
18422 );
18423 assert_eq!(
18424 editor
18425 .selections
18426 .all::<Point>(cx)
18427 .into_iter()
18428 .map(|s| s.range())
18429 .collect::<Vec<_>>(),
18430 vec![Point::zero()..Point::zero()],
18431 "Default selections on initial open in another panel",
18432 );
18433 })
18434 });
18435
18436 editor_2.update_in(cx, |editor, window, cx| {
18437 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18438 });
18439
18440 let _other_editor_1 = workspace
18441 .update_in(cx, |workspace, window, cx| {
18442 workspace.open_path(
18443 (worktree_id, "lib.rs"),
18444 Some(pane_1.downgrade()),
18445 true,
18446 window,
18447 cx,
18448 )
18449 })
18450 .unwrap()
18451 .await
18452 .downcast::<Editor>()
18453 .unwrap();
18454 pane_1
18455 .update_in(cx, |pane, window, cx| {
18456 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18457 .unwrap()
18458 })
18459 .await
18460 .unwrap();
18461 drop(editor_1);
18462 pane_1.update(cx, |pane, cx| {
18463 pane.active_item()
18464 .unwrap()
18465 .downcast::<Editor>()
18466 .unwrap()
18467 .update(cx, |editor, cx| {
18468 assert_eq!(
18469 editor.display_text(cx),
18470 lib_text,
18471 "Other file should be open and active",
18472 );
18473 });
18474 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18475 });
18476
18477 let _other_editor_2 = workspace
18478 .update_in(cx, |workspace, window, cx| {
18479 workspace.open_path(
18480 (worktree_id, "lib.rs"),
18481 Some(pane_2.downgrade()),
18482 true,
18483 window,
18484 cx,
18485 )
18486 })
18487 .unwrap()
18488 .await
18489 .downcast::<Editor>()
18490 .unwrap();
18491 pane_2
18492 .update_in(cx, |pane, window, cx| {
18493 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18494 .unwrap()
18495 })
18496 .await
18497 .unwrap();
18498 drop(editor_2);
18499 pane_2.update(cx, |pane, cx| {
18500 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18501 open_editor.update(cx, |editor, cx| {
18502 assert_eq!(
18503 editor.display_text(cx),
18504 lib_text,
18505 "Other file should be open and active in another panel too",
18506 );
18507 });
18508 assert_eq!(
18509 pane.items().count(),
18510 1,
18511 "No other editors should be open in another pane",
18512 );
18513 });
18514
18515 let _editor_1_reopened = workspace
18516 .update_in(cx, |workspace, window, cx| {
18517 workspace.open_path(
18518 (worktree_id, "main.rs"),
18519 Some(pane_1.downgrade()),
18520 true,
18521 window,
18522 cx,
18523 )
18524 })
18525 .unwrap()
18526 .await
18527 .downcast::<Editor>()
18528 .unwrap();
18529 let _editor_2_reopened = workspace
18530 .update_in(cx, |workspace, window, cx| {
18531 workspace.open_path(
18532 (worktree_id, "main.rs"),
18533 Some(pane_2.downgrade()),
18534 true,
18535 window,
18536 cx,
18537 )
18538 })
18539 .unwrap()
18540 .await
18541 .downcast::<Editor>()
18542 .unwrap();
18543 pane_1.update(cx, |pane, cx| {
18544 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18545 open_editor.update(cx, |editor, cx| {
18546 assert_eq!(
18547 editor.display_text(cx),
18548 main_text,
18549 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18550 );
18551 assert_eq!(
18552 editor
18553 .selections
18554 .all::<Point>(cx)
18555 .into_iter()
18556 .map(|s| s.range())
18557 .collect::<Vec<_>>(),
18558 expected_ranges,
18559 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18560 );
18561 })
18562 });
18563 pane_2.update(cx, |pane, cx| {
18564 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18565 open_editor.update(cx, |editor, cx| {
18566 assert_eq!(
18567 editor.display_text(cx),
18568 r#"fn main() {
18569⋯rintln!("1");
18570⋯intln!("2");
18571⋯ntln!("3");
18572println!("4");
18573println!("5");
18574}"#,
18575 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18576 );
18577 assert_eq!(
18578 editor
18579 .selections
18580 .all::<Point>(cx)
18581 .into_iter()
18582 .map(|s| s.range())
18583 .collect::<Vec<_>>(),
18584 vec![Point::zero()..Point::zero()],
18585 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18586 );
18587 })
18588 });
18589}
18590
18591#[gpui::test]
18592async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18593 init_test(cx, |_| {});
18594
18595 let fs = FakeFs::new(cx.executor());
18596 let main_text = r#"fn main() {
18597println!("1");
18598println!("2");
18599println!("3");
18600println!("4");
18601println!("5");
18602}"#;
18603 let lib_text = "mod foo {}";
18604 fs.insert_tree(
18605 path!("/a"),
18606 json!({
18607 "lib.rs": lib_text,
18608 "main.rs": main_text,
18609 }),
18610 )
18611 .await;
18612
18613 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18614 let (workspace, cx) =
18615 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18616 let worktree_id = workspace.update(cx, |workspace, cx| {
18617 workspace.project().update(cx, |project, cx| {
18618 project.worktrees(cx).next().unwrap().read(cx).id()
18619 })
18620 });
18621
18622 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18623 let editor = workspace
18624 .update_in(cx, |workspace, window, cx| {
18625 workspace.open_path(
18626 (worktree_id, "main.rs"),
18627 Some(pane.downgrade()),
18628 true,
18629 window,
18630 cx,
18631 )
18632 })
18633 .unwrap()
18634 .await
18635 .downcast::<Editor>()
18636 .unwrap();
18637 pane.update(cx, |pane, cx| {
18638 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18639 open_editor.update(cx, |editor, cx| {
18640 assert_eq!(
18641 editor.display_text(cx),
18642 main_text,
18643 "Original main.rs text on initial open",
18644 );
18645 })
18646 });
18647 editor.update_in(cx, |editor, window, cx| {
18648 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18649 });
18650
18651 cx.update_global(|store: &mut SettingsStore, cx| {
18652 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18653 s.restore_on_file_reopen = Some(false);
18654 });
18655 });
18656 editor.update_in(cx, |editor, window, cx| {
18657 editor.fold_ranges(
18658 vec![
18659 Point::new(1, 0)..Point::new(1, 1),
18660 Point::new(2, 0)..Point::new(2, 2),
18661 Point::new(3, 0)..Point::new(3, 3),
18662 ],
18663 false,
18664 window,
18665 cx,
18666 );
18667 });
18668 pane.update_in(cx, |pane, window, cx| {
18669 pane.close_all_items(&CloseAllItems::default(), window, cx)
18670 .unwrap()
18671 })
18672 .await
18673 .unwrap();
18674 pane.update(cx, |pane, _| {
18675 assert!(pane.active_item().is_none());
18676 });
18677 cx.update_global(|store: &mut SettingsStore, cx| {
18678 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18679 s.restore_on_file_reopen = Some(true);
18680 });
18681 });
18682
18683 let _editor_reopened = workspace
18684 .update_in(cx, |workspace, window, cx| {
18685 workspace.open_path(
18686 (worktree_id, "main.rs"),
18687 Some(pane.downgrade()),
18688 true,
18689 window,
18690 cx,
18691 )
18692 })
18693 .unwrap()
18694 .await
18695 .downcast::<Editor>()
18696 .unwrap();
18697 pane.update(cx, |pane, cx| {
18698 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18699 open_editor.update(cx, |editor, cx| {
18700 assert_eq!(
18701 editor.display_text(cx),
18702 main_text,
18703 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18704 );
18705 })
18706 });
18707}
18708
18709fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18710 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18711 point..point
18712}
18713
18714fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18715 let (text, ranges) = marked_text_ranges(marked_text, true);
18716 assert_eq!(editor.text(cx), text);
18717 assert_eq!(
18718 editor.selections.ranges(cx),
18719 ranges,
18720 "Assert selections are {}",
18721 marked_text
18722 );
18723}
18724
18725pub fn handle_signature_help_request(
18726 cx: &mut EditorLspTestContext,
18727 mocked_response: lsp::SignatureHelp,
18728) -> impl Future<Output = ()> + use<> {
18729 let mut request =
18730 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18731 let mocked_response = mocked_response.clone();
18732 async move { Ok(Some(mocked_response)) }
18733 });
18734
18735 async move {
18736 request.next().await;
18737 }
18738}
18739
18740/// Handle completion request passing a marked string specifying where the completion
18741/// should be triggered from using '|' character, what range should be replaced, and what completions
18742/// should be returned using '<' and '>' to delimit the range.
18743///
18744/// Also see `handle_completion_request_with_insert_and_replace`.
18745#[track_caller]
18746pub fn handle_completion_request(
18747 cx: &mut EditorLspTestContext,
18748 marked_string: &str,
18749 completions: Vec<&'static str>,
18750 counter: Arc<AtomicUsize>,
18751) -> impl Future<Output = ()> {
18752 let complete_from_marker: TextRangeMarker = '|'.into();
18753 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18754 let (_, mut marked_ranges) = marked_text_ranges_by(
18755 marked_string,
18756 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18757 );
18758
18759 let complete_from_position =
18760 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18761 let replace_range =
18762 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18763
18764 let mut request =
18765 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18766 let completions = completions.clone();
18767 counter.fetch_add(1, atomic::Ordering::Release);
18768 async move {
18769 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18770 assert_eq!(
18771 params.text_document_position.position,
18772 complete_from_position
18773 );
18774 Ok(Some(lsp::CompletionResponse::Array(
18775 completions
18776 .iter()
18777 .map(|completion_text| lsp::CompletionItem {
18778 label: completion_text.to_string(),
18779 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18780 range: replace_range,
18781 new_text: completion_text.to_string(),
18782 })),
18783 ..Default::default()
18784 })
18785 .collect(),
18786 )))
18787 }
18788 });
18789
18790 async move {
18791 request.next().await;
18792 }
18793}
18794
18795/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
18796/// given instead, which also contains an `insert` range.
18797///
18798/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
18799/// that is, `replace_range.start..cursor_pos`.
18800pub fn handle_completion_request_with_insert_and_replace(
18801 cx: &mut EditorLspTestContext,
18802 marked_string: &str,
18803 completions: Vec<&'static str>,
18804 counter: Arc<AtomicUsize>,
18805) -> impl Future<Output = ()> {
18806 let complete_from_marker: TextRangeMarker = '|'.into();
18807 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18808 let (_, mut marked_ranges) = marked_text_ranges_by(
18809 marked_string,
18810 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18811 );
18812
18813 let complete_from_position =
18814 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18815 let replace_range =
18816 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18817
18818 let mut request =
18819 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18820 let completions = completions.clone();
18821 counter.fetch_add(1, atomic::Ordering::Release);
18822 async move {
18823 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18824 assert_eq!(
18825 params.text_document_position.position, complete_from_position,
18826 "marker `|` position doesn't match",
18827 );
18828 Ok(Some(lsp::CompletionResponse::Array(
18829 completions
18830 .iter()
18831 .map(|completion_text| lsp::CompletionItem {
18832 label: completion_text.to_string(),
18833 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18834 lsp::InsertReplaceEdit {
18835 insert: lsp::Range {
18836 start: replace_range.start,
18837 end: complete_from_position,
18838 },
18839 replace: replace_range,
18840 new_text: completion_text.to_string(),
18841 },
18842 )),
18843 ..Default::default()
18844 })
18845 .collect(),
18846 )))
18847 }
18848 });
18849
18850 async move {
18851 request.next().await;
18852 }
18853}
18854
18855fn handle_resolve_completion_request(
18856 cx: &mut EditorLspTestContext,
18857 edits: Option<Vec<(&'static str, &'static str)>>,
18858) -> impl Future<Output = ()> {
18859 let edits = edits.map(|edits| {
18860 edits
18861 .iter()
18862 .map(|(marked_string, new_text)| {
18863 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18864 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18865 lsp::TextEdit::new(replace_range, new_text.to_string())
18866 })
18867 .collect::<Vec<_>>()
18868 });
18869
18870 let mut request =
18871 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18872 let edits = edits.clone();
18873 async move {
18874 Ok(lsp::CompletionItem {
18875 additional_text_edits: edits,
18876 ..Default::default()
18877 })
18878 }
18879 });
18880
18881 async move {
18882 request.next().await;
18883 }
18884}
18885
18886pub(crate) fn update_test_language_settings(
18887 cx: &mut TestAppContext,
18888 f: impl Fn(&mut AllLanguageSettingsContent),
18889) {
18890 cx.update(|cx| {
18891 SettingsStore::update_global(cx, |store, cx| {
18892 store.update_user_settings::<AllLanguageSettings>(cx, f);
18893 });
18894 });
18895}
18896
18897pub(crate) fn update_test_project_settings(
18898 cx: &mut TestAppContext,
18899 f: impl Fn(&mut ProjectSettings),
18900) {
18901 cx.update(|cx| {
18902 SettingsStore::update_global(cx, |store, cx| {
18903 store.update_user_settings::<ProjectSettings>(cx, f);
18904 });
18905 });
18906}
18907
18908pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
18909 cx.update(|cx| {
18910 assets::Assets.load_test_fonts(cx);
18911 let store = SettingsStore::test(cx);
18912 cx.set_global(store);
18913 theme::init(theme::LoadThemes::JustBase, cx);
18914 release_channel::init(SemanticVersion::default(), cx);
18915 client::init_settings(cx);
18916 language::init(cx);
18917 Project::init_settings(cx);
18918 workspace::init_settings(cx);
18919 crate::init(cx);
18920 });
18921
18922 update_test_language_settings(cx, f);
18923}
18924
18925#[track_caller]
18926fn assert_hunk_revert(
18927 not_reverted_text_with_selections: &str,
18928 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
18929 expected_reverted_text_with_selections: &str,
18930 base_text: &str,
18931 cx: &mut EditorLspTestContext,
18932) {
18933 cx.set_state(not_reverted_text_with_selections);
18934 cx.set_head_text(base_text);
18935 cx.executor().run_until_parked();
18936
18937 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
18938 let snapshot = editor.snapshot(window, cx);
18939 let reverted_hunk_statuses = snapshot
18940 .buffer_snapshot
18941 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
18942 .map(|hunk| hunk.status().kind)
18943 .collect::<Vec<_>>();
18944
18945 editor.git_restore(&Default::default(), window, cx);
18946 reverted_hunk_statuses
18947 });
18948 cx.executor().run_until_parked();
18949 cx.assert_editor_state(expected_reverted_text_with_selections);
18950 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
18951}