1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, FormatterList, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
59 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
60};
61
62#[gpui::test]
63fn test_edit_events(cx: &mut TestAppContext) {
64 init_test(cx, |_| {});
65
66 let buffer = cx.new(|cx| {
67 let mut buffer = language::Buffer::local("123456", cx);
68 buffer.set_group_interval(Duration::from_secs(1));
69 buffer
70 });
71
72 let events = Rc::new(RefCell::new(Vec::new()));
73 let editor1 = cx.add_window({
74 let events = events.clone();
75 |window, cx| {
76 let entity = cx.entity().clone();
77 cx.subscribe_in(
78 &entity,
79 window,
80 move |_, _, event: &EditorEvent, _, _| match event {
81 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
82 EditorEvent::BufferEdited => {
83 events.borrow_mut().push(("editor1", "buffer edited"))
84 }
85 _ => {}
86 },
87 )
88 .detach();
89 Editor::for_buffer(buffer.clone(), None, window, cx)
90 }
91 });
92
93 let editor2 = cx.add_window({
94 let events = events.clone();
95 |window, cx| {
96 cx.subscribe_in(
97 &cx.entity().clone(),
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor2", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
113
114 // Mutating editor 1 will emit an `Edited` event only for that editor.
115 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
116 assert_eq!(
117 mem::take(&mut *events.borrow_mut()),
118 [
119 ("editor1", "edited"),
120 ("editor1", "buffer edited"),
121 ("editor2", "buffer edited"),
122 ]
123 );
124
125 // Mutating editor 2 will emit an `Edited` event only for that editor.
126 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
127 assert_eq!(
128 mem::take(&mut *events.borrow_mut()),
129 [
130 ("editor2", "edited"),
131 ("editor1", "buffer edited"),
132 ("editor2", "buffer edited"),
133 ]
134 );
135
136 // Undoing on editor 1 will emit an `Edited` event only for that editor.
137 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
138 assert_eq!(
139 mem::take(&mut *events.borrow_mut()),
140 [
141 ("editor1", "edited"),
142 ("editor1", "buffer edited"),
143 ("editor2", "buffer edited"),
144 ]
145 );
146
147 // Redoing on editor 1 will emit an `Edited` event only for that editor.
148 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
149 assert_eq!(
150 mem::take(&mut *events.borrow_mut()),
151 [
152 ("editor1", "edited"),
153 ("editor1", "buffer edited"),
154 ("editor2", "buffer edited"),
155 ]
156 );
157
158 // Undoing on editor 2 will emit an `Edited` event only for that editor.
159 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
160 assert_eq!(
161 mem::take(&mut *events.borrow_mut()),
162 [
163 ("editor2", "edited"),
164 ("editor1", "buffer edited"),
165 ("editor2", "buffer edited"),
166 ]
167 );
168
169 // Redoing on editor 2 will emit an `Edited` event only for that editor.
170 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
171 assert_eq!(
172 mem::take(&mut *events.borrow_mut()),
173 [
174 ("editor2", "edited"),
175 ("editor1", "buffer edited"),
176 ("editor2", "buffer edited"),
177 ]
178 );
179
180 // No event is emitted when the mutation is a no-op.
181 _ = editor2.update(cx, |editor, window, cx| {
182 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
183 s.select_ranges([0..0])
184 });
185
186 editor.backspace(&Backspace, window, cx);
187 });
188 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
189}
190
191#[gpui::test]
192fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
193 init_test(cx, |_| {});
194
195 let mut now = Instant::now();
196 let group_interval = Duration::from_millis(1);
197 let buffer = cx.new(|cx| {
198 let mut buf = language::Buffer::local("123456", cx);
199 buf.set_group_interval(group_interval);
200 buf
201 });
202 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
203 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
204
205 _ = editor.update(cx, |editor, window, cx| {
206 editor.start_transaction_at(now, window, cx);
207 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
208 s.select_ranges([2..4])
209 });
210
211 editor.insert("cd", window, cx);
212 editor.end_transaction_at(now, cx);
213 assert_eq!(editor.text(cx), "12cd56");
214 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
215
216 editor.start_transaction_at(now, window, cx);
217 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
218 s.select_ranges([4..5])
219 });
220 editor.insert("e", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cde6");
223 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
224
225 now += group_interval + Duration::from_millis(1);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([2..2])
228 });
229
230 // Simulate an edit in another editor
231 buffer.update(cx, |buffer, cx| {
232 buffer.start_transaction_at(now, cx);
233 buffer.edit([(0..1, "a")], None, cx);
234 buffer.edit([(1..1, "b")], None, cx);
235 buffer.end_transaction_at(now, cx);
236 });
237
238 assert_eq!(editor.text(cx), "ab2cde6");
239 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
240
241 // Last transaction happened past the group interval in a different editor.
242 // Undo it individually and don't restore selections.
243 editor.undo(&Undo, window, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
246
247 // First two transactions happened within the group interval in this editor.
248 // Undo them together and restore selections.
249 editor.undo(&Undo, window, cx);
250 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
251 assert_eq!(editor.text(cx), "123456");
252 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
253
254 // Redo the first two transactions together.
255 editor.redo(&Redo, window, cx);
256 assert_eq!(editor.text(cx), "12cde6");
257 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
258
259 // Redo the last transaction on its own.
260 editor.redo(&Redo, window, cx);
261 assert_eq!(editor.text(cx), "ab2cde6");
262 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
263
264 // Test empty transactions.
265 editor.start_transaction_at(now, window, cx);
266 editor.end_transaction_at(now, cx);
267 editor.undo(&Undo, window, cx);
268 assert_eq!(editor.text(cx), "12cde6");
269 });
270}
271
272#[gpui::test]
273fn test_ime_composition(cx: &mut TestAppContext) {
274 init_test(cx, |_| {});
275
276 let buffer = cx.new(|cx| {
277 let mut buffer = language::Buffer::local("abcde", cx);
278 // Ensure automatic grouping doesn't occur.
279 buffer.set_group_interval(Duration::ZERO);
280 buffer
281 });
282
283 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
284 cx.add_window(|window, cx| {
285 let mut editor = build_editor(buffer.clone(), window, cx);
286
287 // Start a new IME composition.
288 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
289 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
291 assert_eq!(editor.text(cx), "äbcde");
292 assert_eq!(
293 editor.marked_text_ranges(cx),
294 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
295 );
296
297 // Finalize IME composition.
298 editor.replace_text_in_range(None, "ā", window, cx);
299 assert_eq!(editor.text(cx), "ābcde");
300 assert_eq!(editor.marked_text_ranges(cx), None);
301
302 // IME composition edits are grouped and are undone/redone at once.
303 editor.undo(&Default::default(), window, cx);
304 assert_eq!(editor.text(cx), "abcde");
305 assert_eq!(editor.marked_text_ranges(cx), None);
306 editor.redo(&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.
311 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
312 assert_eq!(
313 editor.marked_text_ranges(cx),
314 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
315 );
316
317 // Undoing during an IME composition cancels it.
318 editor.undo(&Default::default(), window, cx);
319 assert_eq!(editor.text(cx), "ābcde");
320 assert_eq!(editor.marked_text_ranges(cx), None);
321
322 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
323 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
324 assert_eq!(editor.text(cx), "ābcdè");
325 assert_eq!(
326 editor.marked_text_ranges(cx),
327 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
328 );
329
330 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
331 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
332 assert_eq!(editor.text(cx), "ābcdę");
333 assert_eq!(editor.marked_text_ranges(cx), None);
334
335 // Start a new IME composition with multiple cursors.
336 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
337 s.select_ranges([
338 OffsetUtf16(1)..OffsetUtf16(1),
339 OffsetUtf16(3)..OffsetUtf16(3),
340 OffsetUtf16(5)..OffsetUtf16(5),
341 ])
342 });
343 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
344 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
345 assert_eq!(
346 editor.marked_text_ranges(cx),
347 Some(vec![
348 OffsetUtf16(0)..OffsetUtf16(3),
349 OffsetUtf16(4)..OffsetUtf16(7),
350 OffsetUtf16(8)..OffsetUtf16(11)
351 ])
352 );
353
354 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
355 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
356 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
357 assert_eq!(
358 editor.marked_text_ranges(cx),
359 Some(vec![
360 OffsetUtf16(1)..OffsetUtf16(2),
361 OffsetUtf16(5)..OffsetUtf16(6),
362 OffsetUtf16(9)..OffsetUtf16(10)
363 ])
364 );
365
366 // Finalize IME composition with multiple cursors.
367 editor.replace_text_in_range(Some(9..10), "2", window, cx);
368 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
369 assert_eq!(editor.marked_text_ranges(cx), None);
370
371 editor
372 });
373}
374
375#[gpui::test]
376fn test_selection_with_mouse(cx: &mut TestAppContext) {
377 init_test(cx, |_| {});
378
379 let editor = cx.add_window(|window, cx| {
380 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
381 build_editor(buffer, window, cx)
382 });
383
384 _ = editor.update(cx, |editor, window, cx| {
385 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
386 });
387 assert_eq!(
388 editor
389 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
390 .unwrap(),
391 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
392 );
393
394 _ = editor.update(cx, |editor, window, cx| {
395 editor.update_selection(
396 DisplayPoint::new(DisplayRow(3), 3),
397 0,
398 gpui::Point::<f32>::default(),
399 window,
400 cx,
401 );
402 });
403
404 assert_eq!(
405 editor
406 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
407 .unwrap(),
408 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
409 );
410
411 _ = editor.update(cx, |editor, window, cx| {
412 editor.update_selection(
413 DisplayPoint::new(DisplayRow(1), 1),
414 0,
415 gpui::Point::<f32>::default(),
416 window,
417 cx,
418 );
419 });
420
421 assert_eq!(
422 editor
423 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
424 .unwrap(),
425 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
426 );
427
428 _ = editor.update(cx, |editor, window, cx| {
429 editor.end_selection(window, cx);
430 editor.update_selection(
431 DisplayPoint::new(DisplayRow(3), 3),
432 0,
433 gpui::Point::<f32>::default(),
434 window,
435 cx,
436 );
437 });
438
439 assert_eq!(
440 editor
441 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
442 .unwrap(),
443 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
444 );
445
446 _ = editor.update(cx, |editor, window, cx| {
447 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
448 editor.update_selection(
449 DisplayPoint::new(DisplayRow(0), 0),
450 0,
451 gpui::Point::<f32>::default(),
452 window,
453 cx,
454 );
455 });
456
457 assert_eq!(
458 editor
459 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
460 .unwrap(),
461 [
462 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
463 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
464 ]
465 );
466
467 _ = editor.update(cx, |editor, window, cx| {
468 editor.end_selection(window, cx);
469 });
470
471 assert_eq!(
472 editor
473 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
474 .unwrap(),
475 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
476 );
477}
478
479#[gpui::test]
480fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
481 init_test(cx, |_| {});
482
483 let editor = cx.add_window(|window, cx| {
484 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
485 build_editor(buffer, window, cx)
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
490 });
491
492 _ = editor.update(cx, |editor, window, cx| {
493 editor.end_selection(window, cx);
494 });
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.end_selection(window, cx);
502 });
503
504 assert_eq!(
505 editor
506 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
507 .unwrap(),
508 [
509 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
510 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
511 ]
512 );
513
514 _ = editor.update(cx, |editor, window, cx| {
515 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
516 });
517
518 _ = editor.update(cx, |editor, window, cx| {
519 editor.end_selection(window, cx);
520 });
521
522 assert_eq!(
523 editor
524 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
525 .unwrap(),
526 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
527 );
528}
529
530#[gpui::test]
531fn test_canceling_pending_selection(cx: &mut TestAppContext) {
532 init_test(cx, |_| {});
533
534 let editor = cx.add_window(|window, cx| {
535 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
536 build_editor(buffer, window, cx)
537 });
538
539 _ = editor.update(cx, |editor, window, cx| {
540 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
541 assert_eq!(
542 editor.selections.display_ranges(cx),
543 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
544 );
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.update_selection(
549 DisplayPoint::new(DisplayRow(3), 3),
550 0,
551 gpui::Point::<f32>::default(),
552 window,
553 cx,
554 );
555 assert_eq!(
556 editor.selections.display_ranges(cx),
557 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
558 );
559 });
560
561 _ = editor.update(cx, |editor, window, cx| {
562 editor.cancel(&Cancel, window, cx);
563 editor.update_selection(
564 DisplayPoint::new(DisplayRow(1), 1),
565 0,
566 gpui::Point::<f32>::default(),
567 window,
568 cx,
569 );
570 assert_eq!(
571 editor.selections.display_ranges(cx),
572 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
573 );
574 });
575}
576
577#[gpui::test]
578fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
579 init_test(cx, |_| {});
580
581 let editor = cx.add_window(|window, cx| {
582 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
583 build_editor(buffer, window, cx)
584 });
585
586 _ = editor.update(cx, |editor, window, cx| {
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_down(&Default::default(), window, cx);
594 assert_eq!(
595 editor.selections.display_ranges(cx),
596 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
597 );
598
599 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
600 assert_eq!(
601 editor.selections.display_ranges(cx),
602 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
603 );
604
605 editor.move_up(&Default::default(), window, cx);
606 assert_eq!(
607 editor.selections.display_ranges(cx),
608 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
609 );
610 });
611}
612
613#[gpui::test]
614fn test_clone(cx: &mut TestAppContext) {
615 init_test(cx, |_| {});
616
617 let (text, selection_ranges) = marked_text_ranges(
618 indoc! {"
619 one
620 two
621 threeˇ
622 four
623 fiveˇ
624 "},
625 true,
626 );
627
628 let editor = cx.add_window(|window, cx| {
629 let buffer = MultiBuffer::build_simple(&text, cx);
630 build_editor(buffer, window, cx)
631 });
632
633 _ = editor.update(cx, |editor, window, cx| {
634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
635 s.select_ranges(selection_ranges.clone())
636 });
637 editor.fold_creases(
638 vec![
639 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
640 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
641 ],
642 true,
643 window,
644 cx,
645 );
646 });
647
648 let cloned_editor = editor
649 .update(cx, |editor, _, cx| {
650 cx.open_window(Default::default(), |window, cx| {
651 cx.new(|cx| editor.clone(window, cx))
652 })
653 })
654 .unwrap()
655 .unwrap();
656
657 let snapshot = editor
658 .update(cx, |e, window, cx| e.snapshot(window, cx))
659 .unwrap();
660 let cloned_snapshot = cloned_editor
661 .update(cx, |e, window, cx| e.snapshot(window, cx))
662 .unwrap();
663
664 assert_eq!(
665 cloned_editor
666 .update(cx, |e, _, cx| e.display_text(cx))
667 .unwrap(),
668 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
669 );
670 assert_eq!(
671 cloned_snapshot
672 .folds_in_range(0..text.len())
673 .collect::<Vec<_>>(),
674 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
675 );
676 assert_set_eq!(
677 cloned_editor
678 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
679 .unwrap(),
680 editor
681 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
682 .unwrap()
683 );
684 assert_set_eq!(
685 cloned_editor
686 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
687 .unwrap(),
688 editor
689 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
690 .unwrap()
691 );
692}
693
694#[gpui::test]
695async fn test_navigation_history(cx: &mut TestAppContext) {
696 init_test(cx, |_| {});
697
698 use workspace::item::Item;
699
700 let fs = FakeFs::new(cx.executor());
701 let project = Project::test(fs, [], cx).await;
702 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
703 let pane = workspace
704 .update(cx, |workspace, _, _| workspace.active_pane().clone())
705 .unwrap();
706
707 _ = workspace.update(cx, |_v, window, cx| {
708 cx.new(|cx| {
709 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
710 let mut editor = build_editor(buffer.clone(), window, cx);
711 let handle = cx.entity();
712 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
713
714 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
715 editor.nav_history.as_mut().unwrap().pop_backward(cx)
716 }
717
718 // Move the cursor a small distance.
719 // Nothing is added to the navigation history.
720 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
721 s.select_display_ranges([
722 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
723 ])
724 });
725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
726 s.select_display_ranges([
727 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
728 ])
729 });
730 assert!(pop_history(&mut editor, cx).is_none());
731
732 // Move the cursor a large distance.
733 // The history can jump back to the previous position.
734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
735 s.select_display_ranges([
736 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
737 ])
738 });
739 let nav_entry = pop_history(&mut editor, cx).unwrap();
740 editor.navigate(nav_entry.data.unwrap(), window, cx);
741 assert_eq!(nav_entry.item.id(), cx.entity_id());
742 assert_eq!(
743 editor.selections.display_ranges(cx),
744 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
745 );
746 assert!(pop_history(&mut editor, cx).is_none());
747
748 // Move the cursor a small distance via the mouse.
749 // Nothing is added to the navigation history.
750 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
751 editor.end_selection(window, cx);
752 assert_eq!(
753 editor.selections.display_ranges(cx),
754 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
755 );
756 assert!(pop_history(&mut editor, cx).is_none());
757
758 // Move the cursor a large distance via the mouse.
759 // The history can jump back to the previous position.
760 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
761 editor.end_selection(window, cx);
762 assert_eq!(
763 editor.selections.display_ranges(cx),
764 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
765 );
766 let nav_entry = pop_history(&mut editor, cx).unwrap();
767 editor.navigate(nav_entry.data.unwrap(), window, cx);
768 assert_eq!(nav_entry.item.id(), cx.entity_id());
769 assert_eq!(
770 editor.selections.display_ranges(cx),
771 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
772 );
773 assert!(pop_history(&mut editor, cx).is_none());
774
775 // Set scroll position to check later
776 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
777 let original_scroll_position = editor.scroll_manager.anchor();
778
779 // Jump to the end of the document and adjust scroll
780 editor.move_to_end(&MoveToEnd, window, cx);
781 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
782 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
783
784 let nav_entry = pop_history(&mut editor, cx).unwrap();
785 editor.navigate(nav_entry.data.unwrap(), window, cx);
786 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
787
788 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
789 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
790 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
791 let invalid_point = Point::new(9999, 0);
792 editor.navigate(
793 Box::new(NavigationData {
794 cursor_anchor: invalid_anchor,
795 cursor_position: invalid_point,
796 scroll_anchor: ScrollAnchor {
797 anchor: invalid_anchor,
798 offset: Default::default(),
799 },
800 scroll_top_row: invalid_point.row,
801 }),
802 window,
803 cx,
804 );
805 assert_eq!(
806 editor.selections.display_ranges(cx),
807 &[editor.max_point(cx)..editor.max_point(cx)]
808 );
809 assert_eq!(
810 editor.scroll_position(cx),
811 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
812 );
813
814 editor
815 })
816 });
817}
818
819#[gpui::test]
820fn test_cancel(cx: &mut TestAppContext) {
821 init_test(cx, |_| {});
822
823 let editor = cx.add_window(|window, cx| {
824 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
825 build_editor(buffer, window, cx)
826 });
827
828 _ = editor.update(cx, |editor, window, cx| {
829 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
830 editor.update_selection(
831 DisplayPoint::new(DisplayRow(1), 1),
832 0,
833 gpui::Point::<f32>::default(),
834 window,
835 cx,
836 );
837 editor.end_selection(window, cx);
838
839 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
840 editor.update_selection(
841 DisplayPoint::new(DisplayRow(0), 3),
842 0,
843 gpui::Point::<f32>::default(),
844 window,
845 cx,
846 );
847 editor.end_selection(window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 [
851 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
852 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
853 ]
854 );
855 });
856
857 _ = editor.update(cx, |editor, window, cx| {
858 editor.cancel(&Cancel, window, cx);
859 assert_eq!(
860 editor.selections.display_ranges(cx),
861 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
862 );
863 });
864
865 _ = editor.update(cx, |editor, window, cx| {
866 editor.cancel(&Cancel, window, cx);
867 assert_eq!(
868 editor.selections.display_ranges(cx),
869 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
870 );
871 });
872}
873
874#[gpui::test]
875fn test_fold_action(cx: &mut TestAppContext) {
876 init_test(cx, |_| {});
877
878 let editor = cx.add_window(|window, cx| {
879 let buffer = MultiBuffer::build_simple(
880 &"
881 impl Foo {
882 // Hello!
883
884 fn a() {
885 1
886 }
887
888 fn b() {
889 2
890 }
891
892 fn c() {
893 3
894 }
895 }
896 "
897 .unindent(),
898 cx,
899 );
900 build_editor(buffer.clone(), window, cx)
901 });
902
903 _ = editor.update(cx, |editor, window, cx| {
904 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
905 s.select_display_ranges([
906 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
907 ]);
908 });
909 editor.fold(&Fold, window, cx);
910 assert_eq!(
911 editor.display_text(cx),
912 "
913 impl Foo {
914 // Hello!
915
916 fn a() {
917 1
918 }
919
920 fn b() {⋯
921 }
922
923 fn c() {⋯
924 }
925 }
926 "
927 .unindent(),
928 );
929
930 editor.fold(&Fold, window, cx);
931 assert_eq!(
932 editor.display_text(cx),
933 "
934 impl Foo {⋯
935 }
936 "
937 .unindent(),
938 );
939
940 editor.unfold_lines(&UnfoldLines, window, cx);
941 assert_eq!(
942 editor.display_text(cx),
943 "
944 impl Foo {
945 // Hello!
946
947 fn a() {
948 1
949 }
950
951 fn b() {⋯
952 }
953
954 fn c() {⋯
955 }
956 }
957 "
958 .unindent(),
959 );
960
961 editor.unfold_lines(&UnfoldLines, window, cx);
962 assert_eq!(
963 editor.display_text(cx),
964 editor.buffer.read(cx).read(cx).text()
965 );
966 });
967}
968
969#[gpui::test]
970fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
971 init_test(cx, |_| {});
972
973 let editor = cx.add_window(|window, cx| {
974 let buffer = MultiBuffer::build_simple(
975 &"
976 class Foo:
977 # Hello!
978
979 def a():
980 print(1)
981
982 def b():
983 print(2)
984
985 def c():
986 print(3)
987 "
988 .unindent(),
989 cx,
990 );
991 build_editor(buffer.clone(), window, cx)
992 });
993
994 _ = editor.update(cx, |editor, window, cx| {
995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
996 s.select_display_ranges([
997 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
998 ]);
999 });
1000 editor.fold(&Fold, window, cx);
1001 assert_eq!(
1002 editor.display_text(cx),
1003 "
1004 class Foo:
1005 # Hello!
1006
1007 def a():
1008 print(1)
1009
1010 def b():⋯
1011
1012 def c():⋯
1013 "
1014 .unindent(),
1015 );
1016
1017 editor.fold(&Fold, window, cx);
1018 assert_eq!(
1019 editor.display_text(cx),
1020 "
1021 class Foo:⋯
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.unfold_lines(&UnfoldLines, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 "
1030 class Foo:
1031 # Hello!
1032
1033 def a():
1034 print(1)
1035
1036 def b():⋯
1037
1038 def c():⋯
1039 "
1040 .unindent(),
1041 );
1042
1043 editor.unfold_lines(&UnfoldLines, window, cx);
1044 assert_eq!(
1045 editor.display_text(cx),
1046 editor.buffer.read(cx).read(cx).text()
1047 );
1048 });
1049}
1050
1051#[gpui::test]
1052fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1053 init_test(cx, |_| {});
1054
1055 let editor = cx.add_window(|window, cx| {
1056 let buffer = MultiBuffer::build_simple(
1057 &"
1058 class Foo:
1059 # Hello!
1060
1061 def a():
1062 print(1)
1063
1064 def b():
1065 print(2)
1066
1067
1068 def c():
1069 print(3)
1070
1071
1072 "
1073 .unindent(),
1074 cx,
1075 );
1076 build_editor(buffer.clone(), window, cx)
1077 });
1078
1079 _ = editor.update(cx, |editor, window, cx| {
1080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1081 s.select_display_ranges([
1082 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1083 ]);
1084 });
1085 editor.fold(&Fold, window, cx);
1086 assert_eq!(
1087 editor.display_text(cx),
1088 "
1089 class Foo:
1090 # Hello!
1091
1092 def a():
1093 print(1)
1094
1095 def b():⋯
1096
1097
1098 def c():⋯
1099
1100
1101 "
1102 .unindent(),
1103 );
1104
1105 editor.fold(&Fold, window, cx);
1106 assert_eq!(
1107 editor.display_text(cx),
1108 "
1109 class Foo:⋯
1110
1111
1112 "
1113 .unindent(),
1114 );
1115
1116 editor.unfold_lines(&UnfoldLines, window, cx);
1117 assert_eq!(
1118 editor.display_text(cx),
1119 "
1120 class Foo:
1121 # Hello!
1122
1123 def a():
1124 print(1)
1125
1126 def b():⋯
1127
1128
1129 def c():⋯
1130
1131
1132 "
1133 .unindent(),
1134 );
1135
1136 editor.unfold_lines(&UnfoldLines, window, cx);
1137 assert_eq!(
1138 editor.display_text(cx),
1139 editor.buffer.read(cx).read(cx).text()
1140 );
1141 });
1142}
1143
1144#[gpui::test]
1145fn test_fold_at_level(cx: &mut TestAppContext) {
1146 init_test(cx, |_| {});
1147
1148 let editor = cx.add_window(|window, cx| {
1149 let buffer = MultiBuffer::build_simple(
1150 &"
1151 class Foo:
1152 # Hello!
1153
1154 def a():
1155 print(1)
1156
1157 def b():
1158 print(2)
1159
1160
1161 class Bar:
1162 # World!
1163
1164 def a():
1165 print(1)
1166
1167 def b():
1168 print(2)
1169
1170
1171 "
1172 .unindent(),
1173 cx,
1174 );
1175 build_editor(buffer.clone(), window, cx)
1176 });
1177
1178 _ = editor.update(cx, |editor, window, cx| {
1179 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1180 assert_eq!(
1181 editor.display_text(cx),
1182 "
1183 class Foo:
1184 # Hello!
1185
1186 def a():⋯
1187
1188 def b():⋯
1189
1190
1191 class Bar:
1192 # World!
1193
1194 def a():⋯
1195
1196 def b():⋯
1197
1198
1199 "
1200 .unindent(),
1201 );
1202
1203 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1204 assert_eq!(
1205 editor.display_text(cx),
1206 "
1207 class Foo:⋯
1208
1209
1210 class Bar:⋯
1211
1212
1213 "
1214 .unindent(),
1215 );
1216
1217 editor.unfold_all(&UnfoldAll, window, cx);
1218 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1219 assert_eq!(
1220 editor.display_text(cx),
1221 "
1222 class Foo:
1223 # Hello!
1224
1225 def a():
1226 print(1)
1227
1228 def b():
1229 print(2)
1230
1231
1232 class Bar:
1233 # World!
1234
1235 def a():
1236 print(1)
1237
1238 def b():
1239 print(2)
1240
1241
1242 "
1243 .unindent(),
1244 );
1245
1246 assert_eq!(
1247 editor.display_text(cx),
1248 editor.buffer.read(cx).read(cx).text()
1249 );
1250 });
1251}
1252
1253#[gpui::test]
1254fn test_move_cursor(cx: &mut TestAppContext) {
1255 init_test(cx, |_| {});
1256
1257 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1258 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1259
1260 buffer.update(cx, |buffer, cx| {
1261 buffer.edit(
1262 vec![
1263 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1264 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1265 ],
1266 None,
1267 cx,
1268 );
1269 });
1270 _ = editor.update(cx, |editor, window, cx| {
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1274 );
1275
1276 editor.move_down(&MoveDown, 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_right(&MoveRight, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1286 );
1287
1288 editor.move_left(&MoveLeft, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1292 );
1293
1294 editor.move_up(&MoveUp, 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.move_to_end(&MoveToEnd, window, cx);
1301 assert_eq!(
1302 editor.selections.display_ranges(cx),
1303 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1304 );
1305
1306 editor.move_to_beginning(&MoveToBeginning, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1310 );
1311
1312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1313 s.select_display_ranges([
1314 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1315 ]);
1316 });
1317 editor.select_to_beginning(&SelectToBeginning, window, cx);
1318 assert_eq!(
1319 editor.selections.display_ranges(cx),
1320 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1321 );
1322
1323 editor.select_to_end(&SelectToEnd, window, cx);
1324 assert_eq!(
1325 editor.selections.display_ranges(cx),
1326 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1327 );
1328 });
1329}
1330
1331#[gpui::test]
1332fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1333 init_test(cx, |_| {});
1334
1335 let editor = cx.add_window(|window, cx| {
1336 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1337 build_editor(buffer.clone(), window, cx)
1338 });
1339
1340 assert_eq!('🟥'.len_utf8(), 4);
1341 assert_eq!('α'.len_utf8(), 2);
1342
1343 _ = editor.update(cx, |editor, window, cx| {
1344 editor.fold_creases(
1345 vec![
1346 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1347 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1349 ],
1350 true,
1351 window,
1352 cx,
1353 );
1354 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1355
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥".len())]
1360 );
1361 editor.move_right(&MoveRight, window, cx);
1362 assert_eq!(
1363 editor.selections.display_ranges(cx),
1364 &[empty_range(0, "🟥🟧".len())]
1365 );
1366 editor.move_right(&MoveRight, window, cx);
1367 assert_eq!(
1368 editor.selections.display_ranges(cx),
1369 &[empty_range(0, "🟥🟧⋯".len())]
1370 );
1371
1372 editor.move_down(&MoveDown, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab⋯e".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "ab⋯".len())]
1381 );
1382 editor.move_left(&MoveLeft, window, cx);
1383 assert_eq!(
1384 editor.selections.display_ranges(cx),
1385 &[empty_range(1, "ab".len())]
1386 );
1387 editor.move_left(&MoveLeft, window, cx);
1388 assert_eq!(
1389 editor.selections.display_ranges(cx),
1390 &[empty_range(1, "a".len())]
1391 );
1392
1393 editor.move_down(&MoveDown, 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 editor.move_right(&MoveRight, window, cx);
1404 assert_eq!(
1405 editor.selections.display_ranges(cx),
1406 &[empty_range(2, "αβ⋯".len())]
1407 );
1408 editor.move_right(&MoveRight, window, cx);
1409 assert_eq!(
1410 editor.selections.display_ranges(cx),
1411 &[empty_range(2, "αβ⋯ε".len())]
1412 );
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 editor.move_down(&MoveDown, window, cx);
1420 assert_eq!(
1421 editor.selections.display_ranges(cx),
1422 &[empty_range(2, "αβ⋯ε".len())]
1423 );
1424 editor.move_up(&MoveUp, window, cx);
1425 assert_eq!(
1426 editor.selections.display_ranges(cx),
1427 &[empty_range(1, "ab⋯e".len())]
1428 );
1429
1430 editor.move_up(&MoveUp, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "🟥🟧".len())]
1434 );
1435 editor.move_left(&MoveLeft, window, cx);
1436 assert_eq!(
1437 editor.selections.display_ranges(cx),
1438 &[empty_range(0, "🟥".len())]
1439 );
1440 editor.move_left(&MoveLeft, window, cx);
1441 assert_eq!(
1442 editor.selections.display_ranges(cx),
1443 &[empty_range(0, "".len())]
1444 );
1445 });
1446}
1447
1448#[gpui::test]
1449fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1450 init_test(cx, |_| {});
1451
1452 let editor = cx.add_window(|window, cx| {
1453 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1454 build_editor(buffer.clone(), window, cx)
1455 });
1456 _ = editor.update(cx, |editor, window, cx| {
1457 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1458 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1459 });
1460
1461 // moving above start of document should move selection to start of document,
1462 // but the next move down should still be at the original goal_x
1463 editor.move_up(&MoveUp, window, cx);
1464 assert_eq!(
1465 editor.selections.display_ranges(cx),
1466 &[empty_range(0, "".len())]
1467 );
1468
1469 editor.move_down(&MoveDown, window, cx);
1470 assert_eq!(
1471 editor.selections.display_ranges(cx),
1472 &[empty_range(1, "abcd".len())]
1473 );
1474
1475 editor.move_down(&MoveDown, window, cx);
1476 assert_eq!(
1477 editor.selections.display_ranges(cx),
1478 &[empty_range(2, "αβγ".len())]
1479 );
1480
1481 editor.move_down(&MoveDown, window, cx);
1482 assert_eq!(
1483 editor.selections.display_ranges(cx),
1484 &[empty_range(3, "abcd".len())]
1485 );
1486
1487 editor.move_down(&MoveDown, window, cx);
1488 assert_eq!(
1489 editor.selections.display_ranges(cx),
1490 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1491 );
1492
1493 // moving past end of document should not change goal_x
1494 editor.move_down(&MoveDown, window, cx);
1495 assert_eq!(
1496 editor.selections.display_ranges(cx),
1497 &[empty_range(5, "".len())]
1498 );
1499
1500 editor.move_down(&MoveDown, window, cx);
1501 assert_eq!(
1502 editor.selections.display_ranges(cx),
1503 &[empty_range(5, "".len())]
1504 );
1505
1506 editor.move_up(&MoveUp, window, cx);
1507 assert_eq!(
1508 editor.selections.display_ranges(cx),
1509 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1510 );
1511
1512 editor.move_up(&MoveUp, window, cx);
1513 assert_eq!(
1514 editor.selections.display_ranges(cx),
1515 &[empty_range(3, "abcd".len())]
1516 );
1517
1518 editor.move_up(&MoveUp, window, cx);
1519 assert_eq!(
1520 editor.selections.display_ranges(cx),
1521 &[empty_range(2, "αβγ".len())]
1522 );
1523 });
1524}
1525
1526#[gpui::test]
1527fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1528 init_test(cx, |_| {});
1529 let move_to_beg = MoveToBeginningOfLine {
1530 stop_at_soft_wraps: true,
1531 stop_at_indent: true,
1532 };
1533
1534 let delete_to_beg = DeleteToBeginningOfLine {
1535 stop_at_indent: false,
1536 };
1537
1538 let move_to_end = MoveToEndOfLine {
1539 stop_at_soft_wraps: true,
1540 };
1541
1542 let editor = cx.add_window(|window, cx| {
1543 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1544 build_editor(buffer, window, cx)
1545 });
1546 _ = editor.update(cx, |editor, window, cx| {
1547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1548 s.select_display_ranges([
1549 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1550 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1551 ]);
1552 });
1553 });
1554
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[
1560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1561 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1562 ]
1563 );
1564 });
1565
1566 _ = editor.update(cx, |editor, window, cx| {
1567 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[
1571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1572 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1573 ]
1574 );
1575 });
1576
1577 _ = editor.update(cx, |editor, window, cx| {
1578 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1579 assert_eq!(
1580 editor.selections.display_ranges(cx),
1581 &[
1582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1583 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1584 ]
1585 );
1586 });
1587
1588 _ = editor.update(cx, |editor, window, cx| {
1589 editor.move_to_end_of_line(&move_to_end, window, cx);
1590 assert_eq!(
1591 editor.selections.display_ranges(cx),
1592 &[
1593 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1594 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1595 ]
1596 );
1597 });
1598
1599 // Moving to the end of line again is a no-op.
1600 _ = editor.update(cx, |editor, window, cx| {
1601 editor.move_to_end_of_line(&move_to_end, window, cx);
1602 assert_eq!(
1603 editor.selections.display_ranges(cx),
1604 &[
1605 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1606 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1607 ]
1608 );
1609 });
1610
1611 _ = editor.update(cx, |editor, window, cx| {
1612 editor.move_left(&MoveLeft, window, cx);
1613 editor.select_to_beginning_of_line(
1614 &SelectToBeginningOfLine {
1615 stop_at_soft_wraps: true,
1616 stop_at_indent: true,
1617 },
1618 window,
1619 cx,
1620 );
1621 assert_eq!(
1622 editor.selections.display_ranges(cx),
1623 &[
1624 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1625 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1626 ]
1627 );
1628 });
1629
1630 _ = editor.update(cx, |editor, window, cx| {
1631 editor.select_to_beginning_of_line(
1632 &SelectToBeginningOfLine {
1633 stop_at_soft_wraps: true,
1634 stop_at_indent: true,
1635 },
1636 window,
1637 cx,
1638 );
1639 assert_eq!(
1640 editor.selections.display_ranges(cx),
1641 &[
1642 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1643 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1644 ]
1645 );
1646 });
1647
1648 _ = editor.update(cx, |editor, window, cx| {
1649 editor.select_to_beginning_of_line(
1650 &SelectToBeginningOfLine {
1651 stop_at_soft_wraps: true,
1652 stop_at_indent: true,
1653 },
1654 window,
1655 cx,
1656 );
1657 assert_eq!(
1658 editor.selections.display_ranges(cx),
1659 &[
1660 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1661 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1662 ]
1663 );
1664 });
1665
1666 _ = editor.update(cx, |editor, window, cx| {
1667 editor.select_to_end_of_line(
1668 &SelectToEndOfLine {
1669 stop_at_soft_wraps: true,
1670 },
1671 window,
1672 cx,
1673 );
1674 assert_eq!(
1675 editor.selections.display_ranges(cx),
1676 &[
1677 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1678 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1679 ]
1680 );
1681 });
1682
1683 _ = editor.update(cx, |editor, window, cx| {
1684 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1685 assert_eq!(editor.display_text(cx), "ab\n de");
1686 assert_eq!(
1687 editor.selections.display_ranges(cx),
1688 &[
1689 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1690 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1691 ]
1692 );
1693 });
1694
1695 _ = editor.update(cx, |editor, window, cx| {
1696 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1697 assert_eq!(editor.display_text(cx), "\n");
1698 assert_eq!(
1699 editor.selections.display_ranges(cx),
1700 &[
1701 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1702 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1703 ]
1704 );
1705 });
1706}
1707
1708#[gpui::test]
1709fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1710 init_test(cx, |_| {});
1711 let move_to_beg = MoveToBeginningOfLine {
1712 stop_at_soft_wraps: false,
1713 stop_at_indent: false,
1714 };
1715
1716 let move_to_end = MoveToEndOfLine {
1717 stop_at_soft_wraps: false,
1718 };
1719
1720 let editor = cx.add_window(|window, cx| {
1721 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1722 build_editor(buffer, window, cx)
1723 });
1724
1725 _ = editor.update(cx, |editor, window, cx| {
1726 editor.set_wrap_width(Some(140.0.into()), cx);
1727
1728 // We expect the following lines after wrapping
1729 // ```
1730 // thequickbrownfox
1731 // jumpedoverthelazydo
1732 // gs
1733 // ```
1734 // The final `gs` was soft-wrapped onto a new line.
1735 assert_eq!(
1736 "thequickbrownfox\njumpedoverthelaz\nydogs",
1737 editor.display_text(cx),
1738 );
1739
1740 // First, let's assert behavior on the first line, that was not soft-wrapped.
1741 // Start the cursor at the `k` on the first line
1742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1743 s.select_display_ranges([
1744 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1745 ]);
1746 });
1747
1748 // Moving to the beginning of the line should put us at the beginning of the line.
1749 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1750 assert_eq!(
1751 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1752 editor.selections.display_ranges(cx)
1753 );
1754
1755 // Moving to the end of the line should put us at the end of the line.
1756 editor.move_to_end_of_line(&move_to_end, window, cx);
1757 assert_eq!(
1758 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1759 editor.selections.display_ranges(cx)
1760 );
1761
1762 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1763 // Start the cursor at the last line (`y` that was wrapped to a new line)
1764 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1765 s.select_display_ranges([
1766 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1767 ]);
1768 });
1769
1770 // Moving to the beginning of the line should put us at the start of the second line of
1771 // display text, i.e., the `j`.
1772 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1773 assert_eq!(
1774 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1775 editor.selections.display_ranges(cx)
1776 );
1777
1778 // Moving to the beginning of the line again should be a no-op.
1779 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1780 assert_eq!(
1781 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1782 editor.selections.display_ranges(cx)
1783 );
1784
1785 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1786 // next display line.
1787 editor.move_to_end_of_line(&move_to_end, window, cx);
1788 assert_eq!(
1789 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1790 editor.selections.display_ranges(cx)
1791 );
1792
1793 // Moving to the end of the line again should be a no-op.
1794 editor.move_to_end_of_line(&move_to_end, window, cx);
1795 assert_eq!(
1796 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1797 editor.selections.display_ranges(cx)
1798 );
1799 });
1800}
1801
1802#[gpui::test]
1803fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1804 init_test(cx, |_| {});
1805
1806 let move_to_beg = MoveToBeginningOfLine {
1807 stop_at_soft_wraps: true,
1808 stop_at_indent: true,
1809 };
1810
1811 let select_to_beg = SelectToBeginningOfLine {
1812 stop_at_soft_wraps: true,
1813 stop_at_indent: true,
1814 };
1815
1816 let delete_to_beg = DeleteToBeginningOfLine {
1817 stop_at_indent: true,
1818 };
1819
1820 let move_to_end = MoveToEndOfLine {
1821 stop_at_soft_wraps: false,
1822 };
1823
1824 let editor = cx.add_window(|window, cx| {
1825 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1826 build_editor(buffer, window, cx)
1827 });
1828
1829 _ = editor.update(cx, |editor, window, cx| {
1830 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1831 s.select_display_ranges([
1832 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1833 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1834 ]);
1835 });
1836
1837 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1838 // and the second cursor at the first non-whitespace character in the line.
1839 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1840 assert_eq!(
1841 editor.selections.display_ranges(cx),
1842 &[
1843 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1844 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1845 ]
1846 );
1847
1848 // Moving to the beginning of the line again should be a no-op for the first cursor,
1849 // and should move the second cursor to the beginning of the line.
1850 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1855 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1856 ]
1857 );
1858
1859 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1860 // and should move the second cursor back to the first non-whitespace character in the line.
1861 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1862 assert_eq!(
1863 editor.selections.display_ranges(cx),
1864 &[
1865 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1866 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1867 ]
1868 );
1869
1870 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1871 // and to the first non-whitespace character in the line for the second cursor.
1872 editor.move_to_end_of_line(&move_to_end, window, cx);
1873 editor.move_left(&MoveLeft, window, cx);
1874 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[
1878 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1879 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1880 ]
1881 );
1882
1883 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1884 // and should select to the beginning of the line for the second cursor.
1885 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1886 assert_eq!(
1887 editor.selections.display_ranges(cx),
1888 &[
1889 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1890 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1891 ]
1892 );
1893
1894 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1895 // and should delete to the first non-whitespace character in the line for the second cursor.
1896 editor.move_to_end_of_line(&move_to_end, window, cx);
1897 editor.move_left(&MoveLeft, window, cx);
1898 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1899 assert_eq!(editor.text(cx), "c\n f");
1900 });
1901}
1902
1903#[gpui::test]
1904fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1905 init_test(cx, |_| {});
1906
1907 let editor = cx.add_window(|window, cx| {
1908 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1909 build_editor(buffer, window, cx)
1910 });
1911 _ = editor.update(cx, |editor, window, cx| {
1912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1913 s.select_display_ranges([
1914 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1915 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1916 ])
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_previous_word_start(&MoveToPreviousWordStart, window, cx);
1925 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1928 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1931 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1932
1933 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1934 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1935
1936 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1937 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1938
1939 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1940 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1941
1942 editor.move_right(&MoveRight, window, cx);
1943 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1944 assert_selection_ranges(
1945 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1946 editor,
1947 cx,
1948 );
1949
1950 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1951 assert_selection_ranges(
1952 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1953 editor,
1954 cx,
1955 );
1956
1957 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1958 assert_selection_ranges(
1959 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1960 editor,
1961 cx,
1962 );
1963 });
1964}
1965
1966#[gpui::test]
1967fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1968 init_test(cx, |_| {});
1969
1970 let editor = cx.add_window(|window, cx| {
1971 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1972 build_editor(buffer, window, cx)
1973 });
1974
1975 _ = editor.update(cx, |editor, window, cx| {
1976 editor.set_wrap_width(Some(140.0.into()), cx);
1977 assert_eq!(
1978 editor.display_text(cx),
1979 "use one::{\n two::three::\n four::five\n};"
1980 );
1981
1982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1983 s.select_display_ranges([
1984 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1985 ]);
1986 });
1987
1988 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1989 assert_eq!(
1990 editor.selections.display_ranges(cx),
1991 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1992 );
1993
1994 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1995 assert_eq!(
1996 editor.selections.display_ranges(cx),
1997 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1998 );
1999
2000 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2001 assert_eq!(
2002 editor.selections.display_ranges(cx),
2003 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2004 );
2005
2006 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2007 assert_eq!(
2008 editor.selections.display_ranges(cx),
2009 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2010 );
2011
2012 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2013 assert_eq!(
2014 editor.selections.display_ranges(cx),
2015 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2016 );
2017
2018 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2019 assert_eq!(
2020 editor.selections.display_ranges(cx),
2021 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2022 );
2023 });
2024}
2025
2026#[gpui::test]
2027async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2028 init_test(cx, |_| {});
2029 let mut cx = EditorTestContext::new(cx).await;
2030
2031 let line_height = cx.editor(|editor, window, _| {
2032 editor
2033 .style()
2034 .unwrap()
2035 .text
2036 .line_height_in_pixels(window.rem_size())
2037 });
2038 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2039
2040 cx.set_state(
2041 &r#"ˇone
2042 two
2043
2044 three
2045 fourˇ
2046 five
2047
2048 six"#
2049 .unindent(),
2050 );
2051
2052 cx.update_editor(|editor, window, cx| {
2053 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2054 });
2055 cx.assert_editor_state(
2056 &r#"one
2057 two
2058 ˇ
2059 three
2060 four
2061 five
2062 ˇ
2063 six"#
2064 .unindent(),
2065 );
2066
2067 cx.update_editor(|editor, window, cx| {
2068 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2069 });
2070 cx.assert_editor_state(
2071 &r#"one
2072 two
2073
2074 three
2075 four
2076 five
2077 ˇ
2078 sixˇ"#
2079 .unindent(),
2080 );
2081
2082 cx.update_editor(|editor, window, cx| {
2083 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2084 });
2085 cx.assert_editor_state(
2086 &r#"one
2087 two
2088
2089 three
2090 four
2091 five
2092
2093 sixˇ"#
2094 .unindent(),
2095 );
2096
2097 cx.update_editor(|editor, window, cx| {
2098 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2099 });
2100 cx.assert_editor_state(
2101 &r#"one
2102 two
2103
2104 three
2105 four
2106 five
2107 ˇ
2108 six"#
2109 .unindent(),
2110 );
2111
2112 cx.update_editor(|editor, window, cx| {
2113 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2114 });
2115 cx.assert_editor_state(
2116 &r#"one
2117 two
2118 ˇ
2119 three
2120 four
2121 five
2122
2123 six"#
2124 .unindent(),
2125 );
2126
2127 cx.update_editor(|editor, window, cx| {
2128 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2129 });
2130 cx.assert_editor_state(
2131 &r#"ˇone
2132 two
2133
2134 three
2135 four
2136 five
2137
2138 six"#
2139 .unindent(),
2140 );
2141}
2142
2143#[gpui::test]
2144async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2145 init_test(cx, |_| {});
2146 let mut cx = EditorTestContext::new(cx).await;
2147 let line_height = cx.editor(|editor, window, _| {
2148 editor
2149 .style()
2150 .unwrap()
2151 .text
2152 .line_height_in_pixels(window.rem_size())
2153 });
2154 let window = cx.window;
2155 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2156
2157 cx.set_state(
2158 r#"ˇone
2159 two
2160 three
2161 four
2162 five
2163 six
2164 seven
2165 eight
2166 nine
2167 ten
2168 "#,
2169 );
2170
2171 cx.update_editor(|editor, window, cx| {
2172 assert_eq!(
2173 editor.snapshot(window, cx).scroll_position(),
2174 gpui::Point::new(0., 0.)
2175 );
2176 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2177 assert_eq!(
2178 editor.snapshot(window, cx).scroll_position(),
2179 gpui::Point::new(0., 3.)
2180 );
2181 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2182 assert_eq!(
2183 editor.snapshot(window, cx).scroll_position(),
2184 gpui::Point::new(0., 6.)
2185 );
2186 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2187 assert_eq!(
2188 editor.snapshot(window, cx).scroll_position(),
2189 gpui::Point::new(0., 3.)
2190 );
2191
2192 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2193 assert_eq!(
2194 editor.snapshot(window, cx).scroll_position(),
2195 gpui::Point::new(0., 1.)
2196 );
2197 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2198 assert_eq!(
2199 editor.snapshot(window, cx).scroll_position(),
2200 gpui::Point::new(0., 3.)
2201 );
2202 });
2203}
2204
2205#[gpui::test]
2206async fn test_autoscroll(cx: &mut TestAppContext) {
2207 init_test(cx, |_| {});
2208 let mut cx = EditorTestContext::new(cx).await;
2209
2210 let line_height = cx.update_editor(|editor, window, cx| {
2211 editor.set_vertical_scroll_margin(2, cx);
2212 editor
2213 .style()
2214 .unwrap()
2215 .text
2216 .line_height_in_pixels(window.rem_size())
2217 });
2218 let window = cx.window;
2219 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2220
2221 cx.set_state(
2222 r#"ˇone
2223 two
2224 three
2225 four
2226 five
2227 six
2228 seven
2229 eight
2230 nine
2231 ten
2232 "#,
2233 );
2234 cx.update_editor(|editor, window, cx| {
2235 assert_eq!(
2236 editor.snapshot(window, cx).scroll_position(),
2237 gpui::Point::new(0., 0.0)
2238 );
2239 });
2240
2241 // Add a cursor below the visible area. Since both cursors cannot fit
2242 // on screen, the editor autoscrolls to reveal the newest cursor, and
2243 // allows the vertical scroll margin below that cursor.
2244 cx.update_editor(|editor, window, cx| {
2245 editor.change_selections(Default::default(), window, cx, |selections| {
2246 selections.select_ranges([
2247 Point::new(0, 0)..Point::new(0, 0),
2248 Point::new(6, 0)..Point::new(6, 0),
2249 ]);
2250 })
2251 });
2252 cx.update_editor(|editor, window, cx| {
2253 assert_eq!(
2254 editor.snapshot(window, cx).scroll_position(),
2255 gpui::Point::new(0., 3.0)
2256 );
2257 });
2258
2259 // Move down. The editor cursor scrolls down to track the newest cursor.
2260 cx.update_editor(|editor, window, cx| {
2261 editor.move_down(&Default::default(), window, cx);
2262 });
2263 cx.update_editor(|editor, window, cx| {
2264 assert_eq!(
2265 editor.snapshot(window, cx).scroll_position(),
2266 gpui::Point::new(0., 4.0)
2267 );
2268 });
2269
2270 // Add a cursor above the visible area. Since both cursors fit on screen,
2271 // the editor scrolls to show both.
2272 cx.update_editor(|editor, window, cx| {
2273 editor.change_selections(Default::default(), window, cx, |selections| {
2274 selections.select_ranges([
2275 Point::new(1, 0)..Point::new(1, 0),
2276 Point::new(6, 0)..Point::new(6, 0),
2277 ]);
2278 })
2279 });
2280 cx.update_editor(|editor, window, cx| {
2281 assert_eq!(
2282 editor.snapshot(window, cx).scroll_position(),
2283 gpui::Point::new(0., 1.0)
2284 );
2285 });
2286}
2287
2288#[gpui::test]
2289async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2290 init_test(cx, |_| {});
2291 let mut cx = EditorTestContext::new(cx).await;
2292
2293 let line_height = cx.editor(|editor, window, _cx| {
2294 editor
2295 .style()
2296 .unwrap()
2297 .text
2298 .line_height_in_pixels(window.rem_size())
2299 });
2300 let window = cx.window;
2301 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2302 cx.set_state(
2303 &r#"
2304 ˇone
2305 two
2306 threeˇ
2307 four
2308 five
2309 six
2310 seven
2311 eight
2312 nine
2313 ten
2314 "#
2315 .unindent(),
2316 );
2317
2318 cx.update_editor(|editor, window, cx| {
2319 editor.move_page_down(&MovePageDown::default(), window, cx)
2320 });
2321 cx.assert_editor_state(
2322 &r#"
2323 one
2324 two
2325 three
2326 ˇfour
2327 five
2328 sixˇ
2329 seven
2330 eight
2331 nine
2332 ten
2333 "#
2334 .unindent(),
2335 );
2336
2337 cx.update_editor(|editor, window, cx| {
2338 editor.move_page_down(&MovePageDown::default(), window, cx)
2339 });
2340 cx.assert_editor_state(
2341 &r#"
2342 one
2343 two
2344 three
2345 four
2346 five
2347 six
2348 ˇseven
2349 eight
2350 nineˇ
2351 ten
2352 "#
2353 .unindent(),
2354 );
2355
2356 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2357 cx.assert_editor_state(
2358 &r#"
2359 one
2360 two
2361 three
2362 ˇfour
2363 five
2364 sixˇ
2365 seven
2366 eight
2367 nine
2368 ten
2369 "#
2370 .unindent(),
2371 );
2372
2373 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2374 cx.assert_editor_state(
2375 &r#"
2376 ˇone
2377 two
2378 threeˇ
2379 four
2380 five
2381 six
2382 seven
2383 eight
2384 nine
2385 ten
2386 "#
2387 .unindent(),
2388 );
2389
2390 // Test select collapsing
2391 cx.update_editor(|editor, window, cx| {
2392 editor.move_page_down(&MovePageDown::default(), window, cx);
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 });
2396 cx.assert_editor_state(
2397 &r#"
2398 one
2399 two
2400 three
2401 four
2402 five
2403 six
2404 seven
2405 eight
2406 nine
2407 ˇten
2408 ˇ"#
2409 .unindent(),
2410 );
2411}
2412
2413#[gpui::test]
2414async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2415 init_test(cx, |_| {});
2416 let mut cx = EditorTestContext::new(cx).await;
2417 cx.set_state("one «two threeˇ» four");
2418 cx.update_editor(|editor, window, cx| {
2419 editor.delete_to_beginning_of_line(
2420 &DeleteToBeginningOfLine {
2421 stop_at_indent: false,
2422 },
2423 window,
2424 cx,
2425 );
2426 assert_eq!(editor.text(cx), " four");
2427 });
2428}
2429
2430#[gpui::test]
2431fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2432 init_test(cx, |_| {});
2433
2434 let editor = cx.add_window(|window, cx| {
2435 let buffer = MultiBuffer::build_simple("one two three four", cx);
2436 build_editor(buffer.clone(), window, cx)
2437 });
2438
2439 _ = editor.update(cx, |editor, window, cx| {
2440 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2441 s.select_display_ranges([
2442 // an empty selection - the preceding word fragment is deleted
2443 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2444 // characters selected - they are deleted
2445 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2446 ])
2447 });
2448 editor.delete_to_previous_word_start(
2449 &DeleteToPreviousWordStart {
2450 ignore_newlines: false,
2451 },
2452 window,
2453 cx,
2454 );
2455 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2456 });
2457
2458 _ = editor.update(cx, |editor, window, cx| {
2459 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2460 s.select_display_ranges([
2461 // an empty selection - the following word fragment is deleted
2462 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2463 // characters selected - they are deleted
2464 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2465 ])
2466 });
2467 editor.delete_to_next_word_end(
2468 &DeleteToNextWordEnd {
2469 ignore_newlines: false,
2470 },
2471 window,
2472 cx,
2473 );
2474 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2475 });
2476}
2477
2478#[gpui::test]
2479fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2480 init_test(cx, |_| {});
2481
2482 let editor = cx.add_window(|window, cx| {
2483 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2484 build_editor(buffer.clone(), window, cx)
2485 });
2486 let del_to_prev_word_start = DeleteToPreviousWordStart {
2487 ignore_newlines: false,
2488 };
2489 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2490 ignore_newlines: true,
2491 };
2492
2493 _ = editor.update(cx, |editor, window, cx| {
2494 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2495 s.select_display_ranges([
2496 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2497 ])
2498 });
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2501 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2502 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2503 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2504 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2505 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2506 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2507 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2508 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2509 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2510 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2511 });
2512}
2513
2514#[gpui::test]
2515fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2516 init_test(cx, |_| {});
2517
2518 let editor = cx.add_window(|window, cx| {
2519 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2520 build_editor(buffer.clone(), window, cx)
2521 });
2522 let del_to_next_word_end = DeleteToNextWordEnd {
2523 ignore_newlines: false,
2524 };
2525 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2526 ignore_newlines: true,
2527 };
2528
2529 _ = editor.update(cx, |editor, window, cx| {
2530 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2531 s.select_display_ranges([
2532 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2533 ])
2534 });
2535 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2536 assert_eq!(
2537 editor.buffer.read(cx).read(cx).text(),
2538 "one\n two\nthree\n four"
2539 );
2540 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2541 assert_eq!(
2542 editor.buffer.read(cx).read(cx).text(),
2543 "\n two\nthree\n four"
2544 );
2545 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2546 assert_eq!(
2547 editor.buffer.read(cx).read(cx).text(),
2548 "two\nthree\n four"
2549 );
2550 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2551 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2552 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2553 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2554 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2555 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2556 });
2557}
2558
2559#[gpui::test]
2560fn test_newline(cx: &mut TestAppContext) {
2561 init_test(cx, |_| {});
2562
2563 let editor = cx.add_window(|window, cx| {
2564 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2565 build_editor(buffer.clone(), window, cx)
2566 });
2567
2568 _ = editor.update(cx, |editor, window, cx| {
2569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2570 s.select_display_ranges([
2571 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2573 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2574 ])
2575 });
2576
2577 editor.newline(&Newline, window, cx);
2578 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2579 });
2580}
2581
2582#[gpui::test]
2583fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2584 init_test(cx, |_| {});
2585
2586 let editor = cx.add_window(|window, cx| {
2587 let buffer = MultiBuffer::build_simple(
2588 "
2589 a
2590 b(
2591 X
2592 )
2593 c(
2594 X
2595 )
2596 "
2597 .unindent()
2598 .as_str(),
2599 cx,
2600 );
2601 let mut editor = build_editor(buffer.clone(), window, cx);
2602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2603 s.select_ranges([
2604 Point::new(2, 4)..Point::new(2, 5),
2605 Point::new(5, 4)..Point::new(5, 5),
2606 ])
2607 });
2608 editor
2609 });
2610
2611 _ = editor.update(cx, |editor, window, cx| {
2612 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2613 editor.buffer.update(cx, |buffer, cx| {
2614 buffer.edit(
2615 [
2616 (Point::new(1, 2)..Point::new(3, 0), ""),
2617 (Point::new(4, 2)..Point::new(6, 0), ""),
2618 ],
2619 None,
2620 cx,
2621 );
2622 assert_eq!(
2623 buffer.read(cx).text(),
2624 "
2625 a
2626 b()
2627 c()
2628 "
2629 .unindent()
2630 );
2631 });
2632 assert_eq!(
2633 editor.selections.ranges(cx),
2634 &[
2635 Point::new(1, 2)..Point::new(1, 2),
2636 Point::new(2, 2)..Point::new(2, 2),
2637 ],
2638 );
2639
2640 editor.newline(&Newline, window, cx);
2641 assert_eq!(
2642 editor.text(cx),
2643 "
2644 a
2645 b(
2646 )
2647 c(
2648 )
2649 "
2650 .unindent()
2651 );
2652
2653 // The selections are moved after the inserted newlines
2654 assert_eq!(
2655 editor.selections.ranges(cx),
2656 &[
2657 Point::new(2, 0)..Point::new(2, 0),
2658 Point::new(4, 0)..Point::new(4, 0),
2659 ],
2660 );
2661 });
2662}
2663
2664#[gpui::test]
2665async fn test_newline_above(cx: &mut TestAppContext) {
2666 init_test(cx, |settings| {
2667 settings.defaults.tab_size = NonZeroU32::new(4)
2668 });
2669
2670 let language = Arc::new(
2671 Language::new(
2672 LanguageConfig::default(),
2673 Some(tree_sitter_rust::LANGUAGE.into()),
2674 )
2675 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2676 .unwrap(),
2677 );
2678
2679 let mut cx = EditorTestContext::new(cx).await;
2680 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2681 cx.set_state(indoc! {"
2682 const a: ˇA = (
2683 (ˇ
2684 «const_functionˇ»(ˇ),
2685 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2686 )ˇ
2687 ˇ);ˇ
2688 "});
2689
2690 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2691 cx.assert_editor_state(indoc! {"
2692 ˇ
2693 const a: A = (
2694 ˇ
2695 (
2696 ˇ
2697 ˇ
2698 const_function(),
2699 ˇ
2700 ˇ
2701 ˇ
2702 ˇ
2703 something_else,
2704 ˇ
2705 )
2706 ˇ
2707 ˇ
2708 );
2709 "});
2710}
2711
2712#[gpui::test]
2713async fn test_newline_below(cx: &mut TestAppContext) {
2714 init_test(cx, |settings| {
2715 settings.defaults.tab_size = NonZeroU32::new(4)
2716 });
2717
2718 let language = Arc::new(
2719 Language::new(
2720 LanguageConfig::default(),
2721 Some(tree_sitter_rust::LANGUAGE.into()),
2722 )
2723 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2724 .unwrap(),
2725 );
2726
2727 let mut cx = EditorTestContext::new(cx).await;
2728 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2729 cx.set_state(indoc! {"
2730 const a: ˇA = (
2731 (ˇ
2732 «const_functionˇ»(ˇ),
2733 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2734 )ˇ
2735 ˇ);ˇ
2736 "});
2737
2738 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2739 cx.assert_editor_state(indoc! {"
2740 const a: A = (
2741 ˇ
2742 (
2743 ˇ
2744 const_function(),
2745 ˇ
2746 ˇ
2747 something_else,
2748 ˇ
2749 ˇ
2750 ˇ
2751 ˇ
2752 )
2753 ˇ
2754 );
2755 ˇ
2756 ˇ
2757 "});
2758}
2759
2760#[gpui::test]
2761async fn test_newline_comments(cx: &mut TestAppContext) {
2762 init_test(cx, |settings| {
2763 settings.defaults.tab_size = NonZeroU32::new(4)
2764 });
2765
2766 let language = Arc::new(Language::new(
2767 LanguageConfig {
2768 line_comments: vec!["// ".into()],
2769 ..LanguageConfig::default()
2770 },
2771 None,
2772 ));
2773 {
2774 let mut cx = EditorTestContext::new(cx).await;
2775 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2776 cx.set_state(indoc! {"
2777 // Fooˇ
2778 "});
2779
2780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2781 cx.assert_editor_state(indoc! {"
2782 // Foo
2783 // ˇ
2784 "});
2785 // Ensure that we add comment prefix when existing line contains space
2786 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2787 cx.assert_editor_state(
2788 indoc! {"
2789 // Foo
2790 //s
2791 // ˇ
2792 "}
2793 .replace("s", " ") // s is used as space placeholder to prevent format on save
2794 .as_str(),
2795 );
2796 // Ensure that we add comment prefix when existing line does not contain space
2797 cx.set_state(indoc! {"
2798 // Foo
2799 //ˇ
2800 "});
2801 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2802 cx.assert_editor_state(indoc! {"
2803 // Foo
2804 //
2805 // ˇ
2806 "});
2807 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2808 cx.set_state(indoc! {"
2809 ˇ// Foo
2810 "});
2811 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2812 cx.assert_editor_state(indoc! {"
2813
2814 ˇ// Foo
2815 "});
2816 }
2817 // Ensure that comment continuations can be disabled.
2818 update_test_language_settings(cx, |settings| {
2819 settings.defaults.extend_comment_on_newline = Some(false);
2820 });
2821 let mut cx = EditorTestContext::new(cx).await;
2822 cx.set_state(indoc! {"
2823 // Fooˇ
2824 "});
2825 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2826 cx.assert_editor_state(indoc! {"
2827 // Foo
2828 ˇ
2829 "});
2830}
2831
2832#[gpui::test]
2833async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2834 init_test(cx, |settings| {
2835 settings.defaults.tab_size = NonZeroU32::new(4)
2836 });
2837
2838 let language = Arc::new(Language::new(
2839 LanguageConfig {
2840 line_comments: vec!["// ".into(), "/// ".into()],
2841 ..LanguageConfig::default()
2842 },
2843 None,
2844 ));
2845 {
2846 let mut cx = EditorTestContext::new(cx).await;
2847 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2848 cx.set_state(indoc! {"
2849 //ˇ
2850 "});
2851 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2852 cx.assert_editor_state(indoc! {"
2853 //
2854 // ˇ
2855 "});
2856
2857 cx.set_state(indoc! {"
2858 ///ˇ
2859 "});
2860 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2861 cx.assert_editor_state(indoc! {"
2862 ///
2863 /// ˇ
2864 "});
2865 }
2866}
2867
2868#[gpui::test]
2869async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2870 init_test(cx, |settings| {
2871 settings.defaults.tab_size = NonZeroU32::new(4)
2872 });
2873
2874 let language = Arc::new(
2875 Language::new(
2876 LanguageConfig {
2877 documentation: Some(language::DocumentationConfig {
2878 start: "/**".into(),
2879 end: "*/".into(),
2880 prefix: "* ".into(),
2881 tab_size: NonZeroU32::new(1).unwrap(),
2882 }),
2883
2884 ..LanguageConfig::default()
2885 },
2886 Some(tree_sitter_rust::LANGUAGE.into()),
2887 )
2888 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2889 .unwrap(),
2890 );
2891
2892 {
2893 let mut cx = EditorTestContext::new(cx).await;
2894 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2895 cx.set_state(indoc! {"
2896 /**ˇ
2897 "});
2898
2899 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2900 cx.assert_editor_state(indoc! {"
2901 /**
2902 * ˇ
2903 "});
2904 // Ensure that if cursor is before the comment start,
2905 // we do not actually insert a comment prefix.
2906 cx.set_state(indoc! {"
2907 ˇ/**
2908 "});
2909 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2910 cx.assert_editor_state(indoc! {"
2911
2912 ˇ/**
2913 "});
2914 // Ensure that if cursor is between it doesn't add comment prefix.
2915 cx.set_state(indoc! {"
2916 /*ˇ*
2917 "});
2918 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2919 cx.assert_editor_state(indoc! {"
2920 /*
2921 ˇ*
2922 "});
2923 // Ensure that if suffix exists on same line after cursor it adds new line.
2924 cx.set_state(indoc! {"
2925 /**ˇ*/
2926 "});
2927 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2928 cx.assert_editor_state(indoc! {"
2929 /**
2930 * ˇ
2931 */
2932 "});
2933 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2934 cx.set_state(indoc! {"
2935 /**ˇ */
2936 "});
2937 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2938 cx.assert_editor_state(indoc! {"
2939 /**
2940 * ˇ
2941 */
2942 "});
2943 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2944 cx.set_state(indoc! {"
2945 /** ˇ*/
2946 "});
2947 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2948 cx.assert_editor_state(
2949 indoc! {"
2950 /**s
2951 * ˇ
2952 */
2953 "}
2954 .replace("s", " ") // s is used as space placeholder to prevent format on save
2955 .as_str(),
2956 );
2957 // Ensure that delimiter space is preserved when newline on already
2958 // spaced delimiter.
2959 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2960 cx.assert_editor_state(
2961 indoc! {"
2962 /**s
2963 *s
2964 * ˇ
2965 */
2966 "}
2967 .replace("s", " ") // s is used as space placeholder to prevent format on save
2968 .as_str(),
2969 );
2970 // Ensure that delimiter space is preserved when space is not
2971 // on existing delimiter.
2972 cx.set_state(indoc! {"
2973 /**
2974 *ˇ
2975 */
2976 "});
2977 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2978 cx.assert_editor_state(indoc! {"
2979 /**
2980 *
2981 * ˇ
2982 */
2983 "});
2984 // Ensure that if suffix exists on same line after cursor it
2985 // doesn't add extra new line if prefix is not on same line.
2986 cx.set_state(indoc! {"
2987 /**
2988 ˇ*/
2989 "});
2990 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2991 cx.assert_editor_state(indoc! {"
2992 /**
2993
2994 ˇ*/
2995 "});
2996 // Ensure that it detects suffix after existing prefix.
2997 cx.set_state(indoc! {"
2998 /**ˇ/
2999 "});
3000 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3001 cx.assert_editor_state(indoc! {"
3002 /**
3003 ˇ/
3004 "});
3005 // Ensure that if suffix exists on same line before
3006 // cursor it does not add comment prefix.
3007 cx.set_state(indoc! {"
3008 /** */ˇ
3009 "});
3010 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3011 cx.assert_editor_state(indoc! {"
3012 /** */
3013 ˇ
3014 "});
3015 // Ensure that if suffix exists on same line before
3016 // cursor it does not add comment prefix.
3017 cx.set_state(indoc! {"
3018 /**
3019 *
3020 */ˇ
3021 "});
3022 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3023 cx.assert_editor_state(indoc! {"
3024 /**
3025 *
3026 */
3027 ˇ
3028 "});
3029
3030 // Ensure that inline comment followed by code
3031 // doesn't add comment prefix on newline
3032 cx.set_state(indoc! {"
3033 /** */ textˇ
3034 "});
3035 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3036 cx.assert_editor_state(indoc! {"
3037 /** */ text
3038 ˇ
3039 "});
3040
3041 // Ensure that text after comment end tag
3042 // doesn't add comment prefix on newline
3043 cx.set_state(indoc! {"
3044 /**
3045 *
3046 */ˇtext
3047 "});
3048 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3049 cx.assert_editor_state(indoc! {"
3050 /**
3051 *
3052 */
3053 ˇtext
3054 "});
3055
3056 // Ensure if not comment block it doesn't
3057 // add comment prefix on newline
3058 cx.set_state(indoc! {"
3059 * textˇ
3060 "});
3061 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3062 cx.assert_editor_state(indoc! {"
3063 * text
3064 ˇ
3065 "});
3066 }
3067 // Ensure that comment continuations can be disabled.
3068 update_test_language_settings(cx, |settings| {
3069 settings.defaults.extend_comment_on_newline = Some(false);
3070 });
3071 let mut cx = EditorTestContext::new(cx).await;
3072 cx.set_state(indoc! {"
3073 /**ˇ
3074 "});
3075 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3076 cx.assert_editor_state(indoc! {"
3077 /**
3078 ˇ
3079 "});
3080}
3081
3082#[gpui::test]
3083fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3084 init_test(cx, |_| {});
3085
3086 let editor = cx.add_window(|window, cx| {
3087 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3088 let mut editor = build_editor(buffer.clone(), window, cx);
3089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3090 s.select_ranges([3..4, 11..12, 19..20])
3091 });
3092 editor
3093 });
3094
3095 _ = editor.update(cx, |editor, window, cx| {
3096 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3097 editor.buffer.update(cx, |buffer, cx| {
3098 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3099 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3100 });
3101 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3102
3103 editor.insert("Z", window, cx);
3104 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3105
3106 // The selections are moved after the inserted characters
3107 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3108 });
3109}
3110
3111#[gpui::test]
3112async fn test_tab(cx: &mut TestAppContext) {
3113 init_test(cx, |settings| {
3114 settings.defaults.tab_size = NonZeroU32::new(3)
3115 });
3116
3117 let mut cx = EditorTestContext::new(cx).await;
3118 cx.set_state(indoc! {"
3119 ˇabˇc
3120 ˇ🏀ˇ🏀ˇefg
3121 dˇ
3122 "});
3123 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3124 cx.assert_editor_state(indoc! {"
3125 ˇab ˇc
3126 ˇ🏀 ˇ🏀 ˇefg
3127 d ˇ
3128 "});
3129
3130 cx.set_state(indoc! {"
3131 a
3132 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3133 "});
3134 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3135 cx.assert_editor_state(indoc! {"
3136 a
3137 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3138 "});
3139}
3140
3141#[gpui::test]
3142async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3143 init_test(cx, |_| {});
3144
3145 let mut cx = EditorTestContext::new(cx).await;
3146 let language = Arc::new(
3147 Language::new(
3148 LanguageConfig::default(),
3149 Some(tree_sitter_rust::LANGUAGE.into()),
3150 )
3151 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3152 .unwrap(),
3153 );
3154 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3155
3156 // test when all cursors are not at suggested indent
3157 // then simply move to their suggested indent location
3158 cx.set_state(indoc! {"
3159 const a: B = (
3160 c(
3161 ˇ
3162 ˇ )
3163 );
3164 "});
3165 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3166 cx.assert_editor_state(indoc! {"
3167 const a: B = (
3168 c(
3169 ˇ
3170 ˇ)
3171 );
3172 "});
3173
3174 // test cursor already at suggested indent not moving when
3175 // other cursors are yet to reach their suggested indents
3176 cx.set_state(indoc! {"
3177 ˇ
3178 const a: B = (
3179 c(
3180 d(
3181 ˇ
3182 )
3183 ˇ
3184 ˇ )
3185 );
3186 "});
3187 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3188 cx.assert_editor_state(indoc! {"
3189 ˇ
3190 const a: B = (
3191 c(
3192 d(
3193 ˇ
3194 )
3195 ˇ
3196 ˇ)
3197 );
3198 "});
3199 // test when all cursors are at suggested indent then tab is inserted
3200 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3201 cx.assert_editor_state(indoc! {"
3202 ˇ
3203 const a: B = (
3204 c(
3205 d(
3206 ˇ
3207 )
3208 ˇ
3209 ˇ)
3210 );
3211 "});
3212
3213 // test when current indent is less than suggested indent,
3214 // we adjust line to match suggested indent and move cursor to it
3215 //
3216 // when no other cursor is at word boundary, all of them should move
3217 cx.set_state(indoc! {"
3218 const a: B = (
3219 c(
3220 d(
3221 ˇ
3222 ˇ )
3223 ˇ )
3224 );
3225 "});
3226 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3227 cx.assert_editor_state(indoc! {"
3228 const a: B = (
3229 c(
3230 d(
3231 ˇ
3232 ˇ)
3233 ˇ)
3234 );
3235 "});
3236
3237 // test when current indent is less than suggested indent,
3238 // we adjust line to match suggested indent and move cursor to it
3239 //
3240 // when some other cursor is at word boundary, it should not move
3241 cx.set_state(indoc! {"
3242 const a: B = (
3243 c(
3244 d(
3245 ˇ
3246 ˇ )
3247 ˇ)
3248 );
3249 "});
3250 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3251 cx.assert_editor_state(indoc! {"
3252 const a: B = (
3253 c(
3254 d(
3255 ˇ
3256 ˇ)
3257 ˇ)
3258 );
3259 "});
3260
3261 // test when current indent is more than suggested indent,
3262 // we just move cursor to current indent instead of suggested indent
3263 //
3264 // when no other cursor is at word boundary, all of them should move
3265 cx.set_state(indoc! {"
3266 const a: B = (
3267 c(
3268 d(
3269 ˇ
3270 ˇ )
3271 ˇ )
3272 );
3273 "});
3274 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3275 cx.assert_editor_state(indoc! {"
3276 const a: B = (
3277 c(
3278 d(
3279 ˇ
3280 ˇ)
3281 ˇ)
3282 );
3283 "});
3284 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3285 cx.assert_editor_state(indoc! {"
3286 const a: B = (
3287 c(
3288 d(
3289 ˇ
3290 ˇ)
3291 ˇ)
3292 );
3293 "});
3294
3295 // test when current indent is more than suggested indent,
3296 // we just move cursor to current indent instead of suggested indent
3297 //
3298 // when some other cursor is at word boundary, it doesn't move
3299 cx.set_state(indoc! {"
3300 const a: B = (
3301 c(
3302 d(
3303 ˇ
3304 ˇ )
3305 ˇ)
3306 );
3307 "});
3308 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3309 cx.assert_editor_state(indoc! {"
3310 const a: B = (
3311 c(
3312 d(
3313 ˇ
3314 ˇ)
3315 ˇ)
3316 );
3317 "});
3318
3319 // handle auto-indent when there are multiple cursors on the same line
3320 cx.set_state(indoc! {"
3321 const a: B = (
3322 c(
3323 ˇ ˇ
3324 ˇ )
3325 );
3326 "});
3327 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3328 cx.assert_editor_state(indoc! {"
3329 const a: B = (
3330 c(
3331 ˇ
3332 ˇ)
3333 );
3334 "});
3335}
3336
3337#[gpui::test]
3338async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3339 init_test(cx, |settings| {
3340 settings.defaults.tab_size = NonZeroU32::new(3)
3341 });
3342
3343 let mut cx = EditorTestContext::new(cx).await;
3344 cx.set_state(indoc! {"
3345 ˇ
3346 \t ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t \t\t \t \t\t \t\t \t \t ˇ
3350 "});
3351
3352 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3353 cx.assert_editor_state(indoc! {"
3354 ˇ
3355 \t ˇ
3356 \t ˇ
3357 \t ˇ
3358 \t \t\t \t \t\t \t\t \t \t ˇ
3359 "});
3360}
3361
3362#[gpui::test]
3363async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3364 init_test(cx, |settings| {
3365 settings.defaults.tab_size = NonZeroU32::new(4)
3366 });
3367
3368 let language = Arc::new(
3369 Language::new(
3370 LanguageConfig::default(),
3371 Some(tree_sitter_rust::LANGUAGE.into()),
3372 )
3373 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3374 .unwrap(),
3375 );
3376
3377 let mut cx = EditorTestContext::new(cx).await;
3378 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3379 cx.set_state(indoc! {"
3380 fn a() {
3381 if b {
3382 \t ˇc
3383 }
3384 }
3385 "});
3386
3387 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3388 cx.assert_editor_state(indoc! {"
3389 fn a() {
3390 if b {
3391 ˇc
3392 }
3393 }
3394 "});
3395}
3396
3397#[gpui::test]
3398async fn test_indent_outdent(cx: &mut TestAppContext) {
3399 init_test(cx, |settings| {
3400 settings.defaults.tab_size = NonZeroU32::new(4);
3401 });
3402
3403 let mut cx = EditorTestContext::new(cx).await;
3404
3405 cx.set_state(indoc! {"
3406 «oneˇ» «twoˇ»
3407 three
3408 four
3409 "});
3410 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3411 cx.assert_editor_state(indoc! {"
3412 «oneˇ» «twoˇ»
3413 three
3414 four
3415 "});
3416
3417 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 «oneˇ» «twoˇ»
3420 three
3421 four
3422 "});
3423
3424 // select across line ending
3425 cx.set_state(indoc! {"
3426 one two
3427 t«hree
3428 ˇ» four
3429 "});
3430 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 one two
3433 t«hree
3434 ˇ» four
3435 "});
3436
3437 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3438 cx.assert_editor_state(indoc! {"
3439 one two
3440 t«hree
3441 ˇ» four
3442 "});
3443
3444 // Ensure that indenting/outdenting works when the cursor is at column 0.
3445 cx.set_state(indoc! {"
3446 one two
3447 ˇthree
3448 four
3449 "});
3450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3451 cx.assert_editor_state(indoc! {"
3452 one two
3453 ˇthree
3454 four
3455 "});
3456
3457 cx.set_state(indoc! {"
3458 one two
3459 ˇ three
3460 four
3461 "});
3462 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 one two
3465 ˇthree
3466 four
3467 "});
3468}
3469
3470#[gpui::test]
3471async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3472 init_test(cx, |settings| {
3473 settings.defaults.hard_tabs = Some(true);
3474 });
3475
3476 let mut cx = EditorTestContext::new(cx).await;
3477
3478 // select two ranges on one line
3479 cx.set_state(indoc! {"
3480 «oneˇ» «twoˇ»
3481 three
3482 four
3483 "});
3484 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3485 cx.assert_editor_state(indoc! {"
3486 \t«oneˇ» «twoˇ»
3487 three
3488 four
3489 "});
3490 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3491 cx.assert_editor_state(indoc! {"
3492 \t\t«oneˇ» «twoˇ»
3493 three
3494 four
3495 "});
3496 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3497 cx.assert_editor_state(indoc! {"
3498 \t«oneˇ» «twoˇ»
3499 three
3500 four
3501 "});
3502 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 «oneˇ» «twoˇ»
3505 three
3506 four
3507 "});
3508
3509 // select across a line ending
3510 cx.set_state(indoc! {"
3511 one two
3512 t«hree
3513 ˇ»four
3514 "});
3515 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3516 cx.assert_editor_state(indoc! {"
3517 one two
3518 \tt«hree
3519 ˇ»four
3520 "});
3521 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3522 cx.assert_editor_state(indoc! {"
3523 one two
3524 \t\tt«hree
3525 ˇ»four
3526 "});
3527 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3528 cx.assert_editor_state(indoc! {"
3529 one two
3530 \tt«hree
3531 ˇ»four
3532 "});
3533 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3534 cx.assert_editor_state(indoc! {"
3535 one two
3536 t«hree
3537 ˇ»four
3538 "});
3539
3540 // Ensure that indenting/outdenting works when the cursor is at column 0.
3541 cx.set_state(indoc! {"
3542 one two
3543 ˇthree
3544 four
3545 "});
3546 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3547 cx.assert_editor_state(indoc! {"
3548 one two
3549 ˇthree
3550 four
3551 "});
3552 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3553 cx.assert_editor_state(indoc! {"
3554 one two
3555 \tˇthree
3556 four
3557 "});
3558 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3559 cx.assert_editor_state(indoc! {"
3560 one two
3561 ˇthree
3562 four
3563 "});
3564}
3565
3566#[gpui::test]
3567fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3568 init_test(cx, |settings| {
3569 settings.languages.extend([
3570 (
3571 "TOML".into(),
3572 LanguageSettingsContent {
3573 tab_size: NonZeroU32::new(2),
3574 ..Default::default()
3575 },
3576 ),
3577 (
3578 "Rust".into(),
3579 LanguageSettingsContent {
3580 tab_size: NonZeroU32::new(4),
3581 ..Default::default()
3582 },
3583 ),
3584 ]);
3585 });
3586
3587 let toml_language = Arc::new(Language::new(
3588 LanguageConfig {
3589 name: "TOML".into(),
3590 ..Default::default()
3591 },
3592 None,
3593 ));
3594 let rust_language = Arc::new(Language::new(
3595 LanguageConfig {
3596 name: "Rust".into(),
3597 ..Default::default()
3598 },
3599 None,
3600 ));
3601
3602 let toml_buffer =
3603 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3604 let rust_buffer =
3605 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3606 let multibuffer = cx.new(|cx| {
3607 let mut multibuffer = MultiBuffer::new(ReadWrite);
3608 multibuffer.push_excerpts(
3609 toml_buffer.clone(),
3610 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3611 cx,
3612 );
3613 multibuffer.push_excerpts(
3614 rust_buffer.clone(),
3615 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3616 cx,
3617 );
3618 multibuffer
3619 });
3620
3621 cx.add_window(|window, cx| {
3622 let mut editor = build_editor(multibuffer, window, cx);
3623
3624 assert_eq!(
3625 editor.text(cx),
3626 indoc! {"
3627 a = 1
3628 b = 2
3629
3630 const c: usize = 3;
3631 "}
3632 );
3633
3634 select_ranges(
3635 &mut editor,
3636 indoc! {"
3637 «aˇ» = 1
3638 b = 2
3639
3640 «const c:ˇ» usize = 3;
3641 "},
3642 window,
3643 cx,
3644 );
3645
3646 editor.tab(&Tab, window, cx);
3647 assert_text_with_selections(
3648 &mut editor,
3649 indoc! {"
3650 «aˇ» = 1
3651 b = 2
3652
3653 «const c:ˇ» usize = 3;
3654 "},
3655 cx,
3656 );
3657 editor.backtab(&Backtab, window, cx);
3658 assert_text_with_selections(
3659 &mut editor,
3660 indoc! {"
3661 «aˇ» = 1
3662 b = 2
3663
3664 «const c:ˇ» usize = 3;
3665 "},
3666 cx,
3667 );
3668
3669 editor
3670 });
3671}
3672
3673#[gpui::test]
3674async fn test_backspace(cx: &mut TestAppContext) {
3675 init_test(cx, |_| {});
3676
3677 let mut cx = EditorTestContext::new(cx).await;
3678
3679 // Basic backspace
3680 cx.set_state(indoc! {"
3681 onˇe two three
3682 fou«rˇ» five six
3683 seven «ˇeight nine
3684 »ten
3685 "});
3686 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3687 cx.assert_editor_state(indoc! {"
3688 oˇe two three
3689 fouˇ five six
3690 seven ˇten
3691 "});
3692
3693 // Test backspace inside and around indents
3694 cx.set_state(indoc! {"
3695 zero
3696 ˇone
3697 ˇtwo
3698 ˇ ˇ ˇ three
3699 ˇ ˇ four
3700 "});
3701 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3702 cx.assert_editor_state(indoc! {"
3703 zero
3704 ˇone
3705 ˇtwo
3706 ˇ threeˇ four
3707 "});
3708}
3709
3710#[gpui::test]
3711async fn test_delete(cx: &mut TestAppContext) {
3712 init_test(cx, |_| {});
3713
3714 let mut cx = EditorTestContext::new(cx).await;
3715 cx.set_state(indoc! {"
3716 onˇe two three
3717 fou«rˇ» five six
3718 seven «ˇeight nine
3719 »ten
3720 "});
3721 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3722 cx.assert_editor_state(indoc! {"
3723 onˇ two three
3724 fouˇ five six
3725 seven ˇten
3726 "});
3727}
3728
3729#[gpui::test]
3730fn test_delete_line(cx: &mut TestAppContext) {
3731 init_test(cx, |_| {});
3732
3733 let editor = cx.add_window(|window, cx| {
3734 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3735 build_editor(buffer, window, cx)
3736 });
3737 _ = editor.update(cx, |editor, window, cx| {
3738 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3739 s.select_display_ranges([
3740 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3741 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3742 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3743 ])
3744 });
3745 editor.delete_line(&DeleteLine, window, cx);
3746 assert_eq!(editor.display_text(cx), "ghi");
3747 assert_eq!(
3748 editor.selections.display_ranges(cx),
3749 vec![
3750 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3751 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3752 ]
3753 );
3754 });
3755
3756 let editor = cx.add_window(|window, cx| {
3757 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3758 build_editor(buffer, window, cx)
3759 });
3760 _ = editor.update(cx, |editor, window, cx| {
3761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3762 s.select_display_ranges([
3763 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3764 ])
3765 });
3766 editor.delete_line(&DeleteLine, window, cx);
3767 assert_eq!(editor.display_text(cx), "ghi\n");
3768 assert_eq!(
3769 editor.selections.display_ranges(cx),
3770 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3771 );
3772 });
3773}
3774
3775#[gpui::test]
3776fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3777 init_test(cx, |_| {});
3778
3779 cx.add_window(|window, cx| {
3780 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3781 let mut editor = build_editor(buffer.clone(), window, cx);
3782 let buffer = buffer.read(cx).as_singleton().unwrap();
3783
3784 assert_eq!(
3785 editor.selections.ranges::<Point>(cx),
3786 &[Point::new(0, 0)..Point::new(0, 0)]
3787 );
3788
3789 // When on single line, replace newline at end by space
3790 editor.join_lines(&JoinLines, window, cx);
3791 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3792 assert_eq!(
3793 editor.selections.ranges::<Point>(cx),
3794 &[Point::new(0, 3)..Point::new(0, 3)]
3795 );
3796
3797 // When multiple lines are selected, remove newlines that are spanned by the selection
3798 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3799 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3800 });
3801 editor.join_lines(&JoinLines, window, cx);
3802 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3803 assert_eq!(
3804 editor.selections.ranges::<Point>(cx),
3805 &[Point::new(0, 11)..Point::new(0, 11)]
3806 );
3807
3808 // Undo should be transactional
3809 editor.undo(&Undo, window, cx);
3810 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3811 assert_eq!(
3812 editor.selections.ranges::<Point>(cx),
3813 &[Point::new(0, 5)..Point::new(2, 2)]
3814 );
3815
3816 // When joining an empty line don't insert a space
3817 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3818 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3819 });
3820 editor.join_lines(&JoinLines, window, cx);
3821 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3822 assert_eq!(
3823 editor.selections.ranges::<Point>(cx),
3824 [Point::new(2, 3)..Point::new(2, 3)]
3825 );
3826
3827 // We can remove trailing newlines
3828 editor.join_lines(&JoinLines, window, cx);
3829 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3830 assert_eq!(
3831 editor.selections.ranges::<Point>(cx),
3832 [Point::new(2, 3)..Point::new(2, 3)]
3833 );
3834
3835 // We don't blow up on the last line
3836 editor.join_lines(&JoinLines, window, cx);
3837 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3838 assert_eq!(
3839 editor.selections.ranges::<Point>(cx),
3840 [Point::new(2, 3)..Point::new(2, 3)]
3841 );
3842
3843 // reset to test indentation
3844 editor.buffer.update(cx, |buffer, cx| {
3845 buffer.edit(
3846 [
3847 (Point::new(1, 0)..Point::new(1, 2), " "),
3848 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3849 ],
3850 None,
3851 cx,
3852 )
3853 });
3854
3855 // We remove any leading spaces
3856 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3858 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3859 });
3860 editor.join_lines(&JoinLines, window, cx);
3861 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3862
3863 // We don't insert a space for a line containing only spaces
3864 editor.join_lines(&JoinLines, window, cx);
3865 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3866
3867 // We ignore any leading tabs
3868 editor.join_lines(&JoinLines, window, cx);
3869 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3870
3871 editor
3872 });
3873}
3874
3875#[gpui::test]
3876fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3877 init_test(cx, |_| {});
3878
3879 cx.add_window(|window, cx| {
3880 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3881 let mut editor = build_editor(buffer.clone(), window, cx);
3882 let buffer = buffer.read(cx).as_singleton().unwrap();
3883
3884 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3885 s.select_ranges([
3886 Point::new(0, 2)..Point::new(1, 1),
3887 Point::new(1, 2)..Point::new(1, 2),
3888 Point::new(3, 1)..Point::new(3, 2),
3889 ])
3890 });
3891
3892 editor.join_lines(&JoinLines, window, cx);
3893 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3894
3895 assert_eq!(
3896 editor.selections.ranges::<Point>(cx),
3897 [
3898 Point::new(0, 7)..Point::new(0, 7),
3899 Point::new(1, 3)..Point::new(1, 3)
3900 ]
3901 );
3902 editor
3903 });
3904}
3905
3906#[gpui::test]
3907async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3908 init_test(cx, |_| {});
3909
3910 let mut cx = EditorTestContext::new(cx).await;
3911
3912 let diff_base = r#"
3913 Line 0
3914 Line 1
3915 Line 2
3916 Line 3
3917 "#
3918 .unindent();
3919
3920 cx.set_state(
3921 &r#"
3922 ˇLine 0
3923 Line 1
3924 Line 2
3925 Line 3
3926 "#
3927 .unindent(),
3928 );
3929
3930 cx.set_head_text(&diff_base);
3931 executor.run_until_parked();
3932
3933 // Join lines
3934 cx.update_editor(|editor, window, cx| {
3935 editor.join_lines(&JoinLines, window, cx);
3936 });
3937 executor.run_until_parked();
3938
3939 cx.assert_editor_state(
3940 &r#"
3941 Line 0ˇ Line 1
3942 Line 2
3943 Line 3
3944 "#
3945 .unindent(),
3946 );
3947 // Join again
3948 cx.update_editor(|editor, window, cx| {
3949 editor.join_lines(&JoinLines, window, cx);
3950 });
3951 executor.run_until_parked();
3952
3953 cx.assert_editor_state(
3954 &r#"
3955 Line 0 Line 1ˇ Line 2
3956 Line 3
3957 "#
3958 .unindent(),
3959 );
3960}
3961
3962#[gpui::test]
3963async fn test_custom_newlines_cause_no_false_positive_diffs(
3964 executor: BackgroundExecutor,
3965 cx: &mut TestAppContext,
3966) {
3967 init_test(cx, |_| {});
3968 let mut cx = EditorTestContext::new(cx).await;
3969 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3970 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3971 executor.run_until_parked();
3972
3973 cx.update_editor(|editor, window, cx| {
3974 let snapshot = editor.snapshot(window, cx);
3975 assert_eq!(
3976 snapshot
3977 .buffer_snapshot
3978 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3979 .collect::<Vec<_>>(),
3980 Vec::new(),
3981 "Should not have any diffs for files with custom newlines"
3982 );
3983 });
3984}
3985
3986#[gpui::test]
3987async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
3988 init_test(cx, |_| {});
3989
3990 let mut cx = EditorTestContext::new(cx).await;
3991
3992 // Test sort_lines_case_insensitive()
3993 cx.set_state(indoc! {"
3994 «z
3995 y
3996 x
3997 Z
3998 Y
3999 Xˇ»
4000 "});
4001 cx.update_editor(|e, window, cx| {
4002 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4003 });
4004 cx.assert_editor_state(indoc! {"
4005 «x
4006 X
4007 y
4008 Y
4009 z
4010 Zˇ»
4011 "});
4012
4013 // Test reverse_lines()
4014 cx.set_state(indoc! {"
4015 «5
4016 4
4017 3
4018 2
4019 1ˇ»
4020 "});
4021 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4022 cx.assert_editor_state(indoc! {"
4023 «1
4024 2
4025 3
4026 4
4027 5ˇ»
4028 "});
4029
4030 // Skip testing shuffle_line()
4031
4032 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4033 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4034
4035 // Don't manipulate when cursor is on single line, but expand the selection
4036 cx.set_state(indoc! {"
4037 ddˇdd
4038 ccc
4039 bb
4040 a
4041 "});
4042 cx.update_editor(|e, window, cx| {
4043 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4044 });
4045 cx.assert_editor_state(indoc! {"
4046 «ddddˇ»
4047 ccc
4048 bb
4049 a
4050 "});
4051
4052 // Basic manipulate case
4053 // Start selection moves to column 0
4054 // End of selection shrinks to fit shorter line
4055 cx.set_state(indoc! {"
4056 dd«d
4057 ccc
4058 bb
4059 aaaaaˇ»
4060 "});
4061 cx.update_editor(|e, window, cx| {
4062 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4063 });
4064 cx.assert_editor_state(indoc! {"
4065 «aaaaa
4066 bb
4067 ccc
4068 dddˇ»
4069 "});
4070
4071 // Manipulate case with newlines
4072 cx.set_state(indoc! {"
4073 dd«d
4074 ccc
4075
4076 bb
4077 aaaaa
4078
4079 ˇ»
4080 "});
4081 cx.update_editor(|e, window, cx| {
4082 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4083 });
4084 cx.assert_editor_state(indoc! {"
4085 «
4086
4087 aaaaa
4088 bb
4089 ccc
4090 dddˇ»
4091
4092 "});
4093
4094 // Adding new line
4095 cx.set_state(indoc! {"
4096 aa«a
4097 bbˇ»b
4098 "});
4099 cx.update_editor(|e, window, cx| {
4100 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4101 });
4102 cx.assert_editor_state(indoc! {"
4103 «aaa
4104 bbb
4105 added_lineˇ»
4106 "});
4107
4108 // Removing line
4109 cx.set_state(indoc! {"
4110 aa«a
4111 bbbˇ»
4112 "});
4113 cx.update_editor(|e, window, cx| {
4114 e.manipulate_immutable_lines(window, cx, |lines| {
4115 lines.pop();
4116 })
4117 });
4118 cx.assert_editor_state(indoc! {"
4119 «aaaˇ»
4120 "});
4121
4122 // Removing all lines
4123 cx.set_state(indoc! {"
4124 aa«a
4125 bbbˇ»
4126 "});
4127 cx.update_editor(|e, window, cx| {
4128 e.manipulate_immutable_lines(window, cx, |lines| {
4129 lines.drain(..);
4130 })
4131 });
4132 cx.assert_editor_state(indoc! {"
4133 ˇ
4134 "});
4135}
4136
4137#[gpui::test]
4138async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4139 init_test(cx, |_| {});
4140
4141 let mut cx = EditorTestContext::new(cx).await;
4142
4143 // Consider continuous selection as single selection
4144 cx.set_state(indoc! {"
4145 Aaa«aa
4146 cˇ»c«c
4147 bb
4148 aaaˇ»aa
4149 "});
4150 cx.update_editor(|e, window, cx| {
4151 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4152 });
4153 cx.assert_editor_state(indoc! {"
4154 «Aaaaa
4155 ccc
4156 bb
4157 aaaaaˇ»
4158 "});
4159
4160 cx.set_state(indoc! {"
4161 Aaa«aa
4162 cˇ»c«c
4163 bb
4164 aaaˇ»aa
4165 "});
4166 cx.update_editor(|e, window, cx| {
4167 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4168 });
4169 cx.assert_editor_state(indoc! {"
4170 «Aaaaa
4171 ccc
4172 bbˇ»
4173 "});
4174
4175 // Consider non continuous selection as distinct dedup operations
4176 cx.set_state(indoc! {"
4177 «aaaaa
4178 bb
4179 aaaaa
4180 aaaaaˇ»
4181
4182 aaa«aaˇ»
4183 "});
4184 cx.update_editor(|e, window, cx| {
4185 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4186 });
4187 cx.assert_editor_state(indoc! {"
4188 «aaaaa
4189 bbˇ»
4190
4191 «aaaaaˇ»
4192 "});
4193}
4194
4195#[gpui::test]
4196async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4197 init_test(cx, |_| {});
4198
4199 let mut cx = EditorTestContext::new(cx).await;
4200
4201 cx.set_state(indoc! {"
4202 «Aaa
4203 aAa
4204 Aaaˇ»
4205 "});
4206 cx.update_editor(|e, window, cx| {
4207 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4208 });
4209 cx.assert_editor_state(indoc! {"
4210 «Aaa
4211 aAaˇ»
4212 "});
4213
4214 cx.set_state(indoc! {"
4215 «Aaa
4216 aAa
4217 aaAˇ»
4218 "});
4219 cx.update_editor(|e, window, cx| {
4220 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4221 });
4222 cx.assert_editor_state(indoc! {"
4223 «Aaaˇ»
4224 "});
4225}
4226
4227#[gpui::test]
4228async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4229 init_test(cx, |_| {});
4230
4231 let mut cx = EditorTestContext::new(cx).await;
4232
4233 // Manipulate with multiple selections on a single line
4234 cx.set_state(indoc! {"
4235 dd«dd
4236 cˇ»c«c
4237 bb
4238 aaaˇ»aa
4239 "});
4240 cx.update_editor(|e, window, cx| {
4241 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4242 });
4243 cx.assert_editor_state(indoc! {"
4244 «aaaaa
4245 bb
4246 ccc
4247 ddddˇ»
4248 "});
4249
4250 // Manipulate with multiple disjoin selections
4251 cx.set_state(indoc! {"
4252 5«
4253 4
4254 3
4255 2
4256 1ˇ»
4257
4258 dd«dd
4259 ccc
4260 bb
4261 aaaˇ»aa
4262 "});
4263 cx.update_editor(|e, window, cx| {
4264 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4265 });
4266 cx.assert_editor_state(indoc! {"
4267 «1
4268 2
4269 3
4270 4
4271 5ˇ»
4272
4273 «aaaaa
4274 bb
4275 ccc
4276 ddddˇ»
4277 "});
4278
4279 // Adding lines on each selection
4280 cx.set_state(indoc! {"
4281 2«
4282 1ˇ»
4283
4284 bb«bb
4285 aaaˇ»aa
4286 "});
4287 cx.update_editor(|e, window, cx| {
4288 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4289 });
4290 cx.assert_editor_state(indoc! {"
4291 «2
4292 1
4293 added lineˇ»
4294
4295 «bbbb
4296 aaaaa
4297 added lineˇ»
4298 "});
4299
4300 // Removing lines on each selection
4301 cx.set_state(indoc! {"
4302 2«
4303 1ˇ»
4304
4305 bb«bb
4306 aaaˇ»aa
4307 "});
4308 cx.update_editor(|e, window, cx| {
4309 e.manipulate_immutable_lines(window, cx, |lines| {
4310 lines.pop();
4311 })
4312 });
4313 cx.assert_editor_state(indoc! {"
4314 «2ˇ»
4315
4316 «bbbbˇ»
4317 "});
4318}
4319
4320#[gpui::test]
4321async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4322 init_test(cx, |settings| {
4323 settings.defaults.tab_size = NonZeroU32::new(3)
4324 });
4325
4326 let mut cx = EditorTestContext::new(cx).await;
4327
4328 // MULTI SELECTION
4329 // Ln.1 "«" tests empty lines
4330 // Ln.9 tests just leading whitespace
4331 cx.set_state(indoc! {"
4332 «
4333 abc // No indentationˇ»
4334 «\tabc // 1 tabˇ»
4335 \t\tabc « ˇ» // 2 tabs
4336 \t ab«c // Tab followed by space
4337 \tabc // Space followed by tab (3 spaces should be the result)
4338 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4339 abˇ»ˇc ˇ ˇ // Already space indented«
4340 \t
4341 \tabc\tdef // Only the leading tab is manipulatedˇ»
4342 "});
4343 cx.update_editor(|e, window, cx| {
4344 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4345 });
4346 cx.assert_editor_state(
4347 indoc! {"
4348 «
4349 abc // No indentation
4350 abc // 1 tab
4351 abc // 2 tabs
4352 abc // Tab followed by space
4353 abc // Space followed by tab (3 spaces should be the result)
4354 abc // Mixed indentation (tab conversion depends on the column)
4355 abc // Already space indented
4356 ·
4357 abc\tdef // Only the leading tab is manipulatedˇ»
4358 "}
4359 .replace("·", "")
4360 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4361 );
4362
4363 // Test on just a few lines, the others should remain unchanged
4364 // Only lines (3, 5, 10, 11) should change
4365 cx.set_state(
4366 indoc! {"
4367 ·
4368 abc // No indentation
4369 \tabcˇ // 1 tab
4370 \t\tabc // 2 tabs
4371 \t abcˇ // Tab followed by space
4372 \tabc // Space followed by tab (3 spaces should be the result)
4373 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4374 abc // Already space indented
4375 «\t
4376 \tabc\tdef // Only the leading tab is manipulatedˇ»
4377 "}
4378 .replace("·", "")
4379 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4380 );
4381 cx.update_editor(|e, window, cx| {
4382 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4383 });
4384 cx.assert_editor_state(
4385 indoc! {"
4386 ·
4387 abc // No indentation
4388 « abc // 1 tabˇ»
4389 \t\tabc // 2 tabs
4390 « abc // Tab followed by spaceˇ»
4391 \tabc // Space followed by tab (3 spaces should be the result)
4392 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4393 abc // Already space indented
4394 « ·
4395 abc\tdef // Only the leading tab is manipulatedˇ»
4396 "}
4397 .replace("·", "")
4398 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4399 );
4400
4401 // SINGLE SELECTION
4402 // Ln.1 "«" tests empty lines
4403 // Ln.9 tests just leading whitespace
4404 cx.set_state(indoc! {"
4405 «
4406 abc // No indentation
4407 \tabc // 1 tab
4408 \t\tabc // 2 tabs
4409 \t abc // Tab followed by space
4410 \tabc // Space followed by tab (3 spaces should be the result)
4411 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4412 abc // Already space indented
4413 \t
4414 \tabc\tdef // Only the leading tab is manipulatedˇ»
4415 "});
4416 cx.update_editor(|e, window, cx| {
4417 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4418 });
4419 cx.assert_editor_state(
4420 indoc! {"
4421 «
4422 abc // No indentation
4423 abc // 1 tab
4424 abc // 2 tabs
4425 abc // Tab followed by space
4426 abc // Space followed by tab (3 spaces should be the result)
4427 abc // Mixed indentation (tab conversion depends on the column)
4428 abc // Already space indented
4429 ·
4430 abc\tdef // Only the leading tab is manipulatedˇ»
4431 "}
4432 .replace("·", "")
4433 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4434 );
4435}
4436
4437#[gpui::test]
4438async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4439 init_test(cx, |settings| {
4440 settings.defaults.tab_size = NonZeroU32::new(3)
4441 });
4442
4443 let mut cx = EditorTestContext::new(cx).await;
4444
4445 // MULTI SELECTION
4446 // Ln.1 "«" tests empty lines
4447 // Ln.11 tests just leading whitespace
4448 cx.set_state(indoc! {"
4449 «
4450 abˇ»ˇc // No indentation
4451 abc ˇ ˇ // 1 space (< 3 so dont convert)
4452 abc « // 2 spaces (< 3 so dont convert)
4453 abc // 3 spaces (convert)
4454 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4455 «\tˇ»\t«\tˇ»abc // Already tab indented
4456 «\t abc // Tab followed by space
4457 \tabc // Space followed by tab (should be consumed due to tab)
4458 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4459 \tˇ» «\t
4460 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4461 "});
4462 cx.update_editor(|e, window, cx| {
4463 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4464 });
4465 cx.assert_editor_state(indoc! {"
4466 «
4467 abc // No indentation
4468 abc // 1 space (< 3 so dont convert)
4469 abc // 2 spaces (< 3 so dont convert)
4470 \tabc // 3 spaces (convert)
4471 \t abc // 5 spaces (1 tab + 2 spaces)
4472 \t\t\tabc // Already tab indented
4473 \t abc // Tab followed by space
4474 \tabc // Space followed by tab (should be consumed due to tab)
4475 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4476 \t\t\t
4477 \tabc \t // Only the leading spaces should be convertedˇ»
4478 "});
4479
4480 // Test on just a few lines, the other should remain unchanged
4481 // Only lines (4, 8, 11, 12) should change
4482 cx.set_state(
4483 indoc! {"
4484 ·
4485 abc // No indentation
4486 abc // 1 space (< 3 so dont convert)
4487 abc // 2 spaces (< 3 so dont convert)
4488 « abc // 3 spaces (convert)ˇ»
4489 abc // 5 spaces (1 tab + 2 spaces)
4490 \t\t\tabc // Already tab indented
4491 \t abc // Tab followed by space
4492 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4493 \t\t \tabc // Mixed indentation
4494 \t \t \t \tabc // Mixed indentation
4495 \t \tˇ
4496 « abc \t // Only the leading spaces should be convertedˇ»
4497 "}
4498 .replace("·", "")
4499 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4500 );
4501 cx.update_editor(|e, window, cx| {
4502 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4503 });
4504 cx.assert_editor_state(
4505 indoc! {"
4506 ·
4507 abc // No indentation
4508 abc // 1 space (< 3 so dont convert)
4509 abc // 2 spaces (< 3 so dont convert)
4510 «\tabc // 3 spaces (convert)ˇ»
4511 abc // 5 spaces (1 tab + 2 spaces)
4512 \t\t\tabc // Already tab indented
4513 \t abc // Tab followed by space
4514 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4515 \t\t \tabc // Mixed indentation
4516 \t \t \t \tabc // Mixed indentation
4517 «\t\t\t
4518 \tabc \t // Only the leading spaces should be convertedˇ»
4519 "}
4520 .replace("·", "")
4521 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4522 );
4523
4524 // SINGLE SELECTION
4525 // Ln.1 "«" tests empty lines
4526 // Ln.11 tests just leading whitespace
4527 cx.set_state(indoc! {"
4528 «
4529 abc // No indentation
4530 abc // 1 space (< 3 so dont convert)
4531 abc // 2 spaces (< 3 so dont convert)
4532 abc // 3 spaces (convert)
4533 abc // 5 spaces (1 tab + 2 spaces)
4534 \t\t\tabc // Already tab indented
4535 \t abc // Tab followed by space
4536 \tabc // Space followed by tab (should be consumed due to tab)
4537 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4538 \t \t
4539 abc \t // Only the leading spaces should be convertedˇ»
4540 "});
4541 cx.update_editor(|e, window, cx| {
4542 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4543 });
4544 cx.assert_editor_state(indoc! {"
4545 «
4546 abc // No indentation
4547 abc // 1 space (< 3 so dont convert)
4548 abc // 2 spaces (< 3 so dont convert)
4549 \tabc // 3 spaces (convert)
4550 \t abc // 5 spaces (1 tab + 2 spaces)
4551 \t\t\tabc // Already tab indented
4552 \t abc // Tab followed by space
4553 \tabc // Space followed by tab (should be consumed due to tab)
4554 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4555 \t\t\t
4556 \tabc \t // Only the leading spaces should be convertedˇ»
4557 "});
4558}
4559
4560#[gpui::test]
4561async fn test_toggle_case(cx: &mut TestAppContext) {
4562 init_test(cx, |_| {});
4563
4564 let mut cx = EditorTestContext::new(cx).await;
4565
4566 // If all lower case -> upper case
4567 cx.set_state(indoc! {"
4568 «hello worldˇ»
4569 "});
4570 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4571 cx.assert_editor_state(indoc! {"
4572 «HELLO WORLDˇ»
4573 "});
4574
4575 // If all upper case -> lower case
4576 cx.set_state(indoc! {"
4577 «HELLO WORLDˇ»
4578 "});
4579 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4580 cx.assert_editor_state(indoc! {"
4581 «hello worldˇ»
4582 "});
4583
4584 // If any upper case characters are identified -> lower case
4585 // This matches JetBrains IDEs
4586 cx.set_state(indoc! {"
4587 «hEllo worldˇ»
4588 "});
4589 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4590 cx.assert_editor_state(indoc! {"
4591 «hello worldˇ»
4592 "});
4593}
4594
4595#[gpui::test]
4596async fn test_manipulate_text(cx: &mut TestAppContext) {
4597 init_test(cx, |_| {});
4598
4599 let mut cx = EditorTestContext::new(cx).await;
4600
4601 // Test convert_to_upper_case()
4602 cx.set_state(indoc! {"
4603 «hello worldˇ»
4604 "});
4605 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4606 cx.assert_editor_state(indoc! {"
4607 «HELLO WORLDˇ»
4608 "});
4609
4610 // Test convert_to_lower_case()
4611 cx.set_state(indoc! {"
4612 «HELLO WORLDˇ»
4613 "});
4614 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4615 cx.assert_editor_state(indoc! {"
4616 «hello worldˇ»
4617 "});
4618
4619 // Test multiple line, single selection case
4620 cx.set_state(indoc! {"
4621 «The quick brown
4622 fox jumps over
4623 the lazy dogˇ»
4624 "});
4625 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4626 cx.assert_editor_state(indoc! {"
4627 «The Quick Brown
4628 Fox Jumps Over
4629 The Lazy Dogˇ»
4630 "});
4631
4632 // Test multiple line, single selection case
4633 cx.set_state(indoc! {"
4634 «The quick brown
4635 fox jumps over
4636 the lazy dogˇ»
4637 "});
4638 cx.update_editor(|e, window, cx| {
4639 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4640 });
4641 cx.assert_editor_state(indoc! {"
4642 «TheQuickBrown
4643 FoxJumpsOver
4644 TheLazyDogˇ»
4645 "});
4646
4647 // From here on out, test more complex cases of manipulate_text()
4648
4649 // Test no selection case - should affect words cursors are in
4650 // Cursor at beginning, middle, and end of word
4651 cx.set_state(indoc! {"
4652 ˇhello big beauˇtiful worldˇ
4653 "});
4654 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4655 cx.assert_editor_state(indoc! {"
4656 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4657 "});
4658
4659 // Test multiple selections on a single line and across multiple lines
4660 cx.set_state(indoc! {"
4661 «Theˇ» quick «brown
4662 foxˇ» jumps «overˇ»
4663 the «lazyˇ» dog
4664 "});
4665 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4666 cx.assert_editor_state(indoc! {"
4667 «THEˇ» quick «BROWN
4668 FOXˇ» jumps «OVERˇ»
4669 the «LAZYˇ» dog
4670 "});
4671
4672 // Test case where text length grows
4673 cx.set_state(indoc! {"
4674 «tschüߡ»
4675 "});
4676 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4677 cx.assert_editor_state(indoc! {"
4678 «TSCHÜSSˇ»
4679 "});
4680
4681 // Test to make sure we don't crash when text shrinks
4682 cx.set_state(indoc! {"
4683 aaa_bbbˇ
4684 "});
4685 cx.update_editor(|e, window, cx| {
4686 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4687 });
4688 cx.assert_editor_state(indoc! {"
4689 «aaaBbbˇ»
4690 "});
4691
4692 // Test to make sure we all aware of the fact that each word can grow and shrink
4693 // Final selections should be aware of this fact
4694 cx.set_state(indoc! {"
4695 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4696 "});
4697 cx.update_editor(|e, window, cx| {
4698 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4699 });
4700 cx.assert_editor_state(indoc! {"
4701 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4702 "});
4703
4704 cx.set_state(indoc! {"
4705 «hElLo, WoRld!ˇ»
4706 "});
4707 cx.update_editor(|e, window, cx| {
4708 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4709 });
4710 cx.assert_editor_state(indoc! {"
4711 «HeLlO, wOrLD!ˇ»
4712 "});
4713}
4714
4715#[gpui::test]
4716fn test_duplicate_line(cx: &mut TestAppContext) {
4717 init_test(cx, |_| {});
4718
4719 let editor = cx.add_window(|window, cx| {
4720 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4721 build_editor(buffer, window, cx)
4722 });
4723 _ = editor.update(cx, |editor, window, cx| {
4724 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4725 s.select_display_ranges([
4726 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4727 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4728 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4729 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4730 ])
4731 });
4732 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4733 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4734 assert_eq!(
4735 editor.selections.display_ranges(cx),
4736 vec![
4737 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4738 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4739 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4740 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4741 ]
4742 );
4743 });
4744
4745 let editor = cx.add_window(|window, cx| {
4746 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4747 build_editor(buffer, window, cx)
4748 });
4749 _ = editor.update(cx, |editor, window, cx| {
4750 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4751 s.select_display_ranges([
4752 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4753 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4754 ])
4755 });
4756 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4757 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4758 assert_eq!(
4759 editor.selections.display_ranges(cx),
4760 vec![
4761 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4762 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4763 ]
4764 );
4765 });
4766
4767 // With `move_upwards` the selections stay in place, except for
4768 // the lines inserted above them
4769 let editor = cx.add_window(|window, cx| {
4770 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4771 build_editor(buffer, window, cx)
4772 });
4773 _ = editor.update(cx, |editor, window, cx| {
4774 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4775 s.select_display_ranges([
4776 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4777 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4778 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4779 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4780 ])
4781 });
4782 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4783 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4784 assert_eq!(
4785 editor.selections.display_ranges(cx),
4786 vec![
4787 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4788 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4789 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4790 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4791 ]
4792 );
4793 });
4794
4795 let editor = cx.add_window(|window, cx| {
4796 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4797 build_editor(buffer, window, cx)
4798 });
4799 _ = editor.update(cx, |editor, window, cx| {
4800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4801 s.select_display_ranges([
4802 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4803 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4804 ])
4805 });
4806 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4807 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4808 assert_eq!(
4809 editor.selections.display_ranges(cx),
4810 vec![
4811 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4812 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4813 ]
4814 );
4815 });
4816
4817 let editor = cx.add_window(|window, cx| {
4818 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4819 build_editor(buffer, window, cx)
4820 });
4821 _ = editor.update(cx, |editor, window, cx| {
4822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4823 s.select_display_ranges([
4824 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4825 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4826 ])
4827 });
4828 editor.duplicate_selection(&DuplicateSelection, window, cx);
4829 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4830 assert_eq!(
4831 editor.selections.display_ranges(cx),
4832 vec![
4833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4834 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4835 ]
4836 );
4837 });
4838}
4839
4840#[gpui::test]
4841fn test_move_line_up_down(cx: &mut TestAppContext) {
4842 init_test(cx, |_| {});
4843
4844 let editor = cx.add_window(|window, cx| {
4845 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4846 build_editor(buffer, window, cx)
4847 });
4848 _ = editor.update(cx, |editor, window, cx| {
4849 editor.fold_creases(
4850 vec![
4851 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4852 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4853 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4854 ],
4855 true,
4856 window,
4857 cx,
4858 );
4859 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4860 s.select_display_ranges([
4861 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4862 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4863 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4864 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4865 ])
4866 });
4867 assert_eq!(
4868 editor.display_text(cx),
4869 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4870 );
4871
4872 editor.move_line_up(&MoveLineUp, window, cx);
4873 assert_eq!(
4874 editor.display_text(cx),
4875 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4876 );
4877 assert_eq!(
4878 editor.selections.display_ranges(cx),
4879 vec![
4880 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4881 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4882 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4883 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4884 ]
4885 );
4886 });
4887
4888 _ = editor.update(cx, |editor, window, cx| {
4889 editor.move_line_down(&MoveLineDown, window, cx);
4890 assert_eq!(
4891 editor.display_text(cx),
4892 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4893 );
4894 assert_eq!(
4895 editor.selections.display_ranges(cx),
4896 vec![
4897 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4898 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4899 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4900 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4901 ]
4902 );
4903 });
4904
4905 _ = editor.update(cx, |editor, window, cx| {
4906 editor.move_line_down(&MoveLineDown, window, cx);
4907 assert_eq!(
4908 editor.display_text(cx),
4909 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4910 );
4911 assert_eq!(
4912 editor.selections.display_ranges(cx),
4913 vec![
4914 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4915 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4916 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4917 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4918 ]
4919 );
4920 });
4921
4922 _ = editor.update(cx, |editor, window, cx| {
4923 editor.move_line_up(&MoveLineUp, window, cx);
4924 assert_eq!(
4925 editor.display_text(cx),
4926 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4927 );
4928 assert_eq!(
4929 editor.selections.display_ranges(cx),
4930 vec![
4931 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4932 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4933 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4934 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4935 ]
4936 );
4937 });
4938}
4939
4940#[gpui::test]
4941fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4942 init_test(cx, |_| {});
4943
4944 let editor = cx.add_window(|window, cx| {
4945 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4946 build_editor(buffer, window, cx)
4947 });
4948 _ = editor.update(cx, |editor, window, cx| {
4949 let snapshot = editor.buffer.read(cx).snapshot(cx);
4950 editor.insert_blocks(
4951 [BlockProperties {
4952 style: BlockStyle::Fixed,
4953 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4954 height: Some(1),
4955 render: Arc::new(|_| div().into_any()),
4956 priority: 0,
4957 render_in_minimap: true,
4958 }],
4959 Some(Autoscroll::fit()),
4960 cx,
4961 );
4962 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4963 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4964 });
4965 editor.move_line_down(&MoveLineDown, window, cx);
4966 });
4967}
4968
4969#[gpui::test]
4970async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4971 init_test(cx, |_| {});
4972
4973 let mut cx = EditorTestContext::new(cx).await;
4974 cx.set_state(
4975 &"
4976 ˇzero
4977 one
4978 two
4979 three
4980 four
4981 five
4982 "
4983 .unindent(),
4984 );
4985
4986 // Create a four-line block that replaces three lines of text.
4987 cx.update_editor(|editor, window, cx| {
4988 let snapshot = editor.snapshot(window, cx);
4989 let snapshot = &snapshot.buffer_snapshot;
4990 let placement = BlockPlacement::Replace(
4991 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4992 );
4993 editor.insert_blocks(
4994 [BlockProperties {
4995 placement,
4996 height: Some(4),
4997 style: BlockStyle::Sticky,
4998 render: Arc::new(|_| gpui::div().into_any_element()),
4999 priority: 0,
5000 render_in_minimap: true,
5001 }],
5002 None,
5003 cx,
5004 );
5005 });
5006
5007 // Move down so that the cursor touches the block.
5008 cx.update_editor(|editor, window, cx| {
5009 editor.move_down(&Default::default(), window, cx);
5010 });
5011 cx.assert_editor_state(
5012 &"
5013 zero
5014 «one
5015 two
5016 threeˇ»
5017 four
5018 five
5019 "
5020 .unindent(),
5021 );
5022
5023 // Move down past the block.
5024 cx.update_editor(|editor, window, cx| {
5025 editor.move_down(&Default::default(), window, cx);
5026 });
5027 cx.assert_editor_state(
5028 &"
5029 zero
5030 one
5031 two
5032 three
5033 ˇfour
5034 five
5035 "
5036 .unindent(),
5037 );
5038}
5039
5040#[gpui::test]
5041fn test_transpose(cx: &mut TestAppContext) {
5042 init_test(cx, |_| {});
5043
5044 _ = cx.add_window(|window, cx| {
5045 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5046 editor.set_style(EditorStyle::default(), window, cx);
5047 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5048 s.select_ranges([1..1])
5049 });
5050 editor.transpose(&Default::default(), window, cx);
5051 assert_eq!(editor.text(cx), "bac");
5052 assert_eq!(editor.selections.ranges(cx), [2..2]);
5053
5054 editor.transpose(&Default::default(), window, cx);
5055 assert_eq!(editor.text(cx), "bca");
5056 assert_eq!(editor.selections.ranges(cx), [3..3]);
5057
5058 editor.transpose(&Default::default(), window, cx);
5059 assert_eq!(editor.text(cx), "bac");
5060 assert_eq!(editor.selections.ranges(cx), [3..3]);
5061
5062 editor
5063 });
5064
5065 _ = cx.add_window(|window, cx| {
5066 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5067 editor.set_style(EditorStyle::default(), window, cx);
5068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5069 s.select_ranges([3..3])
5070 });
5071 editor.transpose(&Default::default(), window, cx);
5072 assert_eq!(editor.text(cx), "acb\nde");
5073 assert_eq!(editor.selections.ranges(cx), [3..3]);
5074
5075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5076 s.select_ranges([4..4])
5077 });
5078 editor.transpose(&Default::default(), window, cx);
5079 assert_eq!(editor.text(cx), "acbd\ne");
5080 assert_eq!(editor.selections.ranges(cx), [5..5]);
5081
5082 editor.transpose(&Default::default(), window, cx);
5083 assert_eq!(editor.text(cx), "acbde\n");
5084 assert_eq!(editor.selections.ranges(cx), [6..6]);
5085
5086 editor.transpose(&Default::default(), window, cx);
5087 assert_eq!(editor.text(cx), "acbd\ne");
5088 assert_eq!(editor.selections.ranges(cx), [6..6]);
5089
5090 editor
5091 });
5092
5093 _ = cx.add_window(|window, cx| {
5094 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5095 editor.set_style(EditorStyle::default(), window, cx);
5096 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5097 s.select_ranges([1..1, 2..2, 4..4])
5098 });
5099 editor.transpose(&Default::default(), window, cx);
5100 assert_eq!(editor.text(cx), "bacd\ne");
5101 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5102
5103 editor.transpose(&Default::default(), window, cx);
5104 assert_eq!(editor.text(cx), "bcade\n");
5105 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5106
5107 editor.transpose(&Default::default(), window, cx);
5108 assert_eq!(editor.text(cx), "bcda\ne");
5109 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5110
5111 editor.transpose(&Default::default(), window, cx);
5112 assert_eq!(editor.text(cx), "bcade\n");
5113 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5114
5115 editor.transpose(&Default::default(), window, cx);
5116 assert_eq!(editor.text(cx), "bcaed\n");
5117 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5118
5119 editor
5120 });
5121
5122 _ = cx.add_window(|window, cx| {
5123 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5124 editor.set_style(EditorStyle::default(), window, cx);
5125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5126 s.select_ranges([4..4])
5127 });
5128 editor.transpose(&Default::default(), window, cx);
5129 assert_eq!(editor.text(cx), "🏀🍐✋");
5130 assert_eq!(editor.selections.ranges(cx), [8..8]);
5131
5132 editor.transpose(&Default::default(), window, cx);
5133 assert_eq!(editor.text(cx), "🏀✋🍐");
5134 assert_eq!(editor.selections.ranges(cx), [11..11]);
5135
5136 editor.transpose(&Default::default(), window, cx);
5137 assert_eq!(editor.text(cx), "🏀🍐✋");
5138 assert_eq!(editor.selections.ranges(cx), [11..11]);
5139
5140 editor
5141 });
5142}
5143
5144#[gpui::test]
5145async fn test_rewrap(cx: &mut TestAppContext) {
5146 init_test(cx, |settings| {
5147 settings.languages.extend([
5148 (
5149 "Markdown".into(),
5150 LanguageSettingsContent {
5151 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5152 ..Default::default()
5153 },
5154 ),
5155 (
5156 "Plain Text".into(),
5157 LanguageSettingsContent {
5158 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5159 ..Default::default()
5160 },
5161 ),
5162 ])
5163 });
5164
5165 let mut cx = EditorTestContext::new(cx).await;
5166
5167 let language_with_c_comments = Arc::new(Language::new(
5168 LanguageConfig {
5169 line_comments: vec!["// ".into()],
5170 ..LanguageConfig::default()
5171 },
5172 None,
5173 ));
5174 let language_with_pound_comments = Arc::new(Language::new(
5175 LanguageConfig {
5176 line_comments: vec!["# ".into()],
5177 ..LanguageConfig::default()
5178 },
5179 None,
5180 ));
5181 let markdown_language = Arc::new(Language::new(
5182 LanguageConfig {
5183 name: "Markdown".into(),
5184 ..LanguageConfig::default()
5185 },
5186 None,
5187 ));
5188 let language_with_doc_comments = Arc::new(Language::new(
5189 LanguageConfig {
5190 line_comments: vec!["// ".into(), "/// ".into()],
5191 ..LanguageConfig::default()
5192 },
5193 Some(tree_sitter_rust::LANGUAGE.into()),
5194 ));
5195
5196 let plaintext_language = Arc::new(Language::new(
5197 LanguageConfig {
5198 name: "Plain Text".into(),
5199 ..LanguageConfig::default()
5200 },
5201 None,
5202 ));
5203
5204 assert_rewrap(
5205 indoc! {"
5206 // ˇ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.
5207 "},
5208 indoc! {"
5209 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5210 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5211 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
5212 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5213 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
5214 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5215 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5216 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5217 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5218 // porttitor id. Aliquam id accumsan eros.
5219 "},
5220 language_with_c_comments.clone(),
5221 &mut cx,
5222 );
5223
5224 // Test that rewrapping works inside of a selection
5225 assert_rewrap(
5226 indoc! {"
5227 «// 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.ˇ»
5228 "},
5229 indoc! {"
5230 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5231 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5232 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
5233 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5234 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
5235 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5236 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5237 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5238 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5239 // porttitor id. Aliquam id accumsan eros.ˇ»
5240 "},
5241 language_with_c_comments.clone(),
5242 &mut cx,
5243 );
5244
5245 // Test that cursors that expand to the same region are collapsed.
5246 assert_rewrap(
5247 indoc! {"
5248 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5249 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5250 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5251 // ˇ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.
5252 "},
5253 indoc! {"
5254 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5255 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5256 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
5257 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5258 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5259 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5260 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5261 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5262 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5263 // porttitor id. Aliquam id accumsan eros.
5264 "},
5265 language_with_c_comments.clone(),
5266 &mut cx,
5267 );
5268
5269 // Test that non-contiguous selections are treated separately.
5270 assert_rewrap(
5271 indoc! {"
5272 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5273 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5274 //
5275 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5276 // ˇ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.
5277 "},
5278 indoc! {"
5279 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5280 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5281 // auctor, eu lacinia sapien scelerisque.
5282 //
5283 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5284 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5285 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5286 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5287 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5288 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5289 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5290 "},
5291 language_with_c_comments.clone(),
5292 &mut cx,
5293 );
5294
5295 // Test that different comment prefixes are supported.
5296 assert_rewrap(
5297 indoc! {"
5298 # ˇ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.
5299 "},
5300 indoc! {"
5301 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5302 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5303 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5304 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5305 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5306 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5307 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5308 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5309 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5310 # accumsan eros.
5311 "},
5312 language_with_pound_comments.clone(),
5313 &mut cx,
5314 );
5315
5316 // Test that rewrapping is ignored outside of comments in most languages.
5317 assert_rewrap(
5318 indoc! {"
5319 /// Adds two numbers.
5320 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5321 fn add(a: u32, b: u32) -> u32 {
5322 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ˇ
5323 }
5324 "},
5325 indoc! {"
5326 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5327 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5328 fn add(a: u32, b: u32) -> u32 {
5329 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ˇ
5330 }
5331 "},
5332 language_with_doc_comments.clone(),
5333 &mut cx,
5334 );
5335
5336 // Test that rewrapping works in Markdown and Plain Text languages.
5337 assert_rewrap(
5338 indoc! {"
5339 # Hello
5340
5341 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.
5342 "},
5343 indoc! {"
5344 # Hello
5345
5346 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5347 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5348 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5349 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5350 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5351 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5352 Integer sit amet scelerisque nisi.
5353 "},
5354 markdown_language,
5355 &mut cx,
5356 );
5357
5358 assert_rewrap(
5359 indoc! {"
5360 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.
5361 "},
5362 indoc! {"
5363 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5364 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5365 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5366 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5367 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5368 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5369 Integer sit amet scelerisque nisi.
5370 "},
5371 plaintext_language.clone(),
5372 &mut cx,
5373 );
5374
5375 // Test rewrapping unaligned comments in a selection.
5376 assert_rewrap(
5377 indoc! {"
5378 fn foo() {
5379 if true {
5380 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5381 // Praesent semper egestas tellus id dignissim.ˇ»
5382 do_something();
5383 } else {
5384 //
5385 }
5386 }
5387 "},
5388 indoc! {"
5389 fn foo() {
5390 if true {
5391 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5392 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5393 // egestas tellus id dignissim.ˇ»
5394 do_something();
5395 } else {
5396 //
5397 }
5398 }
5399 "},
5400 language_with_doc_comments.clone(),
5401 &mut cx,
5402 );
5403
5404 assert_rewrap(
5405 indoc! {"
5406 fn foo() {
5407 if true {
5408 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5409 // Praesent semper egestas tellus id dignissim.»
5410 do_something();
5411 } else {
5412 //
5413 }
5414
5415 }
5416 "},
5417 indoc! {"
5418 fn foo() {
5419 if true {
5420 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5421 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5422 // egestas tellus id dignissim.»
5423 do_something();
5424 } else {
5425 //
5426 }
5427
5428 }
5429 "},
5430 language_with_doc_comments.clone(),
5431 &mut cx,
5432 );
5433
5434 assert_rewrap(
5435 indoc! {"
5436 «ˇone one one one one one one one one one one one one one one one one one one one one one one one one
5437
5438 two»
5439
5440 three
5441
5442 «ˇ\t
5443
5444 four four four four four four four four four four four four four four four four four four four four»
5445
5446 «ˇfive five five five five five five five five five five five five five five five five five five five
5447 \t»
5448 six six six six six six six six six six six six six six six six six six six six six six six six six
5449 "},
5450 indoc! {"
5451 «ˇone one one one one one one one one one one one one one one one one one one one
5452 one one one one one
5453
5454 two»
5455
5456 three
5457
5458 «ˇ\t
5459
5460 four four four four four four four four four four four four four four four four
5461 four four four four»
5462
5463 «ˇfive five five five five five five five five five five five five five five five
5464 five five five five
5465 \t»
5466 six six six six six six six six six six six six six six six six six six six six six six six six six
5467 "},
5468 plaintext_language.clone(),
5469 &mut cx,
5470 );
5471
5472 assert_rewrap(
5473 indoc! {"
5474 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5475 //ˇ
5476 //ˇ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
5477 //ˇ short short short
5478 int main(void) {
5479 return 17;
5480 }
5481 "},
5482 indoc! {"
5483 //ˇ long long long long long long long long long long long long long long long
5484 // long long long long long long long long long long long long long
5485 //ˇ
5486 //ˇ long long long long long long long long long long long long long long long
5487 //ˇ long long long long long long long long long long long long long short short
5488 // short
5489 int main(void) {
5490 return 17;
5491 }
5492 "},
5493 language_with_c_comments,
5494 &mut cx,
5495 );
5496
5497 #[track_caller]
5498 fn assert_rewrap(
5499 unwrapped_text: &str,
5500 wrapped_text: &str,
5501 language: Arc<Language>,
5502 cx: &mut EditorTestContext,
5503 ) {
5504 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5505 cx.set_state(unwrapped_text);
5506 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5507 cx.assert_editor_state(wrapped_text);
5508 }
5509}
5510
5511#[gpui::test]
5512async fn test_hard_wrap(cx: &mut TestAppContext) {
5513 init_test(cx, |_| {});
5514 let mut cx = EditorTestContext::new(cx).await;
5515
5516 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5517 cx.update_editor(|editor, _, cx| {
5518 editor.set_hard_wrap(Some(14), cx);
5519 });
5520
5521 cx.set_state(indoc!(
5522 "
5523 one two three ˇ
5524 "
5525 ));
5526 cx.simulate_input("four");
5527 cx.run_until_parked();
5528
5529 cx.assert_editor_state(indoc!(
5530 "
5531 one two three
5532 fourˇ
5533 "
5534 ));
5535
5536 cx.update_editor(|editor, window, cx| {
5537 editor.newline(&Default::default(), window, cx);
5538 });
5539 cx.run_until_parked();
5540 cx.assert_editor_state(indoc!(
5541 "
5542 one two three
5543 four
5544 ˇ
5545 "
5546 ));
5547
5548 cx.simulate_input("five");
5549 cx.run_until_parked();
5550 cx.assert_editor_state(indoc!(
5551 "
5552 one two three
5553 four
5554 fiveˇ
5555 "
5556 ));
5557
5558 cx.update_editor(|editor, window, cx| {
5559 editor.newline(&Default::default(), window, cx);
5560 });
5561 cx.run_until_parked();
5562 cx.simulate_input("# ");
5563 cx.run_until_parked();
5564 cx.assert_editor_state(indoc!(
5565 "
5566 one two three
5567 four
5568 five
5569 # ˇ
5570 "
5571 ));
5572
5573 cx.update_editor(|editor, window, cx| {
5574 editor.newline(&Default::default(), window, cx);
5575 });
5576 cx.run_until_parked();
5577 cx.assert_editor_state(indoc!(
5578 "
5579 one two three
5580 four
5581 five
5582 #\x20
5583 #ˇ
5584 "
5585 ));
5586
5587 cx.simulate_input(" 6");
5588 cx.run_until_parked();
5589 cx.assert_editor_state(indoc!(
5590 "
5591 one two three
5592 four
5593 five
5594 #
5595 # 6ˇ
5596 "
5597 ));
5598}
5599
5600#[gpui::test]
5601async fn test_clipboard(cx: &mut TestAppContext) {
5602 init_test(cx, |_| {});
5603
5604 let mut cx = EditorTestContext::new(cx).await;
5605
5606 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5607 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5608 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5609
5610 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5611 cx.set_state("two ˇfour ˇsix ˇ");
5612 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5613 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5614
5615 // Paste again but with only two cursors. Since the number of cursors doesn't
5616 // match the number of slices in the clipboard, the entire clipboard text
5617 // is pasted at each cursor.
5618 cx.set_state("ˇtwo one✅ four three six five ˇ");
5619 cx.update_editor(|e, window, cx| {
5620 e.handle_input("( ", window, cx);
5621 e.paste(&Paste, window, cx);
5622 e.handle_input(") ", window, cx);
5623 });
5624 cx.assert_editor_state(
5625 &([
5626 "( one✅ ",
5627 "three ",
5628 "five ) ˇtwo one✅ four three six five ( one✅ ",
5629 "three ",
5630 "five ) ˇ",
5631 ]
5632 .join("\n")),
5633 );
5634
5635 // Cut with three selections, one of which is full-line.
5636 cx.set_state(indoc! {"
5637 1«2ˇ»3
5638 4ˇ567
5639 «8ˇ»9"});
5640 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5641 cx.assert_editor_state(indoc! {"
5642 1ˇ3
5643 ˇ9"});
5644
5645 // Paste with three selections, noticing how the copied selection that was full-line
5646 // gets inserted before the second cursor.
5647 cx.set_state(indoc! {"
5648 1ˇ3
5649 9ˇ
5650 «oˇ»ne"});
5651 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5652 cx.assert_editor_state(indoc! {"
5653 12ˇ3
5654 4567
5655 9ˇ
5656 8ˇne"});
5657
5658 // Copy with a single cursor only, which writes the whole line into the clipboard.
5659 cx.set_state(indoc! {"
5660 The quick brown
5661 fox juˇmps over
5662 the lazy dog"});
5663 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5664 assert_eq!(
5665 cx.read_from_clipboard()
5666 .and_then(|item| item.text().as_deref().map(str::to_string)),
5667 Some("fox jumps over\n".to_string())
5668 );
5669
5670 // Paste with three selections, noticing how the copied full-line selection is inserted
5671 // before the empty selections but replaces the selection that is non-empty.
5672 cx.set_state(indoc! {"
5673 Tˇhe quick brown
5674 «foˇ»x jumps over
5675 tˇhe lazy dog"});
5676 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5677 cx.assert_editor_state(indoc! {"
5678 fox jumps over
5679 Tˇhe quick brown
5680 fox jumps over
5681 ˇx jumps over
5682 fox jumps over
5683 tˇhe lazy dog"});
5684}
5685
5686#[gpui::test]
5687async fn test_copy_trim(cx: &mut TestAppContext) {
5688 init_test(cx, |_| {});
5689
5690 let mut cx = EditorTestContext::new(cx).await;
5691 cx.set_state(
5692 r#" «for selection in selections.iter() {
5693 let mut start = selection.start;
5694 let mut end = selection.end;
5695 let is_entire_line = selection.is_empty();
5696 if is_entire_line {
5697 start = Point::new(start.row, 0);ˇ»
5698 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5699 }
5700 "#,
5701 );
5702 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5703 assert_eq!(
5704 cx.read_from_clipboard()
5705 .and_then(|item| item.text().as_deref().map(str::to_string)),
5706 Some(
5707 "for selection in selections.iter() {
5708 let mut start = selection.start;
5709 let mut end = selection.end;
5710 let is_entire_line = selection.is_empty();
5711 if is_entire_line {
5712 start = Point::new(start.row, 0);"
5713 .to_string()
5714 ),
5715 "Regular copying preserves all indentation selected",
5716 );
5717 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5718 assert_eq!(
5719 cx.read_from_clipboard()
5720 .and_then(|item| item.text().as_deref().map(str::to_string)),
5721 Some(
5722 "for selection in selections.iter() {
5723let mut start = selection.start;
5724let mut end = selection.end;
5725let is_entire_line = selection.is_empty();
5726if is_entire_line {
5727 start = Point::new(start.row, 0);"
5728 .to_string()
5729 ),
5730 "Copying with stripping should strip all leading whitespaces"
5731 );
5732
5733 cx.set_state(
5734 r#" « for selection in selections.iter() {
5735 let mut start = selection.start;
5736 let mut end = selection.end;
5737 let is_entire_line = selection.is_empty();
5738 if is_entire_line {
5739 start = Point::new(start.row, 0);ˇ»
5740 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5741 }
5742 "#,
5743 );
5744 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5745 assert_eq!(
5746 cx.read_from_clipboard()
5747 .and_then(|item| item.text().as_deref().map(str::to_string)),
5748 Some(
5749 " for selection in selections.iter() {
5750 let mut start = selection.start;
5751 let mut end = selection.end;
5752 let is_entire_line = selection.is_empty();
5753 if is_entire_line {
5754 start = Point::new(start.row, 0);"
5755 .to_string()
5756 ),
5757 "Regular copying preserves all indentation selected",
5758 );
5759 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5760 assert_eq!(
5761 cx.read_from_clipboard()
5762 .and_then(|item| item.text().as_deref().map(str::to_string)),
5763 Some(
5764 "for selection in selections.iter() {
5765let mut start = selection.start;
5766let mut end = selection.end;
5767let is_entire_line = selection.is_empty();
5768if is_entire_line {
5769 start = Point::new(start.row, 0);"
5770 .to_string()
5771 ),
5772 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5773 );
5774
5775 cx.set_state(
5776 r#" «ˇ for selection in selections.iter() {
5777 let mut start = selection.start;
5778 let mut end = selection.end;
5779 let is_entire_line = selection.is_empty();
5780 if is_entire_line {
5781 start = Point::new(start.row, 0);»
5782 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5783 }
5784 "#,
5785 );
5786 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5787 assert_eq!(
5788 cx.read_from_clipboard()
5789 .and_then(|item| item.text().as_deref().map(str::to_string)),
5790 Some(
5791 " for selection in selections.iter() {
5792 let mut start = selection.start;
5793 let mut end = selection.end;
5794 let is_entire_line = selection.is_empty();
5795 if is_entire_line {
5796 start = Point::new(start.row, 0);"
5797 .to_string()
5798 ),
5799 "Regular copying for reverse selection works the same",
5800 );
5801 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5802 assert_eq!(
5803 cx.read_from_clipboard()
5804 .and_then(|item| item.text().as_deref().map(str::to_string)),
5805 Some(
5806 "for selection in selections.iter() {
5807let mut start = selection.start;
5808let mut end = selection.end;
5809let is_entire_line = selection.is_empty();
5810if is_entire_line {
5811 start = Point::new(start.row, 0);"
5812 .to_string()
5813 ),
5814 "Copying with stripping for reverse selection works the same"
5815 );
5816
5817 cx.set_state(
5818 r#" for selection «in selections.iter() {
5819 let mut start = selection.start;
5820 let mut end = selection.end;
5821 let is_entire_line = selection.is_empty();
5822 if is_entire_line {
5823 start = Point::new(start.row, 0);ˇ»
5824 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5825 }
5826 "#,
5827 );
5828 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5829 assert_eq!(
5830 cx.read_from_clipboard()
5831 .and_then(|item| item.text().as_deref().map(str::to_string)),
5832 Some(
5833 "in selections.iter() {
5834 let mut start = selection.start;
5835 let mut end = selection.end;
5836 let is_entire_line = selection.is_empty();
5837 if is_entire_line {
5838 start = Point::new(start.row, 0);"
5839 .to_string()
5840 ),
5841 "When selecting past the indent, the copying works as usual",
5842 );
5843 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5844 assert_eq!(
5845 cx.read_from_clipboard()
5846 .and_then(|item| item.text().as_deref().map(str::to_string)),
5847 Some(
5848 "in selections.iter() {
5849 let mut start = selection.start;
5850 let mut end = selection.end;
5851 let is_entire_line = selection.is_empty();
5852 if is_entire_line {
5853 start = Point::new(start.row, 0);"
5854 .to_string()
5855 ),
5856 "When selecting past the indent, nothing is trimmed"
5857 );
5858
5859 cx.set_state(
5860 r#" «for selection in selections.iter() {
5861 let mut start = selection.start;
5862
5863 let mut end = selection.end;
5864 let is_entire_line = selection.is_empty();
5865 if is_entire_line {
5866 start = Point::new(start.row, 0);
5867ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5868 }
5869 "#,
5870 );
5871 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5872 assert_eq!(
5873 cx.read_from_clipboard()
5874 .and_then(|item| item.text().as_deref().map(str::to_string)),
5875 Some(
5876 "for selection in selections.iter() {
5877let mut start = selection.start;
5878
5879let mut end = selection.end;
5880let is_entire_line = selection.is_empty();
5881if is_entire_line {
5882 start = Point::new(start.row, 0);
5883"
5884 .to_string()
5885 ),
5886 "Copying with stripping should ignore empty lines"
5887 );
5888}
5889
5890#[gpui::test]
5891async fn test_paste_multiline(cx: &mut TestAppContext) {
5892 init_test(cx, |_| {});
5893
5894 let mut cx = EditorTestContext::new(cx).await;
5895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5896
5897 // Cut an indented block, without the leading whitespace.
5898 cx.set_state(indoc! {"
5899 const a: B = (
5900 c(),
5901 «d(
5902 e,
5903 f
5904 )ˇ»
5905 );
5906 "});
5907 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5908 cx.assert_editor_state(indoc! {"
5909 const a: B = (
5910 c(),
5911 ˇ
5912 );
5913 "});
5914
5915 // Paste it at the same position.
5916 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5917 cx.assert_editor_state(indoc! {"
5918 const a: B = (
5919 c(),
5920 d(
5921 e,
5922 f
5923 )ˇ
5924 );
5925 "});
5926
5927 // Paste it at a line with a lower indent level.
5928 cx.set_state(indoc! {"
5929 ˇ
5930 const a: B = (
5931 c(),
5932 );
5933 "});
5934 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5935 cx.assert_editor_state(indoc! {"
5936 d(
5937 e,
5938 f
5939 )ˇ
5940 const a: B = (
5941 c(),
5942 );
5943 "});
5944
5945 // Cut an indented block, with the leading whitespace.
5946 cx.set_state(indoc! {"
5947 const a: B = (
5948 c(),
5949 « d(
5950 e,
5951 f
5952 )
5953 ˇ»);
5954 "});
5955 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5956 cx.assert_editor_state(indoc! {"
5957 const a: B = (
5958 c(),
5959 ˇ);
5960 "});
5961
5962 // Paste it at the same position.
5963 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5964 cx.assert_editor_state(indoc! {"
5965 const a: B = (
5966 c(),
5967 d(
5968 e,
5969 f
5970 )
5971 ˇ);
5972 "});
5973
5974 // Paste it at a line with a higher indent level.
5975 cx.set_state(indoc! {"
5976 const a: B = (
5977 c(),
5978 d(
5979 e,
5980 fˇ
5981 )
5982 );
5983 "});
5984 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5985 cx.assert_editor_state(indoc! {"
5986 const a: B = (
5987 c(),
5988 d(
5989 e,
5990 f d(
5991 e,
5992 f
5993 )
5994 ˇ
5995 )
5996 );
5997 "});
5998
5999 // Copy an indented block, starting mid-line
6000 cx.set_state(indoc! {"
6001 const a: B = (
6002 c(),
6003 somethin«g(
6004 e,
6005 f
6006 )ˇ»
6007 );
6008 "});
6009 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6010
6011 // Paste it on a line with a lower indent level
6012 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6013 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6014 cx.assert_editor_state(indoc! {"
6015 const a: B = (
6016 c(),
6017 something(
6018 e,
6019 f
6020 )
6021 );
6022 g(
6023 e,
6024 f
6025 )ˇ"});
6026}
6027
6028#[gpui::test]
6029async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6030 init_test(cx, |_| {});
6031
6032 cx.write_to_clipboard(ClipboardItem::new_string(
6033 " d(\n e\n );\n".into(),
6034 ));
6035
6036 let mut cx = EditorTestContext::new(cx).await;
6037 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6038
6039 cx.set_state(indoc! {"
6040 fn a() {
6041 b();
6042 if c() {
6043 ˇ
6044 }
6045 }
6046 "});
6047
6048 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6049 cx.assert_editor_state(indoc! {"
6050 fn a() {
6051 b();
6052 if c() {
6053 d(
6054 e
6055 );
6056 ˇ
6057 }
6058 }
6059 "});
6060
6061 cx.set_state(indoc! {"
6062 fn a() {
6063 b();
6064 ˇ
6065 }
6066 "});
6067
6068 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6069 cx.assert_editor_state(indoc! {"
6070 fn a() {
6071 b();
6072 d(
6073 e
6074 );
6075 ˇ
6076 }
6077 "});
6078}
6079
6080#[gpui::test]
6081fn test_select_all(cx: &mut TestAppContext) {
6082 init_test(cx, |_| {});
6083
6084 let editor = cx.add_window(|window, cx| {
6085 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6086 build_editor(buffer, window, cx)
6087 });
6088 _ = editor.update(cx, |editor, window, cx| {
6089 editor.select_all(&SelectAll, window, cx);
6090 assert_eq!(
6091 editor.selections.display_ranges(cx),
6092 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6093 );
6094 });
6095}
6096
6097#[gpui::test]
6098fn test_select_line(cx: &mut TestAppContext) {
6099 init_test(cx, |_| {});
6100
6101 let editor = cx.add_window(|window, cx| {
6102 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6103 build_editor(buffer, window, cx)
6104 });
6105 _ = editor.update(cx, |editor, window, cx| {
6106 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6107 s.select_display_ranges([
6108 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6109 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6110 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6111 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6112 ])
6113 });
6114 editor.select_line(&SelectLine, window, cx);
6115 assert_eq!(
6116 editor.selections.display_ranges(cx),
6117 vec![
6118 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6119 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6120 ]
6121 );
6122 });
6123
6124 _ = editor.update(cx, |editor, window, cx| {
6125 editor.select_line(&SelectLine, window, cx);
6126 assert_eq!(
6127 editor.selections.display_ranges(cx),
6128 vec![
6129 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6130 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6131 ]
6132 );
6133 });
6134
6135 _ = editor.update(cx, |editor, window, cx| {
6136 editor.select_line(&SelectLine, window, cx);
6137 assert_eq!(
6138 editor.selections.display_ranges(cx),
6139 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6140 );
6141 });
6142}
6143
6144#[gpui::test]
6145async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6146 init_test(cx, |_| {});
6147 let mut cx = EditorTestContext::new(cx).await;
6148
6149 #[track_caller]
6150 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6151 cx.set_state(initial_state);
6152 cx.update_editor(|e, window, cx| {
6153 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6154 });
6155 cx.assert_editor_state(expected_state);
6156 }
6157
6158 // Selection starts and ends at the middle of lines, left-to-right
6159 test(
6160 &mut cx,
6161 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6162 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6163 );
6164 // Same thing, right-to-left
6165 test(
6166 &mut cx,
6167 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6168 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6169 );
6170
6171 // Whole buffer, left-to-right, last line *doesn't* end with newline
6172 test(
6173 &mut cx,
6174 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6175 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6176 );
6177 // Same thing, right-to-left
6178 test(
6179 &mut cx,
6180 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6181 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6182 );
6183
6184 // Whole buffer, left-to-right, last line ends with newline
6185 test(
6186 &mut cx,
6187 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6188 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6189 );
6190 // Same thing, right-to-left
6191 test(
6192 &mut cx,
6193 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6194 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6195 );
6196
6197 // Starts at the end of a line, ends at the start of another
6198 test(
6199 &mut cx,
6200 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6201 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6202 );
6203}
6204
6205#[gpui::test]
6206async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6207 init_test(cx, |_| {});
6208
6209 let editor = cx.add_window(|window, cx| {
6210 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6211 build_editor(buffer, window, cx)
6212 });
6213
6214 // setup
6215 _ = editor.update(cx, |editor, window, cx| {
6216 editor.fold_creases(
6217 vec![
6218 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6219 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6220 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6221 ],
6222 true,
6223 window,
6224 cx,
6225 );
6226 assert_eq!(
6227 editor.display_text(cx),
6228 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6229 );
6230 });
6231
6232 _ = editor.update(cx, |editor, window, cx| {
6233 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6234 s.select_display_ranges([
6235 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6236 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6237 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6238 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6239 ])
6240 });
6241 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6242 assert_eq!(
6243 editor.display_text(cx),
6244 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6245 );
6246 });
6247 EditorTestContext::for_editor(editor, cx)
6248 .await
6249 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6250
6251 _ = editor.update(cx, |editor, window, cx| {
6252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6253 s.select_display_ranges([
6254 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6255 ])
6256 });
6257 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6258 assert_eq!(
6259 editor.display_text(cx),
6260 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6261 );
6262 assert_eq!(
6263 editor.selections.display_ranges(cx),
6264 [
6265 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6266 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6267 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6268 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6269 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6270 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6271 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6272 ]
6273 );
6274 });
6275 EditorTestContext::for_editor(editor, cx)
6276 .await
6277 .assert_editor_state(
6278 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6279 );
6280}
6281
6282#[gpui::test]
6283async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6284 init_test(cx, |_| {});
6285
6286 let mut cx = EditorTestContext::new(cx).await;
6287
6288 cx.set_state(indoc!(
6289 r#"abc
6290 defˇghi
6291
6292 jk
6293 nlmo
6294 "#
6295 ));
6296
6297 cx.update_editor(|editor, window, cx| {
6298 editor.add_selection_above(&Default::default(), window, cx);
6299 });
6300
6301 cx.assert_editor_state(indoc!(
6302 r#"abcˇ
6303 defˇghi
6304
6305 jk
6306 nlmo
6307 "#
6308 ));
6309
6310 cx.update_editor(|editor, window, cx| {
6311 editor.add_selection_above(&Default::default(), window, cx);
6312 });
6313
6314 cx.assert_editor_state(indoc!(
6315 r#"abcˇ
6316 defˇghi
6317
6318 jk
6319 nlmo
6320 "#
6321 ));
6322
6323 cx.update_editor(|editor, window, cx| {
6324 editor.add_selection_below(&Default::default(), window, cx);
6325 });
6326
6327 cx.assert_editor_state(indoc!(
6328 r#"abc
6329 defˇghi
6330
6331 jk
6332 nlmo
6333 "#
6334 ));
6335
6336 cx.update_editor(|editor, window, cx| {
6337 editor.undo_selection(&Default::default(), window, cx);
6338 });
6339
6340 cx.assert_editor_state(indoc!(
6341 r#"abcˇ
6342 defˇghi
6343
6344 jk
6345 nlmo
6346 "#
6347 ));
6348
6349 cx.update_editor(|editor, window, cx| {
6350 editor.redo_selection(&Default::default(), window, cx);
6351 });
6352
6353 cx.assert_editor_state(indoc!(
6354 r#"abc
6355 defˇghi
6356
6357 jk
6358 nlmo
6359 "#
6360 ));
6361
6362 cx.update_editor(|editor, window, cx| {
6363 editor.add_selection_below(&Default::default(), window, cx);
6364 });
6365
6366 cx.assert_editor_state(indoc!(
6367 r#"abc
6368 defˇghi
6369 ˇ
6370 jk
6371 nlmo
6372 "#
6373 ));
6374
6375 cx.update_editor(|editor, window, cx| {
6376 editor.add_selection_below(&Default::default(), window, cx);
6377 });
6378
6379 cx.assert_editor_state(indoc!(
6380 r#"abc
6381 defˇghi
6382 ˇ
6383 jkˇ
6384 nlmo
6385 "#
6386 ));
6387
6388 cx.update_editor(|editor, window, cx| {
6389 editor.add_selection_below(&Default::default(), window, cx);
6390 });
6391
6392 cx.assert_editor_state(indoc!(
6393 r#"abc
6394 defˇghi
6395 ˇ
6396 jkˇ
6397 nlmˇo
6398 "#
6399 ));
6400
6401 cx.update_editor(|editor, window, cx| {
6402 editor.add_selection_below(&Default::default(), window, cx);
6403 });
6404
6405 cx.assert_editor_state(indoc!(
6406 r#"abc
6407 defˇghi
6408 ˇ
6409 jkˇ
6410 nlmˇo
6411 ˇ"#
6412 ));
6413
6414 // change selections
6415 cx.set_state(indoc!(
6416 r#"abc
6417 def«ˇg»hi
6418
6419 jk
6420 nlmo
6421 "#
6422 ));
6423
6424 cx.update_editor(|editor, window, cx| {
6425 editor.add_selection_below(&Default::default(), window, cx);
6426 });
6427
6428 cx.assert_editor_state(indoc!(
6429 r#"abc
6430 def«ˇg»hi
6431
6432 jk
6433 nlm«ˇo»
6434 "#
6435 ));
6436
6437 cx.update_editor(|editor, window, cx| {
6438 editor.add_selection_below(&Default::default(), window, cx);
6439 });
6440
6441 cx.assert_editor_state(indoc!(
6442 r#"abc
6443 def«ˇg»hi
6444
6445 jk
6446 nlm«ˇo»
6447 "#
6448 ));
6449
6450 cx.update_editor(|editor, window, cx| {
6451 editor.add_selection_above(&Default::default(), window, cx);
6452 });
6453
6454 cx.assert_editor_state(indoc!(
6455 r#"abc
6456 def«ˇg»hi
6457
6458 jk
6459 nlmo
6460 "#
6461 ));
6462
6463 cx.update_editor(|editor, window, cx| {
6464 editor.add_selection_above(&Default::default(), window, cx);
6465 });
6466
6467 cx.assert_editor_state(indoc!(
6468 r#"abc
6469 def«ˇg»hi
6470
6471 jk
6472 nlmo
6473 "#
6474 ));
6475
6476 // Change selections again
6477 cx.set_state(indoc!(
6478 r#"a«bc
6479 defgˇ»hi
6480
6481 jk
6482 nlmo
6483 "#
6484 ));
6485
6486 cx.update_editor(|editor, window, cx| {
6487 editor.add_selection_below(&Default::default(), window, cx);
6488 });
6489
6490 cx.assert_editor_state(indoc!(
6491 r#"a«bcˇ»
6492 d«efgˇ»hi
6493
6494 j«kˇ»
6495 nlmo
6496 "#
6497 ));
6498
6499 cx.update_editor(|editor, window, cx| {
6500 editor.add_selection_below(&Default::default(), window, cx);
6501 });
6502 cx.assert_editor_state(indoc!(
6503 r#"a«bcˇ»
6504 d«efgˇ»hi
6505
6506 j«kˇ»
6507 n«lmoˇ»
6508 "#
6509 ));
6510 cx.update_editor(|editor, window, cx| {
6511 editor.add_selection_above(&Default::default(), window, cx);
6512 });
6513
6514 cx.assert_editor_state(indoc!(
6515 r#"a«bcˇ»
6516 d«efgˇ»hi
6517
6518 j«kˇ»
6519 nlmo
6520 "#
6521 ));
6522
6523 // Change selections again
6524 cx.set_state(indoc!(
6525 r#"abc
6526 d«ˇefghi
6527
6528 jk
6529 nlm»o
6530 "#
6531 ));
6532
6533 cx.update_editor(|editor, window, cx| {
6534 editor.add_selection_above(&Default::default(), window, cx);
6535 });
6536
6537 cx.assert_editor_state(indoc!(
6538 r#"a«ˇbc»
6539 d«ˇef»ghi
6540
6541 j«ˇk»
6542 n«ˇlm»o
6543 "#
6544 ));
6545
6546 cx.update_editor(|editor, window, cx| {
6547 editor.add_selection_below(&Default::default(), window, cx);
6548 });
6549
6550 cx.assert_editor_state(indoc!(
6551 r#"abc
6552 d«ˇef»ghi
6553
6554 j«ˇk»
6555 n«ˇlm»o
6556 "#
6557 ));
6558}
6559
6560#[gpui::test]
6561async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6562 init_test(cx, |_| {});
6563 let mut cx = EditorTestContext::new(cx).await;
6564
6565 cx.set_state(indoc!(
6566 r#"line onˇe
6567 liˇne two
6568 line three
6569 line four"#
6570 ));
6571
6572 cx.update_editor(|editor, window, cx| {
6573 editor.add_selection_below(&Default::default(), window, cx);
6574 });
6575
6576 // test multiple cursors expand in the same direction
6577 cx.assert_editor_state(indoc!(
6578 r#"line onˇe
6579 liˇne twˇo
6580 liˇne three
6581 line four"#
6582 ));
6583
6584 cx.update_editor(|editor, window, cx| {
6585 editor.add_selection_below(&Default::default(), window, cx);
6586 });
6587
6588 cx.update_editor(|editor, window, cx| {
6589 editor.add_selection_below(&Default::default(), window, cx);
6590 });
6591
6592 // test multiple cursors expand below overflow
6593 cx.assert_editor_state(indoc!(
6594 r#"line onˇe
6595 liˇne twˇo
6596 liˇne thˇree
6597 liˇne foˇur"#
6598 ));
6599
6600 cx.update_editor(|editor, window, cx| {
6601 editor.add_selection_above(&Default::default(), window, cx);
6602 });
6603
6604 // test multiple cursors retrieves back correctly
6605 cx.assert_editor_state(indoc!(
6606 r#"line onˇe
6607 liˇne twˇo
6608 liˇne thˇree
6609 line four"#
6610 ));
6611
6612 cx.update_editor(|editor, window, cx| {
6613 editor.add_selection_above(&Default::default(), window, cx);
6614 });
6615
6616 cx.update_editor(|editor, window, cx| {
6617 editor.add_selection_above(&Default::default(), window, cx);
6618 });
6619
6620 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6621 cx.assert_editor_state(indoc!(
6622 r#"liˇne onˇe
6623 liˇne two
6624 line three
6625 line four"#
6626 ));
6627
6628 cx.update_editor(|editor, window, cx| {
6629 editor.undo_selection(&Default::default(), window, cx);
6630 });
6631
6632 // test undo
6633 cx.assert_editor_state(indoc!(
6634 r#"line onˇe
6635 liˇne twˇo
6636 line three
6637 line four"#
6638 ));
6639
6640 cx.update_editor(|editor, window, cx| {
6641 editor.redo_selection(&Default::default(), window, cx);
6642 });
6643
6644 // test redo
6645 cx.assert_editor_state(indoc!(
6646 r#"liˇne onˇe
6647 liˇne two
6648 line three
6649 line four"#
6650 ));
6651
6652 cx.set_state(indoc!(
6653 r#"abcd
6654 ef«ghˇ»
6655 ijkl
6656 «mˇ»nop"#
6657 ));
6658
6659 cx.update_editor(|editor, window, cx| {
6660 editor.add_selection_above(&Default::default(), window, cx);
6661 });
6662
6663 // test multiple selections expand in the same direction
6664 cx.assert_editor_state(indoc!(
6665 r#"ab«cdˇ»
6666 ef«ghˇ»
6667 «iˇ»jkl
6668 «mˇ»nop"#
6669 ));
6670
6671 cx.update_editor(|editor, window, cx| {
6672 editor.add_selection_above(&Default::default(), window, cx);
6673 });
6674
6675 // test multiple selection upward overflow
6676 cx.assert_editor_state(indoc!(
6677 r#"ab«cdˇ»
6678 «eˇ»f«ghˇ»
6679 «iˇ»jkl
6680 «mˇ»nop"#
6681 ));
6682
6683 cx.update_editor(|editor, window, cx| {
6684 editor.add_selection_below(&Default::default(), window, cx);
6685 });
6686
6687 // test multiple selection retrieves back correctly
6688 cx.assert_editor_state(indoc!(
6689 r#"abcd
6690 ef«ghˇ»
6691 «iˇ»jkl
6692 «mˇ»nop"#
6693 ));
6694
6695 cx.update_editor(|editor, window, cx| {
6696 editor.add_selection_below(&Default::default(), window, cx);
6697 });
6698
6699 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6700 cx.assert_editor_state(indoc!(
6701 r#"abcd
6702 ef«ghˇ»
6703 ij«klˇ»
6704 «mˇ»nop"#
6705 ));
6706
6707 cx.update_editor(|editor, window, cx| {
6708 editor.undo_selection(&Default::default(), window, cx);
6709 });
6710
6711 // test undo
6712 cx.assert_editor_state(indoc!(
6713 r#"abcd
6714 ef«ghˇ»
6715 «iˇ»jkl
6716 «mˇ»nop"#
6717 ));
6718
6719 cx.update_editor(|editor, window, cx| {
6720 editor.redo_selection(&Default::default(), window, cx);
6721 });
6722
6723 // test redo
6724 cx.assert_editor_state(indoc!(
6725 r#"abcd
6726 ef«ghˇ»
6727 ij«klˇ»
6728 «mˇ»nop"#
6729 ));
6730}
6731
6732#[gpui::test]
6733async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6734 init_test(cx, |_| {});
6735 let mut cx = EditorTestContext::new(cx).await;
6736
6737 cx.set_state(indoc!(
6738 r#"line onˇe
6739 liˇne two
6740 line three
6741 line four"#
6742 ));
6743
6744 cx.update_editor(|editor, window, cx| {
6745 editor.add_selection_below(&Default::default(), window, cx);
6746 editor.add_selection_below(&Default::default(), window, cx);
6747 editor.add_selection_below(&Default::default(), window, cx);
6748 });
6749
6750 // initial state with two multi cursor groups
6751 cx.assert_editor_state(indoc!(
6752 r#"line onˇe
6753 liˇne twˇo
6754 liˇne thˇree
6755 liˇne foˇur"#
6756 ));
6757
6758 // add single cursor in middle - simulate opt click
6759 cx.update_editor(|editor, window, cx| {
6760 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6761 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6762 editor.end_selection(window, cx);
6763 });
6764
6765 cx.assert_editor_state(indoc!(
6766 r#"line onˇe
6767 liˇne twˇo
6768 liˇneˇ thˇree
6769 liˇne foˇur"#
6770 ));
6771
6772 cx.update_editor(|editor, window, cx| {
6773 editor.add_selection_above(&Default::default(), window, cx);
6774 });
6775
6776 // test new added selection expands above and existing selection shrinks
6777 cx.assert_editor_state(indoc!(
6778 r#"line onˇe
6779 liˇneˇ twˇo
6780 liˇneˇ thˇree
6781 line four"#
6782 ));
6783
6784 cx.update_editor(|editor, window, cx| {
6785 editor.add_selection_above(&Default::default(), window, cx);
6786 });
6787
6788 // test new added selection expands above and existing selection shrinks
6789 cx.assert_editor_state(indoc!(
6790 r#"lineˇ onˇe
6791 liˇneˇ twˇo
6792 lineˇ three
6793 line four"#
6794 ));
6795
6796 // intial state with two selection groups
6797 cx.set_state(indoc!(
6798 r#"abcd
6799 ef«ghˇ»
6800 ijkl
6801 «mˇ»nop"#
6802 ));
6803
6804 cx.update_editor(|editor, window, cx| {
6805 editor.add_selection_above(&Default::default(), window, cx);
6806 editor.add_selection_above(&Default::default(), window, cx);
6807 });
6808
6809 cx.assert_editor_state(indoc!(
6810 r#"ab«cdˇ»
6811 «eˇ»f«ghˇ»
6812 «iˇ»jkl
6813 «mˇ»nop"#
6814 ));
6815
6816 // add single selection in middle - simulate opt drag
6817 cx.update_editor(|editor, window, cx| {
6818 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6819 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6820 editor.update_selection(
6821 DisplayPoint::new(DisplayRow(2), 4),
6822 0,
6823 gpui::Point::<f32>::default(),
6824 window,
6825 cx,
6826 );
6827 editor.end_selection(window, cx);
6828 });
6829
6830 cx.assert_editor_state(indoc!(
6831 r#"ab«cdˇ»
6832 «eˇ»f«ghˇ»
6833 «iˇ»jk«lˇ»
6834 «mˇ»nop"#
6835 ));
6836
6837 cx.update_editor(|editor, window, cx| {
6838 editor.add_selection_below(&Default::default(), window, cx);
6839 });
6840
6841 // test new added selection expands below, others shrinks from above
6842 cx.assert_editor_state(indoc!(
6843 r#"abcd
6844 ef«ghˇ»
6845 «iˇ»jk«lˇ»
6846 «mˇ»no«pˇ»"#
6847 ));
6848}
6849
6850#[gpui::test]
6851async fn test_select_next(cx: &mut TestAppContext) {
6852 init_test(cx, |_| {});
6853
6854 let mut cx = EditorTestContext::new(cx).await;
6855 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6856
6857 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6858 .unwrap();
6859 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6860
6861 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6862 .unwrap();
6863 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6864
6865 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6866 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6867
6868 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6869 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6870
6871 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6872 .unwrap();
6873 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6874
6875 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6876 .unwrap();
6877 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6878
6879 // Test selection direction should be preserved
6880 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6881
6882 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6883 .unwrap();
6884 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6885}
6886
6887#[gpui::test]
6888async fn test_select_all_matches(cx: &mut TestAppContext) {
6889 init_test(cx, |_| {});
6890
6891 let mut cx = EditorTestContext::new(cx).await;
6892
6893 // Test caret-only selections
6894 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6895 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6896 .unwrap();
6897 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6898
6899 // Test left-to-right selections
6900 cx.set_state("abc\n«abcˇ»\nabc");
6901 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6902 .unwrap();
6903 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6904
6905 // Test right-to-left selections
6906 cx.set_state("abc\n«ˇabc»\nabc");
6907 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6908 .unwrap();
6909 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6910
6911 // Test selecting whitespace with caret selection
6912 cx.set_state("abc\nˇ abc\nabc");
6913 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6914 .unwrap();
6915 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6916
6917 // Test selecting whitespace with left-to-right selection
6918 cx.set_state("abc\n«ˇ »abc\nabc");
6919 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6920 .unwrap();
6921 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6922
6923 // Test no matches with right-to-left selection
6924 cx.set_state("abc\n« ˇ»abc\nabc");
6925 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6926 .unwrap();
6927 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6928
6929 // Test with a single word and clip_at_line_ends=true (#29823)
6930 cx.set_state("aˇbc");
6931 cx.update_editor(|e, window, cx| {
6932 e.set_clip_at_line_ends(true, cx);
6933 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
6934 e.set_clip_at_line_ends(false, cx);
6935 });
6936 cx.assert_editor_state("«abcˇ»");
6937}
6938
6939#[gpui::test]
6940async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6941 init_test(cx, |_| {});
6942
6943 let mut cx = EditorTestContext::new(cx).await;
6944
6945 let large_body_1 = "\nd".repeat(200);
6946 let large_body_2 = "\ne".repeat(200);
6947
6948 cx.set_state(&format!(
6949 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6950 ));
6951 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6952 let scroll_position = editor.scroll_position(cx);
6953 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6954 scroll_position
6955 });
6956
6957 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6958 .unwrap();
6959 cx.assert_editor_state(&format!(
6960 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6961 ));
6962 let scroll_position_after_selection =
6963 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6964 assert_eq!(
6965 initial_scroll_position, scroll_position_after_selection,
6966 "Scroll position should not change after selecting all matches"
6967 );
6968}
6969
6970#[gpui::test]
6971async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6972 init_test(cx, |_| {});
6973
6974 let mut cx = EditorLspTestContext::new_rust(
6975 lsp::ServerCapabilities {
6976 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6977 ..Default::default()
6978 },
6979 cx,
6980 )
6981 .await;
6982
6983 cx.set_state(indoc! {"
6984 line 1
6985 line 2
6986 linˇe 3
6987 line 4
6988 line 5
6989 "});
6990
6991 // Make an edit
6992 cx.update_editor(|editor, window, cx| {
6993 editor.handle_input("X", window, cx);
6994 });
6995
6996 // Move cursor to a different position
6997 cx.update_editor(|editor, window, cx| {
6998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6999 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7000 });
7001 });
7002
7003 cx.assert_editor_state(indoc! {"
7004 line 1
7005 line 2
7006 linXe 3
7007 line 4
7008 liˇne 5
7009 "});
7010
7011 cx.lsp
7012 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7013 Ok(Some(vec![lsp::TextEdit::new(
7014 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7015 "PREFIX ".to_string(),
7016 )]))
7017 });
7018
7019 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7020 .unwrap()
7021 .await
7022 .unwrap();
7023
7024 cx.assert_editor_state(indoc! {"
7025 PREFIX line 1
7026 line 2
7027 linXe 3
7028 line 4
7029 liˇne 5
7030 "});
7031
7032 // Undo formatting
7033 cx.update_editor(|editor, window, cx| {
7034 editor.undo(&Default::default(), window, cx);
7035 });
7036
7037 // Verify cursor moved back to position after edit
7038 cx.assert_editor_state(indoc! {"
7039 line 1
7040 line 2
7041 linXˇe 3
7042 line 4
7043 line 5
7044 "});
7045}
7046
7047#[gpui::test]
7048async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7049 init_test(cx, |_| {});
7050
7051 let mut cx = EditorTestContext::new(cx).await;
7052
7053 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7054 cx.update_editor(|editor, window, cx| {
7055 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7056 });
7057
7058 cx.set_state(indoc! {"
7059 line 1
7060 line 2
7061 linˇe 3
7062 line 4
7063 line 5
7064 line 6
7065 line 7
7066 line 8
7067 line 9
7068 line 10
7069 "});
7070
7071 let snapshot = cx.buffer_snapshot();
7072 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7073
7074 cx.update(|_, cx| {
7075 provider.update(cx, |provider, _| {
7076 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7077 id: None,
7078 edits: vec![(edit_position..edit_position, "X".into())],
7079 edit_preview: None,
7080 }))
7081 })
7082 });
7083
7084 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7085 cx.update_editor(|editor, window, cx| {
7086 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7087 });
7088
7089 cx.assert_editor_state(indoc! {"
7090 line 1
7091 line 2
7092 lineXˇ 3
7093 line 4
7094 line 5
7095 line 6
7096 line 7
7097 line 8
7098 line 9
7099 line 10
7100 "});
7101
7102 cx.update_editor(|editor, window, cx| {
7103 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7104 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7105 });
7106 });
7107
7108 cx.assert_editor_state(indoc! {"
7109 line 1
7110 line 2
7111 lineX 3
7112 line 4
7113 line 5
7114 line 6
7115 line 7
7116 line 8
7117 line 9
7118 liˇne 10
7119 "});
7120
7121 cx.update_editor(|editor, window, cx| {
7122 editor.undo(&Default::default(), window, cx);
7123 });
7124
7125 cx.assert_editor_state(indoc! {"
7126 line 1
7127 line 2
7128 lineˇ 3
7129 line 4
7130 line 5
7131 line 6
7132 line 7
7133 line 8
7134 line 9
7135 line 10
7136 "});
7137}
7138
7139#[gpui::test]
7140async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7141 init_test(cx, |_| {});
7142
7143 let mut cx = EditorTestContext::new(cx).await;
7144 cx.set_state(
7145 r#"let foo = 2;
7146lˇet foo = 2;
7147let fooˇ = 2;
7148let foo = 2;
7149let foo = ˇ2;"#,
7150 );
7151
7152 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7153 .unwrap();
7154 cx.assert_editor_state(
7155 r#"let foo = 2;
7156«letˇ» foo = 2;
7157let «fooˇ» = 2;
7158let foo = 2;
7159let foo = «2ˇ»;"#,
7160 );
7161
7162 // noop for multiple selections with different contents
7163 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7164 .unwrap();
7165 cx.assert_editor_state(
7166 r#"let foo = 2;
7167«letˇ» foo = 2;
7168let «fooˇ» = 2;
7169let foo = 2;
7170let foo = «2ˇ»;"#,
7171 );
7172
7173 // Test last selection direction should be preserved
7174 cx.set_state(
7175 r#"let foo = 2;
7176let foo = 2;
7177let «fooˇ» = 2;
7178let «ˇfoo» = 2;
7179let foo = 2;"#,
7180 );
7181
7182 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7183 .unwrap();
7184 cx.assert_editor_state(
7185 r#"let foo = 2;
7186let foo = 2;
7187let «fooˇ» = 2;
7188let «ˇfoo» = 2;
7189let «ˇfoo» = 2;"#,
7190 );
7191}
7192
7193#[gpui::test]
7194async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7195 init_test(cx, |_| {});
7196
7197 let mut cx =
7198 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7199
7200 cx.assert_editor_state(indoc! {"
7201 ˇbbb
7202 ccc
7203
7204 bbb
7205 ccc
7206 "});
7207 cx.dispatch_action(SelectPrevious::default());
7208 cx.assert_editor_state(indoc! {"
7209 «bbbˇ»
7210 ccc
7211
7212 bbb
7213 ccc
7214 "});
7215 cx.dispatch_action(SelectPrevious::default());
7216 cx.assert_editor_state(indoc! {"
7217 «bbbˇ»
7218 ccc
7219
7220 «bbbˇ»
7221 ccc
7222 "});
7223}
7224
7225#[gpui::test]
7226async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7227 init_test(cx, |_| {});
7228
7229 let mut cx = EditorTestContext::new(cx).await;
7230 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7231
7232 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7233 .unwrap();
7234 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7235
7236 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7237 .unwrap();
7238 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7239
7240 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7241 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7242
7243 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7244 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7245
7246 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7247 .unwrap();
7248 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7249
7250 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7251 .unwrap();
7252 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7253}
7254
7255#[gpui::test]
7256async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7257 init_test(cx, |_| {});
7258
7259 let mut cx = EditorTestContext::new(cx).await;
7260 cx.set_state("aˇ");
7261
7262 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7263 .unwrap();
7264 cx.assert_editor_state("«aˇ»");
7265 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7266 .unwrap();
7267 cx.assert_editor_state("«aˇ»");
7268}
7269
7270#[gpui::test]
7271async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7272 init_test(cx, |_| {});
7273
7274 let mut cx = EditorTestContext::new(cx).await;
7275 cx.set_state(
7276 r#"let foo = 2;
7277lˇet foo = 2;
7278let fooˇ = 2;
7279let foo = 2;
7280let foo = ˇ2;"#,
7281 );
7282
7283 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7284 .unwrap();
7285 cx.assert_editor_state(
7286 r#"let foo = 2;
7287«letˇ» foo = 2;
7288let «fooˇ» = 2;
7289let foo = 2;
7290let foo = «2ˇ»;"#,
7291 );
7292
7293 // noop for multiple selections with different contents
7294 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7295 .unwrap();
7296 cx.assert_editor_state(
7297 r#"let foo = 2;
7298«letˇ» foo = 2;
7299let «fooˇ» = 2;
7300let foo = 2;
7301let foo = «2ˇ»;"#,
7302 );
7303}
7304
7305#[gpui::test]
7306async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7307 init_test(cx, |_| {});
7308
7309 let mut cx = EditorTestContext::new(cx).await;
7310 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7311
7312 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7313 .unwrap();
7314 // selection direction is preserved
7315 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7316
7317 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7318 .unwrap();
7319 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7320
7321 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7322 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7323
7324 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7325 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7326
7327 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7328 .unwrap();
7329 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7330
7331 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7332 .unwrap();
7333 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7334}
7335
7336#[gpui::test]
7337async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7338 init_test(cx, |_| {});
7339
7340 let language = Arc::new(Language::new(
7341 LanguageConfig::default(),
7342 Some(tree_sitter_rust::LANGUAGE.into()),
7343 ));
7344
7345 let text = r#"
7346 use mod1::mod2::{mod3, mod4};
7347
7348 fn fn_1(param1: bool, param2: &str) {
7349 let var1 = "text";
7350 }
7351 "#
7352 .unindent();
7353
7354 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7355 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7356 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7357
7358 editor
7359 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7360 .await;
7361
7362 editor.update_in(cx, |editor, window, cx| {
7363 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7364 s.select_display_ranges([
7365 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7366 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7367 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7368 ]);
7369 });
7370 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7371 });
7372 editor.update(cx, |editor, cx| {
7373 assert_text_with_selections(
7374 editor,
7375 indoc! {r#"
7376 use mod1::mod2::{mod3, «mod4ˇ»};
7377
7378 fn fn_1«ˇ(param1: bool, param2: &str)» {
7379 let var1 = "«ˇtext»";
7380 }
7381 "#},
7382 cx,
7383 );
7384 });
7385
7386 editor.update_in(cx, |editor, window, cx| {
7387 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7388 });
7389 editor.update(cx, |editor, cx| {
7390 assert_text_with_selections(
7391 editor,
7392 indoc! {r#"
7393 use mod1::mod2::«{mod3, mod4}ˇ»;
7394
7395 «ˇfn fn_1(param1: bool, param2: &str) {
7396 let var1 = "text";
7397 }»
7398 "#},
7399 cx,
7400 );
7401 });
7402
7403 editor.update_in(cx, |editor, window, cx| {
7404 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7405 });
7406 assert_eq!(
7407 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7408 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7409 );
7410
7411 // Trying to expand the selected syntax node one more time has no effect.
7412 editor.update_in(cx, |editor, window, cx| {
7413 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7414 });
7415 assert_eq!(
7416 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7417 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7418 );
7419
7420 editor.update_in(cx, |editor, window, cx| {
7421 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7422 });
7423 editor.update(cx, |editor, cx| {
7424 assert_text_with_selections(
7425 editor,
7426 indoc! {r#"
7427 use mod1::mod2::«{mod3, mod4}ˇ»;
7428
7429 «ˇfn fn_1(param1: bool, param2: &str) {
7430 let var1 = "text";
7431 }»
7432 "#},
7433 cx,
7434 );
7435 });
7436
7437 editor.update_in(cx, |editor, window, cx| {
7438 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7439 });
7440 editor.update(cx, |editor, cx| {
7441 assert_text_with_selections(
7442 editor,
7443 indoc! {r#"
7444 use mod1::mod2::{mod3, «mod4ˇ»};
7445
7446 fn fn_1«ˇ(param1: bool, param2: &str)» {
7447 let var1 = "«ˇtext»";
7448 }
7449 "#},
7450 cx,
7451 );
7452 });
7453
7454 editor.update_in(cx, |editor, window, cx| {
7455 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7456 });
7457 editor.update(cx, |editor, cx| {
7458 assert_text_with_selections(
7459 editor,
7460 indoc! {r#"
7461 use mod1::mod2::{mod3, mo«ˇ»d4};
7462
7463 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7464 let var1 = "te«ˇ»xt";
7465 }
7466 "#},
7467 cx,
7468 );
7469 });
7470
7471 // Trying to shrink the selected syntax node one more time has no effect.
7472 editor.update_in(cx, |editor, window, cx| {
7473 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7474 });
7475 editor.update_in(cx, |editor, _, cx| {
7476 assert_text_with_selections(
7477 editor,
7478 indoc! {r#"
7479 use mod1::mod2::{mod3, mo«ˇ»d4};
7480
7481 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7482 let var1 = "te«ˇ»xt";
7483 }
7484 "#},
7485 cx,
7486 );
7487 });
7488
7489 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7490 // a fold.
7491 editor.update_in(cx, |editor, window, cx| {
7492 editor.fold_creases(
7493 vec![
7494 Crease::simple(
7495 Point::new(0, 21)..Point::new(0, 24),
7496 FoldPlaceholder::test(),
7497 ),
7498 Crease::simple(
7499 Point::new(3, 20)..Point::new(3, 22),
7500 FoldPlaceholder::test(),
7501 ),
7502 ],
7503 true,
7504 window,
7505 cx,
7506 );
7507 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7508 });
7509 editor.update(cx, |editor, cx| {
7510 assert_text_with_selections(
7511 editor,
7512 indoc! {r#"
7513 use mod1::mod2::«{mod3, mod4}ˇ»;
7514
7515 fn fn_1«ˇ(param1: bool, param2: &str)» {
7516 let var1 = "«ˇtext»";
7517 }
7518 "#},
7519 cx,
7520 );
7521 });
7522}
7523
7524#[gpui::test]
7525async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7526 init_test(cx, |_| {});
7527
7528 let language = Arc::new(Language::new(
7529 LanguageConfig::default(),
7530 Some(tree_sitter_rust::LANGUAGE.into()),
7531 ));
7532
7533 let text = "let a = 2;";
7534
7535 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7536 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7537 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7538
7539 editor
7540 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7541 .await;
7542
7543 // Test case 1: Cursor at end of word
7544 editor.update_in(cx, |editor, window, cx| {
7545 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7546 s.select_display_ranges([
7547 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7548 ]);
7549 });
7550 });
7551 editor.update(cx, |editor, cx| {
7552 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7553 });
7554 editor.update_in(cx, |editor, window, cx| {
7555 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7556 });
7557 editor.update(cx, |editor, cx| {
7558 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7559 });
7560 editor.update_in(cx, |editor, window, cx| {
7561 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7562 });
7563 editor.update(cx, |editor, cx| {
7564 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7565 });
7566
7567 // Test case 2: Cursor at end of statement
7568 editor.update_in(cx, |editor, window, cx| {
7569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7570 s.select_display_ranges([
7571 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7572 ]);
7573 });
7574 });
7575 editor.update(cx, |editor, cx| {
7576 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7577 });
7578 editor.update_in(cx, |editor, window, cx| {
7579 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7580 });
7581 editor.update(cx, |editor, cx| {
7582 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7583 });
7584}
7585
7586#[gpui::test]
7587async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7588 init_test(cx, |_| {});
7589
7590 let language = Arc::new(Language::new(
7591 LanguageConfig::default(),
7592 Some(tree_sitter_rust::LANGUAGE.into()),
7593 ));
7594
7595 let text = r#"
7596 use mod1::mod2::{mod3, mod4};
7597
7598 fn fn_1(param1: bool, param2: &str) {
7599 let var1 = "hello world";
7600 }
7601 "#
7602 .unindent();
7603
7604 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7605 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7606 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7607
7608 editor
7609 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7610 .await;
7611
7612 // Test 1: Cursor on a letter of a string word
7613 editor.update_in(cx, |editor, window, cx| {
7614 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7615 s.select_display_ranges([
7616 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7617 ]);
7618 });
7619 });
7620 editor.update_in(cx, |editor, window, cx| {
7621 assert_text_with_selections(
7622 editor,
7623 indoc! {r#"
7624 use mod1::mod2::{mod3, mod4};
7625
7626 fn fn_1(param1: bool, param2: &str) {
7627 let var1 = "hˇello world";
7628 }
7629 "#},
7630 cx,
7631 );
7632 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7633 assert_text_with_selections(
7634 editor,
7635 indoc! {r#"
7636 use mod1::mod2::{mod3, mod4};
7637
7638 fn fn_1(param1: bool, param2: &str) {
7639 let var1 = "«ˇhello» world";
7640 }
7641 "#},
7642 cx,
7643 );
7644 });
7645
7646 // Test 2: Partial selection within a word
7647 editor.update_in(cx, |editor, window, cx| {
7648 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7649 s.select_display_ranges([
7650 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7651 ]);
7652 });
7653 });
7654 editor.update_in(cx, |editor, window, cx| {
7655 assert_text_with_selections(
7656 editor,
7657 indoc! {r#"
7658 use mod1::mod2::{mod3, mod4};
7659
7660 fn fn_1(param1: bool, param2: &str) {
7661 let var1 = "h«elˇ»lo world";
7662 }
7663 "#},
7664 cx,
7665 );
7666 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7667 assert_text_with_selections(
7668 editor,
7669 indoc! {r#"
7670 use mod1::mod2::{mod3, mod4};
7671
7672 fn fn_1(param1: bool, param2: &str) {
7673 let var1 = "«ˇhello» world";
7674 }
7675 "#},
7676 cx,
7677 );
7678 });
7679
7680 // Test 3: Complete word already selected
7681 editor.update_in(cx, |editor, window, cx| {
7682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7683 s.select_display_ranges([
7684 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7685 ]);
7686 });
7687 });
7688 editor.update_in(cx, |editor, window, cx| {
7689 assert_text_with_selections(
7690 editor,
7691 indoc! {r#"
7692 use mod1::mod2::{mod3, mod4};
7693
7694 fn fn_1(param1: bool, param2: &str) {
7695 let var1 = "«helloˇ» world";
7696 }
7697 "#},
7698 cx,
7699 );
7700 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7701 assert_text_with_selections(
7702 editor,
7703 indoc! {r#"
7704 use mod1::mod2::{mod3, mod4};
7705
7706 fn fn_1(param1: bool, param2: &str) {
7707 let var1 = "«hello worldˇ»";
7708 }
7709 "#},
7710 cx,
7711 );
7712 });
7713
7714 // Test 4: Selection spanning across words
7715 editor.update_in(cx, |editor, window, cx| {
7716 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7717 s.select_display_ranges([
7718 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7719 ]);
7720 });
7721 });
7722 editor.update_in(cx, |editor, window, cx| {
7723 assert_text_with_selections(
7724 editor,
7725 indoc! {r#"
7726 use mod1::mod2::{mod3, mod4};
7727
7728 fn fn_1(param1: bool, param2: &str) {
7729 let var1 = "hel«lo woˇ»rld";
7730 }
7731 "#},
7732 cx,
7733 );
7734 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7735 assert_text_with_selections(
7736 editor,
7737 indoc! {r#"
7738 use mod1::mod2::{mod3, mod4};
7739
7740 fn fn_1(param1: bool, param2: &str) {
7741 let var1 = "«ˇhello world»";
7742 }
7743 "#},
7744 cx,
7745 );
7746 });
7747
7748 // Test 5: Expansion beyond string
7749 editor.update_in(cx, |editor, window, cx| {
7750 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7751 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7752 assert_text_with_selections(
7753 editor,
7754 indoc! {r#"
7755 use mod1::mod2::{mod3, mod4};
7756
7757 fn fn_1(param1: bool, param2: &str) {
7758 «ˇlet var1 = "hello world";»
7759 }
7760 "#},
7761 cx,
7762 );
7763 });
7764}
7765
7766#[gpui::test]
7767async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7768 init_test(cx, |_| {});
7769
7770 let base_text = r#"
7771 impl A {
7772 // this is an uncommitted comment
7773
7774 fn b() {
7775 c();
7776 }
7777
7778 // this is another uncommitted comment
7779
7780 fn d() {
7781 // e
7782 // f
7783 }
7784 }
7785
7786 fn g() {
7787 // h
7788 }
7789 "#
7790 .unindent();
7791
7792 let text = r#"
7793 ˇimpl A {
7794
7795 fn b() {
7796 c();
7797 }
7798
7799 fn d() {
7800 // e
7801 // f
7802 }
7803 }
7804
7805 fn g() {
7806 // h
7807 }
7808 "#
7809 .unindent();
7810
7811 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7812 cx.set_state(&text);
7813 cx.set_head_text(&base_text);
7814 cx.update_editor(|editor, window, cx| {
7815 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7816 });
7817
7818 cx.assert_state_with_diff(
7819 "
7820 ˇimpl A {
7821 - // this is an uncommitted comment
7822
7823 fn b() {
7824 c();
7825 }
7826
7827 - // this is another uncommitted comment
7828 -
7829 fn d() {
7830 // e
7831 // f
7832 }
7833 }
7834
7835 fn g() {
7836 // h
7837 }
7838 "
7839 .unindent(),
7840 );
7841
7842 let expected_display_text = "
7843 impl A {
7844 // this is an uncommitted comment
7845
7846 fn b() {
7847 ⋯
7848 }
7849
7850 // this is another uncommitted comment
7851
7852 fn d() {
7853 ⋯
7854 }
7855 }
7856
7857 fn g() {
7858 ⋯
7859 }
7860 "
7861 .unindent();
7862
7863 cx.update_editor(|editor, window, cx| {
7864 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7865 assert_eq!(editor.display_text(cx), expected_display_text);
7866 });
7867}
7868
7869#[gpui::test]
7870async fn test_autoindent(cx: &mut TestAppContext) {
7871 init_test(cx, |_| {});
7872
7873 let language = Arc::new(
7874 Language::new(
7875 LanguageConfig {
7876 brackets: BracketPairConfig {
7877 pairs: vec![
7878 BracketPair {
7879 start: "{".to_string(),
7880 end: "}".to_string(),
7881 close: false,
7882 surround: false,
7883 newline: true,
7884 },
7885 BracketPair {
7886 start: "(".to_string(),
7887 end: ")".to_string(),
7888 close: false,
7889 surround: false,
7890 newline: true,
7891 },
7892 ],
7893 ..Default::default()
7894 },
7895 ..Default::default()
7896 },
7897 Some(tree_sitter_rust::LANGUAGE.into()),
7898 )
7899 .with_indents_query(
7900 r#"
7901 (_ "(" ")" @end) @indent
7902 (_ "{" "}" @end) @indent
7903 "#,
7904 )
7905 .unwrap(),
7906 );
7907
7908 let text = "fn a() {}";
7909
7910 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7911 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7912 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7913 editor
7914 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7915 .await;
7916
7917 editor.update_in(cx, |editor, window, cx| {
7918 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7919 s.select_ranges([5..5, 8..8, 9..9])
7920 });
7921 editor.newline(&Newline, window, cx);
7922 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7923 assert_eq!(
7924 editor.selections.ranges(cx),
7925 &[
7926 Point::new(1, 4)..Point::new(1, 4),
7927 Point::new(3, 4)..Point::new(3, 4),
7928 Point::new(5, 0)..Point::new(5, 0)
7929 ]
7930 );
7931 });
7932}
7933
7934#[gpui::test]
7935async fn test_autoindent_selections(cx: &mut TestAppContext) {
7936 init_test(cx, |_| {});
7937
7938 {
7939 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7940 cx.set_state(indoc! {"
7941 impl A {
7942
7943 fn b() {}
7944
7945 «fn c() {
7946
7947 }ˇ»
7948 }
7949 "});
7950
7951 cx.update_editor(|editor, window, cx| {
7952 editor.autoindent(&Default::default(), window, cx);
7953 });
7954
7955 cx.assert_editor_state(indoc! {"
7956 impl A {
7957
7958 fn b() {}
7959
7960 «fn c() {
7961
7962 }ˇ»
7963 }
7964 "});
7965 }
7966
7967 {
7968 let mut cx = EditorTestContext::new_multibuffer(
7969 cx,
7970 [indoc! { "
7971 impl A {
7972 «
7973 // a
7974 fn b(){}
7975 »
7976 «
7977 }
7978 fn c(){}
7979 »
7980 "}],
7981 );
7982
7983 let buffer = cx.update_editor(|editor, _, cx| {
7984 let buffer = editor.buffer().update(cx, |buffer, _| {
7985 buffer.all_buffers().iter().next().unwrap().clone()
7986 });
7987 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7988 buffer
7989 });
7990
7991 cx.run_until_parked();
7992 cx.update_editor(|editor, window, cx| {
7993 editor.select_all(&Default::default(), window, cx);
7994 editor.autoindent(&Default::default(), window, cx)
7995 });
7996 cx.run_until_parked();
7997
7998 cx.update(|_, cx| {
7999 assert_eq!(
8000 buffer.read(cx).text(),
8001 indoc! { "
8002 impl A {
8003
8004 // a
8005 fn b(){}
8006
8007
8008 }
8009 fn c(){}
8010
8011 " }
8012 )
8013 });
8014 }
8015}
8016
8017#[gpui::test]
8018async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8019 init_test(cx, |_| {});
8020
8021 let mut cx = EditorTestContext::new(cx).await;
8022
8023 let language = Arc::new(Language::new(
8024 LanguageConfig {
8025 brackets: BracketPairConfig {
8026 pairs: vec![
8027 BracketPair {
8028 start: "{".to_string(),
8029 end: "}".to_string(),
8030 close: true,
8031 surround: true,
8032 newline: true,
8033 },
8034 BracketPair {
8035 start: "(".to_string(),
8036 end: ")".to_string(),
8037 close: true,
8038 surround: true,
8039 newline: true,
8040 },
8041 BracketPair {
8042 start: "/*".to_string(),
8043 end: " */".to_string(),
8044 close: true,
8045 surround: true,
8046 newline: true,
8047 },
8048 BracketPair {
8049 start: "[".to_string(),
8050 end: "]".to_string(),
8051 close: false,
8052 surround: false,
8053 newline: true,
8054 },
8055 BracketPair {
8056 start: "\"".to_string(),
8057 end: "\"".to_string(),
8058 close: true,
8059 surround: true,
8060 newline: false,
8061 },
8062 BracketPair {
8063 start: "<".to_string(),
8064 end: ">".to_string(),
8065 close: false,
8066 surround: true,
8067 newline: true,
8068 },
8069 ],
8070 ..Default::default()
8071 },
8072 autoclose_before: "})]".to_string(),
8073 ..Default::default()
8074 },
8075 Some(tree_sitter_rust::LANGUAGE.into()),
8076 ));
8077
8078 cx.language_registry().add(language.clone());
8079 cx.update_buffer(|buffer, cx| {
8080 buffer.set_language(Some(language), cx);
8081 });
8082
8083 cx.set_state(
8084 &r#"
8085 🏀ˇ
8086 εˇ
8087 ❤️ˇ
8088 "#
8089 .unindent(),
8090 );
8091
8092 // autoclose multiple nested brackets at multiple cursors
8093 cx.update_editor(|editor, window, cx| {
8094 editor.handle_input("{", window, cx);
8095 editor.handle_input("{", window, cx);
8096 editor.handle_input("{", window, cx);
8097 });
8098 cx.assert_editor_state(
8099 &"
8100 🏀{{{ˇ}}}
8101 ε{{{ˇ}}}
8102 ❤️{{{ˇ}}}
8103 "
8104 .unindent(),
8105 );
8106
8107 // insert a different closing bracket
8108 cx.update_editor(|editor, window, cx| {
8109 editor.handle_input(")", window, cx);
8110 });
8111 cx.assert_editor_state(
8112 &"
8113 🏀{{{)ˇ}}}
8114 ε{{{)ˇ}}}
8115 ❤️{{{)ˇ}}}
8116 "
8117 .unindent(),
8118 );
8119
8120 // skip over the auto-closed brackets when typing a closing bracket
8121 cx.update_editor(|editor, window, cx| {
8122 editor.move_right(&MoveRight, window, cx);
8123 editor.handle_input("}", window, cx);
8124 editor.handle_input("}", window, cx);
8125 editor.handle_input("}", window, cx);
8126 });
8127 cx.assert_editor_state(
8128 &"
8129 🏀{{{)}}}}ˇ
8130 ε{{{)}}}}ˇ
8131 ❤️{{{)}}}}ˇ
8132 "
8133 .unindent(),
8134 );
8135
8136 // autoclose multi-character pairs
8137 cx.set_state(
8138 &"
8139 ˇ
8140 ˇ
8141 "
8142 .unindent(),
8143 );
8144 cx.update_editor(|editor, window, cx| {
8145 editor.handle_input("/", window, cx);
8146 editor.handle_input("*", window, cx);
8147 });
8148 cx.assert_editor_state(
8149 &"
8150 /*ˇ */
8151 /*ˇ */
8152 "
8153 .unindent(),
8154 );
8155
8156 // one cursor autocloses a multi-character pair, one cursor
8157 // does not autoclose.
8158 cx.set_state(
8159 &"
8160 /ˇ
8161 ˇ
8162 "
8163 .unindent(),
8164 );
8165 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8166 cx.assert_editor_state(
8167 &"
8168 /*ˇ */
8169 *ˇ
8170 "
8171 .unindent(),
8172 );
8173
8174 // Don't autoclose if the next character isn't whitespace and isn't
8175 // listed in the language's "autoclose_before" section.
8176 cx.set_state("ˇa b");
8177 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8178 cx.assert_editor_state("{ˇa b");
8179
8180 // Don't autoclose if `close` is false for the bracket pair
8181 cx.set_state("ˇ");
8182 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8183 cx.assert_editor_state("[ˇ");
8184
8185 // Surround with brackets if text is selected
8186 cx.set_state("«aˇ» b");
8187 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8188 cx.assert_editor_state("{«aˇ»} b");
8189
8190 // Autoclose when not immediately after a word character
8191 cx.set_state("a ˇ");
8192 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8193 cx.assert_editor_state("a \"ˇ\"");
8194
8195 // Autoclose pair where the start and end characters are the same
8196 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8197 cx.assert_editor_state("a \"\"ˇ");
8198
8199 // Don't autoclose when immediately after a word character
8200 cx.set_state("aˇ");
8201 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8202 cx.assert_editor_state("a\"ˇ");
8203
8204 // Do autoclose when after a non-word character
8205 cx.set_state("{ˇ");
8206 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8207 cx.assert_editor_state("{\"ˇ\"");
8208
8209 // Non identical pairs autoclose regardless of preceding character
8210 cx.set_state("aˇ");
8211 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8212 cx.assert_editor_state("a{ˇ}");
8213
8214 // Don't autoclose pair if autoclose is disabled
8215 cx.set_state("ˇ");
8216 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8217 cx.assert_editor_state("<ˇ");
8218
8219 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8220 cx.set_state("«aˇ» b");
8221 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8222 cx.assert_editor_state("<«aˇ»> b");
8223}
8224
8225#[gpui::test]
8226async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8227 init_test(cx, |settings| {
8228 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8229 });
8230
8231 let mut cx = EditorTestContext::new(cx).await;
8232
8233 let language = Arc::new(Language::new(
8234 LanguageConfig {
8235 brackets: BracketPairConfig {
8236 pairs: vec![
8237 BracketPair {
8238 start: "{".to_string(),
8239 end: "}".to_string(),
8240 close: true,
8241 surround: true,
8242 newline: true,
8243 },
8244 BracketPair {
8245 start: "(".to_string(),
8246 end: ")".to_string(),
8247 close: true,
8248 surround: true,
8249 newline: true,
8250 },
8251 BracketPair {
8252 start: "[".to_string(),
8253 end: "]".to_string(),
8254 close: false,
8255 surround: false,
8256 newline: true,
8257 },
8258 ],
8259 ..Default::default()
8260 },
8261 autoclose_before: "})]".to_string(),
8262 ..Default::default()
8263 },
8264 Some(tree_sitter_rust::LANGUAGE.into()),
8265 ));
8266
8267 cx.language_registry().add(language.clone());
8268 cx.update_buffer(|buffer, cx| {
8269 buffer.set_language(Some(language), cx);
8270 });
8271
8272 cx.set_state(
8273 &"
8274 ˇ
8275 ˇ
8276 ˇ
8277 "
8278 .unindent(),
8279 );
8280
8281 // ensure only matching closing brackets are skipped over
8282 cx.update_editor(|editor, window, cx| {
8283 editor.handle_input("}", window, cx);
8284 editor.move_left(&MoveLeft, window, cx);
8285 editor.handle_input(")", window, cx);
8286 editor.move_left(&MoveLeft, window, cx);
8287 });
8288 cx.assert_editor_state(
8289 &"
8290 ˇ)}
8291 ˇ)}
8292 ˇ)}
8293 "
8294 .unindent(),
8295 );
8296
8297 // skip-over closing brackets at multiple cursors
8298 cx.update_editor(|editor, window, cx| {
8299 editor.handle_input(")", window, cx);
8300 editor.handle_input("}", window, cx);
8301 });
8302 cx.assert_editor_state(
8303 &"
8304 )}ˇ
8305 )}ˇ
8306 )}ˇ
8307 "
8308 .unindent(),
8309 );
8310
8311 // ignore non-close brackets
8312 cx.update_editor(|editor, window, cx| {
8313 editor.handle_input("]", window, cx);
8314 editor.move_left(&MoveLeft, window, cx);
8315 editor.handle_input("]", window, cx);
8316 });
8317 cx.assert_editor_state(
8318 &"
8319 )}]ˇ]
8320 )}]ˇ]
8321 )}]ˇ]
8322 "
8323 .unindent(),
8324 );
8325}
8326
8327#[gpui::test]
8328async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8329 init_test(cx, |_| {});
8330
8331 let mut cx = EditorTestContext::new(cx).await;
8332
8333 let html_language = Arc::new(
8334 Language::new(
8335 LanguageConfig {
8336 name: "HTML".into(),
8337 brackets: BracketPairConfig {
8338 pairs: vec![
8339 BracketPair {
8340 start: "<".into(),
8341 end: ">".into(),
8342 close: true,
8343 ..Default::default()
8344 },
8345 BracketPair {
8346 start: "{".into(),
8347 end: "}".into(),
8348 close: true,
8349 ..Default::default()
8350 },
8351 BracketPair {
8352 start: "(".into(),
8353 end: ")".into(),
8354 close: true,
8355 ..Default::default()
8356 },
8357 ],
8358 ..Default::default()
8359 },
8360 autoclose_before: "})]>".into(),
8361 ..Default::default()
8362 },
8363 Some(tree_sitter_html::LANGUAGE.into()),
8364 )
8365 .with_injection_query(
8366 r#"
8367 (script_element
8368 (raw_text) @injection.content
8369 (#set! injection.language "javascript"))
8370 "#,
8371 )
8372 .unwrap(),
8373 );
8374
8375 let javascript_language = Arc::new(Language::new(
8376 LanguageConfig {
8377 name: "JavaScript".into(),
8378 brackets: BracketPairConfig {
8379 pairs: vec![
8380 BracketPair {
8381 start: "/*".into(),
8382 end: " */".into(),
8383 close: true,
8384 ..Default::default()
8385 },
8386 BracketPair {
8387 start: "{".into(),
8388 end: "}".into(),
8389 close: true,
8390 ..Default::default()
8391 },
8392 BracketPair {
8393 start: "(".into(),
8394 end: ")".into(),
8395 close: true,
8396 ..Default::default()
8397 },
8398 ],
8399 ..Default::default()
8400 },
8401 autoclose_before: "})]>".into(),
8402 ..Default::default()
8403 },
8404 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8405 ));
8406
8407 cx.language_registry().add(html_language.clone());
8408 cx.language_registry().add(javascript_language.clone());
8409
8410 cx.update_buffer(|buffer, cx| {
8411 buffer.set_language(Some(html_language), cx);
8412 });
8413
8414 cx.set_state(
8415 &r#"
8416 <body>ˇ
8417 <script>
8418 var x = 1;ˇ
8419 </script>
8420 </body>ˇ
8421 "#
8422 .unindent(),
8423 );
8424
8425 // Precondition: different languages are active at different locations.
8426 cx.update_editor(|editor, window, cx| {
8427 let snapshot = editor.snapshot(window, cx);
8428 let cursors = editor.selections.ranges::<usize>(cx);
8429 let languages = cursors
8430 .iter()
8431 .map(|c| snapshot.language_at(c.start).unwrap().name())
8432 .collect::<Vec<_>>();
8433 assert_eq!(
8434 languages,
8435 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8436 );
8437 });
8438
8439 // Angle brackets autoclose in HTML, but not JavaScript.
8440 cx.update_editor(|editor, window, cx| {
8441 editor.handle_input("<", window, cx);
8442 editor.handle_input("a", window, cx);
8443 });
8444 cx.assert_editor_state(
8445 &r#"
8446 <body><aˇ>
8447 <script>
8448 var x = 1;<aˇ
8449 </script>
8450 </body><aˇ>
8451 "#
8452 .unindent(),
8453 );
8454
8455 // Curly braces and parens autoclose in both HTML and JavaScript.
8456 cx.update_editor(|editor, window, cx| {
8457 editor.handle_input(" b=", window, cx);
8458 editor.handle_input("{", window, cx);
8459 editor.handle_input("c", window, cx);
8460 editor.handle_input("(", window, cx);
8461 });
8462 cx.assert_editor_state(
8463 &r#"
8464 <body><a b={c(ˇ)}>
8465 <script>
8466 var x = 1;<a b={c(ˇ)}
8467 </script>
8468 </body><a b={c(ˇ)}>
8469 "#
8470 .unindent(),
8471 );
8472
8473 // Brackets that were already autoclosed are skipped.
8474 cx.update_editor(|editor, window, cx| {
8475 editor.handle_input(")", window, cx);
8476 editor.handle_input("d", window, cx);
8477 editor.handle_input("}", window, cx);
8478 });
8479 cx.assert_editor_state(
8480 &r#"
8481 <body><a b={c()d}ˇ>
8482 <script>
8483 var x = 1;<a b={c()d}ˇ
8484 </script>
8485 </body><a b={c()d}ˇ>
8486 "#
8487 .unindent(),
8488 );
8489 cx.update_editor(|editor, window, cx| {
8490 editor.handle_input(">", window, cx);
8491 });
8492 cx.assert_editor_state(
8493 &r#"
8494 <body><a b={c()d}>ˇ
8495 <script>
8496 var x = 1;<a b={c()d}>ˇ
8497 </script>
8498 </body><a b={c()d}>ˇ
8499 "#
8500 .unindent(),
8501 );
8502
8503 // Reset
8504 cx.set_state(
8505 &r#"
8506 <body>ˇ
8507 <script>
8508 var x = 1;ˇ
8509 </script>
8510 </body>ˇ
8511 "#
8512 .unindent(),
8513 );
8514
8515 cx.update_editor(|editor, window, cx| {
8516 editor.handle_input("<", window, cx);
8517 });
8518 cx.assert_editor_state(
8519 &r#"
8520 <body><ˇ>
8521 <script>
8522 var x = 1;<ˇ
8523 </script>
8524 </body><ˇ>
8525 "#
8526 .unindent(),
8527 );
8528
8529 // When backspacing, the closing angle brackets are removed.
8530 cx.update_editor(|editor, window, cx| {
8531 editor.backspace(&Backspace, window, cx);
8532 });
8533 cx.assert_editor_state(
8534 &r#"
8535 <body>ˇ
8536 <script>
8537 var x = 1;ˇ
8538 </script>
8539 </body>ˇ
8540 "#
8541 .unindent(),
8542 );
8543
8544 // Block comments autoclose in JavaScript, but not HTML.
8545 cx.update_editor(|editor, window, cx| {
8546 editor.handle_input("/", window, cx);
8547 editor.handle_input("*", window, cx);
8548 });
8549 cx.assert_editor_state(
8550 &r#"
8551 <body>/*ˇ
8552 <script>
8553 var x = 1;/*ˇ */
8554 </script>
8555 </body>/*ˇ
8556 "#
8557 .unindent(),
8558 );
8559}
8560
8561#[gpui::test]
8562async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8563 init_test(cx, |_| {});
8564
8565 let mut cx = EditorTestContext::new(cx).await;
8566
8567 let rust_language = Arc::new(
8568 Language::new(
8569 LanguageConfig {
8570 name: "Rust".into(),
8571 brackets: serde_json::from_value(json!([
8572 { "start": "{", "end": "}", "close": true, "newline": true },
8573 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8574 ]))
8575 .unwrap(),
8576 autoclose_before: "})]>".into(),
8577 ..Default::default()
8578 },
8579 Some(tree_sitter_rust::LANGUAGE.into()),
8580 )
8581 .with_override_query("(string_literal) @string")
8582 .unwrap(),
8583 );
8584
8585 cx.language_registry().add(rust_language.clone());
8586 cx.update_buffer(|buffer, cx| {
8587 buffer.set_language(Some(rust_language), cx);
8588 });
8589
8590 cx.set_state(
8591 &r#"
8592 let x = ˇ
8593 "#
8594 .unindent(),
8595 );
8596
8597 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8598 cx.update_editor(|editor, window, cx| {
8599 editor.handle_input("\"", window, cx);
8600 });
8601 cx.assert_editor_state(
8602 &r#"
8603 let x = "ˇ"
8604 "#
8605 .unindent(),
8606 );
8607
8608 // Inserting another quotation mark. The cursor moves across the existing
8609 // automatically-inserted quotation mark.
8610 cx.update_editor(|editor, window, cx| {
8611 editor.handle_input("\"", window, cx);
8612 });
8613 cx.assert_editor_state(
8614 &r#"
8615 let x = ""ˇ
8616 "#
8617 .unindent(),
8618 );
8619
8620 // Reset
8621 cx.set_state(
8622 &r#"
8623 let x = ˇ
8624 "#
8625 .unindent(),
8626 );
8627
8628 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8629 cx.update_editor(|editor, window, cx| {
8630 editor.handle_input("\"", window, cx);
8631 editor.handle_input(" ", window, cx);
8632 editor.move_left(&Default::default(), window, cx);
8633 editor.handle_input("\\", window, cx);
8634 editor.handle_input("\"", window, cx);
8635 });
8636 cx.assert_editor_state(
8637 &r#"
8638 let x = "\"ˇ "
8639 "#
8640 .unindent(),
8641 );
8642
8643 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8644 // mark. Nothing is inserted.
8645 cx.update_editor(|editor, window, cx| {
8646 editor.move_right(&Default::default(), window, cx);
8647 editor.handle_input("\"", window, cx);
8648 });
8649 cx.assert_editor_state(
8650 &r#"
8651 let x = "\" "ˇ
8652 "#
8653 .unindent(),
8654 );
8655}
8656
8657#[gpui::test]
8658async fn test_surround_with_pair(cx: &mut TestAppContext) {
8659 init_test(cx, |_| {});
8660
8661 let language = Arc::new(Language::new(
8662 LanguageConfig {
8663 brackets: BracketPairConfig {
8664 pairs: vec![
8665 BracketPair {
8666 start: "{".to_string(),
8667 end: "}".to_string(),
8668 close: true,
8669 surround: true,
8670 newline: true,
8671 },
8672 BracketPair {
8673 start: "/* ".to_string(),
8674 end: "*/".to_string(),
8675 close: true,
8676 surround: true,
8677 ..Default::default()
8678 },
8679 ],
8680 ..Default::default()
8681 },
8682 ..Default::default()
8683 },
8684 Some(tree_sitter_rust::LANGUAGE.into()),
8685 ));
8686
8687 let text = r#"
8688 a
8689 b
8690 c
8691 "#
8692 .unindent();
8693
8694 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8695 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8696 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8697 editor
8698 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8699 .await;
8700
8701 editor.update_in(cx, |editor, window, cx| {
8702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8703 s.select_display_ranges([
8704 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8705 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8706 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8707 ])
8708 });
8709
8710 editor.handle_input("{", window, cx);
8711 editor.handle_input("{", window, cx);
8712 editor.handle_input("{", window, cx);
8713 assert_eq!(
8714 editor.text(cx),
8715 "
8716 {{{a}}}
8717 {{{b}}}
8718 {{{c}}}
8719 "
8720 .unindent()
8721 );
8722 assert_eq!(
8723 editor.selections.display_ranges(cx),
8724 [
8725 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8726 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8727 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8728 ]
8729 );
8730
8731 editor.undo(&Undo, window, cx);
8732 editor.undo(&Undo, window, cx);
8733 editor.undo(&Undo, window, cx);
8734 assert_eq!(
8735 editor.text(cx),
8736 "
8737 a
8738 b
8739 c
8740 "
8741 .unindent()
8742 );
8743 assert_eq!(
8744 editor.selections.display_ranges(cx),
8745 [
8746 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8747 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8748 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8749 ]
8750 );
8751
8752 // Ensure inserting the first character of a multi-byte bracket pair
8753 // doesn't surround the selections with the bracket.
8754 editor.handle_input("/", window, cx);
8755 assert_eq!(
8756 editor.text(cx),
8757 "
8758 /
8759 /
8760 /
8761 "
8762 .unindent()
8763 );
8764 assert_eq!(
8765 editor.selections.display_ranges(cx),
8766 [
8767 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8768 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8769 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8770 ]
8771 );
8772
8773 editor.undo(&Undo, window, cx);
8774 assert_eq!(
8775 editor.text(cx),
8776 "
8777 a
8778 b
8779 c
8780 "
8781 .unindent()
8782 );
8783 assert_eq!(
8784 editor.selections.display_ranges(cx),
8785 [
8786 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8787 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8788 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8789 ]
8790 );
8791
8792 // Ensure inserting the last character of a multi-byte bracket pair
8793 // doesn't surround the selections with the bracket.
8794 editor.handle_input("*", window, cx);
8795 assert_eq!(
8796 editor.text(cx),
8797 "
8798 *
8799 *
8800 *
8801 "
8802 .unindent()
8803 );
8804 assert_eq!(
8805 editor.selections.display_ranges(cx),
8806 [
8807 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8808 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8809 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8810 ]
8811 );
8812 });
8813}
8814
8815#[gpui::test]
8816async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8817 init_test(cx, |_| {});
8818
8819 let language = Arc::new(Language::new(
8820 LanguageConfig {
8821 brackets: BracketPairConfig {
8822 pairs: vec![BracketPair {
8823 start: "{".to_string(),
8824 end: "}".to_string(),
8825 close: true,
8826 surround: true,
8827 newline: true,
8828 }],
8829 ..Default::default()
8830 },
8831 autoclose_before: "}".to_string(),
8832 ..Default::default()
8833 },
8834 Some(tree_sitter_rust::LANGUAGE.into()),
8835 ));
8836
8837 let text = r#"
8838 a
8839 b
8840 c
8841 "#
8842 .unindent();
8843
8844 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8845 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8846 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8847 editor
8848 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8849 .await;
8850
8851 editor.update_in(cx, |editor, window, cx| {
8852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8853 s.select_ranges([
8854 Point::new(0, 1)..Point::new(0, 1),
8855 Point::new(1, 1)..Point::new(1, 1),
8856 Point::new(2, 1)..Point::new(2, 1),
8857 ])
8858 });
8859
8860 editor.handle_input("{", window, cx);
8861 editor.handle_input("{", window, cx);
8862 editor.handle_input("_", window, cx);
8863 assert_eq!(
8864 editor.text(cx),
8865 "
8866 a{{_}}
8867 b{{_}}
8868 c{{_}}
8869 "
8870 .unindent()
8871 );
8872 assert_eq!(
8873 editor.selections.ranges::<Point>(cx),
8874 [
8875 Point::new(0, 4)..Point::new(0, 4),
8876 Point::new(1, 4)..Point::new(1, 4),
8877 Point::new(2, 4)..Point::new(2, 4)
8878 ]
8879 );
8880
8881 editor.backspace(&Default::default(), window, cx);
8882 editor.backspace(&Default::default(), window, cx);
8883 assert_eq!(
8884 editor.text(cx),
8885 "
8886 a{}
8887 b{}
8888 c{}
8889 "
8890 .unindent()
8891 );
8892 assert_eq!(
8893 editor.selections.ranges::<Point>(cx),
8894 [
8895 Point::new(0, 2)..Point::new(0, 2),
8896 Point::new(1, 2)..Point::new(1, 2),
8897 Point::new(2, 2)..Point::new(2, 2)
8898 ]
8899 );
8900
8901 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8902 assert_eq!(
8903 editor.text(cx),
8904 "
8905 a
8906 b
8907 c
8908 "
8909 .unindent()
8910 );
8911 assert_eq!(
8912 editor.selections.ranges::<Point>(cx),
8913 [
8914 Point::new(0, 1)..Point::new(0, 1),
8915 Point::new(1, 1)..Point::new(1, 1),
8916 Point::new(2, 1)..Point::new(2, 1)
8917 ]
8918 );
8919 });
8920}
8921
8922#[gpui::test]
8923async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8924 init_test(cx, |settings| {
8925 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8926 });
8927
8928 let mut cx = EditorTestContext::new(cx).await;
8929
8930 let language = Arc::new(Language::new(
8931 LanguageConfig {
8932 brackets: BracketPairConfig {
8933 pairs: vec![
8934 BracketPair {
8935 start: "{".to_string(),
8936 end: "}".to_string(),
8937 close: true,
8938 surround: true,
8939 newline: true,
8940 },
8941 BracketPair {
8942 start: "(".to_string(),
8943 end: ")".to_string(),
8944 close: true,
8945 surround: true,
8946 newline: true,
8947 },
8948 BracketPair {
8949 start: "[".to_string(),
8950 end: "]".to_string(),
8951 close: false,
8952 surround: true,
8953 newline: true,
8954 },
8955 ],
8956 ..Default::default()
8957 },
8958 autoclose_before: "})]".to_string(),
8959 ..Default::default()
8960 },
8961 Some(tree_sitter_rust::LANGUAGE.into()),
8962 ));
8963
8964 cx.language_registry().add(language.clone());
8965 cx.update_buffer(|buffer, cx| {
8966 buffer.set_language(Some(language), cx);
8967 });
8968
8969 cx.set_state(
8970 &"
8971 {(ˇ)}
8972 [[ˇ]]
8973 {(ˇ)}
8974 "
8975 .unindent(),
8976 );
8977
8978 cx.update_editor(|editor, window, cx| {
8979 editor.backspace(&Default::default(), window, cx);
8980 editor.backspace(&Default::default(), window, cx);
8981 });
8982
8983 cx.assert_editor_state(
8984 &"
8985 ˇ
8986 ˇ]]
8987 ˇ
8988 "
8989 .unindent(),
8990 );
8991
8992 cx.update_editor(|editor, window, cx| {
8993 editor.handle_input("{", window, cx);
8994 editor.handle_input("{", window, cx);
8995 editor.move_right(&MoveRight, window, cx);
8996 editor.move_right(&MoveRight, window, cx);
8997 editor.move_left(&MoveLeft, window, cx);
8998 editor.move_left(&MoveLeft, window, cx);
8999 editor.backspace(&Default::default(), window, cx);
9000 });
9001
9002 cx.assert_editor_state(
9003 &"
9004 {ˇ}
9005 {ˇ}]]
9006 {ˇ}
9007 "
9008 .unindent(),
9009 );
9010
9011 cx.update_editor(|editor, window, cx| {
9012 editor.backspace(&Default::default(), window, cx);
9013 });
9014
9015 cx.assert_editor_state(
9016 &"
9017 ˇ
9018 ˇ]]
9019 ˇ
9020 "
9021 .unindent(),
9022 );
9023}
9024
9025#[gpui::test]
9026async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9027 init_test(cx, |_| {});
9028
9029 let language = Arc::new(Language::new(
9030 LanguageConfig::default(),
9031 Some(tree_sitter_rust::LANGUAGE.into()),
9032 ));
9033
9034 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9035 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9036 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9037 editor
9038 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9039 .await;
9040
9041 editor.update_in(cx, |editor, window, cx| {
9042 editor.set_auto_replace_emoji_shortcode(true);
9043
9044 editor.handle_input("Hello ", window, cx);
9045 editor.handle_input(":wave", window, cx);
9046 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9047
9048 editor.handle_input(":", window, cx);
9049 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9050
9051 editor.handle_input(" :smile", window, cx);
9052 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9053
9054 editor.handle_input(":", window, cx);
9055 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9056
9057 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9058 editor.handle_input(":wave", window, cx);
9059 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9060
9061 editor.handle_input(":", window, cx);
9062 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9063
9064 editor.handle_input(":1", window, cx);
9065 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9066
9067 editor.handle_input(":", window, cx);
9068 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9069
9070 // Ensure shortcode does not get replaced when it is part of a word
9071 editor.handle_input(" Test:wave", window, cx);
9072 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9073
9074 editor.handle_input(":", window, cx);
9075 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9076
9077 editor.set_auto_replace_emoji_shortcode(false);
9078
9079 // Ensure shortcode does not get replaced when auto replace is off
9080 editor.handle_input(" :wave", window, cx);
9081 assert_eq!(
9082 editor.text(cx),
9083 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9084 );
9085
9086 editor.handle_input(":", window, cx);
9087 assert_eq!(
9088 editor.text(cx),
9089 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9090 );
9091 });
9092}
9093
9094#[gpui::test]
9095async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9096 init_test(cx, |_| {});
9097
9098 let (text, insertion_ranges) = marked_text_ranges(
9099 indoc! {"
9100 ˇ
9101 "},
9102 false,
9103 );
9104
9105 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9106 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9107
9108 _ = editor.update_in(cx, |editor, window, cx| {
9109 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9110
9111 editor
9112 .insert_snippet(&insertion_ranges, snippet, window, cx)
9113 .unwrap();
9114
9115 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9116 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9117 assert_eq!(editor.text(cx), expected_text);
9118 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9119 }
9120
9121 assert(
9122 editor,
9123 cx,
9124 indoc! {"
9125 type «» =•
9126 "},
9127 );
9128
9129 assert!(editor.context_menu_visible(), "There should be a matches");
9130 });
9131}
9132
9133#[gpui::test]
9134async fn test_snippets(cx: &mut TestAppContext) {
9135 init_test(cx, |_| {});
9136
9137 let mut cx = EditorTestContext::new(cx).await;
9138
9139 cx.set_state(indoc! {"
9140 a.ˇ b
9141 a.ˇ b
9142 a.ˇ b
9143 "});
9144
9145 cx.update_editor(|editor, window, cx| {
9146 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9147 let insertion_ranges = editor
9148 .selections
9149 .all(cx)
9150 .iter()
9151 .map(|s| s.range().clone())
9152 .collect::<Vec<_>>();
9153 editor
9154 .insert_snippet(&insertion_ranges, snippet, window, cx)
9155 .unwrap();
9156 });
9157
9158 cx.assert_editor_state(indoc! {"
9159 a.f(«oneˇ», two, «threeˇ») b
9160 a.f(«oneˇ», two, «threeˇ») b
9161 a.f(«oneˇ», two, «threeˇ») b
9162 "});
9163
9164 // Can't move earlier than the first tab stop
9165 cx.update_editor(|editor, window, cx| {
9166 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9167 });
9168 cx.assert_editor_state(indoc! {"
9169 a.f(«oneˇ», two, «threeˇ») b
9170 a.f(«oneˇ», two, «threeˇ») b
9171 a.f(«oneˇ», two, «threeˇ») b
9172 "});
9173
9174 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9175 cx.assert_editor_state(indoc! {"
9176 a.f(one, «twoˇ», three) b
9177 a.f(one, «twoˇ», three) b
9178 a.f(one, «twoˇ», three) b
9179 "});
9180
9181 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9182 cx.assert_editor_state(indoc! {"
9183 a.f(«oneˇ», two, «threeˇ») b
9184 a.f(«oneˇ», two, «threeˇ») b
9185 a.f(«oneˇ», two, «threeˇ») b
9186 "});
9187
9188 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9189 cx.assert_editor_state(indoc! {"
9190 a.f(one, «twoˇ», three) b
9191 a.f(one, «twoˇ», three) b
9192 a.f(one, «twoˇ», three) b
9193 "});
9194 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9195 cx.assert_editor_state(indoc! {"
9196 a.f(one, two, three)ˇ b
9197 a.f(one, two, three)ˇ b
9198 a.f(one, two, three)ˇ b
9199 "});
9200
9201 // As soon as the last tab stop is reached, snippet state is gone
9202 cx.update_editor(|editor, window, cx| {
9203 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9204 });
9205 cx.assert_editor_state(indoc! {"
9206 a.f(one, two, three)ˇ b
9207 a.f(one, two, three)ˇ b
9208 a.f(one, two, three)ˇ b
9209 "});
9210}
9211
9212#[gpui::test]
9213async fn test_snippet_indentation(cx: &mut TestAppContext) {
9214 init_test(cx, |_| {});
9215
9216 let mut cx = EditorTestContext::new(cx).await;
9217
9218 cx.update_editor(|editor, window, cx| {
9219 let snippet = Snippet::parse(indoc! {"
9220 /*
9221 * Multiline comment with leading indentation
9222 *
9223 * $1
9224 */
9225 $0"})
9226 .unwrap();
9227 let insertion_ranges = editor
9228 .selections
9229 .all(cx)
9230 .iter()
9231 .map(|s| s.range().clone())
9232 .collect::<Vec<_>>();
9233 editor
9234 .insert_snippet(&insertion_ranges, snippet, window, cx)
9235 .unwrap();
9236 });
9237
9238 cx.assert_editor_state(indoc! {"
9239 /*
9240 * Multiline comment with leading indentation
9241 *
9242 * ˇ
9243 */
9244 "});
9245
9246 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9247 cx.assert_editor_state(indoc! {"
9248 /*
9249 * Multiline comment with leading indentation
9250 *
9251 *•
9252 */
9253 ˇ"});
9254}
9255
9256#[gpui::test]
9257async fn test_document_format_during_save(cx: &mut TestAppContext) {
9258 init_test(cx, |_| {});
9259
9260 let fs = FakeFs::new(cx.executor());
9261 fs.insert_file(path!("/file.rs"), Default::default()).await;
9262
9263 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9264
9265 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9266 language_registry.add(rust_lang());
9267 let mut fake_servers = language_registry.register_fake_lsp(
9268 "Rust",
9269 FakeLspAdapter {
9270 capabilities: lsp::ServerCapabilities {
9271 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9272 ..Default::default()
9273 },
9274 ..Default::default()
9275 },
9276 );
9277
9278 let buffer = project
9279 .update(cx, |project, cx| {
9280 project.open_local_buffer(path!("/file.rs"), cx)
9281 })
9282 .await
9283 .unwrap();
9284
9285 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9286 let (editor, cx) = cx.add_window_view(|window, cx| {
9287 build_editor_with_project(project.clone(), buffer, window, cx)
9288 });
9289 editor.update_in(cx, |editor, window, cx| {
9290 editor.set_text("one\ntwo\nthree\n", window, cx)
9291 });
9292 assert!(cx.read(|cx| editor.is_dirty(cx)));
9293
9294 cx.executor().start_waiting();
9295 let fake_server = fake_servers.next().await.unwrap();
9296
9297 {
9298 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9299 move |params, _| async move {
9300 assert_eq!(
9301 params.text_document.uri,
9302 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9303 );
9304 assert_eq!(params.options.tab_size, 4);
9305 Ok(Some(vec![lsp::TextEdit::new(
9306 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9307 ", ".to_string(),
9308 )]))
9309 },
9310 );
9311 let save = editor
9312 .update_in(cx, |editor, window, cx| {
9313 editor.save(
9314 SaveOptions {
9315 format: true,
9316 autosave: false,
9317 },
9318 project.clone(),
9319 window,
9320 cx,
9321 )
9322 })
9323 .unwrap();
9324 cx.executor().start_waiting();
9325 save.await;
9326
9327 assert_eq!(
9328 editor.update(cx, |editor, cx| editor.text(cx)),
9329 "one, two\nthree\n"
9330 );
9331 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9332 }
9333
9334 {
9335 editor.update_in(cx, |editor, window, cx| {
9336 editor.set_text("one\ntwo\nthree\n", window, cx)
9337 });
9338 assert!(cx.read(|cx| editor.is_dirty(cx)));
9339
9340 // Ensure we can still save even if formatting hangs.
9341 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9342 move |params, _| async move {
9343 assert_eq!(
9344 params.text_document.uri,
9345 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9346 );
9347 futures::future::pending::<()>().await;
9348 unreachable!()
9349 },
9350 );
9351 let save = editor
9352 .update_in(cx, |editor, window, cx| {
9353 editor.save(
9354 SaveOptions {
9355 format: true,
9356 autosave: false,
9357 },
9358 project.clone(),
9359 window,
9360 cx,
9361 )
9362 })
9363 .unwrap();
9364 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9365 cx.executor().start_waiting();
9366 save.await;
9367 assert_eq!(
9368 editor.update(cx, |editor, cx| editor.text(cx)),
9369 "one\ntwo\nthree\n"
9370 );
9371 }
9372
9373 // Set rust language override and assert overridden tabsize is sent to language server
9374 update_test_language_settings(cx, |settings| {
9375 settings.languages.insert(
9376 "Rust".into(),
9377 LanguageSettingsContent {
9378 tab_size: NonZeroU32::new(8),
9379 ..Default::default()
9380 },
9381 );
9382 });
9383
9384 {
9385 editor.update_in(cx, |editor, window, cx| {
9386 editor.set_text("somehting_new\n", window, cx)
9387 });
9388 assert!(cx.read(|cx| editor.is_dirty(cx)));
9389 let _formatting_request_signal = fake_server
9390 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9391 assert_eq!(
9392 params.text_document.uri,
9393 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9394 );
9395 assert_eq!(params.options.tab_size, 8);
9396 Ok(Some(vec![]))
9397 });
9398 let save = editor
9399 .update_in(cx, |editor, window, cx| {
9400 editor.save(
9401 SaveOptions {
9402 format: true,
9403 autosave: false,
9404 },
9405 project.clone(),
9406 window,
9407 cx,
9408 )
9409 })
9410 .unwrap();
9411 cx.executor().start_waiting();
9412 save.await;
9413 }
9414}
9415
9416#[gpui::test]
9417async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9418 init_test(cx, |_| {});
9419
9420 let cols = 4;
9421 let rows = 10;
9422 let sample_text_1 = sample_text(rows, cols, 'a');
9423 assert_eq!(
9424 sample_text_1,
9425 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9426 );
9427 let sample_text_2 = sample_text(rows, cols, 'l');
9428 assert_eq!(
9429 sample_text_2,
9430 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9431 );
9432 let sample_text_3 = sample_text(rows, cols, 'v');
9433 assert_eq!(
9434 sample_text_3,
9435 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9436 );
9437
9438 let fs = FakeFs::new(cx.executor());
9439 fs.insert_tree(
9440 path!("/a"),
9441 json!({
9442 "main.rs": sample_text_1,
9443 "other.rs": sample_text_2,
9444 "lib.rs": sample_text_3,
9445 }),
9446 )
9447 .await;
9448
9449 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9450 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9451 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9452
9453 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9454 language_registry.add(rust_lang());
9455 let mut fake_servers = language_registry.register_fake_lsp(
9456 "Rust",
9457 FakeLspAdapter {
9458 capabilities: lsp::ServerCapabilities {
9459 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9460 ..Default::default()
9461 },
9462 ..Default::default()
9463 },
9464 );
9465
9466 let worktree = project.update(cx, |project, cx| {
9467 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9468 assert_eq!(worktrees.len(), 1);
9469 worktrees.pop().unwrap()
9470 });
9471 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9472
9473 let buffer_1 = project
9474 .update(cx, |project, cx| {
9475 project.open_buffer((worktree_id, "main.rs"), cx)
9476 })
9477 .await
9478 .unwrap();
9479 let buffer_2 = project
9480 .update(cx, |project, cx| {
9481 project.open_buffer((worktree_id, "other.rs"), cx)
9482 })
9483 .await
9484 .unwrap();
9485 let buffer_3 = project
9486 .update(cx, |project, cx| {
9487 project.open_buffer((worktree_id, "lib.rs"), cx)
9488 })
9489 .await
9490 .unwrap();
9491
9492 let multi_buffer = cx.new(|cx| {
9493 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9494 multi_buffer.push_excerpts(
9495 buffer_1.clone(),
9496 [
9497 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9498 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9499 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9500 ],
9501 cx,
9502 );
9503 multi_buffer.push_excerpts(
9504 buffer_2.clone(),
9505 [
9506 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9507 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9508 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9509 ],
9510 cx,
9511 );
9512 multi_buffer.push_excerpts(
9513 buffer_3.clone(),
9514 [
9515 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9516 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9517 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9518 ],
9519 cx,
9520 );
9521 multi_buffer
9522 });
9523 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9524 Editor::new(
9525 EditorMode::full(),
9526 multi_buffer,
9527 Some(project.clone()),
9528 window,
9529 cx,
9530 )
9531 });
9532
9533 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9534 editor.change_selections(
9535 SelectionEffects::scroll(Autoscroll::Next),
9536 window,
9537 cx,
9538 |s| s.select_ranges(Some(1..2)),
9539 );
9540 editor.insert("|one|two|three|", window, cx);
9541 });
9542 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9543 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9544 editor.change_selections(
9545 SelectionEffects::scroll(Autoscroll::Next),
9546 window,
9547 cx,
9548 |s| s.select_ranges(Some(60..70)),
9549 );
9550 editor.insert("|four|five|six|", window, cx);
9551 });
9552 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9553
9554 // First two buffers should be edited, but not the third one.
9555 assert_eq!(
9556 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9557 "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}",
9558 );
9559 buffer_1.update(cx, |buffer, _| {
9560 assert!(buffer.is_dirty());
9561 assert_eq!(
9562 buffer.text(),
9563 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9564 )
9565 });
9566 buffer_2.update(cx, |buffer, _| {
9567 assert!(buffer.is_dirty());
9568 assert_eq!(
9569 buffer.text(),
9570 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9571 )
9572 });
9573 buffer_3.update(cx, |buffer, _| {
9574 assert!(!buffer.is_dirty());
9575 assert_eq!(buffer.text(), sample_text_3,)
9576 });
9577 cx.executor().run_until_parked();
9578
9579 cx.executor().start_waiting();
9580 let save = multi_buffer_editor
9581 .update_in(cx, |editor, window, cx| {
9582 editor.save(
9583 SaveOptions {
9584 format: true,
9585 autosave: false,
9586 },
9587 project.clone(),
9588 window,
9589 cx,
9590 )
9591 })
9592 .unwrap();
9593
9594 let fake_server = fake_servers.next().await.unwrap();
9595 fake_server
9596 .server
9597 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9598 Ok(Some(vec![lsp::TextEdit::new(
9599 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9600 format!("[{} formatted]", params.text_document.uri),
9601 )]))
9602 })
9603 .detach();
9604 save.await;
9605
9606 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9607 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9608 assert_eq!(
9609 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9610 uri!(
9611 "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}"
9612 ),
9613 );
9614 buffer_1.update(cx, |buffer, _| {
9615 assert!(!buffer.is_dirty());
9616 assert_eq!(
9617 buffer.text(),
9618 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9619 )
9620 });
9621 buffer_2.update(cx, |buffer, _| {
9622 assert!(!buffer.is_dirty());
9623 assert_eq!(
9624 buffer.text(),
9625 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9626 )
9627 });
9628 buffer_3.update(cx, |buffer, _| {
9629 assert!(!buffer.is_dirty());
9630 assert_eq!(buffer.text(), sample_text_3,)
9631 });
9632}
9633
9634#[gpui::test]
9635async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9636 init_test(cx, |_| {});
9637
9638 let fs = FakeFs::new(cx.executor());
9639 fs.insert_tree(
9640 path!("/dir"),
9641 json!({
9642 "file1.rs": "fn main() { println!(\"hello\"); }",
9643 "file2.rs": "fn test() { println!(\"test\"); }",
9644 "file3.rs": "fn other() { println!(\"other\"); }\n",
9645 }),
9646 )
9647 .await;
9648
9649 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9650 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9651 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9652
9653 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9654 language_registry.add(rust_lang());
9655
9656 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9657 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9658
9659 // Open three buffers
9660 let buffer_1 = project
9661 .update(cx, |project, cx| {
9662 project.open_buffer((worktree_id, "file1.rs"), cx)
9663 })
9664 .await
9665 .unwrap();
9666 let buffer_2 = project
9667 .update(cx, |project, cx| {
9668 project.open_buffer((worktree_id, "file2.rs"), cx)
9669 })
9670 .await
9671 .unwrap();
9672 let buffer_3 = project
9673 .update(cx, |project, cx| {
9674 project.open_buffer((worktree_id, "file3.rs"), cx)
9675 })
9676 .await
9677 .unwrap();
9678
9679 // Create a multi-buffer with all three buffers
9680 let multi_buffer = cx.new(|cx| {
9681 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9682 multi_buffer.push_excerpts(
9683 buffer_1.clone(),
9684 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9685 cx,
9686 );
9687 multi_buffer.push_excerpts(
9688 buffer_2.clone(),
9689 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9690 cx,
9691 );
9692 multi_buffer.push_excerpts(
9693 buffer_3.clone(),
9694 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9695 cx,
9696 );
9697 multi_buffer
9698 });
9699
9700 let editor = cx.new_window_entity(|window, cx| {
9701 Editor::new(
9702 EditorMode::full(),
9703 multi_buffer,
9704 Some(project.clone()),
9705 window,
9706 cx,
9707 )
9708 });
9709
9710 // Edit only the first buffer
9711 editor.update_in(cx, |editor, window, cx| {
9712 editor.change_selections(
9713 SelectionEffects::scroll(Autoscroll::Next),
9714 window,
9715 cx,
9716 |s| s.select_ranges(Some(10..10)),
9717 );
9718 editor.insert("// edited", window, cx);
9719 });
9720
9721 // Verify that only buffer 1 is dirty
9722 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9723 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9724 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9725
9726 // Get write counts after file creation (files were created with initial content)
9727 // We expect each file to have been written once during creation
9728 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9729 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9730 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9731
9732 // Perform autosave
9733 let save_task = editor.update_in(cx, |editor, window, cx| {
9734 editor.save(
9735 SaveOptions {
9736 format: true,
9737 autosave: true,
9738 },
9739 project.clone(),
9740 window,
9741 cx,
9742 )
9743 });
9744 save_task.await.unwrap();
9745
9746 // Only the dirty buffer should have been saved
9747 assert_eq!(
9748 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9749 1,
9750 "Buffer 1 was dirty, so it should have been written once during autosave"
9751 );
9752 assert_eq!(
9753 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9754 0,
9755 "Buffer 2 was clean, so it should not have been written during autosave"
9756 );
9757 assert_eq!(
9758 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9759 0,
9760 "Buffer 3 was clean, so it should not have been written during autosave"
9761 );
9762
9763 // Verify buffer states after autosave
9764 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9765 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9766 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9767
9768 // Now perform a manual save (format = true)
9769 let save_task = editor.update_in(cx, |editor, window, cx| {
9770 editor.save(
9771 SaveOptions {
9772 format: true,
9773 autosave: false,
9774 },
9775 project.clone(),
9776 window,
9777 cx,
9778 )
9779 });
9780 save_task.await.unwrap();
9781
9782 // During manual save, clean buffers don't get written to disk
9783 // They just get did_save called for language server notifications
9784 assert_eq!(
9785 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9786 1,
9787 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9788 );
9789 assert_eq!(
9790 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9791 0,
9792 "Buffer 2 should not have been written at all"
9793 );
9794 assert_eq!(
9795 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9796 0,
9797 "Buffer 3 should not have been written at all"
9798 );
9799}
9800
9801#[gpui::test]
9802async fn test_range_format_during_save(cx: &mut TestAppContext) {
9803 init_test(cx, |_| {});
9804
9805 let fs = FakeFs::new(cx.executor());
9806 fs.insert_file(path!("/file.rs"), Default::default()).await;
9807
9808 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9809
9810 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9811 language_registry.add(rust_lang());
9812 let mut fake_servers = language_registry.register_fake_lsp(
9813 "Rust",
9814 FakeLspAdapter {
9815 capabilities: lsp::ServerCapabilities {
9816 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9817 ..Default::default()
9818 },
9819 ..Default::default()
9820 },
9821 );
9822
9823 let buffer = project
9824 .update(cx, |project, cx| {
9825 project.open_local_buffer(path!("/file.rs"), cx)
9826 })
9827 .await
9828 .unwrap();
9829
9830 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9831 let (editor, cx) = cx.add_window_view(|window, cx| {
9832 build_editor_with_project(project.clone(), buffer, window, cx)
9833 });
9834 editor.update_in(cx, |editor, window, cx| {
9835 editor.set_text("one\ntwo\nthree\n", window, cx)
9836 });
9837 assert!(cx.read(|cx| editor.is_dirty(cx)));
9838
9839 cx.executor().start_waiting();
9840 let fake_server = fake_servers.next().await.unwrap();
9841
9842 let save = editor
9843 .update_in(cx, |editor, window, cx| {
9844 editor.save(
9845 SaveOptions {
9846 format: true,
9847 autosave: false,
9848 },
9849 project.clone(),
9850 window,
9851 cx,
9852 )
9853 })
9854 .unwrap();
9855 fake_server
9856 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9857 assert_eq!(
9858 params.text_document.uri,
9859 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9860 );
9861 assert_eq!(params.options.tab_size, 4);
9862 Ok(Some(vec![lsp::TextEdit::new(
9863 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9864 ", ".to_string(),
9865 )]))
9866 })
9867 .next()
9868 .await;
9869 cx.executor().start_waiting();
9870 save.await;
9871 assert_eq!(
9872 editor.update(cx, |editor, cx| editor.text(cx)),
9873 "one, two\nthree\n"
9874 );
9875 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9876
9877 editor.update_in(cx, |editor, window, cx| {
9878 editor.set_text("one\ntwo\nthree\n", window, cx)
9879 });
9880 assert!(cx.read(|cx| editor.is_dirty(cx)));
9881
9882 // Ensure we can still save even if formatting hangs.
9883 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9884 move |params, _| async move {
9885 assert_eq!(
9886 params.text_document.uri,
9887 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9888 );
9889 futures::future::pending::<()>().await;
9890 unreachable!()
9891 },
9892 );
9893 let save = editor
9894 .update_in(cx, |editor, window, cx| {
9895 editor.save(
9896 SaveOptions {
9897 format: true,
9898 autosave: false,
9899 },
9900 project.clone(),
9901 window,
9902 cx,
9903 )
9904 })
9905 .unwrap();
9906 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9907 cx.executor().start_waiting();
9908 save.await;
9909 assert_eq!(
9910 editor.update(cx, |editor, cx| editor.text(cx)),
9911 "one\ntwo\nthree\n"
9912 );
9913 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9914
9915 // For non-dirty buffer, no formatting request should be sent
9916 let save = editor
9917 .update_in(cx, |editor, window, cx| {
9918 editor.save(
9919 SaveOptions {
9920 format: false,
9921 autosave: false,
9922 },
9923 project.clone(),
9924 window,
9925 cx,
9926 )
9927 })
9928 .unwrap();
9929 let _pending_format_request = fake_server
9930 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9931 panic!("Should not be invoked");
9932 })
9933 .next();
9934 cx.executor().start_waiting();
9935 save.await;
9936
9937 // Set Rust language override and assert overridden tabsize is sent to language server
9938 update_test_language_settings(cx, |settings| {
9939 settings.languages.insert(
9940 "Rust".into(),
9941 LanguageSettingsContent {
9942 tab_size: NonZeroU32::new(8),
9943 ..Default::default()
9944 },
9945 );
9946 });
9947
9948 editor.update_in(cx, |editor, window, cx| {
9949 editor.set_text("somehting_new\n", window, cx)
9950 });
9951 assert!(cx.read(|cx| editor.is_dirty(cx)));
9952 let save = editor
9953 .update_in(cx, |editor, window, cx| {
9954 editor.save(
9955 SaveOptions {
9956 format: true,
9957 autosave: false,
9958 },
9959 project.clone(),
9960 window,
9961 cx,
9962 )
9963 })
9964 .unwrap();
9965 fake_server
9966 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9967 assert_eq!(
9968 params.text_document.uri,
9969 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9970 );
9971 assert_eq!(params.options.tab_size, 8);
9972 Ok(Some(Vec::new()))
9973 })
9974 .next()
9975 .await;
9976 save.await;
9977}
9978
9979#[gpui::test]
9980async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9981 init_test(cx, |settings| {
9982 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9983 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9984 ))
9985 });
9986
9987 let fs = FakeFs::new(cx.executor());
9988 fs.insert_file(path!("/file.rs"), Default::default()).await;
9989
9990 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9991
9992 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9993 language_registry.add(Arc::new(Language::new(
9994 LanguageConfig {
9995 name: "Rust".into(),
9996 matcher: LanguageMatcher {
9997 path_suffixes: vec!["rs".to_string()],
9998 ..Default::default()
9999 },
10000 ..LanguageConfig::default()
10001 },
10002 Some(tree_sitter_rust::LANGUAGE.into()),
10003 )));
10004 update_test_language_settings(cx, |settings| {
10005 // Enable Prettier formatting for the same buffer, and ensure
10006 // LSP is called instead of Prettier.
10007 settings.defaults.prettier = Some(PrettierSettings {
10008 allowed: true,
10009 ..PrettierSettings::default()
10010 });
10011 });
10012 let mut fake_servers = language_registry.register_fake_lsp(
10013 "Rust",
10014 FakeLspAdapter {
10015 capabilities: lsp::ServerCapabilities {
10016 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10017 ..Default::default()
10018 },
10019 ..Default::default()
10020 },
10021 );
10022
10023 let buffer = project
10024 .update(cx, |project, cx| {
10025 project.open_local_buffer(path!("/file.rs"), cx)
10026 })
10027 .await
10028 .unwrap();
10029
10030 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10031 let (editor, cx) = cx.add_window_view(|window, cx| {
10032 build_editor_with_project(project.clone(), buffer, window, cx)
10033 });
10034 editor.update_in(cx, |editor, window, cx| {
10035 editor.set_text("one\ntwo\nthree\n", window, cx)
10036 });
10037
10038 cx.executor().start_waiting();
10039 let fake_server = fake_servers.next().await.unwrap();
10040
10041 let format = editor
10042 .update_in(cx, |editor, window, cx| {
10043 editor.perform_format(
10044 project.clone(),
10045 FormatTrigger::Manual,
10046 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10047 window,
10048 cx,
10049 )
10050 })
10051 .unwrap();
10052 fake_server
10053 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10054 assert_eq!(
10055 params.text_document.uri,
10056 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10057 );
10058 assert_eq!(params.options.tab_size, 4);
10059 Ok(Some(vec![lsp::TextEdit::new(
10060 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10061 ", ".to_string(),
10062 )]))
10063 })
10064 .next()
10065 .await;
10066 cx.executor().start_waiting();
10067 format.await;
10068 assert_eq!(
10069 editor.update(cx, |editor, cx| editor.text(cx)),
10070 "one, two\nthree\n"
10071 );
10072
10073 editor.update_in(cx, |editor, window, cx| {
10074 editor.set_text("one\ntwo\nthree\n", window, cx)
10075 });
10076 // Ensure we don't lock if formatting hangs.
10077 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10078 move |params, _| async move {
10079 assert_eq!(
10080 params.text_document.uri,
10081 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10082 );
10083 futures::future::pending::<()>().await;
10084 unreachable!()
10085 },
10086 );
10087 let format = editor
10088 .update_in(cx, |editor, window, cx| {
10089 editor.perform_format(
10090 project,
10091 FormatTrigger::Manual,
10092 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10093 window,
10094 cx,
10095 )
10096 })
10097 .unwrap();
10098 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10099 cx.executor().start_waiting();
10100 format.await;
10101 assert_eq!(
10102 editor.update(cx, |editor, cx| editor.text(cx)),
10103 "one\ntwo\nthree\n"
10104 );
10105}
10106
10107#[gpui::test]
10108async fn test_multiple_formatters(cx: &mut TestAppContext) {
10109 init_test(cx, |settings| {
10110 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10111 settings.defaults.formatter =
10112 Some(language_settings::SelectedFormatter::List(FormatterList(
10113 vec![
10114 Formatter::LanguageServer { name: None },
10115 Formatter::CodeActions(
10116 [
10117 ("code-action-1".into(), true),
10118 ("code-action-2".into(), true),
10119 ]
10120 .into_iter()
10121 .collect(),
10122 ),
10123 ]
10124 .into(),
10125 )))
10126 });
10127
10128 let fs = FakeFs::new(cx.executor());
10129 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10130 .await;
10131
10132 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10133 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10134 language_registry.add(rust_lang());
10135
10136 let mut fake_servers = language_registry.register_fake_lsp(
10137 "Rust",
10138 FakeLspAdapter {
10139 capabilities: lsp::ServerCapabilities {
10140 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10141 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10142 commands: vec!["the-command-for-code-action-1".into()],
10143 ..Default::default()
10144 }),
10145 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10146 ..Default::default()
10147 },
10148 ..Default::default()
10149 },
10150 );
10151
10152 let buffer = project
10153 .update(cx, |project, cx| {
10154 project.open_local_buffer(path!("/file.rs"), cx)
10155 })
10156 .await
10157 .unwrap();
10158
10159 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10160 let (editor, cx) = cx.add_window_view(|window, cx| {
10161 build_editor_with_project(project.clone(), buffer, window, cx)
10162 });
10163
10164 cx.executor().start_waiting();
10165
10166 let fake_server = fake_servers.next().await.unwrap();
10167 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10168 move |_params, _| async move {
10169 Ok(Some(vec![lsp::TextEdit::new(
10170 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10171 "applied-formatting\n".to_string(),
10172 )]))
10173 },
10174 );
10175 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10176 move |params, _| async move {
10177 assert_eq!(
10178 params.context.only,
10179 Some(vec!["code-action-1".into(), "code-action-2".into()])
10180 );
10181 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10182 Ok(Some(vec![
10183 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10184 kind: Some("code-action-1".into()),
10185 edit: Some(lsp::WorkspaceEdit::new(
10186 [(
10187 uri.clone(),
10188 vec![lsp::TextEdit::new(
10189 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10190 "applied-code-action-1-edit\n".to_string(),
10191 )],
10192 )]
10193 .into_iter()
10194 .collect(),
10195 )),
10196 command: Some(lsp::Command {
10197 command: "the-command-for-code-action-1".into(),
10198 ..Default::default()
10199 }),
10200 ..Default::default()
10201 }),
10202 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10203 kind: Some("code-action-2".into()),
10204 edit: Some(lsp::WorkspaceEdit::new(
10205 [(
10206 uri.clone(),
10207 vec![lsp::TextEdit::new(
10208 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10209 "applied-code-action-2-edit\n".to_string(),
10210 )],
10211 )]
10212 .into_iter()
10213 .collect(),
10214 )),
10215 ..Default::default()
10216 }),
10217 ]))
10218 },
10219 );
10220
10221 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10222 move |params, _| async move { Ok(params) }
10223 });
10224
10225 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10226 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10227 let fake = fake_server.clone();
10228 let lock = command_lock.clone();
10229 move |params, _| {
10230 assert_eq!(params.command, "the-command-for-code-action-1");
10231 let fake = fake.clone();
10232 let lock = lock.clone();
10233 async move {
10234 lock.lock().await;
10235 fake.server
10236 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10237 label: None,
10238 edit: lsp::WorkspaceEdit {
10239 changes: Some(
10240 [(
10241 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10242 vec![lsp::TextEdit {
10243 range: lsp::Range::new(
10244 lsp::Position::new(0, 0),
10245 lsp::Position::new(0, 0),
10246 ),
10247 new_text: "applied-code-action-1-command\n".into(),
10248 }],
10249 )]
10250 .into_iter()
10251 .collect(),
10252 ),
10253 ..Default::default()
10254 },
10255 })
10256 .await
10257 .into_response()
10258 .unwrap();
10259 Ok(Some(json!(null)))
10260 }
10261 }
10262 });
10263
10264 cx.executor().start_waiting();
10265 editor
10266 .update_in(cx, |editor, window, cx| {
10267 editor.perform_format(
10268 project.clone(),
10269 FormatTrigger::Manual,
10270 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10271 window,
10272 cx,
10273 )
10274 })
10275 .unwrap()
10276 .await;
10277 editor.update(cx, |editor, cx| {
10278 assert_eq!(
10279 editor.text(cx),
10280 r#"
10281 applied-code-action-2-edit
10282 applied-code-action-1-command
10283 applied-code-action-1-edit
10284 applied-formatting
10285 one
10286 two
10287 three
10288 "#
10289 .unindent()
10290 );
10291 });
10292
10293 editor.update_in(cx, |editor, window, cx| {
10294 editor.undo(&Default::default(), window, cx);
10295 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10296 });
10297
10298 // Perform a manual edit while waiting for an LSP command
10299 // that's being run as part of a formatting code action.
10300 let lock_guard = command_lock.lock().await;
10301 let format = editor
10302 .update_in(cx, |editor, window, cx| {
10303 editor.perform_format(
10304 project.clone(),
10305 FormatTrigger::Manual,
10306 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10307 window,
10308 cx,
10309 )
10310 })
10311 .unwrap();
10312 cx.run_until_parked();
10313 editor.update(cx, |editor, cx| {
10314 assert_eq!(
10315 editor.text(cx),
10316 r#"
10317 applied-code-action-1-edit
10318 applied-formatting
10319 one
10320 two
10321 three
10322 "#
10323 .unindent()
10324 );
10325
10326 editor.buffer.update(cx, |buffer, cx| {
10327 let ix = buffer.len(cx);
10328 buffer.edit([(ix..ix, "edited\n")], None, cx);
10329 });
10330 });
10331
10332 // Allow the LSP command to proceed. Because the buffer was edited,
10333 // the second code action will not be run.
10334 drop(lock_guard);
10335 format.await;
10336 editor.update_in(cx, |editor, window, cx| {
10337 assert_eq!(
10338 editor.text(cx),
10339 r#"
10340 applied-code-action-1-command
10341 applied-code-action-1-edit
10342 applied-formatting
10343 one
10344 two
10345 three
10346 edited
10347 "#
10348 .unindent()
10349 );
10350
10351 // The manual edit is undone first, because it is the last thing the user did
10352 // (even though the command completed afterwards).
10353 editor.undo(&Default::default(), window, cx);
10354 assert_eq!(
10355 editor.text(cx),
10356 r#"
10357 applied-code-action-1-command
10358 applied-code-action-1-edit
10359 applied-formatting
10360 one
10361 two
10362 three
10363 "#
10364 .unindent()
10365 );
10366
10367 // All the formatting (including the command, which completed after the manual edit)
10368 // is undone together.
10369 editor.undo(&Default::default(), window, cx);
10370 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10371 });
10372}
10373
10374#[gpui::test]
10375async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10376 init_test(cx, |settings| {
10377 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
10378 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
10379 ))
10380 });
10381
10382 let fs = FakeFs::new(cx.executor());
10383 fs.insert_file(path!("/file.ts"), Default::default()).await;
10384
10385 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10386
10387 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10388 language_registry.add(Arc::new(Language::new(
10389 LanguageConfig {
10390 name: "TypeScript".into(),
10391 matcher: LanguageMatcher {
10392 path_suffixes: vec!["ts".to_string()],
10393 ..Default::default()
10394 },
10395 ..LanguageConfig::default()
10396 },
10397 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10398 )));
10399 update_test_language_settings(cx, |settings| {
10400 settings.defaults.prettier = Some(PrettierSettings {
10401 allowed: true,
10402 ..PrettierSettings::default()
10403 });
10404 });
10405 let mut fake_servers = language_registry.register_fake_lsp(
10406 "TypeScript",
10407 FakeLspAdapter {
10408 capabilities: lsp::ServerCapabilities {
10409 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10410 ..Default::default()
10411 },
10412 ..Default::default()
10413 },
10414 );
10415
10416 let buffer = project
10417 .update(cx, |project, cx| {
10418 project.open_local_buffer(path!("/file.ts"), cx)
10419 })
10420 .await
10421 .unwrap();
10422
10423 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10424 let (editor, cx) = cx.add_window_view(|window, cx| {
10425 build_editor_with_project(project.clone(), buffer, window, cx)
10426 });
10427 editor.update_in(cx, |editor, window, cx| {
10428 editor.set_text(
10429 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10430 window,
10431 cx,
10432 )
10433 });
10434
10435 cx.executor().start_waiting();
10436 let fake_server = fake_servers.next().await.unwrap();
10437
10438 let format = editor
10439 .update_in(cx, |editor, window, cx| {
10440 editor.perform_code_action_kind(
10441 project.clone(),
10442 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10443 window,
10444 cx,
10445 )
10446 })
10447 .unwrap();
10448 fake_server
10449 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10450 assert_eq!(
10451 params.text_document.uri,
10452 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10453 );
10454 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10455 lsp::CodeAction {
10456 title: "Organize Imports".to_string(),
10457 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10458 edit: Some(lsp::WorkspaceEdit {
10459 changes: Some(
10460 [(
10461 params.text_document.uri.clone(),
10462 vec![lsp::TextEdit::new(
10463 lsp::Range::new(
10464 lsp::Position::new(1, 0),
10465 lsp::Position::new(2, 0),
10466 ),
10467 "".to_string(),
10468 )],
10469 )]
10470 .into_iter()
10471 .collect(),
10472 ),
10473 ..Default::default()
10474 }),
10475 ..Default::default()
10476 },
10477 )]))
10478 })
10479 .next()
10480 .await;
10481 cx.executor().start_waiting();
10482 format.await;
10483 assert_eq!(
10484 editor.update(cx, |editor, cx| editor.text(cx)),
10485 "import { a } from 'module';\n\nconst x = a;\n"
10486 );
10487
10488 editor.update_in(cx, |editor, window, cx| {
10489 editor.set_text(
10490 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10491 window,
10492 cx,
10493 )
10494 });
10495 // Ensure we don't lock if code action hangs.
10496 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10497 move |params, _| async move {
10498 assert_eq!(
10499 params.text_document.uri,
10500 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10501 );
10502 futures::future::pending::<()>().await;
10503 unreachable!()
10504 },
10505 );
10506 let format = editor
10507 .update_in(cx, |editor, window, cx| {
10508 editor.perform_code_action_kind(
10509 project,
10510 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10511 window,
10512 cx,
10513 )
10514 })
10515 .unwrap();
10516 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10517 cx.executor().start_waiting();
10518 format.await;
10519 assert_eq!(
10520 editor.update(cx, |editor, cx| editor.text(cx)),
10521 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10522 );
10523}
10524
10525#[gpui::test]
10526async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10527 init_test(cx, |_| {});
10528
10529 let mut cx = EditorLspTestContext::new_rust(
10530 lsp::ServerCapabilities {
10531 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10532 ..Default::default()
10533 },
10534 cx,
10535 )
10536 .await;
10537
10538 cx.set_state(indoc! {"
10539 one.twoˇ
10540 "});
10541
10542 // The format request takes a long time. When it completes, it inserts
10543 // a newline and an indent before the `.`
10544 cx.lsp
10545 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10546 let executor = cx.background_executor().clone();
10547 async move {
10548 executor.timer(Duration::from_millis(100)).await;
10549 Ok(Some(vec![lsp::TextEdit {
10550 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10551 new_text: "\n ".into(),
10552 }]))
10553 }
10554 });
10555
10556 // Submit a format request.
10557 let format_1 = cx
10558 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10559 .unwrap();
10560 cx.executor().run_until_parked();
10561
10562 // Submit a second format request.
10563 let format_2 = cx
10564 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10565 .unwrap();
10566 cx.executor().run_until_parked();
10567
10568 // Wait for both format requests to complete
10569 cx.executor().advance_clock(Duration::from_millis(200));
10570 cx.executor().start_waiting();
10571 format_1.await.unwrap();
10572 cx.executor().start_waiting();
10573 format_2.await.unwrap();
10574
10575 // The formatting edits only happens once.
10576 cx.assert_editor_state(indoc! {"
10577 one
10578 .twoˇ
10579 "});
10580}
10581
10582#[gpui::test]
10583async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10584 init_test(cx, |settings| {
10585 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
10586 });
10587
10588 let mut cx = EditorLspTestContext::new_rust(
10589 lsp::ServerCapabilities {
10590 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10591 ..Default::default()
10592 },
10593 cx,
10594 )
10595 .await;
10596
10597 // Set up a buffer white some trailing whitespace and no trailing newline.
10598 cx.set_state(
10599 &[
10600 "one ", //
10601 "twoˇ", //
10602 "three ", //
10603 "four", //
10604 ]
10605 .join("\n"),
10606 );
10607
10608 // Submit a format request.
10609 let format = cx
10610 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10611 .unwrap();
10612
10613 // Record which buffer changes have been sent to the language server
10614 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10615 cx.lsp
10616 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10617 let buffer_changes = buffer_changes.clone();
10618 move |params, _| {
10619 buffer_changes.lock().extend(
10620 params
10621 .content_changes
10622 .into_iter()
10623 .map(|e| (e.range.unwrap(), e.text)),
10624 );
10625 }
10626 });
10627
10628 // Handle formatting requests to the language server.
10629 cx.lsp
10630 .set_request_handler::<lsp::request::Formatting, _, _>({
10631 let buffer_changes = buffer_changes.clone();
10632 move |_, _| {
10633 // When formatting is requested, trailing whitespace has already been stripped,
10634 // and the trailing newline has already been added.
10635 assert_eq!(
10636 &buffer_changes.lock()[1..],
10637 &[
10638 (
10639 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10640 "".into()
10641 ),
10642 (
10643 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10644 "".into()
10645 ),
10646 (
10647 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10648 "\n".into()
10649 ),
10650 ]
10651 );
10652
10653 // Insert blank lines between each line of the buffer.
10654 async move {
10655 Ok(Some(vec![
10656 lsp::TextEdit {
10657 range: lsp::Range::new(
10658 lsp::Position::new(1, 0),
10659 lsp::Position::new(1, 0),
10660 ),
10661 new_text: "\n".into(),
10662 },
10663 lsp::TextEdit {
10664 range: lsp::Range::new(
10665 lsp::Position::new(2, 0),
10666 lsp::Position::new(2, 0),
10667 ),
10668 new_text: "\n".into(),
10669 },
10670 ]))
10671 }
10672 }
10673 });
10674
10675 // After formatting the buffer, the trailing whitespace is stripped,
10676 // a newline is appended, and the edits provided by the language server
10677 // have been applied.
10678 format.await.unwrap();
10679 cx.assert_editor_state(
10680 &[
10681 "one", //
10682 "", //
10683 "twoˇ", //
10684 "", //
10685 "three", //
10686 "four", //
10687 "", //
10688 ]
10689 .join("\n"),
10690 );
10691
10692 // Undoing the formatting undoes the trailing whitespace removal, the
10693 // trailing newline, and the LSP edits.
10694 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10695 cx.assert_editor_state(
10696 &[
10697 "one ", //
10698 "twoˇ", //
10699 "three ", //
10700 "four", //
10701 ]
10702 .join("\n"),
10703 );
10704}
10705
10706#[gpui::test]
10707async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10708 cx: &mut TestAppContext,
10709) {
10710 init_test(cx, |_| {});
10711
10712 cx.update(|cx| {
10713 cx.update_global::<SettingsStore, _>(|settings, cx| {
10714 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10715 settings.auto_signature_help = Some(true);
10716 });
10717 });
10718 });
10719
10720 let mut cx = EditorLspTestContext::new_rust(
10721 lsp::ServerCapabilities {
10722 signature_help_provider: Some(lsp::SignatureHelpOptions {
10723 ..Default::default()
10724 }),
10725 ..Default::default()
10726 },
10727 cx,
10728 )
10729 .await;
10730
10731 let language = Language::new(
10732 LanguageConfig {
10733 name: "Rust".into(),
10734 brackets: BracketPairConfig {
10735 pairs: vec![
10736 BracketPair {
10737 start: "{".to_string(),
10738 end: "}".to_string(),
10739 close: true,
10740 surround: true,
10741 newline: true,
10742 },
10743 BracketPair {
10744 start: "(".to_string(),
10745 end: ")".to_string(),
10746 close: true,
10747 surround: true,
10748 newline: true,
10749 },
10750 BracketPair {
10751 start: "/*".to_string(),
10752 end: " */".to_string(),
10753 close: true,
10754 surround: true,
10755 newline: true,
10756 },
10757 BracketPair {
10758 start: "[".to_string(),
10759 end: "]".to_string(),
10760 close: false,
10761 surround: false,
10762 newline: true,
10763 },
10764 BracketPair {
10765 start: "\"".to_string(),
10766 end: "\"".to_string(),
10767 close: true,
10768 surround: true,
10769 newline: false,
10770 },
10771 BracketPair {
10772 start: "<".to_string(),
10773 end: ">".to_string(),
10774 close: false,
10775 surround: true,
10776 newline: true,
10777 },
10778 ],
10779 ..Default::default()
10780 },
10781 autoclose_before: "})]".to_string(),
10782 ..Default::default()
10783 },
10784 Some(tree_sitter_rust::LANGUAGE.into()),
10785 );
10786 let language = Arc::new(language);
10787
10788 cx.language_registry().add(language.clone());
10789 cx.update_buffer(|buffer, cx| {
10790 buffer.set_language(Some(language), cx);
10791 });
10792
10793 cx.set_state(
10794 &r#"
10795 fn main() {
10796 sampleˇ
10797 }
10798 "#
10799 .unindent(),
10800 );
10801
10802 cx.update_editor(|editor, window, cx| {
10803 editor.handle_input("(", window, cx);
10804 });
10805 cx.assert_editor_state(
10806 &"
10807 fn main() {
10808 sample(ˇ)
10809 }
10810 "
10811 .unindent(),
10812 );
10813
10814 let mocked_response = lsp::SignatureHelp {
10815 signatures: vec![lsp::SignatureInformation {
10816 label: "fn sample(param1: u8, param2: u8)".to_string(),
10817 documentation: None,
10818 parameters: Some(vec![
10819 lsp::ParameterInformation {
10820 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10821 documentation: None,
10822 },
10823 lsp::ParameterInformation {
10824 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10825 documentation: None,
10826 },
10827 ]),
10828 active_parameter: None,
10829 }],
10830 active_signature: Some(0),
10831 active_parameter: Some(0),
10832 };
10833 handle_signature_help_request(&mut cx, mocked_response).await;
10834
10835 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10836 .await;
10837
10838 cx.editor(|editor, _, _| {
10839 let signature_help_state = editor.signature_help_state.popover().cloned();
10840 assert_eq!(
10841 signature_help_state.unwrap().label,
10842 "param1: u8, param2: u8"
10843 );
10844 });
10845}
10846
10847#[gpui::test]
10848async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10849 init_test(cx, |_| {});
10850
10851 cx.update(|cx| {
10852 cx.update_global::<SettingsStore, _>(|settings, cx| {
10853 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10854 settings.auto_signature_help = Some(false);
10855 settings.show_signature_help_after_edits = Some(false);
10856 });
10857 });
10858 });
10859
10860 let mut cx = EditorLspTestContext::new_rust(
10861 lsp::ServerCapabilities {
10862 signature_help_provider: Some(lsp::SignatureHelpOptions {
10863 ..Default::default()
10864 }),
10865 ..Default::default()
10866 },
10867 cx,
10868 )
10869 .await;
10870
10871 let language = Language::new(
10872 LanguageConfig {
10873 name: "Rust".into(),
10874 brackets: BracketPairConfig {
10875 pairs: vec![
10876 BracketPair {
10877 start: "{".to_string(),
10878 end: "}".to_string(),
10879 close: true,
10880 surround: true,
10881 newline: true,
10882 },
10883 BracketPair {
10884 start: "(".to_string(),
10885 end: ")".to_string(),
10886 close: true,
10887 surround: true,
10888 newline: true,
10889 },
10890 BracketPair {
10891 start: "/*".to_string(),
10892 end: " */".to_string(),
10893 close: true,
10894 surround: true,
10895 newline: true,
10896 },
10897 BracketPair {
10898 start: "[".to_string(),
10899 end: "]".to_string(),
10900 close: false,
10901 surround: false,
10902 newline: true,
10903 },
10904 BracketPair {
10905 start: "\"".to_string(),
10906 end: "\"".to_string(),
10907 close: true,
10908 surround: true,
10909 newline: false,
10910 },
10911 BracketPair {
10912 start: "<".to_string(),
10913 end: ">".to_string(),
10914 close: false,
10915 surround: true,
10916 newline: true,
10917 },
10918 ],
10919 ..Default::default()
10920 },
10921 autoclose_before: "})]".to_string(),
10922 ..Default::default()
10923 },
10924 Some(tree_sitter_rust::LANGUAGE.into()),
10925 );
10926 let language = Arc::new(language);
10927
10928 cx.language_registry().add(language.clone());
10929 cx.update_buffer(|buffer, cx| {
10930 buffer.set_language(Some(language), cx);
10931 });
10932
10933 // Ensure that signature_help is not called when no signature help is enabled.
10934 cx.set_state(
10935 &r#"
10936 fn main() {
10937 sampleˇ
10938 }
10939 "#
10940 .unindent(),
10941 );
10942 cx.update_editor(|editor, window, cx| {
10943 editor.handle_input("(", window, cx);
10944 });
10945 cx.assert_editor_state(
10946 &"
10947 fn main() {
10948 sample(ˇ)
10949 }
10950 "
10951 .unindent(),
10952 );
10953 cx.editor(|editor, _, _| {
10954 assert!(editor.signature_help_state.task().is_none());
10955 });
10956
10957 let mocked_response = lsp::SignatureHelp {
10958 signatures: vec![lsp::SignatureInformation {
10959 label: "fn sample(param1: u8, param2: u8)".to_string(),
10960 documentation: None,
10961 parameters: Some(vec![
10962 lsp::ParameterInformation {
10963 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10964 documentation: None,
10965 },
10966 lsp::ParameterInformation {
10967 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10968 documentation: None,
10969 },
10970 ]),
10971 active_parameter: None,
10972 }],
10973 active_signature: Some(0),
10974 active_parameter: Some(0),
10975 };
10976
10977 // Ensure that signature_help is called when enabled afte edits
10978 cx.update(|_, cx| {
10979 cx.update_global::<SettingsStore, _>(|settings, cx| {
10980 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10981 settings.auto_signature_help = Some(false);
10982 settings.show_signature_help_after_edits = Some(true);
10983 });
10984 });
10985 });
10986 cx.set_state(
10987 &r#"
10988 fn main() {
10989 sampleˇ
10990 }
10991 "#
10992 .unindent(),
10993 );
10994 cx.update_editor(|editor, window, cx| {
10995 editor.handle_input("(", window, cx);
10996 });
10997 cx.assert_editor_state(
10998 &"
10999 fn main() {
11000 sample(ˇ)
11001 }
11002 "
11003 .unindent(),
11004 );
11005 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11006 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11007 .await;
11008 cx.update_editor(|editor, _, _| {
11009 let signature_help_state = editor.signature_help_state.popover().cloned();
11010 assert!(signature_help_state.is_some());
11011 assert_eq!(
11012 signature_help_state.unwrap().label,
11013 "param1: u8, param2: u8"
11014 );
11015 editor.signature_help_state = SignatureHelpState::default();
11016 });
11017
11018 // Ensure that signature_help is called when auto signature help override is enabled
11019 cx.update(|_, cx| {
11020 cx.update_global::<SettingsStore, _>(|settings, cx| {
11021 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11022 settings.auto_signature_help = Some(true);
11023 settings.show_signature_help_after_edits = Some(false);
11024 });
11025 });
11026 });
11027 cx.set_state(
11028 &r#"
11029 fn main() {
11030 sampleˇ
11031 }
11032 "#
11033 .unindent(),
11034 );
11035 cx.update_editor(|editor, window, cx| {
11036 editor.handle_input("(", window, cx);
11037 });
11038 cx.assert_editor_state(
11039 &"
11040 fn main() {
11041 sample(ˇ)
11042 }
11043 "
11044 .unindent(),
11045 );
11046 handle_signature_help_request(&mut cx, mocked_response).await;
11047 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11048 .await;
11049 cx.editor(|editor, _, _| {
11050 let signature_help_state = editor.signature_help_state.popover().cloned();
11051 assert!(signature_help_state.is_some());
11052 assert_eq!(
11053 signature_help_state.unwrap().label,
11054 "param1: u8, param2: u8"
11055 );
11056 });
11057}
11058
11059#[gpui::test]
11060async fn test_signature_help(cx: &mut TestAppContext) {
11061 init_test(cx, |_| {});
11062 cx.update(|cx| {
11063 cx.update_global::<SettingsStore, _>(|settings, cx| {
11064 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11065 settings.auto_signature_help = Some(true);
11066 });
11067 });
11068 });
11069
11070 let mut cx = EditorLspTestContext::new_rust(
11071 lsp::ServerCapabilities {
11072 signature_help_provider: Some(lsp::SignatureHelpOptions {
11073 ..Default::default()
11074 }),
11075 ..Default::default()
11076 },
11077 cx,
11078 )
11079 .await;
11080
11081 // A test that directly calls `show_signature_help`
11082 cx.update_editor(|editor, window, cx| {
11083 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11084 });
11085
11086 let mocked_response = lsp::SignatureHelp {
11087 signatures: vec![lsp::SignatureInformation {
11088 label: "fn sample(param1: u8, param2: u8)".to_string(),
11089 documentation: None,
11090 parameters: Some(vec![
11091 lsp::ParameterInformation {
11092 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11093 documentation: None,
11094 },
11095 lsp::ParameterInformation {
11096 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11097 documentation: None,
11098 },
11099 ]),
11100 active_parameter: None,
11101 }],
11102 active_signature: Some(0),
11103 active_parameter: Some(0),
11104 };
11105 handle_signature_help_request(&mut cx, mocked_response).await;
11106
11107 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11108 .await;
11109
11110 cx.editor(|editor, _, _| {
11111 let signature_help_state = editor.signature_help_state.popover().cloned();
11112 assert!(signature_help_state.is_some());
11113 assert_eq!(
11114 signature_help_state.unwrap().label,
11115 "param1: u8, param2: u8"
11116 );
11117 });
11118
11119 // When exiting outside from inside the brackets, `signature_help` is closed.
11120 cx.set_state(indoc! {"
11121 fn main() {
11122 sample(ˇ);
11123 }
11124
11125 fn sample(param1: u8, param2: u8) {}
11126 "});
11127
11128 cx.update_editor(|editor, window, cx| {
11129 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11130 s.select_ranges([0..0])
11131 });
11132 });
11133
11134 let mocked_response = lsp::SignatureHelp {
11135 signatures: Vec::new(),
11136 active_signature: None,
11137 active_parameter: None,
11138 };
11139 handle_signature_help_request(&mut cx, mocked_response).await;
11140
11141 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11142 .await;
11143
11144 cx.editor(|editor, _, _| {
11145 assert!(!editor.signature_help_state.is_shown());
11146 });
11147
11148 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11149 cx.set_state(indoc! {"
11150 fn main() {
11151 sample(ˇ);
11152 }
11153
11154 fn sample(param1: u8, param2: u8) {}
11155 "});
11156
11157 let mocked_response = lsp::SignatureHelp {
11158 signatures: vec![lsp::SignatureInformation {
11159 label: "fn sample(param1: u8, param2: u8)".to_string(),
11160 documentation: None,
11161 parameters: Some(vec![
11162 lsp::ParameterInformation {
11163 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11164 documentation: None,
11165 },
11166 lsp::ParameterInformation {
11167 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11168 documentation: None,
11169 },
11170 ]),
11171 active_parameter: None,
11172 }],
11173 active_signature: Some(0),
11174 active_parameter: Some(0),
11175 };
11176 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11177 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11178 .await;
11179 cx.editor(|editor, _, _| {
11180 assert!(editor.signature_help_state.is_shown());
11181 });
11182
11183 // Restore the popover with more parameter input
11184 cx.set_state(indoc! {"
11185 fn main() {
11186 sample(param1, param2ˇ);
11187 }
11188
11189 fn sample(param1: u8, param2: u8) {}
11190 "});
11191
11192 let mocked_response = lsp::SignatureHelp {
11193 signatures: vec![lsp::SignatureInformation {
11194 label: "fn sample(param1: u8, param2: u8)".to_string(),
11195 documentation: None,
11196 parameters: Some(vec![
11197 lsp::ParameterInformation {
11198 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11199 documentation: None,
11200 },
11201 lsp::ParameterInformation {
11202 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11203 documentation: None,
11204 },
11205 ]),
11206 active_parameter: None,
11207 }],
11208 active_signature: Some(0),
11209 active_parameter: Some(1),
11210 };
11211 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11212 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11213 .await;
11214
11215 // When selecting a range, the popover is gone.
11216 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11217 cx.update_editor(|editor, window, cx| {
11218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11219 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11220 })
11221 });
11222 cx.assert_editor_state(indoc! {"
11223 fn main() {
11224 sample(param1, «ˇparam2»);
11225 }
11226
11227 fn sample(param1: u8, param2: u8) {}
11228 "});
11229 cx.editor(|editor, _, _| {
11230 assert!(!editor.signature_help_state.is_shown());
11231 });
11232
11233 // When unselecting again, the popover is back if within the brackets.
11234 cx.update_editor(|editor, window, cx| {
11235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11236 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11237 })
11238 });
11239 cx.assert_editor_state(indoc! {"
11240 fn main() {
11241 sample(param1, ˇparam2);
11242 }
11243
11244 fn sample(param1: u8, param2: u8) {}
11245 "});
11246 handle_signature_help_request(&mut cx, mocked_response).await;
11247 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11248 .await;
11249 cx.editor(|editor, _, _| {
11250 assert!(editor.signature_help_state.is_shown());
11251 });
11252
11253 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11254 cx.update_editor(|editor, window, cx| {
11255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11256 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11257 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11258 })
11259 });
11260 cx.assert_editor_state(indoc! {"
11261 fn main() {
11262 sample(param1, ˇparam2);
11263 }
11264
11265 fn sample(param1: u8, param2: u8) {}
11266 "});
11267
11268 let mocked_response = lsp::SignatureHelp {
11269 signatures: vec![lsp::SignatureInformation {
11270 label: "fn sample(param1: u8, param2: u8)".to_string(),
11271 documentation: None,
11272 parameters: Some(vec![
11273 lsp::ParameterInformation {
11274 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11275 documentation: None,
11276 },
11277 lsp::ParameterInformation {
11278 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11279 documentation: None,
11280 },
11281 ]),
11282 active_parameter: None,
11283 }],
11284 active_signature: Some(0),
11285 active_parameter: Some(1),
11286 };
11287 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11288 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11289 .await;
11290 cx.update_editor(|editor, _, cx| {
11291 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11292 });
11293 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11294 .await;
11295 cx.update_editor(|editor, window, cx| {
11296 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11297 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11298 })
11299 });
11300 cx.assert_editor_state(indoc! {"
11301 fn main() {
11302 sample(param1, «ˇparam2»);
11303 }
11304
11305 fn sample(param1: u8, param2: u8) {}
11306 "});
11307 cx.update_editor(|editor, window, cx| {
11308 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11309 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11310 })
11311 });
11312 cx.assert_editor_state(indoc! {"
11313 fn main() {
11314 sample(param1, ˇparam2);
11315 }
11316
11317 fn sample(param1: u8, param2: u8) {}
11318 "});
11319 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11320 .await;
11321}
11322
11323#[gpui::test]
11324async fn test_completion_mode(cx: &mut TestAppContext) {
11325 init_test(cx, |_| {});
11326 let mut cx = EditorLspTestContext::new_rust(
11327 lsp::ServerCapabilities {
11328 completion_provider: Some(lsp::CompletionOptions {
11329 resolve_provider: Some(true),
11330 ..Default::default()
11331 }),
11332 ..Default::default()
11333 },
11334 cx,
11335 )
11336 .await;
11337
11338 struct Run {
11339 run_description: &'static str,
11340 initial_state: String,
11341 buffer_marked_text: String,
11342 completion_label: &'static str,
11343 completion_text: &'static str,
11344 expected_with_insert_mode: String,
11345 expected_with_replace_mode: String,
11346 expected_with_replace_subsequence_mode: String,
11347 expected_with_replace_suffix_mode: String,
11348 }
11349
11350 let runs = [
11351 Run {
11352 run_description: "Start of word matches completion text",
11353 initial_state: "before ediˇ after".into(),
11354 buffer_marked_text: "before <edi|> after".into(),
11355 completion_label: "editor",
11356 completion_text: "editor",
11357 expected_with_insert_mode: "before editorˇ after".into(),
11358 expected_with_replace_mode: "before editorˇ after".into(),
11359 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11360 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11361 },
11362 Run {
11363 run_description: "Accept same text at the middle of the word",
11364 initial_state: "before ediˇtor after".into(),
11365 buffer_marked_text: "before <edi|tor> after".into(),
11366 completion_label: "editor",
11367 completion_text: "editor",
11368 expected_with_insert_mode: "before editorˇtor after".into(),
11369 expected_with_replace_mode: "before editorˇ after".into(),
11370 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11371 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11372 },
11373 Run {
11374 run_description: "End of word matches completion text -- cursor at end",
11375 initial_state: "before torˇ after".into(),
11376 buffer_marked_text: "before <tor|> after".into(),
11377 completion_label: "editor",
11378 completion_text: "editor",
11379 expected_with_insert_mode: "before editorˇ after".into(),
11380 expected_with_replace_mode: "before editorˇ after".into(),
11381 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11382 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11383 },
11384 Run {
11385 run_description: "End of word matches completion text -- cursor at start",
11386 initial_state: "before ˇtor after".into(),
11387 buffer_marked_text: "before <|tor> after".into(),
11388 completion_label: "editor",
11389 completion_text: "editor",
11390 expected_with_insert_mode: "before editorˇtor after".into(),
11391 expected_with_replace_mode: "before editorˇ after".into(),
11392 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11393 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11394 },
11395 Run {
11396 run_description: "Prepend text containing whitespace",
11397 initial_state: "pˇfield: bool".into(),
11398 buffer_marked_text: "<p|field>: bool".into(),
11399 completion_label: "pub ",
11400 completion_text: "pub ",
11401 expected_with_insert_mode: "pub ˇfield: bool".into(),
11402 expected_with_replace_mode: "pub ˇ: bool".into(),
11403 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11404 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11405 },
11406 Run {
11407 run_description: "Add element to start of list",
11408 initial_state: "[element_ˇelement_2]".into(),
11409 buffer_marked_text: "[<element_|element_2>]".into(),
11410 completion_label: "element_1",
11411 completion_text: "element_1",
11412 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11413 expected_with_replace_mode: "[element_1ˇ]".into(),
11414 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11415 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11416 },
11417 Run {
11418 run_description: "Add element to start of list -- first and second elements are equal",
11419 initial_state: "[elˇelement]".into(),
11420 buffer_marked_text: "[<el|element>]".into(),
11421 completion_label: "element",
11422 completion_text: "element",
11423 expected_with_insert_mode: "[elementˇelement]".into(),
11424 expected_with_replace_mode: "[elementˇ]".into(),
11425 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11426 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11427 },
11428 Run {
11429 run_description: "Ends with matching suffix",
11430 initial_state: "SubˇError".into(),
11431 buffer_marked_text: "<Sub|Error>".into(),
11432 completion_label: "SubscriptionError",
11433 completion_text: "SubscriptionError",
11434 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11435 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11436 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11437 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11438 },
11439 Run {
11440 run_description: "Suffix is a subsequence -- contiguous",
11441 initial_state: "SubˇErr".into(),
11442 buffer_marked_text: "<Sub|Err>".into(),
11443 completion_label: "SubscriptionError",
11444 completion_text: "SubscriptionError",
11445 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11446 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11447 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11448 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11449 },
11450 Run {
11451 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11452 initial_state: "Suˇscrirr".into(),
11453 buffer_marked_text: "<Su|scrirr>".into(),
11454 completion_label: "SubscriptionError",
11455 completion_text: "SubscriptionError",
11456 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11457 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11458 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11459 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11460 },
11461 Run {
11462 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11463 initial_state: "foo(indˇix)".into(),
11464 buffer_marked_text: "foo(<ind|ix>)".into(),
11465 completion_label: "node_index",
11466 completion_text: "node_index",
11467 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11468 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11469 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11470 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11471 },
11472 Run {
11473 run_description: "Replace range ends before cursor - should extend to cursor",
11474 initial_state: "before editˇo after".into(),
11475 buffer_marked_text: "before <{ed}>it|o after".into(),
11476 completion_label: "editor",
11477 completion_text: "editor",
11478 expected_with_insert_mode: "before editorˇo after".into(),
11479 expected_with_replace_mode: "before editorˇo after".into(),
11480 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11481 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11482 },
11483 Run {
11484 run_description: "Uses label for suffix matching",
11485 initial_state: "before ediˇtor after".into(),
11486 buffer_marked_text: "before <edi|tor> after".into(),
11487 completion_label: "editor",
11488 completion_text: "editor()",
11489 expected_with_insert_mode: "before editor()ˇtor after".into(),
11490 expected_with_replace_mode: "before editor()ˇ after".into(),
11491 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11492 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11493 },
11494 Run {
11495 run_description: "Case insensitive subsequence and suffix matching",
11496 initial_state: "before EDiˇtoR after".into(),
11497 buffer_marked_text: "before <EDi|toR> after".into(),
11498 completion_label: "editor",
11499 completion_text: "editor",
11500 expected_with_insert_mode: "before editorˇtoR after".into(),
11501 expected_with_replace_mode: "before editorˇ after".into(),
11502 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11503 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11504 },
11505 ];
11506
11507 for run in runs {
11508 let run_variations = [
11509 (LspInsertMode::Insert, run.expected_with_insert_mode),
11510 (LspInsertMode::Replace, run.expected_with_replace_mode),
11511 (
11512 LspInsertMode::ReplaceSubsequence,
11513 run.expected_with_replace_subsequence_mode,
11514 ),
11515 (
11516 LspInsertMode::ReplaceSuffix,
11517 run.expected_with_replace_suffix_mode,
11518 ),
11519 ];
11520
11521 for (lsp_insert_mode, expected_text) in run_variations {
11522 eprintln!(
11523 "run = {:?}, mode = {lsp_insert_mode:.?}",
11524 run.run_description,
11525 );
11526
11527 update_test_language_settings(&mut cx, |settings| {
11528 settings.defaults.completions = Some(CompletionSettings {
11529 lsp_insert_mode,
11530 words: WordsCompletionMode::Disabled,
11531 lsp: true,
11532 lsp_fetch_timeout_ms: 0,
11533 });
11534 });
11535
11536 cx.set_state(&run.initial_state);
11537 cx.update_editor(|editor, window, cx| {
11538 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11539 });
11540
11541 let counter = Arc::new(AtomicUsize::new(0));
11542 handle_completion_request_with_insert_and_replace(
11543 &mut cx,
11544 &run.buffer_marked_text,
11545 vec![(run.completion_label, run.completion_text)],
11546 counter.clone(),
11547 )
11548 .await;
11549 cx.condition(|editor, _| editor.context_menu_visible())
11550 .await;
11551 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11552
11553 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11554 editor
11555 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11556 .unwrap()
11557 });
11558 cx.assert_editor_state(&expected_text);
11559 handle_resolve_completion_request(&mut cx, None).await;
11560 apply_additional_edits.await.unwrap();
11561 }
11562 }
11563}
11564
11565#[gpui::test]
11566async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11567 init_test(cx, |_| {});
11568 let mut cx = EditorLspTestContext::new_rust(
11569 lsp::ServerCapabilities {
11570 completion_provider: Some(lsp::CompletionOptions {
11571 resolve_provider: Some(true),
11572 ..Default::default()
11573 }),
11574 ..Default::default()
11575 },
11576 cx,
11577 )
11578 .await;
11579
11580 let initial_state = "SubˇError";
11581 let buffer_marked_text = "<Sub|Error>";
11582 let completion_text = "SubscriptionError";
11583 let expected_with_insert_mode = "SubscriptionErrorˇError";
11584 let expected_with_replace_mode = "SubscriptionErrorˇ";
11585
11586 update_test_language_settings(&mut cx, |settings| {
11587 settings.defaults.completions = Some(CompletionSettings {
11588 words: WordsCompletionMode::Disabled,
11589 // set the opposite here to ensure that the action is overriding the default behavior
11590 lsp_insert_mode: LspInsertMode::Insert,
11591 lsp: true,
11592 lsp_fetch_timeout_ms: 0,
11593 });
11594 });
11595
11596 cx.set_state(initial_state);
11597 cx.update_editor(|editor, window, cx| {
11598 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11599 });
11600
11601 let counter = Arc::new(AtomicUsize::new(0));
11602 handle_completion_request_with_insert_and_replace(
11603 &mut cx,
11604 &buffer_marked_text,
11605 vec![(completion_text, completion_text)],
11606 counter.clone(),
11607 )
11608 .await;
11609 cx.condition(|editor, _| editor.context_menu_visible())
11610 .await;
11611 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11612
11613 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11614 editor
11615 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11616 .unwrap()
11617 });
11618 cx.assert_editor_state(&expected_with_replace_mode);
11619 handle_resolve_completion_request(&mut cx, None).await;
11620 apply_additional_edits.await.unwrap();
11621
11622 update_test_language_settings(&mut cx, |settings| {
11623 settings.defaults.completions = Some(CompletionSettings {
11624 words: WordsCompletionMode::Disabled,
11625 // set the opposite here to ensure that the action is overriding the default behavior
11626 lsp_insert_mode: LspInsertMode::Replace,
11627 lsp: true,
11628 lsp_fetch_timeout_ms: 0,
11629 });
11630 });
11631
11632 cx.set_state(initial_state);
11633 cx.update_editor(|editor, window, cx| {
11634 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11635 });
11636 handle_completion_request_with_insert_and_replace(
11637 &mut cx,
11638 &buffer_marked_text,
11639 vec![(completion_text, completion_text)],
11640 counter.clone(),
11641 )
11642 .await;
11643 cx.condition(|editor, _| editor.context_menu_visible())
11644 .await;
11645 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11646
11647 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11648 editor
11649 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11650 .unwrap()
11651 });
11652 cx.assert_editor_state(&expected_with_insert_mode);
11653 handle_resolve_completion_request(&mut cx, None).await;
11654 apply_additional_edits.await.unwrap();
11655}
11656
11657#[gpui::test]
11658async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11659 init_test(cx, |_| {});
11660 let mut cx = EditorLspTestContext::new_rust(
11661 lsp::ServerCapabilities {
11662 completion_provider: Some(lsp::CompletionOptions {
11663 resolve_provider: Some(true),
11664 ..Default::default()
11665 }),
11666 ..Default::default()
11667 },
11668 cx,
11669 )
11670 .await;
11671
11672 // scenario: surrounding text matches completion text
11673 let completion_text = "to_offset";
11674 let initial_state = indoc! {"
11675 1. buf.to_offˇsuffix
11676 2. buf.to_offˇsuf
11677 3. buf.to_offˇfix
11678 4. buf.to_offˇ
11679 5. into_offˇensive
11680 6. ˇsuffix
11681 7. let ˇ //
11682 8. aaˇzz
11683 9. buf.to_off«zzzzzˇ»suffix
11684 10. buf.«ˇzzzzz»suffix
11685 11. to_off«ˇzzzzz»
11686
11687 buf.to_offˇsuffix // newest cursor
11688 "};
11689 let completion_marked_buffer = indoc! {"
11690 1. buf.to_offsuffix
11691 2. buf.to_offsuf
11692 3. buf.to_offfix
11693 4. buf.to_off
11694 5. into_offensive
11695 6. suffix
11696 7. let //
11697 8. aazz
11698 9. buf.to_offzzzzzsuffix
11699 10. buf.zzzzzsuffix
11700 11. to_offzzzzz
11701
11702 buf.<to_off|suffix> // newest cursor
11703 "};
11704 let expected = indoc! {"
11705 1. buf.to_offsetˇ
11706 2. buf.to_offsetˇsuf
11707 3. buf.to_offsetˇfix
11708 4. buf.to_offsetˇ
11709 5. into_offsetˇensive
11710 6. to_offsetˇsuffix
11711 7. let to_offsetˇ //
11712 8. aato_offsetˇzz
11713 9. buf.to_offsetˇ
11714 10. buf.to_offsetˇsuffix
11715 11. to_offsetˇ
11716
11717 buf.to_offsetˇ // newest cursor
11718 "};
11719 cx.set_state(initial_state);
11720 cx.update_editor(|editor, window, cx| {
11721 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11722 });
11723 handle_completion_request_with_insert_and_replace(
11724 &mut cx,
11725 completion_marked_buffer,
11726 vec![(completion_text, completion_text)],
11727 Arc::new(AtomicUsize::new(0)),
11728 )
11729 .await;
11730 cx.condition(|editor, _| editor.context_menu_visible())
11731 .await;
11732 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11733 editor
11734 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11735 .unwrap()
11736 });
11737 cx.assert_editor_state(expected);
11738 handle_resolve_completion_request(&mut cx, None).await;
11739 apply_additional_edits.await.unwrap();
11740
11741 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11742 let completion_text = "foo_and_bar";
11743 let initial_state = indoc! {"
11744 1. ooanbˇ
11745 2. zooanbˇ
11746 3. ooanbˇz
11747 4. zooanbˇz
11748 5. ooanˇ
11749 6. oanbˇ
11750
11751 ooanbˇ
11752 "};
11753 let completion_marked_buffer = indoc! {"
11754 1. ooanb
11755 2. zooanb
11756 3. ooanbz
11757 4. zooanbz
11758 5. ooan
11759 6. oanb
11760
11761 <ooanb|>
11762 "};
11763 let expected = indoc! {"
11764 1. foo_and_barˇ
11765 2. zfoo_and_barˇ
11766 3. foo_and_barˇz
11767 4. zfoo_and_barˇz
11768 5. ooanfoo_and_barˇ
11769 6. oanbfoo_and_barˇ
11770
11771 foo_and_barˇ
11772 "};
11773 cx.set_state(initial_state);
11774 cx.update_editor(|editor, window, cx| {
11775 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11776 });
11777 handle_completion_request_with_insert_and_replace(
11778 &mut cx,
11779 completion_marked_buffer,
11780 vec![(completion_text, completion_text)],
11781 Arc::new(AtomicUsize::new(0)),
11782 )
11783 .await;
11784 cx.condition(|editor, _| editor.context_menu_visible())
11785 .await;
11786 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11787 editor
11788 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11789 .unwrap()
11790 });
11791 cx.assert_editor_state(expected);
11792 handle_resolve_completion_request(&mut cx, None).await;
11793 apply_additional_edits.await.unwrap();
11794
11795 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11796 // (expects the same as if it was inserted at the end)
11797 let completion_text = "foo_and_bar";
11798 let initial_state = indoc! {"
11799 1. ooˇanb
11800 2. zooˇanb
11801 3. ooˇanbz
11802 4. zooˇanbz
11803
11804 ooˇanb
11805 "};
11806 let completion_marked_buffer = indoc! {"
11807 1. ooanb
11808 2. zooanb
11809 3. ooanbz
11810 4. zooanbz
11811
11812 <oo|anb>
11813 "};
11814 let expected = indoc! {"
11815 1. foo_and_barˇ
11816 2. zfoo_and_barˇ
11817 3. foo_and_barˇz
11818 4. zfoo_and_barˇz
11819
11820 foo_and_barˇ
11821 "};
11822 cx.set_state(initial_state);
11823 cx.update_editor(|editor, window, cx| {
11824 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11825 });
11826 handle_completion_request_with_insert_and_replace(
11827 &mut cx,
11828 completion_marked_buffer,
11829 vec![(completion_text, completion_text)],
11830 Arc::new(AtomicUsize::new(0)),
11831 )
11832 .await;
11833 cx.condition(|editor, _| editor.context_menu_visible())
11834 .await;
11835 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11836 editor
11837 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11838 .unwrap()
11839 });
11840 cx.assert_editor_state(expected);
11841 handle_resolve_completion_request(&mut cx, None).await;
11842 apply_additional_edits.await.unwrap();
11843}
11844
11845// This used to crash
11846#[gpui::test]
11847async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
11848 init_test(cx, |_| {});
11849
11850 let buffer_text = indoc! {"
11851 fn main() {
11852 10.satu;
11853
11854 //
11855 // separate cursors so they open in different excerpts (manually reproducible)
11856 //
11857
11858 10.satu20;
11859 }
11860 "};
11861 let multibuffer_text_with_selections = indoc! {"
11862 fn main() {
11863 10.satuˇ;
11864
11865 //
11866
11867 //
11868
11869 10.satuˇ20;
11870 }
11871 "};
11872 let expected_multibuffer = indoc! {"
11873 fn main() {
11874 10.saturating_sub()ˇ;
11875
11876 //
11877
11878 //
11879
11880 10.saturating_sub()ˇ;
11881 }
11882 "};
11883
11884 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11885 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11886
11887 let fs = FakeFs::new(cx.executor());
11888 fs.insert_tree(
11889 path!("/a"),
11890 json!({
11891 "main.rs": buffer_text,
11892 }),
11893 )
11894 .await;
11895
11896 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11897 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11898 language_registry.add(rust_lang());
11899 let mut fake_servers = language_registry.register_fake_lsp(
11900 "Rust",
11901 FakeLspAdapter {
11902 capabilities: lsp::ServerCapabilities {
11903 completion_provider: Some(lsp::CompletionOptions {
11904 resolve_provider: None,
11905 ..lsp::CompletionOptions::default()
11906 }),
11907 ..lsp::ServerCapabilities::default()
11908 },
11909 ..FakeLspAdapter::default()
11910 },
11911 );
11912 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11913 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11914 let buffer = project
11915 .update(cx, |project, cx| {
11916 project.open_local_buffer(path!("/a/main.rs"), cx)
11917 })
11918 .await
11919 .unwrap();
11920
11921 let multi_buffer = cx.new(|cx| {
11922 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11923 multi_buffer.push_excerpts(
11924 buffer.clone(),
11925 [ExcerptRange::new(0..first_excerpt_end)],
11926 cx,
11927 );
11928 multi_buffer.push_excerpts(
11929 buffer.clone(),
11930 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11931 cx,
11932 );
11933 multi_buffer
11934 });
11935
11936 let editor = workspace
11937 .update(cx, |_, window, cx| {
11938 cx.new(|cx| {
11939 Editor::new(
11940 EditorMode::Full {
11941 scale_ui_elements_with_buffer_font_size: false,
11942 show_active_line_background: false,
11943 sized_by_content: false,
11944 },
11945 multi_buffer.clone(),
11946 Some(project.clone()),
11947 window,
11948 cx,
11949 )
11950 })
11951 })
11952 .unwrap();
11953
11954 let pane = workspace
11955 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11956 .unwrap();
11957 pane.update_in(cx, |pane, window, cx| {
11958 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11959 });
11960
11961 let fake_server = fake_servers.next().await.unwrap();
11962
11963 editor.update_in(cx, |editor, window, cx| {
11964 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11965 s.select_ranges([
11966 Point::new(1, 11)..Point::new(1, 11),
11967 Point::new(7, 11)..Point::new(7, 11),
11968 ])
11969 });
11970
11971 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11972 });
11973
11974 editor.update_in(cx, |editor, window, cx| {
11975 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11976 });
11977
11978 fake_server
11979 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11980 let completion_item = lsp::CompletionItem {
11981 label: "saturating_sub()".into(),
11982 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11983 lsp::InsertReplaceEdit {
11984 new_text: "saturating_sub()".to_owned(),
11985 insert: lsp::Range::new(
11986 lsp::Position::new(7, 7),
11987 lsp::Position::new(7, 11),
11988 ),
11989 replace: lsp::Range::new(
11990 lsp::Position::new(7, 7),
11991 lsp::Position::new(7, 13),
11992 ),
11993 },
11994 )),
11995 ..lsp::CompletionItem::default()
11996 };
11997
11998 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11999 })
12000 .next()
12001 .await
12002 .unwrap();
12003
12004 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12005 .await;
12006
12007 editor
12008 .update_in(cx, |editor, window, cx| {
12009 editor
12010 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12011 .unwrap()
12012 })
12013 .await
12014 .unwrap();
12015
12016 editor.update(cx, |editor, cx| {
12017 assert_text_with_selections(editor, expected_multibuffer, cx);
12018 })
12019}
12020
12021#[gpui::test]
12022async fn test_completion(cx: &mut TestAppContext) {
12023 init_test(cx, |_| {});
12024
12025 let mut cx = EditorLspTestContext::new_rust(
12026 lsp::ServerCapabilities {
12027 completion_provider: Some(lsp::CompletionOptions {
12028 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12029 resolve_provider: Some(true),
12030 ..Default::default()
12031 }),
12032 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12033 ..Default::default()
12034 },
12035 cx,
12036 )
12037 .await;
12038 let counter = Arc::new(AtomicUsize::new(0));
12039
12040 cx.set_state(indoc! {"
12041 oneˇ
12042 two
12043 three
12044 "});
12045 cx.simulate_keystroke(".");
12046 handle_completion_request(
12047 indoc! {"
12048 one.|<>
12049 two
12050 three
12051 "},
12052 vec!["first_completion", "second_completion"],
12053 true,
12054 counter.clone(),
12055 &mut cx,
12056 )
12057 .await;
12058 cx.condition(|editor, _| editor.context_menu_visible())
12059 .await;
12060 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12061
12062 let _handler = handle_signature_help_request(
12063 &mut cx,
12064 lsp::SignatureHelp {
12065 signatures: vec![lsp::SignatureInformation {
12066 label: "test signature".to_string(),
12067 documentation: None,
12068 parameters: Some(vec![lsp::ParameterInformation {
12069 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12070 documentation: None,
12071 }]),
12072 active_parameter: None,
12073 }],
12074 active_signature: None,
12075 active_parameter: None,
12076 },
12077 );
12078 cx.update_editor(|editor, window, cx| {
12079 assert!(
12080 !editor.signature_help_state.is_shown(),
12081 "No signature help was called for"
12082 );
12083 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12084 });
12085 cx.run_until_parked();
12086 cx.update_editor(|editor, _, _| {
12087 assert!(
12088 !editor.signature_help_state.is_shown(),
12089 "No signature help should be shown when completions menu is open"
12090 );
12091 });
12092
12093 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12094 editor.context_menu_next(&Default::default(), window, cx);
12095 editor
12096 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12097 .unwrap()
12098 });
12099 cx.assert_editor_state(indoc! {"
12100 one.second_completionˇ
12101 two
12102 three
12103 "});
12104
12105 handle_resolve_completion_request(
12106 &mut cx,
12107 Some(vec![
12108 (
12109 //This overlaps with the primary completion edit which is
12110 //misbehavior from the LSP spec, test that we filter it out
12111 indoc! {"
12112 one.second_ˇcompletion
12113 two
12114 threeˇ
12115 "},
12116 "overlapping additional edit",
12117 ),
12118 (
12119 indoc! {"
12120 one.second_completion
12121 two
12122 threeˇ
12123 "},
12124 "\nadditional edit",
12125 ),
12126 ]),
12127 )
12128 .await;
12129 apply_additional_edits.await.unwrap();
12130 cx.assert_editor_state(indoc! {"
12131 one.second_completionˇ
12132 two
12133 three
12134 additional edit
12135 "});
12136
12137 cx.set_state(indoc! {"
12138 one.second_completion
12139 twoˇ
12140 threeˇ
12141 additional edit
12142 "});
12143 cx.simulate_keystroke(" ");
12144 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12145 cx.simulate_keystroke("s");
12146 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12147
12148 cx.assert_editor_state(indoc! {"
12149 one.second_completion
12150 two sˇ
12151 three sˇ
12152 additional edit
12153 "});
12154 handle_completion_request(
12155 indoc! {"
12156 one.second_completion
12157 two s
12158 three <s|>
12159 additional edit
12160 "},
12161 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12162 true,
12163 counter.clone(),
12164 &mut cx,
12165 )
12166 .await;
12167 cx.condition(|editor, _| editor.context_menu_visible())
12168 .await;
12169 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12170
12171 cx.simulate_keystroke("i");
12172
12173 handle_completion_request(
12174 indoc! {"
12175 one.second_completion
12176 two si
12177 three <si|>
12178 additional edit
12179 "},
12180 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12181 true,
12182 counter.clone(),
12183 &mut cx,
12184 )
12185 .await;
12186 cx.condition(|editor, _| editor.context_menu_visible())
12187 .await;
12188 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12189
12190 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12191 editor
12192 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12193 .unwrap()
12194 });
12195 cx.assert_editor_state(indoc! {"
12196 one.second_completion
12197 two sixth_completionˇ
12198 three sixth_completionˇ
12199 additional edit
12200 "});
12201
12202 apply_additional_edits.await.unwrap();
12203
12204 update_test_language_settings(&mut cx, |settings| {
12205 settings.defaults.show_completions_on_input = Some(false);
12206 });
12207 cx.set_state("editorˇ");
12208 cx.simulate_keystroke(".");
12209 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12210 cx.simulate_keystrokes("c l o");
12211 cx.assert_editor_state("editor.cloˇ");
12212 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12213 cx.update_editor(|editor, window, cx| {
12214 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12215 });
12216 handle_completion_request(
12217 "editor.<clo|>",
12218 vec!["close", "clobber"],
12219 true,
12220 counter.clone(),
12221 &mut cx,
12222 )
12223 .await;
12224 cx.condition(|editor, _| editor.context_menu_visible())
12225 .await;
12226 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12227
12228 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12229 editor
12230 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12231 .unwrap()
12232 });
12233 cx.assert_editor_state("editor.clobberˇ");
12234 handle_resolve_completion_request(&mut cx, None).await;
12235 apply_additional_edits.await.unwrap();
12236}
12237
12238#[gpui::test]
12239async fn test_completion_reuse(cx: &mut TestAppContext) {
12240 init_test(cx, |_| {});
12241
12242 let mut cx = EditorLspTestContext::new_rust(
12243 lsp::ServerCapabilities {
12244 completion_provider: Some(lsp::CompletionOptions {
12245 trigger_characters: Some(vec![".".to_string()]),
12246 ..Default::default()
12247 }),
12248 ..Default::default()
12249 },
12250 cx,
12251 )
12252 .await;
12253
12254 let counter = Arc::new(AtomicUsize::new(0));
12255 cx.set_state("objˇ");
12256 cx.simulate_keystroke(".");
12257
12258 // Initial completion request returns complete results
12259 let is_incomplete = false;
12260 handle_completion_request(
12261 "obj.|<>",
12262 vec!["a", "ab", "abc"],
12263 is_incomplete,
12264 counter.clone(),
12265 &mut cx,
12266 )
12267 .await;
12268 cx.run_until_parked();
12269 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12270 cx.assert_editor_state("obj.ˇ");
12271 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12272
12273 // Type "a" - filters existing completions
12274 cx.simulate_keystroke("a");
12275 cx.run_until_parked();
12276 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12277 cx.assert_editor_state("obj.aˇ");
12278 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12279
12280 // Type "b" - filters existing completions
12281 cx.simulate_keystroke("b");
12282 cx.run_until_parked();
12283 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12284 cx.assert_editor_state("obj.abˇ");
12285 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12286
12287 // Type "c" - filters existing completions
12288 cx.simulate_keystroke("c");
12289 cx.run_until_parked();
12290 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12291 cx.assert_editor_state("obj.abcˇ");
12292 check_displayed_completions(vec!["abc"], &mut cx);
12293
12294 // Backspace to delete "c" - filters existing completions
12295 cx.update_editor(|editor, window, cx| {
12296 editor.backspace(&Backspace, window, cx);
12297 });
12298 cx.run_until_parked();
12299 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12300 cx.assert_editor_state("obj.abˇ");
12301 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12302
12303 // Moving cursor to the left dismisses menu.
12304 cx.update_editor(|editor, window, cx| {
12305 editor.move_left(&MoveLeft, window, cx);
12306 });
12307 cx.run_until_parked();
12308 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12309 cx.assert_editor_state("obj.aˇb");
12310 cx.update_editor(|editor, _, _| {
12311 assert_eq!(editor.context_menu_visible(), false);
12312 });
12313
12314 // Type "b" - new request
12315 cx.simulate_keystroke("b");
12316 let is_incomplete = false;
12317 handle_completion_request(
12318 "obj.<ab|>a",
12319 vec!["ab", "abc"],
12320 is_incomplete,
12321 counter.clone(),
12322 &mut cx,
12323 )
12324 .await;
12325 cx.run_until_parked();
12326 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12327 cx.assert_editor_state("obj.abˇb");
12328 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12329
12330 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12331 cx.update_editor(|editor, window, cx| {
12332 editor.backspace(&Backspace, window, cx);
12333 });
12334 let is_incomplete = false;
12335 handle_completion_request(
12336 "obj.<a|>b",
12337 vec!["a", "ab", "abc"],
12338 is_incomplete,
12339 counter.clone(),
12340 &mut cx,
12341 )
12342 .await;
12343 cx.run_until_parked();
12344 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12345 cx.assert_editor_state("obj.aˇb");
12346 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12347
12348 // Backspace to delete "a" - dismisses menu.
12349 cx.update_editor(|editor, window, cx| {
12350 editor.backspace(&Backspace, window, cx);
12351 });
12352 cx.run_until_parked();
12353 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12354 cx.assert_editor_state("obj.ˇb");
12355 cx.update_editor(|editor, _, _| {
12356 assert_eq!(editor.context_menu_visible(), false);
12357 });
12358}
12359
12360#[gpui::test]
12361async fn test_word_completion(cx: &mut TestAppContext) {
12362 let lsp_fetch_timeout_ms = 10;
12363 init_test(cx, |language_settings| {
12364 language_settings.defaults.completions = Some(CompletionSettings {
12365 words: WordsCompletionMode::Fallback,
12366 lsp: true,
12367 lsp_fetch_timeout_ms: 10,
12368 lsp_insert_mode: LspInsertMode::Insert,
12369 });
12370 });
12371
12372 let mut cx = EditorLspTestContext::new_rust(
12373 lsp::ServerCapabilities {
12374 completion_provider: Some(lsp::CompletionOptions {
12375 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12376 ..lsp::CompletionOptions::default()
12377 }),
12378 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12379 ..lsp::ServerCapabilities::default()
12380 },
12381 cx,
12382 )
12383 .await;
12384
12385 let throttle_completions = Arc::new(AtomicBool::new(false));
12386
12387 let lsp_throttle_completions = throttle_completions.clone();
12388 let _completion_requests_handler =
12389 cx.lsp
12390 .server
12391 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12392 let lsp_throttle_completions = lsp_throttle_completions.clone();
12393 let cx = cx.clone();
12394 async move {
12395 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12396 cx.background_executor()
12397 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12398 .await;
12399 }
12400 Ok(Some(lsp::CompletionResponse::Array(vec![
12401 lsp::CompletionItem {
12402 label: "first".into(),
12403 ..lsp::CompletionItem::default()
12404 },
12405 lsp::CompletionItem {
12406 label: "last".into(),
12407 ..lsp::CompletionItem::default()
12408 },
12409 ])))
12410 }
12411 });
12412
12413 cx.set_state(indoc! {"
12414 oneˇ
12415 two
12416 three
12417 "});
12418 cx.simulate_keystroke(".");
12419 cx.executor().run_until_parked();
12420 cx.condition(|editor, _| editor.context_menu_visible())
12421 .await;
12422 cx.update_editor(|editor, window, cx| {
12423 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12424 {
12425 assert_eq!(
12426 completion_menu_entries(&menu),
12427 &["first", "last"],
12428 "When LSP server is fast to reply, no fallback word completions are used"
12429 );
12430 } else {
12431 panic!("expected completion menu to be open");
12432 }
12433 editor.cancel(&Cancel, window, cx);
12434 });
12435 cx.executor().run_until_parked();
12436 cx.condition(|editor, _| !editor.context_menu_visible())
12437 .await;
12438
12439 throttle_completions.store(true, atomic::Ordering::Release);
12440 cx.simulate_keystroke(".");
12441 cx.executor()
12442 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12443 cx.executor().run_until_parked();
12444 cx.condition(|editor, _| editor.context_menu_visible())
12445 .await;
12446 cx.update_editor(|editor, _, _| {
12447 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12448 {
12449 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12450 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12451 } else {
12452 panic!("expected completion menu to be open");
12453 }
12454 });
12455}
12456
12457#[gpui::test]
12458async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12459 init_test(cx, |language_settings| {
12460 language_settings.defaults.completions = Some(CompletionSettings {
12461 words: WordsCompletionMode::Enabled,
12462 lsp: true,
12463 lsp_fetch_timeout_ms: 0,
12464 lsp_insert_mode: LspInsertMode::Insert,
12465 });
12466 });
12467
12468 let mut cx = EditorLspTestContext::new_rust(
12469 lsp::ServerCapabilities {
12470 completion_provider: Some(lsp::CompletionOptions {
12471 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12472 ..lsp::CompletionOptions::default()
12473 }),
12474 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12475 ..lsp::ServerCapabilities::default()
12476 },
12477 cx,
12478 )
12479 .await;
12480
12481 let _completion_requests_handler =
12482 cx.lsp
12483 .server
12484 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12485 Ok(Some(lsp::CompletionResponse::Array(vec![
12486 lsp::CompletionItem {
12487 label: "first".into(),
12488 ..lsp::CompletionItem::default()
12489 },
12490 lsp::CompletionItem {
12491 label: "last".into(),
12492 ..lsp::CompletionItem::default()
12493 },
12494 ])))
12495 });
12496
12497 cx.set_state(indoc! {"ˇ
12498 first
12499 last
12500 second
12501 "});
12502 cx.simulate_keystroke(".");
12503 cx.executor().run_until_parked();
12504 cx.condition(|editor, _| editor.context_menu_visible())
12505 .await;
12506 cx.update_editor(|editor, _, _| {
12507 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12508 {
12509 assert_eq!(
12510 completion_menu_entries(&menu),
12511 &["first", "last", "second"],
12512 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12513 );
12514 } else {
12515 panic!("expected completion menu to be open");
12516 }
12517 });
12518}
12519
12520#[gpui::test]
12521async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12522 init_test(cx, |language_settings| {
12523 language_settings.defaults.completions = Some(CompletionSettings {
12524 words: WordsCompletionMode::Disabled,
12525 lsp: true,
12526 lsp_fetch_timeout_ms: 0,
12527 lsp_insert_mode: LspInsertMode::Insert,
12528 });
12529 });
12530
12531 let mut cx = EditorLspTestContext::new_rust(
12532 lsp::ServerCapabilities {
12533 completion_provider: Some(lsp::CompletionOptions {
12534 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12535 ..lsp::CompletionOptions::default()
12536 }),
12537 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12538 ..lsp::ServerCapabilities::default()
12539 },
12540 cx,
12541 )
12542 .await;
12543
12544 let _completion_requests_handler =
12545 cx.lsp
12546 .server
12547 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12548 panic!("LSP completions should not be queried when dealing with word completions")
12549 });
12550
12551 cx.set_state(indoc! {"ˇ
12552 first
12553 last
12554 second
12555 "});
12556 cx.update_editor(|editor, window, cx| {
12557 editor.show_word_completions(&ShowWordCompletions, window, cx);
12558 });
12559 cx.executor().run_until_parked();
12560 cx.condition(|editor, _| editor.context_menu_visible())
12561 .await;
12562 cx.update_editor(|editor, _, _| {
12563 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12564 {
12565 assert_eq!(
12566 completion_menu_entries(&menu),
12567 &["first", "last", "second"],
12568 "`ShowWordCompletions` action should show word completions"
12569 );
12570 } else {
12571 panic!("expected completion menu to be open");
12572 }
12573 });
12574
12575 cx.simulate_keystroke("l");
12576 cx.executor().run_until_parked();
12577 cx.condition(|editor, _| editor.context_menu_visible())
12578 .await;
12579 cx.update_editor(|editor, _, _| {
12580 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12581 {
12582 assert_eq!(
12583 completion_menu_entries(&menu),
12584 &["last"],
12585 "After showing word completions, further editing should filter them and not query the LSP"
12586 );
12587 } else {
12588 panic!("expected completion menu to be open");
12589 }
12590 });
12591}
12592
12593#[gpui::test]
12594async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12595 init_test(cx, |language_settings| {
12596 language_settings.defaults.completions = Some(CompletionSettings {
12597 words: WordsCompletionMode::Fallback,
12598 lsp: false,
12599 lsp_fetch_timeout_ms: 0,
12600 lsp_insert_mode: LspInsertMode::Insert,
12601 });
12602 });
12603
12604 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12605
12606 cx.set_state(indoc! {"ˇ
12607 0_usize
12608 let
12609 33
12610 4.5f32
12611 "});
12612 cx.update_editor(|editor, window, cx| {
12613 editor.show_completions(&ShowCompletions::default(), window, cx);
12614 });
12615 cx.executor().run_until_parked();
12616 cx.condition(|editor, _| editor.context_menu_visible())
12617 .await;
12618 cx.update_editor(|editor, window, cx| {
12619 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12620 {
12621 assert_eq!(
12622 completion_menu_entries(&menu),
12623 &["let"],
12624 "With no digits in the completion query, no digits should be in the word completions"
12625 );
12626 } else {
12627 panic!("expected completion menu to be open");
12628 }
12629 editor.cancel(&Cancel, window, cx);
12630 });
12631
12632 cx.set_state(indoc! {"3ˇ
12633 0_usize
12634 let
12635 3
12636 33.35f32
12637 "});
12638 cx.update_editor(|editor, window, cx| {
12639 editor.show_completions(&ShowCompletions::default(), window, cx);
12640 });
12641 cx.executor().run_until_parked();
12642 cx.condition(|editor, _| editor.context_menu_visible())
12643 .await;
12644 cx.update_editor(|editor, _, _| {
12645 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12646 {
12647 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12648 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12649 } else {
12650 panic!("expected completion menu to be open");
12651 }
12652 });
12653}
12654
12655fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12656 let position = || lsp::Position {
12657 line: params.text_document_position.position.line,
12658 character: params.text_document_position.position.character,
12659 };
12660 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12661 range: lsp::Range {
12662 start: position(),
12663 end: position(),
12664 },
12665 new_text: text.to_string(),
12666 }))
12667}
12668
12669#[gpui::test]
12670async fn test_multiline_completion(cx: &mut TestAppContext) {
12671 init_test(cx, |_| {});
12672
12673 let fs = FakeFs::new(cx.executor());
12674 fs.insert_tree(
12675 path!("/a"),
12676 json!({
12677 "main.ts": "a",
12678 }),
12679 )
12680 .await;
12681
12682 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12683 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12684 let typescript_language = Arc::new(Language::new(
12685 LanguageConfig {
12686 name: "TypeScript".into(),
12687 matcher: LanguageMatcher {
12688 path_suffixes: vec!["ts".to_string()],
12689 ..LanguageMatcher::default()
12690 },
12691 line_comments: vec!["// ".into()],
12692 ..LanguageConfig::default()
12693 },
12694 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12695 ));
12696 language_registry.add(typescript_language.clone());
12697 let mut fake_servers = language_registry.register_fake_lsp(
12698 "TypeScript",
12699 FakeLspAdapter {
12700 capabilities: lsp::ServerCapabilities {
12701 completion_provider: Some(lsp::CompletionOptions {
12702 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12703 ..lsp::CompletionOptions::default()
12704 }),
12705 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12706 ..lsp::ServerCapabilities::default()
12707 },
12708 // Emulate vtsls label generation
12709 label_for_completion: Some(Box::new(|item, _| {
12710 let text = if let Some(description) = item
12711 .label_details
12712 .as_ref()
12713 .and_then(|label_details| label_details.description.as_ref())
12714 {
12715 format!("{} {}", item.label, description)
12716 } else if let Some(detail) = &item.detail {
12717 format!("{} {}", item.label, detail)
12718 } else {
12719 item.label.clone()
12720 };
12721 let len = text.len();
12722 Some(language::CodeLabel {
12723 text,
12724 runs: Vec::new(),
12725 filter_range: 0..len,
12726 })
12727 })),
12728 ..FakeLspAdapter::default()
12729 },
12730 );
12731 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12732 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12733 let worktree_id = workspace
12734 .update(cx, |workspace, _window, cx| {
12735 workspace.project().update(cx, |project, cx| {
12736 project.worktrees(cx).next().unwrap().read(cx).id()
12737 })
12738 })
12739 .unwrap();
12740 let _buffer = project
12741 .update(cx, |project, cx| {
12742 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12743 })
12744 .await
12745 .unwrap();
12746 let editor = workspace
12747 .update(cx, |workspace, window, cx| {
12748 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12749 })
12750 .unwrap()
12751 .await
12752 .unwrap()
12753 .downcast::<Editor>()
12754 .unwrap();
12755 let fake_server = fake_servers.next().await.unwrap();
12756
12757 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12758 let multiline_label_2 = "a\nb\nc\n";
12759 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12760 let multiline_description = "d\ne\nf\n";
12761 let multiline_detail_2 = "g\nh\ni\n";
12762
12763 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12764 move |params, _| async move {
12765 Ok(Some(lsp::CompletionResponse::Array(vec![
12766 lsp::CompletionItem {
12767 label: multiline_label.to_string(),
12768 text_edit: gen_text_edit(¶ms, "new_text_1"),
12769 ..lsp::CompletionItem::default()
12770 },
12771 lsp::CompletionItem {
12772 label: "single line label 1".to_string(),
12773 detail: Some(multiline_detail.to_string()),
12774 text_edit: gen_text_edit(¶ms, "new_text_2"),
12775 ..lsp::CompletionItem::default()
12776 },
12777 lsp::CompletionItem {
12778 label: "single line label 2".to_string(),
12779 label_details: Some(lsp::CompletionItemLabelDetails {
12780 description: Some(multiline_description.to_string()),
12781 detail: None,
12782 }),
12783 text_edit: gen_text_edit(¶ms, "new_text_2"),
12784 ..lsp::CompletionItem::default()
12785 },
12786 lsp::CompletionItem {
12787 label: multiline_label_2.to_string(),
12788 detail: Some(multiline_detail_2.to_string()),
12789 text_edit: gen_text_edit(¶ms, "new_text_3"),
12790 ..lsp::CompletionItem::default()
12791 },
12792 lsp::CompletionItem {
12793 label: "Label with many spaces and \t but without newlines".to_string(),
12794 detail: Some(
12795 "Details with many spaces and \t but without newlines".to_string(),
12796 ),
12797 text_edit: gen_text_edit(¶ms, "new_text_4"),
12798 ..lsp::CompletionItem::default()
12799 },
12800 ])))
12801 },
12802 );
12803
12804 editor.update_in(cx, |editor, window, cx| {
12805 cx.focus_self(window);
12806 editor.move_to_end(&MoveToEnd, window, cx);
12807 editor.handle_input(".", window, cx);
12808 });
12809 cx.run_until_parked();
12810 completion_handle.next().await.unwrap();
12811
12812 editor.update(cx, |editor, _| {
12813 assert!(editor.context_menu_visible());
12814 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12815 {
12816 let completion_labels = menu
12817 .completions
12818 .borrow()
12819 .iter()
12820 .map(|c| c.label.text.clone())
12821 .collect::<Vec<_>>();
12822 assert_eq!(
12823 completion_labels,
12824 &[
12825 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12826 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12827 "single line label 2 d e f ",
12828 "a b c g h i ",
12829 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12830 ],
12831 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12832 );
12833
12834 for completion in menu
12835 .completions
12836 .borrow()
12837 .iter() {
12838 assert_eq!(
12839 completion.label.filter_range,
12840 0..completion.label.text.len(),
12841 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
12842 );
12843 }
12844 } else {
12845 panic!("expected completion menu to be open");
12846 }
12847 });
12848}
12849
12850#[gpui::test]
12851async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
12852 init_test(cx, |_| {});
12853 let mut cx = EditorLspTestContext::new_rust(
12854 lsp::ServerCapabilities {
12855 completion_provider: Some(lsp::CompletionOptions {
12856 trigger_characters: Some(vec![".".to_string()]),
12857 ..Default::default()
12858 }),
12859 ..Default::default()
12860 },
12861 cx,
12862 )
12863 .await;
12864 cx.lsp
12865 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12866 Ok(Some(lsp::CompletionResponse::Array(vec![
12867 lsp::CompletionItem {
12868 label: "first".into(),
12869 ..Default::default()
12870 },
12871 lsp::CompletionItem {
12872 label: "last".into(),
12873 ..Default::default()
12874 },
12875 ])))
12876 });
12877 cx.set_state("variableˇ");
12878 cx.simulate_keystroke(".");
12879 cx.executor().run_until_parked();
12880
12881 cx.update_editor(|editor, _, _| {
12882 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12883 {
12884 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
12885 } else {
12886 panic!("expected completion menu to be open");
12887 }
12888 });
12889
12890 cx.update_editor(|editor, window, cx| {
12891 editor.move_page_down(&MovePageDown::default(), window, cx);
12892 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12893 {
12894 assert!(
12895 menu.selected_item == 1,
12896 "expected PageDown to select the last item from the context menu"
12897 );
12898 } else {
12899 panic!("expected completion menu to stay open after PageDown");
12900 }
12901 });
12902
12903 cx.update_editor(|editor, window, cx| {
12904 editor.move_page_up(&MovePageUp::default(), window, cx);
12905 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12906 {
12907 assert!(
12908 menu.selected_item == 0,
12909 "expected PageUp to select the first item from the context menu"
12910 );
12911 } else {
12912 panic!("expected completion menu to stay open after PageUp");
12913 }
12914 });
12915}
12916
12917#[gpui::test]
12918async fn test_as_is_completions(cx: &mut TestAppContext) {
12919 init_test(cx, |_| {});
12920 let mut cx = EditorLspTestContext::new_rust(
12921 lsp::ServerCapabilities {
12922 completion_provider: Some(lsp::CompletionOptions {
12923 ..Default::default()
12924 }),
12925 ..Default::default()
12926 },
12927 cx,
12928 )
12929 .await;
12930 cx.lsp
12931 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12932 Ok(Some(lsp::CompletionResponse::Array(vec![
12933 lsp::CompletionItem {
12934 label: "unsafe".into(),
12935 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12936 range: lsp::Range {
12937 start: lsp::Position {
12938 line: 1,
12939 character: 2,
12940 },
12941 end: lsp::Position {
12942 line: 1,
12943 character: 3,
12944 },
12945 },
12946 new_text: "unsafe".to_string(),
12947 })),
12948 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
12949 ..Default::default()
12950 },
12951 ])))
12952 });
12953 cx.set_state("fn a() {}\n nˇ");
12954 cx.executor().run_until_parked();
12955 cx.update_editor(|editor, window, cx| {
12956 editor.show_completions(
12957 &ShowCompletions {
12958 trigger: Some("\n".into()),
12959 },
12960 window,
12961 cx,
12962 );
12963 });
12964 cx.executor().run_until_parked();
12965
12966 cx.update_editor(|editor, window, cx| {
12967 editor.confirm_completion(&Default::default(), window, cx)
12968 });
12969 cx.executor().run_until_parked();
12970 cx.assert_editor_state("fn a() {}\n unsafeˇ");
12971}
12972
12973#[gpui::test]
12974async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
12975 init_test(cx, |_| {});
12976
12977 let mut cx = EditorLspTestContext::new_rust(
12978 lsp::ServerCapabilities {
12979 completion_provider: Some(lsp::CompletionOptions {
12980 trigger_characters: Some(vec![".".to_string()]),
12981 resolve_provider: Some(true),
12982 ..Default::default()
12983 }),
12984 ..Default::default()
12985 },
12986 cx,
12987 )
12988 .await;
12989
12990 cx.set_state("fn main() { let a = 2ˇ; }");
12991 cx.simulate_keystroke(".");
12992 let completion_item = lsp::CompletionItem {
12993 label: "Some".into(),
12994 kind: Some(lsp::CompletionItemKind::SNIPPET),
12995 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12996 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12997 kind: lsp::MarkupKind::Markdown,
12998 value: "```rust\nSome(2)\n```".to_string(),
12999 })),
13000 deprecated: Some(false),
13001 sort_text: Some("Some".to_string()),
13002 filter_text: Some("Some".to_string()),
13003 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13004 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13005 range: lsp::Range {
13006 start: lsp::Position {
13007 line: 0,
13008 character: 22,
13009 },
13010 end: lsp::Position {
13011 line: 0,
13012 character: 22,
13013 },
13014 },
13015 new_text: "Some(2)".to_string(),
13016 })),
13017 additional_text_edits: Some(vec![lsp::TextEdit {
13018 range: lsp::Range {
13019 start: lsp::Position {
13020 line: 0,
13021 character: 20,
13022 },
13023 end: lsp::Position {
13024 line: 0,
13025 character: 22,
13026 },
13027 },
13028 new_text: "".to_string(),
13029 }]),
13030 ..Default::default()
13031 };
13032
13033 let closure_completion_item = completion_item.clone();
13034 let counter = Arc::new(AtomicUsize::new(0));
13035 let counter_clone = counter.clone();
13036 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13037 let task_completion_item = closure_completion_item.clone();
13038 counter_clone.fetch_add(1, atomic::Ordering::Release);
13039 async move {
13040 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13041 is_incomplete: true,
13042 item_defaults: None,
13043 items: vec![task_completion_item],
13044 })))
13045 }
13046 });
13047
13048 cx.condition(|editor, _| editor.context_menu_visible())
13049 .await;
13050 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13051 assert!(request.next().await.is_some());
13052 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13053
13054 cx.simulate_keystrokes("S o m");
13055 cx.condition(|editor, _| editor.context_menu_visible())
13056 .await;
13057 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13058 assert!(request.next().await.is_some());
13059 assert!(request.next().await.is_some());
13060 assert!(request.next().await.is_some());
13061 request.close();
13062 assert!(request.next().await.is_none());
13063 assert_eq!(
13064 counter.load(atomic::Ordering::Acquire),
13065 4,
13066 "With the completions menu open, only one LSP request should happen per input"
13067 );
13068}
13069
13070#[gpui::test]
13071async fn test_toggle_comment(cx: &mut TestAppContext) {
13072 init_test(cx, |_| {});
13073 let mut cx = EditorTestContext::new(cx).await;
13074 let language = Arc::new(Language::new(
13075 LanguageConfig {
13076 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13077 ..Default::default()
13078 },
13079 Some(tree_sitter_rust::LANGUAGE.into()),
13080 ));
13081 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13082
13083 // If multiple selections intersect a line, the line is only toggled once.
13084 cx.set_state(indoc! {"
13085 fn a() {
13086 «//b();
13087 ˇ»// «c();
13088 //ˇ» d();
13089 }
13090 "});
13091
13092 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13093
13094 cx.assert_editor_state(indoc! {"
13095 fn a() {
13096 «b();
13097 c();
13098 ˇ» d();
13099 }
13100 "});
13101
13102 // The comment prefix is inserted at the same column for every line in a
13103 // selection.
13104 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13105
13106 cx.assert_editor_state(indoc! {"
13107 fn a() {
13108 // «b();
13109 // c();
13110 ˇ»// d();
13111 }
13112 "});
13113
13114 // If a selection ends at the beginning of a line, that line is not toggled.
13115 cx.set_selections_state(indoc! {"
13116 fn a() {
13117 // b();
13118 «// c();
13119 ˇ» // d();
13120 }
13121 "});
13122
13123 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13124
13125 cx.assert_editor_state(indoc! {"
13126 fn a() {
13127 // b();
13128 «c();
13129 ˇ» // d();
13130 }
13131 "});
13132
13133 // If a selection span a single line and is empty, the line is toggled.
13134 cx.set_state(indoc! {"
13135 fn a() {
13136 a();
13137 b();
13138 ˇ
13139 }
13140 "});
13141
13142 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13143
13144 cx.assert_editor_state(indoc! {"
13145 fn a() {
13146 a();
13147 b();
13148 //•ˇ
13149 }
13150 "});
13151
13152 // If a selection span multiple lines, empty lines are not toggled.
13153 cx.set_state(indoc! {"
13154 fn a() {
13155 «a();
13156
13157 c();ˇ»
13158 }
13159 "});
13160
13161 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13162
13163 cx.assert_editor_state(indoc! {"
13164 fn a() {
13165 // «a();
13166
13167 // c();ˇ»
13168 }
13169 "});
13170
13171 // If a selection includes multiple comment prefixes, all lines are uncommented.
13172 cx.set_state(indoc! {"
13173 fn a() {
13174 «// a();
13175 /// b();
13176 //! c();ˇ»
13177 }
13178 "});
13179
13180 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13181
13182 cx.assert_editor_state(indoc! {"
13183 fn a() {
13184 «a();
13185 b();
13186 c();ˇ»
13187 }
13188 "});
13189}
13190
13191#[gpui::test]
13192async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13193 init_test(cx, |_| {});
13194 let mut cx = EditorTestContext::new(cx).await;
13195 let language = Arc::new(Language::new(
13196 LanguageConfig {
13197 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13198 ..Default::default()
13199 },
13200 Some(tree_sitter_rust::LANGUAGE.into()),
13201 ));
13202 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13203
13204 let toggle_comments = &ToggleComments {
13205 advance_downwards: false,
13206 ignore_indent: true,
13207 };
13208
13209 // If multiple selections intersect a line, the line is only toggled once.
13210 cx.set_state(indoc! {"
13211 fn a() {
13212 // «b();
13213 // c();
13214 // ˇ» d();
13215 }
13216 "});
13217
13218 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13219
13220 cx.assert_editor_state(indoc! {"
13221 fn a() {
13222 «b();
13223 c();
13224 ˇ» d();
13225 }
13226 "});
13227
13228 // The comment prefix is inserted at the beginning of each line
13229 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13230
13231 cx.assert_editor_state(indoc! {"
13232 fn a() {
13233 // «b();
13234 // c();
13235 // ˇ» d();
13236 }
13237 "});
13238
13239 // If a selection ends at the beginning of a line, that line is not toggled.
13240 cx.set_selections_state(indoc! {"
13241 fn a() {
13242 // b();
13243 // «c();
13244 ˇ»// d();
13245 }
13246 "});
13247
13248 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13249
13250 cx.assert_editor_state(indoc! {"
13251 fn a() {
13252 // b();
13253 «c();
13254 ˇ»// d();
13255 }
13256 "});
13257
13258 // If a selection span a single line and is empty, the line is toggled.
13259 cx.set_state(indoc! {"
13260 fn a() {
13261 a();
13262 b();
13263 ˇ
13264 }
13265 "});
13266
13267 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13268
13269 cx.assert_editor_state(indoc! {"
13270 fn a() {
13271 a();
13272 b();
13273 //ˇ
13274 }
13275 "});
13276
13277 // If a selection span multiple lines, empty lines are not toggled.
13278 cx.set_state(indoc! {"
13279 fn a() {
13280 «a();
13281
13282 c();ˇ»
13283 }
13284 "});
13285
13286 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13287
13288 cx.assert_editor_state(indoc! {"
13289 fn a() {
13290 // «a();
13291
13292 // c();ˇ»
13293 }
13294 "});
13295
13296 // If a selection includes multiple comment prefixes, all lines are uncommented.
13297 cx.set_state(indoc! {"
13298 fn a() {
13299 // «a();
13300 /// b();
13301 //! c();ˇ»
13302 }
13303 "});
13304
13305 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13306
13307 cx.assert_editor_state(indoc! {"
13308 fn a() {
13309 «a();
13310 b();
13311 c();ˇ»
13312 }
13313 "});
13314}
13315
13316#[gpui::test]
13317async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13318 init_test(cx, |_| {});
13319
13320 let language = Arc::new(Language::new(
13321 LanguageConfig {
13322 line_comments: vec!["// ".into()],
13323 ..Default::default()
13324 },
13325 Some(tree_sitter_rust::LANGUAGE.into()),
13326 ));
13327
13328 let mut cx = EditorTestContext::new(cx).await;
13329
13330 cx.language_registry().add(language.clone());
13331 cx.update_buffer(|buffer, cx| {
13332 buffer.set_language(Some(language), cx);
13333 });
13334
13335 let toggle_comments = &ToggleComments {
13336 advance_downwards: true,
13337 ignore_indent: false,
13338 };
13339
13340 // Single cursor on one line -> advance
13341 // Cursor moves horizontally 3 characters as well on non-blank line
13342 cx.set_state(indoc!(
13343 "fn a() {
13344 ˇdog();
13345 cat();
13346 }"
13347 ));
13348 cx.update_editor(|editor, window, cx| {
13349 editor.toggle_comments(toggle_comments, window, cx);
13350 });
13351 cx.assert_editor_state(indoc!(
13352 "fn a() {
13353 // dog();
13354 catˇ();
13355 }"
13356 ));
13357
13358 // Single selection on one line -> don't advance
13359 cx.set_state(indoc!(
13360 "fn a() {
13361 «dog()ˇ»;
13362 cat();
13363 }"
13364 ));
13365 cx.update_editor(|editor, window, cx| {
13366 editor.toggle_comments(toggle_comments, window, cx);
13367 });
13368 cx.assert_editor_state(indoc!(
13369 "fn a() {
13370 // «dog()ˇ»;
13371 cat();
13372 }"
13373 ));
13374
13375 // Multiple cursors on one line -> advance
13376 cx.set_state(indoc!(
13377 "fn a() {
13378 ˇdˇog();
13379 cat();
13380 }"
13381 ));
13382 cx.update_editor(|editor, window, cx| {
13383 editor.toggle_comments(toggle_comments, window, cx);
13384 });
13385 cx.assert_editor_state(indoc!(
13386 "fn a() {
13387 // dog();
13388 catˇ(ˇ);
13389 }"
13390 ));
13391
13392 // Multiple cursors on one line, with selection -> don't advance
13393 cx.set_state(indoc!(
13394 "fn a() {
13395 ˇdˇog«()ˇ»;
13396 cat();
13397 }"
13398 ));
13399 cx.update_editor(|editor, window, cx| {
13400 editor.toggle_comments(toggle_comments, window, cx);
13401 });
13402 cx.assert_editor_state(indoc!(
13403 "fn a() {
13404 // ˇdˇog«()ˇ»;
13405 cat();
13406 }"
13407 ));
13408
13409 // Single cursor on one line -> advance
13410 // Cursor moves to column 0 on blank line
13411 cx.set_state(indoc!(
13412 "fn a() {
13413 ˇdog();
13414
13415 cat();
13416 }"
13417 ));
13418 cx.update_editor(|editor, window, cx| {
13419 editor.toggle_comments(toggle_comments, window, cx);
13420 });
13421 cx.assert_editor_state(indoc!(
13422 "fn a() {
13423 // dog();
13424 ˇ
13425 cat();
13426 }"
13427 ));
13428
13429 // Single cursor on one line -> advance
13430 // Cursor starts and ends at column 0
13431 cx.set_state(indoc!(
13432 "fn a() {
13433 ˇ dog();
13434 cat();
13435 }"
13436 ));
13437 cx.update_editor(|editor, window, cx| {
13438 editor.toggle_comments(toggle_comments, window, cx);
13439 });
13440 cx.assert_editor_state(indoc!(
13441 "fn a() {
13442 // dog();
13443 ˇ cat();
13444 }"
13445 ));
13446}
13447
13448#[gpui::test]
13449async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13450 init_test(cx, |_| {});
13451
13452 let mut cx = EditorTestContext::new(cx).await;
13453
13454 let html_language = Arc::new(
13455 Language::new(
13456 LanguageConfig {
13457 name: "HTML".into(),
13458 block_comment: Some(("<!-- ".into(), " -->".into())),
13459 ..Default::default()
13460 },
13461 Some(tree_sitter_html::LANGUAGE.into()),
13462 )
13463 .with_injection_query(
13464 r#"
13465 (script_element
13466 (raw_text) @injection.content
13467 (#set! injection.language "javascript"))
13468 "#,
13469 )
13470 .unwrap(),
13471 );
13472
13473 let javascript_language = Arc::new(Language::new(
13474 LanguageConfig {
13475 name: "JavaScript".into(),
13476 line_comments: vec!["// ".into()],
13477 ..Default::default()
13478 },
13479 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13480 ));
13481
13482 cx.language_registry().add(html_language.clone());
13483 cx.language_registry().add(javascript_language.clone());
13484 cx.update_buffer(|buffer, cx| {
13485 buffer.set_language(Some(html_language), cx);
13486 });
13487
13488 // Toggle comments for empty selections
13489 cx.set_state(
13490 &r#"
13491 <p>A</p>ˇ
13492 <p>B</p>ˇ
13493 <p>C</p>ˇ
13494 "#
13495 .unindent(),
13496 );
13497 cx.update_editor(|editor, window, cx| {
13498 editor.toggle_comments(&ToggleComments::default(), window, cx)
13499 });
13500 cx.assert_editor_state(
13501 &r#"
13502 <!-- <p>A</p>ˇ -->
13503 <!-- <p>B</p>ˇ -->
13504 <!-- <p>C</p>ˇ -->
13505 "#
13506 .unindent(),
13507 );
13508 cx.update_editor(|editor, window, cx| {
13509 editor.toggle_comments(&ToggleComments::default(), window, cx)
13510 });
13511 cx.assert_editor_state(
13512 &r#"
13513 <p>A</p>ˇ
13514 <p>B</p>ˇ
13515 <p>C</p>ˇ
13516 "#
13517 .unindent(),
13518 );
13519
13520 // Toggle comments for mixture of empty and non-empty selections, where
13521 // multiple selections occupy a given line.
13522 cx.set_state(
13523 &r#"
13524 <p>A«</p>
13525 <p>ˇ»B</p>ˇ
13526 <p>C«</p>
13527 <p>ˇ»D</p>ˇ
13528 "#
13529 .unindent(),
13530 );
13531
13532 cx.update_editor(|editor, window, cx| {
13533 editor.toggle_comments(&ToggleComments::default(), window, cx)
13534 });
13535 cx.assert_editor_state(
13536 &r#"
13537 <!-- <p>A«</p>
13538 <p>ˇ»B</p>ˇ -->
13539 <!-- <p>C«</p>
13540 <p>ˇ»D</p>ˇ -->
13541 "#
13542 .unindent(),
13543 );
13544 cx.update_editor(|editor, window, cx| {
13545 editor.toggle_comments(&ToggleComments::default(), window, cx)
13546 });
13547 cx.assert_editor_state(
13548 &r#"
13549 <p>A«</p>
13550 <p>ˇ»B</p>ˇ
13551 <p>C«</p>
13552 <p>ˇ»D</p>ˇ
13553 "#
13554 .unindent(),
13555 );
13556
13557 // Toggle comments when different languages are active for different
13558 // selections.
13559 cx.set_state(
13560 &r#"
13561 ˇ<script>
13562 ˇvar x = new Y();
13563 ˇ</script>
13564 "#
13565 .unindent(),
13566 );
13567 cx.executor().run_until_parked();
13568 cx.update_editor(|editor, window, cx| {
13569 editor.toggle_comments(&ToggleComments::default(), window, cx)
13570 });
13571 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13572 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13573 cx.assert_editor_state(
13574 &r#"
13575 <!-- ˇ<script> -->
13576 // ˇvar x = new Y();
13577 <!-- ˇ</script> -->
13578 "#
13579 .unindent(),
13580 );
13581}
13582
13583#[gpui::test]
13584fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13585 init_test(cx, |_| {});
13586
13587 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13588 let multibuffer = cx.new(|cx| {
13589 let mut multibuffer = MultiBuffer::new(ReadWrite);
13590 multibuffer.push_excerpts(
13591 buffer.clone(),
13592 [
13593 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13594 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13595 ],
13596 cx,
13597 );
13598 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13599 multibuffer
13600 });
13601
13602 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13603 editor.update_in(cx, |editor, window, cx| {
13604 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13605 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13606 s.select_ranges([
13607 Point::new(0, 0)..Point::new(0, 0),
13608 Point::new(1, 0)..Point::new(1, 0),
13609 ])
13610 });
13611
13612 editor.handle_input("X", window, cx);
13613 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13614 assert_eq!(
13615 editor.selections.ranges(cx),
13616 [
13617 Point::new(0, 1)..Point::new(0, 1),
13618 Point::new(1, 1)..Point::new(1, 1),
13619 ]
13620 );
13621
13622 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13623 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13624 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13625 });
13626 editor.backspace(&Default::default(), window, cx);
13627 assert_eq!(editor.text(cx), "Xa\nbbb");
13628 assert_eq!(
13629 editor.selections.ranges(cx),
13630 [Point::new(1, 0)..Point::new(1, 0)]
13631 );
13632
13633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13634 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13635 });
13636 editor.backspace(&Default::default(), window, cx);
13637 assert_eq!(editor.text(cx), "X\nbb");
13638 assert_eq!(
13639 editor.selections.ranges(cx),
13640 [Point::new(0, 1)..Point::new(0, 1)]
13641 );
13642 });
13643}
13644
13645#[gpui::test]
13646fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13647 init_test(cx, |_| {});
13648
13649 let markers = vec![('[', ']').into(), ('(', ')').into()];
13650 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13651 indoc! {"
13652 [aaaa
13653 (bbbb]
13654 cccc)",
13655 },
13656 markers.clone(),
13657 );
13658 let excerpt_ranges = markers.into_iter().map(|marker| {
13659 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13660 ExcerptRange::new(context.clone())
13661 });
13662 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13663 let multibuffer = cx.new(|cx| {
13664 let mut multibuffer = MultiBuffer::new(ReadWrite);
13665 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13666 multibuffer
13667 });
13668
13669 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13670 editor.update_in(cx, |editor, window, cx| {
13671 let (expected_text, selection_ranges) = marked_text_ranges(
13672 indoc! {"
13673 aaaa
13674 bˇbbb
13675 bˇbbˇb
13676 cccc"
13677 },
13678 true,
13679 );
13680 assert_eq!(editor.text(cx), expected_text);
13681 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13682 s.select_ranges(selection_ranges)
13683 });
13684
13685 editor.handle_input("X", window, cx);
13686
13687 let (expected_text, expected_selections) = marked_text_ranges(
13688 indoc! {"
13689 aaaa
13690 bXˇbbXb
13691 bXˇbbXˇb
13692 cccc"
13693 },
13694 false,
13695 );
13696 assert_eq!(editor.text(cx), expected_text);
13697 assert_eq!(editor.selections.ranges(cx), expected_selections);
13698
13699 editor.newline(&Newline, window, cx);
13700 let (expected_text, expected_selections) = marked_text_ranges(
13701 indoc! {"
13702 aaaa
13703 bX
13704 ˇbbX
13705 b
13706 bX
13707 ˇbbX
13708 ˇb
13709 cccc"
13710 },
13711 false,
13712 );
13713 assert_eq!(editor.text(cx), expected_text);
13714 assert_eq!(editor.selections.ranges(cx), expected_selections);
13715 });
13716}
13717
13718#[gpui::test]
13719fn test_refresh_selections(cx: &mut TestAppContext) {
13720 init_test(cx, |_| {});
13721
13722 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13723 let mut excerpt1_id = None;
13724 let multibuffer = cx.new(|cx| {
13725 let mut multibuffer = MultiBuffer::new(ReadWrite);
13726 excerpt1_id = multibuffer
13727 .push_excerpts(
13728 buffer.clone(),
13729 [
13730 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13731 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13732 ],
13733 cx,
13734 )
13735 .into_iter()
13736 .next();
13737 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13738 multibuffer
13739 });
13740
13741 let editor = cx.add_window(|window, cx| {
13742 let mut editor = build_editor(multibuffer.clone(), window, cx);
13743 let snapshot = editor.snapshot(window, cx);
13744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13745 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13746 });
13747 editor.begin_selection(
13748 Point::new(2, 1).to_display_point(&snapshot),
13749 true,
13750 1,
13751 window,
13752 cx,
13753 );
13754 assert_eq!(
13755 editor.selections.ranges(cx),
13756 [
13757 Point::new(1, 3)..Point::new(1, 3),
13758 Point::new(2, 1)..Point::new(2, 1),
13759 ]
13760 );
13761 editor
13762 });
13763
13764 // Refreshing selections is a no-op when excerpts haven't changed.
13765 _ = editor.update(cx, |editor, window, cx| {
13766 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13767 assert_eq!(
13768 editor.selections.ranges(cx),
13769 [
13770 Point::new(1, 3)..Point::new(1, 3),
13771 Point::new(2, 1)..Point::new(2, 1),
13772 ]
13773 );
13774 });
13775
13776 multibuffer.update(cx, |multibuffer, cx| {
13777 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13778 });
13779 _ = editor.update(cx, |editor, window, cx| {
13780 // Removing an excerpt causes the first selection to become degenerate.
13781 assert_eq!(
13782 editor.selections.ranges(cx),
13783 [
13784 Point::new(0, 0)..Point::new(0, 0),
13785 Point::new(0, 1)..Point::new(0, 1)
13786 ]
13787 );
13788
13789 // Refreshing selections will relocate the first selection to the original buffer
13790 // location.
13791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13792 assert_eq!(
13793 editor.selections.ranges(cx),
13794 [
13795 Point::new(0, 1)..Point::new(0, 1),
13796 Point::new(0, 3)..Point::new(0, 3)
13797 ]
13798 );
13799 assert!(editor.selections.pending_anchor().is_some());
13800 });
13801}
13802
13803#[gpui::test]
13804fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13805 init_test(cx, |_| {});
13806
13807 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13808 let mut excerpt1_id = None;
13809 let multibuffer = cx.new(|cx| {
13810 let mut multibuffer = MultiBuffer::new(ReadWrite);
13811 excerpt1_id = multibuffer
13812 .push_excerpts(
13813 buffer.clone(),
13814 [
13815 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13816 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13817 ],
13818 cx,
13819 )
13820 .into_iter()
13821 .next();
13822 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13823 multibuffer
13824 });
13825
13826 let editor = cx.add_window(|window, cx| {
13827 let mut editor = build_editor(multibuffer.clone(), window, cx);
13828 let snapshot = editor.snapshot(window, cx);
13829 editor.begin_selection(
13830 Point::new(1, 3).to_display_point(&snapshot),
13831 false,
13832 1,
13833 window,
13834 cx,
13835 );
13836 assert_eq!(
13837 editor.selections.ranges(cx),
13838 [Point::new(1, 3)..Point::new(1, 3)]
13839 );
13840 editor
13841 });
13842
13843 multibuffer.update(cx, |multibuffer, cx| {
13844 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13845 });
13846 _ = editor.update(cx, |editor, window, cx| {
13847 assert_eq!(
13848 editor.selections.ranges(cx),
13849 [Point::new(0, 0)..Point::new(0, 0)]
13850 );
13851
13852 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
13853 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13854 assert_eq!(
13855 editor.selections.ranges(cx),
13856 [Point::new(0, 3)..Point::new(0, 3)]
13857 );
13858 assert!(editor.selections.pending_anchor().is_some());
13859 });
13860}
13861
13862#[gpui::test]
13863async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
13864 init_test(cx, |_| {});
13865
13866 let language = Arc::new(
13867 Language::new(
13868 LanguageConfig {
13869 brackets: BracketPairConfig {
13870 pairs: vec![
13871 BracketPair {
13872 start: "{".to_string(),
13873 end: "}".to_string(),
13874 close: true,
13875 surround: true,
13876 newline: true,
13877 },
13878 BracketPair {
13879 start: "/* ".to_string(),
13880 end: " */".to_string(),
13881 close: true,
13882 surround: true,
13883 newline: true,
13884 },
13885 ],
13886 ..Default::default()
13887 },
13888 ..Default::default()
13889 },
13890 Some(tree_sitter_rust::LANGUAGE.into()),
13891 )
13892 .with_indents_query("")
13893 .unwrap(),
13894 );
13895
13896 let text = concat!(
13897 "{ }\n", //
13898 " x\n", //
13899 " /* */\n", //
13900 "x\n", //
13901 "{{} }\n", //
13902 );
13903
13904 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
13905 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13906 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13907 editor
13908 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
13909 .await;
13910
13911 editor.update_in(cx, |editor, window, cx| {
13912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13913 s.select_display_ranges([
13914 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
13915 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
13916 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
13917 ])
13918 });
13919 editor.newline(&Newline, window, cx);
13920
13921 assert_eq!(
13922 editor.buffer().read(cx).read(cx).text(),
13923 concat!(
13924 "{ \n", // Suppress rustfmt
13925 "\n", //
13926 "}\n", //
13927 " x\n", //
13928 " /* \n", //
13929 " \n", //
13930 " */\n", //
13931 "x\n", //
13932 "{{} \n", //
13933 "}\n", //
13934 )
13935 );
13936 });
13937}
13938
13939#[gpui::test]
13940fn test_highlighted_ranges(cx: &mut TestAppContext) {
13941 init_test(cx, |_| {});
13942
13943 let editor = cx.add_window(|window, cx| {
13944 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
13945 build_editor(buffer.clone(), window, cx)
13946 });
13947
13948 _ = editor.update(cx, |editor, window, cx| {
13949 struct Type1;
13950 struct Type2;
13951
13952 let buffer = editor.buffer.read(cx).snapshot(cx);
13953
13954 let anchor_range =
13955 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
13956
13957 editor.highlight_background::<Type1>(
13958 &[
13959 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
13960 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
13961 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
13962 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
13963 ],
13964 |_| Hsla::red(),
13965 cx,
13966 );
13967 editor.highlight_background::<Type2>(
13968 &[
13969 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
13970 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
13971 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
13972 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
13973 ],
13974 |_| Hsla::green(),
13975 cx,
13976 );
13977
13978 let snapshot = editor.snapshot(window, cx);
13979 let mut highlighted_ranges = editor.background_highlights_in_range(
13980 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
13981 &snapshot,
13982 cx.theme(),
13983 );
13984 // Enforce a consistent ordering based on color without relying on the ordering of the
13985 // highlight's `TypeId` which is non-executor.
13986 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
13987 assert_eq!(
13988 highlighted_ranges,
13989 &[
13990 (
13991 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13992 Hsla::red(),
13993 ),
13994 (
13995 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13996 Hsla::red(),
13997 ),
13998 (
13999 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14000 Hsla::green(),
14001 ),
14002 (
14003 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14004 Hsla::green(),
14005 ),
14006 ]
14007 );
14008 assert_eq!(
14009 editor.background_highlights_in_range(
14010 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14011 &snapshot,
14012 cx.theme(),
14013 ),
14014 &[(
14015 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14016 Hsla::red(),
14017 )]
14018 );
14019 });
14020}
14021
14022#[gpui::test]
14023async fn test_following(cx: &mut TestAppContext) {
14024 init_test(cx, |_| {});
14025
14026 let fs = FakeFs::new(cx.executor());
14027 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14028
14029 let buffer = project.update(cx, |project, cx| {
14030 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14031 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14032 });
14033 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14034 let follower = cx.update(|cx| {
14035 cx.open_window(
14036 WindowOptions {
14037 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14038 gpui::Point::new(px(0.), px(0.)),
14039 gpui::Point::new(px(10.), px(80.)),
14040 ))),
14041 ..Default::default()
14042 },
14043 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14044 )
14045 .unwrap()
14046 });
14047
14048 let is_still_following = Rc::new(RefCell::new(true));
14049 let follower_edit_event_count = Rc::new(RefCell::new(0));
14050 let pending_update = Rc::new(RefCell::new(None));
14051 let leader_entity = leader.root(cx).unwrap();
14052 let follower_entity = follower.root(cx).unwrap();
14053 _ = follower.update(cx, {
14054 let update = pending_update.clone();
14055 let is_still_following = is_still_following.clone();
14056 let follower_edit_event_count = follower_edit_event_count.clone();
14057 |_, window, cx| {
14058 cx.subscribe_in(
14059 &leader_entity,
14060 window,
14061 move |_, leader, event, window, cx| {
14062 leader.read(cx).add_event_to_update_proto(
14063 event,
14064 &mut update.borrow_mut(),
14065 window,
14066 cx,
14067 );
14068 },
14069 )
14070 .detach();
14071
14072 cx.subscribe_in(
14073 &follower_entity,
14074 window,
14075 move |_, _, event: &EditorEvent, _window, _cx| {
14076 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14077 *is_still_following.borrow_mut() = false;
14078 }
14079
14080 if let EditorEvent::BufferEdited = event {
14081 *follower_edit_event_count.borrow_mut() += 1;
14082 }
14083 },
14084 )
14085 .detach();
14086 }
14087 });
14088
14089 // Update the selections only
14090 _ = leader.update(cx, |leader, window, cx| {
14091 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14092 s.select_ranges([1..1])
14093 });
14094 });
14095 follower
14096 .update(cx, |follower, window, cx| {
14097 follower.apply_update_proto(
14098 &project,
14099 pending_update.borrow_mut().take().unwrap(),
14100 window,
14101 cx,
14102 )
14103 })
14104 .unwrap()
14105 .await
14106 .unwrap();
14107 _ = follower.update(cx, |follower, _, cx| {
14108 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14109 });
14110 assert!(*is_still_following.borrow());
14111 assert_eq!(*follower_edit_event_count.borrow(), 0);
14112
14113 // Update the scroll position only
14114 _ = leader.update(cx, |leader, window, cx| {
14115 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14116 });
14117 follower
14118 .update(cx, |follower, window, cx| {
14119 follower.apply_update_proto(
14120 &project,
14121 pending_update.borrow_mut().take().unwrap(),
14122 window,
14123 cx,
14124 )
14125 })
14126 .unwrap()
14127 .await
14128 .unwrap();
14129 assert_eq!(
14130 follower
14131 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14132 .unwrap(),
14133 gpui::Point::new(1.5, 3.5)
14134 );
14135 assert!(*is_still_following.borrow());
14136 assert_eq!(*follower_edit_event_count.borrow(), 0);
14137
14138 // Update the selections and scroll position. The follower's scroll position is updated
14139 // via autoscroll, not via the leader's exact scroll position.
14140 _ = leader.update(cx, |leader, window, cx| {
14141 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14142 s.select_ranges([0..0])
14143 });
14144 leader.request_autoscroll(Autoscroll::newest(), cx);
14145 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14146 });
14147 follower
14148 .update(cx, |follower, window, cx| {
14149 follower.apply_update_proto(
14150 &project,
14151 pending_update.borrow_mut().take().unwrap(),
14152 window,
14153 cx,
14154 )
14155 })
14156 .unwrap()
14157 .await
14158 .unwrap();
14159 _ = follower.update(cx, |follower, _, cx| {
14160 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14161 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14162 });
14163 assert!(*is_still_following.borrow());
14164
14165 // Creating a pending selection that precedes another selection
14166 _ = leader.update(cx, |leader, window, cx| {
14167 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14168 s.select_ranges([1..1])
14169 });
14170 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14171 });
14172 follower
14173 .update(cx, |follower, window, cx| {
14174 follower.apply_update_proto(
14175 &project,
14176 pending_update.borrow_mut().take().unwrap(),
14177 window,
14178 cx,
14179 )
14180 })
14181 .unwrap()
14182 .await
14183 .unwrap();
14184 _ = follower.update(cx, |follower, _, cx| {
14185 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14186 });
14187 assert!(*is_still_following.borrow());
14188
14189 // Extend the pending selection so that it surrounds another selection
14190 _ = leader.update(cx, |leader, window, cx| {
14191 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14192 });
14193 follower
14194 .update(cx, |follower, window, cx| {
14195 follower.apply_update_proto(
14196 &project,
14197 pending_update.borrow_mut().take().unwrap(),
14198 window,
14199 cx,
14200 )
14201 })
14202 .unwrap()
14203 .await
14204 .unwrap();
14205 _ = follower.update(cx, |follower, _, cx| {
14206 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14207 });
14208
14209 // Scrolling locally breaks the follow
14210 _ = follower.update(cx, |follower, window, cx| {
14211 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14212 follower.set_scroll_anchor(
14213 ScrollAnchor {
14214 anchor: top_anchor,
14215 offset: gpui::Point::new(0.0, 0.5),
14216 },
14217 window,
14218 cx,
14219 );
14220 });
14221 assert!(!(*is_still_following.borrow()));
14222}
14223
14224#[gpui::test]
14225async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14226 init_test(cx, |_| {});
14227
14228 let fs = FakeFs::new(cx.executor());
14229 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14230 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14231 let pane = workspace
14232 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14233 .unwrap();
14234
14235 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14236
14237 let leader = pane.update_in(cx, |_, window, cx| {
14238 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14239 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14240 });
14241
14242 // Start following the editor when it has no excerpts.
14243 let mut state_message =
14244 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14245 let workspace_entity = workspace.root(cx).unwrap();
14246 let follower_1 = cx
14247 .update_window(*workspace.deref(), |_, window, cx| {
14248 Editor::from_state_proto(
14249 workspace_entity,
14250 ViewId {
14251 creator: CollaboratorId::PeerId(PeerId::default()),
14252 id: 0,
14253 },
14254 &mut state_message,
14255 window,
14256 cx,
14257 )
14258 })
14259 .unwrap()
14260 .unwrap()
14261 .await
14262 .unwrap();
14263
14264 let update_message = Rc::new(RefCell::new(None));
14265 follower_1.update_in(cx, {
14266 let update = update_message.clone();
14267 |_, window, cx| {
14268 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14269 leader.read(cx).add_event_to_update_proto(
14270 event,
14271 &mut update.borrow_mut(),
14272 window,
14273 cx,
14274 );
14275 })
14276 .detach();
14277 }
14278 });
14279
14280 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14281 (
14282 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14283 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14284 )
14285 });
14286
14287 // Insert some excerpts.
14288 leader.update(cx, |leader, cx| {
14289 leader.buffer.update(cx, |multibuffer, cx| {
14290 multibuffer.set_excerpts_for_path(
14291 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14292 buffer_1.clone(),
14293 vec![
14294 Point::row_range(0..3),
14295 Point::row_range(1..6),
14296 Point::row_range(12..15),
14297 ],
14298 0,
14299 cx,
14300 );
14301 multibuffer.set_excerpts_for_path(
14302 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14303 buffer_2.clone(),
14304 vec![Point::row_range(0..6), Point::row_range(8..12)],
14305 0,
14306 cx,
14307 );
14308 });
14309 });
14310
14311 // Apply the update of adding the excerpts.
14312 follower_1
14313 .update_in(cx, |follower, window, cx| {
14314 follower.apply_update_proto(
14315 &project,
14316 update_message.borrow().clone().unwrap(),
14317 window,
14318 cx,
14319 )
14320 })
14321 .await
14322 .unwrap();
14323 assert_eq!(
14324 follower_1.update(cx, |editor, cx| editor.text(cx)),
14325 leader.update(cx, |editor, cx| editor.text(cx))
14326 );
14327 update_message.borrow_mut().take();
14328
14329 // Start following separately after it already has excerpts.
14330 let mut state_message =
14331 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14332 let workspace_entity = workspace.root(cx).unwrap();
14333 let follower_2 = cx
14334 .update_window(*workspace.deref(), |_, window, cx| {
14335 Editor::from_state_proto(
14336 workspace_entity,
14337 ViewId {
14338 creator: CollaboratorId::PeerId(PeerId::default()),
14339 id: 0,
14340 },
14341 &mut state_message,
14342 window,
14343 cx,
14344 )
14345 })
14346 .unwrap()
14347 .unwrap()
14348 .await
14349 .unwrap();
14350 assert_eq!(
14351 follower_2.update(cx, |editor, cx| editor.text(cx)),
14352 leader.update(cx, |editor, cx| editor.text(cx))
14353 );
14354
14355 // Remove some excerpts.
14356 leader.update(cx, |leader, cx| {
14357 leader.buffer.update(cx, |multibuffer, cx| {
14358 let excerpt_ids = multibuffer.excerpt_ids();
14359 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14360 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14361 });
14362 });
14363
14364 // Apply the update of removing the excerpts.
14365 follower_1
14366 .update_in(cx, |follower, window, cx| {
14367 follower.apply_update_proto(
14368 &project,
14369 update_message.borrow().clone().unwrap(),
14370 window,
14371 cx,
14372 )
14373 })
14374 .await
14375 .unwrap();
14376 follower_2
14377 .update_in(cx, |follower, window, cx| {
14378 follower.apply_update_proto(
14379 &project,
14380 update_message.borrow().clone().unwrap(),
14381 window,
14382 cx,
14383 )
14384 })
14385 .await
14386 .unwrap();
14387 update_message.borrow_mut().take();
14388 assert_eq!(
14389 follower_1.update(cx, |editor, cx| editor.text(cx)),
14390 leader.update(cx, |editor, cx| editor.text(cx))
14391 );
14392}
14393
14394#[gpui::test]
14395async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14396 init_test(cx, |_| {});
14397
14398 let mut cx = EditorTestContext::new(cx).await;
14399 let lsp_store =
14400 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14401
14402 cx.set_state(indoc! {"
14403 ˇfn func(abc def: i32) -> u32 {
14404 }
14405 "});
14406
14407 cx.update(|_, cx| {
14408 lsp_store.update(cx, |lsp_store, cx| {
14409 lsp_store
14410 .update_diagnostics(
14411 LanguageServerId(0),
14412 lsp::PublishDiagnosticsParams {
14413 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14414 version: None,
14415 diagnostics: vec![
14416 lsp::Diagnostic {
14417 range: lsp::Range::new(
14418 lsp::Position::new(0, 11),
14419 lsp::Position::new(0, 12),
14420 ),
14421 severity: Some(lsp::DiagnosticSeverity::ERROR),
14422 ..Default::default()
14423 },
14424 lsp::Diagnostic {
14425 range: lsp::Range::new(
14426 lsp::Position::new(0, 12),
14427 lsp::Position::new(0, 15),
14428 ),
14429 severity: Some(lsp::DiagnosticSeverity::ERROR),
14430 ..Default::default()
14431 },
14432 lsp::Diagnostic {
14433 range: lsp::Range::new(
14434 lsp::Position::new(0, 25),
14435 lsp::Position::new(0, 28),
14436 ),
14437 severity: Some(lsp::DiagnosticSeverity::ERROR),
14438 ..Default::default()
14439 },
14440 ],
14441 },
14442 None,
14443 DiagnosticSourceKind::Pushed,
14444 &[],
14445 cx,
14446 )
14447 .unwrap()
14448 });
14449 });
14450
14451 executor.run_until_parked();
14452
14453 cx.update_editor(|editor, window, cx| {
14454 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14455 });
14456
14457 cx.assert_editor_state(indoc! {"
14458 fn func(abc def: i32) -> ˇu32 {
14459 }
14460 "});
14461
14462 cx.update_editor(|editor, window, cx| {
14463 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14464 });
14465
14466 cx.assert_editor_state(indoc! {"
14467 fn func(abc ˇdef: i32) -> u32 {
14468 }
14469 "});
14470
14471 cx.update_editor(|editor, window, cx| {
14472 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14473 });
14474
14475 cx.assert_editor_state(indoc! {"
14476 fn func(abcˇ def: i32) -> u32 {
14477 }
14478 "});
14479
14480 cx.update_editor(|editor, window, cx| {
14481 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14482 });
14483
14484 cx.assert_editor_state(indoc! {"
14485 fn func(abc def: i32) -> ˇu32 {
14486 }
14487 "});
14488}
14489
14490#[gpui::test]
14491async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14492 init_test(cx, |_| {});
14493
14494 let mut cx = EditorTestContext::new(cx).await;
14495
14496 let diff_base = r#"
14497 use some::mod;
14498
14499 const A: u32 = 42;
14500
14501 fn main() {
14502 println!("hello");
14503
14504 println!("world");
14505 }
14506 "#
14507 .unindent();
14508
14509 // Edits are modified, removed, modified, added
14510 cx.set_state(
14511 &r#"
14512 use some::modified;
14513
14514 ˇ
14515 fn main() {
14516 println!("hello there");
14517
14518 println!("around the");
14519 println!("world");
14520 }
14521 "#
14522 .unindent(),
14523 );
14524
14525 cx.set_head_text(&diff_base);
14526 executor.run_until_parked();
14527
14528 cx.update_editor(|editor, window, cx| {
14529 //Wrap around the bottom of the buffer
14530 for _ in 0..3 {
14531 editor.go_to_next_hunk(&GoToHunk, window, cx);
14532 }
14533 });
14534
14535 cx.assert_editor_state(
14536 &r#"
14537 ˇuse some::modified;
14538
14539
14540 fn main() {
14541 println!("hello there");
14542
14543 println!("around the");
14544 println!("world");
14545 }
14546 "#
14547 .unindent(),
14548 );
14549
14550 cx.update_editor(|editor, window, cx| {
14551 //Wrap around the top of the buffer
14552 for _ in 0..2 {
14553 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14554 }
14555 });
14556
14557 cx.assert_editor_state(
14558 &r#"
14559 use some::modified;
14560
14561
14562 fn main() {
14563 ˇ println!("hello there");
14564
14565 println!("around the");
14566 println!("world");
14567 }
14568 "#
14569 .unindent(),
14570 );
14571
14572 cx.update_editor(|editor, window, cx| {
14573 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14574 });
14575
14576 cx.assert_editor_state(
14577 &r#"
14578 use some::modified;
14579
14580 ˇ
14581 fn main() {
14582 println!("hello there");
14583
14584 println!("around the");
14585 println!("world");
14586 }
14587 "#
14588 .unindent(),
14589 );
14590
14591 cx.update_editor(|editor, window, cx| {
14592 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14593 });
14594
14595 cx.assert_editor_state(
14596 &r#"
14597 ˇuse some::modified;
14598
14599
14600 fn main() {
14601 println!("hello there");
14602
14603 println!("around the");
14604 println!("world");
14605 }
14606 "#
14607 .unindent(),
14608 );
14609
14610 cx.update_editor(|editor, window, cx| {
14611 for _ in 0..2 {
14612 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14613 }
14614 });
14615
14616 cx.assert_editor_state(
14617 &r#"
14618 use some::modified;
14619
14620
14621 fn main() {
14622 ˇ println!("hello there");
14623
14624 println!("around the");
14625 println!("world");
14626 }
14627 "#
14628 .unindent(),
14629 );
14630
14631 cx.update_editor(|editor, window, cx| {
14632 editor.fold(&Fold, window, cx);
14633 });
14634
14635 cx.update_editor(|editor, window, cx| {
14636 editor.go_to_next_hunk(&GoToHunk, window, cx);
14637 });
14638
14639 cx.assert_editor_state(
14640 &r#"
14641 ˇuse some::modified;
14642
14643
14644 fn main() {
14645 println!("hello there");
14646
14647 println!("around the");
14648 println!("world");
14649 }
14650 "#
14651 .unindent(),
14652 );
14653}
14654
14655#[test]
14656fn test_split_words() {
14657 fn split(text: &str) -> Vec<&str> {
14658 split_words(text).collect()
14659 }
14660
14661 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14662 assert_eq!(split("hello_world"), &["hello_", "world"]);
14663 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14664 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14665 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14666 assert_eq!(split("helloworld"), &["helloworld"]);
14667
14668 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14669}
14670
14671#[gpui::test]
14672async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14673 init_test(cx, |_| {});
14674
14675 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14676 let mut assert = |before, after| {
14677 let _state_context = cx.set_state(before);
14678 cx.run_until_parked();
14679 cx.update_editor(|editor, window, cx| {
14680 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14681 });
14682 cx.run_until_parked();
14683 cx.assert_editor_state(after);
14684 };
14685
14686 // Outside bracket jumps to outside of matching bracket
14687 assert("console.logˇ(var);", "console.log(var)ˇ;");
14688 assert("console.log(var)ˇ;", "console.logˇ(var);");
14689
14690 // Inside bracket jumps to inside of matching bracket
14691 assert("console.log(ˇvar);", "console.log(varˇ);");
14692 assert("console.log(varˇ);", "console.log(ˇvar);");
14693
14694 // When outside a bracket and inside, favor jumping to the inside bracket
14695 assert(
14696 "console.log('foo', [1, 2, 3]ˇ);",
14697 "console.log(ˇ'foo', [1, 2, 3]);",
14698 );
14699 assert(
14700 "console.log(ˇ'foo', [1, 2, 3]);",
14701 "console.log('foo', [1, 2, 3]ˇ);",
14702 );
14703
14704 // Bias forward if two options are equally likely
14705 assert(
14706 "let result = curried_fun()ˇ();",
14707 "let result = curried_fun()()ˇ;",
14708 );
14709
14710 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14711 assert(
14712 indoc! {"
14713 function test() {
14714 console.log('test')ˇ
14715 }"},
14716 indoc! {"
14717 function test() {
14718 console.logˇ('test')
14719 }"},
14720 );
14721}
14722
14723#[gpui::test]
14724async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14725 init_test(cx, |_| {});
14726
14727 let fs = FakeFs::new(cx.executor());
14728 fs.insert_tree(
14729 path!("/a"),
14730 json!({
14731 "main.rs": "fn main() { let a = 5; }",
14732 "other.rs": "// Test file",
14733 }),
14734 )
14735 .await;
14736 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14737
14738 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14739 language_registry.add(Arc::new(Language::new(
14740 LanguageConfig {
14741 name: "Rust".into(),
14742 matcher: LanguageMatcher {
14743 path_suffixes: vec!["rs".to_string()],
14744 ..Default::default()
14745 },
14746 brackets: BracketPairConfig {
14747 pairs: vec![BracketPair {
14748 start: "{".to_string(),
14749 end: "}".to_string(),
14750 close: true,
14751 surround: true,
14752 newline: true,
14753 }],
14754 disabled_scopes_by_bracket_ix: Vec::new(),
14755 },
14756 ..Default::default()
14757 },
14758 Some(tree_sitter_rust::LANGUAGE.into()),
14759 )));
14760 let mut fake_servers = language_registry.register_fake_lsp(
14761 "Rust",
14762 FakeLspAdapter {
14763 capabilities: lsp::ServerCapabilities {
14764 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14765 first_trigger_character: "{".to_string(),
14766 more_trigger_character: None,
14767 }),
14768 ..Default::default()
14769 },
14770 ..Default::default()
14771 },
14772 );
14773
14774 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14775
14776 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14777
14778 let worktree_id = workspace
14779 .update(cx, |workspace, _, cx| {
14780 workspace.project().update(cx, |project, cx| {
14781 project.worktrees(cx).next().unwrap().read(cx).id()
14782 })
14783 })
14784 .unwrap();
14785
14786 let buffer = project
14787 .update(cx, |project, cx| {
14788 project.open_local_buffer(path!("/a/main.rs"), cx)
14789 })
14790 .await
14791 .unwrap();
14792 let editor_handle = workspace
14793 .update(cx, |workspace, window, cx| {
14794 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14795 })
14796 .unwrap()
14797 .await
14798 .unwrap()
14799 .downcast::<Editor>()
14800 .unwrap();
14801
14802 cx.executor().start_waiting();
14803 let fake_server = fake_servers.next().await.unwrap();
14804
14805 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14806 |params, _| async move {
14807 assert_eq!(
14808 params.text_document_position.text_document.uri,
14809 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14810 );
14811 assert_eq!(
14812 params.text_document_position.position,
14813 lsp::Position::new(0, 21),
14814 );
14815
14816 Ok(Some(vec![lsp::TextEdit {
14817 new_text: "]".to_string(),
14818 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14819 }]))
14820 },
14821 );
14822
14823 editor_handle.update_in(cx, |editor, window, cx| {
14824 window.focus(&editor.focus_handle(cx));
14825 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14826 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14827 });
14828 editor.handle_input("{", window, cx);
14829 });
14830
14831 cx.executor().run_until_parked();
14832
14833 buffer.update(cx, |buffer, _| {
14834 assert_eq!(
14835 buffer.text(),
14836 "fn main() { let a = {5}; }",
14837 "No extra braces from on type formatting should appear in the buffer"
14838 )
14839 });
14840}
14841
14842#[gpui::test(iterations = 20, seeds(31))]
14843async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
14844 init_test(cx, |_| {});
14845
14846 let mut cx = EditorLspTestContext::new_rust(
14847 lsp::ServerCapabilities {
14848 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14849 first_trigger_character: ".".to_string(),
14850 more_trigger_character: None,
14851 }),
14852 ..Default::default()
14853 },
14854 cx,
14855 )
14856 .await;
14857
14858 cx.update_buffer(|buffer, _| {
14859 // This causes autoindent to be async.
14860 buffer.set_sync_parse_timeout(Duration::ZERO)
14861 });
14862
14863 cx.set_state("fn c() {\n d()ˇ\n}\n");
14864 cx.simulate_keystroke("\n");
14865 cx.run_until_parked();
14866
14867 let buffer_cloned =
14868 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
14869 let mut request =
14870 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
14871 let buffer_cloned = buffer_cloned.clone();
14872 async move {
14873 buffer_cloned.update(&mut cx, |buffer, _| {
14874 assert_eq!(
14875 buffer.text(),
14876 "fn c() {\n d()\n .\n}\n",
14877 "OnTypeFormatting should triggered after autoindent applied"
14878 )
14879 })?;
14880
14881 Ok(Some(vec![]))
14882 }
14883 });
14884
14885 cx.simulate_keystroke(".");
14886 cx.run_until_parked();
14887
14888 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
14889 assert!(request.next().await.is_some());
14890 request.close();
14891 assert!(request.next().await.is_none());
14892}
14893
14894#[gpui::test]
14895async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
14896 init_test(cx, |_| {});
14897
14898 let fs = FakeFs::new(cx.executor());
14899 fs.insert_tree(
14900 path!("/a"),
14901 json!({
14902 "main.rs": "fn main() { let a = 5; }",
14903 "other.rs": "// Test file",
14904 }),
14905 )
14906 .await;
14907
14908 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14909
14910 let server_restarts = Arc::new(AtomicUsize::new(0));
14911 let closure_restarts = Arc::clone(&server_restarts);
14912 let language_server_name = "test language server";
14913 let language_name: LanguageName = "Rust".into();
14914
14915 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14916 language_registry.add(Arc::new(Language::new(
14917 LanguageConfig {
14918 name: language_name.clone(),
14919 matcher: LanguageMatcher {
14920 path_suffixes: vec!["rs".to_string()],
14921 ..Default::default()
14922 },
14923 ..Default::default()
14924 },
14925 Some(tree_sitter_rust::LANGUAGE.into()),
14926 )));
14927 let mut fake_servers = language_registry.register_fake_lsp(
14928 "Rust",
14929 FakeLspAdapter {
14930 name: language_server_name,
14931 initialization_options: Some(json!({
14932 "testOptionValue": true
14933 })),
14934 initializer: Some(Box::new(move |fake_server| {
14935 let task_restarts = Arc::clone(&closure_restarts);
14936 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
14937 task_restarts.fetch_add(1, atomic::Ordering::Release);
14938 futures::future::ready(Ok(()))
14939 });
14940 })),
14941 ..Default::default()
14942 },
14943 );
14944
14945 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14946 let _buffer = project
14947 .update(cx, |project, cx| {
14948 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
14949 })
14950 .await
14951 .unwrap();
14952 let _fake_server = fake_servers.next().await.unwrap();
14953 update_test_language_settings(cx, |language_settings| {
14954 language_settings.languages.insert(
14955 language_name.clone(),
14956 LanguageSettingsContent {
14957 tab_size: NonZeroU32::new(8),
14958 ..Default::default()
14959 },
14960 );
14961 });
14962 cx.executor().run_until_parked();
14963 assert_eq!(
14964 server_restarts.load(atomic::Ordering::Acquire),
14965 0,
14966 "Should not restart LSP server on an unrelated change"
14967 );
14968
14969 update_test_project_settings(cx, |project_settings| {
14970 project_settings.lsp.insert(
14971 "Some other server name".into(),
14972 LspSettings {
14973 binary: None,
14974 settings: None,
14975 initialization_options: Some(json!({
14976 "some other init value": false
14977 })),
14978 enable_lsp_tasks: false,
14979 },
14980 );
14981 });
14982 cx.executor().run_until_parked();
14983 assert_eq!(
14984 server_restarts.load(atomic::Ordering::Acquire),
14985 0,
14986 "Should not restart LSP server on an unrelated LSP settings change"
14987 );
14988
14989 update_test_project_settings(cx, |project_settings| {
14990 project_settings.lsp.insert(
14991 language_server_name.into(),
14992 LspSettings {
14993 binary: None,
14994 settings: None,
14995 initialization_options: Some(json!({
14996 "anotherInitValue": false
14997 })),
14998 enable_lsp_tasks: false,
14999 },
15000 );
15001 });
15002 cx.executor().run_until_parked();
15003 assert_eq!(
15004 server_restarts.load(atomic::Ordering::Acquire),
15005 1,
15006 "Should restart LSP server on a related LSP settings change"
15007 );
15008
15009 update_test_project_settings(cx, |project_settings| {
15010 project_settings.lsp.insert(
15011 language_server_name.into(),
15012 LspSettings {
15013 binary: None,
15014 settings: None,
15015 initialization_options: Some(json!({
15016 "anotherInitValue": false
15017 })),
15018 enable_lsp_tasks: false,
15019 },
15020 );
15021 });
15022 cx.executor().run_until_parked();
15023 assert_eq!(
15024 server_restarts.load(atomic::Ordering::Acquire),
15025 1,
15026 "Should not restart LSP server on a related LSP settings change that is the same"
15027 );
15028
15029 update_test_project_settings(cx, |project_settings| {
15030 project_settings.lsp.insert(
15031 language_server_name.into(),
15032 LspSettings {
15033 binary: None,
15034 settings: None,
15035 initialization_options: None,
15036 enable_lsp_tasks: false,
15037 },
15038 );
15039 });
15040 cx.executor().run_until_parked();
15041 assert_eq!(
15042 server_restarts.load(atomic::Ordering::Acquire),
15043 2,
15044 "Should restart LSP server on another related LSP settings change"
15045 );
15046}
15047
15048#[gpui::test]
15049async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15050 init_test(cx, |_| {});
15051
15052 let mut cx = EditorLspTestContext::new_rust(
15053 lsp::ServerCapabilities {
15054 completion_provider: Some(lsp::CompletionOptions {
15055 trigger_characters: Some(vec![".".to_string()]),
15056 resolve_provider: Some(true),
15057 ..Default::default()
15058 }),
15059 ..Default::default()
15060 },
15061 cx,
15062 )
15063 .await;
15064
15065 cx.set_state("fn main() { let a = 2ˇ; }");
15066 cx.simulate_keystroke(".");
15067 let completion_item = lsp::CompletionItem {
15068 label: "some".into(),
15069 kind: Some(lsp::CompletionItemKind::SNIPPET),
15070 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15071 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15072 kind: lsp::MarkupKind::Markdown,
15073 value: "```rust\nSome(2)\n```".to_string(),
15074 })),
15075 deprecated: Some(false),
15076 sort_text: Some("fffffff2".to_string()),
15077 filter_text: Some("some".to_string()),
15078 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15079 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15080 range: lsp::Range {
15081 start: lsp::Position {
15082 line: 0,
15083 character: 22,
15084 },
15085 end: lsp::Position {
15086 line: 0,
15087 character: 22,
15088 },
15089 },
15090 new_text: "Some(2)".to_string(),
15091 })),
15092 additional_text_edits: Some(vec![lsp::TextEdit {
15093 range: lsp::Range {
15094 start: lsp::Position {
15095 line: 0,
15096 character: 20,
15097 },
15098 end: lsp::Position {
15099 line: 0,
15100 character: 22,
15101 },
15102 },
15103 new_text: "".to_string(),
15104 }]),
15105 ..Default::default()
15106 };
15107
15108 let closure_completion_item = completion_item.clone();
15109 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15110 let task_completion_item = closure_completion_item.clone();
15111 async move {
15112 Ok(Some(lsp::CompletionResponse::Array(vec![
15113 task_completion_item,
15114 ])))
15115 }
15116 });
15117
15118 request.next().await;
15119
15120 cx.condition(|editor, _| editor.context_menu_visible())
15121 .await;
15122 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15123 editor
15124 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15125 .unwrap()
15126 });
15127 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15128
15129 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15130 let task_completion_item = completion_item.clone();
15131 async move { Ok(task_completion_item) }
15132 })
15133 .next()
15134 .await
15135 .unwrap();
15136 apply_additional_edits.await.unwrap();
15137 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15138}
15139
15140#[gpui::test]
15141async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15142 init_test(cx, |_| {});
15143
15144 let mut cx = EditorLspTestContext::new_rust(
15145 lsp::ServerCapabilities {
15146 completion_provider: Some(lsp::CompletionOptions {
15147 trigger_characters: Some(vec![".".to_string()]),
15148 resolve_provider: Some(true),
15149 ..Default::default()
15150 }),
15151 ..Default::default()
15152 },
15153 cx,
15154 )
15155 .await;
15156
15157 cx.set_state("fn main() { let a = 2ˇ; }");
15158 cx.simulate_keystroke(".");
15159
15160 let item1 = lsp::CompletionItem {
15161 label: "method id()".to_string(),
15162 filter_text: Some("id".to_string()),
15163 detail: None,
15164 documentation: None,
15165 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15166 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15167 new_text: ".id".to_string(),
15168 })),
15169 ..lsp::CompletionItem::default()
15170 };
15171
15172 let item2 = lsp::CompletionItem {
15173 label: "other".to_string(),
15174 filter_text: Some("other".to_string()),
15175 detail: None,
15176 documentation: None,
15177 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15178 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15179 new_text: ".other".to_string(),
15180 })),
15181 ..lsp::CompletionItem::default()
15182 };
15183
15184 let item1 = item1.clone();
15185 cx.set_request_handler::<lsp::request::Completion, _, _>({
15186 let item1 = item1.clone();
15187 move |_, _, _| {
15188 let item1 = item1.clone();
15189 let item2 = item2.clone();
15190 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15191 }
15192 })
15193 .next()
15194 .await;
15195
15196 cx.condition(|editor, _| editor.context_menu_visible())
15197 .await;
15198 cx.update_editor(|editor, _, _| {
15199 let context_menu = editor.context_menu.borrow_mut();
15200 let context_menu = context_menu
15201 .as_ref()
15202 .expect("Should have the context menu deployed");
15203 match context_menu {
15204 CodeContextMenu::Completions(completions_menu) => {
15205 let completions = completions_menu.completions.borrow_mut();
15206 assert_eq!(
15207 completions
15208 .iter()
15209 .map(|completion| &completion.label.text)
15210 .collect::<Vec<_>>(),
15211 vec!["method id()", "other"]
15212 )
15213 }
15214 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15215 }
15216 });
15217
15218 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15219 let item1 = item1.clone();
15220 move |_, item_to_resolve, _| {
15221 let item1 = item1.clone();
15222 async move {
15223 if item1 == item_to_resolve {
15224 Ok(lsp::CompletionItem {
15225 label: "method id()".to_string(),
15226 filter_text: Some("id".to_string()),
15227 detail: Some("Now resolved!".to_string()),
15228 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15229 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15230 range: lsp::Range::new(
15231 lsp::Position::new(0, 22),
15232 lsp::Position::new(0, 22),
15233 ),
15234 new_text: ".id".to_string(),
15235 })),
15236 ..lsp::CompletionItem::default()
15237 })
15238 } else {
15239 Ok(item_to_resolve)
15240 }
15241 }
15242 }
15243 })
15244 .next()
15245 .await
15246 .unwrap();
15247 cx.run_until_parked();
15248
15249 cx.update_editor(|editor, window, cx| {
15250 editor.context_menu_next(&Default::default(), window, cx);
15251 });
15252
15253 cx.update_editor(|editor, _, _| {
15254 let context_menu = editor.context_menu.borrow_mut();
15255 let context_menu = context_menu
15256 .as_ref()
15257 .expect("Should have the context menu deployed");
15258 match context_menu {
15259 CodeContextMenu::Completions(completions_menu) => {
15260 let completions = completions_menu.completions.borrow_mut();
15261 assert_eq!(
15262 completions
15263 .iter()
15264 .map(|completion| &completion.label.text)
15265 .collect::<Vec<_>>(),
15266 vec!["method id() Now resolved!", "other"],
15267 "Should update first completion label, but not second as the filter text did not match."
15268 );
15269 }
15270 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15271 }
15272 });
15273}
15274
15275#[gpui::test]
15276async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15277 init_test(cx, |_| {});
15278 let mut cx = EditorLspTestContext::new_rust(
15279 lsp::ServerCapabilities {
15280 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15281 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15282 completion_provider: Some(lsp::CompletionOptions {
15283 resolve_provider: Some(true),
15284 ..Default::default()
15285 }),
15286 ..Default::default()
15287 },
15288 cx,
15289 )
15290 .await;
15291 cx.set_state(indoc! {"
15292 struct TestStruct {
15293 field: i32
15294 }
15295
15296 fn mainˇ() {
15297 let unused_var = 42;
15298 let test_struct = TestStruct { field: 42 };
15299 }
15300 "});
15301 let symbol_range = cx.lsp_range(indoc! {"
15302 struct TestStruct {
15303 field: i32
15304 }
15305
15306 «fn main»() {
15307 let unused_var = 42;
15308 let test_struct = TestStruct { field: 42 };
15309 }
15310 "});
15311 let mut hover_requests =
15312 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15313 Ok(Some(lsp::Hover {
15314 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15315 kind: lsp::MarkupKind::Markdown,
15316 value: "Function documentation".to_string(),
15317 }),
15318 range: Some(symbol_range),
15319 }))
15320 });
15321
15322 // Case 1: Test that code action menu hide hover popover
15323 cx.dispatch_action(Hover);
15324 hover_requests.next().await;
15325 cx.condition(|editor, _| editor.hover_state.visible()).await;
15326 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15327 move |_, _, _| async move {
15328 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15329 lsp::CodeAction {
15330 title: "Remove unused variable".to_string(),
15331 kind: Some(CodeActionKind::QUICKFIX),
15332 edit: Some(lsp::WorkspaceEdit {
15333 changes: Some(
15334 [(
15335 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15336 vec![lsp::TextEdit {
15337 range: lsp::Range::new(
15338 lsp::Position::new(5, 4),
15339 lsp::Position::new(5, 27),
15340 ),
15341 new_text: "".to_string(),
15342 }],
15343 )]
15344 .into_iter()
15345 .collect(),
15346 ),
15347 ..Default::default()
15348 }),
15349 ..Default::default()
15350 },
15351 )]))
15352 },
15353 );
15354 cx.update_editor(|editor, window, cx| {
15355 editor.toggle_code_actions(
15356 &ToggleCodeActions {
15357 deployed_from: None,
15358 quick_launch: false,
15359 },
15360 window,
15361 cx,
15362 );
15363 });
15364 code_action_requests.next().await;
15365 cx.run_until_parked();
15366 cx.condition(|editor, _| editor.context_menu_visible())
15367 .await;
15368 cx.update_editor(|editor, _, _| {
15369 assert!(
15370 !editor.hover_state.visible(),
15371 "Hover popover should be hidden when code action menu is shown"
15372 );
15373 // Hide code actions
15374 editor.context_menu.take();
15375 });
15376
15377 // Case 2: Test that code completions hide hover popover
15378 cx.dispatch_action(Hover);
15379 hover_requests.next().await;
15380 cx.condition(|editor, _| editor.hover_state.visible()).await;
15381 let counter = Arc::new(AtomicUsize::new(0));
15382 let mut completion_requests =
15383 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15384 let counter = counter.clone();
15385 async move {
15386 counter.fetch_add(1, atomic::Ordering::Release);
15387 Ok(Some(lsp::CompletionResponse::Array(vec![
15388 lsp::CompletionItem {
15389 label: "main".into(),
15390 kind: Some(lsp::CompletionItemKind::FUNCTION),
15391 detail: Some("() -> ()".to_string()),
15392 ..Default::default()
15393 },
15394 lsp::CompletionItem {
15395 label: "TestStruct".into(),
15396 kind: Some(lsp::CompletionItemKind::STRUCT),
15397 detail: Some("struct TestStruct".to_string()),
15398 ..Default::default()
15399 },
15400 ])))
15401 }
15402 });
15403 cx.update_editor(|editor, window, cx| {
15404 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15405 });
15406 completion_requests.next().await;
15407 cx.condition(|editor, _| editor.context_menu_visible())
15408 .await;
15409 cx.update_editor(|editor, _, _| {
15410 assert!(
15411 !editor.hover_state.visible(),
15412 "Hover popover should be hidden when completion menu is shown"
15413 );
15414 });
15415}
15416
15417#[gpui::test]
15418async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15419 init_test(cx, |_| {});
15420
15421 let mut cx = EditorLspTestContext::new_rust(
15422 lsp::ServerCapabilities {
15423 completion_provider: Some(lsp::CompletionOptions {
15424 trigger_characters: Some(vec![".".to_string()]),
15425 resolve_provider: Some(true),
15426 ..Default::default()
15427 }),
15428 ..Default::default()
15429 },
15430 cx,
15431 )
15432 .await;
15433
15434 cx.set_state("fn main() { let a = 2ˇ; }");
15435 cx.simulate_keystroke(".");
15436
15437 let unresolved_item_1 = lsp::CompletionItem {
15438 label: "id".to_string(),
15439 filter_text: Some("id".to_string()),
15440 detail: None,
15441 documentation: None,
15442 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15443 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15444 new_text: ".id".to_string(),
15445 })),
15446 ..lsp::CompletionItem::default()
15447 };
15448 let resolved_item_1 = lsp::CompletionItem {
15449 additional_text_edits: Some(vec![lsp::TextEdit {
15450 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15451 new_text: "!!".to_string(),
15452 }]),
15453 ..unresolved_item_1.clone()
15454 };
15455 let unresolved_item_2 = lsp::CompletionItem {
15456 label: "other".to_string(),
15457 filter_text: Some("other".to_string()),
15458 detail: None,
15459 documentation: None,
15460 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15461 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15462 new_text: ".other".to_string(),
15463 })),
15464 ..lsp::CompletionItem::default()
15465 };
15466 let resolved_item_2 = lsp::CompletionItem {
15467 additional_text_edits: Some(vec![lsp::TextEdit {
15468 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15469 new_text: "??".to_string(),
15470 }]),
15471 ..unresolved_item_2.clone()
15472 };
15473
15474 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15475 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15476 cx.lsp
15477 .server
15478 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15479 let unresolved_item_1 = unresolved_item_1.clone();
15480 let resolved_item_1 = resolved_item_1.clone();
15481 let unresolved_item_2 = unresolved_item_2.clone();
15482 let resolved_item_2 = resolved_item_2.clone();
15483 let resolve_requests_1 = resolve_requests_1.clone();
15484 let resolve_requests_2 = resolve_requests_2.clone();
15485 move |unresolved_request, _| {
15486 let unresolved_item_1 = unresolved_item_1.clone();
15487 let resolved_item_1 = resolved_item_1.clone();
15488 let unresolved_item_2 = unresolved_item_2.clone();
15489 let resolved_item_2 = resolved_item_2.clone();
15490 let resolve_requests_1 = resolve_requests_1.clone();
15491 let resolve_requests_2 = resolve_requests_2.clone();
15492 async move {
15493 if unresolved_request == unresolved_item_1 {
15494 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15495 Ok(resolved_item_1.clone())
15496 } else if unresolved_request == unresolved_item_2 {
15497 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15498 Ok(resolved_item_2.clone())
15499 } else {
15500 panic!("Unexpected completion item {unresolved_request:?}")
15501 }
15502 }
15503 }
15504 })
15505 .detach();
15506
15507 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15508 let unresolved_item_1 = unresolved_item_1.clone();
15509 let unresolved_item_2 = unresolved_item_2.clone();
15510 async move {
15511 Ok(Some(lsp::CompletionResponse::Array(vec![
15512 unresolved_item_1,
15513 unresolved_item_2,
15514 ])))
15515 }
15516 })
15517 .next()
15518 .await;
15519
15520 cx.condition(|editor, _| editor.context_menu_visible())
15521 .await;
15522 cx.update_editor(|editor, _, _| {
15523 let context_menu = editor.context_menu.borrow_mut();
15524 let context_menu = context_menu
15525 .as_ref()
15526 .expect("Should have the context menu deployed");
15527 match context_menu {
15528 CodeContextMenu::Completions(completions_menu) => {
15529 let completions = completions_menu.completions.borrow_mut();
15530 assert_eq!(
15531 completions
15532 .iter()
15533 .map(|completion| &completion.label.text)
15534 .collect::<Vec<_>>(),
15535 vec!["id", "other"]
15536 )
15537 }
15538 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15539 }
15540 });
15541 cx.run_until_parked();
15542
15543 cx.update_editor(|editor, window, cx| {
15544 editor.context_menu_next(&ContextMenuNext, window, cx);
15545 });
15546 cx.run_until_parked();
15547 cx.update_editor(|editor, window, cx| {
15548 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15549 });
15550 cx.run_until_parked();
15551 cx.update_editor(|editor, window, cx| {
15552 editor.context_menu_next(&ContextMenuNext, window, cx);
15553 });
15554 cx.run_until_parked();
15555 cx.update_editor(|editor, window, cx| {
15556 editor
15557 .compose_completion(&ComposeCompletion::default(), window, cx)
15558 .expect("No task returned")
15559 })
15560 .await
15561 .expect("Completion failed");
15562 cx.run_until_parked();
15563
15564 cx.update_editor(|editor, _, cx| {
15565 assert_eq!(
15566 resolve_requests_1.load(atomic::Ordering::Acquire),
15567 1,
15568 "Should always resolve once despite multiple selections"
15569 );
15570 assert_eq!(
15571 resolve_requests_2.load(atomic::Ordering::Acquire),
15572 1,
15573 "Should always resolve once after multiple selections and applying the completion"
15574 );
15575 assert_eq!(
15576 editor.text(cx),
15577 "fn main() { let a = ??.other; }",
15578 "Should use resolved data when applying the completion"
15579 );
15580 });
15581}
15582
15583#[gpui::test]
15584async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15585 init_test(cx, |_| {});
15586
15587 let item_0 = lsp::CompletionItem {
15588 label: "abs".into(),
15589 insert_text: Some("abs".into()),
15590 data: Some(json!({ "very": "special"})),
15591 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15592 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15593 lsp::InsertReplaceEdit {
15594 new_text: "abs".to_string(),
15595 insert: lsp::Range::default(),
15596 replace: lsp::Range::default(),
15597 },
15598 )),
15599 ..lsp::CompletionItem::default()
15600 };
15601 let items = iter::once(item_0.clone())
15602 .chain((11..51).map(|i| lsp::CompletionItem {
15603 label: format!("item_{}", i),
15604 insert_text: Some(format!("item_{}", i)),
15605 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15606 ..lsp::CompletionItem::default()
15607 }))
15608 .collect::<Vec<_>>();
15609
15610 let default_commit_characters = vec!["?".to_string()];
15611 let default_data = json!({ "default": "data"});
15612 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15613 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15614 let default_edit_range = lsp::Range {
15615 start: lsp::Position {
15616 line: 0,
15617 character: 5,
15618 },
15619 end: lsp::Position {
15620 line: 0,
15621 character: 5,
15622 },
15623 };
15624
15625 let mut cx = EditorLspTestContext::new_rust(
15626 lsp::ServerCapabilities {
15627 completion_provider: Some(lsp::CompletionOptions {
15628 trigger_characters: Some(vec![".".to_string()]),
15629 resolve_provider: Some(true),
15630 ..Default::default()
15631 }),
15632 ..Default::default()
15633 },
15634 cx,
15635 )
15636 .await;
15637
15638 cx.set_state("fn main() { let a = 2ˇ; }");
15639 cx.simulate_keystroke(".");
15640
15641 let completion_data = default_data.clone();
15642 let completion_characters = default_commit_characters.clone();
15643 let completion_items = items.clone();
15644 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15645 let default_data = completion_data.clone();
15646 let default_commit_characters = completion_characters.clone();
15647 let items = completion_items.clone();
15648 async move {
15649 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15650 items,
15651 item_defaults: Some(lsp::CompletionListItemDefaults {
15652 data: Some(default_data.clone()),
15653 commit_characters: Some(default_commit_characters.clone()),
15654 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15655 default_edit_range,
15656 )),
15657 insert_text_format: Some(default_insert_text_format),
15658 insert_text_mode: Some(default_insert_text_mode),
15659 }),
15660 ..lsp::CompletionList::default()
15661 })))
15662 }
15663 })
15664 .next()
15665 .await;
15666
15667 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15668 cx.lsp
15669 .server
15670 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15671 let closure_resolved_items = resolved_items.clone();
15672 move |item_to_resolve, _| {
15673 let closure_resolved_items = closure_resolved_items.clone();
15674 async move {
15675 closure_resolved_items.lock().push(item_to_resolve.clone());
15676 Ok(item_to_resolve)
15677 }
15678 }
15679 })
15680 .detach();
15681
15682 cx.condition(|editor, _| editor.context_menu_visible())
15683 .await;
15684 cx.run_until_parked();
15685 cx.update_editor(|editor, _, _| {
15686 let menu = editor.context_menu.borrow_mut();
15687 match menu.as_ref().expect("should have the completions menu") {
15688 CodeContextMenu::Completions(completions_menu) => {
15689 assert_eq!(
15690 completions_menu
15691 .entries
15692 .borrow()
15693 .iter()
15694 .map(|mat| mat.string.clone())
15695 .collect::<Vec<String>>(),
15696 items
15697 .iter()
15698 .map(|completion| completion.label.clone())
15699 .collect::<Vec<String>>()
15700 );
15701 }
15702 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15703 }
15704 });
15705 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15706 // with 4 from the end.
15707 assert_eq!(
15708 *resolved_items.lock(),
15709 [&items[0..16], &items[items.len() - 4..items.len()]]
15710 .concat()
15711 .iter()
15712 .cloned()
15713 .map(|mut item| {
15714 if item.data.is_none() {
15715 item.data = Some(default_data.clone());
15716 }
15717 item
15718 })
15719 .collect::<Vec<lsp::CompletionItem>>(),
15720 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15721 );
15722 resolved_items.lock().clear();
15723
15724 cx.update_editor(|editor, window, cx| {
15725 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15726 });
15727 cx.run_until_parked();
15728 // Completions that have already been resolved are skipped.
15729 assert_eq!(
15730 *resolved_items.lock(),
15731 items[items.len() - 17..items.len() - 4]
15732 .iter()
15733 .cloned()
15734 .map(|mut item| {
15735 if item.data.is_none() {
15736 item.data = Some(default_data.clone());
15737 }
15738 item
15739 })
15740 .collect::<Vec<lsp::CompletionItem>>()
15741 );
15742 resolved_items.lock().clear();
15743}
15744
15745#[gpui::test]
15746async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15747 init_test(cx, |_| {});
15748
15749 let mut cx = EditorLspTestContext::new(
15750 Language::new(
15751 LanguageConfig {
15752 matcher: LanguageMatcher {
15753 path_suffixes: vec!["jsx".into()],
15754 ..Default::default()
15755 },
15756 overrides: [(
15757 "element".into(),
15758 LanguageConfigOverride {
15759 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15760 ..Default::default()
15761 },
15762 )]
15763 .into_iter()
15764 .collect(),
15765 ..Default::default()
15766 },
15767 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15768 )
15769 .with_override_query("(jsx_self_closing_element) @element")
15770 .unwrap(),
15771 lsp::ServerCapabilities {
15772 completion_provider: Some(lsp::CompletionOptions {
15773 trigger_characters: Some(vec![":".to_string()]),
15774 ..Default::default()
15775 }),
15776 ..Default::default()
15777 },
15778 cx,
15779 )
15780 .await;
15781
15782 cx.lsp
15783 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15784 Ok(Some(lsp::CompletionResponse::Array(vec![
15785 lsp::CompletionItem {
15786 label: "bg-blue".into(),
15787 ..Default::default()
15788 },
15789 lsp::CompletionItem {
15790 label: "bg-red".into(),
15791 ..Default::default()
15792 },
15793 lsp::CompletionItem {
15794 label: "bg-yellow".into(),
15795 ..Default::default()
15796 },
15797 ])))
15798 });
15799
15800 cx.set_state(r#"<p class="bgˇ" />"#);
15801
15802 // Trigger completion when typing a dash, because the dash is an extra
15803 // word character in the 'element' scope, which contains the cursor.
15804 cx.simulate_keystroke("-");
15805 cx.executor().run_until_parked();
15806 cx.update_editor(|editor, _, _| {
15807 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15808 {
15809 assert_eq!(
15810 completion_menu_entries(&menu),
15811 &["bg-blue", "bg-red", "bg-yellow"]
15812 );
15813 } else {
15814 panic!("expected completion menu to be open");
15815 }
15816 });
15817
15818 cx.simulate_keystroke("l");
15819 cx.executor().run_until_parked();
15820 cx.update_editor(|editor, _, _| {
15821 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15822 {
15823 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15824 } else {
15825 panic!("expected completion menu to be open");
15826 }
15827 });
15828
15829 // When filtering completions, consider the character after the '-' to
15830 // be the start of a subword.
15831 cx.set_state(r#"<p class="yelˇ" />"#);
15832 cx.simulate_keystroke("l");
15833 cx.executor().run_until_parked();
15834 cx.update_editor(|editor, _, _| {
15835 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15836 {
15837 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15838 } else {
15839 panic!("expected completion menu to be open");
15840 }
15841 });
15842}
15843
15844fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
15845 let entries = menu.entries.borrow();
15846 entries.iter().map(|mat| mat.string.clone()).collect()
15847}
15848
15849#[gpui::test]
15850async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
15851 init_test(cx, |settings| {
15852 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
15853 FormatterList(vec![Formatter::Prettier].into()),
15854 ))
15855 });
15856
15857 let fs = FakeFs::new(cx.executor());
15858 fs.insert_file(path!("/file.ts"), Default::default()).await;
15859
15860 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
15861 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15862
15863 language_registry.add(Arc::new(Language::new(
15864 LanguageConfig {
15865 name: "TypeScript".into(),
15866 matcher: LanguageMatcher {
15867 path_suffixes: vec!["ts".to_string()],
15868 ..Default::default()
15869 },
15870 ..Default::default()
15871 },
15872 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15873 )));
15874 update_test_language_settings(cx, |settings| {
15875 settings.defaults.prettier = Some(PrettierSettings {
15876 allowed: true,
15877 ..PrettierSettings::default()
15878 });
15879 });
15880
15881 let test_plugin = "test_plugin";
15882 let _ = language_registry.register_fake_lsp(
15883 "TypeScript",
15884 FakeLspAdapter {
15885 prettier_plugins: vec![test_plugin],
15886 ..Default::default()
15887 },
15888 );
15889
15890 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
15891 let buffer = project
15892 .update(cx, |project, cx| {
15893 project.open_local_buffer(path!("/file.ts"), cx)
15894 })
15895 .await
15896 .unwrap();
15897
15898 let buffer_text = "one\ntwo\nthree\n";
15899 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15900 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
15901 editor.update_in(cx, |editor, window, cx| {
15902 editor.set_text(buffer_text, window, cx)
15903 });
15904
15905 editor
15906 .update_in(cx, |editor, window, cx| {
15907 editor.perform_format(
15908 project.clone(),
15909 FormatTrigger::Manual,
15910 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15911 window,
15912 cx,
15913 )
15914 })
15915 .unwrap()
15916 .await;
15917 assert_eq!(
15918 editor.update(cx, |editor, cx| editor.text(cx)),
15919 buffer_text.to_string() + prettier_format_suffix,
15920 "Test prettier formatting was not applied to the original buffer text",
15921 );
15922
15923 update_test_language_settings(cx, |settings| {
15924 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
15925 });
15926 let format = editor.update_in(cx, |editor, window, cx| {
15927 editor.perform_format(
15928 project.clone(),
15929 FormatTrigger::Manual,
15930 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15931 window,
15932 cx,
15933 )
15934 });
15935 format.await.unwrap();
15936 assert_eq!(
15937 editor.update(cx, |editor, cx| editor.text(cx)),
15938 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
15939 "Autoformatting (via test prettier) was not applied to the original buffer text",
15940 );
15941}
15942
15943#[gpui::test]
15944async fn test_addition_reverts(cx: &mut TestAppContext) {
15945 init_test(cx, |_| {});
15946 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15947 let base_text = indoc! {r#"
15948 struct Row;
15949 struct Row1;
15950 struct Row2;
15951
15952 struct Row4;
15953 struct Row5;
15954 struct Row6;
15955
15956 struct Row8;
15957 struct Row9;
15958 struct Row10;"#};
15959
15960 // When addition hunks are not adjacent to carets, no hunk revert is performed
15961 assert_hunk_revert(
15962 indoc! {r#"struct Row;
15963 struct Row1;
15964 struct Row1.1;
15965 struct Row1.2;
15966 struct Row2;ˇ
15967
15968 struct Row4;
15969 struct Row5;
15970 struct Row6;
15971
15972 struct Row8;
15973 ˇstruct Row9;
15974 struct Row9.1;
15975 struct Row9.2;
15976 struct Row9.3;
15977 struct Row10;"#},
15978 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
15979 indoc! {r#"struct Row;
15980 struct Row1;
15981 struct Row1.1;
15982 struct Row1.2;
15983 struct Row2;ˇ
15984
15985 struct Row4;
15986 struct Row5;
15987 struct Row6;
15988
15989 struct Row8;
15990 ˇstruct Row9;
15991 struct Row9.1;
15992 struct Row9.2;
15993 struct Row9.3;
15994 struct Row10;"#},
15995 base_text,
15996 &mut cx,
15997 );
15998 // Same for selections
15999 assert_hunk_revert(
16000 indoc! {r#"struct Row;
16001 struct Row1;
16002 struct Row2;
16003 struct Row2.1;
16004 struct Row2.2;
16005 «ˇ
16006 struct Row4;
16007 struct» Row5;
16008 «struct Row6;
16009 ˇ»
16010 struct Row9.1;
16011 struct Row9.2;
16012 struct Row9.3;
16013 struct Row8;
16014 struct Row9;
16015 struct Row10;"#},
16016 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16017 indoc! {r#"struct Row;
16018 struct Row1;
16019 struct Row2;
16020 struct Row2.1;
16021 struct Row2.2;
16022 «ˇ
16023 struct Row4;
16024 struct» Row5;
16025 «struct Row6;
16026 ˇ»
16027 struct Row9.1;
16028 struct Row9.2;
16029 struct Row9.3;
16030 struct Row8;
16031 struct Row9;
16032 struct Row10;"#},
16033 base_text,
16034 &mut cx,
16035 );
16036
16037 // When carets and selections intersect the addition hunks, those are reverted.
16038 // Adjacent carets got merged.
16039 assert_hunk_revert(
16040 indoc! {r#"struct Row;
16041 ˇ// something on the top
16042 struct Row1;
16043 struct Row2;
16044 struct Roˇw3.1;
16045 struct Row2.2;
16046 struct Row2.3;ˇ
16047
16048 struct Row4;
16049 struct ˇRow5.1;
16050 struct Row5.2;
16051 struct «Rowˇ»5.3;
16052 struct Row5;
16053 struct Row6;
16054 ˇ
16055 struct Row9.1;
16056 struct «Rowˇ»9.2;
16057 struct «ˇRow»9.3;
16058 struct Row8;
16059 struct Row9;
16060 «ˇ// something on bottom»
16061 struct Row10;"#},
16062 vec![
16063 DiffHunkStatusKind::Added,
16064 DiffHunkStatusKind::Added,
16065 DiffHunkStatusKind::Added,
16066 DiffHunkStatusKind::Added,
16067 DiffHunkStatusKind::Added,
16068 ],
16069 indoc! {r#"struct Row;
16070 ˇstruct Row1;
16071 struct Row2;
16072 ˇ
16073 struct Row4;
16074 ˇstruct Row5;
16075 struct Row6;
16076 ˇ
16077 ˇstruct Row8;
16078 struct Row9;
16079 ˇstruct Row10;"#},
16080 base_text,
16081 &mut cx,
16082 );
16083}
16084
16085#[gpui::test]
16086async fn test_modification_reverts(cx: &mut TestAppContext) {
16087 init_test(cx, |_| {});
16088 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16089 let base_text = indoc! {r#"
16090 struct Row;
16091 struct Row1;
16092 struct Row2;
16093
16094 struct Row4;
16095 struct Row5;
16096 struct Row6;
16097
16098 struct Row8;
16099 struct Row9;
16100 struct Row10;"#};
16101
16102 // Modification hunks behave the same as the addition ones.
16103 assert_hunk_revert(
16104 indoc! {r#"struct Row;
16105 struct Row1;
16106 struct Row33;
16107 ˇ
16108 struct Row4;
16109 struct Row5;
16110 struct Row6;
16111 ˇ
16112 struct Row99;
16113 struct Row9;
16114 struct Row10;"#},
16115 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16116 indoc! {r#"struct Row;
16117 struct Row1;
16118 struct Row33;
16119 ˇ
16120 struct Row4;
16121 struct Row5;
16122 struct Row6;
16123 ˇ
16124 struct Row99;
16125 struct Row9;
16126 struct Row10;"#},
16127 base_text,
16128 &mut cx,
16129 );
16130 assert_hunk_revert(
16131 indoc! {r#"struct Row;
16132 struct Row1;
16133 struct Row33;
16134 «ˇ
16135 struct Row4;
16136 struct» Row5;
16137 «struct Row6;
16138 ˇ»
16139 struct Row99;
16140 struct Row9;
16141 struct Row10;"#},
16142 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16143 indoc! {r#"struct Row;
16144 struct Row1;
16145 struct Row33;
16146 «ˇ
16147 struct Row4;
16148 struct» Row5;
16149 «struct Row6;
16150 ˇ»
16151 struct Row99;
16152 struct Row9;
16153 struct Row10;"#},
16154 base_text,
16155 &mut cx,
16156 );
16157
16158 assert_hunk_revert(
16159 indoc! {r#"ˇstruct Row1.1;
16160 struct Row1;
16161 «ˇstr»uct Row22;
16162
16163 struct ˇRow44;
16164 struct Row5;
16165 struct «Rˇ»ow66;ˇ
16166
16167 «struˇ»ct Row88;
16168 struct Row9;
16169 struct Row1011;ˇ"#},
16170 vec![
16171 DiffHunkStatusKind::Modified,
16172 DiffHunkStatusKind::Modified,
16173 DiffHunkStatusKind::Modified,
16174 DiffHunkStatusKind::Modified,
16175 DiffHunkStatusKind::Modified,
16176 DiffHunkStatusKind::Modified,
16177 ],
16178 indoc! {r#"struct Row;
16179 ˇstruct Row1;
16180 struct Row2;
16181 ˇ
16182 struct Row4;
16183 ˇstruct Row5;
16184 struct Row6;
16185 ˇ
16186 struct Row8;
16187 ˇstruct Row9;
16188 struct Row10;ˇ"#},
16189 base_text,
16190 &mut cx,
16191 );
16192}
16193
16194#[gpui::test]
16195async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16196 init_test(cx, |_| {});
16197 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16198 let base_text = indoc! {r#"
16199 one
16200
16201 two
16202 three
16203 "#};
16204
16205 cx.set_head_text(base_text);
16206 cx.set_state("\nˇ\n");
16207 cx.executor().run_until_parked();
16208 cx.update_editor(|editor, _window, cx| {
16209 editor.expand_selected_diff_hunks(cx);
16210 });
16211 cx.executor().run_until_parked();
16212 cx.update_editor(|editor, window, cx| {
16213 editor.backspace(&Default::default(), window, cx);
16214 });
16215 cx.run_until_parked();
16216 cx.assert_state_with_diff(
16217 indoc! {r#"
16218
16219 - two
16220 - threeˇ
16221 +
16222 "#}
16223 .to_string(),
16224 );
16225}
16226
16227#[gpui::test]
16228async fn test_deletion_reverts(cx: &mut TestAppContext) {
16229 init_test(cx, |_| {});
16230 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16231 let base_text = indoc! {r#"struct Row;
16232struct Row1;
16233struct Row2;
16234
16235struct Row4;
16236struct Row5;
16237struct Row6;
16238
16239struct Row8;
16240struct Row9;
16241struct Row10;"#};
16242
16243 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16244 assert_hunk_revert(
16245 indoc! {r#"struct Row;
16246 struct Row2;
16247
16248 ˇstruct Row4;
16249 struct Row5;
16250 struct Row6;
16251 ˇ
16252 struct Row8;
16253 struct Row10;"#},
16254 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16255 indoc! {r#"struct Row;
16256 struct Row2;
16257
16258 ˇstruct Row4;
16259 struct Row5;
16260 struct Row6;
16261 ˇ
16262 struct Row8;
16263 struct Row10;"#},
16264 base_text,
16265 &mut cx,
16266 );
16267 assert_hunk_revert(
16268 indoc! {r#"struct Row;
16269 struct Row2;
16270
16271 «ˇstruct Row4;
16272 struct» Row5;
16273 «struct Row6;
16274 ˇ»
16275 struct Row8;
16276 struct Row10;"#},
16277 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16278 indoc! {r#"struct Row;
16279 struct Row2;
16280
16281 «ˇstruct Row4;
16282 struct» Row5;
16283 «struct Row6;
16284 ˇ»
16285 struct Row8;
16286 struct Row10;"#},
16287 base_text,
16288 &mut cx,
16289 );
16290
16291 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16292 assert_hunk_revert(
16293 indoc! {r#"struct Row;
16294 ˇstruct Row2;
16295
16296 struct Row4;
16297 struct Row5;
16298 struct Row6;
16299
16300 struct Row8;ˇ
16301 struct Row10;"#},
16302 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16303 indoc! {r#"struct Row;
16304 struct Row1;
16305 ˇstruct Row2;
16306
16307 struct Row4;
16308 struct Row5;
16309 struct Row6;
16310
16311 struct Row8;ˇ
16312 struct Row9;
16313 struct Row10;"#},
16314 base_text,
16315 &mut cx,
16316 );
16317 assert_hunk_revert(
16318 indoc! {r#"struct Row;
16319 struct Row2«ˇ;
16320 struct Row4;
16321 struct» Row5;
16322 «struct Row6;
16323
16324 struct Row8;ˇ»
16325 struct Row10;"#},
16326 vec![
16327 DiffHunkStatusKind::Deleted,
16328 DiffHunkStatusKind::Deleted,
16329 DiffHunkStatusKind::Deleted,
16330 ],
16331 indoc! {r#"struct Row;
16332 struct Row1;
16333 struct Row2«ˇ;
16334
16335 struct Row4;
16336 struct» Row5;
16337 «struct Row6;
16338
16339 struct Row8;ˇ»
16340 struct Row9;
16341 struct Row10;"#},
16342 base_text,
16343 &mut cx,
16344 );
16345}
16346
16347#[gpui::test]
16348async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16349 init_test(cx, |_| {});
16350
16351 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16352 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16353 let base_text_3 =
16354 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16355
16356 let text_1 = edit_first_char_of_every_line(base_text_1);
16357 let text_2 = edit_first_char_of_every_line(base_text_2);
16358 let text_3 = edit_first_char_of_every_line(base_text_3);
16359
16360 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16361 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16362 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16363
16364 let multibuffer = cx.new(|cx| {
16365 let mut multibuffer = MultiBuffer::new(ReadWrite);
16366 multibuffer.push_excerpts(
16367 buffer_1.clone(),
16368 [
16369 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16370 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16371 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16372 ],
16373 cx,
16374 );
16375 multibuffer.push_excerpts(
16376 buffer_2.clone(),
16377 [
16378 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16379 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16380 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16381 ],
16382 cx,
16383 );
16384 multibuffer.push_excerpts(
16385 buffer_3.clone(),
16386 [
16387 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16388 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16389 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16390 ],
16391 cx,
16392 );
16393 multibuffer
16394 });
16395
16396 let fs = FakeFs::new(cx.executor());
16397 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16398 let (editor, cx) = cx
16399 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16400 editor.update_in(cx, |editor, _window, cx| {
16401 for (buffer, diff_base) in [
16402 (buffer_1.clone(), base_text_1),
16403 (buffer_2.clone(), base_text_2),
16404 (buffer_3.clone(), base_text_3),
16405 ] {
16406 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16407 editor
16408 .buffer
16409 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16410 }
16411 });
16412 cx.executor().run_until_parked();
16413
16414 editor.update_in(cx, |editor, window, cx| {
16415 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}");
16416 editor.select_all(&SelectAll, window, cx);
16417 editor.git_restore(&Default::default(), window, cx);
16418 });
16419 cx.executor().run_until_parked();
16420
16421 // When all ranges are selected, all buffer hunks are reverted.
16422 editor.update(cx, |editor, cx| {
16423 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");
16424 });
16425 buffer_1.update(cx, |buffer, _| {
16426 assert_eq!(buffer.text(), base_text_1);
16427 });
16428 buffer_2.update(cx, |buffer, _| {
16429 assert_eq!(buffer.text(), base_text_2);
16430 });
16431 buffer_3.update(cx, |buffer, _| {
16432 assert_eq!(buffer.text(), base_text_3);
16433 });
16434
16435 editor.update_in(cx, |editor, window, cx| {
16436 editor.undo(&Default::default(), window, cx);
16437 });
16438
16439 editor.update_in(cx, |editor, window, cx| {
16440 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16441 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16442 });
16443 editor.git_restore(&Default::default(), window, cx);
16444 });
16445
16446 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16447 // but not affect buffer_2 and its related excerpts.
16448 editor.update(cx, |editor, cx| {
16449 assert_eq!(
16450 editor.text(cx),
16451 "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}"
16452 );
16453 });
16454 buffer_1.update(cx, |buffer, _| {
16455 assert_eq!(buffer.text(), base_text_1);
16456 });
16457 buffer_2.update(cx, |buffer, _| {
16458 assert_eq!(
16459 buffer.text(),
16460 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16461 );
16462 });
16463 buffer_3.update(cx, |buffer, _| {
16464 assert_eq!(
16465 buffer.text(),
16466 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16467 );
16468 });
16469
16470 fn edit_first_char_of_every_line(text: &str) -> String {
16471 text.split('\n')
16472 .map(|line| format!("X{}", &line[1..]))
16473 .collect::<Vec<_>>()
16474 .join("\n")
16475 }
16476}
16477
16478#[gpui::test]
16479async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16480 init_test(cx, |_| {});
16481
16482 let cols = 4;
16483 let rows = 10;
16484 let sample_text_1 = sample_text(rows, cols, 'a');
16485 assert_eq!(
16486 sample_text_1,
16487 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16488 );
16489 let sample_text_2 = sample_text(rows, cols, 'l');
16490 assert_eq!(
16491 sample_text_2,
16492 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16493 );
16494 let sample_text_3 = sample_text(rows, cols, 'v');
16495 assert_eq!(
16496 sample_text_3,
16497 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16498 );
16499
16500 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16501 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16502 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16503
16504 let multi_buffer = cx.new(|cx| {
16505 let mut multibuffer = MultiBuffer::new(ReadWrite);
16506 multibuffer.push_excerpts(
16507 buffer_1.clone(),
16508 [
16509 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16510 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16511 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16512 ],
16513 cx,
16514 );
16515 multibuffer.push_excerpts(
16516 buffer_2.clone(),
16517 [
16518 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16519 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16520 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16521 ],
16522 cx,
16523 );
16524 multibuffer.push_excerpts(
16525 buffer_3.clone(),
16526 [
16527 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16528 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16529 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16530 ],
16531 cx,
16532 );
16533 multibuffer
16534 });
16535
16536 let fs = FakeFs::new(cx.executor());
16537 fs.insert_tree(
16538 "/a",
16539 json!({
16540 "main.rs": sample_text_1,
16541 "other.rs": sample_text_2,
16542 "lib.rs": sample_text_3,
16543 }),
16544 )
16545 .await;
16546 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16547 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16548 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16549 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16550 Editor::new(
16551 EditorMode::full(),
16552 multi_buffer,
16553 Some(project.clone()),
16554 window,
16555 cx,
16556 )
16557 });
16558 let multibuffer_item_id = workspace
16559 .update(cx, |workspace, window, cx| {
16560 assert!(
16561 workspace.active_item(cx).is_none(),
16562 "active item should be None before the first item is added"
16563 );
16564 workspace.add_item_to_active_pane(
16565 Box::new(multi_buffer_editor.clone()),
16566 None,
16567 true,
16568 window,
16569 cx,
16570 );
16571 let active_item = workspace
16572 .active_item(cx)
16573 .expect("should have an active item after adding the multi buffer");
16574 assert!(
16575 !active_item.is_singleton(cx),
16576 "A multi buffer was expected to active after adding"
16577 );
16578 active_item.item_id()
16579 })
16580 .unwrap();
16581 cx.executor().run_until_parked();
16582
16583 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16584 editor.change_selections(
16585 SelectionEffects::scroll(Autoscroll::Next),
16586 window,
16587 cx,
16588 |s| s.select_ranges(Some(1..2)),
16589 );
16590 editor.open_excerpts(&OpenExcerpts, window, cx);
16591 });
16592 cx.executor().run_until_parked();
16593 let first_item_id = workspace
16594 .update(cx, |workspace, window, cx| {
16595 let active_item = workspace
16596 .active_item(cx)
16597 .expect("should have an active item after navigating into the 1st buffer");
16598 let first_item_id = active_item.item_id();
16599 assert_ne!(
16600 first_item_id, multibuffer_item_id,
16601 "Should navigate into the 1st buffer and activate it"
16602 );
16603 assert!(
16604 active_item.is_singleton(cx),
16605 "New active item should be a singleton buffer"
16606 );
16607 assert_eq!(
16608 active_item
16609 .act_as::<Editor>(cx)
16610 .expect("should have navigated into an editor for the 1st buffer")
16611 .read(cx)
16612 .text(cx),
16613 sample_text_1
16614 );
16615
16616 workspace
16617 .go_back(workspace.active_pane().downgrade(), window, cx)
16618 .detach_and_log_err(cx);
16619
16620 first_item_id
16621 })
16622 .unwrap();
16623 cx.executor().run_until_parked();
16624 workspace
16625 .update(cx, |workspace, _, cx| {
16626 let active_item = workspace
16627 .active_item(cx)
16628 .expect("should have an active item after navigating back");
16629 assert_eq!(
16630 active_item.item_id(),
16631 multibuffer_item_id,
16632 "Should navigate back to the multi buffer"
16633 );
16634 assert!(!active_item.is_singleton(cx));
16635 })
16636 .unwrap();
16637
16638 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16639 editor.change_selections(
16640 SelectionEffects::scroll(Autoscroll::Next),
16641 window,
16642 cx,
16643 |s| s.select_ranges(Some(39..40)),
16644 );
16645 editor.open_excerpts(&OpenExcerpts, window, cx);
16646 });
16647 cx.executor().run_until_parked();
16648 let second_item_id = workspace
16649 .update(cx, |workspace, window, cx| {
16650 let active_item = workspace
16651 .active_item(cx)
16652 .expect("should have an active item after navigating into the 2nd buffer");
16653 let second_item_id = active_item.item_id();
16654 assert_ne!(
16655 second_item_id, multibuffer_item_id,
16656 "Should navigate away from the multibuffer"
16657 );
16658 assert_ne!(
16659 second_item_id, first_item_id,
16660 "Should navigate into the 2nd buffer and activate it"
16661 );
16662 assert!(
16663 active_item.is_singleton(cx),
16664 "New active item should be a singleton buffer"
16665 );
16666 assert_eq!(
16667 active_item
16668 .act_as::<Editor>(cx)
16669 .expect("should have navigated into an editor")
16670 .read(cx)
16671 .text(cx),
16672 sample_text_2
16673 );
16674
16675 workspace
16676 .go_back(workspace.active_pane().downgrade(), window, cx)
16677 .detach_and_log_err(cx);
16678
16679 second_item_id
16680 })
16681 .unwrap();
16682 cx.executor().run_until_parked();
16683 workspace
16684 .update(cx, |workspace, _, cx| {
16685 let active_item = workspace
16686 .active_item(cx)
16687 .expect("should have an active item after navigating back from the 2nd buffer");
16688 assert_eq!(
16689 active_item.item_id(),
16690 multibuffer_item_id,
16691 "Should navigate back from the 2nd buffer to the multi buffer"
16692 );
16693 assert!(!active_item.is_singleton(cx));
16694 })
16695 .unwrap();
16696
16697 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16698 editor.change_selections(
16699 SelectionEffects::scroll(Autoscroll::Next),
16700 window,
16701 cx,
16702 |s| s.select_ranges(Some(70..70)),
16703 );
16704 editor.open_excerpts(&OpenExcerpts, window, cx);
16705 });
16706 cx.executor().run_until_parked();
16707 workspace
16708 .update(cx, |workspace, window, cx| {
16709 let active_item = workspace
16710 .active_item(cx)
16711 .expect("should have an active item after navigating into the 3rd buffer");
16712 let third_item_id = active_item.item_id();
16713 assert_ne!(
16714 third_item_id, multibuffer_item_id,
16715 "Should navigate into the 3rd buffer and activate it"
16716 );
16717 assert_ne!(third_item_id, first_item_id);
16718 assert_ne!(third_item_id, second_item_id);
16719 assert!(
16720 active_item.is_singleton(cx),
16721 "New active item should be a singleton buffer"
16722 );
16723 assert_eq!(
16724 active_item
16725 .act_as::<Editor>(cx)
16726 .expect("should have navigated into an editor")
16727 .read(cx)
16728 .text(cx),
16729 sample_text_3
16730 );
16731
16732 workspace
16733 .go_back(workspace.active_pane().downgrade(), window, cx)
16734 .detach_and_log_err(cx);
16735 })
16736 .unwrap();
16737 cx.executor().run_until_parked();
16738 workspace
16739 .update(cx, |workspace, _, cx| {
16740 let active_item = workspace
16741 .active_item(cx)
16742 .expect("should have an active item after navigating back from the 3rd buffer");
16743 assert_eq!(
16744 active_item.item_id(),
16745 multibuffer_item_id,
16746 "Should navigate back from the 3rd buffer to the multi buffer"
16747 );
16748 assert!(!active_item.is_singleton(cx));
16749 })
16750 .unwrap();
16751}
16752
16753#[gpui::test]
16754async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16755 init_test(cx, |_| {});
16756
16757 let mut cx = EditorTestContext::new(cx).await;
16758
16759 let diff_base = r#"
16760 use some::mod;
16761
16762 const A: u32 = 42;
16763
16764 fn main() {
16765 println!("hello");
16766
16767 println!("world");
16768 }
16769 "#
16770 .unindent();
16771
16772 cx.set_state(
16773 &r#"
16774 use some::modified;
16775
16776 ˇ
16777 fn main() {
16778 println!("hello there");
16779
16780 println!("around the");
16781 println!("world");
16782 }
16783 "#
16784 .unindent(),
16785 );
16786
16787 cx.set_head_text(&diff_base);
16788 executor.run_until_parked();
16789
16790 cx.update_editor(|editor, window, cx| {
16791 editor.go_to_next_hunk(&GoToHunk, window, cx);
16792 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16793 });
16794 executor.run_until_parked();
16795 cx.assert_state_with_diff(
16796 r#"
16797 use some::modified;
16798
16799
16800 fn main() {
16801 - println!("hello");
16802 + ˇ println!("hello there");
16803
16804 println!("around the");
16805 println!("world");
16806 }
16807 "#
16808 .unindent(),
16809 );
16810
16811 cx.update_editor(|editor, window, cx| {
16812 for _ in 0..2 {
16813 editor.go_to_next_hunk(&GoToHunk, window, cx);
16814 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16815 }
16816 });
16817 executor.run_until_parked();
16818 cx.assert_state_with_diff(
16819 r#"
16820 - use some::mod;
16821 + ˇuse some::modified;
16822
16823
16824 fn main() {
16825 - println!("hello");
16826 + println!("hello there");
16827
16828 + println!("around the");
16829 println!("world");
16830 }
16831 "#
16832 .unindent(),
16833 );
16834
16835 cx.update_editor(|editor, window, cx| {
16836 editor.go_to_next_hunk(&GoToHunk, window, cx);
16837 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16838 });
16839 executor.run_until_parked();
16840 cx.assert_state_with_diff(
16841 r#"
16842 - use some::mod;
16843 + use some::modified;
16844
16845 - const A: u32 = 42;
16846 ˇ
16847 fn main() {
16848 - println!("hello");
16849 + println!("hello there");
16850
16851 + println!("around the");
16852 println!("world");
16853 }
16854 "#
16855 .unindent(),
16856 );
16857
16858 cx.update_editor(|editor, window, cx| {
16859 editor.cancel(&Cancel, window, cx);
16860 });
16861
16862 cx.assert_state_with_diff(
16863 r#"
16864 use some::modified;
16865
16866 ˇ
16867 fn main() {
16868 println!("hello there");
16869
16870 println!("around the");
16871 println!("world");
16872 }
16873 "#
16874 .unindent(),
16875 );
16876}
16877
16878#[gpui::test]
16879async fn test_diff_base_change_with_expanded_diff_hunks(
16880 executor: BackgroundExecutor,
16881 cx: &mut TestAppContext,
16882) {
16883 init_test(cx, |_| {});
16884
16885 let mut cx = EditorTestContext::new(cx).await;
16886
16887 let diff_base = r#"
16888 use some::mod1;
16889 use some::mod2;
16890
16891 const A: u32 = 42;
16892 const B: u32 = 42;
16893 const C: u32 = 42;
16894
16895 fn main() {
16896 println!("hello");
16897
16898 println!("world");
16899 }
16900 "#
16901 .unindent();
16902
16903 cx.set_state(
16904 &r#"
16905 use some::mod2;
16906
16907 const A: u32 = 42;
16908 const C: u32 = 42;
16909
16910 fn main(ˇ) {
16911 //println!("hello");
16912
16913 println!("world");
16914 //
16915 //
16916 }
16917 "#
16918 .unindent(),
16919 );
16920
16921 cx.set_head_text(&diff_base);
16922 executor.run_until_parked();
16923
16924 cx.update_editor(|editor, window, cx| {
16925 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16926 });
16927 executor.run_until_parked();
16928 cx.assert_state_with_diff(
16929 r#"
16930 - use some::mod1;
16931 use some::mod2;
16932
16933 const A: u32 = 42;
16934 - const B: u32 = 42;
16935 const C: u32 = 42;
16936
16937 fn main(ˇ) {
16938 - println!("hello");
16939 + //println!("hello");
16940
16941 println!("world");
16942 + //
16943 + //
16944 }
16945 "#
16946 .unindent(),
16947 );
16948
16949 cx.set_head_text("new diff base!");
16950 executor.run_until_parked();
16951 cx.assert_state_with_diff(
16952 r#"
16953 - new diff base!
16954 + use some::mod2;
16955 +
16956 + const A: u32 = 42;
16957 + const C: u32 = 42;
16958 +
16959 + fn main(ˇ) {
16960 + //println!("hello");
16961 +
16962 + println!("world");
16963 + //
16964 + //
16965 + }
16966 "#
16967 .unindent(),
16968 );
16969}
16970
16971#[gpui::test]
16972async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
16973 init_test(cx, |_| {});
16974
16975 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16976 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
16977 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16978 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
16979 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
16980 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
16981
16982 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
16983 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
16984 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
16985
16986 let multi_buffer = cx.new(|cx| {
16987 let mut multibuffer = MultiBuffer::new(ReadWrite);
16988 multibuffer.push_excerpts(
16989 buffer_1.clone(),
16990 [
16991 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16992 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16993 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
16994 ],
16995 cx,
16996 );
16997 multibuffer.push_excerpts(
16998 buffer_2.clone(),
16999 [
17000 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17001 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17002 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17003 ],
17004 cx,
17005 );
17006 multibuffer.push_excerpts(
17007 buffer_3.clone(),
17008 [
17009 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17010 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17011 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17012 ],
17013 cx,
17014 );
17015 multibuffer
17016 });
17017
17018 let editor =
17019 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17020 editor
17021 .update(cx, |editor, _window, cx| {
17022 for (buffer, diff_base) in [
17023 (buffer_1.clone(), file_1_old),
17024 (buffer_2.clone(), file_2_old),
17025 (buffer_3.clone(), file_3_old),
17026 ] {
17027 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17028 editor
17029 .buffer
17030 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17031 }
17032 })
17033 .unwrap();
17034
17035 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17036 cx.run_until_parked();
17037
17038 cx.assert_editor_state(
17039 &"
17040 ˇaaa
17041 ccc
17042 ddd
17043
17044 ggg
17045 hhh
17046
17047
17048 lll
17049 mmm
17050 NNN
17051
17052 qqq
17053 rrr
17054
17055 uuu
17056 111
17057 222
17058 333
17059
17060 666
17061 777
17062
17063 000
17064 !!!"
17065 .unindent(),
17066 );
17067
17068 cx.update_editor(|editor, window, cx| {
17069 editor.select_all(&SelectAll, window, cx);
17070 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17071 });
17072 cx.executor().run_until_parked();
17073
17074 cx.assert_state_with_diff(
17075 "
17076 «aaa
17077 - bbb
17078 ccc
17079 ddd
17080
17081 ggg
17082 hhh
17083
17084
17085 lll
17086 mmm
17087 - nnn
17088 + NNN
17089
17090 qqq
17091 rrr
17092
17093 uuu
17094 111
17095 222
17096 333
17097
17098 + 666
17099 777
17100
17101 000
17102 !!!ˇ»"
17103 .unindent(),
17104 );
17105}
17106
17107#[gpui::test]
17108async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17109 init_test(cx, |_| {});
17110
17111 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17112 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17113
17114 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17115 let multi_buffer = cx.new(|cx| {
17116 let mut multibuffer = MultiBuffer::new(ReadWrite);
17117 multibuffer.push_excerpts(
17118 buffer.clone(),
17119 [
17120 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17121 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17122 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17123 ],
17124 cx,
17125 );
17126 multibuffer
17127 });
17128
17129 let editor =
17130 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17131 editor
17132 .update(cx, |editor, _window, cx| {
17133 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17134 editor
17135 .buffer
17136 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17137 })
17138 .unwrap();
17139
17140 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17141 cx.run_until_parked();
17142
17143 cx.update_editor(|editor, window, cx| {
17144 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17145 });
17146 cx.executor().run_until_parked();
17147
17148 // When the start of a hunk coincides with the start of its excerpt,
17149 // the hunk is expanded. When the start of a a hunk is earlier than
17150 // the start of its excerpt, the hunk is not expanded.
17151 cx.assert_state_with_diff(
17152 "
17153 ˇaaa
17154 - bbb
17155 + BBB
17156
17157 - ddd
17158 - eee
17159 + DDD
17160 + EEE
17161 fff
17162
17163 iii
17164 "
17165 .unindent(),
17166 );
17167}
17168
17169#[gpui::test]
17170async fn test_edits_around_expanded_insertion_hunks(
17171 executor: BackgroundExecutor,
17172 cx: &mut TestAppContext,
17173) {
17174 init_test(cx, |_| {});
17175
17176 let mut cx = EditorTestContext::new(cx).await;
17177
17178 let diff_base = r#"
17179 use some::mod1;
17180 use some::mod2;
17181
17182 const A: u32 = 42;
17183
17184 fn main() {
17185 println!("hello");
17186
17187 println!("world");
17188 }
17189 "#
17190 .unindent();
17191 executor.run_until_parked();
17192 cx.set_state(
17193 &r#"
17194 use some::mod1;
17195 use some::mod2;
17196
17197 const A: u32 = 42;
17198 const B: u32 = 42;
17199 const C: u32 = 42;
17200 ˇ
17201
17202 fn main() {
17203 println!("hello");
17204
17205 println!("world");
17206 }
17207 "#
17208 .unindent(),
17209 );
17210
17211 cx.set_head_text(&diff_base);
17212 executor.run_until_parked();
17213
17214 cx.update_editor(|editor, window, cx| {
17215 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17216 });
17217 executor.run_until_parked();
17218
17219 cx.assert_state_with_diff(
17220 r#"
17221 use some::mod1;
17222 use some::mod2;
17223
17224 const A: u32 = 42;
17225 + const B: u32 = 42;
17226 + const C: u32 = 42;
17227 + ˇ
17228
17229 fn main() {
17230 println!("hello");
17231
17232 println!("world");
17233 }
17234 "#
17235 .unindent(),
17236 );
17237
17238 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17239 executor.run_until_parked();
17240
17241 cx.assert_state_with_diff(
17242 r#"
17243 use some::mod1;
17244 use some::mod2;
17245
17246 const A: u32 = 42;
17247 + const B: u32 = 42;
17248 + const C: u32 = 42;
17249 + const D: u32 = 42;
17250 + ˇ
17251
17252 fn main() {
17253 println!("hello");
17254
17255 println!("world");
17256 }
17257 "#
17258 .unindent(),
17259 );
17260
17261 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17262 executor.run_until_parked();
17263
17264 cx.assert_state_with_diff(
17265 r#"
17266 use some::mod1;
17267 use some::mod2;
17268
17269 const A: u32 = 42;
17270 + const B: u32 = 42;
17271 + const C: u32 = 42;
17272 + const D: u32 = 42;
17273 + const E: u32 = 42;
17274 + ˇ
17275
17276 fn main() {
17277 println!("hello");
17278
17279 println!("world");
17280 }
17281 "#
17282 .unindent(),
17283 );
17284
17285 cx.update_editor(|editor, window, cx| {
17286 editor.delete_line(&DeleteLine, window, cx);
17287 });
17288 executor.run_until_parked();
17289
17290 cx.assert_state_with_diff(
17291 r#"
17292 use some::mod1;
17293 use some::mod2;
17294
17295 const A: u32 = 42;
17296 + const B: u32 = 42;
17297 + const C: u32 = 42;
17298 + const D: u32 = 42;
17299 + const E: u32 = 42;
17300 ˇ
17301 fn main() {
17302 println!("hello");
17303
17304 println!("world");
17305 }
17306 "#
17307 .unindent(),
17308 );
17309
17310 cx.update_editor(|editor, window, cx| {
17311 editor.move_up(&MoveUp, window, cx);
17312 editor.delete_line(&DeleteLine, window, cx);
17313 editor.move_up(&MoveUp, window, cx);
17314 editor.delete_line(&DeleteLine, window, cx);
17315 editor.move_up(&MoveUp, window, cx);
17316 editor.delete_line(&DeleteLine, window, cx);
17317 });
17318 executor.run_until_parked();
17319 cx.assert_state_with_diff(
17320 r#"
17321 use some::mod1;
17322 use some::mod2;
17323
17324 const A: u32 = 42;
17325 + const B: u32 = 42;
17326 ˇ
17327 fn main() {
17328 println!("hello");
17329
17330 println!("world");
17331 }
17332 "#
17333 .unindent(),
17334 );
17335
17336 cx.update_editor(|editor, window, cx| {
17337 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17338 editor.delete_line(&DeleteLine, window, cx);
17339 });
17340 executor.run_until_parked();
17341 cx.assert_state_with_diff(
17342 r#"
17343 ˇ
17344 fn main() {
17345 println!("hello");
17346
17347 println!("world");
17348 }
17349 "#
17350 .unindent(),
17351 );
17352}
17353
17354#[gpui::test]
17355async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17356 init_test(cx, |_| {});
17357
17358 let mut cx = EditorTestContext::new(cx).await;
17359 cx.set_head_text(indoc! { "
17360 one
17361 two
17362 three
17363 four
17364 five
17365 "
17366 });
17367 cx.set_state(indoc! { "
17368 one
17369 ˇthree
17370 five
17371 "});
17372 cx.run_until_parked();
17373 cx.update_editor(|editor, window, cx| {
17374 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17375 });
17376 cx.assert_state_with_diff(
17377 indoc! { "
17378 one
17379 - two
17380 ˇthree
17381 - four
17382 five
17383 "}
17384 .to_string(),
17385 );
17386 cx.update_editor(|editor, window, cx| {
17387 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17388 });
17389
17390 cx.assert_state_with_diff(
17391 indoc! { "
17392 one
17393 ˇthree
17394 five
17395 "}
17396 .to_string(),
17397 );
17398
17399 cx.set_state(indoc! { "
17400 one
17401 ˇTWO
17402 three
17403 four
17404 five
17405 "});
17406 cx.run_until_parked();
17407 cx.update_editor(|editor, window, cx| {
17408 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17409 });
17410
17411 cx.assert_state_with_diff(
17412 indoc! { "
17413 one
17414 - two
17415 + ˇTWO
17416 three
17417 four
17418 five
17419 "}
17420 .to_string(),
17421 );
17422 cx.update_editor(|editor, window, cx| {
17423 editor.move_up(&Default::default(), window, cx);
17424 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17425 });
17426 cx.assert_state_with_diff(
17427 indoc! { "
17428 one
17429 ˇTWO
17430 three
17431 four
17432 five
17433 "}
17434 .to_string(),
17435 );
17436}
17437
17438#[gpui::test]
17439async fn test_edits_around_expanded_deletion_hunks(
17440 executor: BackgroundExecutor,
17441 cx: &mut TestAppContext,
17442) {
17443 init_test(cx, |_| {});
17444
17445 let mut cx = EditorTestContext::new(cx).await;
17446
17447 let diff_base = r#"
17448 use some::mod1;
17449 use some::mod2;
17450
17451 const A: u32 = 42;
17452 const B: u32 = 42;
17453 const C: u32 = 42;
17454
17455
17456 fn main() {
17457 println!("hello");
17458
17459 println!("world");
17460 }
17461 "#
17462 .unindent();
17463 executor.run_until_parked();
17464 cx.set_state(
17465 &r#"
17466 use some::mod1;
17467 use some::mod2;
17468
17469 ˇconst B: u32 = 42;
17470 const C: u32 = 42;
17471
17472
17473 fn main() {
17474 println!("hello");
17475
17476 println!("world");
17477 }
17478 "#
17479 .unindent(),
17480 );
17481
17482 cx.set_head_text(&diff_base);
17483 executor.run_until_parked();
17484
17485 cx.update_editor(|editor, window, cx| {
17486 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17487 });
17488 executor.run_until_parked();
17489
17490 cx.assert_state_with_diff(
17491 r#"
17492 use some::mod1;
17493 use some::mod2;
17494
17495 - const A: u32 = 42;
17496 ˇconst B: u32 = 42;
17497 const C: u32 = 42;
17498
17499
17500 fn main() {
17501 println!("hello");
17502
17503 println!("world");
17504 }
17505 "#
17506 .unindent(),
17507 );
17508
17509 cx.update_editor(|editor, window, cx| {
17510 editor.delete_line(&DeleteLine, window, cx);
17511 });
17512 executor.run_until_parked();
17513 cx.assert_state_with_diff(
17514 r#"
17515 use some::mod1;
17516 use some::mod2;
17517
17518 - const A: u32 = 42;
17519 - const B: u32 = 42;
17520 ˇconst C: u32 = 42;
17521
17522
17523 fn main() {
17524 println!("hello");
17525
17526 println!("world");
17527 }
17528 "#
17529 .unindent(),
17530 );
17531
17532 cx.update_editor(|editor, window, cx| {
17533 editor.delete_line(&DeleteLine, window, cx);
17534 });
17535 executor.run_until_parked();
17536 cx.assert_state_with_diff(
17537 r#"
17538 use some::mod1;
17539 use some::mod2;
17540
17541 - const A: u32 = 42;
17542 - const B: u32 = 42;
17543 - const C: u32 = 42;
17544 ˇ
17545
17546 fn main() {
17547 println!("hello");
17548
17549 println!("world");
17550 }
17551 "#
17552 .unindent(),
17553 );
17554
17555 cx.update_editor(|editor, window, cx| {
17556 editor.handle_input("replacement", window, cx);
17557 });
17558 executor.run_until_parked();
17559 cx.assert_state_with_diff(
17560 r#"
17561 use some::mod1;
17562 use some::mod2;
17563
17564 - const A: u32 = 42;
17565 - const B: u32 = 42;
17566 - const C: u32 = 42;
17567 -
17568 + replacementˇ
17569
17570 fn main() {
17571 println!("hello");
17572
17573 println!("world");
17574 }
17575 "#
17576 .unindent(),
17577 );
17578}
17579
17580#[gpui::test]
17581async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17582 init_test(cx, |_| {});
17583
17584 let mut cx = EditorTestContext::new(cx).await;
17585
17586 let base_text = r#"
17587 one
17588 two
17589 three
17590 four
17591 five
17592 "#
17593 .unindent();
17594 executor.run_until_parked();
17595 cx.set_state(
17596 &r#"
17597 one
17598 two
17599 fˇour
17600 five
17601 "#
17602 .unindent(),
17603 );
17604
17605 cx.set_head_text(&base_text);
17606 executor.run_until_parked();
17607
17608 cx.update_editor(|editor, window, cx| {
17609 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17610 });
17611 executor.run_until_parked();
17612
17613 cx.assert_state_with_diff(
17614 r#"
17615 one
17616 two
17617 - three
17618 fˇour
17619 five
17620 "#
17621 .unindent(),
17622 );
17623
17624 cx.update_editor(|editor, window, cx| {
17625 editor.backspace(&Backspace, window, cx);
17626 editor.backspace(&Backspace, window, cx);
17627 });
17628 executor.run_until_parked();
17629 cx.assert_state_with_diff(
17630 r#"
17631 one
17632 two
17633 - threeˇ
17634 - four
17635 + our
17636 five
17637 "#
17638 .unindent(),
17639 );
17640}
17641
17642#[gpui::test]
17643async fn test_edit_after_expanded_modification_hunk(
17644 executor: BackgroundExecutor,
17645 cx: &mut TestAppContext,
17646) {
17647 init_test(cx, |_| {});
17648
17649 let mut cx = EditorTestContext::new(cx).await;
17650
17651 let diff_base = r#"
17652 use some::mod1;
17653 use some::mod2;
17654
17655 const A: u32 = 42;
17656 const B: u32 = 42;
17657 const C: u32 = 42;
17658 const D: u32 = 42;
17659
17660
17661 fn main() {
17662 println!("hello");
17663
17664 println!("world");
17665 }"#
17666 .unindent();
17667
17668 cx.set_state(
17669 &r#"
17670 use some::mod1;
17671 use some::mod2;
17672
17673 const A: u32 = 42;
17674 const B: u32 = 42;
17675 const C: u32 = 43ˇ
17676 const D: u32 = 42;
17677
17678
17679 fn main() {
17680 println!("hello");
17681
17682 println!("world");
17683 }"#
17684 .unindent(),
17685 );
17686
17687 cx.set_head_text(&diff_base);
17688 executor.run_until_parked();
17689 cx.update_editor(|editor, window, cx| {
17690 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17691 });
17692 executor.run_until_parked();
17693
17694 cx.assert_state_with_diff(
17695 r#"
17696 use some::mod1;
17697 use some::mod2;
17698
17699 const A: u32 = 42;
17700 const B: u32 = 42;
17701 - const C: u32 = 42;
17702 + const C: u32 = 43ˇ
17703 const D: u32 = 42;
17704
17705
17706 fn main() {
17707 println!("hello");
17708
17709 println!("world");
17710 }"#
17711 .unindent(),
17712 );
17713
17714 cx.update_editor(|editor, window, cx| {
17715 editor.handle_input("\nnew_line\n", window, cx);
17716 });
17717 executor.run_until_parked();
17718
17719 cx.assert_state_with_diff(
17720 r#"
17721 use some::mod1;
17722 use some::mod2;
17723
17724 const A: u32 = 42;
17725 const B: u32 = 42;
17726 - const C: u32 = 42;
17727 + const C: u32 = 43
17728 + new_line
17729 + ˇ
17730 const D: u32 = 42;
17731
17732
17733 fn main() {
17734 println!("hello");
17735
17736 println!("world");
17737 }"#
17738 .unindent(),
17739 );
17740}
17741
17742#[gpui::test]
17743async fn test_stage_and_unstage_added_file_hunk(
17744 executor: BackgroundExecutor,
17745 cx: &mut TestAppContext,
17746) {
17747 init_test(cx, |_| {});
17748
17749 let mut cx = EditorTestContext::new(cx).await;
17750 cx.update_editor(|editor, _, cx| {
17751 editor.set_expand_all_diff_hunks(cx);
17752 });
17753
17754 let working_copy = r#"
17755 ˇfn main() {
17756 println!("hello, world!");
17757 }
17758 "#
17759 .unindent();
17760
17761 cx.set_state(&working_copy);
17762 executor.run_until_parked();
17763
17764 cx.assert_state_with_diff(
17765 r#"
17766 + ˇfn main() {
17767 + println!("hello, world!");
17768 + }
17769 "#
17770 .unindent(),
17771 );
17772 cx.assert_index_text(None);
17773
17774 cx.update_editor(|editor, window, cx| {
17775 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17776 });
17777 executor.run_until_parked();
17778 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17779 cx.assert_state_with_diff(
17780 r#"
17781 + ˇfn main() {
17782 + println!("hello, world!");
17783 + }
17784 "#
17785 .unindent(),
17786 );
17787
17788 cx.update_editor(|editor, window, cx| {
17789 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17790 });
17791 executor.run_until_parked();
17792 cx.assert_index_text(None);
17793}
17794
17795async fn setup_indent_guides_editor(
17796 text: &str,
17797 cx: &mut TestAppContext,
17798) -> (BufferId, EditorTestContext) {
17799 init_test(cx, |_| {});
17800
17801 let mut cx = EditorTestContext::new(cx).await;
17802
17803 let buffer_id = cx.update_editor(|editor, window, cx| {
17804 editor.set_text(text, window, cx);
17805 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17806
17807 buffer_ids[0]
17808 });
17809
17810 (buffer_id, cx)
17811}
17812
17813fn assert_indent_guides(
17814 range: Range<u32>,
17815 expected: Vec<IndentGuide>,
17816 active_indices: Option<Vec<usize>>,
17817 cx: &mut EditorTestContext,
17818) {
17819 let indent_guides = cx.update_editor(|editor, window, cx| {
17820 let snapshot = editor.snapshot(window, cx).display_snapshot;
17821 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17822 editor,
17823 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17824 true,
17825 &snapshot,
17826 cx,
17827 );
17828
17829 indent_guides.sort_by(|a, b| {
17830 a.depth.cmp(&b.depth).then(
17831 a.start_row
17832 .cmp(&b.start_row)
17833 .then(a.end_row.cmp(&b.end_row)),
17834 )
17835 });
17836 indent_guides
17837 });
17838
17839 if let Some(expected) = active_indices {
17840 let active_indices = cx.update_editor(|editor, window, cx| {
17841 let snapshot = editor.snapshot(window, cx).display_snapshot;
17842 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
17843 });
17844
17845 assert_eq!(
17846 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
17847 expected,
17848 "Active indent guide indices do not match"
17849 );
17850 }
17851
17852 assert_eq!(indent_guides, expected, "Indent guides do not match");
17853}
17854
17855fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
17856 IndentGuide {
17857 buffer_id,
17858 start_row: MultiBufferRow(start_row),
17859 end_row: MultiBufferRow(end_row),
17860 depth,
17861 tab_size: 4,
17862 settings: IndentGuideSettings {
17863 enabled: true,
17864 line_width: 1,
17865 active_line_width: 1,
17866 ..Default::default()
17867 },
17868 }
17869}
17870
17871#[gpui::test]
17872async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
17873 let (buffer_id, mut cx) = setup_indent_guides_editor(
17874 &"
17875 fn main() {
17876 let a = 1;
17877 }"
17878 .unindent(),
17879 cx,
17880 )
17881 .await;
17882
17883 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17884}
17885
17886#[gpui::test]
17887async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
17888 let (buffer_id, mut cx) = setup_indent_guides_editor(
17889 &"
17890 fn main() {
17891 let a = 1;
17892 let b = 2;
17893 }"
17894 .unindent(),
17895 cx,
17896 )
17897 .await;
17898
17899 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
17900}
17901
17902#[gpui::test]
17903async fn test_indent_guide_nested(cx: &mut TestAppContext) {
17904 let (buffer_id, mut cx) = setup_indent_guides_editor(
17905 &"
17906 fn main() {
17907 let a = 1;
17908 if a == 3 {
17909 let b = 2;
17910 } else {
17911 let c = 3;
17912 }
17913 }"
17914 .unindent(),
17915 cx,
17916 )
17917 .await;
17918
17919 assert_indent_guides(
17920 0..8,
17921 vec![
17922 indent_guide(buffer_id, 1, 6, 0),
17923 indent_guide(buffer_id, 3, 3, 1),
17924 indent_guide(buffer_id, 5, 5, 1),
17925 ],
17926 None,
17927 &mut cx,
17928 );
17929}
17930
17931#[gpui::test]
17932async fn test_indent_guide_tab(cx: &mut TestAppContext) {
17933 let (buffer_id, mut cx) = setup_indent_guides_editor(
17934 &"
17935 fn main() {
17936 let a = 1;
17937 let b = 2;
17938 let c = 3;
17939 }"
17940 .unindent(),
17941 cx,
17942 )
17943 .await;
17944
17945 assert_indent_guides(
17946 0..5,
17947 vec![
17948 indent_guide(buffer_id, 1, 3, 0),
17949 indent_guide(buffer_id, 2, 2, 1),
17950 ],
17951 None,
17952 &mut cx,
17953 );
17954}
17955
17956#[gpui::test]
17957async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
17958 let (buffer_id, mut cx) = setup_indent_guides_editor(
17959 &"
17960 fn main() {
17961 let a = 1;
17962
17963 let c = 3;
17964 }"
17965 .unindent(),
17966 cx,
17967 )
17968 .await;
17969
17970 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
17971}
17972
17973#[gpui::test]
17974async fn test_indent_guide_complex(cx: &mut TestAppContext) {
17975 let (buffer_id, mut cx) = setup_indent_guides_editor(
17976 &"
17977 fn main() {
17978 let a = 1;
17979
17980 let c = 3;
17981
17982 if a == 3 {
17983 let b = 2;
17984 } else {
17985 let c = 3;
17986 }
17987 }"
17988 .unindent(),
17989 cx,
17990 )
17991 .await;
17992
17993 assert_indent_guides(
17994 0..11,
17995 vec![
17996 indent_guide(buffer_id, 1, 9, 0),
17997 indent_guide(buffer_id, 6, 6, 1),
17998 indent_guide(buffer_id, 8, 8, 1),
17999 ],
18000 None,
18001 &mut cx,
18002 );
18003}
18004
18005#[gpui::test]
18006async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18007 let (buffer_id, mut cx) = setup_indent_guides_editor(
18008 &"
18009 fn main() {
18010 let a = 1;
18011
18012 let c = 3;
18013
18014 if a == 3 {
18015 let b = 2;
18016 } else {
18017 let c = 3;
18018 }
18019 }"
18020 .unindent(),
18021 cx,
18022 )
18023 .await;
18024
18025 assert_indent_guides(
18026 1..11,
18027 vec![
18028 indent_guide(buffer_id, 1, 9, 0),
18029 indent_guide(buffer_id, 6, 6, 1),
18030 indent_guide(buffer_id, 8, 8, 1),
18031 ],
18032 None,
18033 &mut cx,
18034 );
18035}
18036
18037#[gpui::test]
18038async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18039 let (buffer_id, mut cx) = setup_indent_guides_editor(
18040 &"
18041 fn main() {
18042 let a = 1;
18043
18044 let c = 3;
18045
18046 if a == 3 {
18047 let b = 2;
18048 } else {
18049 let c = 3;
18050 }
18051 }"
18052 .unindent(),
18053 cx,
18054 )
18055 .await;
18056
18057 assert_indent_guides(
18058 1..10,
18059 vec![
18060 indent_guide(buffer_id, 1, 9, 0),
18061 indent_guide(buffer_id, 6, 6, 1),
18062 indent_guide(buffer_id, 8, 8, 1),
18063 ],
18064 None,
18065 &mut cx,
18066 );
18067}
18068
18069#[gpui::test]
18070async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18071 let (buffer_id, mut cx) = setup_indent_guides_editor(
18072 &"
18073 fn main() {
18074 if a {
18075 b(
18076 c,
18077 d,
18078 )
18079 } else {
18080 e(
18081 f
18082 )
18083 }
18084 }"
18085 .unindent(),
18086 cx,
18087 )
18088 .await;
18089
18090 assert_indent_guides(
18091 0..11,
18092 vec![
18093 indent_guide(buffer_id, 1, 10, 0),
18094 indent_guide(buffer_id, 2, 5, 1),
18095 indent_guide(buffer_id, 7, 9, 1),
18096 indent_guide(buffer_id, 3, 4, 2),
18097 indent_guide(buffer_id, 8, 8, 2),
18098 ],
18099 None,
18100 &mut cx,
18101 );
18102
18103 cx.update_editor(|editor, window, cx| {
18104 editor.fold_at(MultiBufferRow(2), window, cx);
18105 assert_eq!(
18106 editor.display_text(cx),
18107 "
18108 fn main() {
18109 if a {
18110 b(⋯
18111 )
18112 } else {
18113 e(
18114 f
18115 )
18116 }
18117 }"
18118 .unindent()
18119 );
18120 });
18121
18122 assert_indent_guides(
18123 0..11,
18124 vec![
18125 indent_guide(buffer_id, 1, 10, 0),
18126 indent_guide(buffer_id, 2, 5, 1),
18127 indent_guide(buffer_id, 7, 9, 1),
18128 indent_guide(buffer_id, 8, 8, 2),
18129 ],
18130 None,
18131 &mut cx,
18132 );
18133}
18134
18135#[gpui::test]
18136async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18137 let (buffer_id, mut cx) = setup_indent_guides_editor(
18138 &"
18139 block1
18140 block2
18141 block3
18142 block4
18143 block2
18144 block1
18145 block1"
18146 .unindent(),
18147 cx,
18148 )
18149 .await;
18150
18151 assert_indent_guides(
18152 1..10,
18153 vec![
18154 indent_guide(buffer_id, 1, 4, 0),
18155 indent_guide(buffer_id, 2, 3, 1),
18156 indent_guide(buffer_id, 3, 3, 2),
18157 ],
18158 None,
18159 &mut cx,
18160 );
18161}
18162
18163#[gpui::test]
18164async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18165 let (buffer_id, mut cx) = setup_indent_guides_editor(
18166 &"
18167 block1
18168 block2
18169 block3
18170
18171 block1
18172 block1"
18173 .unindent(),
18174 cx,
18175 )
18176 .await;
18177
18178 assert_indent_guides(
18179 0..6,
18180 vec![
18181 indent_guide(buffer_id, 1, 2, 0),
18182 indent_guide(buffer_id, 2, 2, 1),
18183 ],
18184 None,
18185 &mut cx,
18186 );
18187}
18188
18189#[gpui::test]
18190async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18191 let (buffer_id, mut cx) = setup_indent_guides_editor(
18192 &"
18193 function component() {
18194 \treturn (
18195 \t\t\t
18196 \t\t<div>
18197 \t\t\t<abc></abc>
18198 \t\t</div>
18199 \t)
18200 }"
18201 .unindent(),
18202 cx,
18203 )
18204 .await;
18205
18206 assert_indent_guides(
18207 0..8,
18208 vec![
18209 indent_guide(buffer_id, 1, 6, 0),
18210 indent_guide(buffer_id, 2, 5, 1),
18211 indent_guide(buffer_id, 4, 4, 2),
18212 ],
18213 None,
18214 &mut cx,
18215 );
18216}
18217
18218#[gpui::test]
18219async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18220 let (buffer_id, mut cx) = setup_indent_guides_editor(
18221 &"
18222 function component() {
18223 \treturn (
18224 \t
18225 \t\t<div>
18226 \t\t\t<abc></abc>
18227 \t\t</div>
18228 \t)
18229 }"
18230 .unindent(),
18231 cx,
18232 )
18233 .await;
18234
18235 assert_indent_guides(
18236 0..8,
18237 vec![
18238 indent_guide(buffer_id, 1, 6, 0),
18239 indent_guide(buffer_id, 2, 5, 1),
18240 indent_guide(buffer_id, 4, 4, 2),
18241 ],
18242 None,
18243 &mut cx,
18244 );
18245}
18246
18247#[gpui::test]
18248async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18249 let (buffer_id, mut cx) = setup_indent_guides_editor(
18250 &"
18251 block1
18252
18253
18254
18255 block2
18256 "
18257 .unindent(),
18258 cx,
18259 )
18260 .await;
18261
18262 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18263}
18264
18265#[gpui::test]
18266async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18267 let (buffer_id, mut cx) = setup_indent_guides_editor(
18268 &"
18269 def a:
18270 \tb = 3
18271 \tif True:
18272 \t\tc = 4
18273 \t\td = 5
18274 \tprint(b)
18275 "
18276 .unindent(),
18277 cx,
18278 )
18279 .await;
18280
18281 assert_indent_guides(
18282 0..6,
18283 vec![
18284 indent_guide(buffer_id, 1, 5, 0),
18285 indent_guide(buffer_id, 3, 4, 1),
18286 ],
18287 None,
18288 &mut cx,
18289 );
18290}
18291
18292#[gpui::test]
18293async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18294 let (buffer_id, mut cx) = setup_indent_guides_editor(
18295 &"
18296 fn main() {
18297 let a = 1;
18298 }"
18299 .unindent(),
18300 cx,
18301 )
18302 .await;
18303
18304 cx.update_editor(|editor, window, cx| {
18305 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18306 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18307 });
18308 });
18309
18310 assert_indent_guides(
18311 0..3,
18312 vec![indent_guide(buffer_id, 1, 1, 0)],
18313 Some(vec![0]),
18314 &mut cx,
18315 );
18316}
18317
18318#[gpui::test]
18319async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18320 let (buffer_id, mut cx) = setup_indent_guides_editor(
18321 &"
18322 fn main() {
18323 if 1 == 2 {
18324 let a = 1;
18325 }
18326 }"
18327 .unindent(),
18328 cx,
18329 )
18330 .await;
18331
18332 cx.update_editor(|editor, window, cx| {
18333 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18334 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18335 });
18336 });
18337
18338 assert_indent_guides(
18339 0..4,
18340 vec![
18341 indent_guide(buffer_id, 1, 3, 0),
18342 indent_guide(buffer_id, 2, 2, 1),
18343 ],
18344 Some(vec![1]),
18345 &mut cx,
18346 );
18347
18348 cx.update_editor(|editor, window, cx| {
18349 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18350 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18351 });
18352 });
18353
18354 assert_indent_guides(
18355 0..4,
18356 vec![
18357 indent_guide(buffer_id, 1, 3, 0),
18358 indent_guide(buffer_id, 2, 2, 1),
18359 ],
18360 Some(vec![1]),
18361 &mut cx,
18362 );
18363
18364 cx.update_editor(|editor, window, cx| {
18365 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18366 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18367 });
18368 });
18369
18370 assert_indent_guides(
18371 0..4,
18372 vec![
18373 indent_guide(buffer_id, 1, 3, 0),
18374 indent_guide(buffer_id, 2, 2, 1),
18375 ],
18376 Some(vec![0]),
18377 &mut cx,
18378 );
18379}
18380
18381#[gpui::test]
18382async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18383 let (buffer_id, mut cx) = setup_indent_guides_editor(
18384 &"
18385 fn main() {
18386 let a = 1;
18387
18388 let b = 2;
18389 }"
18390 .unindent(),
18391 cx,
18392 )
18393 .await;
18394
18395 cx.update_editor(|editor, window, cx| {
18396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18397 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18398 });
18399 });
18400
18401 assert_indent_guides(
18402 0..5,
18403 vec![indent_guide(buffer_id, 1, 3, 0)],
18404 Some(vec![0]),
18405 &mut cx,
18406 );
18407}
18408
18409#[gpui::test]
18410async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18411 let (buffer_id, mut cx) = setup_indent_guides_editor(
18412 &"
18413 def m:
18414 a = 1
18415 pass"
18416 .unindent(),
18417 cx,
18418 )
18419 .await;
18420
18421 cx.update_editor(|editor, window, cx| {
18422 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18423 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18424 });
18425 });
18426
18427 assert_indent_guides(
18428 0..3,
18429 vec![indent_guide(buffer_id, 1, 2, 0)],
18430 Some(vec![0]),
18431 &mut cx,
18432 );
18433}
18434
18435#[gpui::test]
18436async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18437 init_test(cx, |_| {});
18438 let mut cx = EditorTestContext::new(cx).await;
18439 let text = indoc! {
18440 "
18441 impl A {
18442 fn b() {
18443 0;
18444 3;
18445 5;
18446 6;
18447 7;
18448 }
18449 }
18450 "
18451 };
18452 let base_text = indoc! {
18453 "
18454 impl A {
18455 fn b() {
18456 0;
18457 1;
18458 2;
18459 3;
18460 4;
18461 }
18462 fn c() {
18463 5;
18464 6;
18465 7;
18466 }
18467 }
18468 "
18469 };
18470
18471 cx.update_editor(|editor, window, cx| {
18472 editor.set_text(text, window, cx);
18473
18474 editor.buffer().update(cx, |multibuffer, cx| {
18475 let buffer = multibuffer.as_singleton().unwrap();
18476 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18477
18478 multibuffer.set_all_diff_hunks_expanded(cx);
18479 multibuffer.add_diff(diff, cx);
18480
18481 buffer.read(cx).remote_id()
18482 })
18483 });
18484 cx.run_until_parked();
18485
18486 cx.assert_state_with_diff(
18487 indoc! { "
18488 impl A {
18489 fn b() {
18490 0;
18491 - 1;
18492 - 2;
18493 3;
18494 - 4;
18495 - }
18496 - fn c() {
18497 5;
18498 6;
18499 7;
18500 }
18501 }
18502 ˇ"
18503 }
18504 .to_string(),
18505 );
18506
18507 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18508 editor
18509 .snapshot(window, cx)
18510 .buffer_snapshot
18511 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18512 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18513 .collect::<Vec<_>>()
18514 });
18515 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18516 assert_eq!(
18517 actual_guides,
18518 vec![
18519 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18520 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18521 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18522 ]
18523 );
18524}
18525
18526#[gpui::test]
18527async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18528 init_test(cx, |_| {});
18529 let mut cx = EditorTestContext::new(cx).await;
18530
18531 let diff_base = r#"
18532 a
18533 b
18534 c
18535 "#
18536 .unindent();
18537
18538 cx.set_state(
18539 &r#"
18540 ˇA
18541 b
18542 C
18543 "#
18544 .unindent(),
18545 );
18546 cx.set_head_text(&diff_base);
18547 cx.update_editor(|editor, window, cx| {
18548 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18549 });
18550 executor.run_until_parked();
18551
18552 let both_hunks_expanded = r#"
18553 - a
18554 + ˇA
18555 b
18556 - c
18557 + C
18558 "#
18559 .unindent();
18560
18561 cx.assert_state_with_diff(both_hunks_expanded.clone());
18562
18563 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18564 let snapshot = editor.snapshot(window, cx);
18565 let hunks = editor
18566 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18567 .collect::<Vec<_>>();
18568 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18569 let buffer_id = hunks[0].buffer_id;
18570 hunks
18571 .into_iter()
18572 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18573 .collect::<Vec<_>>()
18574 });
18575 assert_eq!(hunk_ranges.len(), 2);
18576
18577 cx.update_editor(|editor, _, cx| {
18578 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18579 });
18580 executor.run_until_parked();
18581
18582 let second_hunk_expanded = r#"
18583 ˇA
18584 b
18585 - c
18586 + C
18587 "#
18588 .unindent();
18589
18590 cx.assert_state_with_diff(second_hunk_expanded);
18591
18592 cx.update_editor(|editor, _, cx| {
18593 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18594 });
18595 executor.run_until_parked();
18596
18597 cx.assert_state_with_diff(both_hunks_expanded.clone());
18598
18599 cx.update_editor(|editor, _, cx| {
18600 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18601 });
18602 executor.run_until_parked();
18603
18604 let first_hunk_expanded = r#"
18605 - a
18606 + ˇA
18607 b
18608 C
18609 "#
18610 .unindent();
18611
18612 cx.assert_state_with_diff(first_hunk_expanded);
18613
18614 cx.update_editor(|editor, _, cx| {
18615 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18616 });
18617 executor.run_until_parked();
18618
18619 cx.assert_state_with_diff(both_hunks_expanded);
18620
18621 cx.set_state(
18622 &r#"
18623 ˇA
18624 b
18625 "#
18626 .unindent(),
18627 );
18628 cx.run_until_parked();
18629
18630 // TODO this cursor position seems bad
18631 cx.assert_state_with_diff(
18632 r#"
18633 - ˇa
18634 + A
18635 b
18636 "#
18637 .unindent(),
18638 );
18639
18640 cx.update_editor(|editor, window, cx| {
18641 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18642 });
18643
18644 cx.assert_state_with_diff(
18645 r#"
18646 - ˇa
18647 + A
18648 b
18649 - c
18650 "#
18651 .unindent(),
18652 );
18653
18654 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18655 let snapshot = editor.snapshot(window, cx);
18656 let hunks = editor
18657 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18658 .collect::<Vec<_>>();
18659 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18660 let buffer_id = hunks[0].buffer_id;
18661 hunks
18662 .into_iter()
18663 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18664 .collect::<Vec<_>>()
18665 });
18666 assert_eq!(hunk_ranges.len(), 2);
18667
18668 cx.update_editor(|editor, _, cx| {
18669 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18670 });
18671 executor.run_until_parked();
18672
18673 cx.assert_state_with_diff(
18674 r#"
18675 - ˇa
18676 + A
18677 b
18678 "#
18679 .unindent(),
18680 );
18681}
18682
18683#[gpui::test]
18684async fn test_toggle_deletion_hunk_at_start_of_file(
18685 executor: BackgroundExecutor,
18686 cx: &mut TestAppContext,
18687) {
18688 init_test(cx, |_| {});
18689 let mut cx = EditorTestContext::new(cx).await;
18690
18691 let diff_base = r#"
18692 a
18693 b
18694 c
18695 "#
18696 .unindent();
18697
18698 cx.set_state(
18699 &r#"
18700 ˇb
18701 c
18702 "#
18703 .unindent(),
18704 );
18705 cx.set_head_text(&diff_base);
18706 cx.update_editor(|editor, window, cx| {
18707 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18708 });
18709 executor.run_until_parked();
18710
18711 let hunk_expanded = r#"
18712 - a
18713 ˇb
18714 c
18715 "#
18716 .unindent();
18717
18718 cx.assert_state_with_diff(hunk_expanded.clone());
18719
18720 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18721 let snapshot = editor.snapshot(window, cx);
18722 let hunks = editor
18723 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18724 .collect::<Vec<_>>();
18725 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18726 let buffer_id = hunks[0].buffer_id;
18727 hunks
18728 .into_iter()
18729 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18730 .collect::<Vec<_>>()
18731 });
18732 assert_eq!(hunk_ranges.len(), 1);
18733
18734 cx.update_editor(|editor, _, cx| {
18735 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18736 });
18737 executor.run_until_parked();
18738
18739 let hunk_collapsed = r#"
18740 ˇb
18741 c
18742 "#
18743 .unindent();
18744
18745 cx.assert_state_with_diff(hunk_collapsed);
18746
18747 cx.update_editor(|editor, _, cx| {
18748 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18749 });
18750 executor.run_until_parked();
18751
18752 cx.assert_state_with_diff(hunk_expanded.clone());
18753}
18754
18755#[gpui::test]
18756async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18757 init_test(cx, |_| {});
18758
18759 let fs = FakeFs::new(cx.executor());
18760 fs.insert_tree(
18761 path!("/test"),
18762 json!({
18763 ".git": {},
18764 "file-1": "ONE\n",
18765 "file-2": "TWO\n",
18766 "file-3": "THREE\n",
18767 }),
18768 )
18769 .await;
18770
18771 fs.set_head_for_repo(
18772 path!("/test/.git").as_ref(),
18773 &[
18774 ("file-1".into(), "one\n".into()),
18775 ("file-2".into(), "two\n".into()),
18776 ("file-3".into(), "three\n".into()),
18777 ],
18778 "deadbeef",
18779 );
18780
18781 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18782 let mut buffers = vec![];
18783 for i in 1..=3 {
18784 let buffer = project
18785 .update(cx, |project, cx| {
18786 let path = format!(path!("/test/file-{}"), i);
18787 project.open_local_buffer(path, cx)
18788 })
18789 .await
18790 .unwrap();
18791 buffers.push(buffer);
18792 }
18793
18794 let multibuffer = cx.new(|cx| {
18795 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18796 multibuffer.set_all_diff_hunks_expanded(cx);
18797 for buffer in &buffers {
18798 let snapshot = buffer.read(cx).snapshot();
18799 multibuffer.set_excerpts_for_path(
18800 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18801 buffer.clone(),
18802 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18803 DEFAULT_MULTIBUFFER_CONTEXT,
18804 cx,
18805 );
18806 }
18807 multibuffer
18808 });
18809
18810 let editor = cx.add_window(|window, cx| {
18811 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18812 });
18813 cx.run_until_parked();
18814
18815 let snapshot = editor
18816 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18817 .unwrap();
18818 let hunks = snapshot
18819 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18820 .map(|hunk| match hunk {
18821 DisplayDiffHunk::Unfolded {
18822 display_row_range, ..
18823 } => display_row_range,
18824 DisplayDiffHunk::Folded { .. } => unreachable!(),
18825 })
18826 .collect::<Vec<_>>();
18827 assert_eq!(
18828 hunks,
18829 [
18830 DisplayRow(2)..DisplayRow(4),
18831 DisplayRow(7)..DisplayRow(9),
18832 DisplayRow(12)..DisplayRow(14),
18833 ]
18834 );
18835}
18836
18837#[gpui::test]
18838async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18839 init_test(cx, |_| {});
18840
18841 let mut cx = EditorTestContext::new(cx).await;
18842 cx.set_head_text(indoc! { "
18843 one
18844 two
18845 three
18846 four
18847 five
18848 "
18849 });
18850 cx.set_index_text(indoc! { "
18851 one
18852 two
18853 three
18854 four
18855 five
18856 "
18857 });
18858 cx.set_state(indoc! {"
18859 one
18860 TWO
18861 ˇTHREE
18862 FOUR
18863 five
18864 "});
18865 cx.run_until_parked();
18866 cx.update_editor(|editor, window, cx| {
18867 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18868 });
18869 cx.run_until_parked();
18870 cx.assert_index_text(Some(indoc! {"
18871 one
18872 TWO
18873 THREE
18874 FOUR
18875 five
18876 "}));
18877 cx.set_state(indoc! { "
18878 one
18879 TWO
18880 ˇTHREE-HUNDRED
18881 FOUR
18882 five
18883 "});
18884 cx.run_until_parked();
18885 cx.update_editor(|editor, window, cx| {
18886 let snapshot = editor.snapshot(window, cx);
18887 let hunks = editor
18888 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18889 .collect::<Vec<_>>();
18890 assert_eq!(hunks.len(), 1);
18891 assert_eq!(
18892 hunks[0].status(),
18893 DiffHunkStatus {
18894 kind: DiffHunkStatusKind::Modified,
18895 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
18896 }
18897 );
18898
18899 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18900 });
18901 cx.run_until_parked();
18902 cx.assert_index_text(Some(indoc! {"
18903 one
18904 TWO
18905 THREE-HUNDRED
18906 FOUR
18907 five
18908 "}));
18909}
18910
18911#[gpui::test]
18912fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
18913 init_test(cx, |_| {});
18914
18915 let editor = cx.add_window(|window, cx| {
18916 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
18917 build_editor(buffer, window, cx)
18918 });
18919
18920 let render_args = Arc::new(Mutex::new(None));
18921 let snapshot = editor
18922 .update(cx, |editor, window, cx| {
18923 let snapshot = editor.buffer().read(cx).snapshot(cx);
18924 let range =
18925 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
18926
18927 struct RenderArgs {
18928 row: MultiBufferRow,
18929 folded: bool,
18930 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
18931 }
18932
18933 let crease = Crease::inline(
18934 range,
18935 FoldPlaceholder::test(),
18936 {
18937 let toggle_callback = render_args.clone();
18938 move |row, folded, callback, _window, _cx| {
18939 *toggle_callback.lock() = Some(RenderArgs {
18940 row,
18941 folded,
18942 callback,
18943 });
18944 div()
18945 }
18946 },
18947 |_row, _folded, _window, _cx| div(),
18948 );
18949
18950 editor.insert_creases(Some(crease), cx);
18951 let snapshot = editor.snapshot(window, cx);
18952 let _div = snapshot.render_crease_toggle(
18953 MultiBufferRow(1),
18954 false,
18955 cx.entity().clone(),
18956 window,
18957 cx,
18958 );
18959 snapshot
18960 })
18961 .unwrap();
18962
18963 let render_args = render_args.lock().take().unwrap();
18964 assert_eq!(render_args.row, MultiBufferRow(1));
18965 assert!(!render_args.folded);
18966 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18967
18968 cx.update_window(*editor, |_, window, cx| {
18969 (render_args.callback)(true, window, cx)
18970 })
18971 .unwrap();
18972 let snapshot = editor
18973 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18974 .unwrap();
18975 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
18976
18977 cx.update_window(*editor, |_, window, cx| {
18978 (render_args.callback)(false, window, cx)
18979 })
18980 .unwrap();
18981 let snapshot = editor
18982 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18983 .unwrap();
18984 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
18985}
18986
18987#[gpui::test]
18988async fn test_input_text(cx: &mut TestAppContext) {
18989 init_test(cx, |_| {});
18990 let mut cx = EditorTestContext::new(cx).await;
18991
18992 cx.set_state(
18993 &r#"ˇone
18994 two
18995
18996 three
18997 fourˇ
18998 five
18999
19000 siˇx"#
19001 .unindent(),
19002 );
19003
19004 cx.dispatch_action(HandleInput(String::new()));
19005 cx.assert_editor_state(
19006 &r#"ˇone
19007 two
19008
19009 three
19010 fourˇ
19011 five
19012
19013 siˇx"#
19014 .unindent(),
19015 );
19016
19017 cx.dispatch_action(HandleInput("AAAA".to_string()));
19018 cx.assert_editor_state(
19019 &r#"AAAAˇone
19020 two
19021
19022 three
19023 fourAAAAˇ
19024 five
19025
19026 siAAAAˇx"#
19027 .unindent(),
19028 );
19029}
19030
19031#[gpui::test]
19032async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19033 init_test(cx, |_| {});
19034
19035 let mut cx = EditorTestContext::new(cx).await;
19036 cx.set_state(
19037 r#"let foo = 1;
19038let foo = 2;
19039let foo = 3;
19040let fooˇ = 4;
19041let foo = 5;
19042let foo = 6;
19043let foo = 7;
19044let foo = 8;
19045let foo = 9;
19046let foo = 10;
19047let foo = 11;
19048let foo = 12;
19049let foo = 13;
19050let foo = 14;
19051let foo = 15;"#,
19052 );
19053
19054 cx.update_editor(|e, window, cx| {
19055 assert_eq!(
19056 e.next_scroll_position,
19057 NextScrollCursorCenterTopBottom::Center,
19058 "Default next scroll direction is center",
19059 );
19060
19061 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19062 assert_eq!(
19063 e.next_scroll_position,
19064 NextScrollCursorCenterTopBottom::Top,
19065 "After center, next scroll direction should be top",
19066 );
19067
19068 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19069 assert_eq!(
19070 e.next_scroll_position,
19071 NextScrollCursorCenterTopBottom::Bottom,
19072 "After top, next scroll direction should be bottom",
19073 );
19074
19075 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19076 assert_eq!(
19077 e.next_scroll_position,
19078 NextScrollCursorCenterTopBottom::Center,
19079 "After bottom, scrolling should start over",
19080 );
19081
19082 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19083 assert_eq!(
19084 e.next_scroll_position,
19085 NextScrollCursorCenterTopBottom::Top,
19086 "Scrolling continues if retriggered fast enough"
19087 );
19088 });
19089
19090 cx.executor()
19091 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19092 cx.executor().run_until_parked();
19093 cx.update_editor(|e, _, _| {
19094 assert_eq!(
19095 e.next_scroll_position,
19096 NextScrollCursorCenterTopBottom::Center,
19097 "If scrolling is not triggered fast enough, it should reset"
19098 );
19099 });
19100}
19101
19102#[gpui::test]
19103async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19104 init_test(cx, |_| {});
19105 let mut cx = EditorLspTestContext::new_rust(
19106 lsp::ServerCapabilities {
19107 definition_provider: Some(lsp::OneOf::Left(true)),
19108 references_provider: Some(lsp::OneOf::Left(true)),
19109 ..lsp::ServerCapabilities::default()
19110 },
19111 cx,
19112 )
19113 .await;
19114
19115 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19116 let go_to_definition = cx
19117 .lsp
19118 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19119 move |params, _| async move {
19120 if empty_go_to_definition {
19121 Ok(None)
19122 } else {
19123 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19124 uri: params.text_document_position_params.text_document.uri,
19125 range: lsp::Range::new(
19126 lsp::Position::new(4, 3),
19127 lsp::Position::new(4, 6),
19128 ),
19129 })))
19130 }
19131 },
19132 );
19133 let references = cx
19134 .lsp
19135 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19136 Ok(Some(vec![lsp::Location {
19137 uri: params.text_document_position.text_document.uri,
19138 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19139 }]))
19140 });
19141 (go_to_definition, references)
19142 };
19143
19144 cx.set_state(
19145 &r#"fn one() {
19146 let mut a = ˇtwo();
19147 }
19148
19149 fn two() {}"#
19150 .unindent(),
19151 );
19152 set_up_lsp_handlers(false, &mut cx);
19153 let navigated = cx
19154 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19155 .await
19156 .expect("Failed to navigate to definition");
19157 assert_eq!(
19158 navigated,
19159 Navigated::Yes,
19160 "Should have navigated to definition from the GetDefinition response"
19161 );
19162 cx.assert_editor_state(
19163 &r#"fn one() {
19164 let mut a = two();
19165 }
19166
19167 fn «twoˇ»() {}"#
19168 .unindent(),
19169 );
19170
19171 let editors = cx.update_workspace(|workspace, _, cx| {
19172 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19173 });
19174 cx.update_editor(|_, _, test_editor_cx| {
19175 assert_eq!(
19176 editors.len(),
19177 1,
19178 "Initially, only one, test, editor should be open in the workspace"
19179 );
19180 assert_eq!(
19181 test_editor_cx.entity(),
19182 editors.last().expect("Asserted len is 1").clone()
19183 );
19184 });
19185
19186 set_up_lsp_handlers(true, &mut cx);
19187 let navigated = cx
19188 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19189 .await
19190 .expect("Failed to navigate to lookup references");
19191 assert_eq!(
19192 navigated,
19193 Navigated::Yes,
19194 "Should have navigated to references as a fallback after empty GoToDefinition response"
19195 );
19196 // We should not change the selections in the existing file,
19197 // if opening another milti buffer with the references
19198 cx.assert_editor_state(
19199 &r#"fn one() {
19200 let mut a = two();
19201 }
19202
19203 fn «twoˇ»() {}"#
19204 .unindent(),
19205 );
19206 let editors = cx.update_workspace(|workspace, _, cx| {
19207 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19208 });
19209 cx.update_editor(|_, _, test_editor_cx| {
19210 assert_eq!(
19211 editors.len(),
19212 2,
19213 "After falling back to references search, we open a new editor with the results"
19214 );
19215 let references_fallback_text = editors
19216 .into_iter()
19217 .find(|new_editor| *new_editor != test_editor_cx.entity())
19218 .expect("Should have one non-test editor now")
19219 .read(test_editor_cx)
19220 .text(test_editor_cx);
19221 assert_eq!(
19222 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19223 "Should use the range from the references response and not the GoToDefinition one"
19224 );
19225 });
19226}
19227
19228#[gpui::test]
19229async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19230 init_test(cx, |_| {});
19231 cx.update(|cx| {
19232 let mut editor_settings = EditorSettings::get_global(cx).clone();
19233 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19234 EditorSettings::override_global(editor_settings, cx);
19235 });
19236 let mut cx = EditorLspTestContext::new_rust(
19237 lsp::ServerCapabilities {
19238 definition_provider: Some(lsp::OneOf::Left(true)),
19239 references_provider: Some(lsp::OneOf::Left(true)),
19240 ..lsp::ServerCapabilities::default()
19241 },
19242 cx,
19243 )
19244 .await;
19245 let original_state = r#"fn one() {
19246 let mut a = ˇtwo();
19247 }
19248
19249 fn two() {}"#
19250 .unindent();
19251 cx.set_state(&original_state);
19252
19253 let mut go_to_definition = cx
19254 .lsp
19255 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19256 move |_, _| async move { Ok(None) },
19257 );
19258 let _references = cx
19259 .lsp
19260 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19261 panic!("Should not call for references with no go to definition fallback")
19262 });
19263
19264 let navigated = cx
19265 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19266 .await
19267 .expect("Failed to navigate to lookup references");
19268 go_to_definition
19269 .next()
19270 .await
19271 .expect("Should have called the go_to_definition handler");
19272
19273 assert_eq!(
19274 navigated,
19275 Navigated::No,
19276 "Should have navigated to references as a fallback after empty GoToDefinition response"
19277 );
19278 cx.assert_editor_state(&original_state);
19279 let editors = cx.update_workspace(|workspace, _, cx| {
19280 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19281 });
19282 cx.update_editor(|_, _, _| {
19283 assert_eq!(
19284 editors.len(),
19285 1,
19286 "After unsuccessful fallback, no other editor should have been opened"
19287 );
19288 });
19289}
19290
19291#[gpui::test]
19292async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19293 init_test(cx, |_| {});
19294
19295 let language = Arc::new(Language::new(
19296 LanguageConfig::default(),
19297 Some(tree_sitter_rust::LANGUAGE.into()),
19298 ));
19299
19300 let text = r#"
19301 #[cfg(test)]
19302 mod tests() {
19303 #[test]
19304 fn runnable_1() {
19305 let a = 1;
19306 }
19307
19308 #[test]
19309 fn runnable_2() {
19310 let a = 1;
19311 let b = 2;
19312 }
19313 }
19314 "#
19315 .unindent();
19316
19317 let fs = FakeFs::new(cx.executor());
19318 fs.insert_file("/file.rs", Default::default()).await;
19319
19320 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19321 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19322 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19323 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19324 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19325
19326 let editor = cx.new_window_entity(|window, cx| {
19327 Editor::new(
19328 EditorMode::full(),
19329 multi_buffer,
19330 Some(project.clone()),
19331 window,
19332 cx,
19333 )
19334 });
19335
19336 editor.update_in(cx, |editor, window, cx| {
19337 let snapshot = editor.buffer().read(cx).snapshot(cx);
19338 editor.tasks.insert(
19339 (buffer.read(cx).remote_id(), 3),
19340 RunnableTasks {
19341 templates: vec![],
19342 offset: snapshot.anchor_before(43),
19343 column: 0,
19344 extra_variables: HashMap::default(),
19345 context_range: BufferOffset(43)..BufferOffset(85),
19346 },
19347 );
19348 editor.tasks.insert(
19349 (buffer.read(cx).remote_id(), 8),
19350 RunnableTasks {
19351 templates: vec![],
19352 offset: snapshot.anchor_before(86),
19353 column: 0,
19354 extra_variables: HashMap::default(),
19355 context_range: BufferOffset(86)..BufferOffset(191),
19356 },
19357 );
19358
19359 // Test finding task when cursor is inside function body
19360 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19361 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19362 });
19363 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19364 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19365
19366 // Test finding task when cursor is on function name
19367 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19368 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19369 });
19370 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19371 assert_eq!(row, 8, "Should find task when cursor is on function name");
19372 });
19373}
19374
19375#[gpui::test]
19376async fn test_folding_buffers(cx: &mut TestAppContext) {
19377 init_test(cx, |_| {});
19378
19379 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19380 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19381 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19382
19383 let fs = FakeFs::new(cx.executor());
19384 fs.insert_tree(
19385 path!("/a"),
19386 json!({
19387 "first.rs": sample_text_1,
19388 "second.rs": sample_text_2,
19389 "third.rs": sample_text_3,
19390 }),
19391 )
19392 .await;
19393 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19394 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19395 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19396 let worktree = project.update(cx, |project, cx| {
19397 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19398 assert_eq!(worktrees.len(), 1);
19399 worktrees.pop().unwrap()
19400 });
19401 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19402
19403 let buffer_1 = project
19404 .update(cx, |project, cx| {
19405 project.open_buffer((worktree_id, "first.rs"), cx)
19406 })
19407 .await
19408 .unwrap();
19409 let buffer_2 = project
19410 .update(cx, |project, cx| {
19411 project.open_buffer((worktree_id, "second.rs"), cx)
19412 })
19413 .await
19414 .unwrap();
19415 let buffer_3 = project
19416 .update(cx, |project, cx| {
19417 project.open_buffer((worktree_id, "third.rs"), cx)
19418 })
19419 .await
19420 .unwrap();
19421
19422 let multi_buffer = cx.new(|cx| {
19423 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19424 multi_buffer.push_excerpts(
19425 buffer_1.clone(),
19426 [
19427 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19428 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19429 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19430 ],
19431 cx,
19432 );
19433 multi_buffer.push_excerpts(
19434 buffer_2.clone(),
19435 [
19436 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19437 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19438 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19439 ],
19440 cx,
19441 );
19442 multi_buffer.push_excerpts(
19443 buffer_3.clone(),
19444 [
19445 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19446 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19447 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19448 ],
19449 cx,
19450 );
19451 multi_buffer
19452 });
19453 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19454 Editor::new(
19455 EditorMode::full(),
19456 multi_buffer.clone(),
19457 Some(project.clone()),
19458 window,
19459 cx,
19460 )
19461 });
19462
19463 assert_eq!(
19464 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19465 "\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",
19466 );
19467
19468 multi_buffer_editor.update(cx, |editor, cx| {
19469 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19470 });
19471 assert_eq!(
19472 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19473 "\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",
19474 "After folding the first buffer, its text should not be displayed"
19475 );
19476
19477 multi_buffer_editor.update(cx, |editor, cx| {
19478 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19479 });
19480 assert_eq!(
19481 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19482 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19483 "After folding the second buffer, its text should not be displayed"
19484 );
19485
19486 multi_buffer_editor.update(cx, |editor, cx| {
19487 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19488 });
19489 assert_eq!(
19490 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19491 "\n\n\n\n\n",
19492 "After folding the third buffer, its text should not be displayed"
19493 );
19494
19495 // Emulate selection inside the fold logic, that should work
19496 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19497 editor
19498 .snapshot(window, cx)
19499 .next_line_boundary(Point::new(0, 4));
19500 });
19501
19502 multi_buffer_editor.update(cx, |editor, cx| {
19503 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19504 });
19505 assert_eq!(
19506 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19507 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19508 "After unfolding the second buffer, its text should be displayed"
19509 );
19510
19511 // Typing inside of buffer 1 causes that buffer to be unfolded.
19512 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19513 assert_eq!(
19514 multi_buffer
19515 .read(cx)
19516 .snapshot(cx)
19517 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19518 .collect::<String>(),
19519 "bbbb"
19520 );
19521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19522 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19523 });
19524 editor.handle_input("B", window, cx);
19525 });
19526
19527 assert_eq!(
19528 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19529 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19530 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19531 );
19532
19533 multi_buffer_editor.update(cx, |editor, cx| {
19534 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19535 });
19536 assert_eq!(
19537 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19538 "\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",
19539 "After unfolding the all buffers, all original text should be displayed"
19540 );
19541}
19542
19543#[gpui::test]
19544async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19545 init_test(cx, |_| {});
19546
19547 let sample_text_1 = "1111\n2222\n3333".to_string();
19548 let sample_text_2 = "4444\n5555\n6666".to_string();
19549 let sample_text_3 = "7777\n8888\n9999".to_string();
19550
19551 let fs = FakeFs::new(cx.executor());
19552 fs.insert_tree(
19553 path!("/a"),
19554 json!({
19555 "first.rs": sample_text_1,
19556 "second.rs": sample_text_2,
19557 "third.rs": sample_text_3,
19558 }),
19559 )
19560 .await;
19561 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19562 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19563 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19564 let worktree = project.update(cx, |project, cx| {
19565 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19566 assert_eq!(worktrees.len(), 1);
19567 worktrees.pop().unwrap()
19568 });
19569 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19570
19571 let buffer_1 = project
19572 .update(cx, |project, cx| {
19573 project.open_buffer((worktree_id, "first.rs"), cx)
19574 })
19575 .await
19576 .unwrap();
19577 let buffer_2 = project
19578 .update(cx, |project, cx| {
19579 project.open_buffer((worktree_id, "second.rs"), cx)
19580 })
19581 .await
19582 .unwrap();
19583 let buffer_3 = project
19584 .update(cx, |project, cx| {
19585 project.open_buffer((worktree_id, "third.rs"), cx)
19586 })
19587 .await
19588 .unwrap();
19589
19590 let multi_buffer = cx.new(|cx| {
19591 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19592 multi_buffer.push_excerpts(
19593 buffer_1.clone(),
19594 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19595 cx,
19596 );
19597 multi_buffer.push_excerpts(
19598 buffer_2.clone(),
19599 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19600 cx,
19601 );
19602 multi_buffer.push_excerpts(
19603 buffer_3.clone(),
19604 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19605 cx,
19606 );
19607 multi_buffer
19608 });
19609
19610 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19611 Editor::new(
19612 EditorMode::full(),
19613 multi_buffer,
19614 Some(project.clone()),
19615 window,
19616 cx,
19617 )
19618 });
19619
19620 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19621 assert_eq!(
19622 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19623 full_text,
19624 );
19625
19626 multi_buffer_editor.update(cx, |editor, cx| {
19627 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19628 });
19629 assert_eq!(
19630 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19631 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19632 "After folding the first buffer, its text should not be displayed"
19633 );
19634
19635 multi_buffer_editor.update(cx, |editor, cx| {
19636 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19637 });
19638
19639 assert_eq!(
19640 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19641 "\n\n\n\n\n\n7777\n8888\n9999",
19642 "After folding the second buffer, its text should not be displayed"
19643 );
19644
19645 multi_buffer_editor.update(cx, |editor, cx| {
19646 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19647 });
19648 assert_eq!(
19649 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19650 "\n\n\n\n\n",
19651 "After folding the third buffer, its text should not be displayed"
19652 );
19653
19654 multi_buffer_editor.update(cx, |editor, cx| {
19655 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19656 });
19657 assert_eq!(
19658 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19659 "\n\n\n\n4444\n5555\n6666\n\n",
19660 "After unfolding the second buffer, its text should be displayed"
19661 );
19662
19663 multi_buffer_editor.update(cx, |editor, cx| {
19664 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19665 });
19666 assert_eq!(
19667 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19668 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19669 "After unfolding the first buffer, its text should be displayed"
19670 );
19671
19672 multi_buffer_editor.update(cx, |editor, cx| {
19673 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19674 });
19675 assert_eq!(
19676 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19677 full_text,
19678 "After unfolding all buffers, all original text should be displayed"
19679 );
19680}
19681
19682#[gpui::test]
19683async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19684 init_test(cx, |_| {});
19685
19686 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19687
19688 let fs = FakeFs::new(cx.executor());
19689 fs.insert_tree(
19690 path!("/a"),
19691 json!({
19692 "main.rs": sample_text,
19693 }),
19694 )
19695 .await;
19696 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19697 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19698 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19699 let worktree = project.update(cx, |project, cx| {
19700 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19701 assert_eq!(worktrees.len(), 1);
19702 worktrees.pop().unwrap()
19703 });
19704 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19705
19706 let buffer_1 = project
19707 .update(cx, |project, cx| {
19708 project.open_buffer((worktree_id, "main.rs"), cx)
19709 })
19710 .await
19711 .unwrap();
19712
19713 let multi_buffer = cx.new(|cx| {
19714 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19715 multi_buffer.push_excerpts(
19716 buffer_1.clone(),
19717 [ExcerptRange::new(
19718 Point::new(0, 0)
19719 ..Point::new(
19720 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19721 0,
19722 ),
19723 )],
19724 cx,
19725 );
19726 multi_buffer
19727 });
19728 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19729 Editor::new(
19730 EditorMode::full(),
19731 multi_buffer,
19732 Some(project.clone()),
19733 window,
19734 cx,
19735 )
19736 });
19737
19738 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19739 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19740 enum TestHighlight {}
19741 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19742 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19743 editor.highlight_text::<TestHighlight>(
19744 vec![highlight_range.clone()],
19745 HighlightStyle::color(Hsla::green()),
19746 cx,
19747 );
19748 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19749 s.select_ranges(Some(highlight_range))
19750 });
19751 });
19752
19753 let full_text = format!("\n\n{sample_text}");
19754 assert_eq!(
19755 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19756 full_text,
19757 );
19758}
19759
19760#[gpui::test]
19761async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19762 init_test(cx, |_| {});
19763 cx.update(|cx| {
19764 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19765 "keymaps/default-linux.json",
19766 cx,
19767 )
19768 .unwrap();
19769 cx.bind_keys(default_key_bindings);
19770 });
19771
19772 let (editor, cx) = cx.add_window_view(|window, cx| {
19773 let multi_buffer = MultiBuffer::build_multi(
19774 [
19775 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19776 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19777 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19778 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19779 ],
19780 cx,
19781 );
19782 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19783
19784 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19785 // fold all but the second buffer, so that we test navigating between two
19786 // adjacent folded buffers, as well as folded buffers at the start and
19787 // end the multibuffer
19788 editor.fold_buffer(buffer_ids[0], cx);
19789 editor.fold_buffer(buffer_ids[2], cx);
19790 editor.fold_buffer(buffer_ids[3], cx);
19791
19792 editor
19793 });
19794 cx.simulate_resize(size(px(1000.), px(1000.)));
19795
19796 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19797 cx.assert_excerpts_with_selections(indoc! {"
19798 [EXCERPT]
19799 ˇ[FOLDED]
19800 [EXCERPT]
19801 a1
19802 b1
19803 [EXCERPT]
19804 [FOLDED]
19805 [EXCERPT]
19806 [FOLDED]
19807 "
19808 });
19809 cx.simulate_keystroke("down");
19810 cx.assert_excerpts_with_selections(indoc! {"
19811 [EXCERPT]
19812 [FOLDED]
19813 [EXCERPT]
19814 ˇa1
19815 b1
19816 [EXCERPT]
19817 [FOLDED]
19818 [EXCERPT]
19819 [FOLDED]
19820 "
19821 });
19822 cx.simulate_keystroke("down");
19823 cx.assert_excerpts_with_selections(indoc! {"
19824 [EXCERPT]
19825 [FOLDED]
19826 [EXCERPT]
19827 a1
19828 ˇb1
19829 [EXCERPT]
19830 [FOLDED]
19831 [EXCERPT]
19832 [FOLDED]
19833 "
19834 });
19835 cx.simulate_keystroke("down");
19836 cx.assert_excerpts_with_selections(indoc! {"
19837 [EXCERPT]
19838 [FOLDED]
19839 [EXCERPT]
19840 a1
19841 b1
19842 ˇ[EXCERPT]
19843 [FOLDED]
19844 [EXCERPT]
19845 [FOLDED]
19846 "
19847 });
19848 cx.simulate_keystroke("down");
19849 cx.assert_excerpts_with_selections(indoc! {"
19850 [EXCERPT]
19851 [FOLDED]
19852 [EXCERPT]
19853 a1
19854 b1
19855 [EXCERPT]
19856 ˇ[FOLDED]
19857 [EXCERPT]
19858 [FOLDED]
19859 "
19860 });
19861 for _ in 0..5 {
19862 cx.simulate_keystroke("down");
19863 cx.assert_excerpts_with_selections(indoc! {"
19864 [EXCERPT]
19865 [FOLDED]
19866 [EXCERPT]
19867 a1
19868 b1
19869 [EXCERPT]
19870 [FOLDED]
19871 [EXCERPT]
19872 ˇ[FOLDED]
19873 "
19874 });
19875 }
19876
19877 cx.simulate_keystroke("up");
19878 cx.assert_excerpts_with_selections(indoc! {"
19879 [EXCERPT]
19880 [FOLDED]
19881 [EXCERPT]
19882 a1
19883 b1
19884 [EXCERPT]
19885 ˇ[FOLDED]
19886 [EXCERPT]
19887 [FOLDED]
19888 "
19889 });
19890 cx.simulate_keystroke("up");
19891 cx.assert_excerpts_with_selections(indoc! {"
19892 [EXCERPT]
19893 [FOLDED]
19894 [EXCERPT]
19895 a1
19896 b1
19897 ˇ[EXCERPT]
19898 [FOLDED]
19899 [EXCERPT]
19900 [FOLDED]
19901 "
19902 });
19903 cx.simulate_keystroke("up");
19904 cx.assert_excerpts_with_selections(indoc! {"
19905 [EXCERPT]
19906 [FOLDED]
19907 [EXCERPT]
19908 a1
19909 ˇb1
19910 [EXCERPT]
19911 [FOLDED]
19912 [EXCERPT]
19913 [FOLDED]
19914 "
19915 });
19916 cx.simulate_keystroke("up");
19917 cx.assert_excerpts_with_selections(indoc! {"
19918 [EXCERPT]
19919 [FOLDED]
19920 [EXCERPT]
19921 ˇa1
19922 b1
19923 [EXCERPT]
19924 [FOLDED]
19925 [EXCERPT]
19926 [FOLDED]
19927 "
19928 });
19929 for _ in 0..5 {
19930 cx.simulate_keystroke("up");
19931 cx.assert_excerpts_with_selections(indoc! {"
19932 [EXCERPT]
19933 ˇ[FOLDED]
19934 [EXCERPT]
19935 a1
19936 b1
19937 [EXCERPT]
19938 [FOLDED]
19939 [EXCERPT]
19940 [FOLDED]
19941 "
19942 });
19943 }
19944}
19945
19946#[gpui::test]
19947async fn test_inline_completion_text(cx: &mut TestAppContext) {
19948 init_test(cx, |_| {});
19949
19950 // Simple insertion
19951 assert_highlighted_edits(
19952 "Hello, world!",
19953 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
19954 true,
19955 cx,
19956 |highlighted_edits, cx| {
19957 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
19958 assert_eq!(highlighted_edits.highlights.len(), 1);
19959 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
19960 assert_eq!(
19961 highlighted_edits.highlights[0].1.background_color,
19962 Some(cx.theme().status().created_background)
19963 );
19964 },
19965 )
19966 .await;
19967
19968 // Replacement
19969 assert_highlighted_edits(
19970 "This is a test.",
19971 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
19972 false,
19973 cx,
19974 |highlighted_edits, cx| {
19975 assert_eq!(highlighted_edits.text, "That is a test.");
19976 assert_eq!(highlighted_edits.highlights.len(), 1);
19977 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
19978 assert_eq!(
19979 highlighted_edits.highlights[0].1.background_color,
19980 Some(cx.theme().status().created_background)
19981 );
19982 },
19983 )
19984 .await;
19985
19986 // Multiple edits
19987 assert_highlighted_edits(
19988 "Hello, world!",
19989 vec![
19990 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
19991 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
19992 ],
19993 false,
19994 cx,
19995 |highlighted_edits, cx| {
19996 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
19997 assert_eq!(highlighted_edits.highlights.len(), 2);
19998 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
19999 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20000 assert_eq!(
20001 highlighted_edits.highlights[0].1.background_color,
20002 Some(cx.theme().status().created_background)
20003 );
20004 assert_eq!(
20005 highlighted_edits.highlights[1].1.background_color,
20006 Some(cx.theme().status().created_background)
20007 );
20008 },
20009 )
20010 .await;
20011
20012 // Multiple lines with edits
20013 assert_highlighted_edits(
20014 "First line\nSecond line\nThird line\nFourth line",
20015 vec![
20016 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20017 (
20018 Point::new(2, 0)..Point::new(2, 10),
20019 "New third line".to_string(),
20020 ),
20021 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20022 ],
20023 false,
20024 cx,
20025 |highlighted_edits, cx| {
20026 assert_eq!(
20027 highlighted_edits.text,
20028 "Second modified\nNew third line\nFourth updated line"
20029 );
20030 assert_eq!(highlighted_edits.highlights.len(), 3);
20031 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20032 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20033 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20034 for highlight in &highlighted_edits.highlights {
20035 assert_eq!(
20036 highlight.1.background_color,
20037 Some(cx.theme().status().created_background)
20038 );
20039 }
20040 },
20041 )
20042 .await;
20043}
20044
20045#[gpui::test]
20046async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20047 init_test(cx, |_| {});
20048
20049 // Deletion
20050 assert_highlighted_edits(
20051 "Hello, world!",
20052 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20053 true,
20054 cx,
20055 |highlighted_edits, cx| {
20056 assert_eq!(highlighted_edits.text, "Hello, world!");
20057 assert_eq!(highlighted_edits.highlights.len(), 1);
20058 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20059 assert_eq!(
20060 highlighted_edits.highlights[0].1.background_color,
20061 Some(cx.theme().status().deleted_background)
20062 );
20063 },
20064 )
20065 .await;
20066
20067 // Insertion
20068 assert_highlighted_edits(
20069 "Hello, world!",
20070 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20071 true,
20072 cx,
20073 |highlighted_edits, cx| {
20074 assert_eq!(highlighted_edits.highlights.len(), 1);
20075 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20076 assert_eq!(
20077 highlighted_edits.highlights[0].1.background_color,
20078 Some(cx.theme().status().created_background)
20079 );
20080 },
20081 )
20082 .await;
20083}
20084
20085async fn assert_highlighted_edits(
20086 text: &str,
20087 edits: Vec<(Range<Point>, String)>,
20088 include_deletions: bool,
20089 cx: &mut TestAppContext,
20090 assertion_fn: impl Fn(HighlightedText, &App),
20091) {
20092 let window = cx.add_window(|window, cx| {
20093 let buffer = MultiBuffer::build_simple(text, cx);
20094 Editor::new(EditorMode::full(), buffer, None, window, cx)
20095 });
20096 let cx = &mut VisualTestContext::from_window(*window, cx);
20097
20098 let (buffer, snapshot) = window
20099 .update(cx, |editor, _window, cx| {
20100 (
20101 editor.buffer().clone(),
20102 editor.buffer().read(cx).snapshot(cx),
20103 )
20104 })
20105 .unwrap();
20106
20107 let edits = edits
20108 .into_iter()
20109 .map(|(range, edit)| {
20110 (
20111 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20112 edit,
20113 )
20114 })
20115 .collect::<Vec<_>>();
20116
20117 let text_anchor_edits = edits
20118 .clone()
20119 .into_iter()
20120 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20121 .collect::<Vec<_>>();
20122
20123 let edit_preview = window
20124 .update(cx, |_, _window, cx| {
20125 buffer
20126 .read(cx)
20127 .as_singleton()
20128 .unwrap()
20129 .read(cx)
20130 .preview_edits(text_anchor_edits.into(), cx)
20131 })
20132 .unwrap()
20133 .await;
20134
20135 cx.update(|_window, cx| {
20136 let highlighted_edits = inline_completion_edit_text(
20137 &snapshot.as_singleton().unwrap().2,
20138 &edits,
20139 &edit_preview,
20140 include_deletions,
20141 cx,
20142 );
20143 assertion_fn(highlighted_edits, cx)
20144 });
20145}
20146
20147#[track_caller]
20148fn assert_breakpoint(
20149 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20150 path: &Arc<Path>,
20151 expected: Vec<(u32, Breakpoint)>,
20152) {
20153 if expected.len() == 0usize {
20154 assert!(!breakpoints.contains_key(path), "{}", path.display());
20155 } else {
20156 let mut breakpoint = breakpoints
20157 .get(path)
20158 .unwrap()
20159 .into_iter()
20160 .map(|breakpoint| {
20161 (
20162 breakpoint.row,
20163 Breakpoint {
20164 message: breakpoint.message.clone(),
20165 state: breakpoint.state,
20166 condition: breakpoint.condition.clone(),
20167 hit_condition: breakpoint.hit_condition.clone(),
20168 },
20169 )
20170 })
20171 .collect::<Vec<_>>();
20172
20173 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20174
20175 assert_eq!(expected, breakpoint);
20176 }
20177}
20178
20179fn add_log_breakpoint_at_cursor(
20180 editor: &mut Editor,
20181 log_message: &str,
20182 window: &mut Window,
20183 cx: &mut Context<Editor>,
20184) {
20185 let (anchor, bp) = editor
20186 .breakpoints_at_cursors(window, cx)
20187 .first()
20188 .and_then(|(anchor, bp)| {
20189 if let Some(bp) = bp {
20190 Some((*anchor, bp.clone()))
20191 } else {
20192 None
20193 }
20194 })
20195 .unwrap_or_else(|| {
20196 let cursor_position: Point = editor.selections.newest(cx).head();
20197
20198 let breakpoint_position = editor
20199 .snapshot(window, cx)
20200 .display_snapshot
20201 .buffer_snapshot
20202 .anchor_before(Point::new(cursor_position.row, 0));
20203
20204 (breakpoint_position, Breakpoint::new_log(&log_message))
20205 });
20206
20207 editor.edit_breakpoint_at_anchor(
20208 anchor,
20209 bp,
20210 BreakpointEditAction::EditLogMessage(log_message.into()),
20211 cx,
20212 );
20213}
20214
20215#[gpui::test]
20216async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20217 init_test(cx, |_| {});
20218
20219 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20220 let fs = FakeFs::new(cx.executor());
20221 fs.insert_tree(
20222 path!("/a"),
20223 json!({
20224 "main.rs": sample_text,
20225 }),
20226 )
20227 .await;
20228 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20229 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20230 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20231
20232 let fs = FakeFs::new(cx.executor());
20233 fs.insert_tree(
20234 path!("/a"),
20235 json!({
20236 "main.rs": sample_text,
20237 }),
20238 )
20239 .await;
20240 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20241 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20242 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20243 let worktree_id = workspace
20244 .update(cx, |workspace, _window, cx| {
20245 workspace.project().update(cx, |project, cx| {
20246 project.worktrees(cx).next().unwrap().read(cx).id()
20247 })
20248 })
20249 .unwrap();
20250
20251 let buffer = project
20252 .update(cx, |project, cx| {
20253 project.open_buffer((worktree_id, "main.rs"), cx)
20254 })
20255 .await
20256 .unwrap();
20257
20258 let (editor, cx) = cx.add_window_view(|window, cx| {
20259 Editor::new(
20260 EditorMode::full(),
20261 MultiBuffer::build_from_buffer(buffer, cx),
20262 Some(project.clone()),
20263 window,
20264 cx,
20265 )
20266 });
20267
20268 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20269 let abs_path = project.read_with(cx, |project, cx| {
20270 project
20271 .absolute_path(&project_path, cx)
20272 .map(|path_buf| Arc::from(path_buf.to_owned()))
20273 .unwrap()
20274 });
20275
20276 // assert we can add breakpoint on the first line
20277 editor.update_in(cx, |editor, window, cx| {
20278 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20279 editor.move_to_end(&MoveToEnd, window, cx);
20280 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20281 });
20282
20283 let breakpoints = editor.update(cx, |editor, cx| {
20284 editor
20285 .breakpoint_store()
20286 .as_ref()
20287 .unwrap()
20288 .read(cx)
20289 .all_source_breakpoints(cx)
20290 .clone()
20291 });
20292
20293 assert_eq!(1, breakpoints.len());
20294 assert_breakpoint(
20295 &breakpoints,
20296 &abs_path,
20297 vec![
20298 (0, Breakpoint::new_standard()),
20299 (3, Breakpoint::new_standard()),
20300 ],
20301 );
20302
20303 editor.update_in(cx, |editor, window, cx| {
20304 editor.move_to_beginning(&MoveToBeginning, window, cx);
20305 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20306 });
20307
20308 let breakpoints = editor.update(cx, |editor, cx| {
20309 editor
20310 .breakpoint_store()
20311 .as_ref()
20312 .unwrap()
20313 .read(cx)
20314 .all_source_breakpoints(cx)
20315 .clone()
20316 });
20317
20318 assert_eq!(1, breakpoints.len());
20319 assert_breakpoint(
20320 &breakpoints,
20321 &abs_path,
20322 vec![(3, Breakpoint::new_standard())],
20323 );
20324
20325 editor.update_in(cx, |editor, window, cx| {
20326 editor.move_to_end(&MoveToEnd, window, cx);
20327 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20328 });
20329
20330 let breakpoints = editor.update(cx, |editor, cx| {
20331 editor
20332 .breakpoint_store()
20333 .as_ref()
20334 .unwrap()
20335 .read(cx)
20336 .all_source_breakpoints(cx)
20337 .clone()
20338 });
20339
20340 assert_eq!(0, breakpoints.len());
20341 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20342}
20343
20344#[gpui::test]
20345async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20346 init_test(cx, |_| {});
20347
20348 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20349
20350 let fs = FakeFs::new(cx.executor());
20351 fs.insert_tree(
20352 path!("/a"),
20353 json!({
20354 "main.rs": sample_text,
20355 }),
20356 )
20357 .await;
20358 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20359 let (workspace, cx) =
20360 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20361
20362 let worktree_id = workspace.update(cx, |workspace, cx| {
20363 workspace.project().update(cx, |project, cx| {
20364 project.worktrees(cx).next().unwrap().read(cx).id()
20365 })
20366 });
20367
20368 let buffer = project
20369 .update(cx, |project, cx| {
20370 project.open_buffer((worktree_id, "main.rs"), cx)
20371 })
20372 .await
20373 .unwrap();
20374
20375 let (editor, cx) = cx.add_window_view(|window, cx| {
20376 Editor::new(
20377 EditorMode::full(),
20378 MultiBuffer::build_from_buffer(buffer, cx),
20379 Some(project.clone()),
20380 window,
20381 cx,
20382 )
20383 });
20384
20385 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20386 let abs_path = project.read_with(cx, |project, cx| {
20387 project
20388 .absolute_path(&project_path, cx)
20389 .map(|path_buf| Arc::from(path_buf.to_owned()))
20390 .unwrap()
20391 });
20392
20393 editor.update_in(cx, |editor, window, cx| {
20394 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20395 });
20396
20397 let breakpoints = editor.update(cx, |editor, cx| {
20398 editor
20399 .breakpoint_store()
20400 .as_ref()
20401 .unwrap()
20402 .read(cx)
20403 .all_source_breakpoints(cx)
20404 .clone()
20405 });
20406
20407 assert_breakpoint(
20408 &breakpoints,
20409 &abs_path,
20410 vec![(0, Breakpoint::new_log("hello world"))],
20411 );
20412
20413 // Removing a log message from a log breakpoint should remove it
20414 editor.update_in(cx, |editor, window, cx| {
20415 add_log_breakpoint_at_cursor(editor, "", window, cx);
20416 });
20417
20418 let breakpoints = editor.update(cx, |editor, cx| {
20419 editor
20420 .breakpoint_store()
20421 .as_ref()
20422 .unwrap()
20423 .read(cx)
20424 .all_source_breakpoints(cx)
20425 .clone()
20426 });
20427
20428 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20429
20430 editor.update_in(cx, |editor, window, cx| {
20431 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20432 editor.move_to_end(&MoveToEnd, window, cx);
20433 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20434 // Not adding a log message to a standard breakpoint shouldn't remove it
20435 add_log_breakpoint_at_cursor(editor, "", window, cx);
20436 });
20437
20438 let breakpoints = editor.update(cx, |editor, cx| {
20439 editor
20440 .breakpoint_store()
20441 .as_ref()
20442 .unwrap()
20443 .read(cx)
20444 .all_source_breakpoints(cx)
20445 .clone()
20446 });
20447
20448 assert_breakpoint(
20449 &breakpoints,
20450 &abs_path,
20451 vec![
20452 (0, Breakpoint::new_standard()),
20453 (3, Breakpoint::new_standard()),
20454 ],
20455 );
20456
20457 editor.update_in(cx, |editor, window, cx| {
20458 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20459 });
20460
20461 let breakpoints = editor.update(cx, |editor, cx| {
20462 editor
20463 .breakpoint_store()
20464 .as_ref()
20465 .unwrap()
20466 .read(cx)
20467 .all_source_breakpoints(cx)
20468 .clone()
20469 });
20470
20471 assert_breakpoint(
20472 &breakpoints,
20473 &abs_path,
20474 vec![
20475 (0, Breakpoint::new_standard()),
20476 (3, Breakpoint::new_log("hello world")),
20477 ],
20478 );
20479
20480 editor.update_in(cx, |editor, window, cx| {
20481 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20482 });
20483
20484 let breakpoints = editor.update(cx, |editor, cx| {
20485 editor
20486 .breakpoint_store()
20487 .as_ref()
20488 .unwrap()
20489 .read(cx)
20490 .all_source_breakpoints(cx)
20491 .clone()
20492 });
20493
20494 assert_breakpoint(
20495 &breakpoints,
20496 &abs_path,
20497 vec![
20498 (0, Breakpoint::new_standard()),
20499 (3, Breakpoint::new_log("hello Earth!!")),
20500 ],
20501 );
20502}
20503
20504/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20505/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20506/// or when breakpoints were placed out of order. This tests for a regression too
20507#[gpui::test]
20508async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20509 init_test(cx, |_| {});
20510
20511 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20512 let fs = FakeFs::new(cx.executor());
20513 fs.insert_tree(
20514 path!("/a"),
20515 json!({
20516 "main.rs": sample_text,
20517 }),
20518 )
20519 .await;
20520 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20521 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20522 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20523
20524 let fs = FakeFs::new(cx.executor());
20525 fs.insert_tree(
20526 path!("/a"),
20527 json!({
20528 "main.rs": sample_text,
20529 }),
20530 )
20531 .await;
20532 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20533 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20534 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20535 let worktree_id = workspace
20536 .update(cx, |workspace, _window, cx| {
20537 workspace.project().update(cx, |project, cx| {
20538 project.worktrees(cx).next().unwrap().read(cx).id()
20539 })
20540 })
20541 .unwrap();
20542
20543 let buffer = project
20544 .update(cx, |project, cx| {
20545 project.open_buffer((worktree_id, "main.rs"), cx)
20546 })
20547 .await
20548 .unwrap();
20549
20550 let (editor, cx) = cx.add_window_view(|window, cx| {
20551 Editor::new(
20552 EditorMode::full(),
20553 MultiBuffer::build_from_buffer(buffer, cx),
20554 Some(project.clone()),
20555 window,
20556 cx,
20557 )
20558 });
20559
20560 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20561 let abs_path = project.read_with(cx, |project, cx| {
20562 project
20563 .absolute_path(&project_path, cx)
20564 .map(|path_buf| Arc::from(path_buf.to_owned()))
20565 .unwrap()
20566 });
20567
20568 // assert we can add breakpoint on the first line
20569 editor.update_in(cx, |editor, window, cx| {
20570 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20571 editor.move_to_end(&MoveToEnd, window, cx);
20572 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20573 editor.move_up(&MoveUp, window, cx);
20574 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20575 });
20576
20577 let breakpoints = editor.update(cx, |editor, cx| {
20578 editor
20579 .breakpoint_store()
20580 .as_ref()
20581 .unwrap()
20582 .read(cx)
20583 .all_source_breakpoints(cx)
20584 .clone()
20585 });
20586
20587 assert_eq!(1, breakpoints.len());
20588 assert_breakpoint(
20589 &breakpoints,
20590 &abs_path,
20591 vec![
20592 (0, Breakpoint::new_standard()),
20593 (2, Breakpoint::new_standard()),
20594 (3, Breakpoint::new_standard()),
20595 ],
20596 );
20597
20598 editor.update_in(cx, |editor, window, cx| {
20599 editor.move_to_beginning(&MoveToBeginning, window, cx);
20600 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20601 editor.move_to_end(&MoveToEnd, window, cx);
20602 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20603 // Disabling a breakpoint that doesn't exist should do nothing
20604 editor.move_up(&MoveUp, window, cx);
20605 editor.move_up(&MoveUp, window, cx);
20606 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20607 });
20608
20609 let breakpoints = editor.update(cx, |editor, cx| {
20610 editor
20611 .breakpoint_store()
20612 .as_ref()
20613 .unwrap()
20614 .read(cx)
20615 .all_source_breakpoints(cx)
20616 .clone()
20617 });
20618
20619 let disable_breakpoint = {
20620 let mut bp = Breakpoint::new_standard();
20621 bp.state = BreakpointState::Disabled;
20622 bp
20623 };
20624
20625 assert_eq!(1, breakpoints.len());
20626 assert_breakpoint(
20627 &breakpoints,
20628 &abs_path,
20629 vec![
20630 (0, disable_breakpoint.clone()),
20631 (2, Breakpoint::new_standard()),
20632 (3, disable_breakpoint.clone()),
20633 ],
20634 );
20635
20636 editor.update_in(cx, |editor, window, cx| {
20637 editor.move_to_beginning(&MoveToBeginning, window, cx);
20638 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20639 editor.move_to_end(&MoveToEnd, window, cx);
20640 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20641 editor.move_up(&MoveUp, window, cx);
20642 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20643 });
20644
20645 let breakpoints = editor.update(cx, |editor, cx| {
20646 editor
20647 .breakpoint_store()
20648 .as_ref()
20649 .unwrap()
20650 .read(cx)
20651 .all_source_breakpoints(cx)
20652 .clone()
20653 });
20654
20655 assert_eq!(1, breakpoints.len());
20656 assert_breakpoint(
20657 &breakpoints,
20658 &abs_path,
20659 vec![
20660 (0, Breakpoint::new_standard()),
20661 (2, disable_breakpoint),
20662 (3, Breakpoint::new_standard()),
20663 ],
20664 );
20665}
20666
20667#[gpui::test]
20668async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20669 init_test(cx, |_| {});
20670 let capabilities = lsp::ServerCapabilities {
20671 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20672 prepare_provider: Some(true),
20673 work_done_progress_options: Default::default(),
20674 })),
20675 ..Default::default()
20676 };
20677 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20678
20679 cx.set_state(indoc! {"
20680 struct Fˇoo {}
20681 "});
20682
20683 cx.update_editor(|editor, _, cx| {
20684 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20685 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20686 editor.highlight_background::<DocumentHighlightRead>(
20687 &[highlight_range],
20688 |theme| theme.colors().editor_document_highlight_read_background,
20689 cx,
20690 );
20691 });
20692
20693 let mut prepare_rename_handler = cx
20694 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20695 move |_, _, _| async move {
20696 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20697 start: lsp::Position {
20698 line: 0,
20699 character: 7,
20700 },
20701 end: lsp::Position {
20702 line: 0,
20703 character: 10,
20704 },
20705 })))
20706 },
20707 );
20708 let prepare_rename_task = cx
20709 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20710 .expect("Prepare rename was not started");
20711 prepare_rename_handler.next().await.unwrap();
20712 prepare_rename_task.await.expect("Prepare rename failed");
20713
20714 let mut rename_handler =
20715 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20716 let edit = lsp::TextEdit {
20717 range: lsp::Range {
20718 start: lsp::Position {
20719 line: 0,
20720 character: 7,
20721 },
20722 end: lsp::Position {
20723 line: 0,
20724 character: 10,
20725 },
20726 },
20727 new_text: "FooRenamed".to_string(),
20728 };
20729 Ok(Some(lsp::WorkspaceEdit::new(
20730 // Specify the same edit twice
20731 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20732 )))
20733 });
20734 let rename_task = cx
20735 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20736 .expect("Confirm rename was not started");
20737 rename_handler.next().await.unwrap();
20738 rename_task.await.expect("Confirm rename failed");
20739 cx.run_until_parked();
20740
20741 // Despite two edits, only one is actually applied as those are identical
20742 cx.assert_editor_state(indoc! {"
20743 struct FooRenamedˇ {}
20744 "});
20745}
20746
20747#[gpui::test]
20748async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20749 init_test(cx, |_| {});
20750 // These capabilities indicate that the server does not support prepare rename.
20751 let capabilities = lsp::ServerCapabilities {
20752 rename_provider: Some(lsp::OneOf::Left(true)),
20753 ..Default::default()
20754 };
20755 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20756
20757 cx.set_state(indoc! {"
20758 struct Fˇoo {}
20759 "});
20760
20761 cx.update_editor(|editor, _window, cx| {
20762 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20763 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20764 editor.highlight_background::<DocumentHighlightRead>(
20765 &[highlight_range],
20766 |theme| theme.colors().editor_document_highlight_read_background,
20767 cx,
20768 );
20769 });
20770
20771 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20772 .expect("Prepare rename was not started")
20773 .await
20774 .expect("Prepare rename failed");
20775
20776 let mut rename_handler =
20777 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20778 let edit = lsp::TextEdit {
20779 range: lsp::Range {
20780 start: lsp::Position {
20781 line: 0,
20782 character: 7,
20783 },
20784 end: lsp::Position {
20785 line: 0,
20786 character: 10,
20787 },
20788 },
20789 new_text: "FooRenamed".to_string(),
20790 };
20791 Ok(Some(lsp::WorkspaceEdit::new(
20792 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20793 )))
20794 });
20795 let rename_task = cx
20796 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20797 .expect("Confirm rename was not started");
20798 rename_handler.next().await.unwrap();
20799 rename_task.await.expect("Confirm rename failed");
20800 cx.run_until_parked();
20801
20802 // Correct range is renamed, as `surrounding_word` is used to find it.
20803 cx.assert_editor_state(indoc! {"
20804 struct FooRenamedˇ {}
20805 "});
20806}
20807
20808#[gpui::test]
20809async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20810 init_test(cx, |_| {});
20811 let mut cx = EditorTestContext::new(cx).await;
20812
20813 let language = Arc::new(
20814 Language::new(
20815 LanguageConfig::default(),
20816 Some(tree_sitter_html::LANGUAGE.into()),
20817 )
20818 .with_brackets_query(
20819 r#"
20820 ("<" @open "/>" @close)
20821 ("</" @open ">" @close)
20822 ("<" @open ">" @close)
20823 ("\"" @open "\"" @close)
20824 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20825 "#,
20826 )
20827 .unwrap(),
20828 );
20829 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20830
20831 cx.set_state(indoc! {"
20832 <span>ˇ</span>
20833 "});
20834 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20835 cx.assert_editor_state(indoc! {"
20836 <span>
20837 ˇ
20838 </span>
20839 "});
20840
20841 cx.set_state(indoc! {"
20842 <span><span></span>ˇ</span>
20843 "});
20844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20845 cx.assert_editor_state(indoc! {"
20846 <span><span></span>
20847 ˇ</span>
20848 "});
20849
20850 cx.set_state(indoc! {"
20851 <span>ˇ
20852 </span>
20853 "});
20854 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20855 cx.assert_editor_state(indoc! {"
20856 <span>
20857 ˇ
20858 </span>
20859 "});
20860}
20861
20862#[gpui::test(iterations = 10)]
20863async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
20864 init_test(cx, |_| {});
20865
20866 let fs = FakeFs::new(cx.executor());
20867 fs.insert_tree(
20868 path!("/dir"),
20869 json!({
20870 "a.ts": "a",
20871 }),
20872 )
20873 .await;
20874
20875 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
20876 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20877 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20878
20879 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20880 language_registry.add(Arc::new(Language::new(
20881 LanguageConfig {
20882 name: "TypeScript".into(),
20883 matcher: LanguageMatcher {
20884 path_suffixes: vec!["ts".to_string()],
20885 ..Default::default()
20886 },
20887 ..Default::default()
20888 },
20889 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
20890 )));
20891 let mut fake_language_servers = language_registry.register_fake_lsp(
20892 "TypeScript",
20893 FakeLspAdapter {
20894 capabilities: lsp::ServerCapabilities {
20895 code_lens_provider: Some(lsp::CodeLensOptions {
20896 resolve_provider: Some(true),
20897 }),
20898 execute_command_provider: Some(lsp::ExecuteCommandOptions {
20899 commands: vec!["_the/command".to_string()],
20900 ..lsp::ExecuteCommandOptions::default()
20901 }),
20902 ..lsp::ServerCapabilities::default()
20903 },
20904 ..FakeLspAdapter::default()
20905 },
20906 );
20907
20908 let (buffer, _handle) = project
20909 .update(cx, |p, cx| {
20910 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
20911 })
20912 .await
20913 .unwrap();
20914 cx.executor().run_until_parked();
20915
20916 let fake_server = fake_language_servers.next().await.unwrap();
20917
20918 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
20919 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
20920 drop(buffer_snapshot);
20921 let actions = cx
20922 .update_window(*workspace, |_, window, cx| {
20923 project.code_actions(&buffer, anchor..anchor, window, cx)
20924 })
20925 .unwrap();
20926
20927 fake_server
20928 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
20929 Ok(Some(vec![
20930 lsp::CodeLens {
20931 range: lsp::Range::default(),
20932 command: Some(lsp::Command {
20933 title: "Code lens command".to_owned(),
20934 command: "_the/command".to_owned(),
20935 arguments: None,
20936 }),
20937 data: None,
20938 },
20939 lsp::CodeLens {
20940 range: lsp::Range::default(),
20941 command: Some(lsp::Command {
20942 title: "Command not in capabilities".to_owned(),
20943 command: "not in capabilities".to_owned(),
20944 arguments: None,
20945 }),
20946 data: None,
20947 },
20948 lsp::CodeLens {
20949 range: lsp::Range {
20950 start: lsp::Position {
20951 line: 1,
20952 character: 1,
20953 },
20954 end: lsp::Position {
20955 line: 1,
20956 character: 1,
20957 },
20958 },
20959 command: Some(lsp::Command {
20960 title: "Command not in range".to_owned(),
20961 command: "_the/command".to_owned(),
20962 arguments: None,
20963 }),
20964 data: None,
20965 },
20966 ]))
20967 })
20968 .next()
20969 .await;
20970
20971 let actions = actions.await.unwrap();
20972 assert_eq!(
20973 actions.len(),
20974 1,
20975 "Should have only one valid action for the 0..0 range"
20976 );
20977 let action = actions[0].clone();
20978 let apply = project.update(cx, |project, cx| {
20979 project.apply_code_action(buffer.clone(), action, true, cx)
20980 });
20981
20982 // Resolving the code action does not populate its edits. In absence of
20983 // edits, we must execute the given command.
20984 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
20985 |mut lens, _| async move {
20986 let lens_command = lens.command.as_mut().expect("should have a command");
20987 assert_eq!(lens_command.title, "Code lens command");
20988 lens_command.arguments = Some(vec![json!("the-argument")]);
20989 Ok(lens)
20990 },
20991 );
20992
20993 // While executing the command, the language server sends the editor
20994 // a `workspaceEdit` request.
20995 fake_server
20996 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
20997 let fake = fake_server.clone();
20998 move |params, _| {
20999 assert_eq!(params.command, "_the/command");
21000 let fake = fake.clone();
21001 async move {
21002 fake.server
21003 .request::<lsp::request::ApplyWorkspaceEdit>(
21004 lsp::ApplyWorkspaceEditParams {
21005 label: None,
21006 edit: lsp::WorkspaceEdit {
21007 changes: Some(
21008 [(
21009 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21010 vec![lsp::TextEdit {
21011 range: lsp::Range::new(
21012 lsp::Position::new(0, 0),
21013 lsp::Position::new(0, 0),
21014 ),
21015 new_text: "X".into(),
21016 }],
21017 )]
21018 .into_iter()
21019 .collect(),
21020 ),
21021 ..Default::default()
21022 },
21023 },
21024 )
21025 .await
21026 .into_response()
21027 .unwrap();
21028 Ok(Some(json!(null)))
21029 }
21030 }
21031 })
21032 .next()
21033 .await;
21034
21035 // Applying the code lens command returns a project transaction containing the edits
21036 // sent by the language server in its `workspaceEdit` request.
21037 let transaction = apply.await.unwrap();
21038 assert!(transaction.0.contains_key(&buffer));
21039 buffer.update(cx, |buffer, cx| {
21040 assert_eq!(buffer.text(), "Xa");
21041 buffer.undo(cx);
21042 assert_eq!(buffer.text(), "a");
21043 });
21044}
21045
21046#[gpui::test]
21047async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21048 init_test(cx, |_| {});
21049
21050 let fs = FakeFs::new(cx.executor());
21051 let main_text = r#"fn main() {
21052println!("1");
21053println!("2");
21054println!("3");
21055println!("4");
21056println!("5");
21057}"#;
21058 let lib_text = "mod foo {}";
21059 fs.insert_tree(
21060 path!("/a"),
21061 json!({
21062 "lib.rs": lib_text,
21063 "main.rs": main_text,
21064 }),
21065 )
21066 .await;
21067
21068 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21069 let (workspace, cx) =
21070 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21071 let worktree_id = workspace.update(cx, |workspace, cx| {
21072 workspace.project().update(cx, |project, cx| {
21073 project.worktrees(cx).next().unwrap().read(cx).id()
21074 })
21075 });
21076
21077 let expected_ranges = vec![
21078 Point::new(0, 0)..Point::new(0, 0),
21079 Point::new(1, 0)..Point::new(1, 1),
21080 Point::new(2, 0)..Point::new(2, 2),
21081 Point::new(3, 0)..Point::new(3, 3),
21082 ];
21083
21084 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21085 let editor_1 = workspace
21086 .update_in(cx, |workspace, window, cx| {
21087 workspace.open_path(
21088 (worktree_id, "main.rs"),
21089 Some(pane_1.downgrade()),
21090 true,
21091 window,
21092 cx,
21093 )
21094 })
21095 .unwrap()
21096 .await
21097 .downcast::<Editor>()
21098 .unwrap();
21099 pane_1.update(cx, |pane, cx| {
21100 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21101 open_editor.update(cx, |editor, cx| {
21102 assert_eq!(
21103 editor.display_text(cx),
21104 main_text,
21105 "Original main.rs text on initial open",
21106 );
21107 assert_eq!(
21108 editor
21109 .selections
21110 .all::<Point>(cx)
21111 .into_iter()
21112 .map(|s| s.range())
21113 .collect::<Vec<_>>(),
21114 vec![Point::zero()..Point::zero()],
21115 "Default selections on initial open",
21116 );
21117 })
21118 });
21119 editor_1.update_in(cx, |editor, window, cx| {
21120 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21121 s.select_ranges(expected_ranges.clone());
21122 });
21123 });
21124
21125 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21126 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21127 });
21128 let editor_2 = workspace
21129 .update_in(cx, |workspace, window, cx| {
21130 workspace.open_path(
21131 (worktree_id, "main.rs"),
21132 Some(pane_2.downgrade()),
21133 true,
21134 window,
21135 cx,
21136 )
21137 })
21138 .unwrap()
21139 .await
21140 .downcast::<Editor>()
21141 .unwrap();
21142 pane_2.update(cx, |pane, cx| {
21143 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21144 open_editor.update(cx, |editor, cx| {
21145 assert_eq!(
21146 editor.display_text(cx),
21147 main_text,
21148 "Original main.rs text on initial open in another panel",
21149 );
21150 assert_eq!(
21151 editor
21152 .selections
21153 .all::<Point>(cx)
21154 .into_iter()
21155 .map(|s| s.range())
21156 .collect::<Vec<_>>(),
21157 vec![Point::zero()..Point::zero()],
21158 "Default selections on initial open in another panel",
21159 );
21160 })
21161 });
21162
21163 editor_2.update_in(cx, |editor, window, cx| {
21164 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21165 });
21166
21167 let _other_editor_1 = workspace
21168 .update_in(cx, |workspace, window, cx| {
21169 workspace.open_path(
21170 (worktree_id, "lib.rs"),
21171 Some(pane_1.downgrade()),
21172 true,
21173 window,
21174 cx,
21175 )
21176 })
21177 .unwrap()
21178 .await
21179 .downcast::<Editor>()
21180 .unwrap();
21181 pane_1
21182 .update_in(cx, |pane, window, cx| {
21183 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21184 })
21185 .await
21186 .unwrap();
21187 drop(editor_1);
21188 pane_1.update(cx, |pane, cx| {
21189 pane.active_item()
21190 .unwrap()
21191 .downcast::<Editor>()
21192 .unwrap()
21193 .update(cx, |editor, cx| {
21194 assert_eq!(
21195 editor.display_text(cx),
21196 lib_text,
21197 "Other file should be open and active",
21198 );
21199 });
21200 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21201 });
21202
21203 let _other_editor_2 = workspace
21204 .update_in(cx, |workspace, window, cx| {
21205 workspace.open_path(
21206 (worktree_id, "lib.rs"),
21207 Some(pane_2.downgrade()),
21208 true,
21209 window,
21210 cx,
21211 )
21212 })
21213 .unwrap()
21214 .await
21215 .downcast::<Editor>()
21216 .unwrap();
21217 pane_2
21218 .update_in(cx, |pane, window, cx| {
21219 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21220 })
21221 .await
21222 .unwrap();
21223 drop(editor_2);
21224 pane_2.update(cx, |pane, cx| {
21225 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21226 open_editor.update(cx, |editor, cx| {
21227 assert_eq!(
21228 editor.display_text(cx),
21229 lib_text,
21230 "Other file should be open and active in another panel too",
21231 );
21232 });
21233 assert_eq!(
21234 pane.items().count(),
21235 1,
21236 "No other editors should be open in another pane",
21237 );
21238 });
21239
21240 let _editor_1_reopened = workspace
21241 .update_in(cx, |workspace, window, cx| {
21242 workspace.open_path(
21243 (worktree_id, "main.rs"),
21244 Some(pane_1.downgrade()),
21245 true,
21246 window,
21247 cx,
21248 )
21249 })
21250 .unwrap()
21251 .await
21252 .downcast::<Editor>()
21253 .unwrap();
21254 let _editor_2_reopened = workspace
21255 .update_in(cx, |workspace, window, cx| {
21256 workspace.open_path(
21257 (worktree_id, "main.rs"),
21258 Some(pane_2.downgrade()),
21259 true,
21260 window,
21261 cx,
21262 )
21263 })
21264 .unwrap()
21265 .await
21266 .downcast::<Editor>()
21267 .unwrap();
21268 pane_1.update(cx, |pane, cx| {
21269 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21270 open_editor.update(cx, |editor, cx| {
21271 assert_eq!(
21272 editor.display_text(cx),
21273 main_text,
21274 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21275 );
21276 assert_eq!(
21277 editor
21278 .selections
21279 .all::<Point>(cx)
21280 .into_iter()
21281 .map(|s| s.range())
21282 .collect::<Vec<_>>(),
21283 expected_ranges,
21284 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21285 );
21286 })
21287 });
21288 pane_2.update(cx, |pane, cx| {
21289 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21290 open_editor.update(cx, |editor, cx| {
21291 assert_eq!(
21292 editor.display_text(cx),
21293 r#"fn main() {
21294⋯rintln!("1");
21295⋯intln!("2");
21296⋯ntln!("3");
21297println!("4");
21298println!("5");
21299}"#,
21300 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21301 );
21302 assert_eq!(
21303 editor
21304 .selections
21305 .all::<Point>(cx)
21306 .into_iter()
21307 .map(|s| s.range())
21308 .collect::<Vec<_>>(),
21309 vec![Point::zero()..Point::zero()],
21310 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21311 );
21312 })
21313 });
21314}
21315
21316#[gpui::test]
21317async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21318 init_test(cx, |_| {});
21319
21320 let fs = FakeFs::new(cx.executor());
21321 let main_text = r#"fn main() {
21322println!("1");
21323println!("2");
21324println!("3");
21325println!("4");
21326println!("5");
21327}"#;
21328 let lib_text = "mod foo {}";
21329 fs.insert_tree(
21330 path!("/a"),
21331 json!({
21332 "lib.rs": lib_text,
21333 "main.rs": main_text,
21334 }),
21335 )
21336 .await;
21337
21338 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21339 let (workspace, cx) =
21340 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21341 let worktree_id = workspace.update(cx, |workspace, cx| {
21342 workspace.project().update(cx, |project, cx| {
21343 project.worktrees(cx).next().unwrap().read(cx).id()
21344 })
21345 });
21346
21347 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21348 let editor = workspace
21349 .update_in(cx, |workspace, window, cx| {
21350 workspace.open_path(
21351 (worktree_id, "main.rs"),
21352 Some(pane.downgrade()),
21353 true,
21354 window,
21355 cx,
21356 )
21357 })
21358 .unwrap()
21359 .await
21360 .downcast::<Editor>()
21361 .unwrap();
21362 pane.update(cx, |pane, cx| {
21363 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21364 open_editor.update(cx, |editor, cx| {
21365 assert_eq!(
21366 editor.display_text(cx),
21367 main_text,
21368 "Original main.rs text on initial open",
21369 );
21370 })
21371 });
21372 editor.update_in(cx, |editor, window, cx| {
21373 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21374 });
21375
21376 cx.update_global(|store: &mut SettingsStore, cx| {
21377 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21378 s.restore_on_file_reopen = Some(false);
21379 });
21380 });
21381 editor.update_in(cx, |editor, window, cx| {
21382 editor.fold_ranges(
21383 vec![
21384 Point::new(1, 0)..Point::new(1, 1),
21385 Point::new(2, 0)..Point::new(2, 2),
21386 Point::new(3, 0)..Point::new(3, 3),
21387 ],
21388 false,
21389 window,
21390 cx,
21391 );
21392 });
21393 pane.update_in(cx, |pane, window, cx| {
21394 pane.close_all_items(&CloseAllItems::default(), window, cx)
21395 })
21396 .await
21397 .unwrap();
21398 pane.update(cx, |pane, _| {
21399 assert!(pane.active_item().is_none());
21400 });
21401 cx.update_global(|store: &mut SettingsStore, cx| {
21402 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21403 s.restore_on_file_reopen = Some(true);
21404 });
21405 });
21406
21407 let _editor_reopened = workspace
21408 .update_in(cx, |workspace, window, cx| {
21409 workspace.open_path(
21410 (worktree_id, "main.rs"),
21411 Some(pane.downgrade()),
21412 true,
21413 window,
21414 cx,
21415 )
21416 })
21417 .unwrap()
21418 .await
21419 .downcast::<Editor>()
21420 .unwrap();
21421 pane.update(cx, |pane, cx| {
21422 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21423 open_editor.update(cx, |editor, cx| {
21424 assert_eq!(
21425 editor.display_text(cx),
21426 main_text,
21427 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21428 );
21429 })
21430 });
21431}
21432
21433#[gpui::test]
21434async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21435 struct EmptyModalView {
21436 focus_handle: gpui::FocusHandle,
21437 }
21438 impl EventEmitter<DismissEvent> for EmptyModalView {}
21439 impl Render for EmptyModalView {
21440 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21441 div()
21442 }
21443 }
21444 impl Focusable for EmptyModalView {
21445 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21446 self.focus_handle.clone()
21447 }
21448 }
21449 impl workspace::ModalView for EmptyModalView {}
21450 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21451 EmptyModalView {
21452 focus_handle: cx.focus_handle(),
21453 }
21454 }
21455
21456 init_test(cx, |_| {});
21457
21458 let fs = FakeFs::new(cx.executor());
21459 let project = Project::test(fs, [], cx).await;
21460 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21461 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21462 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21463 let editor = cx.new_window_entity(|window, cx| {
21464 Editor::new(
21465 EditorMode::full(),
21466 buffer,
21467 Some(project.clone()),
21468 window,
21469 cx,
21470 )
21471 });
21472 workspace
21473 .update(cx, |workspace, window, cx| {
21474 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21475 })
21476 .unwrap();
21477 editor.update_in(cx, |editor, window, cx| {
21478 editor.open_context_menu(&OpenContextMenu, window, cx);
21479 assert!(editor.mouse_context_menu.is_some());
21480 });
21481 workspace
21482 .update(cx, |workspace, window, cx| {
21483 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21484 })
21485 .unwrap();
21486 cx.read(|cx| {
21487 assert!(editor.read(cx).mouse_context_menu.is_none());
21488 });
21489}
21490
21491#[gpui::test]
21492async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21493 init_test(cx, |_| {});
21494
21495 let fs = FakeFs::new(cx.executor());
21496 fs.insert_file(path!("/file.html"), Default::default())
21497 .await;
21498
21499 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21500
21501 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21502 let html_language = Arc::new(Language::new(
21503 LanguageConfig {
21504 name: "HTML".into(),
21505 matcher: LanguageMatcher {
21506 path_suffixes: vec!["html".to_string()],
21507 ..LanguageMatcher::default()
21508 },
21509 brackets: BracketPairConfig {
21510 pairs: vec![BracketPair {
21511 start: "<".into(),
21512 end: ">".into(),
21513 close: true,
21514 ..Default::default()
21515 }],
21516 ..Default::default()
21517 },
21518 ..Default::default()
21519 },
21520 Some(tree_sitter_html::LANGUAGE.into()),
21521 ));
21522 language_registry.add(html_language);
21523 let mut fake_servers = language_registry.register_fake_lsp(
21524 "HTML",
21525 FakeLspAdapter {
21526 capabilities: lsp::ServerCapabilities {
21527 completion_provider: Some(lsp::CompletionOptions {
21528 resolve_provider: Some(true),
21529 ..Default::default()
21530 }),
21531 ..Default::default()
21532 },
21533 ..Default::default()
21534 },
21535 );
21536
21537 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21538 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21539
21540 let worktree_id = workspace
21541 .update(cx, |workspace, _window, cx| {
21542 workspace.project().update(cx, |project, cx| {
21543 project.worktrees(cx).next().unwrap().read(cx).id()
21544 })
21545 })
21546 .unwrap();
21547 project
21548 .update(cx, |project, cx| {
21549 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21550 })
21551 .await
21552 .unwrap();
21553 let editor = workspace
21554 .update(cx, |workspace, window, cx| {
21555 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21556 })
21557 .unwrap()
21558 .await
21559 .unwrap()
21560 .downcast::<Editor>()
21561 .unwrap();
21562
21563 let fake_server = fake_servers.next().await.unwrap();
21564 editor.update_in(cx, |editor, window, cx| {
21565 editor.set_text("<ad></ad>", window, cx);
21566 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21567 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21568 });
21569 let Some((buffer, _)) = editor
21570 .buffer
21571 .read(cx)
21572 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21573 else {
21574 panic!("Failed to get buffer for selection position");
21575 };
21576 let buffer = buffer.read(cx);
21577 let buffer_id = buffer.remote_id();
21578 let opening_range =
21579 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21580 let closing_range =
21581 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21582 let mut linked_ranges = HashMap::default();
21583 linked_ranges.insert(
21584 buffer_id,
21585 vec![(opening_range.clone(), vec![closing_range.clone()])],
21586 );
21587 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21588 });
21589 let mut completion_handle =
21590 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21591 Ok(Some(lsp::CompletionResponse::Array(vec![
21592 lsp::CompletionItem {
21593 label: "head".to_string(),
21594 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21595 lsp::InsertReplaceEdit {
21596 new_text: "head".to_string(),
21597 insert: lsp::Range::new(
21598 lsp::Position::new(0, 1),
21599 lsp::Position::new(0, 3),
21600 ),
21601 replace: lsp::Range::new(
21602 lsp::Position::new(0, 1),
21603 lsp::Position::new(0, 3),
21604 ),
21605 },
21606 )),
21607 ..Default::default()
21608 },
21609 ])))
21610 });
21611 editor.update_in(cx, |editor, window, cx| {
21612 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21613 });
21614 cx.run_until_parked();
21615 completion_handle.next().await.unwrap();
21616 editor.update(cx, |editor, _| {
21617 assert!(
21618 editor.context_menu_visible(),
21619 "Completion menu should be visible"
21620 );
21621 });
21622 editor.update_in(cx, |editor, window, cx| {
21623 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21624 });
21625 cx.executor().run_until_parked();
21626 editor.update(cx, |editor, cx| {
21627 assert_eq!(editor.text(cx), "<head></head>");
21628 });
21629}
21630
21631#[gpui::test]
21632async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21633 init_test(cx, |_| {});
21634
21635 let fs = FakeFs::new(cx.executor());
21636 fs.insert_tree(
21637 path!("/root"),
21638 json!({
21639 "a": {
21640 "main.rs": "fn main() {}",
21641 },
21642 "foo": {
21643 "bar": {
21644 "external_file.rs": "pub mod external {}",
21645 }
21646 }
21647 }),
21648 )
21649 .await;
21650
21651 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21652 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21653 language_registry.add(rust_lang());
21654 let _fake_servers = language_registry.register_fake_lsp(
21655 "Rust",
21656 FakeLspAdapter {
21657 ..FakeLspAdapter::default()
21658 },
21659 );
21660 let (workspace, cx) =
21661 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21662 let worktree_id = workspace.update(cx, |workspace, cx| {
21663 workspace.project().update(cx, |project, cx| {
21664 project.worktrees(cx).next().unwrap().read(cx).id()
21665 })
21666 });
21667
21668 let assert_language_servers_count =
21669 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21670 project.update(cx, |project, cx| {
21671 let current = project
21672 .lsp_store()
21673 .read(cx)
21674 .as_local()
21675 .unwrap()
21676 .language_servers
21677 .len();
21678 assert_eq!(expected, current, "{context}");
21679 });
21680 };
21681
21682 assert_language_servers_count(
21683 0,
21684 "No servers should be running before any file is open",
21685 cx,
21686 );
21687 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21688 let main_editor = workspace
21689 .update_in(cx, |workspace, window, cx| {
21690 workspace.open_path(
21691 (worktree_id, "main.rs"),
21692 Some(pane.downgrade()),
21693 true,
21694 window,
21695 cx,
21696 )
21697 })
21698 .unwrap()
21699 .await
21700 .downcast::<Editor>()
21701 .unwrap();
21702 pane.update(cx, |pane, cx| {
21703 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21704 open_editor.update(cx, |editor, cx| {
21705 assert_eq!(
21706 editor.display_text(cx),
21707 "fn main() {}",
21708 "Original main.rs text on initial open",
21709 );
21710 });
21711 assert_eq!(open_editor, main_editor);
21712 });
21713 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21714
21715 let external_editor = workspace
21716 .update_in(cx, |workspace, window, cx| {
21717 workspace.open_abs_path(
21718 PathBuf::from("/root/foo/bar/external_file.rs"),
21719 OpenOptions::default(),
21720 window,
21721 cx,
21722 )
21723 })
21724 .await
21725 .expect("opening external file")
21726 .downcast::<Editor>()
21727 .expect("downcasted external file's open element to editor");
21728 pane.update(cx, |pane, cx| {
21729 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21730 open_editor.update(cx, |editor, cx| {
21731 assert_eq!(
21732 editor.display_text(cx),
21733 "pub mod external {}",
21734 "External file is open now",
21735 );
21736 });
21737 assert_eq!(open_editor, external_editor);
21738 });
21739 assert_language_servers_count(
21740 1,
21741 "Second, external, *.rs file should join the existing server",
21742 cx,
21743 );
21744
21745 pane.update_in(cx, |pane, window, cx| {
21746 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21747 })
21748 .await
21749 .unwrap();
21750 pane.update_in(cx, |pane, window, cx| {
21751 pane.navigate_backward(window, cx);
21752 });
21753 cx.run_until_parked();
21754 pane.update(cx, |pane, cx| {
21755 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21756 open_editor.update(cx, |editor, cx| {
21757 assert_eq!(
21758 editor.display_text(cx),
21759 "pub mod external {}",
21760 "External file is open now",
21761 );
21762 });
21763 });
21764 assert_language_servers_count(
21765 1,
21766 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21767 cx,
21768 );
21769
21770 cx.update(|_, cx| {
21771 workspace::reload(&workspace::Reload::default(), cx);
21772 });
21773 assert_language_servers_count(
21774 1,
21775 "After reloading the worktree with local and external files opened, only one project should be started",
21776 cx,
21777 );
21778}
21779
21780#[gpui::test]
21781async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21782 init_test(cx, |_| {});
21783
21784 let mut cx = EditorTestContext::new(cx).await;
21785 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21786 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21787
21788 // test cursor move to start of each line on tab
21789 // for `if`, `elif`, `else`, `while`, `with` and `for`
21790 cx.set_state(indoc! {"
21791 def main():
21792 ˇ for item in items:
21793 ˇ while item.active:
21794 ˇ if item.value > 10:
21795 ˇ continue
21796 ˇ elif item.value < 0:
21797 ˇ break
21798 ˇ else:
21799 ˇ with item.context() as ctx:
21800 ˇ yield count
21801 ˇ else:
21802 ˇ log('while else')
21803 ˇ else:
21804 ˇ log('for else')
21805 "});
21806 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21807 cx.assert_editor_state(indoc! {"
21808 def main():
21809 ˇfor item in items:
21810 ˇwhile item.active:
21811 ˇif item.value > 10:
21812 ˇcontinue
21813 ˇelif item.value < 0:
21814 ˇbreak
21815 ˇelse:
21816 ˇwith item.context() as ctx:
21817 ˇyield count
21818 ˇelse:
21819 ˇlog('while else')
21820 ˇelse:
21821 ˇlog('for else')
21822 "});
21823 // test relative indent is preserved when tab
21824 // for `if`, `elif`, `else`, `while`, `with` and `for`
21825 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21826 cx.assert_editor_state(indoc! {"
21827 def main():
21828 ˇfor item in items:
21829 ˇwhile item.active:
21830 ˇif item.value > 10:
21831 ˇcontinue
21832 ˇelif item.value < 0:
21833 ˇbreak
21834 ˇelse:
21835 ˇwith item.context() as ctx:
21836 ˇyield count
21837 ˇelse:
21838 ˇlog('while else')
21839 ˇelse:
21840 ˇlog('for else')
21841 "});
21842
21843 // test cursor move to start of each line on tab
21844 // for `try`, `except`, `else`, `finally`, `match` and `def`
21845 cx.set_state(indoc! {"
21846 def main():
21847 ˇ try:
21848 ˇ fetch()
21849 ˇ except ValueError:
21850 ˇ handle_error()
21851 ˇ else:
21852 ˇ match value:
21853 ˇ case _:
21854 ˇ finally:
21855 ˇ def status():
21856 ˇ return 0
21857 "});
21858 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21859 cx.assert_editor_state(indoc! {"
21860 def main():
21861 ˇtry:
21862 ˇfetch()
21863 ˇexcept ValueError:
21864 ˇhandle_error()
21865 ˇelse:
21866 ˇmatch value:
21867 ˇcase _:
21868 ˇfinally:
21869 ˇdef status():
21870 ˇreturn 0
21871 "});
21872 // test relative indent is preserved when tab
21873 // for `try`, `except`, `else`, `finally`, `match` and `def`
21874 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21875 cx.assert_editor_state(indoc! {"
21876 def main():
21877 ˇtry:
21878 ˇfetch()
21879 ˇexcept ValueError:
21880 ˇhandle_error()
21881 ˇelse:
21882 ˇmatch value:
21883 ˇcase _:
21884 ˇfinally:
21885 ˇdef status():
21886 ˇreturn 0
21887 "});
21888}
21889
21890#[gpui::test]
21891async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
21892 init_test(cx, |_| {});
21893
21894 let mut cx = EditorTestContext::new(cx).await;
21895 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21896 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21897
21898 // test `else` auto outdents when typed inside `if` block
21899 cx.set_state(indoc! {"
21900 def main():
21901 if i == 2:
21902 return
21903 ˇ
21904 "});
21905 cx.update_editor(|editor, window, cx| {
21906 editor.handle_input("else:", window, cx);
21907 });
21908 cx.assert_editor_state(indoc! {"
21909 def main():
21910 if i == 2:
21911 return
21912 else:ˇ
21913 "});
21914
21915 // test `except` auto outdents when typed inside `try` block
21916 cx.set_state(indoc! {"
21917 def main():
21918 try:
21919 i = 2
21920 ˇ
21921 "});
21922 cx.update_editor(|editor, window, cx| {
21923 editor.handle_input("except:", window, cx);
21924 });
21925 cx.assert_editor_state(indoc! {"
21926 def main():
21927 try:
21928 i = 2
21929 except:ˇ
21930 "});
21931
21932 // test `else` auto outdents when typed inside `except` block
21933 cx.set_state(indoc! {"
21934 def main():
21935 try:
21936 i = 2
21937 except:
21938 j = 2
21939 ˇ
21940 "});
21941 cx.update_editor(|editor, window, cx| {
21942 editor.handle_input("else:", window, cx);
21943 });
21944 cx.assert_editor_state(indoc! {"
21945 def main():
21946 try:
21947 i = 2
21948 except:
21949 j = 2
21950 else:ˇ
21951 "});
21952
21953 // test `finally` auto outdents when typed inside `else` block
21954 cx.set_state(indoc! {"
21955 def main():
21956 try:
21957 i = 2
21958 except:
21959 j = 2
21960 else:
21961 k = 2
21962 ˇ
21963 "});
21964 cx.update_editor(|editor, window, cx| {
21965 editor.handle_input("finally:", window, cx);
21966 });
21967 cx.assert_editor_state(indoc! {"
21968 def main():
21969 try:
21970 i = 2
21971 except:
21972 j = 2
21973 else:
21974 k = 2
21975 finally:ˇ
21976 "});
21977
21978 // test `else` does not outdents when typed inside `except` block right after for block
21979 cx.set_state(indoc! {"
21980 def main():
21981 try:
21982 i = 2
21983 except:
21984 for i in range(n):
21985 pass
21986 ˇ
21987 "});
21988 cx.update_editor(|editor, window, cx| {
21989 editor.handle_input("else:", window, cx);
21990 });
21991 cx.assert_editor_state(indoc! {"
21992 def main():
21993 try:
21994 i = 2
21995 except:
21996 for i in range(n):
21997 pass
21998 else:ˇ
21999 "});
22000
22001 // test `finally` auto outdents when typed inside `else` block right after for block
22002 cx.set_state(indoc! {"
22003 def main():
22004 try:
22005 i = 2
22006 except:
22007 j = 2
22008 else:
22009 for i in range(n):
22010 pass
22011 ˇ
22012 "});
22013 cx.update_editor(|editor, window, cx| {
22014 editor.handle_input("finally:", window, cx);
22015 });
22016 cx.assert_editor_state(indoc! {"
22017 def main():
22018 try:
22019 i = 2
22020 except:
22021 j = 2
22022 else:
22023 for i in range(n):
22024 pass
22025 finally:ˇ
22026 "});
22027
22028 // test `except` outdents to inner "try" block
22029 cx.set_state(indoc! {"
22030 def main():
22031 try:
22032 i = 2
22033 if i == 2:
22034 try:
22035 i = 3
22036 ˇ
22037 "});
22038 cx.update_editor(|editor, window, cx| {
22039 editor.handle_input("except:", window, cx);
22040 });
22041 cx.assert_editor_state(indoc! {"
22042 def main():
22043 try:
22044 i = 2
22045 if i == 2:
22046 try:
22047 i = 3
22048 except:ˇ
22049 "});
22050
22051 // test `except` outdents to outer "try" block
22052 cx.set_state(indoc! {"
22053 def main():
22054 try:
22055 i = 2
22056 if i == 2:
22057 try:
22058 i = 3
22059 ˇ
22060 "});
22061 cx.update_editor(|editor, window, cx| {
22062 editor.handle_input("except:", window, cx);
22063 });
22064 cx.assert_editor_state(indoc! {"
22065 def main():
22066 try:
22067 i = 2
22068 if i == 2:
22069 try:
22070 i = 3
22071 except:ˇ
22072 "});
22073
22074 // test `else` stays at correct indent when typed after `for` block
22075 cx.set_state(indoc! {"
22076 def main():
22077 for i in range(10):
22078 if i == 3:
22079 break
22080 ˇ
22081 "});
22082 cx.update_editor(|editor, window, cx| {
22083 editor.handle_input("else:", window, cx);
22084 });
22085 cx.assert_editor_state(indoc! {"
22086 def main():
22087 for i in range(10):
22088 if i == 3:
22089 break
22090 else:ˇ
22091 "});
22092
22093 // test does not outdent on typing after line with square brackets
22094 cx.set_state(indoc! {"
22095 def f() -> list[str]:
22096 ˇ
22097 "});
22098 cx.update_editor(|editor, window, cx| {
22099 editor.handle_input("a", window, cx);
22100 });
22101 cx.assert_editor_state(indoc! {"
22102 def f() -> list[str]:
22103 aˇ
22104 "});
22105}
22106
22107#[gpui::test]
22108async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22109 init_test(cx, |_| {});
22110 update_test_language_settings(cx, |settings| {
22111 settings.defaults.extend_comment_on_newline = Some(false);
22112 });
22113 let mut cx = EditorTestContext::new(cx).await;
22114 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22115 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22116
22117 // test correct indent after newline on comment
22118 cx.set_state(indoc! {"
22119 # COMMENT:ˇ
22120 "});
22121 cx.update_editor(|editor, window, cx| {
22122 editor.newline(&Newline, window, cx);
22123 });
22124 cx.assert_editor_state(indoc! {"
22125 # COMMENT:
22126 ˇ
22127 "});
22128
22129 // test correct indent after newline in brackets
22130 cx.set_state(indoc! {"
22131 {ˇ}
22132 "});
22133 cx.update_editor(|editor, window, cx| {
22134 editor.newline(&Newline, window, cx);
22135 });
22136 cx.run_until_parked();
22137 cx.assert_editor_state(indoc! {"
22138 {
22139 ˇ
22140 }
22141 "});
22142
22143 cx.set_state(indoc! {"
22144 (ˇ)
22145 "});
22146 cx.update_editor(|editor, window, cx| {
22147 editor.newline(&Newline, window, cx);
22148 });
22149 cx.run_until_parked();
22150 cx.assert_editor_state(indoc! {"
22151 (
22152 ˇ
22153 )
22154 "});
22155
22156 // do not indent after empty lists or dictionaries
22157 cx.set_state(indoc! {"
22158 a = []ˇ
22159 "});
22160 cx.update_editor(|editor, window, cx| {
22161 editor.newline(&Newline, window, cx);
22162 });
22163 cx.run_until_parked();
22164 cx.assert_editor_state(indoc! {"
22165 a = []
22166 ˇ
22167 "});
22168}
22169
22170fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22171 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22172 point..point
22173}
22174
22175#[track_caller]
22176fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22177 let (text, ranges) = marked_text_ranges(marked_text, true);
22178 assert_eq!(editor.text(cx), text);
22179 assert_eq!(
22180 editor.selections.ranges(cx),
22181 ranges,
22182 "Assert selections are {}",
22183 marked_text
22184 );
22185}
22186
22187pub fn handle_signature_help_request(
22188 cx: &mut EditorLspTestContext,
22189 mocked_response: lsp::SignatureHelp,
22190) -> impl Future<Output = ()> + use<> {
22191 let mut request =
22192 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22193 let mocked_response = mocked_response.clone();
22194 async move { Ok(Some(mocked_response)) }
22195 });
22196
22197 async move {
22198 request.next().await;
22199 }
22200}
22201
22202#[track_caller]
22203pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22204 cx.update_editor(|editor, _, _| {
22205 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22206 let entries = menu.entries.borrow();
22207 let entries = entries
22208 .iter()
22209 .map(|entry| entry.string.as_str())
22210 .collect::<Vec<_>>();
22211 assert_eq!(entries, expected);
22212 } else {
22213 panic!("Expected completions menu");
22214 }
22215 });
22216}
22217
22218/// Handle completion request passing a marked string specifying where the completion
22219/// should be triggered from using '|' character, what range should be replaced, and what completions
22220/// should be returned using '<' and '>' to delimit the range.
22221///
22222/// Also see `handle_completion_request_with_insert_and_replace`.
22223#[track_caller]
22224pub fn handle_completion_request(
22225 marked_string: &str,
22226 completions: Vec<&'static str>,
22227 is_incomplete: bool,
22228 counter: Arc<AtomicUsize>,
22229 cx: &mut EditorLspTestContext,
22230) -> impl Future<Output = ()> {
22231 let complete_from_marker: TextRangeMarker = '|'.into();
22232 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22233 let (_, mut marked_ranges) = marked_text_ranges_by(
22234 marked_string,
22235 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22236 );
22237
22238 let complete_from_position =
22239 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22240 let replace_range =
22241 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22242
22243 let mut request =
22244 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22245 let completions = completions.clone();
22246 counter.fetch_add(1, atomic::Ordering::Release);
22247 async move {
22248 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22249 assert_eq!(
22250 params.text_document_position.position,
22251 complete_from_position
22252 );
22253 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22254 is_incomplete: is_incomplete,
22255 item_defaults: None,
22256 items: completions
22257 .iter()
22258 .map(|completion_text| lsp::CompletionItem {
22259 label: completion_text.to_string(),
22260 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22261 range: replace_range,
22262 new_text: completion_text.to_string(),
22263 })),
22264 ..Default::default()
22265 })
22266 .collect(),
22267 })))
22268 }
22269 });
22270
22271 async move {
22272 request.next().await;
22273 }
22274}
22275
22276/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22277/// given instead, which also contains an `insert` range.
22278///
22279/// This function uses markers to define ranges:
22280/// - `|` marks the cursor position
22281/// - `<>` marks the replace range
22282/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22283pub fn handle_completion_request_with_insert_and_replace(
22284 cx: &mut EditorLspTestContext,
22285 marked_string: &str,
22286 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22287 counter: Arc<AtomicUsize>,
22288) -> impl Future<Output = ()> {
22289 let complete_from_marker: TextRangeMarker = '|'.into();
22290 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22291 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22292
22293 let (_, mut marked_ranges) = marked_text_ranges_by(
22294 marked_string,
22295 vec![
22296 complete_from_marker.clone(),
22297 replace_range_marker.clone(),
22298 insert_range_marker.clone(),
22299 ],
22300 );
22301
22302 let complete_from_position =
22303 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22304 let replace_range =
22305 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22306
22307 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22308 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22309 _ => lsp::Range {
22310 start: replace_range.start,
22311 end: complete_from_position,
22312 },
22313 };
22314
22315 let mut request =
22316 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22317 let completions = completions.clone();
22318 counter.fetch_add(1, atomic::Ordering::Release);
22319 async move {
22320 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22321 assert_eq!(
22322 params.text_document_position.position, complete_from_position,
22323 "marker `|` position doesn't match",
22324 );
22325 Ok(Some(lsp::CompletionResponse::Array(
22326 completions
22327 .iter()
22328 .map(|(label, new_text)| lsp::CompletionItem {
22329 label: label.to_string(),
22330 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22331 lsp::InsertReplaceEdit {
22332 insert: insert_range,
22333 replace: replace_range,
22334 new_text: new_text.to_string(),
22335 },
22336 )),
22337 ..Default::default()
22338 })
22339 .collect(),
22340 )))
22341 }
22342 });
22343
22344 async move {
22345 request.next().await;
22346 }
22347}
22348
22349fn handle_resolve_completion_request(
22350 cx: &mut EditorLspTestContext,
22351 edits: Option<Vec<(&'static str, &'static str)>>,
22352) -> impl Future<Output = ()> {
22353 let edits = edits.map(|edits| {
22354 edits
22355 .iter()
22356 .map(|(marked_string, new_text)| {
22357 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22358 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22359 lsp::TextEdit::new(replace_range, new_text.to_string())
22360 })
22361 .collect::<Vec<_>>()
22362 });
22363
22364 let mut request =
22365 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22366 let edits = edits.clone();
22367 async move {
22368 Ok(lsp::CompletionItem {
22369 additional_text_edits: edits,
22370 ..Default::default()
22371 })
22372 }
22373 });
22374
22375 async move {
22376 request.next().await;
22377 }
22378}
22379
22380pub(crate) fn update_test_language_settings(
22381 cx: &mut TestAppContext,
22382 f: impl Fn(&mut AllLanguageSettingsContent),
22383) {
22384 cx.update(|cx| {
22385 SettingsStore::update_global(cx, |store, cx| {
22386 store.update_user_settings::<AllLanguageSettings>(cx, f);
22387 });
22388 });
22389}
22390
22391pub(crate) fn update_test_project_settings(
22392 cx: &mut TestAppContext,
22393 f: impl Fn(&mut ProjectSettings),
22394) {
22395 cx.update(|cx| {
22396 SettingsStore::update_global(cx, |store, cx| {
22397 store.update_user_settings::<ProjectSettings>(cx, f);
22398 });
22399 });
22400}
22401
22402pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22403 cx.update(|cx| {
22404 assets::Assets.load_test_fonts(cx);
22405 let store = SettingsStore::test(cx);
22406 cx.set_global(store);
22407 theme::init(theme::LoadThemes::JustBase, cx);
22408 release_channel::init(SemanticVersion::default(), cx);
22409 client::init_settings(cx);
22410 language::init(cx);
22411 Project::init_settings(cx);
22412 workspace::init_settings(cx);
22413 crate::init(cx);
22414 });
22415
22416 update_test_language_settings(cx, f);
22417}
22418
22419#[track_caller]
22420fn assert_hunk_revert(
22421 not_reverted_text_with_selections: &str,
22422 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22423 expected_reverted_text_with_selections: &str,
22424 base_text: &str,
22425 cx: &mut EditorLspTestContext,
22426) {
22427 cx.set_state(not_reverted_text_with_selections);
22428 cx.set_head_text(base_text);
22429 cx.executor().run_until_parked();
22430
22431 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22432 let snapshot = editor.snapshot(window, cx);
22433 let reverted_hunk_statuses = snapshot
22434 .buffer_snapshot
22435 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22436 .map(|hunk| hunk.status().kind)
22437 .collect::<Vec<_>>();
22438
22439 editor.git_restore(&Default::default(), window, cx);
22440 reverted_hunk_statuses
22441 });
22442 cx.executor().run_until_parked();
22443 cx.assert_editor_state(expected_reverted_text_with_selections);
22444 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22445}
22446
22447#[gpui::test(iterations = 10)]
22448async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22449 init_test(cx, |_| {});
22450
22451 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22452 let counter = diagnostic_requests.clone();
22453
22454 let fs = FakeFs::new(cx.executor());
22455 fs.insert_tree(
22456 path!("/a"),
22457 json!({
22458 "first.rs": "fn main() { let a = 5; }",
22459 "second.rs": "// Test file",
22460 }),
22461 )
22462 .await;
22463
22464 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22465 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22466 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22467
22468 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22469 language_registry.add(rust_lang());
22470 let mut fake_servers = language_registry.register_fake_lsp(
22471 "Rust",
22472 FakeLspAdapter {
22473 capabilities: lsp::ServerCapabilities {
22474 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22475 lsp::DiagnosticOptions {
22476 identifier: None,
22477 inter_file_dependencies: true,
22478 workspace_diagnostics: true,
22479 work_done_progress_options: Default::default(),
22480 },
22481 )),
22482 ..Default::default()
22483 },
22484 ..Default::default()
22485 },
22486 );
22487
22488 let editor = workspace
22489 .update(cx, |workspace, window, cx| {
22490 workspace.open_abs_path(
22491 PathBuf::from(path!("/a/first.rs")),
22492 OpenOptions::default(),
22493 window,
22494 cx,
22495 )
22496 })
22497 .unwrap()
22498 .await
22499 .unwrap()
22500 .downcast::<Editor>()
22501 .unwrap();
22502 let fake_server = fake_servers.next().await.unwrap();
22503 let server_id = fake_server.server.server_id();
22504 let mut first_request = fake_server
22505 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22506 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22507 let result_id = Some(new_result_id.to_string());
22508 assert_eq!(
22509 params.text_document.uri,
22510 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22511 );
22512 async move {
22513 Ok(lsp::DocumentDiagnosticReportResult::Report(
22514 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22515 related_documents: None,
22516 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22517 items: Vec::new(),
22518 result_id,
22519 },
22520 }),
22521 ))
22522 }
22523 });
22524
22525 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22526 project.update(cx, |project, cx| {
22527 let buffer_id = editor
22528 .read(cx)
22529 .buffer()
22530 .read(cx)
22531 .as_singleton()
22532 .expect("created a singleton buffer")
22533 .read(cx)
22534 .remote_id();
22535 let buffer_result_id = project
22536 .lsp_store()
22537 .read(cx)
22538 .result_id(server_id, buffer_id, cx);
22539 assert_eq!(expected, buffer_result_id);
22540 });
22541 };
22542
22543 ensure_result_id(None, cx);
22544 cx.executor().advance_clock(Duration::from_millis(60));
22545 cx.executor().run_until_parked();
22546 assert_eq!(
22547 diagnostic_requests.load(atomic::Ordering::Acquire),
22548 1,
22549 "Opening file should trigger diagnostic request"
22550 );
22551 first_request
22552 .next()
22553 .await
22554 .expect("should have sent the first diagnostics pull request");
22555 ensure_result_id(Some("1".to_string()), cx);
22556
22557 // Editing should trigger diagnostics
22558 editor.update_in(cx, |editor, window, cx| {
22559 editor.handle_input("2", window, cx)
22560 });
22561 cx.executor().advance_clock(Duration::from_millis(60));
22562 cx.executor().run_until_parked();
22563 assert_eq!(
22564 diagnostic_requests.load(atomic::Ordering::Acquire),
22565 2,
22566 "Editing should trigger diagnostic request"
22567 );
22568 ensure_result_id(Some("2".to_string()), cx);
22569
22570 // Moving cursor should not trigger diagnostic request
22571 editor.update_in(cx, |editor, window, cx| {
22572 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22573 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22574 });
22575 });
22576 cx.executor().advance_clock(Duration::from_millis(60));
22577 cx.executor().run_until_parked();
22578 assert_eq!(
22579 diagnostic_requests.load(atomic::Ordering::Acquire),
22580 2,
22581 "Cursor movement should not trigger diagnostic request"
22582 );
22583 ensure_result_id(Some("2".to_string()), cx);
22584 // Multiple rapid edits should be debounced
22585 for _ in 0..5 {
22586 editor.update_in(cx, |editor, window, cx| {
22587 editor.handle_input("x", window, cx)
22588 });
22589 }
22590 cx.executor().advance_clock(Duration::from_millis(60));
22591 cx.executor().run_until_parked();
22592
22593 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22594 assert!(
22595 final_requests <= 4,
22596 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22597 );
22598 ensure_result_id(Some(final_requests.to_string()), cx);
22599}
22600
22601#[gpui::test]
22602async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22603 // Regression test for issue #11671
22604 // Previously, adding a cursor after moving multiple cursors would reset
22605 // the cursor count instead of adding to the existing cursors.
22606 init_test(cx, |_| {});
22607 let mut cx = EditorTestContext::new(cx).await;
22608
22609 // Create a simple buffer with cursor at start
22610 cx.set_state(indoc! {"
22611 ˇaaaa
22612 bbbb
22613 cccc
22614 dddd
22615 eeee
22616 ffff
22617 gggg
22618 hhhh"});
22619
22620 // Add 2 cursors below (so we have 3 total)
22621 cx.update_editor(|editor, window, cx| {
22622 editor.add_selection_below(&Default::default(), window, cx);
22623 editor.add_selection_below(&Default::default(), window, cx);
22624 });
22625
22626 // Verify we have 3 cursors
22627 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22628 assert_eq!(
22629 initial_count, 3,
22630 "Should have 3 cursors after adding 2 below"
22631 );
22632
22633 // Move down one line
22634 cx.update_editor(|editor, window, cx| {
22635 editor.move_down(&MoveDown, window, cx);
22636 });
22637
22638 // Add another cursor below
22639 cx.update_editor(|editor, window, cx| {
22640 editor.add_selection_below(&Default::default(), window, cx);
22641 });
22642
22643 // Should now have 4 cursors (3 original + 1 new)
22644 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22645 assert_eq!(
22646 final_count, 4,
22647 "Should have 4 cursors after moving and adding another"
22648 );
22649}
22650
22651#[gpui::test]
22652async fn test_mtime_and_document_colors(cx: &mut TestAppContext) {
22653 let expected_color = Rgba {
22654 r: 0.33,
22655 g: 0.33,
22656 b: 0.33,
22657 a: 0.33,
22658 };
22659
22660 init_test(cx, |_| {});
22661
22662 let fs = FakeFs::new(cx.executor());
22663 fs.insert_tree(
22664 path!("/a"),
22665 json!({
22666 "first.rs": "fn main() { let a = 5; }",
22667 }),
22668 )
22669 .await;
22670
22671 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22672 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22673 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22674
22675 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22676 language_registry.add(rust_lang());
22677 let mut fake_servers = language_registry.register_fake_lsp(
22678 "Rust",
22679 FakeLspAdapter {
22680 capabilities: lsp::ServerCapabilities {
22681 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22682 ..lsp::ServerCapabilities::default()
22683 },
22684 name: "rust-analyzer",
22685 ..FakeLspAdapter::default()
22686 },
22687 );
22688 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22689 "Rust",
22690 FakeLspAdapter {
22691 capabilities: lsp::ServerCapabilities {
22692 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22693 ..lsp::ServerCapabilities::default()
22694 },
22695 name: "not-rust-analyzer",
22696 ..FakeLspAdapter::default()
22697 },
22698 );
22699
22700 let editor = workspace
22701 .update(cx, |workspace, window, cx| {
22702 workspace.open_abs_path(
22703 PathBuf::from(path!("/a/first.rs")),
22704 OpenOptions::default(),
22705 window,
22706 cx,
22707 )
22708 })
22709 .unwrap()
22710 .await
22711 .unwrap()
22712 .downcast::<Editor>()
22713 .unwrap();
22714 let fake_language_server = fake_servers.next().await.unwrap();
22715 let fake_language_server_without_capabilities =
22716 fake_servers_without_capabilities.next().await.unwrap();
22717 let requests_made = Arc::new(AtomicUsize::new(0));
22718 let closure_requests_made = Arc::clone(&requests_made);
22719 let mut color_request_handle = fake_language_server
22720 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22721 let requests_made = Arc::clone(&closure_requests_made);
22722 async move {
22723 assert_eq!(
22724 params.text_document.uri,
22725 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22726 );
22727 requests_made.fetch_add(1, atomic::Ordering::Release);
22728 Ok(vec![
22729 lsp::ColorInformation {
22730 range: lsp::Range {
22731 start: lsp::Position {
22732 line: 0,
22733 character: 0,
22734 },
22735 end: lsp::Position {
22736 line: 0,
22737 character: 1,
22738 },
22739 },
22740 color: lsp::Color {
22741 red: 0.33,
22742 green: 0.33,
22743 blue: 0.33,
22744 alpha: 0.33,
22745 },
22746 },
22747 lsp::ColorInformation {
22748 range: lsp::Range {
22749 start: lsp::Position {
22750 line: 0,
22751 character: 0,
22752 },
22753 end: lsp::Position {
22754 line: 0,
22755 character: 1,
22756 },
22757 },
22758 color: lsp::Color {
22759 red: 0.33,
22760 green: 0.33,
22761 blue: 0.33,
22762 alpha: 0.33,
22763 },
22764 },
22765 ])
22766 }
22767 });
22768
22769 let _handle = fake_language_server_without_capabilities
22770 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
22771 panic!("Should not be called");
22772 });
22773 color_request_handle.next().await.unwrap();
22774 cx.run_until_parked();
22775 color_request_handle.next().await.unwrap();
22776 cx.run_until_parked();
22777 assert_eq!(
22778 3,
22779 requests_made.load(atomic::Ordering::Acquire),
22780 "Should query for colors once per editor open (1) and once after the language server startup (2)"
22781 );
22782
22783 cx.executor().advance_clock(Duration::from_millis(500));
22784 let save = editor.update_in(cx, |editor, window, cx| {
22785 assert_eq!(
22786 vec![expected_color],
22787 extract_color_inlays(editor, cx),
22788 "Should have an initial inlay"
22789 );
22790
22791 editor.move_to_end(&MoveToEnd, window, cx);
22792 editor.handle_input("dirty", window, cx);
22793 editor.save(
22794 SaveOptions {
22795 format: true,
22796 autosave: true,
22797 },
22798 project.clone(),
22799 window,
22800 cx,
22801 )
22802 });
22803 save.await.unwrap();
22804
22805 color_request_handle.next().await.unwrap();
22806 cx.run_until_parked();
22807 color_request_handle.next().await.unwrap();
22808 cx.run_until_parked();
22809 assert_eq!(
22810 5,
22811 requests_made.load(atomic::Ordering::Acquire),
22812 "Should query for colors once per save and once per formatting after save"
22813 );
22814
22815 drop(editor);
22816 let close = workspace
22817 .update(cx, |workspace, window, cx| {
22818 workspace.active_pane().update(cx, |pane, cx| {
22819 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22820 })
22821 })
22822 .unwrap();
22823 close.await.unwrap();
22824 assert_eq!(
22825 5,
22826 requests_made.load(atomic::Ordering::Acquire),
22827 "After saving and closing the editor, no extra requests should be made"
22828 );
22829
22830 workspace
22831 .update(cx, |workspace, window, cx| {
22832 workspace.active_pane().update(cx, |pane, cx| {
22833 pane.navigate_backward(window, cx);
22834 })
22835 })
22836 .unwrap();
22837 cx.executor().advance_clock(Duration::from_millis(100));
22838 color_request_handle.next().await.unwrap();
22839 cx.run_until_parked();
22840 assert_eq!(
22841 6,
22842 requests_made.load(atomic::Ordering::Acquire),
22843 "After navigating back to an editor and reopening it, another color request should be made"
22844 );
22845 let editor = workspace
22846 .update(cx, |workspace, _, cx| {
22847 workspace
22848 .active_item(cx)
22849 .expect("Should have reopened the editor again after navigating back")
22850 .downcast::<Editor>()
22851 .expect("Should be an editor")
22852 })
22853 .unwrap();
22854 editor.update(cx, |editor, cx| {
22855 assert_eq!(
22856 vec![expected_color],
22857 extract_color_inlays(editor, cx),
22858 "Should have an initial inlay"
22859 );
22860 });
22861}
22862
22863#[gpui::test]
22864async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
22865 init_test(cx, |_| {});
22866 let (editor, cx) = cx.add_window_view(Editor::single_line);
22867 editor.update_in(cx, |editor, window, cx| {
22868 editor.set_text("oops\n\nwow\n", window, cx)
22869 });
22870 cx.run_until_parked();
22871 editor.update(cx, |editor, cx| {
22872 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
22873 });
22874 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
22875 cx.run_until_parked();
22876 editor.update(cx, |editor, cx| {
22877 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
22878 });
22879}
22880
22881#[track_caller]
22882fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
22883 editor
22884 .all_inlays(cx)
22885 .into_iter()
22886 .filter_map(|inlay| inlay.get_color())
22887 .map(Rgba::from)
22888 .collect()
22889}