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 enable_lsp_tasks: false,
12543 },
12544 );
12545 });
12546 cx.executor().run_until_parked();
12547 assert_eq!(
12548 server_restarts.load(atomic::Ordering::Acquire),
12549 0,
12550 "Should not restart LSP server on an unrelated LSP settings change"
12551 );
12552
12553 update_test_project_settings(cx, |project_settings| {
12554 project_settings.lsp.insert(
12555 language_server_name.into(),
12556 LspSettings {
12557 binary: None,
12558 settings: None,
12559 initialization_options: Some(json!({
12560 "anotherInitValue": false
12561 })),
12562 enable_lsp_tasks: false,
12563 },
12564 );
12565 });
12566 cx.executor().run_until_parked();
12567 assert_eq!(
12568 server_restarts.load(atomic::Ordering::Acquire),
12569 1,
12570 "Should restart LSP server on a related LSP settings change"
12571 );
12572
12573 update_test_project_settings(cx, |project_settings| {
12574 project_settings.lsp.insert(
12575 language_server_name.into(),
12576 LspSettings {
12577 binary: None,
12578 settings: None,
12579 initialization_options: Some(json!({
12580 "anotherInitValue": false
12581 })),
12582 enable_lsp_tasks: false,
12583 },
12584 );
12585 });
12586 cx.executor().run_until_parked();
12587 assert_eq!(
12588 server_restarts.load(atomic::Ordering::Acquire),
12589 1,
12590 "Should not restart LSP server on a related LSP settings change that is the same"
12591 );
12592
12593 update_test_project_settings(cx, |project_settings| {
12594 project_settings.lsp.insert(
12595 language_server_name.into(),
12596 LspSettings {
12597 binary: None,
12598 settings: None,
12599 initialization_options: None,
12600 enable_lsp_tasks: false,
12601 },
12602 );
12603 });
12604 cx.executor().run_until_parked();
12605 assert_eq!(
12606 server_restarts.load(atomic::Ordering::Acquire),
12607 2,
12608 "Should restart LSP server on another related LSP settings change"
12609 );
12610}
12611
12612#[gpui::test]
12613async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12614 init_test(cx, |_| {});
12615
12616 let mut cx = EditorLspTestContext::new_rust(
12617 lsp::ServerCapabilities {
12618 completion_provider: Some(lsp::CompletionOptions {
12619 trigger_characters: Some(vec![".".to_string()]),
12620 resolve_provider: Some(true),
12621 ..Default::default()
12622 }),
12623 ..Default::default()
12624 },
12625 cx,
12626 )
12627 .await;
12628
12629 cx.set_state("fn main() { let a = 2ˇ; }");
12630 cx.simulate_keystroke(".");
12631 let completion_item = lsp::CompletionItem {
12632 label: "some".into(),
12633 kind: Some(lsp::CompletionItemKind::SNIPPET),
12634 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12635 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12636 kind: lsp::MarkupKind::Markdown,
12637 value: "```rust\nSome(2)\n```".to_string(),
12638 })),
12639 deprecated: Some(false),
12640 sort_text: Some("fffffff2".to_string()),
12641 filter_text: Some("some".to_string()),
12642 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12643 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12644 range: lsp::Range {
12645 start: lsp::Position {
12646 line: 0,
12647 character: 22,
12648 },
12649 end: lsp::Position {
12650 line: 0,
12651 character: 22,
12652 },
12653 },
12654 new_text: "Some(2)".to_string(),
12655 })),
12656 additional_text_edits: Some(vec![lsp::TextEdit {
12657 range: lsp::Range {
12658 start: lsp::Position {
12659 line: 0,
12660 character: 20,
12661 },
12662 end: lsp::Position {
12663 line: 0,
12664 character: 22,
12665 },
12666 },
12667 new_text: "".to_string(),
12668 }]),
12669 ..Default::default()
12670 };
12671
12672 let closure_completion_item = completion_item.clone();
12673 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12674 let task_completion_item = closure_completion_item.clone();
12675 async move {
12676 Ok(Some(lsp::CompletionResponse::Array(vec![
12677 task_completion_item,
12678 ])))
12679 }
12680 });
12681
12682 request.next().await;
12683
12684 cx.condition(|editor, _| editor.context_menu_visible())
12685 .await;
12686 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12687 editor
12688 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12689 .unwrap()
12690 });
12691 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12692
12693 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12694 let task_completion_item = completion_item.clone();
12695 async move { Ok(task_completion_item) }
12696 })
12697 .next()
12698 .await
12699 .unwrap();
12700 apply_additional_edits.await.unwrap();
12701 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12702}
12703
12704#[gpui::test]
12705async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12706 init_test(cx, |_| {});
12707
12708 let mut cx = EditorLspTestContext::new_rust(
12709 lsp::ServerCapabilities {
12710 completion_provider: Some(lsp::CompletionOptions {
12711 trigger_characters: Some(vec![".".to_string()]),
12712 resolve_provider: Some(true),
12713 ..Default::default()
12714 }),
12715 ..Default::default()
12716 },
12717 cx,
12718 )
12719 .await;
12720
12721 cx.set_state("fn main() { let a = 2ˇ; }");
12722 cx.simulate_keystroke(".");
12723
12724 let item1 = lsp::CompletionItem {
12725 label: "method id()".to_string(),
12726 filter_text: Some("id".to_string()),
12727 detail: None,
12728 documentation: None,
12729 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12730 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12731 new_text: ".id".to_string(),
12732 })),
12733 ..lsp::CompletionItem::default()
12734 };
12735
12736 let item2 = lsp::CompletionItem {
12737 label: "other".to_string(),
12738 filter_text: Some("other".to_string()),
12739 detail: None,
12740 documentation: None,
12741 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12742 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12743 new_text: ".other".to_string(),
12744 })),
12745 ..lsp::CompletionItem::default()
12746 };
12747
12748 let item1 = item1.clone();
12749 cx.set_request_handler::<lsp::request::Completion, _, _>({
12750 let item1 = item1.clone();
12751 move |_, _, _| {
12752 let item1 = item1.clone();
12753 let item2 = item2.clone();
12754 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12755 }
12756 })
12757 .next()
12758 .await;
12759
12760 cx.condition(|editor, _| editor.context_menu_visible())
12761 .await;
12762 cx.update_editor(|editor, _, _| {
12763 let context_menu = editor.context_menu.borrow_mut();
12764 let context_menu = context_menu
12765 .as_ref()
12766 .expect("Should have the context menu deployed");
12767 match context_menu {
12768 CodeContextMenu::Completions(completions_menu) => {
12769 let completions = completions_menu.completions.borrow_mut();
12770 assert_eq!(
12771 completions
12772 .iter()
12773 .map(|completion| &completion.label.text)
12774 .collect::<Vec<_>>(),
12775 vec!["method id()", "other"]
12776 )
12777 }
12778 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12779 }
12780 });
12781
12782 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12783 let item1 = item1.clone();
12784 move |_, item_to_resolve, _| {
12785 let item1 = item1.clone();
12786 async move {
12787 if item1 == item_to_resolve {
12788 Ok(lsp::CompletionItem {
12789 label: "method id()".to_string(),
12790 filter_text: Some("id".to_string()),
12791 detail: Some("Now resolved!".to_string()),
12792 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12793 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12794 range: lsp::Range::new(
12795 lsp::Position::new(0, 22),
12796 lsp::Position::new(0, 22),
12797 ),
12798 new_text: ".id".to_string(),
12799 })),
12800 ..lsp::CompletionItem::default()
12801 })
12802 } else {
12803 Ok(item_to_resolve)
12804 }
12805 }
12806 }
12807 })
12808 .next()
12809 .await
12810 .unwrap();
12811 cx.run_until_parked();
12812
12813 cx.update_editor(|editor, window, cx| {
12814 editor.context_menu_next(&Default::default(), window, cx);
12815 });
12816
12817 cx.update_editor(|editor, _, _| {
12818 let context_menu = editor.context_menu.borrow_mut();
12819 let context_menu = context_menu
12820 .as_ref()
12821 .expect("Should have the context menu deployed");
12822 match context_menu {
12823 CodeContextMenu::Completions(completions_menu) => {
12824 let completions = completions_menu.completions.borrow_mut();
12825 assert_eq!(
12826 completions
12827 .iter()
12828 .map(|completion| &completion.label.text)
12829 .collect::<Vec<_>>(),
12830 vec!["method id() Now resolved!", "other"],
12831 "Should update first completion label, but not second as the filter text did not match."
12832 );
12833 }
12834 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12835 }
12836 });
12837}
12838
12839#[gpui::test]
12840async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12841 init_test(cx, |_| {});
12842
12843 let mut cx = EditorLspTestContext::new_rust(
12844 lsp::ServerCapabilities {
12845 completion_provider: Some(lsp::CompletionOptions {
12846 trigger_characters: Some(vec![".".to_string()]),
12847 resolve_provider: Some(true),
12848 ..Default::default()
12849 }),
12850 ..Default::default()
12851 },
12852 cx,
12853 )
12854 .await;
12855
12856 cx.set_state("fn main() { let a = 2ˇ; }");
12857 cx.simulate_keystroke(".");
12858
12859 let unresolved_item_1 = lsp::CompletionItem {
12860 label: "id".to_string(),
12861 filter_text: Some("id".to_string()),
12862 detail: None,
12863 documentation: None,
12864 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12865 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12866 new_text: ".id".to_string(),
12867 })),
12868 ..lsp::CompletionItem::default()
12869 };
12870 let resolved_item_1 = lsp::CompletionItem {
12871 additional_text_edits: Some(vec![lsp::TextEdit {
12872 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12873 new_text: "!!".to_string(),
12874 }]),
12875 ..unresolved_item_1.clone()
12876 };
12877 let unresolved_item_2 = lsp::CompletionItem {
12878 label: "other".to_string(),
12879 filter_text: Some("other".to_string()),
12880 detail: None,
12881 documentation: None,
12882 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12883 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12884 new_text: ".other".to_string(),
12885 })),
12886 ..lsp::CompletionItem::default()
12887 };
12888 let resolved_item_2 = lsp::CompletionItem {
12889 additional_text_edits: Some(vec![lsp::TextEdit {
12890 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
12891 new_text: "??".to_string(),
12892 }]),
12893 ..unresolved_item_2.clone()
12894 };
12895
12896 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
12897 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
12898 cx.lsp
12899 .server
12900 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
12901 let unresolved_item_1 = unresolved_item_1.clone();
12902 let resolved_item_1 = resolved_item_1.clone();
12903 let unresolved_item_2 = unresolved_item_2.clone();
12904 let resolved_item_2 = resolved_item_2.clone();
12905 let resolve_requests_1 = resolve_requests_1.clone();
12906 let resolve_requests_2 = resolve_requests_2.clone();
12907 move |unresolved_request, _| {
12908 let unresolved_item_1 = unresolved_item_1.clone();
12909 let resolved_item_1 = resolved_item_1.clone();
12910 let unresolved_item_2 = unresolved_item_2.clone();
12911 let resolved_item_2 = resolved_item_2.clone();
12912 let resolve_requests_1 = resolve_requests_1.clone();
12913 let resolve_requests_2 = resolve_requests_2.clone();
12914 async move {
12915 if unresolved_request == unresolved_item_1 {
12916 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
12917 Ok(resolved_item_1.clone())
12918 } else if unresolved_request == unresolved_item_2 {
12919 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
12920 Ok(resolved_item_2.clone())
12921 } else {
12922 panic!("Unexpected completion item {unresolved_request:?}")
12923 }
12924 }
12925 }
12926 })
12927 .detach();
12928
12929 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12930 let unresolved_item_1 = unresolved_item_1.clone();
12931 let unresolved_item_2 = unresolved_item_2.clone();
12932 async move {
12933 Ok(Some(lsp::CompletionResponse::Array(vec![
12934 unresolved_item_1,
12935 unresolved_item_2,
12936 ])))
12937 }
12938 })
12939 .next()
12940 .await;
12941
12942 cx.condition(|editor, _| editor.context_menu_visible())
12943 .await;
12944 cx.update_editor(|editor, _, _| {
12945 let context_menu = editor.context_menu.borrow_mut();
12946 let context_menu = context_menu
12947 .as_ref()
12948 .expect("Should have the context menu deployed");
12949 match context_menu {
12950 CodeContextMenu::Completions(completions_menu) => {
12951 let completions = completions_menu.completions.borrow_mut();
12952 assert_eq!(
12953 completions
12954 .iter()
12955 .map(|completion| &completion.label.text)
12956 .collect::<Vec<_>>(),
12957 vec!["id", "other"]
12958 )
12959 }
12960 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12961 }
12962 });
12963 cx.run_until_parked();
12964
12965 cx.update_editor(|editor, window, cx| {
12966 editor.context_menu_next(&ContextMenuNext, window, cx);
12967 });
12968 cx.run_until_parked();
12969 cx.update_editor(|editor, window, cx| {
12970 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
12971 });
12972 cx.run_until_parked();
12973 cx.update_editor(|editor, window, cx| {
12974 editor.context_menu_next(&ContextMenuNext, window, cx);
12975 });
12976 cx.run_until_parked();
12977 cx.update_editor(|editor, window, cx| {
12978 editor
12979 .compose_completion(&ComposeCompletion::default(), window, cx)
12980 .expect("No task returned")
12981 })
12982 .await
12983 .expect("Completion failed");
12984 cx.run_until_parked();
12985
12986 cx.update_editor(|editor, _, cx| {
12987 assert_eq!(
12988 resolve_requests_1.load(atomic::Ordering::Acquire),
12989 1,
12990 "Should always resolve once despite multiple selections"
12991 );
12992 assert_eq!(
12993 resolve_requests_2.load(atomic::Ordering::Acquire),
12994 1,
12995 "Should always resolve once after multiple selections and applying the completion"
12996 );
12997 assert_eq!(
12998 editor.text(cx),
12999 "fn main() { let a = ??.other; }",
13000 "Should use resolved data when applying the completion"
13001 );
13002 });
13003}
13004
13005#[gpui::test]
13006async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13007 init_test(cx, |_| {});
13008
13009 let item_0 = lsp::CompletionItem {
13010 label: "abs".into(),
13011 insert_text: Some("abs".into()),
13012 data: Some(json!({ "very": "special"})),
13013 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13014 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13015 lsp::InsertReplaceEdit {
13016 new_text: "abs".to_string(),
13017 insert: lsp::Range::default(),
13018 replace: lsp::Range::default(),
13019 },
13020 )),
13021 ..lsp::CompletionItem::default()
13022 };
13023 let items = iter::once(item_0.clone())
13024 .chain((11..51).map(|i| lsp::CompletionItem {
13025 label: format!("item_{}", i),
13026 insert_text: Some(format!("item_{}", i)),
13027 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13028 ..lsp::CompletionItem::default()
13029 }))
13030 .collect::<Vec<_>>();
13031
13032 let default_commit_characters = vec!["?".to_string()];
13033 let default_data = json!({ "default": "data"});
13034 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13035 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13036 let default_edit_range = lsp::Range {
13037 start: lsp::Position {
13038 line: 0,
13039 character: 5,
13040 },
13041 end: lsp::Position {
13042 line: 0,
13043 character: 5,
13044 },
13045 };
13046
13047 let mut cx = EditorLspTestContext::new_rust(
13048 lsp::ServerCapabilities {
13049 completion_provider: Some(lsp::CompletionOptions {
13050 trigger_characters: Some(vec![".".to_string()]),
13051 resolve_provider: Some(true),
13052 ..Default::default()
13053 }),
13054 ..Default::default()
13055 },
13056 cx,
13057 )
13058 .await;
13059
13060 cx.set_state("fn main() { let a = 2ˇ; }");
13061 cx.simulate_keystroke(".");
13062
13063 let completion_data = default_data.clone();
13064 let completion_characters = default_commit_characters.clone();
13065 let completion_items = items.clone();
13066 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13067 let default_data = completion_data.clone();
13068 let default_commit_characters = completion_characters.clone();
13069 let items = completion_items.clone();
13070 async move {
13071 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13072 items,
13073 item_defaults: Some(lsp::CompletionListItemDefaults {
13074 data: Some(default_data.clone()),
13075 commit_characters: Some(default_commit_characters.clone()),
13076 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13077 default_edit_range,
13078 )),
13079 insert_text_format: Some(default_insert_text_format),
13080 insert_text_mode: Some(default_insert_text_mode),
13081 }),
13082 ..lsp::CompletionList::default()
13083 })))
13084 }
13085 })
13086 .next()
13087 .await;
13088
13089 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13090 cx.lsp
13091 .server
13092 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13093 let closure_resolved_items = resolved_items.clone();
13094 move |item_to_resolve, _| {
13095 let closure_resolved_items = closure_resolved_items.clone();
13096 async move {
13097 closure_resolved_items.lock().push(item_to_resolve.clone());
13098 Ok(item_to_resolve)
13099 }
13100 }
13101 })
13102 .detach();
13103
13104 cx.condition(|editor, _| editor.context_menu_visible())
13105 .await;
13106 cx.run_until_parked();
13107 cx.update_editor(|editor, _, _| {
13108 let menu = editor.context_menu.borrow_mut();
13109 match menu.as_ref().expect("should have the completions menu") {
13110 CodeContextMenu::Completions(completions_menu) => {
13111 assert_eq!(
13112 completions_menu
13113 .entries
13114 .borrow()
13115 .iter()
13116 .map(|mat| mat.string.clone())
13117 .collect::<Vec<String>>(),
13118 items
13119 .iter()
13120 .map(|completion| completion.label.clone())
13121 .collect::<Vec<String>>()
13122 );
13123 }
13124 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13125 }
13126 });
13127 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13128 // with 4 from the end.
13129 assert_eq!(
13130 *resolved_items.lock(),
13131 [&items[0..16], &items[items.len() - 4..items.len()]]
13132 .concat()
13133 .iter()
13134 .cloned()
13135 .map(|mut item| {
13136 if item.data.is_none() {
13137 item.data = Some(default_data.clone());
13138 }
13139 item
13140 })
13141 .collect::<Vec<lsp::CompletionItem>>(),
13142 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13143 );
13144 resolved_items.lock().clear();
13145
13146 cx.update_editor(|editor, window, cx| {
13147 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13148 });
13149 cx.run_until_parked();
13150 // Completions that have already been resolved are skipped.
13151 assert_eq!(
13152 *resolved_items.lock(),
13153 items[items.len() - 16..items.len() - 4]
13154 .iter()
13155 .cloned()
13156 .map(|mut item| {
13157 if item.data.is_none() {
13158 item.data = Some(default_data.clone());
13159 }
13160 item
13161 })
13162 .collect::<Vec<lsp::CompletionItem>>()
13163 );
13164 resolved_items.lock().clear();
13165}
13166
13167#[gpui::test]
13168async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13169 init_test(cx, |_| {});
13170
13171 let mut cx = EditorLspTestContext::new(
13172 Language::new(
13173 LanguageConfig {
13174 matcher: LanguageMatcher {
13175 path_suffixes: vec!["jsx".into()],
13176 ..Default::default()
13177 },
13178 overrides: [(
13179 "element".into(),
13180 LanguageConfigOverride {
13181 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13182 ..Default::default()
13183 },
13184 )]
13185 .into_iter()
13186 .collect(),
13187 ..Default::default()
13188 },
13189 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13190 )
13191 .with_override_query("(jsx_self_closing_element) @element")
13192 .unwrap(),
13193 lsp::ServerCapabilities {
13194 completion_provider: Some(lsp::CompletionOptions {
13195 trigger_characters: Some(vec![":".to_string()]),
13196 ..Default::default()
13197 }),
13198 ..Default::default()
13199 },
13200 cx,
13201 )
13202 .await;
13203
13204 cx.lsp
13205 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13206 Ok(Some(lsp::CompletionResponse::Array(vec![
13207 lsp::CompletionItem {
13208 label: "bg-blue".into(),
13209 ..Default::default()
13210 },
13211 lsp::CompletionItem {
13212 label: "bg-red".into(),
13213 ..Default::default()
13214 },
13215 lsp::CompletionItem {
13216 label: "bg-yellow".into(),
13217 ..Default::default()
13218 },
13219 ])))
13220 });
13221
13222 cx.set_state(r#"<p class="bgˇ" />"#);
13223
13224 // Trigger completion when typing a dash, because the dash is an extra
13225 // word character in the 'element' scope, which contains the cursor.
13226 cx.simulate_keystroke("-");
13227 cx.executor().run_until_parked();
13228 cx.update_editor(|editor, _, _| {
13229 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13230 {
13231 assert_eq!(
13232 completion_menu_entries(&menu),
13233 &["bg-red", "bg-blue", "bg-yellow"]
13234 );
13235 } else {
13236 panic!("expected completion menu to be open");
13237 }
13238 });
13239
13240 cx.simulate_keystroke("l");
13241 cx.executor().run_until_parked();
13242 cx.update_editor(|editor, _, _| {
13243 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13244 {
13245 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13246 } else {
13247 panic!("expected completion menu to be open");
13248 }
13249 });
13250
13251 // When filtering completions, consider the character after the '-' to
13252 // be the start of a subword.
13253 cx.set_state(r#"<p class="yelˇ" />"#);
13254 cx.simulate_keystroke("l");
13255 cx.executor().run_until_parked();
13256 cx.update_editor(|editor, _, _| {
13257 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13258 {
13259 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13260 } else {
13261 panic!("expected completion menu to be open");
13262 }
13263 });
13264}
13265
13266fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13267 let entries = menu.entries.borrow();
13268 entries.iter().map(|mat| mat.string.clone()).collect()
13269}
13270
13271#[gpui::test]
13272async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13273 init_test(cx, |settings| {
13274 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13275 FormatterList(vec![Formatter::Prettier].into()),
13276 ))
13277 });
13278
13279 let fs = FakeFs::new(cx.executor());
13280 fs.insert_file(path!("/file.ts"), Default::default()).await;
13281
13282 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13283 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13284
13285 language_registry.add(Arc::new(Language::new(
13286 LanguageConfig {
13287 name: "TypeScript".into(),
13288 matcher: LanguageMatcher {
13289 path_suffixes: vec!["ts".to_string()],
13290 ..Default::default()
13291 },
13292 ..Default::default()
13293 },
13294 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13295 )));
13296 update_test_language_settings(cx, |settings| {
13297 settings.defaults.prettier = Some(PrettierSettings {
13298 allowed: true,
13299 ..PrettierSettings::default()
13300 });
13301 });
13302
13303 let test_plugin = "test_plugin";
13304 let _ = language_registry.register_fake_lsp(
13305 "TypeScript",
13306 FakeLspAdapter {
13307 prettier_plugins: vec![test_plugin],
13308 ..Default::default()
13309 },
13310 );
13311
13312 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13313 let buffer = project
13314 .update(cx, |project, cx| {
13315 project.open_local_buffer(path!("/file.ts"), cx)
13316 })
13317 .await
13318 .unwrap();
13319
13320 let buffer_text = "one\ntwo\nthree\n";
13321 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13322 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13323 editor.update_in(cx, |editor, window, cx| {
13324 editor.set_text(buffer_text, window, cx)
13325 });
13326
13327 editor
13328 .update_in(cx, |editor, window, cx| {
13329 editor.perform_format(
13330 project.clone(),
13331 FormatTrigger::Manual,
13332 FormatTarget::Buffers,
13333 window,
13334 cx,
13335 )
13336 })
13337 .unwrap()
13338 .await;
13339 assert_eq!(
13340 editor.update(cx, |editor, cx| editor.text(cx)),
13341 buffer_text.to_string() + prettier_format_suffix,
13342 "Test prettier formatting was not applied to the original buffer text",
13343 );
13344
13345 update_test_language_settings(cx, |settings| {
13346 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13347 });
13348 let format = editor.update_in(cx, |editor, window, cx| {
13349 editor.perform_format(
13350 project.clone(),
13351 FormatTrigger::Manual,
13352 FormatTarget::Buffers,
13353 window,
13354 cx,
13355 )
13356 });
13357 format.await.unwrap();
13358 assert_eq!(
13359 editor.update(cx, |editor, cx| editor.text(cx)),
13360 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13361 "Autoformatting (via test prettier) was not applied to the original buffer text",
13362 );
13363}
13364
13365#[gpui::test]
13366async fn test_addition_reverts(cx: &mut TestAppContext) {
13367 init_test(cx, |_| {});
13368 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13369 let base_text = indoc! {r#"
13370 struct Row;
13371 struct Row1;
13372 struct Row2;
13373
13374 struct Row4;
13375 struct Row5;
13376 struct Row6;
13377
13378 struct Row8;
13379 struct Row9;
13380 struct Row10;"#};
13381
13382 // When addition hunks are not adjacent to carets, no hunk revert is performed
13383 assert_hunk_revert(
13384 indoc! {r#"struct Row;
13385 struct Row1;
13386 struct Row1.1;
13387 struct Row1.2;
13388 struct Row2;ˇ
13389
13390 struct Row4;
13391 struct Row5;
13392 struct Row6;
13393
13394 struct Row8;
13395 ˇstruct Row9;
13396 struct Row9.1;
13397 struct Row9.2;
13398 struct Row9.3;
13399 struct Row10;"#},
13400 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13401 indoc! {r#"struct Row;
13402 struct Row1;
13403 struct Row1.1;
13404 struct Row1.2;
13405 struct Row2;ˇ
13406
13407 struct Row4;
13408 struct Row5;
13409 struct Row6;
13410
13411 struct Row8;
13412 ˇstruct Row9;
13413 struct Row9.1;
13414 struct Row9.2;
13415 struct Row9.3;
13416 struct Row10;"#},
13417 base_text,
13418 &mut cx,
13419 );
13420 // Same for selections
13421 assert_hunk_revert(
13422 indoc! {r#"struct Row;
13423 struct Row1;
13424 struct Row2;
13425 struct Row2.1;
13426 struct Row2.2;
13427 «ˇ
13428 struct Row4;
13429 struct» Row5;
13430 «struct Row6;
13431 ˇ»
13432 struct Row9.1;
13433 struct Row9.2;
13434 struct Row9.3;
13435 struct Row8;
13436 struct Row9;
13437 struct Row10;"#},
13438 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13439 indoc! {r#"struct Row;
13440 struct Row1;
13441 struct Row2;
13442 struct Row2.1;
13443 struct Row2.2;
13444 «ˇ
13445 struct Row4;
13446 struct» Row5;
13447 «struct Row6;
13448 ˇ»
13449 struct Row9.1;
13450 struct Row9.2;
13451 struct Row9.3;
13452 struct Row8;
13453 struct Row9;
13454 struct Row10;"#},
13455 base_text,
13456 &mut cx,
13457 );
13458
13459 // When carets and selections intersect the addition hunks, those are reverted.
13460 // Adjacent carets got merged.
13461 assert_hunk_revert(
13462 indoc! {r#"struct Row;
13463 ˇ// something on the top
13464 struct Row1;
13465 struct Row2;
13466 struct Roˇw3.1;
13467 struct Row2.2;
13468 struct Row2.3;ˇ
13469
13470 struct Row4;
13471 struct ˇRow5.1;
13472 struct Row5.2;
13473 struct «Rowˇ»5.3;
13474 struct Row5;
13475 struct Row6;
13476 ˇ
13477 struct Row9.1;
13478 struct «Rowˇ»9.2;
13479 struct «ˇRow»9.3;
13480 struct Row8;
13481 struct Row9;
13482 «ˇ// something on bottom»
13483 struct Row10;"#},
13484 vec![
13485 DiffHunkStatusKind::Added,
13486 DiffHunkStatusKind::Added,
13487 DiffHunkStatusKind::Added,
13488 DiffHunkStatusKind::Added,
13489 DiffHunkStatusKind::Added,
13490 ],
13491 indoc! {r#"struct Row;
13492 ˇstruct Row1;
13493 struct Row2;
13494 ˇ
13495 struct Row4;
13496 ˇstruct Row5;
13497 struct Row6;
13498 ˇ
13499 ˇstruct Row8;
13500 struct Row9;
13501 ˇstruct Row10;"#},
13502 base_text,
13503 &mut cx,
13504 );
13505}
13506
13507#[gpui::test]
13508async fn test_modification_reverts(cx: &mut TestAppContext) {
13509 init_test(cx, |_| {});
13510 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13511 let base_text = indoc! {r#"
13512 struct Row;
13513 struct Row1;
13514 struct Row2;
13515
13516 struct Row4;
13517 struct Row5;
13518 struct Row6;
13519
13520 struct Row8;
13521 struct Row9;
13522 struct Row10;"#};
13523
13524 // Modification hunks behave the same as the addition ones.
13525 assert_hunk_revert(
13526 indoc! {r#"struct Row;
13527 struct Row1;
13528 struct Row33;
13529 ˇ
13530 struct Row4;
13531 struct Row5;
13532 struct Row6;
13533 ˇ
13534 struct Row99;
13535 struct Row9;
13536 struct Row10;"#},
13537 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13538 indoc! {r#"struct Row;
13539 struct Row1;
13540 struct Row33;
13541 ˇ
13542 struct Row4;
13543 struct Row5;
13544 struct Row6;
13545 ˇ
13546 struct Row99;
13547 struct Row9;
13548 struct Row10;"#},
13549 base_text,
13550 &mut cx,
13551 );
13552 assert_hunk_revert(
13553 indoc! {r#"struct Row;
13554 struct Row1;
13555 struct Row33;
13556 «ˇ
13557 struct Row4;
13558 struct» Row5;
13559 «struct Row6;
13560 ˇ»
13561 struct Row99;
13562 struct Row9;
13563 struct Row10;"#},
13564 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13565 indoc! {r#"struct Row;
13566 struct Row1;
13567 struct Row33;
13568 «ˇ
13569 struct Row4;
13570 struct» Row5;
13571 «struct Row6;
13572 ˇ»
13573 struct Row99;
13574 struct Row9;
13575 struct Row10;"#},
13576 base_text,
13577 &mut cx,
13578 );
13579
13580 assert_hunk_revert(
13581 indoc! {r#"ˇstruct Row1.1;
13582 struct Row1;
13583 «ˇstr»uct Row22;
13584
13585 struct ˇRow44;
13586 struct Row5;
13587 struct «Rˇ»ow66;ˇ
13588
13589 «struˇ»ct Row88;
13590 struct Row9;
13591 struct Row1011;ˇ"#},
13592 vec![
13593 DiffHunkStatusKind::Modified,
13594 DiffHunkStatusKind::Modified,
13595 DiffHunkStatusKind::Modified,
13596 DiffHunkStatusKind::Modified,
13597 DiffHunkStatusKind::Modified,
13598 DiffHunkStatusKind::Modified,
13599 ],
13600 indoc! {r#"struct Row;
13601 ˇstruct Row1;
13602 struct Row2;
13603 ˇ
13604 struct Row4;
13605 ˇstruct Row5;
13606 struct Row6;
13607 ˇ
13608 struct Row8;
13609 ˇstruct Row9;
13610 struct Row10;ˇ"#},
13611 base_text,
13612 &mut cx,
13613 );
13614}
13615
13616#[gpui::test]
13617async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13618 init_test(cx, |_| {});
13619 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13620 let base_text = indoc! {r#"
13621 one
13622
13623 two
13624 three
13625 "#};
13626
13627 cx.set_head_text(base_text);
13628 cx.set_state("\nˇ\n");
13629 cx.executor().run_until_parked();
13630 cx.update_editor(|editor, _window, cx| {
13631 editor.expand_selected_diff_hunks(cx);
13632 });
13633 cx.executor().run_until_parked();
13634 cx.update_editor(|editor, window, cx| {
13635 editor.backspace(&Default::default(), window, cx);
13636 });
13637 cx.run_until_parked();
13638 cx.assert_state_with_diff(
13639 indoc! {r#"
13640
13641 - two
13642 - threeˇ
13643 +
13644 "#}
13645 .to_string(),
13646 );
13647}
13648
13649#[gpui::test]
13650async fn test_deletion_reverts(cx: &mut TestAppContext) {
13651 init_test(cx, |_| {});
13652 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13653 let base_text = indoc! {r#"struct Row;
13654struct Row1;
13655struct Row2;
13656
13657struct Row4;
13658struct Row5;
13659struct Row6;
13660
13661struct Row8;
13662struct Row9;
13663struct Row10;"#};
13664
13665 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13666 assert_hunk_revert(
13667 indoc! {r#"struct Row;
13668 struct Row2;
13669
13670 ˇstruct Row4;
13671 struct Row5;
13672 struct Row6;
13673 ˇ
13674 struct Row8;
13675 struct Row10;"#},
13676 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13677 indoc! {r#"struct Row;
13678 struct Row2;
13679
13680 ˇstruct Row4;
13681 struct Row5;
13682 struct Row6;
13683 ˇ
13684 struct Row8;
13685 struct Row10;"#},
13686 base_text,
13687 &mut cx,
13688 );
13689 assert_hunk_revert(
13690 indoc! {r#"struct Row;
13691 struct Row2;
13692
13693 «ˇstruct Row4;
13694 struct» Row5;
13695 «struct Row6;
13696 ˇ»
13697 struct Row8;
13698 struct Row10;"#},
13699 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13700 indoc! {r#"struct Row;
13701 struct Row2;
13702
13703 «ˇstruct Row4;
13704 struct» Row5;
13705 «struct Row6;
13706 ˇ»
13707 struct Row8;
13708 struct Row10;"#},
13709 base_text,
13710 &mut cx,
13711 );
13712
13713 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13714 assert_hunk_revert(
13715 indoc! {r#"struct Row;
13716 ˇstruct Row2;
13717
13718 struct Row4;
13719 struct Row5;
13720 struct Row6;
13721
13722 struct Row8;ˇ
13723 struct Row10;"#},
13724 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13725 indoc! {r#"struct Row;
13726 struct Row1;
13727 ˇstruct Row2;
13728
13729 struct Row4;
13730 struct Row5;
13731 struct Row6;
13732
13733 struct Row8;ˇ
13734 struct Row9;
13735 struct Row10;"#},
13736 base_text,
13737 &mut cx,
13738 );
13739 assert_hunk_revert(
13740 indoc! {r#"struct Row;
13741 struct Row2«ˇ;
13742 struct Row4;
13743 struct» Row5;
13744 «struct Row6;
13745
13746 struct Row8;ˇ»
13747 struct Row10;"#},
13748 vec![
13749 DiffHunkStatusKind::Deleted,
13750 DiffHunkStatusKind::Deleted,
13751 DiffHunkStatusKind::Deleted,
13752 ],
13753 indoc! {r#"struct Row;
13754 struct Row1;
13755 struct Row2«ˇ;
13756
13757 struct Row4;
13758 struct» Row5;
13759 «struct Row6;
13760
13761 struct Row8;ˇ»
13762 struct Row9;
13763 struct Row10;"#},
13764 base_text,
13765 &mut cx,
13766 );
13767}
13768
13769#[gpui::test]
13770async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13771 init_test(cx, |_| {});
13772
13773 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13774 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13775 let base_text_3 =
13776 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13777
13778 let text_1 = edit_first_char_of_every_line(base_text_1);
13779 let text_2 = edit_first_char_of_every_line(base_text_2);
13780 let text_3 = edit_first_char_of_every_line(base_text_3);
13781
13782 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13783 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13784 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13785
13786 let multibuffer = cx.new(|cx| {
13787 let mut multibuffer = MultiBuffer::new(ReadWrite);
13788 multibuffer.push_excerpts(
13789 buffer_1.clone(),
13790 [
13791 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13792 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13793 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13794 ],
13795 cx,
13796 );
13797 multibuffer.push_excerpts(
13798 buffer_2.clone(),
13799 [
13800 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13801 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13802 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13803 ],
13804 cx,
13805 );
13806 multibuffer.push_excerpts(
13807 buffer_3.clone(),
13808 [
13809 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13810 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13811 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13812 ],
13813 cx,
13814 );
13815 multibuffer
13816 });
13817
13818 let fs = FakeFs::new(cx.executor());
13819 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13820 let (editor, cx) = cx
13821 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13822 editor.update_in(cx, |editor, _window, cx| {
13823 for (buffer, diff_base) in [
13824 (buffer_1.clone(), base_text_1),
13825 (buffer_2.clone(), base_text_2),
13826 (buffer_3.clone(), base_text_3),
13827 ] {
13828 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13829 editor
13830 .buffer
13831 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13832 }
13833 });
13834 cx.executor().run_until_parked();
13835
13836 editor.update_in(cx, |editor, window, cx| {
13837 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}");
13838 editor.select_all(&SelectAll, window, cx);
13839 editor.git_restore(&Default::default(), window, cx);
13840 });
13841 cx.executor().run_until_parked();
13842
13843 // When all ranges are selected, all buffer hunks are reverted.
13844 editor.update(cx, |editor, cx| {
13845 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");
13846 });
13847 buffer_1.update(cx, |buffer, _| {
13848 assert_eq!(buffer.text(), base_text_1);
13849 });
13850 buffer_2.update(cx, |buffer, _| {
13851 assert_eq!(buffer.text(), base_text_2);
13852 });
13853 buffer_3.update(cx, |buffer, _| {
13854 assert_eq!(buffer.text(), base_text_3);
13855 });
13856
13857 editor.update_in(cx, |editor, window, cx| {
13858 editor.undo(&Default::default(), window, cx);
13859 });
13860
13861 editor.update_in(cx, |editor, window, cx| {
13862 editor.change_selections(None, window, cx, |s| {
13863 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
13864 });
13865 editor.git_restore(&Default::default(), window, cx);
13866 });
13867
13868 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
13869 // but not affect buffer_2 and its related excerpts.
13870 editor.update(cx, |editor, cx| {
13871 assert_eq!(
13872 editor.text(cx),
13873 "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}"
13874 );
13875 });
13876 buffer_1.update(cx, |buffer, _| {
13877 assert_eq!(buffer.text(), base_text_1);
13878 });
13879 buffer_2.update(cx, |buffer, _| {
13880 assert_eq!(
13881 buffer.text(),
13882 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
13883 );
13884 });
13885 buffer_3.update(cx, |buffer, _| {
13886 assert_eq!(
13887 buffer.text(),
13888 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
13889 );
13890 });
13891
13892 fn edit_first_char_of_every_line(text: &str) -> String {
13893 text.split('\n')
13894 .map(|line| format!("X{}", &line[1..]))
13895 .collect::<Vec<_>>()
13896 .join("\n")
13897 }
13898}
13899
13900#[gpui::test]
13901async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
13902 init_test(cx, |_| {});
13903
13904 let cols = 4;
13905 let rows = 10;
13906 let sample_text_1 = sample_text(rows, cols, 'a');
13907 assert_eq!(
13908 sample_text_1,
13909 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
13910 );
13911 let sample_text_2 = sample_text(rows, cols, 'l');
13912 assert_eq!(
13913 sample_text_2,
13914 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
13915 );
13916 let sample_text_3 = sample_text(rows, cols, 'v');
13917 assert_eq!(
13918 sample_text_3,
13919 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
13920 );
13921
13922 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
13923 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
13924 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
13925
13926 let multi_buffer = cx.new(|cx| {
13927 let mut multibuffer = MultiBuffer::new(ReadWrite);
13928 multibuffer.push_excerpts(
13929 buffer_1.clone(),
13930 [
13931 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13932 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13933 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13934 ],
13935 cx,
13936 );
13937 multibuffer.push_excerpts(
13938 buffer_2.clone(),
13939 [
13940 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13941 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13942 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13943 ],
13944 cx,
13945 );
13946 multibuffer.push_excerpts(
13947 buffer_3.clone(),
13948 [
13949 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13950 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13951 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13952 ],
13953 cx,
13954 );
13955 multibuffer
13956 });
13957
13958 let fs = FakeFs::new(cx.executor());
13959 fs.insert_tree(
13960 "/a",
13961 json!({
13962 "main.rs": sample_text_1,
13963 "other.rs": sample_text_2,
13964 "lib.rs": sample_text_3,
13965 }),
13966 )
13967 .await;
13968 let project = Project::test(fs, ["/a".as_ref()], cx).await;
13969 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13970 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13971 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
13972 Editor::new(
13973 EditorMode::Full,
13974 multi_buffer,
13975 Some(project.clone()),
13976 window,
13977 cx,
13978 )
13979 });
13980 let multibuffer_item_id = workspace
13981 .update(cx, |workspace, window, cx| {
13982 assert!(
13983 workspace.active_item(cx).is_none(),
13984 "active item should be None before the first item is added"
13985 );
13986 workspace.add_item_to_active_pane(
13987 Box::new(multi_buffer_editor.clone()),
13988 None,
13989 true,
13990 window,
13991 cx,
13992 );
13993 let active_item = workspace
13994 .active_item(cx)
13995 .expect("should have an active item after adding the multi buffer");
13996 assert!(
13997 !active_item.is_singleton(cx),
13998 "A multi buffer was expected to active after adding"
13999 );
14000 active_item.item_id()
14001 })
14002 .unwrap();
14003 cx.executor().run_until_parked();
14004
14005 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14006 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14007 s.select_ranges(Some(1..2))
14008 });
14009 editor.open_excerpts(&OpenExcerpts, window, cx);
14010 });
14011 cx.executor().run_until_parked();
14012 let first_item_id = workspace
14013 .update(cx, |workspace, window, cx| {
14014 let active_item = workspace
14015 .active_item(cx)
14016 .expect("should have an active item after navigating into the 1st buffer");
14017 let first_item_id = active_item.item_id();
14018 assert_ne!(
14019 first_item_id, multibuffer_item_id,
14020 "Should navigate into the 1st buffer and activate it"
14021 );
14022 assert!(
14023 active_item.is_singleton(cx),
14024 "New active item should be a singleton buffer"
14025 );
14026 assert_eq!(
14027 active_item
14028 .act_as::<Editor>(cx)
14029 .expect("should have navigated into an editor for the 1st buffer")
14030 .read(cx)
14031 .text(cx),
14032 sample_text_1
14033 );
14034
14035 workspace
14036 .go_back(workspace.active_pane().downgrade(), window, cx)
14037 .detach_and_log_err(cx);
14038
14039 first_item_id
14040 })
14041 .unwrap();
14042 cx.executor().run_until_parked();
14043 workspace
14044 .update(cx, |workspace, _, cx| {
14045 let active_item = workspace
14046 .active_item(cx)
14047 .expect("should have an active item after navigating back");
14048 assert_eq!(
14049 active_item.item_id(),
14050 multibuffer_item_id,
14051 "Should navigate back to the multi buffer"
14052 );
14053 assert!(!active_item.is_singleton(cx));
14054 })
14055 .unwrap();
14056
14057 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14058 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14059 s.select_ranges(Some(39..40))
14060 });
14061 editor.open_excerpts(&OpenExcerpts, window, cx);
14062 });
14063 cx.executor().run_until_parked();
14064 let second_item_id = workspace
14065 .update(cx, |workspace, window, cx| {
14066 let active_item = workspace
14067 .active_item(cx)
14068 .expect("should have an active item after navigating into the 2nd buffer");
14069 let second_item_id = active_item.item_id();
14070 assert_ne!(
14071 second_item_id, multibuffer_item_id,
14072 "Should navigate away from the multibuffer"
14073 );
14074 assert_ne!(
14075 second_item_id, first_item_id,
14076 "Should navigate into the 2nd buffer and activate it"
14077 );
14078 assert!(
14079 active_item.is_singleton(cx),
14080 "New active item should be a singleton buffer"
14081 );
14082 assert_eq!(
14083 active_item
14084 .act_as::<Editor>(cx)
14085 .expect("should have navigated into an editor")
14086 .read(cx)
14087 .text(cx),
14088 sample_text_2
14089 );
14090
14091 workspace
14092 .go_back(workspace.active_pane().downgrade(), window, cx)
14093 .detach_and_log_err(cx);
14094
14095 second_item_id
14096 })
14097 .unwrap();
14098 cx.executor().run_until_parked();
14099 workspace
14100 .update(cx, |workspace, _, cx| {
14101 let active_item = workspace
14102 .active_item(cx)
14103 .expect("should have an active item after navigating back from the 2nd buffer");
14104 assert_eq!(
14105 active_item.item_id(),
14106 multibuffer_item_id,
14107 "Should navigate back from the 2nd buffer to the multi buffer"
14108 );
14109 assert!(!active_item.is_singleton(cx));
14110 })
14111 .unwrap();
14112
14113 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14114 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14115 s.select_ranges(Some(70..70))
14116 });
14117 editor.open_excerpts(&OpenExcerpts, window, cx);
14118 });
14119 cx.executor().run_until_parked();
14120 workspace
14121 .update(cx, |workspace, window, cx| {
14122 let active_item = workspace
14123 .active_item(cx)
14124 .expect("should have an active item after navigating into the 3rd buffer");
14125 let third_item_id = active_item.item_id();
14126 assert_ne!(
14127 third_item_id, multibuffer_item_id,
14128 "Should navigate into the 3rd buffer and activate it"
14129 );
14130 assert_ne!(third_item_id, first_item_id);
14131 assert_ne!(third_item_id, second_item_id);
14132 assert!(
14133 active_item.is_singleton(cx),
14134 "New active item should be a singleton buffer"
14135 );
14136 assert_eq!(
14137 active_item
14138 .act_as::<Editor>(cx)
14139 .expect("should have navigated into an editor")
14140 .read(cx)
14141 .text(cx),
14142 sample_text_3
14143 );
14144
14145 workspace
14146 .go_back(workspace.active_pane().downgrade(), window, cx)
14147 .detach_and_log_err(cx);
14148 })
14149 .unwrap();
14150 cx.executor().run_until_parked();
14151 workspace
14152 .update(cx, |workspace, _, cx| {
14153 let active_item = workspace
14154 .active_item(cx)
14155 .expect("should have an active item after navigating back from the 3rd buffer");
14156 assert_eq!(
14157 active_item.item_id(),
14158 multibuffer_item_id,
14159 "Should navigate back from the 3rd buffer to the multi buffer"
14160 );
14161 assert!(!active_item.is_singleton(cx));
14162 })
14163 .unwrap();
14164}
14165
14166#[gpui::test]
14167async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14168 init_test(cx, |_| {});
14169
14170 let mut cx = EditorTestContext::new(cx).await;
14171
14172 let diff_base = r#"
14173 use some::mod;
14174
14175 const A: u32 = 42;
14176
14177 fn main() {
14178 println!("hello");
14179
14180 println!("world");
14181 }
14182 "#
14183 .unindent();
14184
14185 cx.set_state(
14186 &r#"
14187 use some::modified;
14188
14189 ˇ
14190 fn main() {
14191 println!("hello there");
14192
14193 println!("around the");
14194 println!("world");
14195 }
14196 "#
14197 .unindent(),
14198 );
14199
14200 cx.set_head_text(&diff_base);
14201 executor.run_until_parked();
14202
14203 cx.update_editor(|editor, window, cx| {
14204 editor.go_to_next_hunk(&GoToHunk, window, cx);
14205 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14206 });
14207 executor.run_until_parked();
14208 cx.assert_state_with_diff(
14209 r#"
14210 use some::modified;
14211
14212
14213 fn main() {
14214 - println!("hello");
14215 + ˇ println!("hello there");
14216
14217 println!("around the");
14218 println!("world");
14219 }
14220 "#
14221 .unindent(),
14222 );
14223
14224 cx.update_editor(|editor, window, cx| {
14225 for _ in 0..2 {
14226 editor.go_to_next_hunk(&GoToHunk, window, cx);
14227 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14228 }
14229 });
14230 executor.run_until_parked();
14231 cx.assert_state_with_diff(
14232 r#"
14233 - use some::mod;
14234 + ˇuse some::modified;
14235
14236
14237 fn main() {
14238 - println!("hello");
14239 + println!("hello there");
14240
14241 + println!("around the");
14242 println!("world");
14243 }
14244 "#
14245 .unindent(),
14246 );
14247
14248 cx.update_editor(|editor, window, cx| {
14249 editor.go_to_next_hunk(&GoToHunk, window, cx);
14250 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14251 });
14252 executor.run_until_parked();
14253 cx.assert_state_with_diff(
14254 r#"
14255 - use some::mod;
14256 + use some::modified;
14257
14258 - const A: u32 = 42;
14259 ˇ
14260 fn main() {
14261 - println!("hello");
14262 + println!("hello there");
14263
14264 + println!("around the");
14265 println!("world");
14266 }
14267 "#
14268 .unindent(),
14269 );
14270
14271 cx.update_editor(|editor, window, cx| {
14272 editor.cancel(&Cancel, window, cx);
14273 });
14274
14275 cx.assert_state_with_diff(
14276 r#"
14277 use some::modified;
14278
14279 ˇ
14280 fn main() {
14281 println!("hello there");
14282
14283 println!("around the");
14284 println!("world");
14285 }
14286 "#
14287 .unindent(),
14288 );
14289}
14290
14291#[gpui::test]
14292async fn test_diff_base_change_with_expanded_diff_hunks(
14293 executor: BackgroundExecutor,
14294 cx: &mut TestAppContext,
14295) {
14296 init_test(cx, |_| {});
14297
14298 let mut cx = EditorTestContext::new(cx).await;
14299
14300 let diff_base = r#"
14301 use some::mod1;
14302 use some::mod2;
14303
14304 const A: u32 = 42;
14305 const B: u32 = 42;
14306 const C: u32 = 42;
14307
14308 fn main() {
14309 println!("hello");
14310
14311 println!("world");
14312 }
14313 "#
14314 .unindent();
14315
14316 cx.set_state(
14317 &r#"
14318 use some::mod2;
14319
14320 const A: u32 = 42;
14321 const C: u32 = 42;
14322
14323 fn main(ˇ) {
14324 //println!("hello");
14325
14326 println!("world");
14327 //
14328 //
14329 }
14330 "#
14331 .unindent(),
14332 );
14333
14334 cx.set_head_text(&diff_base);
14335 executor.run_until_parked();
14336
14337 cx.update_editor(|editor, window, cx| {
14338 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14339 });
14340 executor.run_until_parked();
14341 cx.assert_state_with_diff(
14342 r#"
14343 - use some::mod1;
14344 use some::mod2;
14345
14346 const A: u32 = 42;
14347 - const B: u32 = 42;
14348 const C: u32 = 42;
14349
14350 fn main(ˇ) {
14351 - println!("hello");
14352 + //println!("hello");
14353
14354 println!("world");
14355 + //
14356 + //
14357 }
14358 "#
14359 .unindent(),
14360 );
14361
14362 cx.set_head_text("new diff base!");
14363 executor.run_until_parked();
14364 cx.assert_state_with_diff(
14365 r#"
14366 - new diff base!
14367 + use some::mod2;
14368 +
14369 + const A: u32 = 42;
14370 + const C: u32 = 42;
14371 +
14372 + fn main(ˇ) {
14373 + //println!("hello");
14374 +
14375 + println!("world");
14376 + //
14377 + //
14378 + }
14379 "#
14380 .unindent(),
14381 );
14382}
14383
14384#[gpui::test]
14385async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14386 init_test(cx, |_| {});
14387
14388 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14389 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14390 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14391 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14392 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14393 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14394
14395 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14396 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14397 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14398
14399 let multi_buffer = cx.new(|cx| {
14400 let mut multibuffer = MultiBuffer::new(ReadWrite);
14401 multibuffer.push_excerpts(
14402 buffer_1.clone(),
14403 [
14404 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14405 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14406 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14407 ],
14408 cx,
14409 );
14410 multibuffer.push_excerpts(
14411 buffer_2.clone(),
14412 [
14413 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14414 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14415 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14416 ],
14417 cx,
14418 );
14419 multibuffer.push_excerpts(
14420 buffer_3.clone(),
14421 [
14422 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14423 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14424 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14425 ],
14426 cx,
14427 );
14428 multibuffer
14429 });
14430
14431 let editor =
14432 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14433 editor
14434 .update(cx, |editor, _window, cx| {
14435 for (buffer, diff_base) in [
14436 (buffer_1.clone(), file_1_old),
14437 (buffer_2.clone(), file_2_old),
14438 (buffer_3.clone(), file_3_old),
14439 ] {
14440 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14441 editor
14442 .buffer
14443 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14444 }
14445 })
14446 .unwrap();
14447
14448 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14449 cx.run_until_parked();
14450
14451 cx.assert_editor_state(
14452 &"
14453 ˇaaa
14454 ccc
14455 ddd
14456
14457 ggg
14458 hhh
14459
14460
14461 lll
14462 mmm
14463 NNN
14464
14465 qqq
14466 rrr
14467
14468 uuu
14469 111
14470 222
14471 333
14472
14473 666
14474 777
14475
14476 000
14477 !!!"
14478 .unindent(),
14479 );
14480
14481 cx.update_editor(|editor, window, cx| {
14482 editor.select_all(&SelectAll, window, cx);
14483 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14484 });
14485 cx.executor().run_until_parked();
14486
14487 cx.assert_state_with_diff(
14488 "
14489 «aaa
14490 - bbb
14491 ccc
14492 ddd
14493
14494 ggg
14495 hhh
14496
14497
14498 lll
14499 mmm
14500 - nnn
14501 + NNN
14502
14503 qqq
14504 rrr
14505
14506 uuu
14507 111
14508 222
14509 333
14510
14511 + 666
14512 777
14513
14514 000
14515 !!!ˇ»"
14516 .unindent(),
14517 );
14518}
14519
14520#[gpui::test]
14521async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14522 init_test(cx, |_| {});
14523
14524 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14525 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14526
14527 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14528 let multi_buffer = cx.new(|cx| {
14529 let mut multibuffer = MultiBuffer::new(ReadWrite);
14530 multibuffer.push_excerpts(
14531 buffer.clone(),
14532 [
14533 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
14534 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
14535 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
14536 ],
14537 cx,
14538 );
14539 multibuffer
14540 });
14541
14542 let editor =
14543 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14544 editor
14545 .update(cx, |editor, _window, cx| {
14546 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14547 editor
14548 .buffer
14549 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14550 })
14551 .unwrap();
14552
14553 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14554 cx.run_until_parked();
14555
14556 cx.update_editor(|editor, window, cx| {
14557 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14558 });
14559 cx.executor().run_until_parked();
14560
14561 // When the start of a hunk coincides with the start of its excerpt,
14562 // the hunk is expanded. When the start of a a hunk is earlier than
14563 // the start of its excerpt, the hunk is not expanded.
14564 cx.assert_state_with_diff(
14565 "
14566 ˇaaa
14567 - bbb
14568 + BBB
14569
14570 - ddd
14571 - eee
14572 + DDD
14573 + EEE
14574 fff
14575
14576 iii
14577 "
14578 .unindent(),
14579 );
14580}
14581
14582#[gpui::test]
14583async fn test_edits_around_expanded_insertion_hunks(
14584 executor: BackgroundExecutor,
14585 cx: &mut TestAppContext,
14586) {
14587 init_test(cx, |_| {});
14588
14589 let mut cx = EditorTestContext::new(cx).await;
14590
14591 let diff_base = r#"
14592 use some::mod1;
14593 use some::mod2;
14594
14595 const A: u32 = 42;
14596
14597 fn main() {
14598 println!("hello");
14599
14600 println!("world");
14601 }
14602 "#
14603 .unindent();
14604 executor.run_until_parked();
14605 cx.set_state(
14606 &r#"
14607 use some::mod1;
14608 use some::mod2;
14609
14610 const A: u32 = 42;
14611 const B: u32 = 42;
14612 const C: u32 = 42;
14613 ˇ
14614
14615 fn main() {
14616 println!("hello");
14617
14618 println!("world");
14619 }
14620 "#
14621 .unindent(),
14622 );
14623
14624 cx.set_head_text(&diff_base);
14625 executor.run_until_parked();
14626
14627 cx.update_editor(|editor, window, cx| {
14628 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14629 });
14630 executor.run_until_parked();
14631
14632 cx.assert_state_with_diff(
14633 r#"
14634 use some::mod1;
14635 use some::mod2;
14636
14637 const A: u32 = 42;
14638 + const B: u32 = 42;
14639 + const C: u32 = 42;
14640 + ˇ
14641
14642 fn main() {
14643 println!("hello");
14644
14645 println!("world");
14646 }
14647 "#
14648 .unindent(),
14649 );
14650
14651 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14652 executor.run_until_parked();
14653
14654 cx.assert_state_with_diff(
14655 r#"
14656 use some::mod1;
14657 use some::mod2;
14658
14659 const A: u32 = 42;
14660 + const B: u32 = 42;
14661 + const C: u32 = 42;
14662 + const D: u32 = 42;
14663 + ˇ
14664
14665 fn main() {
14666 println!("hello");
14667
14668 println!("world");
14669 }
14670 "#
14671 .unindent(),
14672 );
14673
14674 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14675 executor.run_until_parked();
14676
14677 cx.assert_state_with_diff(
14678 r#"
14679 use some::mod1;
14680 use some::mod2;
14681
14682 const A: u32 = 42;
14683 + const B: u32 = 42;
14684 + const C: u32 = 42;
14685 + const D: u32 = 42;
14686 + const E: u32 = 42;
14687 + ˇ
14688
14689 fn main() {
14690 println!("hello");
14691
14692 println!("world");
14693 }
14694 "#
14695 .unindent(),
14696 );
14697
14698 cx.update_editor(|editor, window, cx| {
14699 editor.delete_line(&DeleteLine, window, cx);
14700 });
14701 executor.run_until_parked();
14702
14703 cx.assert_state_with_diff(
14704 r#"
14705 use some::mod1;
14706 use some::mod2;
14707
14708 const A: u32 = 42;
14709 + const B: u32 = 42;
14710 + const C: u32 = 42;
14711 + const D: u32 = 42;
14712 + const E: u32 = 42;
14713 ˇ
14714 fn main() {
14715 println!("hello");
14716
14717 println!("world");
14718 }
14719 "#
14720 .unindent(),
14721 );
14722
14723 cx.update_editor(|editor, window, cx| {
14724 editor.move_up(&MoveUp, window, cx);
14725 editor.delete_line(&DeleteLine, window, cx);
14726 editor.move_up(&MoveUp, window, cx);
14727 editor.delete_line(&DeleteLine, window, cx);
14728 editor.move_up(&MoveUp, window, cx);
14729 editor.delete_line(&DeleteLine, window, cx);
14730 });
14731 executor.run_until_parked();
14732 cx.assert_state_with_diff(
14733 r#"
14734 use some::mod1;
14735 use some::mod2;
14736
14737 const A: u32 = 42;
14738 + const B: u32 = 42;
14739 ˇ
14740 fn main() {
14741 println!("hello");
14742
14743 println!("world");
14744 }
14745 "#
14746 .unindent(),
14747 );
14748
14749 cx.update_editor(|editor, window, cx| {
14750 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14751 editor.delete_line(&DeleteLine, window, cx);
14752 });
14753 executor.run_until_parked();
14754 cx.assert_state_with_diff(
14755 r#"
14756 ˇ
14757 fn main() {
14758 println!("hello");
14759
14760 println!("world");
14761 }
14762 "#
14763 .unindent(),
14764 );
14765}
14766
14767#[gpui::test]
14768async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14769 init_test(cx, |_| {});
14770
14771 let mut cx = EditorTestContext::new(cx).await;
14772 cx.set_head_text(indoc! { "
14773 one
14774 two
14775 three
14776 four
14777 five
14778 "
14779 });
14780 cx.set_state(indoc! { "
14781 one
14782 ˇthree
14783 five
14784 "});
14785 cx.run_until_parked();
14786 cx.update_editor(|editor, window, cx| {
14787 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14788 });
14789 cx.assert_state_with_diff(
14790 indoc! { "
14791 one
14792 - two
14793 ˇthree
14794 - four
14795 five
14796 "}
14797 .to_string(),
14798 );
14799 cx.update_editor(|editor, window, cx| {
14800 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14801 });
14802
14803 cx.assert_state_with_diff(
14804 indoc! { "
14805 one
14806 ˇthree
14807 five
14808 "}
14809 .to_string(),
14810 );
14811
14812 cx.set_state(indoc! { "
14813 one
14814 ˇTWO
14815 three
14816 four
14817 five
14818 "});
14819 cx.run_until_parked();
14820 cx.update_editor(|editor, window, cx| {
14821 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14822 });
14823
14824 cx.assert_state_with_diff(
14825 indoc! { "
14826 one
14827 - two
14828 + ˇTWO
14829 three
14830 four
14831 five
14832 "}
14833 .to_string(),
14834 );
14835 cx.update_editor(|editor, window, cx| {
14836 editor.move_up(&Default::default(), window, cx);
14837 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14838 });
14839 cx.assert_state_with_diff(
14840 indoc! { "
14841 one
14842 ˇTWO
14843 three
14844 four
14845 five
14846 "}
14847 .to_string(),
14848 );
14849}
14850
14851#[gpui::test]
14852async fn test_edits_around_expanded_deletion_hunks(
14853 executor: BackgroundExecutor,
14854 cx: &mut TestAppContext,
14855) {
14856 init_test(cx, |_| {});
14857
14858 let mut cx = EditorTestContext::new(cx).await;
14859
14860 let diff_base = r#"
14861 use some::mod1;
14862 use some::mod2;
14863
14864 const A: u32 = 42;
14865 const B: u32 = 42;
14866 const C: u32 = 42;
14867
14868
14869 fn main() {
14870 println!("hello");
14871
14872 println!("world");
14873 }
14874 "#
14875 .unindent();
14876 executor.run_until_parked();
14877 cx.set_state(
14878 &r#"
14879 use some::mod1;
14880 use some::mod2;
14881
14882 ˇconst B: u32 = 42;
14883 const C: u32 = 42;
14884
14885
14886 fn main() {
14887 println!("hello");
14888
14889 println!("world");
14890 }
14891 "#
14892 .unindent(),
14893 );
14894
14895 cx.set_head_text(&diff_base);
14896 executor.run_until_parked();
14897
14898 cx.update_editor(|editor, window, cx| {
14899 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14900 });
14901 executor.run_until_parked();
14902
14903 cx.assert_state_with_diff(
14904 r#"
14905 use some::mod1;
14906 use some::mod2;
14907
14908 - const A: u32 = 42;
14909 ˇconst B: u32 = 42;
14910 const C: u32 = 42;
14911
14912
14913 fn main() {
14914 println!("hello");
14915
14916 println!("world");
14917 }
14918 "#
14919 .unindent(),
14920 );
14921
14922 cx.update_editor(|editor, window, cx| {
14923 editor.delete_line(&DeleteLine, window, cx);
14924 });
14925 executor.run_until_parked();
14926 cx.assert_state_with_diff(
14927 r#"
14928 use some::mod1;
14929 use some::mod2;
14930
14931 - const A: u32 = 42;
14932 - const B: u32 = 42;
14933 ˇconst C: u32 = 42;
14934
14935
14936 fn main() {
14937 println!("hello");
14938
14939 println!("world");
14940 }
14941 "#
14942 .unindent(),
14943 );
14944
14945 cx.update_editor(|editor, window, cx| {
14946 editor.delete_line(&DeleteLine, window, cx);
14947 });
14948 executor.run_until_parked();
14949 cx.assert_state_with_diff(
14950 r#"
14951 use some::mod1;
14952 use some::mod2;
14953
14954 - const A: u32 = 42;
14955 - const B: u32 = 42;
14956 - const C: u32 = 42;
14957 ˇ
14958
14959 fn main() {
14960 println!("hello");
14961
14962 println!("world");
14963 }
14964 "#
14965 .unindent(),
14966 );
14967
14968 cx.update_editor(|editor, window, cx| {
14969 editor.handle_input("replacement", window, cx);
14970 });
14971 executor.run_until_parked();
14972 cx.assert_state_with_diff(
14973 r#"
14974 use some::mod1;
14975 use some::mod2;
14976
14977 - const A: u32 = 42;
14978 - const B: u32 = 42;
14979 - const C: u32 = 42;
14980 -
14981 + replacementˇ
14982
14983 fn main() {
14984 println!("hello");
14985
14986 println!("world");
14987 }
14988 "#
14989 .unindent(),
14990 );
14991}
14992
14993#[gpui::test]
14994async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14995 init_test(cx, |_| {});
14996
14997 let mut cx = EditorTestContext::new(cx).await;
14998
14999 let base_text = r#"
15000 one
15001 two
15002 three
15003 four
15004 five
15005 "#
15006 .unindent();
15007 executor.run_until_parked();
15008 cx.set_state(
15009 &r#"
15010 one
15011 two
15012 fˇour
15013 five
15014 "#
15015 .unindent(),
15016 );
15017
15018 cx.set_head_text(&base_text);
15019 executor.run_until_parked();
15020
15021 cx.update_editor(|editor, window, cx| {
15022 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15023 });
15024 executor.run_until_parked();
15025
15026 cx.assert_state_with_diff(
15027 r#"
15028 one
15029 two
15030 - three
15031 fˇour
15032 five
15033 "#
15034 .unindent(),
15035 );
15036
15037 cx.update_editor(|editor, window, cx| {
15038 editor.backspace(&Backspace, window, cx);
15039 editor.backspace(&Backspace, window, cx);
15040 });
15041 executor.run_until_parked();
15042 cx.assert_state_with_diff(
15043 r#"
15044 one
15045 two
15046 - threeˇ
15047 - four
15048 + our
15049 five
15050 "#
15051 .unindent(),
15052 );
15053}
15054
15055#[gpui::test]
15056async fn test_edit_after_expanded_modification_hunk(
15057 executor: BackgroundExecutor,
15058 cx: &mut TestAppContext,
15059) {
15060 init_test(cx, |_| {});
15061
15062 let mut cx = EditorTestContext::new(cx).await;
15063
15064 let diff_base = r#"
15065 use some::mod1;
15066 use some::mod2;
15067
15068 const A: u32 = 42;
15069 const B: u32 = 42;
15070 const C: u32 = 42;
15071 const D: u32 = 42;
15072
15073
15074 fn main() {
15075 println!("hello");
15076
15077 println!("world");
15078 }"#
15079 .unindent();
15080
15081 cx.set_state(
15082 &r#"
15083 use some::mod1;
15084 use some::mod2;
15085
15086 const A: u32 = 42;
15087 const B: u32 = 42;
15088 const C: u32 = 43ˇ
15089 const D: u32 = 42;
15090
15091
15092 fn main() {
15093 println!("hello");
15094
15095 println!("world");
15096 }"#
15097 .unindent(),
15098 );
15099
15100 cx.set_head_text(&diff_base);
15101 executor.run_until_parked();
15102 cx.update_editor(|editor, window, cx| {
15103 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15104 });
15105 executor.run_until_parked();
15106
15107 cx.assert_state_with_diff(
15108 r#"
15109 use some::mod1;
15110 use some::mod2;
15111
15112 const A: u32 = 42;
15113 const B: u32 = 42;
15114 - const C: u32 = 42;
15115 + const C: u32 = 43ˇ
15116 const D: u32 = 42;
15117
15118
15119 fn main() {
15120 println!("hello");
15121
15122 println!("world");
15123 }"#
15124 .unindent(),
15125 );
15126
15127 cx.update_editor(|editor, window, cx| {
15128 editor.handle_input("\nnew_line\n", window, cx);
15129 });
15130 executor.run_until_parked();
15131
15132 cx.assert_state_with_diff(
15133 r#"
15134 use some::mod1;
15135 use some::mod2;
15136
15137 const A: u32 = 42;
15138 const B: u32 = 42;
15139 - const C: u32 = 42;
15140 + const C: u32 = 43
15141 + new_line
15142 + ˇ
15143 const D: u32 = 42;
15144
15145
15146 fn main() {
15147 println!("hello");
15148
15149 println!("world");
15150 }"#
15151 .unindent(),
15152 );
15153}
15154
15155#[gpui::test]
15156async fn test_stage_and_unstage_added_file_hunk(
15157 executor: BackgroundExecutor,
15158 cx: &mut TestAppContext,
15159) {
15160 init_test(cx, |_| {});
15161
15162 let mut cx = EditorTestContext::new(cx).await;
15163 cx.update_editor(|editor, _, cx| {
15164 editor.set_expand_all_diff_hunks(cx);
15165 });
15166
15167 let working_copy = r#"
15168 ˇfn main() {
15169 println!("hello, world!");
15170 }
15171 "#
15172 .unindent();
15173
15174 cx.set_state(&working_copy);
15175 executor.run_until_parked();
15176
15177 cx.assert_state_with_diff(
15178 r#"
15179 + ˇfn main() {
15180 + println!("hello, world!");
15181 + }
15182 "#
15183 .unindent(),
15184 );
15185 cx.assert_index_text(None);
15186
15187 cx.update_editor(|editor, window, cx| {
15188 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15189 });
15190 executor.run_until_parked();
15191 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15192 cx.assert_state_with_diff(
15193 r#"
15194 + ˇfn main() {
15195 + println!("hello, world!");
15196 + }
15197 "#
15198 .unindent(),
15199 );
15200
15201 cx.update_editor(|editor, window, cx| {
15202 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15203 });
15204 executor.run_until_parked();
15205 cx.assert_index_text(None);
15206}
15207
15208async fn setup_indent_guides_editor(
15209 text: &str,
15210 cx: &mut TestAppContext,
15211) -> (BufferId, EditorTestContext) {
15212 init_test(cx, |_| {});
15213
15214 let mut cx = EditorTestContext::new(cx).await;
15215
15216 let buffer_id = cx.update_editor(|editor, window, cx| {
15217 editor.set_text(text, window, cx);
15218 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15219
15220 buffer_ids[0]
15221 });
15222
15223 (buffer_id, cx)
15224}
15225
15226fn assert_indent_guides(
15227 range: Range<u32>,
15228 expected: Vec<IndentGuide>,
15229 active_indices: Option<Vec<usize>>,
15230 cx: &mut EditorTestContext,
15231) {
15232 let indent_guides = cx.update_editor(|editor, window, cx| {
15233 let snapshot = editor.snapshot(window, cx).display_snapshot;
15234 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15235 editor,
15236 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15237 true,
15238 &snapshot,
15239 cx,
15240 );
15241
15242 indent_guides.sort_by(|a, b| {
15243 a.depth.cmp(&b.depth).then(
15244 a.start_row
15245 .cmp(&b.start_row)
15246 .then(a.end_row.cmp(&b.end_row)),
15247 )
15248 });
15249 indent_guides
15250 });
15251
15252 if let Some(expected) = active_indices {
15253 let active_indices = cx.update_editor(|editor, window, cx| {
15254 let snapshot = editor.snapshot(window, cx).display_snapshot;
15255 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15256 });
15257
15258 assert_eq!(
15259 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15260 expected,
15261 "Active indent guide indices do not match"
15262 );
15263 }
15264
15265 assert_eq!(indent_guides, expected, "Indent guides do not match");
15266}
15267
15268fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15269 IndentGuide {
15270 buffer_id,
15271 start_row: MultiBufferRow(start_row),
15272 end_row: MultiBufferRow(end_row),
15273 depth,
15274 tab_size: 4,
15275 settings: IndentGuideSettings {
15276 enabled: true,
15277 line_width: 1,
15278 active_line_width: 1,
15279 ..Default::default()
15280 },
15281 }
15282}
15283
15284#[gpui::test]
15285async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15286 let (buffer_id, mut cx) = setup_indent_guides_editor(
15287 &"
15288 fn main() {
15289 let a = 1;
15290 }"
15291 .unindent(),
15292 cx,
15293 )
15294 .await;
15295
15296 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15297}
15298
15299#[gpui::test]
15300async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15301 let (buffer_id, mut cx) = setup_indent_guides_editor(
15302 &"
15303 fn main() {
15304 let a = 1;
15305 let b = 2;
15306 }"
15307 .unindent(),
15308 cx,
15309 )
15310 .await;
15311
15312 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15313}
15314
15315#[gpui::test]
15316async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15317 let (buffer_id, mut cx) = setup_indent_guides_editor(
15318 &"
15319 fn main() {
15320 let a = 1;
15321 if a == 3 {
15322 let b = 2;
15323 } else {
15324 let c = 3;
15325 }
15326 }"
15327 .unindent(),
15328 cx,
15329 )
15330 .await;
15331
15332 assert_indent_guides(
15333 0..8,
15334 vec![
15335 indent_guide(buffer_id, 1, 6, 0),
15336 indent_guide(buffer_id, 3, 3, 1),
15337 indent_guide(buffer_id, 5, 5, 1),
15338 ],
15339 None,
15340 &mut cx,
15341 );
15342}
15343
15344#[gpui::test]
15345async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15346 let (buffer_id, mut cx) = setup_indent_guides_editor(
15347 &"
15348 fn main() {
15349 let a = 1;
15350 let b = 2;
15351 let c = 3;
15352 }"
15353 .unindent(),
15354 cx,
15355 )
15356 .await;
15357
15358 assert_indent_guides(
15359 0..5,
15360 vec![
15361 indent_guide(buffer_id, 1, 3, 0),
15362 indent_guide(buffer_id, 2, 2, 1),
15363 ],
15364 None,
15365 &mut cx,
15366 );
15367}
15368
15369#[gpui::test]
15370async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15371 let (buffer_id, mut cx) = setup_indent_guides_editor(
15372 &"
15373 fn main() {
15374 let a = 1;
15375
15376 let c = 3;
15377 }"
15378 .unindent(),
15379 cx,
15380 )
15381 .await;
15382
15383 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15384}
15385
15386#[gpui::test]
15387async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15388 let (buffer_id, mut cx) = setup_indent_guides_editor(
15389 &"
15390 fn main() {
15391 let a = 1;
15392
15393 let c = 3;
15394
15395 if a == 3 {
15396 let b = 2;
15397 } else {
15398 let c = 3;
15399 }
15400 }"
15401 .unindent(),
15402 cx,
15403 )
15404 .await;
15405
15406 assert_indent_guides(
15407 0..11,
15408 vec![
15409 indent_guide(buffer_id, 1, 9, 0),
15410 indent_guide(buffer_id, 6, 6, 1),
15411 indent_guide(buffer_id, 8, 8, 1),
15412 ],
15413 None,
15414 &mut cx,
15415 );
15416}
15417
15418#[gpui::test]
15419async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15420 let (buffer_id, mut cx) = setup_indent_guides_editor(
15421 &"
15422 fn main() {
15423 let a = 1;
15424
15425 let c = 3;
15426
15427 if a == 3 {
15428 let b = 2;
15429 } else {
15430 let c = 3;
15431 }
15432 }"
15433 .unindent(),
15434 cx,
15435 )
15436 .await;
15437
15438 assert_indent_guides(
15439 1..11,
15440 vec![
15441 indent_guide(buffer_id, 1, 9, 0),
15442 indent_guide(buffer_id, 6, 6, 1),
15443 indent_guide(buffer_id, 8, 8, 1),
15444 ],
15445 None,
15446 &mut cx,
15447 );
15448}
15449
15450#[gpui::test]
15451async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15452 let (buffer_id, mut cx) = setup_indent_guides_editor(
15453 &"
15454 fn main() {
15455 let a = 1;
15456
15457 let c = 3;
15458
15459 if a == 3 {
15460 let b = 2;
15461 } else {
15462 let c = 3;
15463 }
15464 }"
15465 .unindent(),
15466 cx,
15467 )
15468 .await;
15469
15470 assert_indent_guides(
15471 1..10,
15472 vec![
15473 indent_guide(buffer_id, 1, 9, 0),
15474 indent_guide(buffer_id, 6, 6, 1),
15475 indent_guide(buffer_id, 8, 8, 1),
15476 ],
15477 None,
15478 &mut cx,
15479 );
15480}
15481
15482#[gpui::test]
15483async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15484 let (buffer_id, mut cx) = setup_indent_guides_editor(
15485 &"
15486 block1
15487 block2
15488 block3
15489 block4
15490 block2
15491 block1
15492 block1"
15493 .unindent(),
15494 cx,
15495 )
15496 .await;
15497
15498 assert_indent_guides(
15499 1..10,
15500 vec![
15501 indent_guide(buffer_id, 1, 4, 0),
15502 indent_guide(buffer_id, 2, 3, 1),
15503 indent_guide(buffer_id, 3, 3, 2),
15504 ],
15505 None,
15506 &mut cx,
15507 );
15508}
15509
15510#[gpui::test]
15511async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15512 let (buffer_id, mut cx) = setup_indent_guides_editor(
15513 &"
15514 block1
15515 block2
15516 block3
15517
15518 block1
15519 block1"
15520 .unindent(),
15521 cx,
15522 )
15523 .await;
15524
15525 assert_indent_guides(
15526 0..6,
15527 vec![
15528 indent_guide(buffer_id, 1, 2, 0),
15529 indent_guide(buffer_id, 2, 2, 1),
15530 ],
15531 None,
15532 &mut cx,
15533 );
15534}
15535
15536#[gpui::test]
15537async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15538 let (buffer_id, mut cx) = setup_indent_guides_editor(
15539 &"
15540 block1
15541
15542
15543
15544 block2
15545 "
15546 .unindent(),
15547 cx,
15548 )
15549 .await;
15550
15551 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15552}
15553
15554#[gpui::test]
15555async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15556 let (buffer_id, mut cx) = setup_indent_guides_editor(
15557 &"
15558 def a:
15559 \tb = 3
15560 \tif True:
15561 \t\tc = 4
15562 \t\td = 5
15563 \tprint(b)
15564 "
15565 .unindent(),
15566 cx,
15567 )
15568 .await;
15569
15570 assert_indent_guides(
15571 0..6,
15572 vec![
15573 indent_guide(buffer_id, 1, 6, 0),
15574 indent_guide(buffer_id, 3, 4, 1),
15575 ],
15576 None,
15577 &mut cx,
15578 );
15579}
15580
15581#[gpui::test]
15582async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15583 let (buffer_id, mut cx) = setup_indent_guides_editor(
15584 &"
15585 fn main() {
15586 let a = 1;
15587 }"
15588 .unindent(),
15589 cx,
15590 )
15591 .await;
15592
15593 cx.update_editor(|editor, window, cx| {
15594 editor.change_selections(None, window, cx, |s| {
15595 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15596 });
15597 });
15598
15599 assert_indent_guides(
15600 0..3,
15601 vec![indent_guide(buffer_id, 1, 1, 0)],
15602 Some(vec![0]),
15603 &mut cx,
15604 );
15605}
15606
15607#[gpui::test]
15608async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15609 let (buffer_id, mut cx) = setup_indent_guides_editor(
15610 &"
15611 fn main() {
15612 if 1 == 2 {
15613 let a = 1;
15614 }
15615 }"
15616 .unindent(),
15617 cx,
15618 )
15619 .await;
15620
15621 cx.update_editor(|editor, window, cx| {
15622 editor.change_selections(None, window, cx, |s| {
15623 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15624 });
15625 });
15626
15627 assert_indent_guides(
15628 0..4,
15629 vec![
15630 indent_guide(buffer_id, 1, 3, 0),
15631 indent_guide(buffer_id, 2, 2, 1),
15632 ],
15633 Some(vec![1]),
15634 &mut cx,
15635 );
15636
15637 cx.update_editor(|editor, window, cx| {
15638 editor.change_selections(None, window, cx, |s| {
15639 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15640 });
15641 });
15642
15643 assert_indent_guides(
15644 0..4,
15645 vec![
15646 indent_guide(buffer_id, 1, 3, 0),
15647 indent_guide(buffer_id, 2, 2, 1),
15648 ],
15649 Some(vec![1]),
15650 &mut cx,
15651 );
15652
15653 cx.update_editor(|editor, window, cx| {
15654 editor.change_selections(None, window, cx, |s| {
15655 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15656 });
15657 });
15658
15659 assert_indent_guides(
15660 0..4,
15661 vec![
15662 indent_guide(buffer_id, 1, 3, 0),
15663 indent_guide(buffer_id, 2, 2, 1),
15664 ],
15665 Some(vec![0]),
15666 &mut cx,
15667 );
15668}
15669
15670#[gpui::test]
15671async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15672 let (buffer_id, mut cx) = setup_indent_guides_editor(
15673 &"
15674 fn main() {
15675 let a = 1;
15676
15677 let b = 2;
15678 }"
15679 .unindent(),
15680 cx,
15681 )
15682 .await;
15683
15684 cx.update_editor(|editor, window, cx| {
15685 editor.change_selections(None, window, cx, |s| {
15686 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15687 });
15688 });
15689
15690 assert_indent_guides(
15691 0..5,
15692 vec![indent_guide(buffer_id, 1, 3, 0)],
15693 Some(vec![0]),
15694 &mut cx,
15695 );
15696}
15697
15698#[gpui::test]
15699async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15700 let (buffer_id, mut cx) = setup_indent_guides_editor(
15701 &"
15702 def m:
15703 a = 1
15704 pass"
15705 .unindent(),
15706 cx,
15707 )
15708 .await;
15709
15710 cx.update_editor(|editor, window, cx| {
15711 editor.change_selections(None, window, cx, |s| {
15712 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15713 });
15714 });
15715
15716 assert_indent_guides(
15717 0..3,
15718 vec![indent_guide(buffer_id, 1, 2, 0)],
15719 Some(vec![0]),
15720 &mut cx,
15721 );
15722}
15723
15724#[gpui::test]
15725async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15726 init_test(cx, |_| {});
15727 let mut cx = EditorTestContext::new(cx).await;
15728 let text = indoc! {
15729 "
15730 impl A {
15731 fn b() {
15732 0;
15733 3;
15734 5;
15735 6;
15736 7;
15737 }
15738 }
15739 "
15740 };
15741 let base_text = indoc! {
15742 "
15743 impl A {
15744 fn b() {
15745 0;
15746 1;
15747 2;
15748 3;
15749 4;
15750 }
15751 fn c() {
15752 5;
15753 6;
15754 7;
15755 }
15756 }
15757 "
15758 };
15759
15760 cx.update_editor(|editor, window, cx| {
15761 editor.set_text(text, window, cx);
15762
15763 editor.buffer().update(cx, |multibuffer, cx| {
15764 let buffer = multibuffer.as_singleton().unwrap();
15765 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15766
15767 multibuffer.set_all_diff_hunks_expanded(cx);
15768 multibuffer.add_diff(diff, cx);
15769
15770 buffer.read(cx).remote_id()
15771 })
15772 });
15773 cx.run_until_parked();
15774
15775 cx.assert_state_with_diff(
15776 indoc! { "
15777 impl A {
15778 fn b() {
15779 0;
15780 - 1;
15781 - 2;
15782 3;
15783 - 4;
15784 - }
15785 - fn c() {
15786 5;
15787 6;
15788 7;
15789 }
15790 }
15791 ˇ"
15792 }
15793 .to_string(),
15794 );
15795
15796 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15797 editor
15798 .snapshot(window, cx)
15799 .buffer_snapshot
15800 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15801 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15802 .collect::<Vec<_>>()
15803 });
15804 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15805 assert_eq!(
15806 actual_guides,
15807 vec![
15808 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15809 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15810 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15811 ]
15812 );
15813}
15814
15815#[gpui::test]
15816async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15817 init_test(cx, |_| {});
15818 let mut cx = EditorTestContext::new(cx).await;
15819
15820 let diff_base = r#"
15821 a
15822 b
15823 c
15824 "#
15825 .unindent();
15826
15827 cx.set_state(
15828 &r#"
15829 ˇA
15830 b
15831 C
15832 "#
15833 .unindent(),
15834 );
15835 cx.set_head_text(&diff_base);
15836 cx.update_editor(|editor, window, cx| {
15837 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15838 });
15839 executor.run_until_parked();
15840
15841 let both_hunks_expanded = r#"
15842 - a
15843 + ˇA
15844 b
15845 - c
15846 + C
15847 "#
15848 .unindent();
15849
15850 cx.assert_state_with_diff(both_hunks_expanded.clone());
15851
15852 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15853 let snapshot = editor.snapshot(window, cx);
15854 let hunks = editor
15855 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15856 .collect::<Vec<_>>();
15857 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15858 let buffer_id = hunks[0].buffer_id;
15859 hunks
15860 .into_iter()
15861 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15862 .collect::<Vec<_>>()
15863 });
15864 assert_eq!(hunk_ranges.len(), 2);
15865
15866 cx.update_editor(|editor, _, cx| {
15867 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15868 });
15869 executor.run_until_parked();
15870
15871 let second_hunk_expanded = r#"
15872 ˇA
15873 b
15874 - c
15875 + C
15876 "#
15877 .unindent();
15878
15879 cx.assert_state_with_diff(second_hunk_expanded);
15880
15881 cx.update_editor(|editor, _, cx| {
15882 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
15883 });
15884 executor.run_until_parked();
15885
15886 cx.assert_state_with_diff(both_hunks_expanded.clone());
15887
15888 cx.update_editor(|editor, _, cx| {
15889 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15890 });
15891 executor.run_until_parked();
15892
15893 let first_hunk_expanded = r#"
15894 - a
15895 + ˇA
15896 b
15897 C
15898 "#
15899 .unindent();
15900
15901 cx.assert_state_with_diff(first_hunk_expanded);
15902
15903 cx.update_editor(|editor, _, cx| {
15904 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15905 });
15906 executor.run_until_parked();
15907
15908 cx.assert_state_with_diff(both_hunks_expanded);
15909
15910 cx.set_state(
15911 &r#"
15912 ˇA
15913 b
15914 "#
15915 .unindent(),
15916 );
15917 cx.run_until_parked();
15918
15919 // TODO this cursor position seems bad
15920 cx.assert_state_with_diff(
15921 r#"
15922 - ˇa
15923 + A
15924 b
15925 "#
15926 .unindent(),
15927 );
15928
15929 cx.update_editor(|editor, window, cx| {
15930 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15931 });
15932
15933 cx.assert_state_with_diff(
15934 r#"
15935 - ˇa
15936 + A
15937 b
15938 - c
15939 "#
15940 .unindent(),
15941 );
15942
15943 let hunk_ranges = cx.update_editor(|editor, window, cx| {
15944 let snapshot = editor.snapshot(window, cx);
15945 let hunks = editor
15946 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
15947 .collect::<Vec<_>>();
15948 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
15949 let buffer_id = hunks[0].buffer_id;
15950 hunks
15951 .into_iter()
15952 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
15953 .collect::<Vec<_>>()
15954 });
15955 assert_eq!(hunk_ranges.len(), 2);
15956
15957 cx.update_editor(|editor, _, cx| {
15958 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
15959 });
15960 executor.run_until_parked();
15961
15962 cx.assert_state_with_diff(
15963 r#"
15964 - ˇa
15965 + A
15966 b
15967 "#
15968 .unindent(),
15969 );
15970}
15971
15972#[gpui::test]
15973async fn test_toggle_deletion_hunk_at_start_of_file(
15974 executor: BackgroundExecutor,
15975 cx: &mut TestAppContext,
15976) {
15977 init_test(cx, |_| {});
15978 let mut cx = EditorTestContext::new(cx).await;
15979
15980 let diff_base = r#"
15981 a
15982 b
15983 c
15984 "#
15985 .unindent();
15986
15987 cx.set_state(
15988 &r#"
15989 ˇb
15990 c
15991 "#
15992 .unindent(),
15993 );
15994 cx.set_head_text(&diff_base);
15995 cx.update_editor(|editor, window, cx| {
15996 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15997 });
15998 executor.run_until_parked();
15999
16000 let hunk_expanded = r#"
16001 - a
16002 ˇb
16003 c
16004 "#
16005 .unindent();
16006
16007 cx.assert_state_with_diff(hunk_expanded.clone());
16008
16009 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16010 let snapshot = editor.snapshot(window, cx);
16011 let hunks = editor
16012 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16013 .collect::<Vec<_>>();
16014 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16015 let buffer_id = hunks[0].buffer_id;
16016 hunks
16017 .into_iter()
16018 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16019 .collect::<Vec<_>>()
16020 });
16021 assert_eq!(hunk_ranges.len(), 1);
16022
16023 cx.update_editor(|editor, _, cx| {
16024 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16025 });
16026 executor.run_until_parked();
16027
16028 let hunk_collapsed = r#"
16029 ˇb
16030 c
16031 "#
16032 .unindent();
16033
16034 cx.assert_state_with_diff(hunk_collapsed);
16035
16036 cx.update_editor(|editor, _, cx| {
16037 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16038 });
16039 executor.run_until_parked();
16040
16041 cx.assert_state_with_diff(hunk_expanded.clone());
16042}
16043
16044#[gpui::test]
16045async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16046 init_test(cx, |_| {});
16047
16048 let fs = FakeFs::new(cx.executor());
16049 fs.insert_tree(
16050 path!("/test"),
16051 json!({
16052 ".git": {},
16053 "file-1": "ONE\n",
16054 "file-2": "TWO\n",
16055 "file-3": "THREE\n",
16056 }),
16057 )
16058 .await;
16059
16060 fs.set_head_for_repo(
16061 path!("/test/.git").as_ref(),
16062 &[
16063 ("file-1".into(), "one\n".into()),
16064 ("file-2".into(), "two\n".into()),
16065 ("file-3".into(), "three\n".into()),
16066 ],
16067 );
16068
16069 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16070 let mut buffers = vec![];
16071 for i in 1..=3 {
16072 let buffer = project
16073 .update(cx, |project, cx| {
16074 let path = format!(path!("/test/file-{}"), i);
16075 project.open_local_buffer(path, cx)
16076 })
16077 .await
16078 .unwrap();
16079 buffers.push(buffer);
16080 }
16081
16082 let multibuffer = cx.new(|cx| {
16083 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16084 multibuffer.set_all_diff_hunks_expanded(cx);
16085 for buffer in &buffers {
16086 let snapshot = buffer.read(cx).snapshot();
16087 multibuffer.set_excerpts_for_path(
16088 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16089 buffer.clone(),
16090 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16091 DEFAULT_MULTIBUFFER_CONTEXT,
16092 cx,
16093 );
16094 }
16095 multibuffer
16096 });
16097
16098 let editor = cx.add_window(|window, cx| {
16099 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16100 });
16101 cx.run_until_parked();
16102
16103 let snapshot = editor
16104 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16105 .unwrap();
16106 let hunks = snapshot
16107 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16108 .map(|hunk| match hunk {
16109 DisplayDiffHunk::Unfolded {
16110 display_row_range, ..
16111 } => display_row_range,
16112 DisplayDiffHunk::Folded { .. } => unreachable!(),
16113 })
16114 .collect::<Vec<_>>();
16115 assert_eq!(
16116 hunks,
16117 [
16118 DisplayRow(2)..DisplayRow(4),
16119 DisplayRow(7)..DisplayRow(9),
16120 DisplayRow(12)..DisplayRow(14),
16121 ]
16122 );
16123}
16124
16125#[gpui::test]
16126async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16127 init_test(cx, |_| {});
16128
16129 let mut cx = EditorTestContext::new(cx).await;
16130 cx.set_head_text(indoc! { "
16131 one
16132 two
16133 three
16134 four
16135 five
16136 "
16137 });
16138 cx.set_index_text(indoc! { "
16139 one
16140 two
16141 three
16142 four
16143 five
16144 "
16145 });
16146 cx.set_state(indoc! {"
16147 one
16148 TWO
16149 ˇTHREE
16150 FOUR
16151 five
16152 "});
16153 cx.run_until_parked();
16154 cx.update_editor(|editor, window, cx| {
16155 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16156 });
16157 cx.run_until_parked();
16158 cx.assert_index_text(Some(indoc! {"
16159 one
16160 TWO
16161 THREE
16162 FOUR
16163 five
16164 "}));
16165 cx.set_state(indoc! { "
16166 one
16167 TWO
16168 ˇTHREE-HUNDRED
16169 FOUR
16170 five
16171 "});
16172 cx.run_until_parked();
16173 cx.update_editor(|editor, window, cx| {
16174 let snapshot = editor.snapshot(window, cx);
16175 let hunks = editor
16176 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16177 .collect::<Vec<_>>();
16178 assert_eq!(hunks.len(), 1);
16179 assert_eq!(
16180 hunks[0].status(),
16181 DiffHunkStatus {
16182 kind: DiffHunkStatusKind::Modified,
16183 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16184 }
16185 );
16186
16187 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16188 });
16189 cx.run_until_parked();
16190 cx.assert_index_text(Some(indoc! {"
16191 one
16192 TWO
16193 THREE-HUNDRED
16194 FOUR
16195 five
16196 "}));
16197}
16198
16199#[gpui::test]
16200fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16201 init_test(cx, |_| {});
16202
16203 let editor = cx.add_window(|window, cx| {
16204 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16205 build_editor(buffer, window, cx)
16206 });
16207
16208 let render_args = Arc::new(Mutex::new(None));
16209 let snapshot = editor
16210 .update(cx, |editor, window, cx| {
16211 let snapshot = editor.buffer().read(cx).snapshot(cx);
16212 let range =
16213 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16214
16215 struct RenderArgs {
16216 row: MultiBufferRow,
16217 folded: bool,
16218 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16219 }
16220
16221 let crease = Crease::inline(
16222 range,
16223 FoldPlaceholder::test(),
16224 {
16225 let toggle_callback = render_args.clone();
16226 move |row, folded, callback, _window, _cx| {
16227 *toggle_callback.lock() = Some(RenderArgs {
16228 row,
16229 folded,
16230 callback,
16231 });
16232 div()
16233 }
16234 },
16235 |_row, _folded, _window, _cx| div(),
16236 );
16237
16238 editor.insert_creases(Some(crease), cx);
16239 let snapshot = editor.snapshot(window, cx);
16240 let _div = snapshot.render_crease_toggle(
16241 MultiBufferRow(1),
16242 false,
16243 cx.entity().clone(),
16244 window,
16245 cx,
16246 );
16247 snapshot
16248 })
16249 .unwrap();
16250
16251 let render_args = render_args.lock().take().unwrap();
16252 assert_eq!(render_args.row, MultiBufferRow(1));
16253 assert!(!render_args.folded);
16254 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16255
16256 cx.update_window(*editor, |_, window, cx| {
16257 (render_args.callback)(true, window, cx)
16258 })
16259 .unwrap();
16260 let snapshot = editor
16261 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16262 .unwrap();
16263 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16264
16265 cx.update_window(*editor, |_, window, cx| {
16266 (render_args.callback)(false, window, cx)
16267 })
16268 .unwrap();
16269 let snapshot = editor
16270 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16271 .unwrap();
16272 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16273}
16274
16275#[gpui::test]
16276async fn test_input_text(cx: &mut TestAppContext) {
16277 init_test(cx, |_| {});
16278 let mut cx = EditorTestContext::new(cx).await;
16279
16280 cx.set_state(
16281 &r#"ˇone
16282 two
16283
16284 three
16285 fourˇ
16286 five
16287
16288 siˇx"#
16289 .unindent(),
16290 );
16291
16292 cx.dispatch_action(HandleInput(String::new()));
16293 cx.assert_editor_state(
16294 &r#"ˇone
16295 two
16296
16297 three
16298 fourˇ
16299 five
16300
16301 siˇx"#
16302 .unindent(),
16303 );
16304
16305 cx.dispatch_action(HandleInput("AAAA".to_string()));
16306 cx.assert_editor_state(
16307 &r#"AAAAˇone
16308 two
16309
16310 three
16311 fourAAAAˇ
16312 five
16313
16314 siAAAAˇx"#
16315 .unindent(),
16316 );
16317}
16318
16319#[gpui::test]
16320async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16321 init_test(cx, |_| {});
16322
16323 let mut cx = EditorTestContext::new(cx).await;
16324 cx.set_state(
16325 r#"let foo = 1;
16326let foo = 2;
16327let foo = 3;
16328let fooˇ = 4;
16329let foo = 5;
16330let foo = 6;
16331let foo = 7;
16332let foo = 8;
16333let foo = 9;
16334let foo = 10;
16335let foo = 11;
16336let foo = 12;
16337let foo = 13;
16338let foo = 14;
16339let foo = 15;"#,
16340 );
16341
16342 cx.update_editor(|e, window, cx| {
16343 assert_eq!(
16344 e.next_scroll_position,
16345 NextScrollCursorCenterTopBottom::Center,
16346 "Default next scroll direction is center",
16347 );
16348
16349 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16350 assert_eq!(
16351 e.next_scroll_position,
16352 NextScrollCursorCenterTopBottom::Top,
16353 "After center, next scroll direction should be top",
16354 );
16355
16356 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16357 assert_eq!(
16358 e.next_scroll_position,
16359 NextScrollCursorCenterTopBottom::Bottom,
16360 "After top, next scroll direction should be bottom",
16361 );
16362
16363 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16364 assert_eq!(
16365 e.next_scroll_position,
16366 NextScrollCursorCenterTopBottom::Center,
16367 "After bottom, scrolling should start over",
16368 );
16369
16370 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16371 assert_eq!(
16372 e.next_scroll_position,
16373 NextScrollCursorCenterTopBottom::Top,
16374 "Scrolling continues if retriggered fast enough"
16375 );
16376 });
16377
16378 cx.executor()
16379 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16380 cx.executor().run_until_parked();
16381 cx.update_editor(|e, _, _| {
16382 assert_eq!(
16383 e.next_scroll_position,
16384 NextScrollCursorCenterTopBottom::Center,
16385 "If scrolling is not triggered fast enough, it should reset"
16386 );
16387 });
16388}
16389
16390#[gpui::test]
16391async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16392 init_test(cx, |_| {});
16393 let mut cx = EditorLspTestContext::new_rust(
16394 lsp::ServerCapabilities {
16395 definition_provider: Some(lsp::OneOf::Left(true)),
16396 references_provider: Some(lsp::OneOf::Left(true)),
16397 ..lsp::ServerCapabilities::default()
16398 },
16399 cx,
16400 )
16401 .await;
16402
16403 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16404 let go_to_definition = cx
16405 .lsp
16406 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16407 move |params, _| async move {
16408 if empty_go_to_definition {
16409 Ok(None)
16410 } else {
16411 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16412 uri: params.text_document_position_params.text_document.uri,
16413 range: lsp::Range::new(
16414 lsp::Position::new(4, 3),
16415 lsp::Position::new(4, 6),
16416 ),
16417 })))
16418 }
16419 },
16420 );
16421 let references = cx
16422 .lsp
16423 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16424 Ok(Some(vec![lsp::Location {
16425 uri: params.text_document_position.text_document.uri,
16426 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16427 }]))
16428 });
16429 (go_to_definition, references)
16430 };
16431
16432 cx.set_state(
16433 &r#"fn one() {
16434 let mut a = ˇtwo();
16435 }
16436
16437 fn two() {}"#
16438 .unindent(),
16439 );
16440 set_up_lsp_handlers(false, &mut cx);
16441 let navigated = cx
16442 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16443 .await
16444 .expect("Failed to navigate to definition");
16445 assert_eq!(
16446 navigated,
16447 Navigated::Yes,
16448 "Should have navigated to definition from the GetDefinition response"
16449 );
16450 cx.assert_editor_state(
16451 &r#"fn one() {
16452 let mut a = two();
16453 }
16454
16455 fn «twoˇ»() {}"#
16456 .unindent(),
16457 );
16458
16459 let editors = cx.update_workspace(|workspace, _, cx| {
16460 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16461 });
16462 cx.update_editor(|_, _, test_editor_cx| {
16463 assert_eq!(
16464 editors.len(),
16465 1,
16466 "Initially, only one, test, editor should be open in the workspace"
16467 );
16468 assert_eq!(
16469 test_editor_cx.entity(),
16470 editors.last().expect("Asserted len is 1").clone()
16471 );
16472 });
16473
16474 set_up_lsp_handlers(true, &mut cx);
16475 let navigated = cx
16476 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16477 .await
16478 .expect("Failed to navigate to lookup references");
16479 assert_eq!(
16480 navigated,
16481 Navigated::Yes,
16482 "Should have navigated to references as a fallback after empty GoToDefinition response"
16483 );
16484 // We should not change the selections in the existing file,
16485 // if opening another milti buffer with the references
16486 cx.assert_editor_state(
16487 &r#"fn one() {
16488 let mut a = two();
16489 }
16490
16491 fn «twoˇ»() {}"#
16492 .unindent(),
16493 );
16494 let editors = cx.update_workspace(|workspace, _, cx| {
16495 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16496 });
16497 cx.update_editor(|_, _, test_editor_cx| {
16498 assert_eq!(
16499 editors.len(),
16500 2,
16501 "After falling back to references search, we open a new editor with the results"
16502 );
16503 let references_fallback_text = editors
16504 .into_iter()
16505 .find(|new_editor| *new_editor != test_editor_cx.entity())
16506 .expect("Should have one non-test editor now")
16507 .read(test_editor_cx)
16508 .text(test_editor_cx);
16509 assert_eq!(
16510 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16511 "Should use the range from the references response and not the GoToDefinition one"
16512 );
16513 });
16514}
16515
16516#[gpui::test]
16517async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16518 init_test(cx, |_| {});
16519 cx.update(|cx| {
16520 let mut editor_settings = EditorSettings::get_global(cx).clone();
16521 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16522 EditorSettings::override_global(editor_settings, cx);
16523 });
16524 let mut cx = EditorLspTestContext::new_rust(
16525 lsp::ServerCapabilities {
16526 definition_provider: Some(lsp::OneOf::Left(true)),
16527 references_provider: Some(lsp::OneOf::Left(true)),
16528 ..lsp::ServerCapabilities::default()
16529 },
16530 cx,
16531 )
16532 .await;
16533 let original_state = r#"fn one() {
16534 let mut a = ˇtwo();
16535 }
16536
16537 fn two() {}"#
16538 .unindent();
16539 cx.set_state(&original_state);
16540
16541 let mut go_to_definition = cx
16542 .lsp
16543 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16544 move |_, _| async move { Ok(None) },
16545 );
16546 let _references = cx
16547 .lsp
16548 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16549 panic!("Should not call for references with no go to definition fallback")
16550 });
16551
16552 let navigated = cx
16553 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16554 .await
16555 .expect("Failed to navigate to lookup references");
16556 go_to_definition
16557 .next()
16558 .await
16559 .expect("Should have called the go_to_definition handler");
16560
16561 assert_eq!(
16562 navigated,
16563 Navigated::No,
16564 "Should have navigated to references as a fallback after empty GoToDefinition response"
16565 );
16566 cx.assert_editor_state(&original_state);
16567 let editors = cx.update_workspace(|workspace, _, cx| {
16568 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16569 });
16570 cx.update_editor(|_, _, _| {
16571 assert_eq!(
16572 editors.len(),
16573 1,
16574 "After unsuccessful fallback, no other editor should have been opened"
16575 );
16576 });
16577}
16578
16579#[gpui::test]
16580async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16581 init_test(cx, |_| {});
16582
16583 let language = Arc::new(Language::new(
16584 LanguageConfig::default(),
16585 Some(tree_sitter_rust::LANGUAGE.into()),
16586 ));
16587
16588 let text = r#"
16589 #[cfg(test)]
16590 mod tests() {
16591 #[test]
16592 fn runnable_1() {
16593 let a = 1;
16594 }
16595
16596 #[test]
16597 fn runnable_2() {
16598 let a = 1;
16599 let b = 2;
16600 }
16601 }
16602 "#
16603 .unindent();
16604
16605 let fs = FakeFs::new(cx.executor());
16606 fs.insert_file("/file.rs", Default::default()).await;
16607
16608 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16609 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16610 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16611 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16612 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16613
16614 let editor = cx.new_window_entity(|window, cx| {
16615 Editor::new(
16616 EditorMode::Full,
16617 multi_buffer,
16618 Some(project.clone()),
16619 window,
16620 cx,
16621 )
16622 });
16623
16624 editor.update_in(cx, |editor, window, cx| {
16625 let snapshot = editor.buffer().read(cx).snapshot(cx);
16626 editor.tasks.insert(
16627 (buffer.read(cx).remote_id(), 3),
16628 RunnableTasks {
16629 templates: vec![],
16630 offset: snapshot.anchor_before(43),
16631 column: 0,
16632 extra_variables: HashMap::default(),
16633 context_range: BufferOffset(43)..BufferOffset(85),
16634 },
16635 );
16636 editor.tasks.insert(
16637 (buffer.read(cx).remote_id(), 8),
16638 RunnableTasks {
16639 templates: vec![],
16640 offset: snapshot.anchor_before(86),
16641 column: 0,
16642 extra_variables: HashMap::default(),
16643 context_range: BufferOffset(86)..BufferOffset(191),
16644 },
16645 );
16646
16647 // Test finding task when cursor is inside function body
16648 editor.change_selections(None, window, cx, |s| {
16649 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16650 });
16651 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16652 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16653
16654 // Test finding task when cursor is on function name
16655 editor.change_selections(None, window, cx, |s| {
16656 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16657 });
16658 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16659 assert_eq!(row, 8, "Should find task when cursor is on function name");
16660 });
16661}
16662
16663#[gpui::test]
16664async fn test_folding_buffers(cx: &mut TestAppContext) {
16665 init_test(cx, |_| {});
16666
16667 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16668 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16669 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16670
16671 let fs = FakeFs::new(cx.executor());
16672 fs.insert_tree(
16673 path!("/a"),
16674 json!({
16675 "first.rs": sample_text_1,
16676 "second.rs": sample_text_2,
16677 "third.rs": sample_text_3,
16678 }),
16679 )
16680 .await;
16681 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16682 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16683 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16684 let worktree = project.update(cx, |project, cx| {
16685 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16686 assert_eq!(worktrees.len(), 1);
16687 worktrees.pop().unwrap()
16688 });
16689 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16690
16691 let buffer_1 = project
16692 .update(cx, |project, cx| {
16693 project.open_buffer((worktree_id, "first.rs"), cx)
16694 })
16695 .await
16696 .unwrap();
16697 let buffer_2 = project
16698 .update(cx, |project, cx| {
16699 project.open_buffer((worktree_id, "second.rs"), cx)
16700 })
16701 .await
16702 .unwrap();
16703 let buffer_3 = project
16704 .update(cx, |project, cx| {
16705 project.open_buffer((worktree_id, "third.rs"), cx)
16706 })
16707 .await
16708 .unwrap();
16709
16710 let multi_buffer = cx.new(|cx| {
16711 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16712 multi_buffer.push_excerpts(
16713 buffer_1.clone(),
16714 [
16715 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16716 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16717 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16718 ],
16719 cx,
16720 );
16721 multi_buffer.push_excerpts(
16722 buffer_2.clone(),
16723 [
16724 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16725 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16726 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16727 ],
16728 cx,
16729 );
16730 multi_buffer.push_excerpts(
16731 buffer_3.clone(),
16732 [
16733 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16734 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16735 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16736 ],
16737 cx,
16738 );
16739 multi_buffer
16740 });
16741 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16742 Editor::new(
16743 EditorMode::Full,
16744 multi_buffer.clone(),
16745 Some(project.clone()),
16746 window,
16747 cx,
16748 )
16749 });
16750
16751 assert_eq!(
16752 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16753 "\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",
16754 );
16755
16756 multi_buffer_editor.update(cx, |editor, cx| {
16757 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16758 });
16759 assert_eq!(
16760 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16761 "\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",
16762 "After folding the first buffer, its text should not be displayed"
16763 );
16764
16765 multi_buffer_editor.update(cx, |editor, cx| {
16766 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16767 });
16768 assert_eq!(
16769 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16770 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16771 "After folding the second buffer, its text should not be displayed"
16772 );
16773
16774 multi_buffer_editor.update(cx, |editor, cx| {
16775 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16776 });
16777 assert_eq!(
16778 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16779 "\n\n\n\n\n",
16780 "After folding the third buffer, its text should not be displayed"
16781 );
16782
16783 // Emulate selection inside the fold logic, that should work
16784 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16785 editor
16786 .snapshot(window, cx)
16787 .next_line_boundary(Point::new(0, 4));
16788 });
16789
16790 multi_buffer_editor.update(cx, |editor, cx| {
16791 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16792 });
16793 assert_eq!(
16794 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16795 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16796 "After unfolding the second buffer, its text should be displayed"
16797 );
16798
16799 // Typing inside of buffer 1 causes that buffer to be unfolded.
16800 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16801 assert_eq!(
16802 multi_buffer
16803 .read(cx)
16804 .snapshot(cx)
16805 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16806 .collect::<String>(),
16807 "bbbb"
16808 );
16809 editor.change_selections(None, window, cx, |selections| {
16810 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16811 });
16812 editor.handle_input("B", window, cx);
16813 });
16814
16815 assert_eq!(
16816 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16817 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16818 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16819 );
16820
16821 multi_buffer_editor.update(cx, |editor, cx| {
16822 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16823 });
16824 assert_eq!(
16825 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16826 "\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",
16827 "After unfolding the all buffers, all original text should be displayed"
16828 );
16829}
16830
16831#[gpui::test]
16832async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16833 init_test(cx, |_| {});
16834
16835 let sample_text_1 = "1111\n2222\n3333".to_string();
16836 let sample_text_2 = "4444\n5555\n6666".to_string();
16837 let sample_text_3 = "7777\n8888\n9999".to_string();
16838
16839 let fs = FakeFs::new(cx.executor());
16840 fs.insert_tree(
16841 path!("/a"),
16842 json!({
16843 "first.rs": sample_text_1,
16844 "second.rs": sample_text_2,
16845 "third.rs": sample_text_3,
16846 }),
16847 )
16848 .await;
16849 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16850 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16851 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16852 let worktree = project.update(cx, |project, cx| {
16853 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16854 assert_eq!(worktrees.len(), 1);
16855 worktrees.pop().unwrap()
16856 });
16857 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16858
16859 let buffer_1 = project
16860 .update(cx, |project, cx| {
16861 project.open_buffer((worktree_id, "first.rs"), cx)
16862 })
16863 .await
16864 .unwrap();
16865 let buffer_2 = project
16866 .update(cx, |project, cx| {
16867 project.open_buffer((worktree_id, "second.rs"), cx)
16868 })
16869 .await
16870 .unwrap();
16871 let buffer_3 = project
16872 .update(cx, |project, cx| {
16873 project.open_buffer((worktree_id, "third.rs"), cx)
16874 })
16875 .await
16876 .unwrap();
16877
16878 let multi_buffer = cx.new(|cx| {
16879 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16880 multi_buffer.push_excerpts(
16881 buffer_1.clone(),
16882 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16883 cx,
16884 );
16885 multi_buffer.push_excerpts(
16886 buffer_2.clone(),
16887 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16888 cx,
16889 );
16890 multi_buffer.push_excerpts(
16891 buffer_3.clone(),
16892 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
16893 cx,
16894 );
16895 multi_buffer
16896 });
16897
16898 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16899 Editor::new(
16900 EditorMode::Full,
16901 multi_buffer,
16902 Some(project.clone()),
16903 window,
16904 cx,
16905 )
16906 });
16907
16908 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
16909 assert_eq!(
16910 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16911 full_text,
16912 );
16913
16914 multi_buffer_editor.update(cx, |editor, cx| {
16915 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16916 });
16917 assert_eq!(
16918 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16919 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
16920 "After folding the first buffer, its text should not be displayed"
16921 );
16922
16923 multi_buffer_editor.update(cx, |editor, cx| {
16924 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16925 });
16926
16927 assert_eq!(
16928 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16929 "\n\n\n\n\n\n7777\n8888\n9999",
16930 "After folding the second buffer, its text should not be displayed"
16931 );
16932
16933 multi_buffer_editor.update(cx, |editor, cx| {
16934 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16935 });
16936 assert_eq!(
16937 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16938 "\n\n\n\n\n",
16939 "After folding the third buffer, its text should not be displayed"
16940 );
16941
16942 multi_buffer_editor.update(cx, |editor, cx| {
16943 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16944 });
16945 assert_eq!(
16946 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16947 "\n\n\n\n4444\n5555\n6666\n\n",
16948 "After unfolding the second buffer, its text should be displayed"
16949 );
16950
16951 multi_buffer_editor.update(cx, |editor, cx| {
16952 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
16953 });
16954 assert_eq!(
16955 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16956 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
16957 "After unfolding the first buffer, its text should be displayed"
16958 );
16959
16960 multi_buffer_editor.update(cx, |editor, cx| {
16961 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16962 });
16963 assert_eq!(
16964 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16965 full_text,
16966 "After unfolding all buffers, all original text should be displayed"
16967 );
16968}
16969
16970#[gpui::test]
16971async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
16972 init_test(cx, |_| {});
16973
16974 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16975
16976 let fs = FakeFs::new(cx.executor());
16977 fs.insert_tree(
16978 path!("/a"),
16979 json!({
16980 "main.rs": sample_text,
16981 }),
16982 )
16983 .await;
16984 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16985 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16986 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16987 let worktree = project.update(cx, |project, cx| {
16988 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16989 assert_eq!(worktrees.len(), 1);
16990 worktrees.pop().unwrap()
16991 });
16992 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16993
16994 let buffer_1 = project
16995 .update(cx, |project, cx| {
16996 project.open_buffer((worktree_id, "main.rs"), cx)
16997 })
16998 .await
16999 .unwrap();
17000
17001 let multi_buffer = cx.new(|cx| {
17002 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17003 multi_buffer.push_excerpts(
17004 buffer_1.clone(),
17005 [ExcerptRange::new(
17006 Point::new(0, 0)
17007 ..Point::new(
17008 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17009 0,
17010 ),
17011 )],
17012 cx,
17013 );
17014 multi_buffer
17015 });
17016 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17017 Editor::new(
17018 EditorMode::Full,
17019 multi_buffer,
17020 Some(project.clone()),
17021 window,
17022 cx,
17023 )
17024 });
17025
17026 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17027 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17028 enum TestHighlight {}
17029 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17030 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17031 editor.highlight_text::<TestHighlight>(
17032 vec![highlight_range.clone()],
17033 HighlightStyle::color(Hsla::green()),
17034 cx,
17035 );
17036 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17037 });
17038
17039 let full_text = format!("\n\n{sample_text}");
17040 assert_eq!(
17041 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17042 full_text,
17043 );
17044}
17045
17046#[gpui::test]
17047async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17048 init_test(cx, |_| {});
17049 cx.update(|cx| {
17050 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17051 "keymaps/default-linux.json",
17052 cx,
17053 )
17054 .unwrap();
17055 cx.bind_keys(default_key_bindings);
17056 });
17057
17058 let (editor, cx) = cx.add_window_view(|window, cx| {
17059 let multi_buffer = MultiBuffer::build_multi(
17060 [
17061 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17062 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17063 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17064 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17065 ],
17066 cx,
17067 );
17068 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17069
17070 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17071 // fold all but the second buffer, so that we test navigating between two
17072 // adjacent folded buffers, as well as folded buffers at the start and
17073 // end the multibuffer
17074 editor.fold_buffer(buffer_ids[0], cx);
17075 editor.fold_buffer(buffer_ids[2], cx);
17076 editor.fold_buffer(buffer_ids[3], cx);
17077
17078 editor
17079 });
17080 cx.simulate_resize(size(px(1000.), px(1000.)));
17081
17082 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17083 cx.assert_excerpts_with_selections(indoc! {"
17084 [EXCERPT]
17085 ˇ[FOLDED]
17086 [EXCERPT]
17087 a1
17088 b1
17089 [EXCERPT]
17090 [FOLDED]
17091 [EXCERPT]
17092 [FOLDED]
17093 "
17094 });
17095 cx.simulate_keystroke("down");
17096 cx.assert_excerpts_with_selections(indoc! {"
17097 [EXCERPT]
17098 [FOLDED]
17099 [EXCERPT]
17100 ˇa1
17101 b1
17102 [EXCERPT]
17103 [FOLDED]
17104 [EXCERPT]
17105 [FOLDED]
17106 "
17107 });
17108 cx.simulate_keystroke("down");
17109 cx.assert_excerpts_with_selections(indoc! {"
17110 [EXCERPT]
17111 [FOLDED]
17112 [EXCERPT]
17113 a1
17114 ˇb1
17115 [EXCERPT]
17116 [FOLDED]
17117 [EXCERPT]
17118 [FOLDED]
17119 "
17120 });
17121 cx.simulate_keystroke("down");
17122 cx.assert_excerpts_with_selections(indoc! {"
17123 [EXCERPT]
17124 [FOLDED]
17125 [EXCERPT]
17126 a1
17127 b1
17128 ˇ[EXCERPT]
17129 [FOLDED]
17130 [EXCERPT]
17131 [FOLDED]
17132 "
17133 });
17134 cx.simulate_keystroke("down");
17135 cx.assert_excerpts_with_selections(indoc! {"
17136 [EXCERPT]
17137 [FOLDED]
17138 [EXCERPT]
17139 a1
17140 b1
17141 [EXCERPT]
17142 ˇ[FOLDED]
17143 [EXCERPT]
17144 [FOLDED]
17145 "
17146 });
17147 for _ in 0..5 {
17148 cx.simulate_keystroke("down");
17149 cx.assert_excerpts_with_selections(indoc! {"
17150 [EXCERPT]
17151 [FOLDED]
17152 [EXCERPT]
17153 a1
17154 b1
17155 [EXCERPT]
17156 [FOLDED]
17157 [EXCERPT]
17158 ˇ[FOLDED]
17159 "
17160 });
17161 }
17162
17163 cx.simulate_keystroke("up");
17164 cx.assert_excerpts_with_selections(indoc! {"
17165 [EXCERPT]
17166 [FOLDED]
17167 [EXCERPT]
17168 a1
17169 b1
17170 [EXCERPT]
17171 ˇ[FOLDED]
17172 [EXCERPT]
17173 [FOLDED]
17174 "
17175 });
17176 cx.simulate_keystroke("up");
17177 cx.assert_excerpts_with_selections(indoc! {"
17178 [EXCERPT]
17179 [FOLDED]
17180 [EXCERPT]
17181 a1
17182 b1
17183 ˇ[EXCERPT]
17184 [FOLDED]
17185 [EXCERPT]
17186 [FOLDED]
17187 "
17188 });
17189 cx.simulate_keystroke("up");
17190 cx.assert_excerpts_with_selections(indoc! {"
17191 [EXCERPT]
17192 [FOLDED]
17193 [EXCERPT]
17194 a1
17195 ˇb1
17196 [EXCERPT]
17197 [FOLDED]
17198 [EXCERPT]
17199 [FOLDED]
17200 "
17201 });
17202 cx.simulate_keystroke("up");
17203 cx.assert_excerpts_with_selections(indoc! {"
17204 [EXCERPT]
17205 [FOLDED]
17206 [EXCERPT]
17207 ˇa1
17208 b1
17209 [EXCERPT]
17210 [FOLDED]
17211 [EXCERPT]
17212 [FOLDED]
17213 "
17214 });
17215 for _ in 0..5 {
17216 cx.simulate_keystroke("up");
17217 cx.assert_excerpts_with_selections(indoc! {"
17218 [EXCERPT]
17219 ˇ[FOLDED]
17220 [EXCERPT]
17221 a1
17222 b1
17223 [EXCERPT]
17224 [FOLDED]
17225 [EXCERPT]
17226 [FOLDED]
17227 "
17228 });
17229 }
17230}
17231
17232#[gpui::test]
17233async fn test_inline_completion_text(cx: &mut TestAppContext) {
17234 init_test(cx, |_| {});
17235
17236 // Simple insertion
17237 assert_highlighted_edits(
17238 "Hello, world!",
17239 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17240 true,
17241 cx,
17242 |highlighted_edits, cx| {
17243 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17244 assert_eq!(highlighted_edits.highlights.len(), 1);
17245 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17246 assert_eq!(
17247 highlighted_edits.highlights[0].1.background_color,
17248 Some(cx.theme().status().created_background)
17249 );
17250 },
17251 )
17252 .await;
17253
17254 // Replacement
17255 assert_highlighted_edits(
17256 "This is a test.",
17257 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17258 false,
17259 cx,
17260 |highlighted_edits, cx| {
17261 assert_eq!(highlighted_edits.text, "That is a test.");
17262 assert_eq!(highlighted_edits.highlights.len(), 1);
17263 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17264 assert_eq!(
17265 highlighted_edits.highlights[0].1.background_color,
17266 Some(cx.theme().status().created_background)
17267 );
17268 },
17269 )
17270 .await;
17271
17272 // Multiple edits
17273 assert_highlighted_edits(
17274 "Hello, world!",
17275 vec![
17276 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17277 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17278 ],
17279 false,
17280 cx,
17281 |highlighted_edits, cx| {
17282 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17283 assert_eq!(highlighted_edits.highlights.len(), 2);
17284 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17285 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17286 assert_eq!(
17287 highlighted_edits.highlights[0].1.background_color,
17288 Some(cx.theme().status().created_background)
17289 );
17290 assert_eq!(
17291 highlighted_edits.highlights[1].1.background_color,
17292 Some(cx.theme().status().created_background)
17293 );
17294 },
17295 )
17296 .await;
17297
17298 // Multiple lines with edits
17299 assert_highlighted_edits(
17300 "First line\nSecond line\nThird line\nFourth line",
17301 vec![
17302 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17303 (
17304 Point::new(2, 0)..Point::new(2, 10),
17305 "New third line".to_string(),
17306 ),
17307 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17308 ],
17309 false,
17310 cx,
17311 |highlighted_edits, cx| {
17312 assert_eq!(
17313 highlighted_edits.text,
17314 "Second modified\nNew third line\nFourth updated line"
17315 );
17316 assert_eq!(highlighted_edits.highlights.len(), 3);
17317 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17318 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17319 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17320 for highlight in &highlighted_edits.highlights {
17321 assert_eq!(
17322 highlight.1.background_color,
17323 Some(cx.theme().status().created_background)
17324 );
17325 }
17326 },
17327 )
17328 .await;
17329}
17330
17331#[gpui::test]
17332async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17333 init_test(cx, |_| {});
17334
17335 // Deletion
17336 assert_highlighted_edits(
17337 "Hello, world!",
17338 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17339 true,
17340 cx,
17341 |highlighted_edits, cx| {
17342 assert_eq!(highlighted_edits.text, "Hello, world!");
17343 assert_eq!(highlighted_edits.highlights.len(), 1);
17344 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17345 assert_eq!(
17346 highlighted_edits.highlights[0].1.background_color,
17347 Some(cx.theme().status().deleted_background)
17348 );
17349 },
17350 )
17351 .await;
17352
17353 // Insertion
17354 assert_highlighted_edits(
17355 "Hello, world!",
17356 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17357 true,
17358 cx,
17359 |highlighted_edits, cx| {
17360 assert_eq!(highlighted_edits.highlights.len(), 1);
17361 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17362 assert_eq!(
17363 highlighted_edits.highlights[0].1.background_color,
17364 Some(cx.theme().status().created_background)
17365 );
17366 },
17367 )
17368 .await;
17369}
17370
17371async fn assert_highlighted_edits(
17372 text: &str,
17373 edits: Vec<(Range<Point>, String)>,
17374 include_deletions: bool,
17375 cx: &mut TestAppContext,
17376 assertion_fn: impl Fn(HighlightedText, &App),
17377) {
17378 let window = cx.add_window(|window, cx| {
17379 let buffer = MultiBuffer::build_simple(text, cx);
17380 Editor::new(EditorMode::Full, buffer, None, window, cx)
17381 });
17382 let cx = &mut VisualTestContext::from_window(*window, cx);
17383
17384 let (buffer, snapshot) = window
17385 .update(cx, |editor, _window, cx| {
17386 (
17387 editor.buffer().clone(),
17388 editor.buffer().read(cx).snapshot(cx),
17389 )
17390 })
17391 .unwrap();
17392
17393 let edits = edits
17394 .into_iter()
17395 .map(|(range, edit)| {
17396 (
17397 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17398 edit,
17399 )
17400 })
17401 .collect::<Vec<_>>();
17402
17403 let text_anchor_edits = edits
17404 .clone()
17405 .into_iter()
17406 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17407 .collect::<Vec<_>>();
17408
17409 let edit_preview = window
17410 .update(cx, |_, _window, cx| {
17411 buffer
17412 .read(cx)
17413 .as_singleton()
17414 .unwrap()
17415 .read(cx)
17416 .preview_edits(text_anchor_edits.into(), cx)
17417 })
17418 .unwrap()
17419 .await;
17420
17421 cx.update(|_window, cx| {
17422 let highlighted_edits = inline_completion_edit_text(
17423 &snapshot.as_singleton().unwrap().2,
17424 &edits,
17425 &edit_preview,
17426 include_deletions,
17427 cx,
17428 );
17429 assertion_fn(highlighted_edits, cx)
17430 });
17431}
17432
17433#[track_caller]
17434fn assert_breakpoint(
17435 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17436 path: &Arc<Path>,
17437 expected: Vec<(u32, Breakpoint)>,
17438) {
17439 if expected.len() == 0usize {
17440 assert!(!breakpoints.contains_key(path), "{}", path.display());
17441 } else {
17442 let mut breakpoint = breakpoints
17443 .get(path)
17444 .unwrap()
17445 .into_iter()
17446 .map(|breakpoint| {
17447 (
17448 breakpoint.row,
17449 Breakpoint {
17450 message: breakpoint.message.clone(),
17451 state: breakpoint.state,
17452 condition: breakpoint.condition.clone(),
17453 hit_condition: breakpoint.hit_condition.clone(),
17454 },
17455 )
17456 })
17457 .collect::<Vec<_>>();
17458
17459 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17460
17461 assert_eq!(expected, breakpoint);
17462 }
17463}
17464
17465fn add_log_breakpoint_at_cursor(
17466 editor: &mut Editor,
17467 log_message: &str,
17468 window: &mut Window,
17469 cx: &mut Context<Editor>,
17470) {
17471 let (anchor, bp) = editor
17472 .breakpoint_at_cursor_head(window, cx)
17473 .unwrap_or_else(|| {
17474 let cursor_position: Point = editor.selections.newest(cx).head();
17475
17476 let breakpoint_position = editor
17477 .snapshot(window, cx)
17478 .display_snapshot
17479 .buffer_snapshot
17480 .anchor_before(Point::new(cursor_position.row, 0));
17481
17482 (breakpoint_position, Breakpoint::new_log(&log_message))
17483 });
17484
17485 editor.edit_breakpoint_at_anchor(
17486 anchor,
17487 bp,
17488 BreakpointEditAction::EditLogMessage(log_message.into()),
17489 cx,
17490 );
17491}
17492
17493#[gpui::test]
17494async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17495 init_test(cx, |_| {});
17496
17497 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17498 let fs = FakeFs::new(cx.executor());
17499 fs.insert_tree(
17500 path!("/a"),
17501 json!({
17502 "main.rs": sample_text,
17503 }),
17504 )
17505 .await;
17506 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17507 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17508 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17509
17510 let fs = FakeFs::new(cx.executor());
17511 fs.insert_tree(
17512 path!("/a"),
17513 json!({
17514 "main.rs": sample_text,
17515 }),
17516 )
17517 .await;
17518 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17519 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17520 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17521 let worktree_id = workspace
17522 .update(cx, |workspace, _window, cx| {
17523 workspace.project().update(cx, |project, cx| {
17524 project.worktrees(cx).next().unwrap().read(cx).id()
17525 })
17526 })
17527 .unwrap();
17528
17529 let buffer = project
17530 .update(cx, |project, cx| {
17531 project.open_buffer((worktree_id, "main.rs"), cx)
17532 })
17533 .await
17534 .unwrap();
17535
17536 let (editor, cx) = cx.add_window_view(|window, cx| {
17537 Editor::new(
17538 EditorMode::Full,
17539 MultiBuffer::build_from_buffer(buffer, cx),
17540 Some(project.clone()),
17541 window,
17542 cx,
17543 )
17544 });
17545
17546 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17547 let abs_path = project.read_with(cx, |project, cx| {
17548 project
17549 .absolute_path(&project_path, cx)
17550 .map(|path_buf| Arc::from(path_buf.to_owned()))
17551 .unwrap()
17552 });
17553
17554 // assert we can add breakpoint on the first line
17555 editor.update_in(cx, |editor, window, cx| {
17556 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17557 editor.move_to_end(&MoveToEnd, window, cx);
17558 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17559 });
17560
17561 let breakpoints = editor.update(cx, |editor, cx| {
17562 editor
17563 .breakpoint_store()
17564 .as_ref()
17565 .unwrap()
17566 .read(cx)
17567 .all_breakpoints(cx)
17568 .clone()
17569 });
17570
17571 assert_eq!(1, breakpoints.len());
17572 assert_breakpoint(
17573 &breakpoints,
17574 &abs_path,
17575 vec![
17576 (0, Breakpoint::new_standard()),
17577 (3, Breakpoint::new_standard()),
17578 ],
17579 );
17580
17581 editor.update_in(cx, |editor, window, cx| {
17582 editor.move_to_beginning(&MoveToBeginning, window, cx);
17583 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17584 });
17585
17586 let breakpoints = editor.update(cx, |editor, cx| {
17587 editor
17588 .breakpoint_store()
17589 .as_ref()
17590 .unwrap()
17591 .read(cx)
17592 .all_breakpoints(cx)
17593 .clone()
17594 });
17595
17596 assert_eq!(1, breakpoints.len());
17597 assert_breakpoint(
17598 &breakpoints,
17599 &abs_path,
17600 vec![(3, Breakpoint::new_standard())],
17601 );
17602
17603 editor.update_in(cx, |editor, window, cx| {
17604 editor.move_to_end(&MoveToEnd, window, cx);
17605 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17606 });
17607
17608 let breakpoints = editor.update(cx, |editor, cx| {
17609 editor
17610 .breakpoint_store()
17611 .as_ref()
17612 .unwrap()
17613 .read(cx)
17614 .all_breakpoints(cx)
17615 .clone()
17616 });
17617
17618 assert_eq!(0, breakpoints.len());
17619 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17620}
17621
17622#[gpui::test]
17623async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17624 init_test(cx, |_| {});
17625
17626 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17627
17628 let fs = FakeFs::new(cx.executor());
17629 fs.insert_tree(
17630 path!("/a"),
17631 json!({
17632 "main.rs": sample_text,
17633 }),
17634 )
17635 .await;
17636 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17637 let (workspace, cx) =
17638 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17639
17640 let worktree_id = workspace.update(cx, |workspace, cx| {
17641 workspace.project().update(cx, |project, cx| {
17642 project.worktrees(cx).next().unwrap().read(cx).id()
17643 })
17644 });
17645
17646 let buffer = project
17647 .update(cx, |project, cx| {
17648 project.open_buffer((worktree_id, "main.rs"), cx)
17649 })
17650 .await
17651 .unwrap();
17652
17653 let (editor, cx) = cx.add_window_view(|window, cx| {
17654 Editor::new(
17655 EditorMode::Full,
17656 MultiBuffer::build_from_buffer(buffer, cx),
17657 Some(project.clone()),
17658 window,
17659 cx,
17660 )
17661 });
17662
17663 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17664 let abs_path = project.read_with(cx, |project, cx| {
17665 project
17666 .absolute_path(&project_path, cx)
17667 .map(|path_buf| Arc::from(path_buf.to_owned()))
17668 .unwrap()
17669 });
17670
17671 editor.update_in(cx, |editor, window, cx| {
17672 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17673 });
17674
17675 let breakpoints = editor.update(cx, |editor, cx| {
17676 editor
17677 .breakpoint_store()
17678 .as_ref()
17679 .unwrap()
17680 .read(cx)
17681 .all_breakpoints(cx)
17682 .clone()
17683 });
17684
17685 assert_breakpoint(
17686 &breakpoints,
17687 &abs_path,
17688 vec![(0, Breakpoint::new_log("hello world"))],
17689 );
17690
17691 // Removing a log message from a log breakpoint should remove it
17692 editor.update_in(cx, |editor, window, cx| {
17693 add_log_breakpoint_at_cursor(editor, "", window, cx);
17694 });
17695
17696 let breakpoints = editor.update(cx, |editor, cx| {
17697 editor
17698 .breakpoint_store()
17699 .as_ref()
17700 .unwrap()
17701 .read(cx)
17702 .all_breakpoints(cx)
17703 .clone()
17704 });
17705
17706 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17707
17708 editor.update_in(cx, |editor, window, cx| {
17709 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17710 editor.move_to_end(&MoveToEnd, window, cx);
17711 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17712 // Not adding a log message to a standard breakpoint shouldn't remove it
17713 add_log_breakpoint_at_cursor(editor, "", window, cx);
17714 });
17715
17716 let breakpoints = editor.update(cx, |editor, cx| {
17717 editor
17718 .breakpoint_store()
17719 .as_ref()
17720 .unwrap()
17721 .read(cx)
17722 .all_breakpoints(cx)
17723 .clone()
17724 });
17725
17726 assert_breakpoint(
17727 &breakpoints,
17728 &abs_path,
17729 vec![
17730 (0, Breakpoint::new_standard()),
17731 (3, Breakpoint::new_standard()),
17732 ],
17733 );
17734
17735 editor.update_in(cx, |editor, window, cx| {
17736 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17737 });
17738
17739 let breakpoints = editor.update(cx, |editor, cx| {
17740 editor
17741 .breakpoint_store()
17742 .as_ref()
17743 .unwrap()
17744 .read(cx)
17745 .all_breakpoints(cx)
17746 .clone()
17747 });
17748
17749 assert_breakpoint(
17750 &breakpoints,
17751 &abs_path,
17752 vec![
17753 (0, Breakpoint::new_standard()),
17754 (3, Breakpoint::new_log("hello world")),
17755 ],
17756 );
17757
17758 editor.update_in(cx, |editor, window, cx| {
17759 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17760 });
17761
17762 let breakpoints = editor.update(cx, |editor, cx| {
17763 editor
17764 .breakpoint_store()
17765 .as_ref()
17766 .unwrap()
17767 .read(cx)
17768 .all_breakpoints(cx)
17769 .clone()
17770 });
17771
17772 assert_breakpoint(
17773 &breakpoints,
17774 &abs_path,
17775 vec![
17776 (0, Breakpoint::new_standard()),
17777 (3, Breakpoint::new_log("hello Earth!!")),
17778 ],
17779 );
17780}
17781
17782/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17783/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17784/// or when breakpoints were placed out of order. This tests for a regression too
17785#[gpui::test]
17786async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17787 init_test(cx, |_| {});
17788
17789 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17790 let fs = FakeFs::new(cx.executor());
17791 fs.insert_tree(
17792 path!("/a"),
17793 json!({
17794 "main.rs": sample_text,
17795 }),
17796 )
17797 .await;
17798 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17799 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17800 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17801
17802 let fs = FakeFs::new(cx.executor());
17803 fs.insert_tree(
17804 path!("/a"),
17805 json!({
17806 "main.rs": sample_text,
17807 }),
17808 )
17809 .await;
17810 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17811 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17812 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17813 let worktree_id = workspace
17814 .update(cx, |workspace, _window, cx| {
17815 workspace.project().update(cx, |project, cx| {
17816 project.worktrees(cx).next().unwrap().read(cx).id()
17817 })
17818 })
17819 .unwrap();
17820
17821 let buffer = project
17822 .update(cx, |project, cx| {
17823 project.open_buffer((worktree_id, "main.rs"), cx)
17824 })
17825 .await
17826 .unwrap();
17827
17828 let (editor, cx) = cx.add_window_view(|window, cx| {
17829 Editor::new(
17830 EditorMode::Full,
17831 MultiBuffer::build_from_buffer(buffer, cx),
17832 Some(project.clone()),
17833 window,
17834 cx,
17835 )
17836 });
17837
17838 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17839 let abs_path = project.read_with(cx, |project, cx| {
17840 project
17841 .absolute_path(&project_path, cx)
17842 .map(|path_buf| Arc::from(path_buf.to_owned()))
17843 .unwrap()
17844 });
17845
17846 // assert we can add breakpoint on the first line
17847 editor.update_in(cx, |editor, window, cx| {
17848 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17849 editor.move_to_end(&MoveToEnd, window, cx);
17850 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17851 editor.move_up(&MoveUp, window, cx);
17852 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17853 });
17854
17855 let breakpoints = editor.update(cx, |editor, cx| {
17856 editor
17857 .breakpoint_store()
17858 .as_ref()
17859 .unwrap()
17860 .read(cx)
17861 .all_breakpoints(cx)
17862 .clone()
17863 });
17864
17865 assert_eq!(1, breakpoints.len());
17866 assert_breakpoint(
17867 &breakpoints,
17868 &abs_path,
17869 vec![
17870 (0, Breakpoint::new_standard()),
17871 (2, Breakpoint::new_standard()),
17872 (3, Breakpoint::new_standard()),
17873 ],
17874 );
17875
17876 editor.update_in(cx, |editor, window, cx| {
17877 editor.move_to_beginning(&MoveToBeginning, window, cx);
17878 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17879 editor.move_to_end(&MoveToEnd, window, cx);
17880 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17881 // Disabling a breakpoint that doesn't exist should do nothing
17882 editor.move_up(&MoveUp, window, cx);
17883 editor.move_up(&MoveUp, window, cx);
17884 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17885 });
17886
17887 let breakpoints = editor.update(cx, |editor, cx| {
17888 editor
17889 .breakpoint_store()
17890 .as_ref()
17891 .unwrap()
17892 .read(cx)
17893 .all_breakpoints(cx)
17894 .clone()
17895 });
17896
17897 let disable_breakpoint = {
17898 let mut bp = Breakpoint::new_standard();
17899 bp.state = BreakpointState::Disabled;
17900 bp
17901 };
17902
17903 assert_eq!(1, breakpoints.len());
17904 assert_breakpoint(
17905 &breakpoints,
17906 &abs_path,
17907 vec![
17908 (0, disable_breakpoint.clone()),
17909 (2, Breakpoint::new_standard()),
17910 (3, disable_breakpoint.clone()),
17911 ],
17912 );
17913
17914 editor.update_in(cx, |editor, window, cx| {
17915 editor.move_to_beginning(&MoveToBeginning, window, cx);
17916 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17917 editor.move_to_end(&MoveToEnd, window, cx);
17918 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
17919 editor.move_up(&MoveUp, window, cx);
17920 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
17921 });
17922
17923 let breakpoints = editor.update(cx, |editor, cx| {
17924 editor
17925 .breakpoint_store()
17926 .as_ref()
17927 .unwrap()
17928 .read(cx)
17929 .all_breakpoints(cx)
17930 .clone()
17931 });
17932
17933 assert_eq!(1, breakpoints.len());
17934 assert_breakpoint(
17935 &breakpoints,
17936 &abs_path,
17937 vec![
17938 (0, Breakpoint::new_standard()),
17939 (2, disable_breakpoint),
17940 (3, Breakpoint::new_standard()),
17941 ],
17942 );
17943}
17944
17945#[gpui::test]
17946async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
17947 init_test(cx, |_| {});
17948 let capabilities = lsp::ServerCapabilities {
17949 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
17950 prepare_provider: Some(true),
17951 work_done_progress_options: Default::default(),
17952 })),
17953 ..Default::default()
17954 };
17955 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
17956
17957 cx.set_state(indoc! {"
17958 struct Fˇoo {}
17959 "});
17960
17961 cx.update_editor(|editor, _, cx| {
17962 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
17963 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
17964 editor.highlight_background::<DocumentHighlightRead>(
17965 &[highlight_range],
17966 |c| c.editor_document_highlight_read_background,
17967 cx,
17968 );
17969 });
17970
17971 let mut prepare_rename_handler = cx
17972 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
17973 move |_, _, _| async move {
17974 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
17975 start: lsp::Position {
17976 line: 0,
17977 character: 7,
17978 },
17979 end: lsp::Position {
17980 line: 0,
17981 character: 10,
17982 },
17983 })))
17984 },
17985 );
17986 let prepare_rename_task = cx
17987 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
17988 .expect("Prepare rename was not started");
17989 prepare_rename_handler.next().await.unwrap();
17990 prepare_rename_task.await.expect("Prepare rename failed");
17991
17992 let mut rename_handler =
17993 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
17994 let edit = lsp::TextEdit {
17995 range: lsp::Range {
17996 start: lsp::Position {
17997 line: 0,
17998 character: 7,
17999 },
18000 end: lsp::Position {
18001 line: 0,
18002 character: 10,
18003 },
18004 },
18005 new_text: "FooRenamed".to_string(),
18006 };
18007 Ok(Some(lsp::WorkspaceEdit::new(
18008 // Specify the same edit twice
18009 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18010 )))
18011 });
18012 let rename_task = cx
18013 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18014 .expect("Confirm rename was not started");
18015 rename_handler.next().await.unwrap();
18016 rename_task.await.expect("Confirm rename failed");
18017 cx.run_until_parked();
18018
18019 // Despite two edits, only one is actually applied as those are identical
18020 cx.assert_editor_state(indoc! {"
18021 struct FooRenamedˇ {}
18022 "});
18023}
18024
18025#[gpui::test]
18026async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18027 init_test(cx, |_| {});
18028 // These capabilities indicate that the server does not support prepare rename.
18029 let capabilities = lsp::ServerCapabilities {
18030 rename_provider: Some(lsp::OneOf::Left(true)),
18031 ..Default::default()
18032 };
18033 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18034
18035 cx.set_state(indoc! {"
18036 struct Fˇoo {}
18037 "});
18038
18039 cx.update_editor(|editor, _window, cx| {
18040 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18041 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18042 editor.highlight_background::<DocumentHighlightRead>(
18043 &[highlight_range],
18044 |c| c.editor_document_highlight_read_background,
18045 cx,
18046 );
18047 });
18048
18049 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18050 .expect("Prepare rename was not started")
18051 .await
18052 .expect("Prepare rename failed");
18053
18054 let mut rename_handler =
18055 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18056 let edit = lsp::TextEdit {
18057 range: lsp::Range {
18058 start: lsp::Position {
18059 line: 0,
18060 character: 7,
18061 },
18062 end: lsp::Position {
18063 line: 0,
18064 character: 10,
18065 },
18066 },
18067 new_text: "FooRenamed".to_string(),
18068 };
18069 Ok(Some(lsp::WorkspaceEdit::new(
18070 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18071 )))
18072 });
18073 let rename_task = cx
18074 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18075 .expect("Confirm rename was not started");
18076 rename_handler.next().await.unwrap();
18077 rename_task.await.expect("Confirm rename failed");
18078 cx.run_until_parked();
18079
18080 // Correct range is renamed, as `surrounding_word` is used to find it.
18081 cx.assert_editor_state(indoc! {"
18082 struct FooRenamedˇ {}
18083 "});
18084}
18085
18086#[gpui::test]
18087async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18088 init_test(cx, |_| {});
18089 let mut cx = EditorTestContext::new(cx).await;
18090
18091 let language = Arc::new(
18092 Language::new(
18093 LanguageConfig::default(),
18094 Some(tree_sitter_html::LANGUAGE.into()),
18095 )
18096 .with_brackets_query(
18097 r#"
18098 ("<" @open "/>" @close)
18099 ("</" @open ">" @close)
18100 ("<" @open ">" @close)
18101 ("\"" @open "\"" @close)
18102 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18103 "#,
18104 )
18105 .unwrap(),
18106 );
18107 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18108
18109 cx.set_state(indoc! {"
18110 <span>ˇ</span>
18111 "});
18112 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18113 cx.assert_editor_state(indoc! {"
18114 <span>
18115 ˇ
18116 </span>
18117 "});
18118
18119 cx.set_state(indoc! {"
18120 <span><span></span>ˇ</span>
18121 "});
18122 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18123 cx.assert_editor_state(indoc! {"
18124 <span><span></span>
18125 ˇ</span>
18126 "});
18127
18128 cx.set_state(indoc! {"
18129 <span>ˇ
18130 </span>
18131 "});
18132 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18133 cx.assert_editor_state(indoc! {"
18134 <span>
18135 ˇ
18136 </span>
18137 "});
18138}
18139
18140#[gpui::test(iterations = 10)]
18141async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18142 init_test(cx, |_| {});
18143
18144 let fs = FakeFs::new(cx.executor());
18145 fs.insert_tree(
18146 path!("/dir"),
18147 json!({
18148 "a.ts": "a",
18149 }),
18150 )
18151 .await;
18152
18153 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18154 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18155 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18156
18157 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18158 language_registry.add(Arc::new(Language::new(
18159 LanguageConfig {
18160 name: "TypeScript".into(),
18161 matcher: LanguageMatcher {
18162 path_suffixes: vec!["ts".to_string()],
18163 ..Default::default()
18164 },
18165 ..Default::default()
18166 },
18167 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18168 )));
18169 let mut fake_language_servers = language_registry.register_fake_lsp(
18170 "TypeScript",
18171 FakeLspAdapter {
18172 capabilities: lsp::ServerCapabilities {
18173 code_lens_provider: Some(lsp::CodeLensOptions {
18174 resolve_provider: Some(true),
18175 }),
18176 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18177 commands: vec!["_the/command".to_string()],
18178 ..lsp::ExecuteCommandOptions::default()
18179 }),
18180 ..lsp::ServerCapabilities::default()
18181 },
18182 ..FakeLspAdapter::default()
18183 },
18184 );
18185
18186 let (buffer, _handle) = project
18187 .update(cx, |p, cx| {
18188 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18189 })
18190 .await
18191 .unwrap();
18192 cx.executor().run_until_parked();
18193
18194 let fake_server = fake_language_servers.next().await.unwrap();
18195
18196 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18197 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18198 drop(buffer_snapshot);
18199 let actions = cx
18200 .update_window(*workspace, |_, window, cx| {
18201 project.code_actions(&buffer, anchor..anchor, window, cx)
18202 })
18203 .unwrap();
18204
18205 fake_server
18206 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18207 Ok(Some(vec![
18208 lsp::CodeLens {
18209 range: lsp::Range::default(),
18210 command: Some(lsp::Command {
18211 title: "Code lens command".to_owned(),
18212 command: "_the/command".to_owned(),
18213 arguments: None,
18214 }),
18215 data: None,
18216 },
18217 lsp::CodeLens {
18218 range: lsp::Range::default(),
18219 command: Some(lsp::Command {
18220 title: "Command not in capabilities".to_owned(),
18221 command: "not in capabilities".to_owned(),
18222 arguments: None,
18223 }),
18224 data: None,
18225 },
18226 lsp::CodeLens {
18227 range: lsp::Range {
18228 start: lsp::Position {
18229 line: 1,
18230 character: 1,
18231 },
18232 end: lsp::Position {
18233 line: 1,
18234 character: 1,
18235 },
18236 },
18237 command: Some(lsp::Command {
18238 title: "Command not in range".to_owned(),
18239 command: "_the/command".to_owned(),
18240 arguments: None,
18241 }),
18242 data: None,
18243 },
18244 ]))
18245 })
18246 .next()
18247 .await;
18248
18249 let actions = actions.await.unwrap();
18250 assert_eq!(
18251 actions.len(),
18252 1,
18253 "Should have only one valid action for the 0..0 range"
18254 );
18255 let action = actions[0].clone();
18256 let apply = project.update(cx, |project, cx| {
18257 project.apply_code_action(buffer.clone(), action, true, cx)
18258 });
18259
18260 // Resolving the code action does not populate its edits. In absence of
18261 // edits, we must execute the given command.
18262 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18263 |mut lens, _| async move {
18264 let lens_command = lens.command.as_mut().expect("should have a command");
18265 assert_eq!(lens_command.title, "Code lens command");
18266 lens_command.arguments = Some(vec![json!("the-argument")]);
18267 Ok(lens)
18268 },
18269 );
18270
18271 // While executing the command, the language server sends the editor
18272 // a `workspaceEdit` request.
18273 fake_server
18274 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18275 let fake = fake_server.clone();
18276 move |params, _| {
18277 assert_eq!(params.command, "_the/command");
18278 let fake = fake.clone();
18279 async move {
18280 fake.server
18281 .request::<lsp::request::ApplyWorkspaceEdit>(
18282 lsp::ApplyWorkspaceEditParams {
18283 label: None,
18284 edit: lsp::WorkspaceEdit {
18285 changes: Some(
18286 [(
18287 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18288 vec![lsp::TextEdit {
18289 range: lsp::Range::new(
18290 lsp::Position::new(0, 0),
18291 lsp::Position::new(0, 0),
18292 ),
18293 new_text: "X".into(),
18294 }],
18295 )]
18296 .into_iter()
18297 .collect(),
18298 ),
18299 ..Default::default()
18300 },
18301 },
18302 )
18303 .await
18304 .unwrap();
18305 Ok(Some(json!(null)))
18306 }
18307 }
18308 })
18309 .next()
18310 .await;
18311
18312 // Applying the code lens command returns a project transaction containing the edits
18313 // sent by the language server in its `workspaceEdit` request.
18314 let transaction = apply.await.unwrap();
18315 assert!(transaction.0.contains_key(&buffer));
18316 buffer.update(cx, |buffer, cx| {
18317 assert_eq!(buffer.text(), "Xa");
18318 buffer.undo(cx);
18319 assert_eq!(buffer.text(), "a");
18320 });
18321}
18322
18323#[gpui::test]
18324async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18325 init_test(cx, |_| {});
18326
18327 let fs = FakeFs::new(cx.executor());
18328 let main_text = r#"fn main() {
18329println!("1");
18330println!("2");
18331println!("3");
18332println!("4");
18333println!("5");
18334}"#;
18335 let lib_text = "mod foo {}";
18336 fs.insert_tree(
18337 path!("/a"),
18338 json!({
18339 "lib.rs": lib_text,
18340 "main.rs": main_text,
18341 }),
18342 )
18343 .await;
18344
18345 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18346 let (workspace, cx) =
18347 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18348 let worktree_id = workspace.update(cx, |workspace, cx| {
18349 workspace.project().update(cx, |project, cx| {
18350 project.worktrees(cx).next().unwrap().read(cx).id()
18351 })
18352 });
18353
18354 let expected_ranges = vec![
18355 Point::new(0, 0)..Point::new(0, 0),
18356 Point::new(1, 0)..Point::new(1, 1),
18357 Point::new(2, 0)..Point::new(2, 2),
18358 Point::new(3, 0)..Point::new(3, 3),
18359 ];
18360
18361 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18362 let editor_1 = workspace
18363 .update_in(cx, |workspace, window, cx| {
18364 workspace.open_path(
18365 (worktree_id, "main.rs"),
18366 Some(pane_1.downgrade()),
18367 true,
18368 window,
18369 cx,
18370 )
18371 })
18372 .unwrap()
18373 .await
18374 .downcast::<Editor>()
18375 .unwrap();
18376 pane_1.update(cx, |pane, cx| {
18377 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18378 open_editor.update(cx, |editor, cx| {
18379 assert_eq!(
18380 editor.display_text(cx),
18381 main_text,
18382 "Original main.rs text on initial open",
18383 );
18384 assert_eq!(
18385 editor
18386 .selections
18387 .all::<Point>(cx)
18388 .into_iter()
18389 .map(|s| s.range())
18390 .collect::<Vec<_>>(),
18391 vec![Point::zero()..Point::zero()],
18392 "Default selections on initial open",
18393 );
18394 })
18395 });
18396 editor_1.update_in(cx, |editor, window, cx| {
18397 editor.change_selections(None, window, cx, |s| {
18398 s.select_ranges(expected_ranges.clone());
18399 });
18400 });
18401
18402 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18403 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18404 });
18405 let editor_2 = workspace
18406 .update_in(cx, |workspace, window, cx| {
18407 workspace.open_path(
18408 (worktree_id, "main.rs"),
18409 Some(pane_2.downgrade()),
18410 true,
18411 window,
18412 cx,
18413 )
18414 })
18415 .unwrap()
18416 .await
18417 .downcast::<Editor>()
18418 .unwrap();
18419 pane_2.update(cx, |pane, cx| {
18420 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18421 open_editor.update(cx, |editor, cx| {
18422 assert_eq!(
18423 editor.display_text(cx),
18424 main_text,
18425 "Original main.rs text on initial open in another panel",
18426 );
18427 assert_eq!(
18428 editor
18429 .selections
18430 .all::<Point>(cx)
18431 .into_iter()
18432 .map(|s| s.range())
18433 .collect::<Vec<_>>(),
18434 vec![Point::zero()..Point::zero()],
18435 "Default selections on initial open in another panel",
18436 );
18437 })
18438 });
18439
18440 editor_2.update_in(cx, |editor, window, cx| {
18441 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18442 });
18443
18444 let _other_editor_1 = workspace
18445 .update_in(cx, |workspace, window, cx| {
18446 workspace.open_path(
18447 (worktree_id, "lib.rs"),
18448 Some(pane_1.downgrade()),
18449 true,
18450 window,
18451 cx,
18452 )
18453 })
18454 .unwrap()
18455 .await
18456 .downcast::<Editor>()
18457 .unwrap();
18458 pane_1
18459 .update_in(cx, |pane, window, cx| {
18460 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18461 .unwrap()
18462 })
18463 .await
18464 .unwrap();
18465 drop(editor_1);
18466 pane_1.update(cx, |pane, cx| {
18467 pane.active_item()
18468 .unwrap()
18469 .downcast::<Editor>()
18470 .unwrap()
18471 .update(cx, |editor, cx| {
18472 assert_eq!(
18473 editor.display_text(cx),
18474 lib_text,
18475 "Other file should be open and active",
18476 );
18477 });
18478 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18479 });
18480
18481 let _other_editor_2 = workspace
18482 .update_in(cx, |workspace, window, cx| {
18483 workspace.open_path(
18484 (worktree_id, "lib.rs"),
18485 Some(pane_2.downgrade()),
18486 true,
18487 window,
18488 cx,
18489 )
18490 })
18491 .unwrap()
18492 .await
18493 .downcast::<Editor>()
18494 .unwrap();
18495 pane_2
18496 .update_in(cx, |pane, window, cx| {
18497 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18498 .unwrap()
18499 })
18500 .await
18501 .unwrap();
18502 drop(editor_2);
18503 pane_2.update(cx, |pane, cx| {
18504 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18505 open_editor.update(cx, |editor, cx| {
18506 assert_eq!(
18507 editor.display_text(cx),
18508 lib_text,
18509 "Other file should be open and active in another panel too",
18510 );
18511 });
18512 assert_eq!(
18513 pane.items().count(),
18514 1,
18515 "No other editors should be open in another pane",
18516 );
18517 });
18518
18519 let _editor_1_reopened = workspace
18520 .update_in(cx, |workspace, window, cx| {
18521 workspace.open_path(
18522 (worktree_id, "main.rs"),
18523 Some(pane_1.downgrade()),
18524 true,
18525 window,
18526 cx,
18527 )
18528 })
18529 .unwrap()
18530 .await
18531 .downcast::<Editor>()
18532 .unwrap();
18533 let _editor_2_reopened = workspace
18534 .update_in(cx, |workspace, window, cx| {
18535 workspace.open_path(
18536 (worktree_id, "main.rs"),
18537 Some(pane_2.downgrade()),
18538 true,
18539 window,
18540 cx,
18541 )
18542 })
18543 .unwrap()
18544 .await
18545 .downcast::<Editor>()
18546 .unwrap();
18547 pane_1.update(cx, |pane, cx| {
18548 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18549 open_editor.update(cx, |editor, cx| {
18550 assert_eq!(
18551 editor.display_text(cx),
18552 main_text,
18553 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18554 );
18555 assert_eq!(
18556 editor
18557 .selections
18558 .all::<Point>(cx)
18559 .into_iter()
18560 .map(|s| s.range())
18561 .collect::<Vec<_>>(),
18562 expected_ranges,
18563 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18564 );
18565 })
18566 });
18567 pane_2.update(cx, |pane, cx| {
18568 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18569 open_editor.update(cx, |editor, cx| {
18570 assert_eq!(
18571 editor.display_text(cx),
18572 r#"fn main() {
18573⋯rintln!("1");
18574⋯intln!("2");
18575⋯ntln!("3");
18576println!("4");
18577println!("5");
18578}"#,
18579 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18580 );
18581 assert_eq!(
18582 editor
18583 .selections
18584 .all::<Point>(cx)
18585 .into_iter()
18586 .map(|s| s.range())
18587 .collect::<Vec<_>>(),
18588 vec![Point::zero()..Point::zero()],
18589 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18590 );
18591 })
18592 });
18593}
18594
18595#[gpui::test]
18596async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18597 init_test(cx, |_| {});
18598
18599 let fs = FakeFs::new(cx.executor());
18600 let main_text = r#"fn main() {
18601println!("1");
18602println!("2");
18603println!("3");
18604println!("4");
18605println!("5");
18606}"#;
18607 let lib_text = "mod foo {}";
18608 fs.insert_tree(
18609 path!("/a"),
18610 json!({
18611 "lib.rs": lib_text,
18612 "main.rs": main_text,
18613 }),
18614 )
18615 .await;
18616
18617 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18618 let (workspace, cx) =
18619 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18620 let worktree_id = workspace.update(cx, |workspace, cx| {
18621 workspace.project().update(cx, |project, cx| {
18622 project.worktrees(cx).next().unwrap().read(cx).id()
18623 })
18624 });
18625
18626 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18627 let editor = workspace
18628 .update_in(cx, |workspace, window, cx| {
18629 workspace.open_path(
18630 (worktree_id, "main.rs"),
18631 Some(pane.downgrade()),
18632 true,
18633 window,
18634 cx,
18635 )
18636 })
18637 .unwrap()
18638 .await
18639 .downcast::<Editor>()
18640 .unwrap();
18641 pane.update(cx, |pane, cx| {
18642 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18643 open_editor.update(cx, |editor, cx| {
18644 assert_eq!(
18645 editor.display_text(cx),
18646 main_text,
18647 "Original main.rs text on initial open",
18648 );
18649 })
18650 });
18651 editor.update_in(cx, |editor, window, cx| {
18652 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18653 });
18654
18655 cx.update_global(|store: &mut SettingsStore, cx| {
18656 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18657 s.restore_on_file_reopen = Some(false);
18658 });
18659 });
18660 editor.update_in(cx, |editor, window, cx| {
18661 editor.fold_ranges(
18662 vec![
18663 Point::new(1, 0)..Point::new(1, 1),
18664 Point::new(2, 0)..Point::new(2, 2),
18665 Point::new(3, 0)..Point::new(3, 3),
18666 ],
18667 false,
18668 window,
18669 cx,
18670 );
18671 });
18672 pane.update_in(cx, |pane, window, cx| {
18673 pane.close_all_items(&CloseAllItems::default(), window, cx)
18674 .unwrap()
18675 })
18676 .await
18677 .unwrap();
18678 pane.update(cx, |pane, _| {
18679 assert!(pane.active_item().is_none());
18680 });
18681 cx.update_global(|store: &mut SettingsStore, cx| {
18682 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18683 s.restore_on_file_reopen = Some(true);
18684 });
18685 });
18686
18687 let _editor_reopened = workspace
18688 .update_in(cx, |workspace, window, cx| {
18689 workspace.open_path(
18690 (worktree_id, "main.rs"),
18691 Some(pane.downgrade()),
18692 true,
18693 window,
18694 cx,
18695 )
18696 })
18697 .unwrap()
18698 .await
18699 .downcast::<Editor>()
18700 .unwrap();
18701 pane.update(cx, |pane, cx| {
18702 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18703 open_editor.update(cx, |editor, cx| {
18704 assert_eq!(
18705 editor.display_text(cx),
18706 main_text,
18707 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18708 );
18709 })
18710 });
18711}
18712
18713fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18714 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18715 point..point
18716}
18717
18718fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18719 let (text, ranges) = marked_text_ranges(marked_text, true);
18720 assert_eq!(editor.text(cx), text);
18721 assert_eq!(
18722 editor.selections.ranges(cx),
18723 ranges,
18724 "Assert selections are {}",
18725 marked_text
18726 );
18727}
18728
18729pub fn handle_signature_help_request(
18730 cx: &mut EditorLspTestContext,
18731 mocked_response: lsp::SignatureHelp,
18732) -> impl Future<Output = ()> + use<> {
18733 let mut request =
18734 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18735 let mocked_response = mocked_response.clone();
18736 async move { Ok(Some(mocked_response)) }
18737 });
18738
18739 async move {
18740 request.next().await;
18741 }
18742}
18743
18744/// Handle completion request passing a marked string specifying where the completion
18745/// should be triggered from using '|' character, what range should be replaced, and what completions
18746/// should be returned using '<' and '>' to delimit the range.
18747///
18748/// Also see `handle_completion_request_with_insert_and_replace`.
18749#[track_caller]
18750pub fn handle_completion_request(
18751 cx: &mut EditorLspTestContext,
18752 marked_string: &str,
18753 completions: Vec<&'static str>,
18754 counter: Arc<AtomicUsize>,
18755) -> impl Future<Output = ()> {
18756 let complete_from_marker: TextRangeMarker = '|'.into();
18757 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18758 let (_, mut marked_ranges) = marked_text_ranges_by(
18759 marked_string,
18760 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18761 );
18762
18763 let complete_from_position =
18764 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18765 let replace_range =
18766 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18767
18768 let mut request =
18769 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18770 let completions = completions.clone();
18771 counter.fetch_add(1, atomic::Ordering::Release);
18772 async move {
18773 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18774 assert_eq!(
18775 params.text_document_position.position,
18776 complete_from_position
18777 );
18778 Ok(Some(lsp::CompletionResponse::Array(
18779 completions
18780 .iter()
18781 .map(|completion_text| lsp::CompletionItem {
18782 label: completion_text.to_string(),
18783 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18784 range: replace_range,
18785 new_text: completion_text.to_string(),
18786 })),
18787 ..Default::default()
18788 })
18789 .collect(),
18790 )))
18791 }
18792 });
18793
18794 async move {
18795 request.next().await;
18796 }
18797}
18798
18799/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
18800/// given instead, which also contains an `insert` range.
18801///
18802/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
18803/// that is, `replace_range.start..cursor_pos`.
18804pub fn handle_completion_request_with_insert_and_replace(
18805 cx: &mut EditorLspTestContext,
18806 marked_string: &str,
18807 completions: Vec<&'static str>,
18808 counter: Arc<AtomicUsize>,
18809) -> impl Future<Output = ()> {
18810 let complete_from_marker: TextRangeMarker = '|'.into();
18811 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18812 let (_, mut marked_ranges) = marked_text_ranges_by(
18813 marked_string,
18814 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18815 );
18816
18817 let complete_from_position =
18818 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18819 let replace_range =
18820 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18821
18822 let mut request =
18823 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18824 let completions = completions.clone();
18825 counter.fetch_add(1, atomic::Ordering::Release);
18826 async move {
18827 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18828 assert_eq!(
18829 params.text_document_position.position, complete_from_position,
18830 "marker `|` position doesn't match",
18831 );
18832 Ok(Some(lsp::CompletionResponse::Array(
18833 completions
18834 .iter()
18835 .map(|completion_text| lsp::CompletionItem {
18836 label: completion_text.to_string(),
18837 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18838 lsp::InsertReplaceEdit {
18839 insert: lsp::Range {
18840 start: replace_range.start,
18841 end: complete_from_position,
18842 },
18843 replace: replace_range,
18844 new_text: completion_text.to_string(),
18845 },
18846 )),
18847 ..Default::default()
18848 })
18849 .collect(),
18850 )))
18851 }
18852 });
18853
18854 async move {
18855 request.next().await;
18856 }
18857}
18858
18859fn handle_resolve_completion_request(
18860 cx: &mut EditorLspTestContext,
18861 edits: Option<Vec<(&'static str, &'static str)>>,
18862) -> impl Future<Output = ()> {
18863 let edits = edits.map(|edits| {
18864 edits
18865 .iter()
18866 .map(|(marked_string, new_text)| {
18867 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
18868 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
18869 lsp::TextEdit::new(replace_range, new_text.to_string())
18870 })
18871 .collect::<Vec<_>>()
18872 });
18873
18874 let mut request =
18875 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18876 let edits = edits.clone();
18877 async move {
18878 Ok(lsp::CompletionItem {
18879 additional_text_edits: edits,
18880 ..Default::default()
18881 })
18882 }
18883 });
18884
18885 async move {
18886 request.next().await;
18887 }
18888}
18889
18890pub(crate) fn update_test_language_settings(
18891 cx: &mut TestAppContext,
18892 f: impl Fn(&mut AllLanguageSettingsContent),
18893) {
18894 cx.update(|cx| {
18895 SettingsStore::update_global(cx, |store, cx| {
18896 store.update_user_settings::<AllLanguageSettings>(cx, f);
18897 });
18898 });
18899}
18900
18901pub(crate) fn update_test_project_settings(
18902 cx: &mut TestAppContext,
18903 f: impl Fn(&mut ProjectSettings),
18904) {
18905 cx.update(|cx| {
18906 SettingsStore::update_global(cx, |store, cx| {
18907 store.update_user_settings::<ProjectSettings>(cx, f);
18908 });
18909 });
18910}
18911
18912pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
18913 cx.update(|cx| {
18914 assets::Assets.load_test_fonts(cx);
18915 let store = SettingsStore::test(cx);
18916 cx.set_global(store);
18917 theme::init(theme::LoadThemes::JustBase, cx);
18918 release_channel::init(SemanticVersion::default(), cx);
18919 client::init_settings(cx);
18920 language::init(cx);
18921 Project::init_settings(cx);
18922 workspace::init_settings(cx);
18923 crate::init(cx);
18924 });
18925
18926 update_test_language_settings(cx, f);
18927}
18928
18929#[track_caller]
18930fn assert_hunk_revert(
18931 not_reverted_text_with_selections: &str,
18932 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
18933 expected_reverted_text_with_selections: &str,
18934 base_text: &str,
18935 cx: &mut EditorLspTestContext,
18936) {
18937 cx.set_state(not_reverted_text_with_selections);
18938 cx.set_head_text(base_text);
18939 cx.executor().run_until_parked();
18940
18941 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
18942 let snapshot = editor.snapshot(window, cx);
18943 let reverted_hunk_statuses = snapshot
18944 .buffer_snapshot
18945 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
18946 .map(|hunk| hunk.status().kind)
18947 .collect::<Vec<_>>();
18948
18949 editor.git_restore(&Default::default(), window, cx);
18950 reverted_hunk_statuses
18951 });
18952 cx.executor().run_until_parked();
18953 cx.assert_editor_state(expected_reverted_text_with_selections);
18954 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
18955}